@angular/build
Version:
Official build system for Angular
338 lines (337 loc) • 18.8 kB
JavaScript
"use strict";
/**
* @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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AotCompilation = void 0;
const node_assert_1 = __importDefault(require("node:assert"));
const node_path_1 = require("node:path");
const typescript_1 = __importDefault(require("typescript"));
const profiling_1 = require("../../esbuild/profiling");
const angular_host_1 = require("../angular-host");
const jit_bootstrap_transformer_1 = require("../transformers/jit-bootstrap-transformer");
const lazy_routes_transformer_1 = require("../transformers/lazy-routes-transformer");
const web_worker_transformer_1 = require("../transformers/web-worker-transformer");
const angular_compilation_1 = require("./angular-compilation");
const hmr_candidates_1 = require("./hmr-candidates");
/**
* The modified files count limit for performing component HMR analysis.
* Performing content analysis for a large amount of files can result in longer rebuild times
* than a full rebuild would entail.
*/
const HMR_MODIFIED_FILE_LIMIT = 32;
class AngularCompilationState {
angularProgram;
compilerHost;
typeScriptProgram;
affectedFiles;
templateDiagnosticsOptimization;
webWorkerTransform;
diagnosticCache;
constructor(angularProgram, compilerHost, typeScriptProgram, affectedFiles, templateDiagnosticsOptimization, webWorkerTransform, diagnosticCache = new WeakMap()) {
this.angularProgram = angularProgram;
this.compilerHost = compilerHost;
this.typeScriptProgram = typeScriptProgram;
this.affectedFiles = affectedFiles;
this.templateDiagnosticsOptimization = templateDiagnosticsOptimization;
this.webWorkerTransform = webWorkerTransform;
this.diagnosticCache = diagnosticCache;
}
get angularCompiler() {
return this.angularProgram.compiler;
}
}
class AotCompilation extends angular_compilation_1.AngularCompilation {
browserOnlyBuild;
#state;
constructor(browserOnlyBuild) {
super();
this.browserOnlyBuild = browserOnlyBuild;
}
async initialize(tsconfig, hostOptions, compilerOptionsTransformer) {
// Dynamically load the Angular compiler CLI package
const { NgtscProgram, OptimizeFor } = await angular_compilation_1.AngularCompilation.loadCompilerCli();
// Load the compiler configuration and transform as needed
const { options: originalCompilerOptions, rootNames, errors: configurationDiagnostics, } = await this.loadConfiguration(tsconfig);
const compilerOptions = compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
if (compilerOptions.externalRuntimeStyles) {
hostOptions.externalStylesheets ??= new Map();
}
// Reuse the package.json cache from the previous compilation
const packageJsonCache = this.#state?.compilerHost
.getModuleResolutionCache?.()
?.getPackageJsonInfoCache();
const useHmr = compilerOptions['_enableHmr'] &&
hostOptions.modifiedFiles &&
hostOptions.modifiedFiles.size <= HMR_MODIFIED_FILE_LIMIT;
let staleSourceFiles;
let clearPackageJsonCache = false;
if (hostOptions.modifiedFiles && this.#state) {
for (const modifiedFile of hostOptions.modifiedFiles) {
// Clear package.json cache if a node modules file was modified
if (!clearPackageJsonCache && modifiedFile.includes('node_modules')) {
clearPackageJsonCache = true;
packageJsonCache?.clear();
}
// Collect stale source files for HMR analysis of inline component resources
if (useHmr) {
const sourceFile = this.#state.typeScriptProgram.getSourceFile(modifiedFile);
if (sourceFile) {
staleSourceFiles ??= new Map();
staleSourceFiles.set(modifiedFile, sourceFile);
}
}
}
}
// Create Angular compiler host
const host = (0, angular_host_1.createAngularCompilerHost)(typescript_1.default, compilerOptions, hostOptions, packageJsonCache);
// Create the Angular specific program that contains the Angular compiler
const angularProgram = (0, profiling_1.profileSync)('NG_CREATE_PROGRAM', () => new NgtscProgram(rootNames, compilerOptions, host, this.#state?.angularProgram));
const angularCompiler = angularProgram.compiler;
const angularTypeScriptProgram = angularProgram.getTsProgram();
(0, angular_host_1.ensureSourceFileVersions)(angularTypeScriptProgram);
let oldProgram = this.#state?.typeScriptProgram;
let usingBuildInfo = false;
if (!oldProgram) {
oldProgram = typescript_1.default.readBuilderProgram(compilerOptions, host);
usingBuildInfo = !!oldProgram;
}
const typeScriptProgram = typescript_1.default.createEmitAndSemanticDiagnosticsBuilderProgram(angularTypeScriptProgram, host, oldProgram, configurationDiagnostics);
await (0, profiling_1.profileAsync)('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
let templateUpdates;
if (useHmr && hostOptions.modifiedFiles && this.#state) {
const componentNodes = (0, hmr_candidates_1.collectHmrCandidates)(hostOptions.modifiedFiles, angularProgram, staleSourceFiles);
for (const node of componentNodes) {
if (!typescript_1.default.isClassDeclaration(node)) {
continue;
}
const componentFilename = node.getSourceFile().fileName;
let relativePath = (0, node_path_1.relative)(host.getCurrentDirectory(), componentFilename);
if (relativePath.startsWith('..')) {
relativePath = componentFilename;
}
relativePath = relativePath.replaceAll('\\', '/');
const updateId = encodeURIComponent(`${host.getCanonicalFileName(relativePath)}@${node.name?.text}`);
const updateText = angularCompiler.emitHmrUpdateModule(node);
// If compiler cannot generate an update for the component, prevent template updates.
if (updateText === null) {
// Build is needed if a template cannot be updated
templateUpdates = undefined;
break;
}
templateUpdates ??= new Map();
templateUpdates.set(updateId, updateText);
}
}
const affectedFiles = (0, profiling_1.profileSync)('NG_FIND_AFFECTED', () => findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo));
// Get all files referenced in the TypeScript/Angular program including component resources
const referencedFiles = typeScriptProgram
.getSourceFiles()
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
.flatMap((sourceFile) => {
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
// Also invalidate Angular diagnostics for a source file if component resources are modified
if (this.#state && hostOptions.modifiedFiles?.size) {
for (const resourceDependency of resourceDependencies) {
if (hostOptions.modifiedFiles.has(resourceDependency)) {
this.#state.diagnosticCache.delete(sourceFile);
// Also mark as affected in case changed template affects diagnostics
affectedFiles.add(sourceFile);
}
}
}
return [sourceFile.fileName, ...resourceDependencies];
});
this.#state = new AngularCompilationState(angularProgram, host, typeScriptProgram, affectedFiles, affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram, (0, web_worker_transformer_1.createWorkerTransformer)(hostOptions.processWebWorker.bind(hostOptions)), this.#state?.diagnosticCache);
return {
affectedFiles,
compilerOptions,
referencedFiles,
externalStylesheets: hostOptions.externalStylesheets,
templateUpdates,
};
}
*collectDiagnostics(modes) {
(0, node_assert_1.default)(this.#state, 'Angular compilation must be initialized prior to collecting diagnostics.');
const { affectedFiles, angularCompiler, diagnosticCache, templateDiagnosticsOptimization, typeScriptProgram, } = this.#state;
const syntactic = modes & angular_compilation_1.DiagnosticModes.Syntactic;
const semantic = modes & angular_compilation_1.DiagnosticModes.Semantic;
// Collect program level diagnostics
if (modes & angular_compilation_1.DiagnosticModes.Option) {
yield* typeScriptProgram.getConfigFileParsingDiagnostics();
yield* angularCompiler.getOptionDiagnostics();
yield* typeScriptProgram.getOptionsDiagnostics();
}
if (syntactic) {
yield* typeScriptProgram.getGlobalDiagnostics();
}
// Collect source file specific diagnostics
for (const sourceFile of typeScriptProgram.getSourceFiles()) {
if (angularCompiler.ignoreForDiagnostics.has(sourceFile)) {
continue;
}
if (syntactic) {
// TypeScript will use cached diagnostics for files that have not been
// changed or affected for this build when using incremental building.
yield* (0, profiling_1.profileSync)('NG_DIAGNOSTICS_SYNTACTIC', () => typeScriptProgram.getSyntacticDiagnostics(sourceFile), true);
}
if (!semantic) {
continue;
}
yield* (0, profiling_1.profileSync)('NG_DIAGNOSTICS_SEMANTIC', () => typeScriptProgram.getSemanticDiagnostics(sourceFile), true);
// 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 = (0, profiling_1.profileSync)('NG_DIAGNOSTICS_TEMPLATE', () => angularCompiler.getDiagnosticsForFile(sourceFile, templateDiagnosticsOptimization), true);
diagnosticCache.set(sourceFile, angularDiagnostics);
yield* angularDiagnostics;
}
else {
const angularDiagnostics = diagnosticCache.get(sourceFile);
if (angularDiagnostics) {
yield* angularDiagnostics;
}
}
}
}
emitAffectedFiles() {
(0, node_assert_1.default)(this.#state, 'Angular compilation must be initialized prior to emitting files.');
const { affectedFiles, angularCompiler, compilerHost, typeScriptProgram, webWorkerTransform } = this.#state;
const compilerOptions = typeScriptProgram.getCompilerOptions();
const buildInfoFilename = compilerOptions.tsBuildInfoFile ?? '.tsbuildinfo';
const useTypeScriptTranspilation = !compilerOptions.isolatedModules ||
!!compilerOptions.sourceMap ||
!!compilerOptions.inlineSourceMap;
const emittedFiles = new Map();
const writeFileCallback = (filename, contents, _a, _b, sourceFiles) => {
if (!sourceFiles?.length && filename.endsWith(buildInfoFilename)) {
// Save builder info contents to specified location
compilerHost.writeFile(filename, contents, false);
return;
}
(0, node_assert_1.default)(sourceFiles?.length === 1, 'Invalid TypeScript program emit for ' + filename);
const sourceFile = typescript_1.default.getOriginalNode(sourceFiles[0], typescript_1.default.isSourceFile);
if (angularCompiler.ignoreForEmit.has(sourceFile)) {
return;
}
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
};
const transformers = angularCompiler.prepareEmit().transformers;
transformers.before ??= [];
transformers.before.push((0, jit_bootstrap_transformer_1.replaceBootstrap)(() => typeScriptProgram.getProgram().getTypeChecker()), webWorkerTransform);
if (!this.browserOnlyBuild) {
transformers.before.push((0, lazy_routes_transformer_1.lazyRoutesTransformer)(compilerOptions, compilerHost));
}
// Emit is handled in write file callback when using TypeScript
if (useTypeScriptTranspilation) {
// TypeScript will loop until there are no more affected files in the program
while (typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)) {
/* empty */
}
}
else if (compilerOptions.tsBuildInfoFile) {
// Manually get the builder state for the persistent cache
// The TypeScript API currently embeds this behavior inside the program emit
// via emitNextAffectedFile but that also applies all internal transforms.
const programWithGetState = typeScriptProgram.getProgram();
(0, node_assert_1.default)(typeof programWithGetState.emitBuildInfo === 'function', 'TypeScript program emitBuildInfo is missing.');
programWithGetState.emitBuildInfo();
}
// Angular may have files that must be emitted but TypeScript does not consider affected
for (const sourceFile of typeScriptProgram.getSourceFiles()) {
if (emittedFiles.has(sourceFile) || angularCompiler.ignoreForEmit.has(sourceFile)) {
continue;
}
if (sourceFile.isDeclarationFile) {
continue;
}
if (angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile) &&
!affectedFiles.has(sourceFile)) {
continue;
}
if (useTypeScriptTranspilation) {
typeScriptProgram.emit(sourceFile, writeFileCallback, undefined, undefined, transformers);
continue;
}
// When not using TypeScript transpilation, directly apply only Angular specific transformations
const transformResult = typescript_1.default.transform(sourceFile, [
...(transformers.before ?? []),
...(transformers.after ?? []),
], compilerOptions);
(0, node_assert_1.default)(transformResult.transformed.length === 1, 'TypeScript transforms should not produce multiple outputs for ' + sourceFile.fileName);
let contents;
if (sourceFile === transformResult.transformed[0]) {
// Use original content if no changes were made
contents = sourceFile.text;
}
else {
// Otherwise, print the transformed source file
const printer = typescript_1.default.createPrinter(compilerOptions, transformResult);
contents = printer.printFile(transformResult.transformed[0]);
}
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
}
return emittedFiles.values();
}
}
exports.AotCompilation = AotCompilation;
function findAffectedFiles(builder, { ignoreForDiagnostics }, includeTTC) {
const affectedFiles = new Set();
// 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.
// A TTC shim is a file that has been ignored for diagnostics and has a filename ending in `.ngtypecheck.ts`.
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);
}
// 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 (includeTTC) {
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);
}
}
}
}
return affectedFiles;
}