-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathREADME.ts
executable file
·177 lines (142 loc) · 6.82 KB
/
README.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
#!/usr/bin/env -S deno run --allow-read --allow-run=bash,git,cargo --allow-net=docs.rs:443 --allow-env --allow-sys
import * as zx from "npm:zx"
import { z, ZodSchema, ZodTypeDef } from "https://deno.land/x/[email protected]/mod.ts"
import { assert, assertEquals } from "jsr:@std/[email protected]"
import { toSnakeCase } from "jsr:@std/[email protected]"
const CargoTomlSchema = z.object({
package: z.object({
name: z.string().min(1),
description: z.string().min(1),
repository: z.string().url().min(1),
metadata: z.object({
details: z.object({
title: z.string().min(1).optional(),
tagline: z.string().optional(),
summary: z.string().optional(),
peers: z.array(z.string()).default([]).describe("Packages that should be installed alongside this package"),
}).default({}),
}).default({}),
}),
})
type CargoToml = z.infer<typeof CargoTomlSchema>
const CargoMetadataSchema = z.object({
packages: z.array(z.object({
name: z.string(),
source: z.string().nullable(),
targets: z.array(z.object({
name: z.string(),
kind: z.array(z.string()),
})),
})),
})
type CargoMetadata = z.infer<typeof CargoMetadataSchema>
const RepoSchema = z.object({
url: z.string().url(),
})
type Repo = z.infer<typeof RepoSchema>
const BadgeSchema = z.object({
name: z.string().min(1),
image: z.string().url(),
url: z.string().url(),
})
type Badge = z.infer<typeof BadgeSchema>
const badge = (name: string, image: string, url: string): Badge => BadgeSchema.parse({ name, url, image })
const SectionSchema = z.object({
title: z.string().min(1),
body: z.string(),
})
type Section = z.infer<typeof SectionSchema>
const section = (title: string, body: string): Section => SectionSchema.parse({ title, body })
const pushSection = (sections: Section[], title: string, body: string) => sections.push(section(title, body))
// Nested sections not supported
const renderSection = ({ title, body }: Section) => `## ${title}\n\n${body}`
const renderNonEmptySections = (sections: Section[]) => sections.filter((s) => s.body).map(renderSection).join("\n\n")
const stub = <T>(message = "Implement me"): T => {
throw new Error(message)
}
const dirname = import.meta.dirname
if (!dirname) throw new Error("Cannot determine the current script dirname")
const $ = zx.$({ cwd: dirname })
// deno-lint-ignore no-explicit-any
const parse = <Output = any, Def extends ZodTypeDef = ZodTypeDef, Input = Output>(schema: ZodSchema<Output, Def, Input>, input: zx.ProcessOutput) => schema.parse(JSON.parse(input.stdout))
const theCargoToml: CargoToml = parse(CargoTomlSchema, await $`yj -t < Cargo.toml`)
const { package: { name, description, metadata: { details } } } = theCargoToml
const title = details.title || description
const peers = details.peers
const _libTargetName = toSnakeCase(name)
const theCargoMetadata: CargoMetadata = parse(CargoMetadataSchema, await $`cargo metadata --format-version 1`)
const thePackageMetadata = theCargoMetadata.packages.find((p) => p.name == name)
assert(thePackageMetadata, "Could not find package metadata")
const primaryTarget = thePackageMetadata.targets[0]
assert(primaryTarget, "Could not find package primary target")
const primaryBinTarget = thePackageMetadata.targets.find((t) => t.name == name && t.kind.includes("bin"))
// NOTE: primaryTarget may be equal to primaryBinTarget
const primaryTargets = [primaryTarget, primaryBinTarget]
const secondaryTargets = thePackageMetadata.targets.filter((t) => !primaryTargets.includes(t))
const secondaryBinTargets = secondaryTargets.filter((t) => t.kind.includes("bin"))
const docsUrl = `https://docs.rs/${name}`
// launch multiple promises in parallel
const doc2ReadmePromise = $`cargo doc2readme --template README.jl --target-name ${primaryTarget.name} --out -`
const ghRepoPromise = $`gh repo view --json url`
const docsUrlPromise = fetch(docsUrl, { method: "HEAD" })
const helpPromise = primaryBinTarget ? $`cargo run --quiet --bin ${primaryBinTarget.name} -- --help` : undefined
const doc = await doc2ReadmePromise
const docStr = doc.stdout.trim()
const repo: Repo = parse(RepoSchema, await ghRepoPromise)
assertEquals(repo.url, theCargoToml.package.repository)
const docsUrlHead = await docsUrlPromise
const docsUrlIs200 = docsUrlHead.status === 200
const badges: Badge[] = [
badge("Build", `${repo.url}/actions/workflows/ci.yml/badge.svg`, repo.url),
]
if (docsUrlIs200) {
badges.push(badge("Documentation", `https://docs.rs/${name}/badge.svg`, docsUrl))
}
const badgesStr = badges.map(({ name, image, url }) => `[](${url})`).join("\n")
const renderMarkdownList = (items: string[]) => items.map((bin) => `* ${bin}`).join("\n")
const renderShellCode = (code: string) => `\`\`\`shell\n${code}\n\`\`\``
const titleSectionBodyParts = [
badgesStr,
docStr,
].filter((s) => s.length)
const titleSectionBody = titleSectionBodyParts.join("\n\n")
const sections: Section[] = []
// NOTE: We need to use the package name (not the target name) in cargo commands
const installationSectionBodyParts = []
const installationSectionUseExpandedFormat = primaryBinTarget && primaryTarget !== primaryBinTarget
if (primaryBinTarget) {
const cmd = renderShellCode(`cargo install --locked ${name}`)
const text = installationSectionUseExpandedFormat ? `Install as executable:\n\n${cmd}` : cmd
installationSectionBodyParts.push(text)
}
if (primaryTarget !== primaryBinTarget) {
const cmd = renderShellCode(`cargo add ${[name, ...peers].join(" ")}`)
const text = installationSectionUseExpandedFormat ? `Install as library dependency in your package:\n\n${cmd}` : cmd
installationSectionBodyParts.push(text)
}
pushSection(sections, "Installation", installationSectionBodyParts.join("\n\n"))
if (helpPromise) {
const help = await helpPromise
pushSection(sections, "Usage", renderShellCode(help.stdout.trim()))
}
if (secondaryBinTargets.length) {
const secondaryBinTargetsNames = secondaryBinTargets.map((t) => t.name)
pushSection(sections, "Additional binaries", renderMarkdownList(secondaryBinTargetsNames.map((bin) => `\`${bin}\``)))
}
pushSection(sections, "Gratitude", `Like the project? [⭐ Star this repo](${repo.url}) on GitHub!`)
pushSection(
sections,
"License",
`
[Apache License 2.0](LICENSE-APACHE) or [MIT License](LICENSE-MIT) at your option.
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.
`.trim(),
)
const body = renderNonEmptySections(sections)
console.info(`
<!-- DO NOT EDIT -->
<!-- This file is automatically generated by README.ts. -->
<!-- Edit README.ts if you want to make changes. -->
# ${title}
${titleSectionBody}
${body}`.trim())