Skip to content

Commit 751adc3

Browse files
authored
perf(@angular/build): cache pre-declaration transformed SourceFiles during AoT emit
Cache transformed `SourceFile` ASTs in a `Map` in `ensurePreDeclarationTransformers` instead of only tracking visited files in a `Set`. When `.d.ts`-only emit runs prior to `.js` emit (such as for transitive importers during incremental library builds), the subsequent `.js` emit now reuses the cached transformed `SourceFile` instead of executing `before` transformers a second time.
1 parent c3881e9 commit 751adc3

2 files changed

Lines changed: 176 additions & 18 deletions

File tree

‎packages/angular/build/src/tools/angular/compilation/angular-compilation_spec.ts‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import path from 'node:path';
1010
import ts from 'typescript';
1111
import type { AngularHostOptions } from '../angular-host';
12+
import { ensurePreDeclarationTransformers } from './aot-compilation';
1213
import { CompilerOptionOverrides, transformCompilerOptions } from './compiler-options';
1314
import { TypeScriptCompilation } from './typescript-compilation';
1415
import {
@@ -509,4 +510,141 @@ describe('AngularCompilation', () => {
509510
expect(warnings).toEqual([]);
510511
});
511512
});
513+
514+
describe('ensurePreDeclarationTransformers', () => {
515+
function createTestProgram(
516+
fileName: string,
517+
content: string,
518+
): {
519+
program: ts.Program;
520+
sourceFile: ts.SourceFile;
521+
} {
522+
const options: ts.CompilerOptions = {
523+
target: ts.ScriptTarget.ES2022,
524+
module: ts.ModuleKind.ES2022,
525+
declaration: true,
526+
noLib: true,
527+
};
528+
const sourceFile = ts.createSourceFile(fileName, content, ts.ScriptTarget.ES2022, true);
529+
const host = ts.createCompilerHost(options);
530+
host.getSourceFile = (name) => (name === fileName ? sourceFile : undefined);
531+
host.fileExists = (name) => name === fileName;
532+
host.readFile = (name) => (name === fileName ? content : undefined);
533+
host.writeFile = () => {};
534+
535+
const program = ts.createProgram([fileName], options, host);
536+
537+
return { program, sourceFile };
538+
}
539+
540+
it('transforms each SourceFile only once when emitting .d.ts followed by .js and reuses the cached SourceFile', () => {
541+
const { program, sourceFile } = createTestProgram('/index.ts', 'export const value = 1;');
542+
543+
let preDeclStatePopulated = false;
544+
const beforeSpy1 = jasmine
545+
.createSpy('beforeTransformer1')
546+
.and.callFake((sf: ts.SourceFile) => {
547+
preDeclStatePopulated = true;
548+
const addedStmt = ts.factory.createVariableStatement(
549+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
550+
ts.factory.createVariableDeclarationList(
551+
[
552+
ts.factory.createVariableDeclaration(
553+
'fromBefore1',
554+
undefined,
555+
undefined,
556+
ts.factory.createNumericLiteral(2),
557+
),
558+
],
559+
ts.NodeFlags.Const,
560+
),
561+
);
562+
563+
return ts.factory.updateSourceFile(sf, [...sf.statements, addedStmt]);
564+
});
565+
566+
const beforeSpy2 = jasmine
567+
.createSpy('beforeTransformer2')
568+
.and.callFake((sf: ts.SourceFile) => {
569+
const addedStmt = ts.factory.createVariableStatement(
570+
[ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
571+
ts.factory.createVariableDeclarationList(
572+
[
573+
ts.factory.createVariableDeclaration(
574+
'fromBefore2',
575+
undefined,
576+
undefined,
577+
ts.factory.createNumericLiteral(3),
578+
),
579+
],
580+
ts.NodeFlags.Const,
581+
),
582+
);
583+
584+
return ts.factory.updateSourceFile(sf, [...sf.statements, addedStmt]);
585+
});
586+
587+
let sawPopulatedPreDeclState = false;
588+
const afterDeclSpy = jasmine
589+
.createSpy('afterDeclarationsTransformer')
590+
.and.callFake((sf: ts.SourceFile) => {
591+
sawPopulatedPreDeclState = preDeclStatePopulated;
592+
593+
return sf;
594+
});
595+
596+
const transformers: ts.CustomTransformers = {
597+
before: [() => beforeSpy1, () => beforeSpy2],
598+
afterDeclarations: [() => afterDeclSpy],
599+
};
600+
601+
ensurePreDeclarationTransformers(transformers);
602+
603+
const emittedFiles = new Map<string, string>();
604+
const writeFile: ts.WriteFileCallback = (fileName, text) => {
605+
emittedFiles.set(fileName, text);
606+
};
607+
608+
// Emit .d.ts first (emitOnlyDtsFiles = true)
609+
program.emit(sourceFile, writeFile, undefined, true, transformers);
610+
611+
expect(sawPopulatedPreDeclState).toBeTrue();
612+
expect(beforeSpy1).toHaveBeenCalledTimes(1);
613+
expect(beforeSpy2).toHaveBeenCalledTimes(1);
614+
expect(afterDeclSpy).toHaveBeenCalledTimes(1);
615+
expect(emittedFiles.has('/index.d.ts')).toBeTrue();
616+
expect(emittedFiles.has('/index.js')).toBeFalse();
617+
618+
// Emit .js second; before transformers must not execute again and cached AST must be emitted
619+
program.emit(sourceFile, writeFile, undefined, 0 as unknown as boolean, transformers);
620+
621+
expect(beforeSpy1).toHaveBeenCalledTimes(1);
622+
expect(beforeSpy2).toHaveBeenCalledTimes(1);
623+
expect(emittedFiles.get('/index.js')).toContain('export const fromBefore1 = 2;');
624+
expect(emittedFiles.get('/index.js')).toContain('export const fromBefore2 = 3;');
625+
});
626+
627+
it('transforms each SourceFile only once when emitting .js and .d.ts together', () => {
628+
const { program, sourceFile } = createTestProgram('/index.ts', 'export const value = 1;');
629+
630+
const beforeSpy = jasmine
631+
.createSpy('beforeTransformer')
632+
.and.callFake((sf: ts.SourceFile) => sf);
633+
const afterDeclSpy = jasmine
634+
.createSpy('afterDeclarationsTransformer')
635+
.and.callFake((sf: ts.SourceFile) => sf);
636+
637+
const transformers: ts.CustomTransformers = {
638+
before: [() => beforeSpy],
639+
afterDeclarations: [() => afterDeclSpy],
640+
};
641+
642+
ensurePreDeclarationTransformers(transformers);
643+
644+
program.emit(sourceFile, () => {}, undefined, false, transformers);
645+
646+
expect(beforeSpy).toHaveBeenCalledTimes(1);
647+
expect(afterDeclSpy).toHaveBeenCalledTimes(1);
648+
});
649+
});
512650
});

‎packages/angular/build/src/tools/angular/compilation/aot-compilation.ts‎

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -681,40 +681,60 @@ export class AotCompilation extends TypeScriptCompilation {
681681
* `afterDeclarations`, and caches the transformed `SourceFile` so a subsequent `.js` emit for the
682682
* same file does not re-execute `before` transformers.
683683
*/
684-
function ensurePreDeclarationTransformers(transformers: ts.CustomTransformers): void {
684+
export function ensurePreDeclarationTransformers(transformers: ts.CustomTransformers): void {
685685
if (!transformers.before?.length || !transformers.afterDeclarations?.length) {
686686
return;
687687
}
688688

689689
const beforeTransformers = transformers.before as ts.TransformerFactory<ts.SourceFile>[];
690-
const transformedSourceFiles = new Set<ts.SourceFile>();
691-
transformers.before = beforeTransformers.map((factory, index) =>
692-
index === 0
693-
? (context) => {
694-
const transform = factory(context);
690+
const seenSourceFiles = new Set<ts.SourceFile>();
691+
const preTransformedSourceFiles = new Map<ts.SourceFile, ts.SourceFile>();
692+
const runBefore = (
693+
beforeTransforms: ts.Transformer<ts.SourceFile>[],
694+
sourceFile: ts.SourceFile,
695+
originalSourceFile: ts.SourceFile,
696+
): ts.SourceFile => {
697+
seenSourceFiles.add(originalSourceFile);
698+
let current = sourceFile;
699+
for (const transform of beforeTransforms) {
700+
current = transform(current);
701+
}
702+
703+
return current;
704+
};
705+
706+
transformers.before = [
707+
(context) => {
708+
let beforeTransforms: ts.Transformer<ts.SourceFile>[] | undefined;
695709

696-
return (sourceFile) => {
697-
transformedSourceFiles.add(ts.getOriginalNode(sourceFile, ts.isSourceFile));
710+
return (sourceFile) => {
711+
const originalSourceFile = ts.getOriginalNode(sourceFile, ts.isSourceFile);
712+
const preTransformed = preTransformedSourceFiles.get(originalSourceFile);
713+
if (preTransformed !== undefined) {
714+
preTransformedSourceFiles.delete(originalSourceFile);
698715

699-
return transform(sourceFile);
700-
};
716+
return preTransformed;
701717
}
702-
: factory,
703-
);
718+
719+
beforeTransforms ??= beforeTransformers.map((factory) => factory(context));
720+
721+
return runBefore(beforeTransforms, sourceFile, originalSourceFile);
722+
};
723+
},
724+
];
704725
transformers.afterDeclarations = [
705726
(context) => {
706727
let beforeTransforms: ts.Transformer<ts.SourceFile>[] | undefined;
707728

708729
return (fileOrBundle) => {
709730
if (!ts.isBundle(fileOrBundle)) {
710731
const originalSourceFile = ts.getOriginalNode(fileOrBundle, ts.isSourceFile);
711-
if (!transformedSourceFiles.has(originalSourceFile)) {
712-
transformedSourceFiles.add(originalSourceFile);
732+
if (!seenSourceFiles.has(originalSourceFile)) {
713733
beforeTransforms ??= beforeTransformers.map((factory) => factory(context));
714-
let current = originalSourceFile;
715-
for (const transform of beforeTransforms) {
716-
current = transform(current);
717-
}
734+
preTransformedSourceFiles.set(
735+
originalSourceFile,
736+
runBefore(beforeTransforms, originalSourceFile, originalSourceFile),
737+
);
718738
}
719739
}
720740

0 commit comments

Comments
 (0)