/** * Adapted from the original ng-packagr source. * * Changes made: * - Remove ngccProcessor (Angular < 16) * - Use custom cacheCompilerHost instead of the one provided by ng-packagr. * - Support Angular Compiler `incrementalDriver` for Angular < 16. */ import type { CompilerOptions, ParsedConfiguration, } from '@angular/compiler-cli'; import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; import { EntryPointNode, PackageNode, isEntryPointInProgress, isPackage, } from 'ng-packagr/lib/ng-package/nodes'; import { augmentProgramWithVersioning } from 'ng-packagr/lib/ts/cache-compiler-host'; import * as log from 'ng-packagr/lib/utils/log'; import { join } from 'node:path'; import * as ts from 'typescript'; import { getInstalledAngularVersionInfo } from '../../../utilities/angular-version-utils'; import { ngCompilerCli } from '../../../utilities/ng-compiler-cli'; import { NgPackagrOptions } from '../ng-package/options.di'; import { StylesheetProcessor } from '../styles/stylesheet-processor'; import { cacheCompilerHost } from '../ts/cache-compiler-host'; export async function compileSourceFiles( graph: BuildGraph, tsConfig: ParsedConfiguration, moduleResolutionCache: ts.ModuleResolutionCache, options: NgPackagrOptions, extraOptions?: Partial, stylesheetProcessor?: StylesheetProcessor ) { const { NgtscProgram, formatDiagnostics } = await ngCompilerCli(); const { cacheDirectory, watch, cacheEnabled } = options; const tsConfigOptions: CompilerOptions = { ...tsConfig.options, ...extraOptions, }; const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress()); const ngPackageNode: PackageNode = graph.find(isPackage); const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage; const cacheDir = cacheEnabled && cacheDirectory; if (cacheDir) { tsConfigOptions.incremental ??= true; tsConfigOptions.tsBuildInfoFile ??= join( cacheDir, `tsbuildinfo/${entryPoint.data.entryPoint.flatModuleFile}.tsbuildinfo` ); } const tsCompilerHost = cacheCompilerHost( graph, entryPoint, tsConfigOptions, moduleResolutionCache, stylesheetProcessor, inlineStyleLanguage ); const cache = entryPoint.cache; const sourceFileCache = cache.sourcesFileCache; let usingBuildInfo = false; let oldBuilder = cache.oldBuilder; if (!oldBuilder && cacheDir) { oldBuilder = ts.readBuilderProgram(tsConfigOptions, tsCompilerHost); usingBuildInfo = true; } // Create the Angular specific program that contains the Angular compiler const angularProgram = new NgtscProgram( tsConfig.rootNames, tsConfigOptions, tsCompilerHost, cache.oldNgtscProgram ); const angularCompiler = angularProgram.compiler; const { ignoreForDiagnostics, ignoreForEmit } = angularCompiler; // SourceFile versions are required for builder programs. // The wrapped host inside NgtscProgram adds additional files that will not have versions. const typeScriptProgram = angularProgram.getTsProgram(); augmentProgramWithVersioning(typeScriptProgram); let builder: ts.BuilderProgram | ts.EmitAndSemanticDiagnosticsBuilderProgram; if (watch || cacheDir) { builder = cache.oldBuilder = ts.createEmitAndSemanticDiagnosticsBuilderProgram( typeScriptProgram, tsCompilerHost, oldBuilder ); cache.oldNgtscProgram = angularProgram; } else { builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram( typeScriptProgram, tsCompilerHost ); } // Update semantic diagnostics cache const affectedFiles = new Set(); // Analyze affected files when in watch mode for incremental type checking if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) { // eslint-disable-next-line no-constant-condition while (true) { const result = builder.getSemanticDiagnosticsOfNextAffectedFile( undefined, (sourceFile) => { // If the affected file is a TTC shim, add the shim's original source file. // This ensures that changes that affect TTC are typechecked even when the changes // are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes. // For example, changing @Input property types of a directive used in another component's // template. if ( ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts') ) { // This file name conversion relies on internal compiler logic and should be converted // to an official method when available. 15 is length of `.ngtypecheck.ts` const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts'; const originalSourceFile = builder.getSourceFile(originalFilename); if (originalSourceFile) { affectedFiles.add(originalSourceFile); } return true; } return false; } ); if (!result) { break; } affectedFiles.add(result.affected as ts.SourceFile); } // Add all files with associated template type checking files. // Stored TS build info does not have knowledge of the AOT compiler or the typechecking state of the templates. // To ensure that errors are reported correctly, all AOT component diagnostics need to be analyzed even if build // info is present. if (usingBuildInfo) { for (const sourceFile of builder.getSourceFiles()) { if ( ignoreForDiagnostics.has(sourceFile) && sourceFile.fileName.endsWith('.ngtypecheck.ts') ) { // This file name conversion relies on internal compiler logic and should be converted // to an official method when available. 15 is length of `.ngtypecheck.ts` const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts'; const originalSourceFile = builder.getSourceFile(originalFilename); if (originalSourceFile) { affectedFiles.add(originalSourceFile); } } } } } // Collect program level diagnostics const allDiagnostics: ts.Diagnostic[] = [ ...angularCompiler.getOptionDiagnostics(), ...builder.getOptionsDiagnostics(), ...builder.getGlobalDiagnostics(), ]; // Required to support asynchronous resource loading // Must be done before creating transformers or getting template diagnostics await angularCompiler.analyzeAsync(); // Collect source file specific diagnostics for (const sourceFile of builder.getSourceFiles()) { if (ignoreForDiagnostics.has(sourceFile)) { continue; } allDiagnostics.push( ...builder.getDeclarationDiagnostics(sourceFile), ...builder.getSyntacticDiagnostics(sourceFile), ...builder.getSemanticDiagnostics(sourceFile) ); // Declaration files cannot have template diagnostics if (sourceFile.isDeclarationFile) { continue; } // Only request Angular template diagnostics for affected files to avoid // overhead of template diagnostics for unchanged files. if (affectedFiles.has(sourceFile)) { const angularDiagnostics = angularCompiler.getDiagnosticsForFile( sourceFile, affectedFiles.size === 1 ? /** OptimizeFor.SingleFile **/ 0 : /** OptimizeFor.WholeProgram */ 1 ); allDiagnostics.push(...angularDiagnostics); sourceFileCache.updateAngularDiagnostics(sourceFile, angularDiagnostics); } } const otherDiagnostics = []; const errorDiagnostics = []; for (const diagnostic of allDiagnostics) { if (diagnostic.category === ts.DiagnosticCategory.Error) { errorDiagnostics.push(diagnostic); } else { otherDiagnostics.push(diagnostic); } } if (otherDiagnostics.length) { log.msg(formatDiagnostics(errorDiagnostics)); } if (errorDiagnostics.length) { throw new Error(formatDiagnostics(errorDiagnostics)); } const transformers = angularCompiler.prepareEmit().transformers; if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) { while ( builder.emitNextAffectedFile( (fileName, data, writeByteOrderMark, onError, sourceFiles) => { if (fileName.endsWith('.tsbuildinfo')) { tsCompilerHost.writeFile( fileName, data, writeByteOrderMark, onError, sourceFiles ); } } ) ) { // empty } } const angularVersion = getInstalledAngularVersionInfo(); const incrementalCompilation: typeof angularCompiler.incrementalCompilation = angularVersion.major < 16 ? (angularCompiler as any).incrementalDriver : angularCompiler.incrementalCompilation; for (const sourceFile of builder.getSourceFiles()) { if (ignoreForEmit.has(sourceFile)) { continue; } if (incrementalCompilation.safeToSkipEmit(sourceFile)) { continue; } builder.emit(sourceFile, undefined, undefined, undefined, transformers); incrementalCompilation.recordSuccessfulEmit(sourceFile); } }