/
githubmirror
/
components
Обзор
Документация
Войти
/
githubmirror
/
components
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
v21.2.14
scripts/detect-component-id-collisions.mts
204 строки
7 KB
Kristiyan Kostadinov
build: update chalk
24 июн 2025, 13:14
24 июн 2025, 13:14
0d19121
Код
Авторство
О чём код?
import ts from 'typescript'; import chalk from 'chalk'; import {readFileSync} from 'fs'; import {join} from 'path'; import {sync as glob} from 'glob'; // This script aims to detect if the unique IDs of two components may collide at runtime // in order to avoid issues like https://github.com/angular/components/issues/27163. const errors: string[] = []; const seenMetadata = new Map<string, ts.ClassDeclaration>(); const fileToCheck = join(process.cwd(), 'src/**/!(*.spec).ts'); const ignoredPatterns = [ '**/components-examples/**', '**/dev-app/**', '**/e2e-app/**', '**/universal-app/**', ]; // Use glob + createSourceFile since we don't need type information // and this generally faster than creating a program. glob(fileToCheck, {absolute: true, ignore: ignoredPatterns}) .map(path => ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, true)) .forEach(sourceFile => { sourceFile.statements.forEach(statement => { if (ts.isClassDeclaration(statement)) { validateClass(statement); } }); }); if (errors.length) { console.error(chalk.red('Detected identical metadata between following components:')); errors.forEach(err => console.error(chalk.red(err))); console.error( chalk.red( `\nThe metadata of one of each of these components should be ` + `changed to be slightly different in order to avoid conflicts at runtime.\n`, ), ); process.exit(1); } /** Validates if a class will conflict with any of the classes that have been checked so far. */ function validateClass(node: ts.ClassDeclaration): void { const metadata = getComponentMetadata(node); if (metadata) { // Create an ID for the component based on its metadata. This is based on what the framework // does at runtime at https://github.com/angular/angular/blob/main/packages/core/src/render3/definition.ts#L679. // Note that the behavior isn't exactly the same, because the framework uses some fields that // are generated by the compiler based on the component's template. const key = [ serializeField('selector', metadata), serializeField('host', metadata), serializeField('encapsulation', metadata), serializeField('standalone', metadata), serializeField('signals', metadata), serializeField('exportAs', metadata), serializeBindings(node, metadata, 'inputs', 'Input'), serializeBindings(node, metadata, 'outputs', 'Output'), ].join('|'); if (seenMetadata.has(key)) { errors.push(`- ${node.name?.text} and ${seenMetadata.get(key)!.name?.text}`); } else { seenMetadata.set(key, node); } } } /** Serializes a field of an object literal node to a string. */ function serializeField(name: string, metadata: ts.ObjectLiteralExpression): string { const prop = findPropAssignment(name, metadata); return prop ? serializeValue(prop.initializer).trim() : '<none>'; } /** Extracts the input/output bindings of a component and serializes them into a string. */ function serializeBindings( node: ts.ClassDeclaration, metadata: ts.ObjectLiteralExpression, metaName: string, decoratorName: string, ): string { const bindings: Record<string, string> = {}; const metaProp = findPropAssignment(metaName, metadata); if (metaProp && ts.isArrayLiteralExpression(metaProp.initializer)) { metaProp.initializer.elements.forEach(el => { if (ts.isStringLiteralLike(el)) { const [name, alias] = el.text.split(':').map(p => p.trim()); bindings[alias || name] = name; } else if (ts.isObjectLiteralExpression(el)) { const name = findPropAssignment('name', el); const alias = findPropAssignment('alias', el); if (name && ts.isStringLiteralLike(name.initializer)) { const publicName = alias && ts.isStringLiteralLike(alias.initializer) ? alias.initializer.text : name.initializer.text; bindings[publicName] = name.initializer.text; } } }); } node.members.forEach(member => { if (!ts.isPropertyDeclaration(member) || !ts.isIdentifier(member.name)) { return; } const decorator = findDecorator(decoratorName, member); if (decorator) { const publicName = decorator.expression.arguments.length > 0 && ts.isStringLiteralLike(decorator.expression.arguments[0]) ? decorator.expression.arguments[0].text : member.name.text; bindings[publicName] = member.name.text; } }); return JSON.stringify(bindings); } /** Serializes a single value to a string. */ function serializeValue(node: ts.Node): string { if (ts.isStringLiteralLike(node) || ts.isIdentifier(node)) { return node.text; } if (ts.isArrayLiteralExpression(node)) { return JSON.stringify(node.elements.map(serializeValue)); } if (ts.isObjectLiteralExpression(node)) { const serialized = node.properties .slice() // Sort the fields since JS engines preserve the order properties in object literals. .sort((a, b) => (a.name?.getText() || '').localeCompare(b.name?.getText() || '')) .reduce( (accumulator, prop) => { if (ts.isPropertyAssignment(prop)) { accumulator[prop.name.getText()] = serializeValue(prop.initializer); } return accumulator; }, {} as Record<string, string>, ); return JSON.stringify(serialized); } return node.getText(); } /** Gets the object literal containing the Angular component metadata of a class. */ function getComponentMetadata(node: ts.ClassDeclaration): ts.ObjectLiteralExpression | null { const decorator = findDecorator('Component', node); if (!decorator) { return null; } if ( decorator.expression.arguments.length === 0 || !ts.isObjectLiteralExpression(decorator.expression.arguments[0]) ) { throw new Error( `Cannot analyze class ${node.name?.text || 'Anonymous'} in ${node.getSourceFile().fileName}.`, ); } return decorator.expression.arguments[0]; } /** Finds a decorator with a specific name on a node. */ function findDecorator(name: string, node: ts.HasDecorators) { return ts .getDecorators(node) ?.find( current => ts.isCallExpression(current.expression) && ts.isIdentifier(current.expression.expression) && current.expression.expression.text === name, ) as (ts.Decorator & {expression: ts.CallExpression}) | undefined; } /** Finds a specific property of an object literal node. */ function findPropAssignment( name: string, literal: ts.ObjectLiteralExpression, ): ts.PropertyAssignment | undefined { return literal.properties.find( current => ts.isPropertyAssignment(current) && ts.isIdentifier(current.name) && current.name.text === name, ) as ts.PropertyAssignment | undefined; }