Skip to content

Commit 2534767

Browse files
Merge remote-tracking branch 'repoconf-rust-public-lib-template/main'
2 parents dcacccc + 1b904be commit 2534767

File tree

7 files changed

+169
-37
lines changed

7 files changed

+169
-37
lines changed

.repoconf/data/init-removed-files

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
README.md

.repoconf/data/managed-files

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.github/workflows/ci.yml
2+
.gitignore
3+
commitlint.config.mjs
4+
deno.json
5+
lefthook.yml
6+
mise.toml
7+
README.ts
8+
rustfmt.toml
9+
CLAUDE.md

.repoconf/hooks/post-init.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env -S usage bash
2+
#USAGE flag "-n --name <name>" help="Package name (if this flag is not provided, then the package name is inferred from the directory name)"
3+
#USAGE arg "<dir>"
4+
5+
set -xeuo pipefail
6+
7+
dir=$(realpath "${usage_dir:?}")
8+
name_new_default="${usage_name:-$(basename "$dir")}"
9+
cargo_toml="$dir/Cargo.toml"
10+
#mise_toml="$dir/mise.toml"
11+
12+
read -r -p "Rust package name (default: $name_new_default): " name_new
13+
read -r -p "Rust package description: " description
14+
read -r -p "Rust package title (default: same as description): " title
15+
16+
if [[ -z $name_new ]]; then
17+
name_new=$name_new_default
18+
fi
19+
20+
if [[ -z $title ]]; then
21+
title=$description
22+
fi
23+
24+
(cd "$dir" && mise trust)
25+
(cd "$dir" && mise install)
26+
27+
name_old=$(taplo get -f "$cargo_toml" "package.name")
28+
name_old_snake_case=$(ccase --to snake "$name_old")
29+
name_new_snake_case=$(ccase --to snake "$name_new")
30+
repo_url=$(cd "$dir" && gh repo view --json url | jq -r .url)
31+
32+
tomli set -f "$cargo_toml" "package.name" "$name_new" | sponge "$cargo_toml"
33+
tomli set -f "$cargo_toml" "package.repository" "$repo_url" | sponge "$cargo_toml"
34+
tomli set -f "$cargo_toml" "package.homepage" "$repo_url" | sponge "$cargo_toml"
35+
tomli set -f "$cargo_toml" "package.description" "$description" | sponge "$cargo_toml"
36+
tomli set -f "$cargo_toml" "package.metadata.details.title" "$title" | sponge "$cargo_toml"
37+
tomli delete --if-exists -f "$cargo_toml" "package.metadata.details.readme.generate" | sponge "$cargo_toml"
38+
39+
while IFS= read -r file; do
40+
rm "$dir/$file"
41+
done < "$dir/.repoconf/data/init-removed-files"
42+
43+
# rg exits with status code = 1 if it doesn't find any files, so we need to disable & re-enable "set -e"
44+
set +e
45+
rg --files-with-matches "$name_old_snake_case" "$dir" | xargs gsed -i "s/\b$name_old_snake_case\b/$name_new_snake_case/g"
46+
set -e
47+
48+
(cd "$dir" && mise exec "npm:lefthook" -- lefthook install)
49+
50+
(cd "$dir" && mise run build)
51+
52+
(cd "$dir" && mise run test)
53+
54+
# remove .repoconf just before the final commit, in the same line
55+
(cd "$dir" && rm -r "$dir/.repoconf" && git add . && git commit -a -m "chore: update package details")

CLAUDE.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Guidelines
2+
3+
## General
4+
5+
* Read `Cargo.toml` before executing any tasks
6+
* Don't create git commits
7+
8+
## Commands
9+
10+
Always use `mise run ...` commands to run the tests / lints.
11+
12+
* Run tests: `mise run test` (use this instead of `cargo test`)
13+
* Run specific test: `mise run test <test_file_path>` (use this instead of `cargo test`)
14+
* Format code: `mise run fmt` (use this instead of `cargo fmt`)
15+
* Lint code: `mise run lint` (use this instead of `cargo clippy`)
16+
* Check types: `mise run check` (use this instead of `cargo check`)
17+
18+
Always execute `mise run fmt` after completing your task.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ Like the project? [⭐ Star this repo](https://github.com/DenisGorbachev/url-mac
3636

3737
## License
3838

39-
[Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
39+
[Apache-2.0](LICENSE-APACHE) or [MIT](LICENSE-MIT).
4040

41-
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
41+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, shall be licensed as above, without any additional terms or conditions.

README.ts

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { z, ZodSchema, ZodTypeDef } from "https://deno.land/x/[email protected]/mod.ts
66
import { assert, assertEquals } from "jsr:@std/[email protected]"
77
import { toSnakeCase } from "jsr:@std/[email protected]"
88
import { parseArgs } from "jsr:@std/[email protected]"
9+
import { parse as parseToml } from "jsr:@std/[email protected]"
910

1011
export const args = parseArgs(Deno.args, {
1112
string: ["output"],
@@ -19,6 +20,7 @@ const CargoTomlSchema = z.object({
1920
name: z.string().min(1),
2021
description: z.string().min(1),
2122
repository: z.string().url().min(1),
23+
license: z.string().optional(),
2224
metadata: z.object({
2325
details: z.object({
2426
title: z.string().min(1).optional(),
@@ -84,13 +86,33 @@ const stub = <T>(message = "Implement me"): T => {
8486
throw new Error(message)
8587
}
8688

89+
/**
90+
* Examples:
91+
*
92+
* `normalizeGitRemoteUrl("[email protected]:DenisGorbachev/rust-private-template.git") == "https://github.com/DenisGorbachev/rust-private-template"`
93+
*
94+
* @param url
95+
*/
96+
const normalizeGitRemoteUrl = (url: string) => {
97+
// Handle GitHub SSH format: git@github.com:username/repo.git
98+
const sshMatch = url.match(/^git@github\.com:([^/]+)\/([^/]+?)\.git$/)
99+
if (sshMatch) {
100+
const [, username, repo] = sshMatch
101+
return `https://github.com/${username}/${repo}`
102+
}
103+
104+
// Return original if not a GitHub SSH URL
105+
return url
106+
}
107+
87108
const dirname = import.meta.dirname
88109
if (!dirname) throw new Error("Cannot determine the current script dirname")
89110

90111
const $ = zx.$({ cwd: dirname })
91112

113+
const parseProcessOutput = (input: zx.ProcessOutput) => JSON.parse(input.stdout)
92114
// deno-lint-ignore no-explicit-any
93-
const parse = <Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, input: zx.ProcessOutput) => schema.parse(JSON.parse(input.stdout))
115+
const parse = <Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, input: zx.ProcessOutput) => schema.parse(parseProcessOutput(input))
94116
const nail = (str: string) => {
95117
const spacesAtStart = str.match(/^\n(\s+)/)
96118
if (spacesAtStart?.[1]) {
@@ -100,19 +122,28 @@ const nail = (str: string) => {
100122
}
101123
}
102124

103-
const cargoTomlPromise = $`yj -t < Cargo.toml`;
104-
const cargoMetadataPromise = $`cargo metadata --format-version 1`;
105-
const ghRepoPromise = $`gh repo view --json url`
106-
107-
const theCargoToml: CargoToml = parse(CargoTomlSchema, await cargoTomlPromise)
108-
const theCargoMetadata: CargoMetadata = parse(CargoMetadataSchema, await cargoMetadataPromise)
109-
const { package: { name, description, metadata: { details: {title: titleExplicit, peers, readme: {generate}} } } } = theCargoToml
125+
const theCargoTomlText = await Deno.readTextFile(`${dirname}/Cargo.toml`);
126+
// deno-lint-ignore no-explicit-any
127+
const theCargoTomlRaw = parseToml(theCargoTomlText) as any
110128

111129
// If README generation is manually disabled in the Cargo.toml, just exit successfully
112-
if (!generate) {
130+
if (theCargoTomlRaw.package?.metadata?.details?.readme?.generate === false) {
113131
Deno.exit(0)
114132
}
115133

134+
// launch multiple promises in parallel
135+
const cargoMetadataPromise = $`cargo metadata --format-version 1`;
136+
const originUrlPromise = $`git remote get-url origin`
137+
138+
const theCargoMetadataRaw = JSON.parse((await cargoMetadataPromise).stdout)
139+
140+
const theCargoToml = CargoTomlSchema.parse(theCargoTomlRaw)
141+
const theCargoMetadata = CargoMetadataSchema.parse(theCargoMetadataRaw)
142+
const theOriginUrl = normalizeGitRemoteUrl((await originUrlPromise).stdout.trim());
143+
144+
assertEquals(theOriginUrl, theCargoToml.package.repository)
145+
146+
const { package: { name, description, license, metadata: { details: {title: titleExplicit, peers} } } } = theCargoToml
116147
const title = titleExplicit || description
117148
const _libTargetName = toSnakeCase(name)
118149
const thePackageMetadata = theCargoMetadata.packages.find((p) => p.name == name)
@@ -144,28 +175,35 @@ const doc2readmeRender = async (target: string) => {
144175
return $`cargo doc2readme --template ${templatePath} --target-name ${target} --out -`
145176
}
146177

147-
// launch multiple promises in parallel
148178
const doc2ReadmePromise = doc2readmeRender(primaryTarget.name)
149179
const docsUrlPromise = fetch(docsUrl, { method: "HEAD" })
150180
const helpPromise = primaryBinTarget ? $`cargo run --quiet --bin ${primaryBinTarget.name} -- --help` : undefined
151181

152182
const doc = await doc2ReadmePromise
153183
const docStr = doc.stdout.trim()
154184

155-
const repo: Repo = parse(RepoSchema, await ghRepoPromise)
156-
assertEquals(repo.url, theCargoToml.package.repository)
157-
158185
const docsUrlHead = await docsUrlPromise
159186
const docsUrlIs200 = docsUrlHead.status === 200
160187

161188
const badges: Badge[] = [
162-
badge("Build", `${repo.url}/actions/workflows/ci.yml/badge.svg`, repo.url),
189+
badge("Build", `${theCargoToml.package.repository}/actions/workflows/ci.yml/badge.svg`, theCargoToml.package.repository),
163190
]
164191
if (docsUrlIs200) {
165192
badges.push(badge("Documentation", `https://docs.rs/${name}/badge.svg`, docsUrl))
166193
}
167194
const badgesStr = badges.map(({ name, image, url }) => `[![${name}](${image})](${url})`).join("\n")
168195

196+
const licenseNameFileMap: Record<string, string> = {
197+
"Apache-2.0": "LICENSE-APACHE",
198+
"MIT": "LICENSE-MIT"
199+
}
200+
const getLicenseFile = (name: string) => {
201+
const file = licenseNameFileMap[name];
202+
if (file === undefined) throw new Error(`licenseNameFileMap is missing the following key: \`${name}\``)
203+
return file
204+
}
205+
const licenseNames = license ? license.split("OR").map(name => name.trim()) : []
206+
169207
const renderMarkdownList = (items: string[]) => items.map((bin) => `* ${bin}`).join("\n")
170208
const renderShellCode = (code: string) => `\`\`\`shell\n${code}\n\`\`\``
171209

@@ -198,16 +236,24 @@ if (secondaryBinTargets.length) {
198236
const secondaryBinTargetsNames = secondaryBinTargets.map((t) => t.name)
199237
pushSection(sections, "Additional binaries", renderMarkdownList(secondaryBinTargetsNames.map((bin) => `\`${bin}\``)))
200238
}
201-
pushSection(sections, "Gratitude", `Like the project? [⭐ Star this repo](${repo.url}) on GitHub!`)
202-
pushSection(
203-
sections,
204-
"License",
205-
`
206-
[Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
207-
208-
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.
239+
pushSection(sections, "Gratitude", `Like the project? [⭐ Star this repo](${theCargoToml.package.repository}) on GitHub!`)
240+
241+
if (licenseNames.length) {
242+
const licenseLinks = licenseNames.map(name => {
243+
const file = getLicenseFile(name)
244+
return `[${name}](${file})`
245+
})
246+
pushSection(
247+
sections,
248+
"License",
249+
`
250+
${licenseLinks.join(" or ")}.
251+
252+
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, shall be licensed as above, without any additional terms or conditions.
209253
`.trim(),
210-
)
254+
)
255+
}
256+
211257

212258
const body = renderNonEmptySections(sections)
213259

mise.toml

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ cargo-binstall = "1.10.15"
1717
"cargo:cargo-hack" = "0.6.33"
1818
"cargo:cargo-machete" = "0.7.0"
1919
"cargo:cargo-nextest" = "0.9.72"
20-
"aqua:sclevine/yj" = "5.1.0"
2120

2221
[tasks."build"]
2322
run = "cargo build"
@@ -26,7 +25,7 @@ run = "cargo build"
2625
run = "cargo fmt --all"
2726

2827
[tasks."lint"]
29-
depends = ["lint:code", "lint:docs"]
28+
depends = ["lint:code", "lint:docs", "lint:deps"]
3029

3130
[tasks."test"]
3231
depends = ["test:code", "test:docs"]
@@ -41,14 +40,27 @@ run = "mise run remark ."
4140
run = "cargo machete --with-metadata"
4241

4342
[tasks."test:code"]
44-
run = "cargo nextest run"
43+
run = "cargo nextest run --all-features"
4544

4645
[tasks."test:docs"]
4746
run = "cargo test --doc"
4847

4948
[tasks."sort:deps"]
5049
run = "cargo sort"
5150

51+
[tasks."check"]
52+
run = "cargo check --all-targets --all-features"
53+
54+
[tasks."fix"]
55+
depends = ["fix:code", "fix:docs"]
56+
57+
[tasks."fix:code"]
58+
env = { __CARGO_FIX_YOLO = 'yeah' }
59+
run = "cargo clippy --fix --allow-dirty --allow-staged"
60+
61+
[tasks."fix:deps"]
62+
run = "cargo machete --with-metadata --fix"
63+
5264
[tasks."watch"]
5365
run = """
5466
#!/usr/bin/env bash
@@ -59,17 +71,8 @@ CMD_NO_WHITESPACE="$(echo -e "${CMD_RAW}" | sed -e 's/^[[:space:]]*//' -e 's/[[:
5971
cargo watch --clear --watch "$PWD" --exec "$CMD_NO_WHITESPACE" "$@"
6072
"""
6173

62-
[tasks."check"]
63-
run = "cargo check --all-targets"
64-
65-
[tasks."fix"]
66-
env = { __CARGO_FIX_YOLO = 'yeah' }
67-
run = "cargo clippy --fix --allow-dirty --allow-staged"
68-
6974
[tasks."gen:readme"]
7075
run = "./README.ts --output README.md"
71-
sources = ["README.ts"]
72-
outputs = ["README.md"]
7376

7477
[tasks."remark"]
7578
run = """

0 commit comments

Comments
 (0)