• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16const ts = require('typescript');
17const path = require('path');
18const fs = require('fs');
19
20/**
21 * Directory location to API .d.ts files.
22 */
23const SOURCE_DTS_DIR = process.argv[2];
24
25/**
26 * Directory location to component JSON files.
27 */
28const COMPONENT_OUTPUT_DIR = process.argv[3];
29
30/**
31 * Directory location to form component JSON files.
32 */
33const FORM_COMPONENT_OUTPUT_DIR = process.argv[4];
34
35/**
36 * Map _special_ extend class name to method names under the class.
37 *
38 * @type {Map<string, string[]>}
39 */
40const SPECIAL_EXTEND_ATTRS = new Map();
41
42/**
43 * _special_ extend class names
44 */
45const SPECIAL_EXTEND_ATTR_NAMES = [
46  'DynamicNode',
47  'BaseSpan',
48  'ScrollableCommonMethod',
49  'CommonShapeMethod',
50  'SecurityComponentMethod'
51];
52
53/**
54 * _special_ component data array. Each data contains:
55 *
56 * - componentName: the component JSON file name.
57 * - apiName: the API .d.ts file name.
58 * - className: the class name in the API .d.ts file.
59 * - includeClassName: whether needs to include the class name in the component JSON file.
60 */
61const SPECIAL_COMPONENTS = [
62  {
63    componentName: 'common_attrs',
64    apiName: 'common',
65    className: 'CommonMethod',
66    includeClassName: false,
67  }
68];
69
70/**
71 * API .d.ts file names to skip finding components.
72 */
73const COMPONENT_WHITE_LIST = ['common'];
74
75/**
76 * API .d.ts file names to skip finding form components.
77 */
78const FORM_COMPONENT_WHITE_LIST = ['common'];
79
80registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'common.d.ts'));
81registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'span.d.ts'));
82registerSpecialExtends(path.join(SOURCE_DTS_DIR, 'security_component.d.ts'));
83generateSpecialTargetFiles();
84generateTargetFile(SOURCE_DTS_DIR);
85
86/**
87 * Register _special_ extend classes (i.e. classes except CommonMethod that Component attributes' extends from).
88 *
89 * Registered classes will be recognized later when an Component attribute extends any of them,
90 * then parse those methods in the extend class as the methods of the Component attribute.
91 *
92 * @param {string} filePath File location of the special extend classes.
93 */
94function registerSpecialExtends(filePath) {
95  const content = fs.readFileSync(filePath, 'utf8');
96  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
97
98  ts.forEachChild(sourceFile, (node) => {
99    if (isSpecialExtendClass(node)) {
100      const specialExtendClassName = node.name.getText();
101      const attrs = getAttrs(node, false);
102      SPECIAL_EXTEND_ATTRS.set(specialExtendClassName, attrs);
103    }
104  });
105}
106
107/**
108 * Generate or update component JSON files for the special extend classes (e.g. CommonMethod).
109 */
110function generateSpecialTargetFiles() {
111  SPECIAL_COMPONENTS.forEach((special) => {
112    const { componentName, apiName, className, includeClassName } = special;
113
114    const apiFilePath = path.join(SOURCE_DTS_DIR, `${apiName}.d.ts`);
115
116    const content = fs.readFileSync(apiFilePath, 'utf8');
117    const sourceFile = ts.createSourceFile(apiFilePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
118
119    ts.forEachChild(sourceFile, (node) => {
120      if (!(ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
121        node.name.getText() === className)) {
122        return;
123      }
124
125      const [flags, isForm] = getFlags(node);
126      const component = { attrs: Array.from(new Set(getAttrs(node, false))), ...flags };
127      const formComponent = { attrs: Array.from(new Set(getAttrs(node, isForm))), ...flags };
128
129      if (includeClassName) {
130        component.name = className;
131        formComponent.name = className;
132      }
133
134      generateFormAndComponents(componentName, component, formComponent, isForm);
135    });
136  });
137}
138
139/**
140 * Generate or update component JSON files for the Component attributes.
141 *
142 * @param {string} filePath Directory location of API .d.ts files.
143 */
144function generateTargetFile(filePath) {
145  const files = [];
146  readFile(filePath, files);
147
148  const program = ts.createProgram(files, {});
149  const checker = program.getTypeChecker();
150  const sourceFiles = program.getSourceFiles()
151    .filter((f) => files.includes(toUnixPath(f.fileName)));
152
153  sourceFiles.forEach((sourceFile) => {
154    const sourceFilePath = path.parse(toUnixPath(sourceFile.fileName));
155    const baseName = path.basename(sourceFilePath.name, path.extname(sourceFilePath.name));
156
157    const [component, formComponent, isForm] = findComponent(sourceFile, checker, program);
158
159    Object.keys(component).length > 0 &&
160      generateFormAndComponents(baseName, component, formComponent, isForm);
161  });
162}
163
164/**
165 * Generate or update component JSON files for form and non-form components.
166 *
167 * @param {string} componentName Component name.
168 * @param {object} component Component JSON data.
169 * @param {object} formComponent Form component JSON data.
170 * @param {boolean} isForm Whether the component can be a form component.
171 */
172function generateFormAndComponents(componentName, component, formComponent, isForm) {
173  if (!COMPONENT_WHITE_LIST.includes(componentName)) {
174    if (isForm && !FORM_COMPONENT_WHITE_LIST.includes(componentName)) {
175      const formComponentFileName = path.join(FORM_COMPONENT_OUTPUT_DIR, `${componentName}.json`);
176      generateComponentJSONFile(formComponentFileName, formComponent);
177    }
178    const componentFileName = path.join(COMPONENT_OUTPUT_DIR, `${componentName}.json`);
179    generateComponentJSONFile(componentFileName, component);
180  }
181}
182
183/**
184 * Write file utility for generating or updating component JSON files.
185 *
186 * This only updates the `attrs` attribute in the JSON file.
187 *
188 * @param {string} filePath File location to write the component JSON file.
189 * @param {object} component component JSON object.
190 */
191function generateComponentJSONFile(filePath, component) {
192  if (!fs.existsSync(filePath)) {
193    fs.writeFileSync(path.resolve(filePath), JSON.stringify(component, null, 2));
194    return;
195  }
196  const source = JSON.parse(fs.readFileSync(filePath, 'utf8'));
197  const updateComponent = { ...source, ...component };
198  fs.writeFileSync(path.resolve(filePath), JSON.stringify(updateComponent, null, 2));
199}
200
201/**
202 * Read file utility for reading all files in the directory `dir`.
203 *
204 * Add all file pathes to the array `fileDir`.
205 *
206 * @param {string} dir Directory location for reading all files.
207 * @param {string[]} fileDir All file pathes array.
208 */
209function readFile(dir, fileDir) {
210  const files = fs.readdirSync(dir);
211  files.forEach((element) => {
212    const filePath = path.join(dir, element);
213    const status = fs.statSync(filePath);
214    if (status.isDirectory()) {
215      readFile(filePath, fileDir);
216    } else {
217      fileDir.push(path.resolve(filePath));
218    }
219  });
220}
221
222/**
223 * Find any Component attribute in the API .d.ts file.
224 *
225 * @param {ts.SourceFile} sourceFile .d.ts file.
226 * @param {ts.TypeChecker} checker type checker.
227 * @param {ts.Program} program program.
228 * @returns {[object, object, boolean]} A component JSON object, a form component JSON object,
229 * and a boolean to indicate whether the Component attribute can be a form component.
230 */
231function findComponent(sourceFile, checker, program) {
232  let component = {};
233  let formComponent = {};
234  let isForm = false;
235
236  ts.forEachChild(sourceFile, (node) => {
237    if (isClass(node)) {
238      const [flags, _isForm] = getFlags(node);
239      component = {
240        name: node.name.getText().replace(/Attribute$/, ''),
241        attrs: Array.from(new Set([
242          ...getAttrs(node, false),
243          ...getCommonAttrs(node)
244        ])),
245        ...flags
246      };
247      formComponent = {
248        name: node.name.getText().replace(/Attribute$/, ''),
249        attrs: Array.from(new Set([
250          ...getAttrs(node, _isForm),
251          ...getCommonAttrs(node)
252        ])),
253        ...flags
254      };
255      isForm = _isForm;
256    }
257  });
258
259  return [component, formComponent, isForm];
260}
261
262/**
263 * Check whether a AST Node is a class.
264 *
265 * @param {ts.Node} node
266 * @returns {boolean}
267 */
268function isClass(node) {
269  return ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
270    /Attribute$/.test(node.name.getText());
271}
272
273/**
274 * Check whether a AST Node is a _special_ extend class.
275 *
276 * @param {ts.Node} node
277 * @returns {boolean}
278 */
279function isSpecialExtendClass(node) {
280  return ts.isClassDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
281    SPECIAL_EXTEND_ATTR_NAMES.includes(node.name.getText());
282}
283
284/**
285 * Check whether a AST Node is a method.
286 *
287 * @param {ts.Node} node
288 * @returns {boolean}
289 */
290function isMethod(node) {
291  return ts.isMethodDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
292    node.name.escapedText;
293}
294
295/**
296 * Check whether a AST Node is a class and extends any _special_ extend class.
297 *
298 * @param {ts.Node} node
299 * @returns {boolean}
300 */
301function isExtendSpecialDeclaration(node) {
302  return ts.isClassDeclaration(node) && node.heritageClauses && node.heritageClauses.length > 0;
303}
304
305/**
306 * Check whether a method belongs to the form component.
307 *
308 * @param {ts.Node} node
309 * @returns {boolean}
310 */
311function isFormComponent(node) {
312  const flags = getFlags(node);
313  return flags && flags[1];
314}
315
316/**
317 * Get all identifiers that this class extends.
318 *
319 * @param {ts.Node} node
320 * @returns {ts.Expression[]}
321 */
322function getExtendIdentifiers(node) {
323  if (!ts.isHeritageClause(node)) {
324    return [];
325  }
326
327  const identifiers = [];
328  node.types.forEach((type) => {
329    if (ts.isExpressionWithTypeArguments(type) && type.expression) {
330      identifiers.push(type.expression);
331    }
332  });
333
334  return identifiers;
335}
336
337/**
338 * Get all method names from _special_ extend class.
339 *
340 * @param {ts.ClassDeclaration} node
341 * @returns {string[]}
342 */
343function getCommonAttrs(node) {
344  if (!isExtendSpecialDeclaration(node)) {
345    return [];
346  }
347
348  const attrs = [];
349  const heritageClause = node.heritageClauses[0];
350  const identifiers = getExtendIdentifiers(heritageClause);
351  identifiers.forEach((identifier) => {
352    if (SPECIAL_EXTEND_ATTRS.has(identifier.escapedText)) {
353      attrs.push(...SPECIAL_EXTEND_ATTRS.get(identifier.escapedText));
354    }
355  });
356  return attrs;
357}
358
359/**
360 * Get all method names from the class (without any method names from extend class).
361 *
362 * @param {ts.ClassDeclaration} node
363 * @param {boolean} shouldFilterForm Whether needs to filter any method names that belongs to form component.
364 * @returns {string[]}
365 */
366function getAttrs(node, shouldFilterForm) {
367  const attrs = [];
368
369  ts.forEachChild(node, (child) => {
370    if (isMethod(child) && (!shouldFilterForm || isFormComponent(child))) {
371      const attrName = child.name.escapedText;
372      attrs.push(attrName);
373    }
374  });
375
376  return attrs;
377}
378
379/**
380 * Get flags and whether is form component boolean.
381 *
382 * @param {ts.Node} node
383 * @returns {[string[], boolean]} Flag names, whether is form component.
384 */
385function getFlags(node) {
386  const tags = parseTags(node);
387  const flags = filterFlags(tags);
388
389  if (flags.form) {
390    delete flags.form;
391    return [flags, true];
392  }
393  return [flags, false];
394}
395
396/**
397 * Parse all JSDoc tags from a AST Node.
398 *
399 * @param {ts.Node} node
400 * @returns {string[]}
401 */
402function parseTags(node) {
403  const tags = [];
404  const jsTags = ts.getJSDocTags(node);
405
406  jsTags.forEach((jsTag) => {
407    tags.push(jsTag.tagName.getText());
408  });
409  return tags;
410}
411
412/**
413 * Filter useful tags from a tag array.
414 *
415 * Currently, we only parse `@form`.
416 *
417 * @param {string} tags
418 * @returns {object}
419 */
420function filterFlags(tags) {
421  let form;
422
423  tags.forEach((tag) => {
424    const name = typeof tag === 'string' ? tag : tag?.title;
425
426    if (name) {
427      if (name === 'form') {
428        form = true;
429      }
430    }
431  });
432
433  return {
434    ...(form && { form }),
435  };
436}
437
438/**
439 * Convert file path format to linux-style.
440 *
441 * @param {string} data file location.
442 * @returns {string}
443 */
444function toUnixPath(data) {
445  if (/^win/.test(require('os').platform())) {
446    const fileTmps = data.split(path.sep);
447    const newData = path.posix.join(...fileTmps);
448    return newData;
449  }
450  return data;
451}
452