@angular/build
Version:
Official build system for Angular
603 lines (602 loc) • 34.3 kB
JavaScript
;
/**
* @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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createCompilerPlugin = createCompilerPlugin;
const node_assert_1 = __importDefault(require("node:assert"));
const node_crypto_1 = require("node:crypto");
const path = __importStar(require("node:path"));
const environment_options_1 = require("../../../utils/environment-options");
const compilation_1 = require("../../angular/compilation");
const javascript_transformer_1 = require("../javascript-transformer");
const load_result_cache_1 = require("../load-result-cache");
const profiling_1 = require("../profiling");
const compilation_state_1 = require("./compilation-state");
const file_reference_tracker_1 = require("./file-reference-tracker");
const jit_plugin_callbacks_1 = require("./jit-plugin-callbacks");
// eslint-disable-next-line max-lines-per-function
function createCompilerPlugin(pluginOptions, compilationOrFactory, stylesheetBundler) {
return {
name: 'angular-compiler',
// eslint-disable-next-line max-lines-per-function
async setup(build) {
let setupWarnings = [];
const preserveSymlinks = build.initialOptions.preserveSymlinks;
// Initialize a worker pool for JavaScript transformations.
// Webcontainers currently do not support this persistent cache store.
let cacheStore;
if (pluginOptions.sourceFileCache?.persistentCachePath && !process.versions.webcontainer) {
try {
const { LmbdCacheStore } = await Promise.resolve().then(() => __importStar(require('../lmdb-cache-store')));
cacheStore = new LmbdCacheStore(path.join(pluginOptions.sourceFileCache.persistentCachePath, 'angular-compiler.db'));
}
catch (e) {
setupWarnings.push({
text: 'Unable to initialize JavaScript cache storage.',
location: null,
notes: [
// Only show first line of lmdb load error which has platform support listed
{ text: e?.message.split('\n')[0] ?? `${e}` },
{
text: 'This will not affect the build output content but may result in slower builds.',
},
],
});
}
}
const javascriptTransformer = new javascript_transformer_1.JavaScriptTransformer({
sourcemap: !!pluginOptions.sourcemap,
thirdPartySourcemaps: pluginOptions.thirdPartySourcemaps,
advancedOptimizations: pluginOptions.advancedOptimizations,
jit: pluginOptions.jit || pluginOptions.includeTestMetadata,
}, environment_options_1.maxWorkers, cacheStore?.createCache('jstransformer'));
// Setup defines based on the values used by the Angular compiler-cli
build.initialOptions.define ??= {};
build.initialOptions.define['ngI18nClosureMode'] ??= 'false';
// The factory is only relevant for compatibility purposes with the private API.
// TODO: Update private API in the next major to allow compilation function factory removal here.
const compilation = typeof compilationOrFactory === 'function'
? await compilationOrFactory()
: compilationOrFactory;
// The in-memory cache of TypeScript file outputs will be used during the build in `onLoad` callbacks for TS files.
// A string value indicates direct TS/NG output and a Uint8Array indicates fully transformed code.
const typeScriptFileCache = pluginOptions.sourceFileCache?.typeScriptFileCache ??
new Map();
// The resources from component stylesheets and web workers that will be added to the build results output files
const additionalResults = new Map();
// Compilation is initially assumed to have errors until emitted
let hasCompilationErrors = true;
// Determines if TypeScript should process JavaScript files based on tsconfig `allowJs` option
let shouldTsIgnoreJs = true;
// Determines if transpilation should be handle by TypeScript or esbuild
let useTypeScriptTranspilation = true;
let sharedTSCompilationState;
// To fully invalidate files, track resource referenced files and their referencing source
const referencedFileTracker = new file_reference_tracker_1.FileReferenceTracker();
// eslint-disable-next-line max-lines-per-function
build.onStart(async () => {
sharedTSCompilationState = (0, compilation_state_1.getSharedCompilationState)();
if (!(compilation instanceof compilation_1.NoopCompilation)) {
sharedTSCompilationState.markAsInProgress();
}
const result = {
warnings: setupWarnings,
};
// Reset debug performance tracking
(0, profiling_1.resetCumulativeDurations)();
// Update the reference tracker and generate a full set of modified files for the
// Angular compiler which does not have direct knowledge of transitive resource
// dependencies or web worker processing.
let modifiedFiles;
if (!(compilation instanceof compilation_1.NoopCompilation) &&
pluginOptions.sourceFileCache?.modifiedFiles.size) {
// TODO: Differentiate between changed input files and stale output files
modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles);
pluginOptions.sourceFileCache.invalidate(modifiedFiles);
// External runtime styles are invalidated and rebuilt at the beginning of a rebuild to avoid
// the need to execute the application bundler for component style only changes.
if (!pluginOptions.externalRuntimeStyles) {
stylesheetBundler.invalidate(modifiedFiles);
}
// Remove any stale additional results based on modified files
modifiedFiles.forEach((file) => additionalResults.delete(file));
}
if (compilation.update && pluginOptions.sourceFileCache?.modifiedFiles.size) {
await compilation.update(modifiedFiles ?? pluginOptions.sourceFileCache.modifiedFiles);
}
// Create Angular compiler host options
const hostOptions = {
fileReplacements: pluginOptions.fileReplacements,
modifiedFiles,
sourceFileCache: pluginOptions.sourceFileCache,
async transformStylesheet(data, containingFile, stylesheetFile, order, className) {
let stylesheetResult;
let resultSource = stylesheetFile ?? containingFile;
// Stylesheet file only exists for external stylesheets
if (stylesheetFile) {
stylesheetResult = await stylesheetBundler.bundleFile(stylesheetFile);
}
else {
stylesheetResult = await stylesheetBundler.bundleInline(data, containingFile,
// Inline stylesheets from a template style element are always CSS; Otherwise, use default.
containingFile.endsWith('.html') ? 'css' : undefined,
// When external runtime styles are enabled, an identifier for the style that does not change
// based on the content is required to avoid emitted JS code changes. Any JS code changes will
// invalid the output and force a full page reload for HMR cases. The containing file and order
// of the style within the containing file is used.
pluginOptions.externalRuntimeStyles
? (0, node_crypto_1.createHash)('sha256')
.update(containingFile)
.update((order ?? 0).toString())
.update(className ?? '')
.digest('hex')
: undefined);
// Adjust result source for inline styles.
// There may be multiple inline styles with the same containing file and to ensure that the results
// do not overwrite each other the result source identifier needs to be unique for each. The class
// name and order fields can be used for this. The structure is arbitrary as long as it is unique.
resultSource += `?class=${className}&order=${order}`;
}
(result.warnings ??= []).push(...stylesheetResult.warnings);
if (stylesheetResult.errors) {
(result.errors ??= []).push(...stylesheetResult.errors);
return '';
}
const { contents, outputFiles, metafile, referencedFiles } = stylesheetResult;
additionalResults.set(resultSource, {
outputFiles,
metafile,
});
if (referencedFiles) {
referencedFileTracker.add(containingFile, referencedFiles);
if (stylesheetFile) {
// Angular AOT compiler needs modified direct resource files to correctly invalidate its analysis
referencedFileTracker.add(stylesheetFile, referencedFiles);
}
}
return contents;
},
processWebWorker(workerFile, containingFile) {
const fullWorkerPath = path.join(path.dirname(containingFile), workerFile);
// The synchronous API must be used due to the TypeScript compilation currently being
// fully synchronous and this process callback being called from within a TypeScript
// transformer.
const workerResult = bundleWebWorker(build, pluginOptions, fullWorkerPath);
(result.warnings ??= []).push(...workerResult.warnings);
if (workerResult.errors.length > 0) {
(result.errors ??= []).push(...workerResult.errors);
// Track worker file errors to allow rebuilds on changes
referencedFileTracker.add(containingFile, workerResult.errors
.map((error) => error.location?.file)
.filter((file) => !!file)
.map((file) => path.join(build.initialOptions.absWorkingDir ?? '', file)));
additionalResults.set(fullWorkerPath, { errors: result.errors });
// Return the original path if the build failed
return workerFile;
}
(0, node_assert_1.default)('outputFiles' in workerResult, 'Invalid web worker bundle result.');
additionalResults.set(fullWorkerPath, {
outputFiles: workerResult.outputFiles,
metafile: workerResult.metafile,
});
referencedFileTracker.add(containingFile, Object.keys(workerResult.metafile.inputs).map((input) => path.join(build.initialOptions.absWorkingDir ?? '', input)));
// Return bundled worker file entry name to be used in the built output
const workerCodeFile = workerResult.outputFiles.find((file) => /^worker-[A-Z0-9]{8}.[cm]?js$/.test(path.basename(file.path)));
(0, node_assert_1.default)(workerCodeFile, 'Web Worker bundled code file should always be present.');
const workerCodePath = path.relative(build.initialOptions.outdir ?? '', workerCodeFile.path);
return workerCodePath.replaceAll('\\', '/');
},
};
// Initialize the Angular compilation for the current build.
// In watch mode, previous build state will be reused.
let referencedFiles;
let externalStylesheets;
try {
const initializationResult = await compilation.initialize(pluginOptions.tsconfig, hostOptions, createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, build.initialOptions.conditions));
shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
// Isolated modules option ensures safe non-TypeScript transpilation.
// Typescript printing support for sourcemaps is not yet integrated.
useTypeScriptTranspilation =
!initializationResult.compilerOptions.isolatedModules ||
!!initializationResult.compilerOptions.sourceMap ||
!!initializationResult.compilerOptions.inlineSourceMap;
referencedFiles = initializationResult.referencedFiles;
externalStylesheets = initializationResult.externalStylesheets;
if (initializationResult.templateUpdates) {
// Propagate any template updates
initializationResult.templateUpdates.forEach((value, key) => pluginOptions.templateUpdates?.set(key, value));
}
}
catch (error) {
(result.errors ??= []).push({
text: 'Angular compilation initialization failed.',
location: null,
notes: [
{
text: error instanceof Error ? (error.stack ?? error.message) : `${error}`,
location: null,
},
],
});
// Initialization failure prevents further compilation steps
hasCompilationErrors = true;
return result;
}
if (compilation instanceof compilation_1.NoopCompilation) {
hasCompilationErrors = await sharedTSCompilationState.waitUntilReady;
return result;
}
if (externalStylesheets) {
// Process any new external stylesheets
for (const [stylesheetFile, externalId] of externalStylesheets) {
await bundleExternalStylesheet(stylesheetBundler, stylesheetFile, externalId, result, additionalResults);
}
}
// Update TypeScript file output cache for all affected files
try {
await (0, profiling_1.profileAsync)('NG_EMIT_TS', async () => {
for (const { filename, contents } of await compilation.emitAffectedFiles()) {
typeScriptFileCache.set(path.normalize(filename), contents);
}
});
}
catch (error) {
(result.errors ??= []).push({
text: 'Angular compilation emit failed.',
location: null,
notes: [
{
text: error instanceof Error ? (error.stack ?? error.message) : `${error}`,
location: null,
},
],
});
}
const diagnostics = await compilation.diagnoseFiles(environment_options_1.useTypeChecking ? compilation_1.DiagnosticModes.All : compilation_1.DiagnosticModes.All & ~compilation_1.DiagnosticModes.Semantic);
if (diagnostics.errors?.length) {
(result.errors ??= []).push(...diagnostics.errors);
}
if (diagnostics.warnings?.length) {
(result.warnings ??= []).push(...diagnostics.warnings);
}
// Add errors from failed additional results.
// This must be done after emit to capture latest web worker results.
for (const { errors } of additionalResults.values()) {
if (errors) {
(result.errors ??= []).push(...errors);
}
}
// Store referenced files for updated file watching if enabled
if (pluginOptions.sourceFileCache) {
pluginOptions.sourceFileCache.referencedFiles = [
...referencedFiles,
...referencedFileTracker.referencedFiles,
];
}
hasCompilationErrors = !!result.errors?.length;
// Reset the setup warnings so that they are only shown during the first build.
setupWarnings = undefined;
sharedTSCompilationState.markAsReady(hasCompilationErrors);
return result;
});
build.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
const request = path.normalize(pluginOptions.fileReplacements?.[path.normalize(args.path)] ?? args.path);
const isJS = /\.[cm]?js$/.test(request);
// Skip TS load attempt if JS TypeScript compilation not enabled and file is JS
if (shouldTsIgnoreJs && isJS) {
return undefined;
}
// The filename is currently used as a cache key. Since the cache is memory only,
// the options cannot change and do not need to be represented in the key. If the
// cache is later stored to disk, then the options that affect transform output
// would need to be added to the key as well as a check for any change of content.
let contents = typeScriptFileCache.get(request);
if (contents === undefined) {
// If the Angular compilation had errors the file may not have been emitted.
// To avoid additional errors about missing files, return empty contents.
if (hasCompilationErrors) {
return { contents: '', loader: 'js' };
}
// No TS result indicates the file is not part of the TypeScript program.
// If allowJs is enabled and the file is JS then defer to the next load hook.
if (!shouldTsIgnoreJs && isJS) {
return undefined;
}
// Otherwise return an error
return {
errors: [
createMissingFileError(request, args.path, build.initialOptions.absWorkingDir ?? ''),
],
};
}
else if (typeof contents === 'string' && (useTypeScriptTranspilation || isJS)) {
// A string indicates untransformed output from the TS/NG compiler.
// This step is unneeded when using esbuild transpilation.
const sideEffects = await hasSideEffects(request);
const instrumentForCoverage = pluginOptions.instrumentForCoverage?.(request);
contents = await javascriptTransformer.transformData(request, contents, true /* skipLinker */, sideEffects, instrumentForCoverage);
// Store as the returned Uint8Array to allow caching the fully transformed code
typeScriptFileCache.set(request, contents);
}
let loader;
if (useTypeScriptTranspilation || isJS) {
// TypeScript has transpiled to JS or is already JS
loader = 'js';
}
else if (request.at(-1) === 'x') {
// TSX and TS have different syntax rules. Only set if input is a TSX file.
loader = 'tsx';
}
else {
// Otherwise, directly bundle TS
loader = 'ts';
}
return {
contents,
loader,
};
});
build.onLoad({ filter: /\.[cm]?js$/ }, (0, load_result_cache_1.createCachedLoad)(pluginOptions.loadResultCache, async (args) => {
let request = args.path;
if (pluginOptions.fileReplacements) {
const replacement = pluginOptions.fileReplacements[path.normalize(args.path)];
if (replacement) {
request = path.normalize(replacement);
}
}
return (0, profiling_1.profileAsync)('NG_EMIT_JS*', async () => {
const sideEffects = await hasSideEffects(request);
const contents = await javascriptTransformer.transformFile(request, pluginOptions.jit, sideEffects);
return {
contents,
loader: 'js',
watchFiles: request !== args.path ? [request] : undefined,
};
}, true);
}));
// Add a load handler if there are file replacement option entries for JSON files
if (pluginOptions.fileReplacements &&
Object.keys(pluginOptions.fileReplacements).some((value) => value.endsWith('.json'))) {
build.onLoad({ filter: /\.json$/ }, (0, load_result_cache_1.createCachedLoad)(pluginOptions.loadResultCache, async (args) => {
const replacement = pluginOptions.fileReplacements?.[path.normalize(args.path)];
if (replacement) {
return {
contents: await Promise.resolve().then(() => __importStar(require('node:fs/promises'))).then(({ readFile }) => readFile(path.normalize(replacement))),
loader: 'json',
watchFiles: [replacement],
};
}
// If no replacement defined, let esbuild handle it directly
return null;
}));
}
// Setup bundling of component templates and stylesheets when in JIT mode
if (pluginOptions.jit) {
(0, jit_plugin_callbacks_1.setupJitPluginCallbacks)(build, stylesheetBundler, additionalResults, pluginOptions.loadResultCache);
}
build.onEnd((result) => {
// Ensure other compilations are unblocked if the main compilation throws during start
sharedTSCompilationState?.markAsReady(hasCompilationErrors);
for (const { outputFiles, metafile } of additionalResults.values()) {
// Add any additional output files to the main output files
if (outputFiles?.length) {
result.outputFiles?.push(...outputFiles);
}
// Combine additional metafiles with main metafile
if (result.metafile && metafile) {
// Append the existing object, by appending to it we prevent unnecessary new objections creations with spread
// mitigating significant performance overhead for large apps.
// See: https://bugs.chromium.org/p/v8/issues/detail?id=11536
Object.assign(result.metafile.inputs, metafile.inputs);
Object.assign(result.metafile.outputs, metafile.outputs);
}
}
(0, profiling_1.logCumulativeDurations)();
});
build.onDispose(() => {
sharedTSCompilationState?.dispose();
void compilation.close?.();
void cacheStore?.close();
});
/**
* Checks if the file has side-effects when `advancedOptimizations` is enabled.
*/
async function hasSideEffects(path) {
if (!pluginOptions.advancedOptimizations) {
return undefined;
}
const { sideEffects } = await build.resolve(path, {
kind: 'import-statement',
resolveDir: build.initialOptions.absWorkingDir ?? '',
});
return sideEffects;
}
},
};
}
async function bundleExternalStylesheet(stylesheetBundler, stylesheetFile, externalId, result, additionalResults) {
const styleResult = await stylesheetBundler.bundleFile(stylesheetFile, externalId);
(result.warnings ??= []).push(...styleResult.warnings);
if (styleResult.errors) {
(result.errors ??= []).push(...styleResult.errors);
}
else {
const { outputFiles, metafile } = styleResult;
// Clear inputs to prevent triggering a rebuild of the application code for component
// stylesheet file only changes when the dev server enables the internal-only external
// stylesheet option. This does not affect builds since only the dev server can enable
// the internal option.
metafile.inputs = {};
additionalResults.set(stylesheetFile, {
outputFiles,
metafile,
});
}
}
function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, customConditions) {
return (compilerOptions) => {
// target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
if (compilerOptions.target === undefined || compilerOptions.target < 9 /** ES2022 */) {
// If 'useDefineForClassFields' is already defined in the users project leave the value as is.
// Otherwise fallback to false due to https://github.com/microsoft/TypeScript/issues/45995
// which breaks the deprecated `@Effects` NGRX decorator and potentially other existing code as well.
compilerOptions.target = 9 /** ES2022 */;
compilerOptions.useDefineForClassFields ??= false;
// Only add the warning on the initial build
setupWarnings?.push({
text: `TypeScript compiler options 'target' and 'useDefineForClassFields' are set to 'ES2022' and ` +
`'false' respectively by the Angular CLI.`,
location: { file: pluginOptions.tsconfig },
notes: [
{
text: 'To control ECMA version and features use the Browserslist configuration. ' +
'For more information, see https://angular.dev/tools/cli/build#configuring-browser-compatibility',
},
],
});
}
if (compilerOptions.compilationMode === 'partial') {
setupWarnings?.push({
text: 'Angular partial compilation mode is not supported when building applications.',
location: null,
notes: [{ text: 'Full compilation mode will be used instead.' }],
});
compilerOptions.compilationMode = 'full';
}
// Enable incremental compilation by default if caching is enabled and incremental is not explicitly disabled
if (compilerOptions.incremental !== false &&
pluginOptions.sourceFileCache?.persistentCachePath) {
compilerOptions.incremental = true;
// Set the build info file location to the configured cache directory
compilerOptions.tsBuildInfoFile = path.join(pluginOptions.sourceFileCache?.persistentCachePath, '.tsbuildinfo');
}
else {
compilerOptions.incremental = false;
}
if (compilerOptions.module === undefined || compilerOptions.module < 5 /** ES2015 */) {
compilerOptions.module = 7; /** ES2022 */
setupWarnings?.push({
text: `TypeScript compiler options 'module' values 'CommonJS', 'UMD', 'System' and 'AMD' are not supported.`,
location: null,
notes: [{ text: `The 'module' option will be set to 'ES2022' instead.` }],
});
}
if (compilerOptions.isolatedModules && compilerOptions.emitDecoratorMetadata) {
setupWarnings?.push({
text: `TypeScript compiler option 'isolatedModules' may prevent the 'emitDecoratorMetadata' option from emitting all metadata.`,
location: null,
notes: [
{
text: `The 'emitDecoratorMetadata' option is not required by Angular` +
'and can be removed if not explictly required by the project.',
},
],
});
}
// Synchronize custom resolve conditions.
// Set if using the supported bundler resolution mode (bundler is the default in new projects)
if (compilerOptions.moduleResolution === 100 /* ModuleResolutionKind.Bundler */ ||
compilerOptions.module === 200 /** ModuleKind.Preserve */) {
compilerOptions.customConditions = customConditions;
}
return {
...compilerOptions,
noEmitOnError: false,
composite: false,
inlineSources: !!pluginOptions.sourcemap,
inlineSourceMap: !!pluginOptions.sourcemap,
sourceMap: undefined,
mapRoot: undefined,
sourceRoot: undefined,
preserveSymlinks,
externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
_enableHmr: !!pluginOptions.templateUpdates,
supportTestBed: !!pluginOptions.includeTestMetadata,
supportJitMode: !!pluginOptions.includeTestMetadata,
};
};
}
function bundleWebWorker(build, pluginOptions, workerFile) {
try {
return build.esbuild.buildSync({
...build.initialOptions,
platform: 'browser',
write: false,
bundle: true,
metafile: true,
format: 'esm',
entryNames: 'worker-[hash]',
entryPoints: [workerFile],
sourcemap: pluginOptions.sourcemap,
// Zone.js is not used in Web workers so no need to disable
supported: undefined,
// Plugins are not supported in sync esbuild calls
plugins: undefined,
});
}
catch (error) {
if (error && typeof error === 'object' && 'errors' in error && 'warnings' in error) {
return error;
}
throw error;
}
}
function createMissingFileError(request, original, root) {
const relativeRequest = path.relative(root, request);
const error = {
text: `File '${relativeRequest}' is missing from the TypeScript compilation.`,
notes: [
{
text: `Ensure the file is part of the TypeScript program via the 'files' or 'include' property.`,
},
],
};
const relativeOriginal = path.relative(root, original);
if (relativeRequest !== relativeOriginal) {
error.notes.push({
text: `File is requested from a file replacement of '${relativeOriginal}'.`,
});
}
return error;
}