|
12 | 12 |
|
13 | 13 | /* eslint-disable no-restricted-syntax, import/no-extraneous-dependencies */
|
14 | 14 |
|
15 |
| -const fs = require('fs'); |
16 |
| -const path = require('path'); |
17 |
| - |
18 | 15 | const Parser = require('tree-sitter');
|
19 | 16 | const JavaScript = require('tree-sitter-javascript');
|
20 | 17 | const JSDoc = require('tree-sitter-jsdoc');
|
21 |
| -const Vue = require('tree-sitter-vue'); |
22 |
| - |
23 |
| -async function* find(dir, predicate = () => true) { |
24 |
| - const files = await fs.promises.readdir(dir); |
25 |
| - |
26 |
| - for await (const file of files) { |
27 |
| - const fpath = path.join(dir, file); |
28 |
| - const fstat = await fs.promises.stat(fpath); |
29 |
| - |
30 |
| - if (fstat.isDirectory()) { |
31 |
| - yield* find(fpath, predicate); |
32 |
| - } else if (predicate(fpath)) { |
33 |
| - yield fpath; |
34 |
| - } |
35 |
| - } |
36 |
| -} |
37 |
| - |
38 |
| -async function* findVueFiles(dir) { |
39 |
| - const isVueFile = fpath => path.extname(fpath) === '.vue'; |
40 |
| - yield* find(dir, isVueFile); |
41 |
| -} |
42 |
| - |
43 |
| -function uniqueCaptures(query, tree) { |
44 |
| - const capturesArray = query.captures(tree.rootNode); |
45 |
| - return capturesArray.reduce((obj, capture) => ({ |
46 |
| - ...obj, |
47 |
| - [capture.name]: capture.node, |
48 |
| - }), {}); |
49 |
| -} |
50 |
| - |
51 |
| -const line = ( |
52 |
| - text, |
53 |
| - range = { |
54 |
| - start: { // FIXME: use real ranges from parser in the future |
55 |
| - line: 0, |
56 |
| - character: 0, |
57 |
| - }, |
58 |
| - end: { |
59 |
| - line: 0, |
60 |
| - character: 0, |
61 |
| - }, |
62 |
| - }, |
63 |
| -) => ({ text, range }); |
64 |
| - |
65 |
| -function createDocComment(descriptionNode = { text: '' }, params = []) { |
66 |
| - const lines = descriptionNode.text.split('\n').map((txt, i) => line((i === 0 ? ( |
67 |
| - txt |
68 |
| - ) : ( |
69 |
| - // this seems like a tree-sitter-jsdoc bug with handling |
70 |
| - // multi-line descriptions |
71 |
| - txt.replace(/^\s*\*/, '') |
72 |
| - )))); |
73 |
| - |
74 |
| - // generate automated parameter description content that just shows the name |
75 |
| - // of each prop and its type |
76 |
| - const hasParamContent = lines.some(l => /^\s*- Parameter/.test(l.text)); |
77 |
| - if (params.length && !hasParamContent) { |
78 |
| - lines.push(line('')); |
79 |
| - lines.push(line('- Parameters:')); |
80 |
| - params.forEach((param) => { |
81 |
| - lines.push(line(` - ${param.name}: \`${param.type}\``)); |
82 |
| - }); |
83 |
| - } |
84 |
| - |
85 |
| - return { lines }; |
86 |
| -} |
87 |
| - |
88 |
| -const Token = { |
89 |
| - identifier: spelling => ({ kind: 'identifier', spelling }), |
90 |
| - string: spelling => ({ kind: 'string', spelling }), |
91 |
| - text: spelling => ({ kind: 'text', spelling }), |
92 |
| - typeIdentifier: spelling => ({ kind: 'typeIdentifier', spelling }), |
93 |
| -}; |
94 |
| - |
95 |
| -function createDeclaration(componentName, slotNames = []) { |
96 |
| - if (!slotNames.length) { |
97 |
| - return [ |
98 |
| - Token.text('<'), |
99 |
| - Token.typeIdentifier(componentName), |
100 |
| - Token.text(' />'), |
101 |
| - ]; |
102 |
| - } |
103 |
| - const isDefault = name => name === 'default'; |
104 |
| - return [ |
105 |
| - Token.text('<'), |
106 |
| - Token.typeIdentifier(componentName), |
107 |
| - Token.text('>\n'), |
108 |
| - ...slotNames.flatMap(name => (isDefault(name) ? ([ |
109 |
| - Token.text(' <slot />\n'), |
110 |
| - ]) : ([ |
111 |
| - Token.text(' <slot name='), |
112 |
| - Token.string(`"${name}"`), |
113 |
| - Token.text(' />\n'), |
114 |
| - ]))), |
115 |
| - Token.text('</'), |
116 |
| - Token.typeIdentifier(componentName), |
117 |
| - Token.text('>'), |
118 |
| - ]; |
119 |
| -} |
120 | 18 |
|
121 | 19 | (async () => {
|
122 |
| - const vueParser = new Parser(); |
123 |
| - vueParser.setLanguage(Vue); |
124 |
| - const scriptTextQuery = new Parser.Query(Vue, |
125 |
| - `(script_element |
126 |
| - (raw_text) @script)`); |
127 |
| - |
128 | 20 | const jsParser = new Parser();
|
129 | 21 | jsParser.setLanguage(JavaScript);
|
130 |
| - const exportNameQuery = new Parser.Query(JavaScript, |
131 |
| - `( |
132 |
| - (comment)? @comment (#match? @comment "^/[*]{2}") |
133 |
| - . |
134 |
| - (export_statement |
135 |
| - (object |
136 |
| - (pair |
137 |
| - (property_identifier) @key (#eq? @key "name") |
138 |
| - . |
139 |
| - (string (string_fragment) @component)))))`); |
140 |
| - const exportPropsQuery = new Parser.Query(JavaScript, |
141 |
| - `(export_statement |
142 |
| - (object |
143 |
| - (pair |
144 |
| - (property_identifier) @key (#eq? @key "props") |
145 |
| - . |
146 |
| - (object |
147 |
| - (pair |
148 |
| - (property_identifier) @prop.name |
149 |
| - . |
150 |
| - (object |
151 |
| - (pair |
152 |
| - (property_identifier) @key2 (#eq? @key2 "type") |
153 |
| - . |
154 |
| - (_) @prop.type)))))))`); |
155 | 22 |
|
156 | 23 | const jsDocParser = new Parser();
|
157 | 24 | jsDocParser.setLanguage(JSDoc);
|
158 |
| - const commentDescriptionQuery = new Parser.Query(JSDoc, |
159 |
| - `(document |
160 |
| - (description) @description)`); |
161 |
| - const slotsQuery = new Parser.Query(Vue, |
162 |
| - `[ |
163 |
| - (self_closing_tag |
164 |
| - (tag_name) @tag |
165 |
| - (attribute |
166 |
| - (attribute_name) @attr.name |
167 |
| - (quoted_attribute_value (attribute_value) @attr.value))? |
168 |
| - (#eq? @tag "slot") |
169 |
| - (#eq? @attr.name "name")) |
170 |
| - (start_tag |
171 |
| - (tag_name) @tag |
172 |
| - (attribute |
173 |
| - (attribute_name) @attr.name |
174 |
| - (quoted_attribute_value (attribute_value) @attr.value))? |
175 |
| - (#eq? @tag "slot") |
176 |
| - (#eq? @attr.name "name")) |
177 |
| - ]`); |
178 | 25 |
|
179 | 26 | const symbols = [];
|
180 | 27 | const relationships = [];
|
181 |
| - const identifiers = new Set(); |
182 |
| - |
183 |
| - const rootDir = path.join(__dirname, '..'); |
184 |
| - const componentsDir = path.join(rootDir, 'src/components'); |
185 |
| - for await (const filepath of findVueFiles(componentsDir)) { |
186 |
| - const contents = await fs.promises.readFile(filepath, { encoding: 'utf8' }); |
187 |
| - const vueTree = vueParser.parse(contents); |
188 |
| - |
189 |
| - const { script } = uniqueCaptures(scriptTextQuery, vueTree); |
190 |
| - if (script) { |
191 |
| - const jsTree = jsParser.parse(script.text); |
192 |
| - |
193 |
| - const { comment, component } = uniqueCaptures(exportNameQuery, jsTree); |
194 |
| - |
195 |
| - if (component) { |
196 |
| - const componentName = component.text; |
197 |
| - const pathComponents = filepath |
198 |
| - .replace(componentsDir, '') |
199 |
| - .split('/') |
200 |
| - .filter(part => part.length) |
201 |
| - .map(part => path.parse(part).name); |
202 |
| - const preciseIdentifier = pathComponents.join(''); |
203 |
| - |
204 |
| - const subHeading = [ |
205 |
| - Token.text('<'), |
206 |
| - Token.identifier(componentName), |
207 |
| - Token.text('>'), |
208 |
| - ]; |
209 |
| - |
210 |
| - let functionSignature; |
211 |
| - const captures = exportPropsQuery.captures(jsTree.rootNode); |
212 |
| - const params = captures.reduce((memo, capture) => { |
213 |
| - if (capture.name === 'prop.name') { |
214 |
| - memo.push({ name: capture.node.text }); |
215 |
| - } |
216 |
| - if (capture.name === 'prop.type') { |
217 |
| - // eslint-disable-next-line no-param-reassign |
218 |
| - memo[memo.length - 1].type = capture.node.text; |
219 |
| - } |
220 |
| - return memo; |
221 |
| - }, []); |
222 |
| - if (params.length) { |
223 |
| - // not sure if DocC actually uses `functionSignature` or not... |
224 |
| - functionSignature = { |
225 |
| - parameters: params.map(param => ({ |
226 |
| - name: param.name, |
227 |
| - declarationFragments: [Token.text(param.type)], |
228 |
| - })), |
229 |
| - }; |
230 |
| - } |
231 |
| - |
232 |
| - // TODO: eventually we should also capture slots that are expressed in |
233 |
| - // a render function instead of the template |
234 |
| - const slots = slotsQuery.captures(vueTree.rootNode).reduce((memo, capture) => { |
235 |
| - if (capture.name === 'tag') { |
236 |
| - memo.push({ name: 'default' }); |
237 |
| - } |
238 |
| - if (capture.name === 'attr.value') { |
239 |
| - // eslint-disable-next-line no-param-reassign |
240 |
| - memo[memo.length - 1].name = capture.node.text; |
241 |
| - } |
242 |
| - return memo; |
243 |
| - }, []); |
244 |
| - const slotNames = [...new Set(slots.map(slot => slot.name))]; |
245 |
| - const declarationFragments = createDeclaration(componentName, slotNames); |
246 |
| - |
247 |
| - let docComment; |
248 |
| - let description; |
249 |
| - if (comment) { |
250 |
| - const jsDocTree = jsDocParser.parse(comment.text); |
251 |
| - description = uniqueCaptures(commentDescriptionQuery, jsDocTree).description; |
252 |
| - } |
253 |
| - if (!!description || params.length) { |
254 |
| - docComment = createDocComment(description, params); |
255 |
| - } |
256 |
| - |
257 |
| - symbols.push({ |
258 |
| - accessLevel: 'public', |
259 |
| - identifier: { |
260 |
| - interfaceLanguage: 'vue', |
261 |
| - precise: preciseIdentifier, |
262 |
| - }, |
263 |
| - kind: { |
264 |
| - identifier: 'class', // FIXME |
265 |
| - displayName: 'Component', |
266 |
| - }, |
267 |
| - names: { |
268 |
| - title: componentName, |
269 |
| - subHeading, |
270 |
| - }, |
271 |
| - pathComponents, |
272 |
| - docComment, |
273 |
| - declarationFragments, |
274 |
| - functionSignature, |
275 |
| - }); |
276 |
| - identifiers.add(preciseIdentifier); |
277 |
| - } |
278 |
| - } |
279 |
| - } |
280 |
| - |
281 |
| - // construct parent/child relationships and fixup the `pathComponents` for |
282 |
| - // each symbol so that it only contains items that map to real symbols (TODO: |
283 |
| - // this could probably be done in the first loop depending on the order that |
284 |
| - // `find` traverses the filesystem (breadth vs depth)) |
285 |
| - for (let i = 0; i < symbols.length; i += 1) { |
286 |
| - const symbol = symbols[i]; |
287 |
| - const { |
288 |
| - identifier: { precise: childIdentifier }, |
289 |
| - pathComponents, |
290 |
| - } = symbol; |
291 |
| - const parentPathComponents = pathComponents.slice(0, pathComponents.length - 1); |
292 |
| - if (!parentPathComponents.length) { |
293 |
| - // eslint-disable-next-line no-continue |
294 |
| - continue; |
295 |
| - } |
296 |
| - |
297 |
| - const parentIdentifier = parentPathComponents.join(''); |
298 |
| - if (identifiers.has(parentIdentifier)) { |
299 |
| - relationships.push({ |
300 |
| - source: childIdentifier, |
301 |
| - target: parentIdentifier, |
302 |
| - kind: 'memberOf', |
303 |
| - }); |
304 |
| - } else { |
305 |
| - symbol.pathComponents = pathComponents.filter((_, j) => ( |
306 |
| - identifiers.has(pathComponents.slice(0, j + 1).join('')) |
307 |
| - )); |
308 |
| - } |
309 |
| - } |
310 | 28 |
|
311 | 29 | const formatVersion = {
|
312 | 30 | major: 0,
|
|
0 commit comments