Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -158,11 +158,48 @@ describe('AngularCompilation', () => {
suppressOutputPathCheck: true,
outDir: undefined,
}),
undefined,
jasmine.any(Map),
);
expect(result.rootNames).toEqual(['/src/main.ts']);
expect(result.compilerOptions.target).toBe(ts.ScriptTarget.ES2022);
expect(result.compilerOptions.inlineSources).toBe(true);
expect(result.warnings.length).toBeGreaterThan(0);
expect(result.tsConfigFiles).toEqual(['tsconfig.json']);
});

it('caches transformed configuration when rootFiles are provided and invalidates on config change', async () => {
const compilation = new MockTypeScriptCompilation();
const mockReadConfig = jasmine.createSpy('readConfiguration').and.returnValue({
options: { target: ts.ScriptTarget.ES2020 },
rootNames: ['/src/main.ts'],
errors: [],
});
spyOn(TypeScriptCompilation, 'loadCompilerCli').and.resolveTo({
readConfiguration: mockReadConfig,
} as unknown as typeof import('@angular/compiler-cli'));

const overrides: CompilerOptionOverrides = { rootFiles: ['/src/main.ts'] };

const result1 = await compilation.testLoadConfiguration('tsconfig.json', overrides);
expect(mockReadConfig).toHaveBeenCalledTimes(1);

// Re-loading with same rootFiles should return cached configuration without calling readConfiguration
const result2 = await compilation.testLoadConfiguration('tsconfig.json', overrides);
expect(mockReadConfig).toHaveBeenCalledTimes(1);
expect(result2).toBe(result1);

// Invalidation of non-config file should keep cache intact
await compilation.update?.(new Set(['/src/main.ts']));
const result3 = await compilation.testLoadConfiguration('tsconfig.json', overrides);
expect(mockReadConfig).toHaveBeenCalledTimes(1);
expect(result3).toBe(result1);

// Invalidation of tsconfig file should clear cache
await compilation.update?.(new Set(['tsconfig.json']));
const result4 = await compilation.testLoadConfiguration('tsconfig.json', overrides);
expect(mockReadConfig).toHaveBeenCalledTimes(2);
expect(result4).toBeDefined();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export class AotCompilation extends TypeScriptCompilation {
rootNames,
errors: configurationDiagnostics,
warnings,
tsConfigFiles,
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);

const useTypeScriptTranspilation =
Expand Down Expand Up @@ -209,25 +210,28 @@ export class AotCompilation extends TypeScriptCompilation {
const componentResourcesDependencies = new Map<string, string[]>();

// 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);
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
// 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);
const referencedFiles = [
...tsConfigFiles,
...typeScriptProgram
.getSourceFiles()
.filter((sourceFile) => !angularCompiler.ignoreForEmit.has(sourceFile))
.flatMap((sourceFile) => {
const resourceDependencies = angularCompiler.getResourceDependencies(sourceFile);
componentResourcesDependencies.set(sourceFile.fileName, resourceDependencies);
// 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];
});
return [sourceFile.fileName, ...resourceDependencies];
}),
];

this.#state = new AngularCompilationState(
angularProgram,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export class JitCompilation extends TypeScriptCompilation {
rootNames,
errors: configurationDiagnostics,
warnings,
tsConfigFiles,
} = await this.loadConfiguration(tsconfig, compilerOptionOverrides, buildType);

if (hostOptions.modifiedFiles) {
Expand Down Expand Up @@ -89,9 +90,10 @@ export class JitCompilation extends TypeScriptCompilation {
createWorkerTransformer(hostOptions.processWebWorker.bind(hostOptions)),
);

const referencedFiles = typeScriptProgram
.getSourceFiles()
.map((sourceFile) => sourceFile.fileName);
const referencedFiles = [
...tsConfigFiles,
...typeScriptProgram.getSourceFiles().map((sourceFile) => sourceFile.fileName),
];

return { compilerOptions, referencedFiles, warnings };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,15 @@ export interface TransformedConfiguration {
rootNames: string[];
errors: ts.Diagnostic[];
warnings: PartialMessage[];
tsConfigFiles: readonly string[];
}

export abstract class TypeScriptCompilation extends AngularCompilation {
static #angularCompilerCliModule?: typeof ng;
#cachedConfiguration?: TransformedConfiguration;
Comment thread
alan-agius4 marked this conversation as resolved.
#cachedRootFiles?: readonly string[];
#tsConfigFiles?: Set<string>;
readonly #extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();

static async loadCompilerCli(): Promise<typeof ng> {
TypeScriptCompilation.#angularCompilerCliModule ??= await import('@angular/compiler-cli');
Expand All @@ -36,31 +41,60 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
compilerOptionOverrides?: CompilerOptionOverrides,
buildType: 'application' | 'library' = 'application',
): Promise<TransformedConfiguration> {
const { readConfiguration } = await TypeScriptCompilation.loadCompilerCli();
// When `rootFiles` are explicitly provided (e.g., library builder), avoid re-parsing `tsconfig.json`
// and walking the project directory tree via `readConfiguration` on every watch rebuild (~200-350ms on large libraries).
const currentRootFiles = compilerOptionOverrides?.rootFiles;
if (
currentRootFiles &&
this.#cachedConfiguration &&
this.#cachedRootFiles &&
this.#cachedRootFiles.length === currentRootFiles.length &&
this.#cachedRootFiles.every((file, index) => file === currentRootFiles[index])
) {
return this.#cachedConfiguration;
}

// TODO(alanagius): remove casting when @angular/compiler-cli exports the correct typings.
const { readConfiguration } = (await TypeScriptCompilation.loadCompilerCli()) as typeof ng & {
readConfiguration(
project: string,
existingOptions?: ng.CompilerOptions,
host?: unknown,
extendedConfigCache?: Map<string, ts.ExtendedConfigCacheEntry>,
): ng.ParsedConfiguration;
};

const {
options: originalCompilerOptions,
rootNames: originalRootNames,
errors,
} = profileSync('NG_READ_CONFIG', () =>
readConfiguration(tsconfig, {
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
suppressOutputPathCheck: true,
outDir: undefined,
sourceMap: false,
declaration: false,
declarationMap: false,
allowEmptyCodegenFiles: false,
annotationsAs: 'decorators',
enableResourceInlining: false,
supportTestBed: false,
supportJitMode: false,
// Disable removing of comments as TS is quite aggressive with these and can
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
removeComments: false,
}),
readConfiguration(
tsconfig,
{
// Angular specific configuration defaults and overrides to ensure a functioning compilation.
suppressOutputPathCheck: true,
outDir: undefined,
sourceMap: false,
declaration: false,
declarationMap: false,
allowEmptyCodegenFiles: false,
annotationsAs: 'decorators',
enableResourceInlining: false,
supportTestBed: false,
supportJitMode: false,
// Disable removing of comments as TS is quite aggressive with these and can
// remove important annotations, such as /* @__PURE__ */ and comments like /* vite-ignore */.
removeComments: false,
},
undefined,
this.#extendedConfigCache,
),
);

const tsConfigFiles = [toPosixPath(tsconfig), ...this.#extendedConfigCache.keys()];
this.#tsConfigFiles = new Set(tsConfigFiles);

let rootNames = originalRootNames;
if (compilerOptionOverrides?.rootFiles?.length) {
const rootFilesSet = new Set(
Expand All @@ -82,19 +116,55 @@ export abstract class TypeScriptCompilation extends AngularCompilation {
buildType,
);

return {
const config: TransformedConfiguration = {
compilerOptions,
rootNames,
errors,
warnings,
tsConfigFiles,
};

if (currentRootFiles?.length) {
this.#cachedConfiguration = config;
this.#cachedRootFiles = currentRootFiles;
}

return config;
}

protected readonly sourceFiles = new Map<string, ts.SourceFile>();

protected invalidateFiles(files: Iterable<string>): void {
const tsConfigFiles = this.#tsConfigFiles;

for (const file of files) {
this.sourceFiles.delete(toPosixPath(file));
const posixFile = toPosixPath(file);
this.sourceFiles.delete(posixFile);

if (!tsConfigFiles) {
continue;
}

let tsConfigCachedPath: string | undefined;
if (tsConfigFiles.has(posixFile)) {
tsConfigCachedPath = posixFile;
} else {
// Check with lowercased key because TypeScript lowercases the keys
// of the extended config cache on case-insensitive operating systems.
const lowerCasedPosixFile = posixFile.toLowerCase();
if (tsConfigFiles.has(lowerCasedPosixFile)) {
tsConfigCachedPath = lowerCasedPosixFile;
}
}

if (!tsConfigCachedPath) {
continue;
}

// If a tsconfig changes, we need to re-read the configuration.
this.#cachedConfiguration = undefined;
this.#cachedRootFiles = undefined;
this.#extendedConfigCache.delete(tsConfigCachedPath);
}
}

Expand Down
Loading