Skip to content

Commit ba8f5af

Browse files
fix: README
1 parent 1d97649 commit ba8f5af

File tree

2 files changed

+109
-60
lines changed

2 files changed

+109
-60
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ let valid = url!("https://www.rust-lang.org/");
2121
let invalid = url!("foo");
2222
```
2323

24-
[__cargo_doc2readme_dependencies_info]: ggGkYW0BYXSEGyMws-dKI-LpG9swkVXG-rikGwSuJGhB0NVbG974QPrPJF6XYXKEG4AA8JRKwJB9G9olxhSTKUcIG1sf0boPKowfG1HA4Nxt7NpkYWSBg2l1cmwtbWFjcm9mMC4xLjExaXVybF9tYWNybw
24+
[__cargo_doc2readme_dependencies_info]: ggGkYW0BYXSEGyMws-dKI-LpG9swkVXG-rikGwSuJGhB0NVbG974QPrPJF6XYXKEG4AA8JRKwJB9G9olxhSTKUcIG1sf0boPKowfG1HA4Nxt7NpkYWSBg2l1cmwtbWFjcm9mMC4xLjEyaXVybF9tYWNybw
2525
[__link0]: https://docs.rs/url-macro/latest/url_macro/?search=url
2626

2727
## Installation

README.ts

Lines changed: 108 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,76 @@
33
import * as zx from "npm:zx"
44
import { z, ZodSchema, ZodTypeDef } from "https://deno.land/x/[email protected]/mod.ts"
55
import { assert, assertEquals } from "jsr:@std/[email protected]"
6+
import { toSnakeCase } from "jsr:@std/[email protected]"
67

78
const CargoTomlSchema = z.object({
8-
package: z.object({
9-
name: z.string().min(1),
10-
description: z.string().min(1),
11-
repository: z.string().url().min(1),
12-
metadata: z.object({
13-
details: z.object({
14-
title: z.string().min(1).optional(),
15-
tagline: z.string().optional(),
16-
summary: z.string().optional(),
17-
peers: z.array(z.string()).default([]).describe("Packages that should be installed alongside this package")
18-
}).default({}),
19-
}).default({}),
20-
}),
9+
package: z.object({
10+
name: z.string().min(1),
11+
description: z.string().min(1),
12+
repository: z.string().url().min(1),
13+
metadata: z.object({
14+
details: z.object({
15+
title: z.string().min(1).optional(),
16+
tagline: z.string().optional(),
17+
summary: z.string().optional(),
18+
peers: z.array(z.string()).default([]).describe("Packages that should be installed alongside this package"),
19+
}).default({}),
20+
}).default({}),
21+
}),
2122
})
2223

2324
type CargoToml = z.infer<typeof CargoTomlSchema>
2425

2526
const CargoMetadataSchema = z.object({
26-
packages: z.array(z.object({
27-
name: z.string(),
28-
targets: z.array(z.object({
29-
name: z.string(),
27+
packages: z.array(z.object({
28+
name: z.string(),
29+
source: z.string().nullable(),
30+
targets: z.array(z.object({
31+
name: z.string(),
32+
kind: z.array(z.string()),
33+
})),
3034
})),
31-
})),
3235
})
3336

3437
type CargoMetadata = z.infer<typeof CargoMetadataSchema>
3538

3639
const RepoSchema = z.object({
37-
url: z.string().url(),
40+
url: z.string().url(),
3841
})
3942

4043
type Repo = z.infer<typeof RepoSchema>
4144

4245
const BadgeSchema = z.object({
43-
name: z.string().min(1),
44-
image: z.string().url(),
45-
url: z.string().url(),
46+
name: z.string().min(1),
47+
image: z.string().url(),
48+
url: z.string().url(),
4649
})
4750

4851
type Badge = z.infer<typeof BadgeSchema>
4952

50-
const badge = (name: string, image: string, url: string): Badge => BadgeSchema.parse({name, url, image})
53+
const badge = (name: string, image: string, url: string): Badge => BadgeSchema.parse({ name, url, image })
54+
55+
const SectionSchema = z.object({
56+
title: z.string().min(1),
57+
body: z.string(),
58+
})
59+
60+
type Section = z.infer<typeof SectionSchema>
61+
62+
const section = (title: string, body: string): Section => SectionSchema.parse({ title, body })
63+
64+
const pushSection = (sections: Section[], title: string, body: string) => sections.push(section(title, body))
65+
66+
// Nested sections not supported
67+
const renderSection = ({ title, body }: Section) => `## ${title}\n\n${body}`
5168

52-
const dirname = import.meta.dirname;
69+
const renderNonEmptySections = (sections: Section[]) => sections.filter((s) => s.body).map(renderSection).join("\n\n")
70+
71+
const stub = <T>(message = "Implement me"): T => {
72+
throw new Error(message)
73+
}
74+
75+
const dirname = import.meta.dirname
5376
if (!dirname) throw new Error("Cannot determine the current script dirname")
5477

5578
const $ = zx.$({ cwd: dirname })
@@ -58,23 +81,30 @@ const $ = zx.$({ cwd: dirname })
5881
const parse = <Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, input: zx.ProcessOutput) => schema.parse(JSON.parse(input.stdout))
5982

6083
const theCargoToml: CargoToml = parse(CargoTomlSchema, await $`yj -t < Cargo.toml`)
61-
const { package: { name, description, metadata: { details } } }= theCargoToml
84+
const { package: { name, description, metadata: { details } } } = theCargoToml
6285
const title = details.title || description
6386
const peers = details.peers
87+
const _libTargetName = toSnakeCase(name)
6488
const theCargoMetadata: CargoMetadata = parse(CargoMetadataSchema, await $`cargo metadata --format-version 1`)
6589
const thePackageMetadata = theCargoMetadata.packages.find((p) => p.name == name)
6690
assert(thePackageMetadata, "Could not find package metadata")
67-
const target = thePackageMetadata.targets[0]
68-
assert(target, "Could not find package first target")
91+
const primaryTarget = thePackageMetadata.targets[0]
92+
assert(primaryTarget, "Could not find package primary target")
93+
const primaryBinTarget = thePackageMetadata.targets.find((t) => t.name == name && t.kind.includes("bin"))
94+
// NOTE: primaryTarget may be equal to primaryBinTarget
95+
const primaryTargets = [primaryTarget, primaryBinTarget]
96+
const secondaryTargets = thePackageMetadata.targets.filter((t) => !primaryTargets.includes(t))
97+
const secondaryBinTargets = secondaryTargets.filter((t) => t.kind.includes("bin"))
6998
const docsUrl = `https://docs.rs/${name}`
7099

71100
// launch multiple promises in parallel
72-
const doc2ReadmePromise = $`cargo doc2readme --template README.jl --target-name ${target.name} --out -`
101+
const doc2ReadmePromise = $`cargo doc2readme --template README.jl --target-name ${primaryTarget.name} --out -`
73102
const ghRepoPromise = $`gh repo view --json url`
74-
const docsUrlPromise = fetch(docsUrl, {method:"HEAD"})
103+
const docsUrlPromise = fetch(docsUrl, { method: "HEAD" })
104+
const helpPromise = primaryBinTarget ? $`cargo run --quiet --bin ${primaryBinTarget.name} -- --help` : undefined
75105

76106
const doc = await doc2ReadmePromise
77-
const docStr = doc.stdout.trim();
107+
const docStr = doc.stdout.trim()
78108

79109
const repo: Repo = parse(RepoSchema, await ghRepoPromise)
80110
assertEquals(repo.url, theCargoToml.package.repository)
@@ -83,46 +113,65 @@ const docsUrlHead = await docsUrlPromise
83113
const docsUrlIs200 = docsUrlHead.status === 200
84114

85115
const badges: Badge[] = [
86-
badge("Build", `${repo.url}/actions/workflows/ci.yml/badge.svg`, repo.url)
116+
badge("Build", `${repo.url}/actions/workflows/ci.yml/badge.svg`, repo.url),
87117
]
88118
if (docsUrlIs200) {
89-
badges.push(badge("Documentation", `https://docs.rs/${name}/badge.svg`, docsUrl))
119+
badges.push(badge("Documentation", `https://docs.rs/${name}/badge.svg`, docsUrl))
90120
}
91-
const badgesStr = badges.map(({name, image, url}) => `[![${name}](${image})](${url})`).join("\n");
121+
const badgesStr = badges.map(({ name, image, url }) => `[![${name}](${image})](${url})`).join("\n")
122+
123+
const renderMarkdownList = (items: string[]) => items.map((bin) => `* ${bin}`).join("\n")
124+
const renderShellCode = (code: string) => `\`\`\`shell\n${code}\n\`\`\``
92125

93-
const titleSection = [
126+
const titleSectionBodyParts = [
94127
badgesStr,
95-
docStr
96-
].filter(s => s.length)
128+
docStr,
129+
].filter((s) => s.length)
130+
const titleSectionBody = titleSectionBodyParts.join("\n\n")
131+
132+
const sections: Section[] = []
133+
// NOTE: We need to use the package name (not the target name) in cargo commands
134+
const installationSectionBodyParts = []
135+
const installationSectionUseExpandedFormat = primaryBinTarget && primaryTarget !== primaryBinTarget
136+
if (primaryBinTarget) {
137+
const cmd = renderShellCode(`cargo install --locked ${name}`)
138+
const text = installationSectionUseExpandedFormat ? `Install as executable:\n\n${cmd}` : cmd
139+
installationSectionBodyParts.push(text)
140+
}
141+
if (primaryTarget !== primaryBinTarget) {
142+
const cmd = renderShellCode(`cargo add ${[name, ...peers].join(" ")}`)
143+
const text = installationSectionUseExpandedFormat ? `Install as library dependency in your package:\n\n${cmd}` : cmd
144+
installationSectionBodyParts.push(text)
145+
}
146+
pushSection(sections, "Installation", installationSectionBodyParts.join("\n\n"))
147+
if (helpPromise) {
148+
const help = await helpPromise
149+
pushSection(sections, "Usage", renderShellCode(help.stdout.trim()))
150+
}
151+
if (secondaryBinTargets.length) {
152+
const secondaryBinTargetsNames = secondaryBinTargets.map((t) => t.name)
153+
pushSection(sections, "Additional binaries", renderMarkdownList(secondaryBinTargetsNames.map((bin) => `\`${bin}\``)))
154+
}
155+
pushSection(sections, "Gratitude", `Like the project? [⭐ Star this repo](${repo.url}) on GitHub!`)
156+
pushSection(
157+
sections,
158+
"License",
159+
`
160+
[Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
161+
162+
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.
163+
`.trim(),
164+
)
97165

98-
const cargoAddPackages = [name, ...peers];
166+
const body = renderNonEmptySections(sections)
99167

100-
const autogenerated = `
168+
console.info(`
101169
<!-- DO NOT EDIT -->
102170
<!-- This file is automatically generated by README.ts. -->
103171
<!-- Edit README.ts if you want to make changes. -->
104-
`.trim()
105-
106-
console.info(`
107-
${autogenerated}
108172
109173
# ${title}
110174
111-
${titleSection.join("\n\n")}
112-
113-
## Installation
175+
${titleSectionBody}
114176
115-
\`\`\`shell
116-
cargo add ${cargoAddPackages.join(" ")}
117-
\`\`\`
118-
119-
## Gratitude
120-
121-
Like the project? [⭐ Star this repo](${repo.url}) on GitHub!
122-
123-
## License
124-
125-
[Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
126-
127-
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.
128-
`.trim())
177+
${body}`.trim())

0 commit comments

Comments
 (0)