/
githubmirror
/
angular-cli
Обзор
Документация
Войти
/
githubmirror
/
angular-cli
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/ngtools/webpack/src/transformers/replace_resources.ts
369 строк
10 KB
Kristiyan Kostadinov
refactor(@angular/build): remove deprecated signature usage
22 сен 2025, 23:46
22 сен 2025, 23:46
06b431f
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import * as ts from 'typescript'; import { InlineAngularResourceLoaderPath } from '../loaders/inline-resource'; export const NG_COMPONENT_RESOURCE_QUERY = 'ngResource'; export function replaceResources( shouldTransform: (fileName: string) => boolean, getTypeChecker: () => ts.TypeChecker, inlineStyleFileExtension?: string, ): ts.TransformerFactory<ts.SourceFile> { return (context: ts.TransformationContext) => { const typeChecker = getTypeChecker(); const resourceImportDeclarations: ts.ImportDeclaration[] = []; const moduleKind = context.getCompilerOptions().module; const nodeFactory = context.factory; const visitNode: ts.Visitor = (node: ts.Node) => { if (ts.isClassDeclaration(node)) { const decorators = ts.getDecorators(node); if (!decorators || decorators.length === 0) { return node; } return nodeFactory.updateClassDeclaration( node, [ ...decorators.map((current) => visitDecorator( nodeFactory, current, typeChecker, resourceImportDeclarations, moduleKind, inlineStyleFileExtension, ), ), ...(ts.getModifiers(node) ?? []), ], node.name, node.typeParameters, node.heritageClauses, node.members, ); } return ts.visitEachChild(node, visitNode, context); }; return (sourceFile: ts.SourceFile) => { if (!shouldTransform(sourceFile.fileName)) { return sourceFile; } const updatedSourceFile = ts.visitNode(sourceFile, visitNode) as ts.SourceFile; if (resourceImportDeclarations.length) { // Add resource imports return context.factory.updateSourceFile( updatedSourceFile, ts.setTextRange( context.factory.createNodeArray([ ...resourceImportDeclarations, ...updatedSourceFile.statements, ]), updatedSourceFile.statements, ), ); } return updatedSourceFile; }; }; } function visitDecorator( nodeFactory: ts.NodeFactory, node: ts.Decorator, typeChecker: ts.TypeChecker, resourceImportDeclarations: ts.ImportDeclaration[], moduleKind?: ts.ModuleKind, inlineStyleFileExtension?: string, ): ts.Decorator { if (!isComponentDecorator(node, typeChecker)) { return node; } if (!ts.isCallExpression(node.expression)) { return node; } const decoratorFactory = node.expression; const args = decoratorFactory.arguments; if (args.length !== 1 || !ts.isObjectLiteralExpression(args[0])) { // Unsupported component metadata return node; } const objectExpression = args[0]; const styleReplacements: ts.Expression[] = []; // visit all properties let properties = ts.visitNodes(objectExpression.properties, (node) => ts.isObjectLiteralElementLike(node) ? visitComponentMetadata( nodeFactory, node, styleReplacements, resourceImportDeclarations, moduleKind, inlineStyleFileExtension, ) : node, ) as ts.NodeArray<ts.ObjectLiteralElementLike>; // replace properties with updated properties if (styleReplacements.length > 0) { const styleProperty = nodeFactory.createPropertyAssignment( nodeFactory.createIdentifier('styles'), nodeFactory.createArrayLiteralExpression(styleReplacements), ); properties = nodeFactory.createNodeArray([...properties, styleProperty]); } return nodeFactory.updateDecorator( node, nodeFactory.updateCallExpression( decoratorFactory, decoratorFactory.expression, decoratorFactory.typeArguments, [nodeFactory.updateObjectLiteralExpression(objectExpression, properties)], ), ); } function visitComponentMetadata( nodeFactory: ts.NodeFactory, node: ts.ObjectLiteralElementLike, styleReplacements: ts.Expression[], resourceImportDeclarations: ts.ImportDeclaration[], moduleKind: ts.ModuleKind = ts.ModuleKind.ES2015, inlineStyleFileExtension?: string, ): ts.ObjectLiteralElementLike | undefined { if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) { return node; } const name = node.name.text; switch (name) { case 'moduleId': return undefined; case 'templateUrl': { const url = getResourceUrl(node.initializer); if (!url) { return node; } const importName = createResourceImport( nodeFactory, url, resourceImportDeclarations, moduleKind, ); if (!importName) { return node; } return nodeFactory.updatePropertyAssignment( node, nodeFactory.createIdentifier('template'), importName, ); } case 'styles': case 'styleUrl': case 'styleUrls': { const isInlineStyle = name === 'styles'; let styles: Iterable<ts.Expression>; if (ts.isStringLiteralLike(node.initializer)) { styles = [ transformInlineStyleLiteral( node.initializer, nodeFactory, isInlineStyle, inlineStyleFileExtension, resourceImportDeclarations, moduleKind, ) as ts.StringLiteralLike, ]; } else if (ts.isArrayLiteralExpression(node.initializer)) { styles = ts.visitNodes(node.initializer.elements, (node) => transformInlineStyleLiteral( node, nodeFactory, isInlineStyle, inlineStyleFileExtension, resourceImportDeclarations, moduleKind, ), ) as ts.NodeArray<ts.Expression>; } else { return node; } // Styles should be placed first if (isInlineStyle) { styleReplacements.unshift(...styles); } else { styleReplacements.push(...styles); } return undefined; } default: return node; } } function transformInlineStyleLiteral( node: ts.Node, nodeFactory: ts.NodeFactory, isInlineStyle: boolean, inlineStyleFileExtension: string | undefined, resourceImportDeclarations: ts.ImportDeclaration[], moduleKind: ts.ModuleKind, ) { if (!ts.isStringLiteralLike(node)) { return node; } // Don't transform empty strings as PostCSS will choke on them. No work to do anyways. if (node.text === '') { return node; } if (!isInlineStyle) { const url = getResourceUrl(node); return url ? createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind) : node; } if (!inlineStyleFileExtension) { return nodeFactory.createStringLiteral(node.text); } const data = Buffer.from(node.text).toString('base64'); const containingFile = node.getSourceFile().fileName; // app.component.ts.css?ngResource!=!@ngtools/webpack/src/loaders/inline-resource.js?data=...!app.component.ts const url = `${containingFile}.${inlineStyleFileExtension}?${NG_COMPONENT_RESOURCE_QUERY}` + `!=!${InlineAngularResourceLoaderPath}?data=${encodeURIComponent(data)}!${containingFile}`; return createResourceImport(nodeFactory, url, resourceImportDeclarations, moduleKind); } export function getResourceUrl(node: ts.Node): string | null { // only analyze strings if (!ts.isStringLiteralLike(node)) { return null; } return `${/^\.?\.\//.test(node.text) ? '' : './'}${node.text}?${NG_COMPONENT_RESOURCE_QUERY}`; } function isComponentDecorator(node: ts.Node, typeChecker: ts.TypeChecker): node is ts.Decorator { if (!ts.isDecorator(node)) { return false; } const origin = getDecoratorOrigin(node, typeChecker); if (origin && origin.module === '@angular/core' && origin.name === 'Component') { return true; } return false; } function createResourceImport( nodeFactory: ts.NodeFactory, url: string, resourceImportDeclarations: ts.ImportDeclaration[], moduleKind: ts.ModuleKind, ): ts.Identifier | ts.Expression { const urlLiteral = nodeFactory.createStringLiteral(url); if (moduleKind < ts.ModuleKind.ES2015) { return nodeFactory.createCallExpression( nodeFactory.createIdentifier('require'), [], [urlLiteral], ); } else { const importName = nodeFactory.createIdentifier( `__NG_CLI_RESOURCE__${resourceImportDeclarations.length}`, ); resourceImportDeclarations.push( nodeFactory.createImportDeclaration( undefined, nodeFactory.createImportClause(undefined, importName, undefined), urlLiteral, ), ); return importName; } } interface DecoratorOrigin { name: string; module: string; } function getDecoratorOrigin( decorator: ts.Decorator, typeChecker: ts.TypeChecker, ): DecoratorOrigin | null { if (!ts.isCallExpression(decorator.expression)) { return null; } let identifier: ts.Node; let name = ''; if (ts.isPropertyAccessExpression(decorator.expression.expression)) { identifier = decorator.expression.expression.expression; name = decorator.expression.expression.name.text; } else if (ts.isIdentifier(decorator.expression.expression)) { identifier = decorator.expression.expression; } else { return null; } // NOTE: resolver.getReferencedImportDeclaration would work as well but is internal const symbol = typeChecker.getSymbolAtLocation(identifier); if (symbol && symbol.declarations && symbol.declarations.length > 0) { const declaration = symbol.declarations[0]; let module: string; if (ts.isImportSpecifier(declaration)) { name = (declaration.propertyName || declaration.name).text; module = (declaration.parent.parent.parent.moduleSpecifier as ts.Identifier).text; } else if (ts.isNamespaceImport(declaration)) { // Use the name from the decorator namespace property access module = (declaration.parent.parent.moduleSpecifier as ts.Identifier).text; } else if (ts.isImportClause(declaration)) { name = (declaration.name as ts.Identifier).text; module = (declaration.parent.moduleSpecifier as ts.Identifier).text; } else { return null; } return { name, module }; } return null; }