1
+ // This [Deno] program scans `target/doc` directory and replaces the non-local
2
+ // crate documentation with redirect pages to docs.rs.
3
+ //
4
+ // [Deno]: https://deno.land/
5
+ //
6
+ // Usage: deno run -A scripts/externalize-non-local-docs.ts
7
+ import { parse as parseFlags } from "https://deno.land/std@0.125.0/flags/mod.ts" ;
8
+ import { parse as parseToml } from "https://deno.land/std@0.125.0/encoding/toml.ts" ;
9
+ import * as path from "https://deno.land/std@0.125.0/path/mod.ts" ;
10
+ import * as log from "https://deno.land/std@0.125.0/log/mod.ts" ;
11
+ import { walk } from "https://deno.land/std@0.125.0/fs/walk.ts" ;
12
+ import * as semver from "https://deno.land/x/semver@v1.4.0/mod.ts" ;
13
+
14
+ const parsedArgs = parseFlags ( Deno . args , {
15
+ "alias" : {
16
+ h : "help" ,
17
+ w : "workspace" ,
18
+ y : "apply" ,
19
+ } ,
20
+ "string" : [
21
+ "workspace" ,
22
+ ] ,
23
+ } ) ;
24
+
25
+ if ( parsedArgs [ "help" ] ) {
26
+ console . log ( "Arguments:" ) ;
27
+ console . log ( " -h --help Displays this message" ) ;
28
+ console . log ( " -w WORKSPACE --workspace=WORKSPACE" ) ;
29
+ console . log ( " Specifies the workspace directory. Defaults to `.` when unspecified." ) ;
30
+ console . log ( " -y --apply Actually make modification" ) ;
31
+ }
32
+
33
+ await log . setup ( {
34
+ handlers : {
35
+ console : new log . handlers . ConsoleHandler ( "DEBUG" ) ,
36
+ } ,
37
+
38
+ loggers : {
39
+ default : {
40
+ level : "INFO" ,
41
+ handlers : [ "console" ] ,
42
+ } ,
43
+ } ,
44
+ } ) ;
45
+
46
+ const logger = log . getLogger ( ) ;
47
+
48
+ // This script is quite destructive, so as a safety measure, we don't make
49
+ // changes unless we're expressly told to do so.
50
+ const shouldModify : boolean = parsedArgs . y ;
51
+
52
+ if ( ! shouldModify ) {
53
+ logger . warning ( "Performing a dry run because the `--apply` flag was not given" ) ;
54
+ }
55
+
56
+ // Get the workspace metadata <https://doc.rust-lang.org/1.58.1/cargo/commands/cargo-metadata.html>
57
+ const workspaceMetaJson = await ( async ( ) => {
58
+ if ( ( parsedArgs . w || "." ) !== "." ) {
59
+ logger . error ( "Unsupported: Operating on a workspace outside the current "
60
+ + "directory is not supported." ) ;
61
+ Deno . exit ( 1 ) ;
62
+ }
63
+ const process = Deno . run ( {
64
+ cmd : [
65
+ "cargo" , "metadata" , "--format-version=1" , "--all-features" ,
66
+ ] ,
67
+ stdout : "piped" ,
68
+ } ) ;
69
+ const [ stdoutBytes , status ] = await Promise . all ( [ process . output ( ) , process . status ( ) ] ) ;
70
+ if ( ! status . success ) {
71
+ Deno . exit ( status . code ) ;
72
+ }
73
+ return new TextDecoder ( ) . decode ( stdoutBytes ) ;
74
+ } ) ( ) ;
75
+ const workspaceMeta : CargoMetadataV1 = JSON . parse ( workspaceMetaJson ) ;
76
+
77
+ type CargoMetadataV1 = {
78
+ packages : PackageMetadataV1 [ ] ,
79
+ } ;
80
+ type PackageMetadataV1 = {
81
+ name : string ,
82
+ version : string ,
83
+ source : string | null ,
84
+ targets : {
85
+ kind : string [ ] ,
86
+ crate_types : string [ ] ,
87
+ name : string ,
88
+ } [ ] ,
89
+ } ;
90
+
91
+ // Calculate the mapping from crate names to docs.rs URLs
92
+ const crateNameToPackage = new Map < string , PackageMetadataV1 [ ] > ( ) ;
93
+ logger . debug ( "Constructing a mapping from crate names to packages" ) ;
94
+ for ( const pkg of workspaceMeta . packages ) {
95
+ logger . debug ( ` - ${ pkg . name } ${ pkg . version } ` ) ;
96
+
97
+ const libraryTarget = pkg . targets . find ( t => t . kind . find ( k => k == "lib" || k == "proc-macro" ) ) ;
98
+ if ( libraryTarget == null ) {
99
+ logger . debug ( "It doesn't provide a library crate - ignoring" ) ;
100
+ continue ;
101
+ }
102
+
103
+ const crateName = libraryTarget . name . replace ( / - / g, '_' ) ;
104
+ logger . debug ( `Crate name = ${ crateName } ` ) ;
105
+ if ( ! crateNameToPackage . has ( crateName ) ) {
106
+ crateNameToPackage . set ( crateName , [ ] ) ;
107
+ }
108
+ crateNameToPackage . get ( crateName ) ! . push ( pkg ) ;
109
+ }
110
+ if ( crateNameToPackage . size === 0 ) {
111
+ logger . error ( "The crate name mapping is empty - something is wrong." ) ;
112
+ Deno . exit ( 1 ) ;
113
+ }
114
+
115
+ // Scan the built documentation directory
116
+ const docPath = path . join ( parsedArgs . w || "." , "target/doc" ) ;
117
+
118
+ for await ( const entry of Deno . readDir ( docPath ) ) {
119
+ if ( entry . isDirectory && entry . name !== "src" && entry . name !== "implementors" ) {
120
+ await processCrateDocumentation (
121
+ path . join ( docPath , entry . name ) ,
122
+ `/${ entry . name } ` ,
123
+ entry . name ,
124
+ ) ;
125
+ }
126
+ }
127
+ for await ( const entry of Deno . readDir ( path . join ( docPath , "src" ) ) ) {
128
+ if ( entry . isDirectory ) {
129
+ await processCrateDocumentation (
130
+ path . join ( docPath , "src" , entry . name ) ,
131
+ `/src/${ entry . name } ` ,
132
+ entry . name ,
133
+ ) ;
134
+ }
135
+ }
136
+
137
+ async function processCrateDocumentation ( docPath : string , relPath : string , crateName : string ) {
138
+ const packages = crateNameToPackage . get ( crateName ) ?? [ ] ;
139
+
140
+ // Ignore non-crates.io packages
141
+ if ( packages . find ( p => p . source !== "registry+https://github.com/rust-lang/crates.io-index" ) ) {
142
+ logger . debug ( `${ docPath } : It might be a non-crates.io package, ignoring` ) ;
143
+ return ;
144
+ }
145
+
146
+ if ( packages . length === 0 ) {
147
+ logger . warning ( `${ docPath } : Unknown crate, ignoring` ) ;
148
+ return ;
149
+ }
150
+
151
+ if ( crateName . startsWith ( "r3_" ) ) {
152
+ logger . warning ( `${ docPath } : It starts with 'r3_' but is about to ` +
153
+ `be processed - something might be wrong.` ) ;
154
+ return ;
155
+ }
156
+
157
+ // If there are multiple candidates, choose the one with the highest version
158
+ const pkg = packages . reduce ( ( x , y ) => semver . gt ( x . version , y . version ) ? x : y ) ;
159
+ if ( packages . length > 1 ) {
160
+ const candidates = JSON . stringify ( packages . map ( p => p . version ) ) ;
161
+ logger . info ( `${ docPath } : Choosing ${ pkg . version } from the candidate(s) ${ candidates } ` ) ;
162
+ }
163
+
164
+ const externalDocBaseUrl = `https://docs.rs/${ pkg . name } /${ pkg . version } ${ relPath } ` ;
165
+
166
+ logger . info ( `${ docPath } : Externalizing the documentation to '${ externalDocBaseUrl } '` ) ;
167
+
168
+ const files = [ ] ;
169
+ for await ( const entry of walk ( docPath , { includeDirs : false } ) ) {
170
+ if ( ! entry . name . endsWith ( '.html' ) ) {
171
+ continue ;
172
+ }
173
+ logger . debug ( `${ docPath } : ${ entry . path } ` ) ;
174
+ if ( ! entry . path . startsWith ( docPath ) ) {
175
+ throw new Error ( ) ;
176
+ }
177
+ files . push ( entry . path ) ;
178
+ }
179
+ logger . info ( `${ docPath } : Replacing ${ files . length } file(s) with redirect pages` ) ;
180
+
181
+ logger . debug ( `${ docPath } : Removing the directory` ) ;
182
+ if ( shouldModify ) {
183
+ await Deno . remove ( docPath , { recursive : true } ) ;
184
+ }
185
+
186
+ for ( const filePath of files ) {
187
+ const url = externalDocBaseUrl + filePath . substring ( docPath . length ) ;
188
+ logger . debug ( `${ docPath } : ${ filePath } → ${ url } ` ) ;
189
+ if ( shouldModify ) {
190
+ await Deno . mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
191
+ await Deno . writeTextFile ( filePath , redirectingHtmlCode ( url ) ) ;
192
+ }
193
+ }
194
+ }
195
+
196
+ function redirectingHtmlCode ( url : string ) : string {
197
+ return `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${ url } ">` ;
198
+ }
0 commit comments