33import * as zx from "npm:zx"
44import { z , ZodSchema , ZodTypeDef } from "https://deno.land/x/[email protected] /mod.ts" 55import { assert , assertEquals } from "jsr:@std/[email protected] " 6+ import { toSnakeCase } from "jsr:@std/[email protected] " 67
78const 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
2324type CargoToml = z . infer < typeof CargoTomlSchema >
2425
2526const 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
3437type CargoMetadata = z . infer < typeof CargoMetadataSchema >
3538
3639const RepoSchema = z . object ( {
37- url : z . string ( ) . url ( ) ,
40+ url : z . string ( ) . url ( ) ,
3841} )
3942
4043type Repo = z . infer < typeof RepoSchema >
4144
4245const 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
4851type 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
5376if ( ! dirname ) throw new Error ( "Cannot determine the current script dirname" )
5477
5578const $ = zx . $ ( { cwd : dirname } )
@@ -58,23 +81,30 @@ const $ = zx.$({ cwd: dirname })
5881const parse = < Output = any , Def extends ZodTypeDef = ZodTypeDef , Input = Output > ( schema : ZodSchema < Output , Def , Input > , input : zx . ProcessOutput ) => schema . parse ( JSON . parse ( input . stdout ) )
5982
6083const 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
6285const title = details . title || description
6386const peers = details . peers
87+ const _libTargetName = toSnakeCase ( name )
6488const theCargoMetadata : CargoMetadata = parse ( CargoMetadataSchema , await $ `cargo metadata --format-version 1` )
6589const thePackageMetadata = theCargoMetadata . packages . find ( ( p ) => p . name == name )
6690assert ( 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" ) )
6998const 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 -`
73102const 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
76106const doc = await doc2ReadmePromise
77- const docStr = doc . stdout . trim ( ) ;
107+ const docStr = doc . stdout . trim ( )
78108
79109const repo : Repo = parse ( RepoSchema , await ghRepoPromise )
80110assertEquals ( repo . url , theCargoToml . package . repository )
@@ -83,46 +113,65 @@ const docsUrlHead = await docsUrlPromise
83113const docsUrlIs200 = docsUrlHead . status === 200
84114
85115const 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]
88118if ( 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} ) => `[](${ url } )` ) . join ( "\n" ) ;
121+ const badgesStr = badges . map ( ( { name, image, url } ) => `[](${ 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