/** * @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 ts from 'typescript'; import {NgCompiler, NgCompilerHost} from './core'; import {NgCompilerOptions} from './core/api'; import {TrackedIncrementalBuildStrategy} from './incremental'; import {ActivePerfRecorder, PerfPhase} from './perf'; import {TsCreateProgramDriver} from './program_driver'; import { CompilationTicket, freshCompilationTicket, incrementalFromCompilerTicket, } from './core/src/compiler'; /** * A driver for the Angular Compiler that performs "Source-to-Source" transformation. * * Unlike `NgtscProgram`, this driver does NOT use `program.emit()`. Instead, it: * 1. Analyzes the program using `NgCompiler`. * 2. Manually runs `ts.transform` with Angular's Ivy transformers. * 3. Prints the transformed AST back to a TypeScript string. * * This mode is designed for a mode where the Angular Compiler * acts as a pre-processor for a downstream TypeScript compiler. */ export class NgtscIsolatedPreprocessor { readonly compiler: NgCompiler; private tsProgram: ts.Program; private host: NgCompilerHost; private incrementalStrategy: TrackedIncrementalBuildStrategy; constructor( rootNames: ReadonlyArray, private options: NgCompilerOptions, delegateHost: ts.CompilerHost, oldProgram?: NgtscIsolatedPreprocessor, ) { // Enable type reification for source-to-source transformation this.options = {...options, '_experimentalEmitIntermediateTs': true}; const perfRecorder = ActivePerfRecorder.zeroedToNow(); perfRecorder.phase(PerfPhase.Setup); const reuseProgram = oldProgram?.compiler.getCurrentProgram(); this.host = NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null); this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram); const programDriver = new TsCreateProgramDriver( this.tsProgram, this.host, this.options, this.host.shimExtensionPrefixes, ); this.incrementalStrategy = oldProgram !== undefined ? oldProgram.incrementalStrategy.toNextBuildStrategy() : new TrackedIncrementalBuildStrategy(); let ticket: CompilationTicket; if (oldProgram === undefined) { ticket = freshCompilationTicket( this.tsProgram, this.options, this.incrementalStrategy, programDriver, perfRecorder, /* enableTemplateTypeChecker */ !!this.options._enableTemplateTypeChecker, /* usePoisonedData */ false, ); } else { ticket = incrementalFromCompilerTicket( oldProgram.compiler, this.tsProgram, this.incrementalStrategy, programDriver, new Set(), // TODO: track modified resource files perfRecorder, ); } this.compiler = NgCompiler.fromTicket(ticket, this.host); } transformAndPrint(): {fileName: string; content: string}[] { // 1. Generate TCBs this.compiler['ensureAnalyzed']().templateTypeChecker.generateAllTypeCheckBlocks(); const transformers = this.compiler.prepareEmit().transformers; // We only care about 'before' transformers for source-to-source const beforeTransformers = (transformers.before || []) as unknown as ts.TransformerFactory[]; const result: {fileName: string; content: string}[] = []; const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false, }); const program = this.compiler.getCurrentProgram(); for (const sf of program.getSourceFiles()) { if (sf.isDeclarationFile) { continue; } // If it is a TCB file (ends in .ngtypecheck.ts), we want it as-is, without any transformations. if (sf.fileName.endsWith('.ngtypecheck.ts')) { const content = printer.printFile(sf); result.push({fileName: sf.fileName, content}); continue; } // Manually transform the source file const transformationResult = ts.transform(sf, beforeTransformers, this.options); if (transformationResult.transformed.length !== 1) { transformationResult.dispose(); throw new Error( `Expected exactly one transformed file, got ${transformationResult.transformed.length}`, ); } const transformedSf = transformationResult.transformed[0]; const content = printer.printFile(transformedSf); result.push({fileName: sf.fileName, content}); transformationResult.dispose(); this.compiler.incrementalCompilation.recordSuccessfulEmit(sf); } return result; } }