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