|
| 1 | +/*! |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.dev/license |
| 7 | + */ |
| 8 | + |
| 9 | +import * as fs from 'fs'; |
| 10 | +import {readFile, writeFile} from 'fs/promises'; |
| 11 | +import {join, relative} from 'path'; |
| 12 | +import ts from 'typescript'; |
| 13 | + |
| 14 | +const [examplesDir, templateFilePath, outputFilePath] = process.argv.slice(2); |
| 15 | + |
| 16 | +const TYPESCRIPT_EXTENSION = '.ts'; |
| 17 | +const SKIP_FILES_WITH_EXTENSIONS = ['.e2e-spec.ts', '.spec.ts', '.po.ts']; |
| 18 | +const EXAMPLES_PATH = `../../content/examples`; |
| 19 | + |
| 20 | +interface File { |
| 21 | + path: string; |
| 22 | + content: string; |
| 23 | +} |
| 24 | + |
| 25 | +interface AnalyzedFiles { |
| 26 | + path: string; |
| 27 | + componentNames: string[]; |
| 28 | +} |
| 29 | + |
| 30 | +main(); |
| 31 | + |
| 32 | +async function main() { |
| 33 | + const files = await retrieveAllTypescriptFiles( |
| 34 | + examplesDir, |
| 35 | + (path) => !SKIP_FILES_WITH_EXTENSIONS.some((extensionToSkip) => path.endsWith(extensionToSkip)), |
| 36 | + ); |
| 37 | + |
| 38 | + const filesWithComponent = files |
| 39 | + .map((file) => ({ |
| 40 | + componentNames: analyzeFile(file), |
| 41 | + path: file.path, |
| 42 | + })) |
| 43 | + .filter((result) => result.componentNames.length > 0); |
| 44 | + |
| 45 | + const previewsComponentMap = generatePreviewsComponentMap(filesWithComponent); |
| 46 | + |
| 47 | + await writeFile(outputFilePath, previewsComponentMap); |
| 48 | +} |
| 49 | + |
| 50 | +/** Recursively search the provided directory for all typescript files and asynchronously load them. */ |
| 51 | +function retrieveAllTypescriptFiles( |
| 52 | + baseDir: string, |
| 53 | + predicateFn: (path: string) => boolean, |
| 54 | +): Promise<File[]> { |
| 55 | + const typescriptFiles: Promise<File>[] = []; |
| 56 | + |
| 57 | + const checkFilesInDirectory = (dir: string) => { |
| 58 | + const files = fs.readdirSync(dir, {withFileTypes: true}); |
| 59 | + for (const file of files) { |
| 60 | + const fullPathToFile = join(dir, file.name); |
| 61 | + const relativeFilePath = relative(baseDir, fullPathToFile); |
| 62 | + |
| 63 | + if ( |
| 64 | + file.isFile() && |
| 65 | + file.name.endsWith(TYPESCRIPT_EXTENSION) && |
| 66 | + predicateFn(relativeFilePath) |
| 67 | + ) { |
| 68 | + typescriptFiles.push( |
| 69 | + readFile(fullPathToFile, {encoding: 'utf-8'}).then((fileContent) => { |
| 70 | + return { |
| 71 | + path: relativeFilePath, |
| 72 | + content: fileContent, |
| 73 | + }; |
| 74 | + }), |
| 75 | + ); |
| 76 | + } else if (file.isDirectory()) { |
| 77 | + checkFilesInDirectory(fullPathToFile); |
| 78 | + } |
| 79 | + } |
| 80 | + }; |
| 81 | + |
| 82 | + checkFilesInDirectory(baseDir); |
| 83 | + |
| 84 | + return Promise.all(typescriptFiles); |
| 85 | +} |
| 86 | + |
| 87 | +/** Returns list of the `Standalone` @Component class names for given file */ |
| 88 | +function analyzeFile(file: File): string[] { |
| 89 | + const componentClassNames: string[] = []; |
| 90 | + const sourceFile = ts.createSourceFile(file.path, file.content, ts.ScriptTarget.Latest, false); |
| 91 | + |
| 92 | + const visitNode = (node: ts.Node): void => { |
| 93 | + if (ts.isClassDeclaration(node)) { |
| 94 | + const decorators = ts.getDecorators(node); |
| 95 | + const componentName = node.name ? node.name.text : null; |
| 96 | + |
| 97 | + if (decorators && decorators.length) { |
| 98 | + for (const decorator of decorators) { |
| 99 | + const call = decorator.expression; |
| 100 | + |
| 101 | + if ( |
| 102 | + ts.isCallExpression(call) && |
| 103 | + ts.isIdentifier(call.expression) && |
| 104 | + call.expression.text === 'Component' && |
| 105 | + call.arguments.length > 0 && |
| 106 | + ts.isObjectLiteralExpression(call.arguments[0]) |
| 107 | + ) { |
| 108 | + const standaloneProperty = call.arguments[0].properties.find( |
| 109 | + (property) => |
| 110 | + property.name && |
| 111 | + ts.isIdentifier(property.name) && |
| 112 | + property.name.text === 'standalone', |
| 113 | + ); |
| 114 | + |
| 115 | + const isStandalone = |
| 116 | + !standaloneProperty || |
| 117 | + (ts.isPropertyAssignment(standaloneProperty) && |
| 118 | + standaloneProperty.initializer.kind === ts.SyntaxKind.TrueKeyword); |
| 119 | + |
| 120 | + if (isStandalone && componentName) { |
| 121 | + componentClassNames.push(componentName); |
| 122 | + } |
| 123 | + } |
| 124 | + } |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + ts.forEachChild(node, visitNode); |
| 129 | + }; |
| 130 | + |
| 131 | + visitNode(sourceFile); |
| 132 | + |
| 133 | + return componentClassNames; |
| 134 | +} |
| 135 | + |
| 136 | +function generatePreviewsComponentMap(data: AnalyzedFiles[]): string { |
| 137 | + let result = ''; |
| 138 | + for (const fileData of data) { |
| 139 | + for (const componentName of fileData.componentNames) { |
| 140 | + const key = `adev/src/content/examples/${fileData.path}${ |
| 141 | + fileData.componentNames.length > 1 ? '_' + componentName : '' |
| 142 | + }`.replace(/\\/g, '/'); |
| 143 | + result += `['${key}']: () => import('${EXAMPLES_PATH}/${fileData.path |
| 144 | + .replace(/\\/g, '/') |
| 145 | + .replace('.ts', '')}').then(c => c.${componentName}),\n`; |
| 146 | + } |
| 147 | + } |
| 148 | + return fs.readFileSync(templateFilePath, 'utf8').replace(/\${previewsComponents}/g, result); |
| 149 | +} |
0 commit comments