Skip to content

Commit 815e1a0

Browse files
authored
refactor(compiler-cli): Add skeleton tests around source->source compiler transform mode
adds skeleton and tests for source->source compiler transform.
1 parent a37b6c5 commit 815e1a0

13 files changed

Lines changed: 358 additions & 3 deletions

File tree

packages/compiler-cli/src/ngtsc/core/src/compiler.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,8 @@ export class NgCompiler {
836836
compilation.isCore,
837837
this.closureCompilerEnabled,
838838
this.emitDeclarationOnly,
839+
compilation.refEmitter,
840+
!!this.options['_experimentalEmitIntermediateTs'],
839841
),
840842
aliasTransformFactory(compilation.traitCompiler.exportStatements),
841843
defaultImportTracker.importPreservingTransformer(),
@@ -1632,6 +1634,7 @@ export class NgCompiler {
16321634
semanticDepGraphUpdater,
16331635
this.adapter,
16341636
this.emitDeclarationOnly,
1637+
!!this.options['_experimentalEmitIntermediateTs'],
16351638
);
16361639

16371640
// Template type-checking may use the `ProgramDriver` to produce new `ts.Program`(s). If this
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from 'typescript';
10+
11+
import {NgCompiler, NgCompilerHost} from './core';
12+
import {NgCompilerOptions} from './core/api';
13+
import {TrackedIncrementalBuildStrategy} from './incremental';
14+
import {ActivePerfRecorder, PerfPhase} from './perf';
15+
import {TsCreateProgramDriver} from './program_driver';
16+
import {
17+
CompilationTicket,
18+
freshCompilationTicket,
19+
incrementalFromCompilerTicket,
20+
} from './core/src/compiler';
21+
22+
/**
23+
* A driver for the Angular Compiler that performs "Source-to-Source" transformation.
24+
*
25+
* Unlike `NgtscProgram`, this driver does NOT use `program.emit()`. Instead, it:
26+
* 1. Analyzes the program using `NgCompiler`.
27+
* 2. Manually runs `ts.transform` with Angular's Ivy transformers.
28+
* 3. Prints the transformed AST back to a TypeScript string.
29+
*
30+
* This mode is designed for a mode where the Angular Compiler
31+
* acts as a pre-processor for a downstream TypeScript compiler.
32+
*/
33+
export class NgtscIsolatedPreprocessor {
34+
readonly compiler: NgCompiler;
35+
private tsProgram: ts.Program;
36+
private host: NgCompilerHost;
37+
private incrementalStrategy: TrackedIncrementalBuildStrategy;
38+
39+
constructor(
40+
rootNames: ReadonlyArray<string>,
41+
private options: NgCompilerOptions,
42+
delegateHost: ts.CompilerHost,
43+
oldProgram?: NgtscIsolatedPreprocessor,
44+
) {
45+
// Enable type reification for source-to-source transformation
46+
this.options = {...options, '_experimentalEmitIntermediateTs': true};
47+
48+
const perfRecorder = ActivePerfRecorder.zeroedToNow();
49+
perfRecorder.phase(PerfPhase.Setup);
50+
51+
const reuseProgram = oldProgram?.compiler.getCurrentProgram();
52+
this.host = NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null);
53+
54+
this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
55+
56+
const programDriver = new TsCreateProgramDriver(
57+
this.tsProgram,
58+
this.host,
59+
this.options,
60+
this.host.shimExtensionPrefixes,
61+
);
62+
63+
this.incrementalStrategy =
64+
oldProgram !== undefined
65+
? oldProgram.incrementalStrategy.toNextBuildStrategy()
66+
: new TrackedIncrementalBuildStrategy();
67+
68+
let ticket: CompilationTicket;
69+
if (oldProgram === undefined) {
70+
ticket = freshCompilationTicket(
71+
this.tsProgram,
72+
this.options,
73+
this.incrementalStrategy,
74+
programDriver,
75+
perfRecorder,
76+
/* enableTemplateTypeChecker */ !!this.options._enableTemplateTypeChecker,
77+
/* usePoisonedData */ false,
78+
);
79+
} else {
80+
ticket = incrementalFromCompilerTicket(
81+
oldProgram.compiler,
82+
this.tsProgram,
83+
this.incrementalStrategy,
84+
programDriver,
85+
new Set(), // TODO: track modified resource files
86+
perfRecorder,
87+
);
88+
}
89+
90+
this.compiler = NgCompiler.fromTicket(ticket, this.host);
91+
}
92+
93+
transformAndPrint(): {fileName: string; content: string}[] {
94+
// 1. Generate TCBs
95+
this.compiler['ensureAnalyzed']().templateTypeChecker.generateAllTypeCheckBlocks();
96+
97+
const transformers = this.compiler.prepareEmit().transformers;
98+
// We only care about 'before' transformers for source-to-source
99+
const beforeTransformers = (transformers.before ||
100+
[]) as unknown as ts.TransformerFactory<ts.SourceFile>[];
101+
102+
const result: {fileName: string; content: string}[] = [];
103+
const printer = ts.createPrinter({
104+
newLine: ts.NewLineKind.LineFeed,
105+
removeComments: false,
106+
});
107+
108+
const program = this.compiler.getCurrentProgram();
109+
110+
for (const sf of program.getSourceFiles()) {
111+
if (sf.isDeclarationFile) {
112+
continue;
113+
}
114+
115+
// If it is a TCB file (ends in .ngtypecheck.ts), we want it as-is, without any transformations.
116+
if (sf.fileName.endsWith('.ngtypecheck.ts')) {
117+
const content = printer.printFile(sf);
118+
result.push({fileName: sf.fileName, content});
119+
continue;
120+
}
121+
122+
// Manually transform the source file
123+
const transformationResult = ts.transform(sf, beforeTransformers, this.options);
124+
125+
if (transformationResult.transformed.length !== 1) {
126+
transformationResult.dispose();
127+
throw new Error(
128+
`Expected exactly one transformed file, got ${transformationResult.transformed.length}`,
129+
);
130+
}
131+
132+
const transformedSf = transformationResult.transformed[0];
133+
134+
const content = printer.printFile(transformedSf);
135+
result.push({fileName: sf.fileName, content});
136+
137+
transformationResult.dispose();
138+
139+
this.compiler.incrementalCompilation.recordSuccessfulEmit(sf);
140+
}
141+
142+
return result;
143+
}
144+
}

packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ export class TraitCompiler implements ProgramTypeCheckAdapter {
119119
private semanticDepGraphUpdater: SemanticDepGraphUpdater | null,
120120
private sourceFileTypeIdentifier: SourceFileTypeIdentifier,
121121
private emitDeclarationOnly: boolean,
122+
private emitIntermediateTs: boolean,
122123
) {
123124
for (const handler of handlers) {
124125
this.handlersByName.set(handler.name, handler);

packages/compiler-cli/src/ngtsc/transform/src/transform.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DefaultImportTracker,
1414
ImportRewriter,
1515
LocalCompilationExtraImportsTracker,
16+
ReferenceEmitter,
1617
} from '../../imports';
1718
import {getDefaultImportDeclaration} from '../../imports/src/default';
1819
import {PerfPhase, PerfRecorder} from '../../perf';
@@ -23,6 +24,7 @@ import {
2324
RecordWrappedNodeFn,
2425
translateExpression,
2526
translateStatement,
27+
translateType,
2628
TranslatorOptions,
2729
} from '../../translator';
2830
import {visit, VisitListEntryResult, Visitor} from '../../util/src/visitor';
@@ -53,6 +55,8 @@ export function ivyTransformFactory(
5355
isCore: boolean,
5456
isClosureCompilerEnabled: boolean,
5557
emitDeclarationOnly: boolean,
58+
refEmitter: ReferenceEmitter | null,
59+
enableTypeReification: boolean,
5660
): ts.TransformerFactory<ts.SourceFile> {
5761
const recordWrappedNode = createRecorderFn(defaultImportTracker);
5862
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
@@ -68,6 +72,8 @@ export function ivyTransformFactory(
6872
isCore,
6973
isClosureCompilerEnabled,
7074
emitDeclarationOnly,
75+
refEmitter,
76+
enableTypeReification,
7177
recordWrappedNode,
7278
),
7379
);
@@ -129,6 +135,8 @@ class IvyTransformationVisitor extends Visitor {
129135
private isClosureCompilerEnabled: boolean,
130136
private isCore: boolean,
131137
private deferrableImports: Set<ts.ImportDeclaration>,
138+
private refEmitter: ReferenceEmitter | null,
139+
private enableTypeReification: boolean,
132140
) {
133141
super();
134142
}
@@ -175,11 +183,22 @@ class IvyTransformationVisitor extends Visitor {
175183
);
176184

177185
// Create a static property declaration for the new field.
186+
let typeNode: ts.TypeNode | undefined = undefined;
187+
if (this.enableTypeReification && this.refEmitter !== null) {
188+
typeNode = translateType(
189+
field.type,
190+
sourceFile,
191+
this.reflector,
192+
this.refEmitter,
193+
this.importManager,
194+
);
195+
}
196+
178197
const property = ts.factory.createPropertyDeclaration(
179198
[ts.factory.createToken(ts.SyntaxKind.StaticKeyword)],
180199
field.name,
181200
undefined,
182-
undefined,
201+
typeNode,
183202
exprNode,
184203
);
185204

@@ -376,6 +395,8 @@ function transformIvySourceFile(
376395
isCore: boolean,
377396
isClosureCompilerEnabled: boolean,
378397
emitDeclarationOnly: boolean,
398+
refEmitter: ReferenceEmitter | null,
399+
enableTypeReification: boolean,
379400
recordWrappedNode: RecordWrappedNodeFn<ts.Expression>,
380401
): ts.SourceFile {
381402
const constantPool = new ConstantPool(isClosureCompilerEnabled);
@@ -414,6 +435,8 @@ function transformIvySourceFile(
414435
isClosureCompilerEnabled,
415436
isCore,
416437
compilationVisitor.deferrableImports,
438+
refEmitter,
439+
enableTypeReification,
417440
);
418441
let sf = visit(file, transformationVisitor, context);
419442

packages/compiler-cli/src/ngtsc/transform/test/compilation_spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ runInEachFileSystem(() => {
6363
null,
6464
fakeSfTypeIdentifier,
6565
/* emitDeclarationOnly */ false,
66+
/* emitIntermediateTs */ false,
6667
);
6768
const sourceFile = program.getSourceFile(filename)!;
6869

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
load("//tools:defaults.bzl", "jasmine_test", "ts_project")
2+
3+
ts_project(
4+
name = "test_lib",
5+
testonly = True,
6+
srcs = ["isolated_compile_spec.ts"],
7+
deps = [
8+
"//:node_modules/typescript",
9+
"//packages/compiler-cli",
10+
"//packages/compiler-cli/src/ngtsc/file_system",
11+
"//packages/compiler-cli/src/ngtsc/testing",
12+
"//packages/compiler-cli/test/compliance/test_helpers",
13+
],
14+
)
15+
16+
jasmine_test(
17+
name = "isolated",
18+
data = [
19+
":test_lib",
20+
"//packages/compiler-cli/test/compliance/test_cases",
21+
"//packages/core:npm_package",
22+
],
23+
shard_count = 2,
24+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import ts from 'typescript';
10+
import {checkExpectations} from '../test_helpers/check_expectations';
11+
import {checkNoUnexpectedErrors} from '../test_helpers/check_errors';
12+
import {FileSystem} from '../../../src/ngtsc/file_system';
13+
import {NgtscTestCompilerHost} from '../../../src/ngtsc/testing';
14+
import {
15+
getBuildOutputDirectory,
16+
initMockTestFileSystem,
17+
getOptions,
18+
getRootDirectory,
19+
} from '../test_helpers/compile_test';
20+
import {ComplianceTest, getAllComplianceTests} from '../test_helpers/get_compliance_tests';
21+
import {NgtscIsolatedPreprocessor} from '@angular/compiler-cli/src/ngtsc/preprocessor';
22+
23+
describe('isolated compliance tests', () => {
24+
for (const test of getAllComplianceTests()) {
25+
if (!test.relativePath.includes('isolated')) {
26+
continue;
27+
}
28+
describe(`[${test.relativePath}]`, () => {
29+
it(test.description, () => {
30+
const fs = initMockTestFileSystem(test.realTestPath);
31+
const {errors} = compileTests(fs, test);
32+
for (const expectation of test.expectations) {
33+
checkExpectations(
34+
fs,
35+
test.relativePath,
36+
expectation.failureMessage,
37+
expectation.files,
38+
expectation.extraChecks,
39+
);
40+
checkNoUnexpectedErrors(test.relativePath, errors);
41+
}
42+
});
43+
});
44+
}
45+
});
46+
47+
function compileTests(fs: FileSystem, test: ComplianceTest): {errors: string[]} {
48+
const rootDir = getRootDirectory(fs);
49+
const outDir = getBuildOutputDirectory(fs);
50+
const compilerOptions = test.compilerOptions;
51+
const angularCompilerOptions = test.angularCompilerOptions;
52+
53+
const options = getOptions(rootDir, outDir, compilerOptions, angularCompilerOptions);
54+
// Resolve inputs relative to rootDir.
55+
const rootNames = test.inputFiles.map((f) => fs.resolve(rootDir, f));
56+
57+
const host = new NgtscTestCompilerHost(fs, options);
58+
const preprocessor = new NgtscIsolatedPreprocessor(rootNames, options, host);
59+
60+
const transformedFiles = preprocessor.transformAndPrint();
61+
62+
const emittedFiles: string[] = [];
63+
const validFiles = new Set<string>();
64+
65+
for (const file of transformedFiles) {
66+
const relativePath = fs.relative(rootDir, fs.resolve(file.fileName));
67+
const path = fs.resolve(outDir, relativePath);
68+
fs.ensureDir(fs.dirname(path));
69+
fs.writeFile(path, file.content);
70+
emittedFiles.push(path);
71+
validFiles.add(path);
72+
}
73+
74+
const verifyHost = new NgtscTestCompilerHost(fs, options);
75+
const verifyProgram = ts.createProgram({
76+
rootNames: [...emittedFiles],
77+
options: {
78+
...test.compilerOptions,
79+
noEmit: true,
80+
skipLibCheck: true,
81+
// Ensure we can resolve the imports in the mock FS
82+
baseUrl: rootDir,
83+
paths: {
84+
'*': ['node_modules/*'],
85+
},
86+
// Use classic Node resolution which works better with the simple mock FS structure
87+
// than 'Bundler' or 'NodeNext' which expect specific package.json exports.
88+
moduleResolution: ts.ModuleResolutionKind.Node10,
89+
strict: true,
90+
// TODO: enable once we fix the generated code
91+
noImplicitAny: false,
92+
target: ts.ScriptTarget.ES2015,
93+
types: [],
94+
},
95+
host: verifyHost,
96+
});
97+
98+
const verifyDiags = ts.getPreEmitDiagnostics(verifyProgram);
99+
100+
return {
101+
errors: verifyDiags.map((d) => {
102+
let message = ts.flattenDiagnosticMessageText(d.messageText, '\n');
103+
if (d.file) {
104+
const {line, character} = d.file.getLineAndCharacterOfPosition(d.start!);
105+
message = `${d.file.fileName} (${line + 1},${character + 1}): ${message}`;
106+
}
107+
return message;
108+
}),
109+
};
110+
}

0 commit comments

Comments
 (0)