1
1
import logger from '@docusaurus/logger' ;
2
2
import { visitParents } from 'unist-util-visit-parents' ;
3
3
import { filter } from 'unist-util-filter' ;
4
- import fs from 'fs' ;
5
- import path from 'path' ;
4
+ import fs from 'node: fs' ;
5
+ import path from 'node: path' ;
6
6
import { Node , Parent } from 'unist' ;
7
7
import type { InlineCode , Link , LinkReference , Text } from 'mdast' ;
8
8
import { MdxJsxFlowElement } from 'mdast-util-mdx-jsx' ;
@@ -20,22 +20,176 @@ const fileContent = new Map<
20
20
string ,
21
21
{ promise : Promise < Parent > ; resolve ?: ( value : Parent ) => void }
22
22
> ( ) ;
23
- const structureDefinitions = new Map < string , string > ( ) ;
24
- const modifiers = new Set < Promise < void > > ( ) ;
23
+
24
+ const EXCLUDE_LIST = [ 'browser-window-options' , 'web-preferences' ] ;
25
25
26
26
export default function attacher ( ) {
27
27
return transformer ;
28
28
}
29
29
30
30
async function transformer ( tree : Parent , file : VFile ) {
31
- // While transforming API structures, put the unmodified content
32
- // (we don't want previews on previews or deadlocks) into a map
33
- // so that when other docs are processed they can grab the content
34
- // which Docusaurus has already processed (important for links).
35
- // Since we don't want to depend on the order in which docs are
36
- // transformed, if other docs are waiting on the content of an
37
- // API structure there will be a promise resolver in the map and
38
- // the other docs will be awaiting the associated promise.
31
+ const structureDefinitions = new Map < string , string > ( ) ;
32
+ const mutations = new Set < Promise < void > > ( ) ;
33
+ /**
34
+ * This function is the test function for the first pass of the tree visitor.
35
+ * Any values returning 'true' will run replaceLinkWithPreview().
36
+ *
37
+ * As a side effect, this function also puts all reference-style links (definitions)
38
+ * for API structures into a Map, which will be used on the second pass.
39
+ */
40
+ const checkLinksandDefinitions = ( node : Node ) : node is Link => {
41
+ if ( isDefinition ( node ) && node . url . includes ( '/api/structures/' ) ) {
42
+ structureDefinitions . set ( node . identifier , node . url ) ;
43
+ }
44
+ if ( isLink ( node ) && node . url . includes ( '/api/structures/' ) ) {
45
+ return EXCLUDE_LIST . every (
46
+ ( excludedFile ) => ! node . url . endsWith ( `/api/structures/${ excludedFile } ` )
47
+ ) ;
48
+ }
49
+
50
+ return false ;
51
+ } ;
52
+
53
+ /**
54
+ * This function is the test function from the second pass of the tree visitor.
55
+ * Any values returning 'true' will run replaceLinkWithPreview().
56
+ */
57
+ function isStructureLinkReference ( node : Node ) : node is LinkReference {
58
+ return isLinkReference ( node ) && structureDefinitions . has ( node . identifier ) ;
59
+ }
60
+
61
+ function replaceLinkWithPreview (
62
+ node : Link | LinkReference ,
63
+ parents : Parent [ ]
64
+ ) {
65
+ // depending on if the node is a direct link or a reference-style link,
66
+ // we get its URL differently.
67
+ let relativeStructureUrl : string ;
68
+ let isInline = false ;
69
+ if ( isLink ( node ) ) {
70
+ relativeStructureUrl = node . url ;
71
+ } else if ( isLinkReference ( node ) ) {
72
+ relativeStructureUrl = structureDefinitions . get ( node . identifier ) ;
73
+ } else {
74
+ return ;
75
+ }
76
+
77
+ if ( relativeStructureUrl . endsWith ( '?inline' ) ) {
78
+ relativeStructureUrl = relativeStructureUrl . split ( '?inline' ) [ 0 ] ;
79
+ isInline = true ;
80
+ }
81
+
82
+ const relativeStructurePath = `${ relativeStructureUrl } .md` ;
83
+
84
+ // No file content promise available, so add one and then wait
85
+ // on it being resolved when the structure doc is processed
86
+ if ( ! fileContent . has ( relativeStructurePath ) ) {
87
+ let resolve : ( value : Parent ) => void ;
88
+ let reject : ( err : Error ) => void ;
89
+
90
+ // Set a timeout as a backstop so we don't deadlock forever if something
91
+ // causes content to never be resolved - in theory an upstream change in
92
+ // Docusaurus could cause that if they limited how many files are being
93
+ // processed in parallel such that too many docs are awaiting others
94
+ const timeoutId = setTimeout ( ( ) => {
95
+ // links in translated locale [xy] have their paths prefixed with /xy/
96
+ const isTranslatedDoc = ! relativeStructurePath . startsWith ( '/docs/' ) ;
97
+
98
+ if ( isTranslatedDoc ) {
99
+ // If we're running locally we might not have translations downloaded
100
+ // so if we don't find it locally just supply the default locale
101
+ const [ _fullPath , locale , docPath ] = relativeStructurePath . match (
102
+ / \/ ( [ a - z ] [ a - z ] ) \/ d o c s \/ ( .* ) /
103
+ ) ;
104
+ const defaultLocalePath = `/docs/${ docPath } ` ;
105
+ const localeDir = path . join ( __dirname , '..' , '..' , 'i18n' , locale ) ;
106
+
107
+ if ( ! fs . existsSync ( localeDir ) ) {
108
+ if ( fileContent . has ( defaultLocalePath ) ) {
109
+ const { promise } = fileContent . get ( defaultLocalePath ) ;
110
+ promise . then ( ( content ) => resolve ( content ) ) ;
111
+ return ;
112
+ }
113
+ }
114
+ }
115
+
116
+ reject (
117
+ new Error (
118
+ `Timed out waiting for API structure content from ${ relativeStructurePath } `
119
+ )
120
+ ) ;
121
+ } , 60000000 ) ;
122
+
123
+ const promise = new Promise < Parent > ( ( resolve_ , reject_ ) => {
124
+ resolve = ( value : Parent ) => {
125
+ clearTimeout ( timeoutId ) ;
126
+ resolve_ ( value ) ;
127
+ } ;
128
+ reject = reject_ ;
129
+ } ) ;
130
+
131
+ fileContent . set ( relativeStructurePath , { promise, resolve } ) ;
132
+ }
133
+
134
+ const { promise : targetStructure } = fileContent . get ( relativeStructurePath ) ;
135
+
136
+ if (
137
+ ( Array . isArray ( node . children ) &&
138
+ node . children . length > 0 &&
139
+ isText ( node . children [ 0 ] ) ) ||
140
+ isInlineCode ( node . children [ 0 ] )
141
+ ) {
142
+ mutations . add (
143
+ targetStructure
144
+ . then ( ( structureContent ) => {
145
+ if ( isInline ) {
146
+ const siblings = parents [ parents . length - 1 ] . children ;
147
+ const filtered = filter (
148
+ structureContent ,
149
+ ( node ) => node . type !== 'mdxjsEsm' && node . type !== 'heading'
150
+ ) ;
151
+ siblings . push ( filtered ) ;
152
+ } else {
153
+ // replace the Link node with an MDX element in-place
154
+ const title = ( node . children [ 0 ] as Text | InlineCode ) . value ;
155
+ const previewNode = node as unknown as MdxJsxFlowElement ;
156
+ previewNode . type = 'mdxJsxFlowElement' ;
157
+ previewNode . name = 'APIStructurePreview' ;
158
+ previewNode . children = [ ] ;
159
+ previewNode . data = {
160
+ _mdxExplicitJsx : true ,
161
+ } ;
162
+ previewNode . attributes = [
163
+ {
164
+ type : 'mdxJsxAttribute' ,
165
+ name : 'url' ,
166
+ value : `${ relativeStructureUrl } ` ,
167
+ } ,
168
+ {
169
+ type : 'mdxJsxAttribute' ,
170
+ name : 'title' ,
171
+ value : title ,
172
+ } ,
173
+ {
174
+ type : 'mdxJsxAttribute' ,
175
+ name : 'content' ,
176
+ value : JSON . stringify ( structureContent ) ,
177
+ } ,
178
+ ] ;
179
+ }
180
+ } )
181
+ . catch ( ( err ) => {
182
+ logger . error ( err ) ;
183
+ // NOTE - if build starts failing, comment the throw out
184
+ throw err ;
185
+ } )
186
+ ) ;
187
+ }
188
+ }
189
+ visitParents ( tree , checkLinksandDefinitions , replaceLinkWithPreview ) ;
190
+ visitParents ( tree , isStructureLinkReference , replaceLinkWithPreview ) ;
191
+ await Promise . all ( Array . from ( mutations ) ) ;
192
+
39
193
if ( file . path . includes ( '/api/structures/' ) ) {
40
194
let relativePath = `/${ path . relative ( file . cwd , file . path ) } ` ;
41
195
@@ -57,171 +211,8 @@ async function transformer(tree: Parent, file: VFile) {
57
211
fileContent . set ( relativePath , { promise : Promise . resolve ( tree ) } ) ;
58
212
}
59
213
}
60
- structureDefinitions . clear ( ) ;
61
- modifiers . clear ( ) ;
62
- visitParents ( tree , checkLinksandDefinitions , replaceLinkWithPreview ) ;
63
- visitParents ( tree , isStructureLinkReference , replaceLinkWithPreview ) ;
64
214
const importNode = getJSXImport ( 'APIStructurePreview' ) ;
65
- if ( modifiers . size ) {
215
+ if ( mutations . size ) {
66
216
tree . children . unshift ( importNode ) ;
67
217
}
68
- await Promise . all ( Array . from ( modifiers ) ) ;
69
- }
70
-
71
- /**
72
- * This function is the test function for the first pass of the tree visitor.
73
- * Any values returning 'true' will run replaceLinkWithPreview().
74
- *
75
- * As a side effect, this function also puts all reference-style links (definitions)
76
- * for API structures into a Map, which will be used on the second pass.
77
- */
78
- const checkLinksandDefinitions = ( node : Node ) : node is Link => {
79
- if ( isDefinition ( node ) && node . url . includes ( '/api/structures/' ) ) {
80
- structureDefinitions . set ( node . identifier , node . url ) ;
81
- }
82
- if ( isLink ( node ) && node . url . includes ( '/api/structures/' ) ) {
83
- return true ;
84
- }
85
-
86
- return false ;
87
- } ;
88
-
89
- /**
90
- * This function is the test function from the second pass of the tree visitor.
91
- * Any values returning 'true' will run replaceLinkWithPreview().
92
- */
93
- function isStructureLinkReference ( node : Node ) : node is LinkReference {
94
- return isLinkReference ( node ) && structureDefinitions . has ( node . identifier ) ;
95
- }
96
-
97
- function replaceLinkWithPreview ( node : Link | LinkReference , parents : Parent [ ] ) {
98
- // depending on if the node is a direct link or a reference-style link,
99
- // we get its URL differently.
100
- let relativeStructureUrl : string ;
101
- let isInline = false ;
102
- if ( isLink ( node ) ) {
103
- relativeStructureUrl = node . url ;
104
- } else if ( isLinkReference ( node ) ) {
105
- relativeStructureUrl = structureDefinitions . get ( node . identifier ) ;
106
- } else {
107
- return ;
108
- }
109
-
110
- if ( relativeStructureUrl . endsWith ( '?inline' ) ) {
111
- relativeStructureUrl = relativeStructureUrl . split ( '?inline' ) [ 0 ] ;
112
- isInline = true ;
113
- }
114
-
115
- const relativeStructurePath = `${ relativeStructureUrl } .md` ;
116
-
117
- // No file content promise available, so add one and then wait
118
- // on it being resolved when the structure doc is processed
119
- if ( ! fileContent . has ( relativeStructurePath ) ) {
120
- let resolve : ( value : Parent ) => void ;
121
- let reject : ( err : Error ) => void ;
122
-
123
- // Set a timeout as a backstop so we don't deadlock forever if something
124
- // causes content to never be resolved - in theory an upstream change in
125
- // Docusaurus could cause that if they limited how many files are being
126
- // processed in parallel such that too many docs are awaiting others
127
- const timeoutId = setTimeout ( ( ) => {
128
- // links in translated locale [xy] have their paths prefixed with /xy/
129
- const isTranslatedDoc = ! relativeStructurePath . startsWith ( '/docs/' ) ;
130
-
131
- if ( isTranslatedDoc ) {
132
- // If we're running locally we might not have translations downloaded
133
- // so if we don't find it locally just supply the default locale
134
- const [ _fullPath , locale , docPath ] = relativeStructurePath . match (
135
- / \/ ( [ a - z ] [ a - z ] ) \/ d o c s \/ ( .* ) /
136
- ) ;
137
- const defaultLocalePath = `/docs/${ docPath } ` ;
138
- const localeDir = path . join ( __dirname , '..' , '..' , 'i18n' , locale ) ;
139
-
140
- if ( ! fs . existsSync ( localeDir ) ) {
141
- if ( fileContent . has ( defaultLocalePath ) ) {
142
- const { promise } = fileContent . get ( defaultLocalePath ) ;
143
- promise . then ( ( content ) => resolve ( content ) ) ;
144
- return ;
145
- }
146
- }
147
- }
148
-
149
- reject (
150
- new Error (
151
- `Timed out waiting for API structure content from ${ relativeStructurePath } `
152
- )
153
- ) ;
154
- } , 60000 ) ;
155
-
156
- const promise = new Promise < Parent > ( ( resolve_ , reject_ ) => {
157
- resolve = ( value : Parent ) => {
158
- clearTimeout ( timeoutId ) ;
159
- resolve_ ( value ) ;
160
- } ;
161
- reject = reject_ ;
162
- } ) ;
163
-
164
- fileContent . set ( relativeStructurePath , { promise, resolve } ) ;
165
- }
166
-
167
- const { promise } = fileContent . get ( relativeStructurePath ) ;
168
-
169
- if (
170
- ( Array . isArray ( node . children ) &&
171
- node . children . length > 0 &&
172
- isText ( node . children [ 0 ] ) ) ||
173
- isInlineCode ( node . children [ 0 ] )
174
- ) {
175
- modifiers . add (
176
- promise
177
- . then ( ( content ) => {
178
- const title = ( node . children [ 0 ] as Text | InlineCode ) . value ;
179
-
180
- if ( isInline ) {
181
- const siblings = parents [ parents . length - 1 ] . children ;
182
- const filtered = filter (
183
- content ,
184
- ( node ) => node . type !== 'mdxjsEsm' && node . type !== 'heading'
185
- ) ;
186
- visitParents (
187
- filtered ,
188
- checkLinksandDefinitions ,
189
- replaceLinkWithPreview
190
- ) ;
191
- siblings . push ( filtered ) ;
192
- } else {
193
- // replace the Link node with an MDX element in-place
194
- const previewNode = node as unknown as MdxJsxFlowElement ;
195
- previewNode . type = 'mdxJsxFlowElement' ;
196
- previewNode . name = 'APIStructurePreview' ;
197
- previewNode . children = [ ] ;
198
- previewNode . data = {
199
- _mdxExplicitJsx : true ,
200
- } ;
201
- previewNode . attributes = [
202
- {
203
- type : 'mdxJsxAttribute' ,
204
- name : 'url' ,
205
- value : `${ relativeStructureUrl } ` ,
206
- } ,
207
- {
208
- type : 'mdxJsxAttribute' ,
209
- name : 'title' ,
210
- value : title ,
211
- } ,
212
- {
213
- type : 'mdxJsxAttribute' ,
214
- name : 'content' ,
215
- value : JSON . stringify ( content ) ,
216
- } ,
217
- ] ;
218
- }
219
- } )
220
- . catch ( ( err ) => {
221
- logger . error ( err ) ;
222
- // NOTE - if build starts failing, comment the throw out
223
- throw err ;
224
- } )
225
- ) ;
226
- }
227
218
}
0 commit comments