mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 02:54:21 +08:00
refactor(@angular-devkit/build-angular): emit affected files as a group in esbuild builder
The internal emit strategy for the TypeScript/Angular compiler has been adjusted to prefill the memory cache during the initial phase of the build. Previously each file was emitted during the bundling process as requested by the bundler. This change has no immediate effect on the build process but enables future build performance improvements.
This commit is contained in:
parent
22c1cb66c5
commit
1333a4e8c0
@ -13,13 +13,11 @@ import { profileSync } from '../profiling';
|
||||
import type { AngularHostOptions } from './angular-host';
|
||||
|
||||
export interface EmitFileResult {
|
||||
content?: string;
|
||||
map?: string;
|
||||
dependencies: readonly string[];
|
||||
filename: string;
|
||||
contents: string;
|
||||
dependencies?: readonly string[];
|
||||
}
|
||||
|
||||
export type FileEmitter = (file: string) => Promise<EmitFileResult | undefined>;
|
||||
|
||||
export abstract class AngularCompilation {
|
||||
static #angularCompilerCliModule?: typeof ng;
|
||||
|
||||
@ -59,5 +57,5 @@ export abstract class AngularCompilation {
|
||||
|
||||
abstract collectDiagnostics(): Iterable<ts.Diagnostic>;
|
||||
|
||||
abstract createFileEmitter(onAfterEmit?: (sourceFile: ts.SourceFile) => void): FileEmitter;
|
||||
abstract emitAffectedFiles(): Iterable<EmitFileResult>;
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import type ng from '@angular/compiler-cli';
|
||||
import assert from 'node:assert';
|
||||
import ts from 'typescript';
|
||||
import { profileAsync, profileSync } from '../profiling';
|
||||
import { AngularCompilation, FileEmitter } from './angular-compilation';
|
||||
import { AngularCompilation, EmitFileResult } from './angular-compilation';
|
||||
import {
|
||||
AngularHostOptions,
|
||||
createAngularCompilerHost,
|
||||
@ -149,38 +149,52 @@ export class AotCompilation extends AngularCompilation {
|
||||
}
|
||||
}
|
||||
|
||||
createFileEmitter(onAfterEmit?: (sourceFile: ts.SourceFile) => void): FileEmitter {
|
||||
emitAffectedFiles(): Iterable<EmitFileResult> {
|
||||
assert(this.#state, 'Angular compilation must be initialized prior to emitting files.');
|
||||
const { angularCompiler, typeScriptProgram } = this.#state;
|
||||
const buildInfoFilename =
|
||||
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
|
||||
|
||||
const emittedFiles = new Map<ts.SourceFile, EmitFileResult>();
|
||||
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
|
||||
if (sourceFiles?.length === 0 && filename.endsWith(buildInfoFilename)) {
|
||||
// TODO: Store incremental build info
|
||||
return;
|
||||
}
|
||||
|
||||
assert(sourceFiles?.length === 1, 'Invalid TypeScript program emit for ' + filename);
|
||||
const sourceFile = sourceFiles[0];
|
||||
if (angularCompiler.ignoreForEmit.has(sourceFile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
emittedFiles.set(sourceFile, { filename: sourceFile.fileName, contents });
|
||||
};
|
||||
const transformers = mergeTransformers(angularCompiler.prepareEmit().transformers, {
|
||||
before: [replaceBootstrap(() => typeScriptProgram.getProgram().getTypeChecker())],
|
||||
});
|
||||
|
||||
return async (file: string) => {
|
||||
const sourceFile = typeScriptProgram.getSourceFile(file);
|
||||
if (!sourceFile) {
|
||||
return undefined;
|
||||
// TypeScript will loop until there are no more affected files in the program
|
||||
while (
|
||||
typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)
|
||||
) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
let content: string | undefined;
|
||||
typeScriptProgram.emit(
|
||||
sourceFile,
|
||||
(filename, data) => {
|
||||
if (/\.[cm]?js$/.test(filename)) {
|
||||
content = data;
|
||||
}
|
||||
},
|
||||
undefined /* cancellationToken */,
|
||||
undefined /* emitOnlyDtsFiles */,
|
||||
transformers,
|
||||
);
|
||||
if (angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
angularCompiler.incrementalCompilation.recordSuccessfulEmit(sourceFile);
|
||||
onAfterEmit?.(sourceFile);
|
||||
typeScriptProgram.emit(sourceFile, writeFileCallback, undefined, undefined, transformers);
|
||||
}
|
||||
|
||||
return { content, dependencies: [] };
|
||||
};
|
||||
return emittedFiles.values();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,6 @@ import type {
|
||||
Plugin,
|
||||
PluginBuild,
|
||||
} from 'esbuild';
|
||||
import * as assert from 'node:assert';
|
||||
import { realpath } from 'node:fs/promises';
|
||||
import { platform } from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
@ -30,7 +29,7 @@ import {
|
||||
resetCumulativeDurations,
|
||||
} from '../profiling';
|
||||
import { BundleStylesheetOptions, bundleComponentStylesheet } from '../stylesheets/bundle-options';
|
||||
import { AngularCompilation, FileEmitter } from './angular-compilation';
|
||||
import { AngularCompilation } from './angular-compilation';
|
||||
import { AngularHostOptions } from './angular-host';
|
||||
import { AotCompilation } from './aot-compilation';
|
||||
import { convertTypeScriptDiagnostic } from './diagnostics';
|
||||
@ -43,7 +42,7 @@ const WINDOWS_SEP_REGEXP = new RegExp(`\\${path.win32.sep}`, 'g');
|
||||
export class SourceFileCache extends Map<string, ts.SourceFile> {
|
||||
readonly modifiedFiles = new Set<string>();
|
||||
readonly babelFileCache = new Map<string, Uint8Array>();
|
||||
readonly typeScriptFileCache = new Map<string, Uint8Array>();
|
||||
readonly typeScriptFileCache = new Map<string, string | Uint8Array>();
|
||||
readonly loadResultCache = new MemoryLoadResultCache();
|
||||
|
||||
invalidate(files: Iterable<string>): void {
|
||||
@ -116,8 +115,11 @@ export function createCompilerPlugin(
|
||||
build.initialOptions.define[key] = value.toString();
|
||||
}
|
||||
|
||||
// The file emitter created during `onStart` that will be used during the build in `onLoad` callbacks for TS files
|
||||
let fileEmitter: FileEmitter | undefined;
|
||||
// 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<string, string | Uint8Array>();
|
||||
|
||||
// The stylesheet resources from component stylesheets that will be added to the build results output files
|
||||
let stylesheetResourceFiles: OutputFile[] = [];
|
||||
@ -178,7 +180,6 @@ export function createCompilerPlugin(
|
||||
// Initialize the Angular compilation for the current build.
|
||||
// In watch mode, previous build state will be reused.
|
||||
const {
|
||||
affectedFiles,
|
||||
compilerOptions: { allowJs },
|
||||
} = await compilation.initialize(tsconfigPath, hostOptions, (compilerOptions) => {
|
||||
if (
|
||||
@ -219,15 +220,6 @@ export function createCompilerPlugin(
|
||||
});
|
||||
shouldTsIgnoreJs = !allowJs;
|
||||
|
||||
// Clear affected files from the cache (if present)
|
||||
if (pluginOptions.sourceFileCache) {
|
||||
for (const affected of affectedFiles) {
|
||||
pluginOptions.sourceFileCache.typeScriptFileCache.delete(
|
||||
pathToFileURL(affected.fileName).href,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
profileSync('NG_DIAGNOSTICS_TOTAL', () => {
|
||||
for (const diagnostic of compilation.collectDiagnostics()) {
|
||||
const message = convertTypeScriptDiagnostic(diagnostic);
|
||||
@ -239,7 +231,10 @@ export function createCompilerPlugin(
|
||||
}
|
||||
});
|
||||
|
||||
fileEmitter = compilation.createFileEmitter();
|
||||
// Update TypeScript file output cache for all affected files
|
||||
for (const { filename, contents } of compilation.emitAffectedFiles()) {
|
||||
typeScriptFileCache.set(pathToFileURL(filename).href, contents);
|
||||
}
|
||||
|
||||
// Reset the setup warnings so that they are only shown during the first build.
|
||||
setupWarnings = undefined;
|
||||
@ -251,8 +246,6 @@ export function createCompilerPlugin(
|
||||
profileAsync(
|
||||
'NG_EMIT_TS*',
|
||||
async () => {
|
||||
assert.ok(fileEmitter, 'Invalid plugin execution order');
|
||||
|
||||
const request = pluginOptions.fileReplacements?.[args.path] ?? args.path;
|
||||
|
||||
// Skip TS load attempt if JS TypeScript compilation not enabled and file is JS
|
||||
@ -264,41 +257,35 @@ export function createCompilerPlugin(
|
||||
// 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 = pluginOptions.sourceFileCache?.typeScriptFileCache.get(
|
||||
pathToFileURL(request).href,
|
||||
);
|
||||
let contents = typeScriptFileCache.get(pathToFileURL(request).href);
|
||||
|
||||
if (contents === undefined) {
|
||||
const typescriptResult = await fileEmitter(request);
|
||||
if (!typescriptResult?.content) {
|
||||
// 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 && /\.[cm]?js$/.test(request)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Otherwise return an error
|
||||
return {
|
||||
errors: [
|
||||
createMissingFileError(
|
||||
request,
|
||||
args.path,
|
||||
build.initialOptions.absWorkingDir ?? '',
|
||||
),
|
||||
],
|
||||
};
|
||||
// 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 && /\.[cm]?js$/.test(request)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Otherwise return an error
|
||||
return {
|
||||
errors: [
|
||||
createMissingFileError(
|
||||
request,
|
||||
args.path,
|
||||
build.initialOptions.absWorkingDir ?? '',
|
||||
),
|
||||
],
|
||||
};
|
||||
} else if (typeof contents === 'string') {
|
||||
// A string indicates untransformed output from the TS/NG compiler
|
||||
contents = await javascriptTransformer.transformData(
|
||||
request,
|
||||
typescriptResult.content,
|
||||
contents,
|
||||
true /* skipLinker */,
|
||||
);
|
||||
|
||||
pluginOptions.sourceFileCache?.typeScriptFileCache.set(
|
||||
pathToFileURL(request).href,
|
||||
contents,
|
||||
);
|
||||
// Store as the returned Uint8Array to allow caching the fully transformed code
|
||||
typeScriptFileCache.set(pathToFileURL(request).href, contents);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -10,7 +10,7 @@ import type ng from '@angular/compiler-cli';
|
||||
import assert from 'node:assert';
|
||||
import ts from 'typescript';
|
||||
import { profileSync } from '../profiling';
|
||||
import { AngularCompilation, FileEmitter } from './angular-compilation';
|
||||
import { AngularCompilation, EmitFileResult } from './angular-compilation';
|
||||
import { AngularHostOptions, createAngularCompilerHost } from './angular-host';
|
||||
import { createJitResourceTransformer } from './jit-resource-transformer';
|
||||
|
||||
@ -83,41 +83,39 @@ export class JitCompilation extends AngularCompilation {
|
||||
yield* profileSync('NG_DIAGNOSTICS_SEMANTIC', () => typeScriptProgram.getSemanticDiagnostics());
|
||||
}
|
||||
|
||||
createFileEmitter(onAfterEmit?: (sourceFile: ts.SourceFile) => void): FileEmitter {
|
||||
emitAffectedFiles(): Iterable<EmitFileResult> {
|
||||
assert(this.#state, 'Compilation must be initialized prior to emitting files.');
|
||||
const {
|
||||
typeScriptProgram,
|
||||
constructorParametersDownlevelTransform,
|
||||
replaceResourcesTransform,
|
||||
} = this.#state;
|
||||
const buildInfoFilename =
|
||||
typeScriptProgram.getCompilerOptions().tsBuildInfoFile ?? '.tsbuildinfo';
|
||||
|
||||
const emittedFiles: EmitFileResult[] = [];
|
||||
const writeFileCallback: ts.WriteFileCallback = (filename, contents, _a, _b, sourceFiles) => {
|
||||
if (sourceFiles?.length === 0 && filename.endsWith(buildInfoFilename)) {
|
||||
// TODO: Store incremental build info
|
||||
return;
|
||||
}
|
||||
|
||||
assert(sourceFiles?.length === 1, 'Invalid TypeScript program emit for ' + filename);
|
||||
|
||||
emittedFiles.push({ filename: sourceFiles[0].fileName, contents });
|
||||
};
|
||||
const transformers = {
|
||||
before: [replaceResourcesTransform, constructorParametersDownlevelTransform],
|
||||
};
|
||||
|
||||
return async (file: string) => {
|
||||
const sourceFile = typeScriptProgram.getSourceFile(file);
|
||||
if (!sourceFile) {
|
||||
return undefined;
|
||||
}
|
||||
// TypeScript will loop until there are no more affected files in the program
|
||||
while (
|
||||
typeScriptProgram.emitNextAffectedFile(writeFileCallback, undefined, undefined, transformers)
|
||||
) {
|
||||
/* empty */
|
||||
}
|
||||
|
||||
let content: string | undefined;
|
||||
typeScriptProgram.emit(
|
||||
sourceFile,
|
||||
(filename, data) => {
|
||||
if (/\.[cm]?js$/.test(filename)) {
|
||||
content = data;
|
||||
}
|
||||
},
|
||||
undefined /* cancellationToken */,
|
||||
undefined /* emitOnlyDtsFiles */,
|
||||
transformers,
|
||||
);
|
||||
|
||||
onAfterEmit?.(sourceFile);
|
||||
|
||||
return { content, dependencies: [] };
|
||||
};
|
||||
return emittedFiles;
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user