Skip to content

Commit 91bd85e

Browse files
committed
perf: cache parsed AST results per file
Avoid redundant parsing when getAST is called multiple times with the same filename and source.
1 parent ff1eca9 commit 91bd85e

11 files changed

Lines changed: 148 additions & 128 deletions

File tree

src/cli/tasks/extract.task.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { NullCache } from '../../cache/null-cache.js';
88
import { CompilerInterface } from '../../compilers/compiler.interface.js';
99
import { ParserInterface } from '../../parsers/parser.interface.js';
1010
import { PostProcessorInterface } from '../../post-processors/post-processor.interface.js';
11+
import { clearAstCache } from '../../utils/ast-helpers.js';
1112
import { cyan, green, bold, dim, red } from '../../utils/cli-color.js';
1213
import { TranslationCollection, TranslationType } from '../../utils/translation.collection.js';
1314
import { TaskInterface } from './task.interface.js';
@@ -137,6 +138,7 @@ export class ExtractTask implements TaskInterface {
137138
});
138139

139140
collectionTypes.push(...cachedCollectionValues);
141+
clearAstCache();
140142
});
141143
});
142144

src/parsers/directive.parser.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ import {
2121
TmplAstForLoopBlock,
2222
TmplAstDeferredBlock,
2323
ParenthesizedExpression,
24+
TmplAstNode,
2425
} from '@angular/compiler';
2526

26-
import { getNodesFromSwitchBlockTmpl } from '../utils/ast-helpers.js';
27+
import { getAST, getNodesFromSwitchBlockTmpl } from '../utils/ast-helpers.js';
2728
import { TranslationCollection } from '../utils/translation.collection.js';
28-
import { extractComponentInlineTemplate, isPathAngularComponent } from '../utils/utils.js';
2929
import { ParserInterface } from './parser.interface.js';
3030

3131
interface BlockNode {
@@ -44,10 +44,13 @@ export class DirectiveParser implements ParserInterface {
4444
public extract(source: string, filePath: string): TranslationCollection | null {
4545
let collection: TranslationCollection = new TranslationCollection();
4646

47-
if (filePath && isPathAngularComponent(filePath)) {
48-
source = extractComponentInlineTemplate(source);
47+
const parsedTemplates = getAST(source, filePath).parsedTemplates;
48+
49+
if (parsedTemplates.length === 0) {
50+
return null;
4951
}
50-
const nodes: Node[] = this.parseTemplate(source, filePath);
52+
53+
const nodes: TmplAstNode[] = parsedTemplates.map((parsedTpl) => parsedTpl.nodes).flat();
5154
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);
5255

5356
elements.forEach((element) => {

src/parsers/function.parser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ export class FunctionParser implements ParserInterface {
99
constructor(private fnName: string) {}
1010

1111
public extract(source: string, filePath: string): TranslationCollection | null {
12-
const sourceFile = getAST(source, filePath);
12+
const sourceFile = getAST(source, filePath).parsedFile;
13+
14+
if (!sourceFile) {
15+
return null;
16+
}
1317

1418
let collection: TranslationCollection = new TranslationCollection();
1519

src/parsers/marker.parser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ const NGX_TRANSLATE_MARKER_IMPORT_NAME = '_';
1111

1212
export class MarkerParser implements ParserInterface {
1313
public extract(source: string, filePath: string): TranslationCollection | null {
14-
const sourceFile = getAST(source, filePath);
14+
const sourceFile = getAST(source, filePath).parsedFile;
15+
16+
if (!sourceFile) {
17+
return null;
18+
}
1519

1620
const markerImportName = this.getMarkerImportNameFromSource(sourceFile);
1721
if (!markerImportName) {

src/parsers/pipe.parser.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,8 @@ import {
2020
ParenthesizedExpression,
2121
} from '@angular/compiler';
2222

23-
import { getNodesFromSwitchBlockTmpl } from '../utils/ast-helpers.js';
23+
import { getAST, getNodesFromSwitchBlockTmpl } from '../utils/ast-helpers.js';
2424
import { TranslationCollection } from '../utils/translation.collection.js';
25-
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils.js';
2625
import { ParserInterface } from './parser.interface.js';
2726

2827
export const TRANSLATE_PIPE_NAMES = ['translate', 'marker'];
@@ -82,13 +81,12 @@ function traverseAstNode<RESULT, NODE extends TmplAstNode | TmplAstElement>(
8281

8382
export class PipeParser implements ParserInterface {
8483
public extract(source: string, filePath: string): TranslationCollection {
85-
if (filePath && isPathAngularComponent(filePath)) {
86-
source = extractComponentInlineTemplate(source);
84+
const parsedTemplates = getAST(source, filePath).parsedTemplates;
85+
if (parsedTemplates.length === 0) {
86+
return new TranslationCollection();
8787
}
88-
8988
let collection: TranslationCollection = new TranslationCollection();
90-
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
91-
89+
const nodes: TmplAstNode[] = parsedTemplates.map((parsedTpl) => parsedTpl.nodes).flat();
9290
const pipes = traverseAstNodes(nodes, (node) => this.findPipesInNode(node));
9391

9492
pipes.forEach((pipe) => {

src/parsers/service.parser.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ export class ServiceParser implements ParserInterface {
2929
private static propertyMap = new Map<string, string[]>();
3030

3131
public extract(source: string, filePath: string): TranslationCollection | null {
32-
const sourceFile = getAST(source, filePath);
32+
const sourceFile = getAST(source, filePath).parsedFile;
33+
34+
if (!sourceFile) {
35+
return null;
36+
}
37+
3338
const classDeclarations = findClassDeclarations(sourceFile);
3439
const functionDeclarations = findFunctionExpressions(sourceFile);
3540

@@ -163,8 +168,8 @@ export class ServiceParser implements ParserInterface {
163168
const allSuperClassPropertyNames: string[] = [];
164169
potentialSuperFiles.forEach((file) => {
165170
const superClassFileContent = fs.readFileSync(file, 'utf8');
166-
const superClassAst = getAST(superClassFileContent, file);
167-
const superClassDeclarations = findClassDeclarations(superClassAst, superClassName);
171+
const superClassAst = getAST(superClassFileContent, file).parsedFile;
172+
const superClassDeclarations = superClassAst ? findClassDeclarations(superClassAst, superClassName) : [];
168173
const superClassPropertyNames = superClassDeclarations.flatMap((superClassDeclaration) =>
169174
findClassPropertiesByType(superClassDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE),
170175
);

src/utils/ast-helpers.ts

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,35 @@
11
import { extname } from 'node:path';
22

3-
import { TmplAstNode, TmplAstSwitchBlock } from '@angular/compiler';
3+
import { ParsedTemplate, parseTemplate, type TmplAstNode, TmplAstSwitchBlock } from '@angular/compiler';
44
import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
55
import pkg, {
6-
Node,
7-
Identifier,
8-
ClassDeclaration,
9-
ConstructorDeclaration,
10-
CallExpression,
11-
Expression,
12-
StringLiteral,
13-
SourceFile,
14-
PropertyDeclaration,
15-
PropertyAccessExpression,
6+
type CallExpression,
7+
type ClassDeclaration,
8+
type ConstructorDeclaration,
9+
type Expression,
10+
type Identifier,
11+
type Node,
12+
type PropertyAccessExpression,
13+
type PropertyAssignment,
14+
type PropertyDeclaration,
15+
type SourceFile,
16+
type StringLiteral,
1617
} from 'typescript';
1718

19+
interface ParsedScriptSource {
20+
source: string;
21+
parsedFile: SourceFile;
22+
parsedTemplates: ParsedTemplate[];
23+
}
24+
25+
interface ParsedTemplateSource {
26+
source: string;
27+
parsedFile: null;
28+
parsedTemplates: [ParsedTemplate];
29+
}
30+
31+
type ParsedSource = ParsedScriptSource | ParsedTemplateSource;
32+
1833
// Importing non-type members from 'typescript' this way to prevent runtime errors such as:
1934
// `SyntaxError: Named export 'isCallExpression' not found. The requested module 'typescript' is a CommonJS module,
2035
// which may not support all module.exports as named exports.`
@@ -28,17 +43,76 @@ const {
2843
SyntaxKind,
2944
} = pkg;
3045

31-
export function getAST(source: string, fileName = ''): SourceFile {
32-
const supportedScriptTypes: Record<string, ScriptKind> = {
33-
'.js': ScriptKind.JS,
34-
'.jsx': ScriptKind.JSX,
35-
'.ts': ScriptKind.TS,
36-
'.tsx': ScriptKind.TSX,
37-
};
46+
const ANGULAR_TEMPLATE_KIND = ScriptKind.Unknown; // Unknown for ts
47+
48+
const SCRIPT_TYPES = new Map([
49+
['.js', ScriptKind.JS],
50+
['.mjs', ScriptKind.JS],
51+
['.jsx', ScriptKind.JSX],
52+
['.ts', ScriptKind.TS],
53+
['.mts', ScriptKind.TS],
54+
['.tsx', ScriptKind.TSX],
55+
['.html', ANGULAR_TEMPLATE_KIND],
56+
]);
57+
58+
const AST_CACHE = new Map<string, ParsedSource>();
3859

39-
const scriptKind = supportedScriptTypes[extname(fileName)] ?? ScriptKind.TS;
60+
export function clearAstCache(): void {
61+
AST_CACHE.clear();
62+
}
63+
64+
export function getAST(source: string, fileName = ''): ParsedSource {
65+
// Skip cache if no fileName is provided
66+
if (!fileName) {
67+
return parseSource(source, fileName);
68+
}
4069

41-
return tsquery.ast(source, fileName, scriptKind);
70+
const cached = AST_CACHE.get(fileName);
71+
if (cached && cached.source === source) {
72+
return cached;
73+
}
74+
75+
const result = parseSource(source, fileName);
76+
AST_CACHE.set(fileName, result);
77+
78+
return result;
79+
}
80+
81+
export function parseSource(source: string, fileName = ''): ParsedSource {
82+
const scriptKind = SCRIPT_TYPES.get(extname(fileName)) ?? ScriptKind.TS;
83+
84+
// Angular template, pass to Angular compiler.
85+
if (scriptKind === ANGULAR_TEMPLATE_KIND) {
86+
return {
87+
source,
88+
parsedFile: null,
89+
parsedTemplates: [parseTemplate(source, fileName, { collectCommentNodes: false })],
90+
};
91+
}
92+
93+
const parsedFile = tsquery.ast(source, fileName, scriptKind);
94+
const parsedTemplates: ParsedTemplate[] = [];
95+
96+
// Check for possible inline templates that need to be processed by the Angular compiler.
97+
if (source.includes('@Component(') && source.includes('template:')) {
98+
getComponentInlineTemplate(parsedFile).forEach((templateNode) => {
99+
const tpl = templateNode.initializer;
100+
if (isStringLiteralLike(tpl)) {
101+
parsedTemplates.push(parseTemplate(tpl.text, fileName, { collectCommentNodes: false }));
102+
}
103+
});
104+
}
105+
106+
return { source, parsedFile, parsedTemplates };
107+
}
108+
109+
/**
110+
* Retrieves inline `template` property assignments from Angular `@Component` decorators.
111+
*/
112+
export function getComponentInlineTemplate(node: Node): PropertyAssignment[] {
113+
const query =
114+
'Decorator > CallExpression:has(Identifier[name="Component"]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name="template"])';
115+
return tsquery<PropertyAssignment>(node, query);
42116
}
43117

44118
/**

src/utils/utils.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,3 @@
1-
/**
2-
* Assumes file is an Angular component if type is javascript/typescript
3-
*/
4-
export function isPathAngularComponent(path: string): boolean {
5-
return /\.ts|js$/i.test(path);
6-
}
7-
8-
/**
9-
* Extract inline template from a component
10-
*/
11-
export function extractComponentInlineTemplate(contents: string): string {
12-
const regExp = /template\s*:\s*(["'`])([\s\S]*?)\1/;
13-
14-
const match = regExp.exec(contents);
15-
if (match !== null) {
16-
return match[2];
17-
}
18-
return '';
19-
}
20-
211
export function stripBOM(contents: string): string {
222
return contents.trim();
233
}

0 commit comments

Comments
 (0)