@angular/core
Version:
Angular - the core framework
186 lines (179 loc) • 7.62 kB
JavaScript
;
/**
* @license Angular v21.0.5
* (c) 2010-2025 Google LLC. https://angular.io/
* License: MIT
*/
;
var ts = require('typescript');
var compilerCli = require('@angular/compiler-cli');
var ng_decorators = require('./ng_decorators-DSFlWYQY.cjs');
var property_name = require('./property_name-BBwFuqMe.cjs');
/**
* Unwraps a given expression TypeScript node. Expressions can be wrapped within multiple
* parentheses or as expression. e.g. "(((({exp}))))()". The function should return the
* TypeScript node referring to the inner expression. e.g "exp".
*/
function unwrapExpression(node) {
if (ts.isParenthesizedExpression(node) || ts.isAsExpression(node)) {
return unwrapExpression(node.expression);
}
else {
return node;
}
}
/** Extracts `@Directive` or `@Component` metadata from the given class. */
function extractAngularClassMetadata(typeChecker, node) {
const decorators = ts.getDecorators(node);
if (!decorators || !decorators.length) {
return null;
}
const ngDecorators = ng_decorators.getAngularDecorators(typeChecker, decorators);
const componentDecorator = ngDecorators.find((dec) => dec.name === 'Component');
const directiveDecorator = ngDecorators.find((dec) => dec.name === 'Directive');
const decorator = componentDecorator ?? directiveDecorator;
// In case no decorator could be found on the current class, skip.
if (!decorator) {
return null;
}
const decoratorCall = decorator.node.expression;
// In case the decorator call is not valid, skip this class declaration.
if (decoratorCall.arguments.length !== 1) {
return null;
}
const metadata = unwrapExpression(decoratorCall.arguments[0]);
// Ensure that the metadata is an object literal expression.
if (!ts.isObjectLiteralExpression(metadata)) {
return null;
}
return {
type: componentDecorator ? 'component' : 'directive',
node: metadata,
};
}
const LF_CHAR = 10;
const CR_CHAR = 13;
const LINE_SEP_CHAR = 8232;
const PARAGRAPH_CHAR = 8233;
/** Gets the line and character for the given position from the line starts map. */
function getLineAndCharacterFromPosition(lineStartsMap, position) {
const lineIndex = findClosestLineStartPosition(lineStartsMap, position);
return { character: position - lineStartsMap[lineIndex], line: lineIndex };
}
/**
* Computes the line start map of the given text. This can be used in order to
* retrieve the line and character of a given text position index.
*/
function computeLineStartsMap(text) {
const result = [0];
let pos = 0;
while (pos < text.length) {
const char = text.charCodeAt(pos++);
// Handles the "CRLF" line break. In that case we peek the character
// after the "CR" and check if it is a line feed.
if (char === CR_CHAR) {
if (text.charCodeAt(pos) === LF_CHAR) {
pos++;
}
result.push(pos);
}
else if (char === LF_CHAR || char === LINE_SEP_CHAR || char === PARAGRAPH_CHAR) {
result.push(pos);
}
}
result.push(pos);
return result;
}
/** Finds the closest line start for the given position. */
function findClosestLineStartPosition(linesMap, position, low = 0, high = linesMap.length - 1) {
while (low <= high) {
const pivotIdx = Math.floor((low + high) / 2);
const pivotEl = linesMap[pivotIdx];
if (pivotEl === position) {
return pivotIdx;
}
else if (position > pivotEl) {
low = pivotIdx + 1;
}
else {
high = pivotIdx - 1;
}
}
// In case there was no exact match, return the closest "lower" line index. We also
// subtract the index by one because want the index of the previous line start.
return low - 1;
}
/**
* Visitor that can be used to determine Angular templates referenced within given
* TypeScript source files (inline templates or external referenced templates)
*/
class NgComponentTemplateVisitor {
typeChecker;
resolvedTemplates = [];
fs = compilerCli.getFileSystem();
constructor(typeChecker) {
this.typeChecker = typeChecker;
}
visitNode(node) {
if (node.kind === ts.SyntaxKind.ClassDeclaration) {
this.visitClassDeclaration(node);
}
ts.forEachChild(node, (n) => this.visitNode(n));
}
visitClassDeclaration(node) {
const metadata = extractAngularClassMetadata(this.typeChecker, node);
if (metadata === null || metadata.type !== 'component') {
return;
}
const sourceFile = node.getSourceFile();
const sourceFileName = sourceFile.fileName;
// Walk through all component metadata properties and determine the referenced
// HTML templates (either external or inline)
metadata.node.properties.forEach((property) => {
if (!ts.isPropertyAssignment(property)) {
return;
}
const propertyName = property_name.getPropertyNameText(property.name);
// In case there is an inline template specified, ensure that the value is statically
// analyzable by checking if the initializer is a string literal-like node.
if (propertyName === 'template' && ts.isStringLiteralLike(property.initializer)) {
// Need to add an offset of one to the start because the template quotes are
// not part of the template content.
// The `getText()` method gives us the original raw text.
// We could have used the `text` property, but if the template is defined as a backtick
// string then the `text` property contains a "cooked" version of the string. Such cooked
// strings will have converted CRLF characters to only LF. This messes up string
// replacements in template migrations.
// The raw text returned by `getText()` includes the enclosing quotes so we change the
// `content` and `start` values accordingly.
const content = property.initializer.getText().slice(1, -1);
const start = property.initializer.getStart() + 1;
this.resolvedTemplates.push({
filePath: sourceFileName,
container: node,
content,
inline: true,
start: start,
getCharacterAndLineOfPosition: (pos) => ts.getLineAndCharacterOfPosition(sourceFile, pos + start),
});
}
if (propertyName === 'templateUrl' && ts.isStringLiteralLike(property.initializer)) {
const absolutePath = this.fs.resolve(this.fs.dirname(sourceFileName), property.initializer.text);
if (!this.fs.exists(absolutePath)) {
return;
}
const fileContent = this.fs.readFile(absolutePath);
const lineStartsMap = computeLineStartsMap(fileContent);
this.resolvedTemplates.push({
filePath: absolutePath,
container: node,
content: fileContent,
inline: false,
start: 0,
getCharacterAndLineOfPosition: (pos) => getLineAndCharacterFromPosition(lineStartsMap, pos),
});
}
});
}
}
exports.NgComponentTemplateVisitor = NgComponentTemplateVisitor;