diff --git a/packages/ngtools/webpack/src/ivy/host.ts b/packages/ngtools/webpack/src/ivy/host.ts index 01f8aa541449..d832e0adde09 100644 --- a/packages/ngtools/webpack/src/ivy/host.ts +++ b/packages/ngtools/webpack/src/ivy/host.ts @@ -90,6 +90,78 @@ function augmentResolveModuleNames( } } +/** + * Augments a TypeScript Compiler Host's resolveModuleNames function to collect dependencies + * of the containing file passed to the resolveModuleNames function. This process assumes + * that consumers of the Compiler Host will only call resolveModuleNames with modules that are + * actually present in a containing file. + * This process is a workaround for gathering a TypeScript SourceFile's dependencies as there + * is no currently exposed public method to do so. A BuilderProgram does have a `getAllDependencies` + * function. However, that function returns all transitive dependencies as well which can cause + * excessive Webpack rebuilds. + * + * @param host The CompilerHost to augment. + * @param dependencies A Map which will be used to store file dependencies. + * @param moduleResolutionCache An optional resolution cache to use when the host resolves a module. + */ +export function augmentHostWithDependencyCollection( + host: ts.CompilerHost, + dependencies: Map>, + moduleResolutionCache?: ts.ModuleResolutionCache, +): void { + if (host.resolveModuleNames) { + const baseResolveModuleNames = host.resolveModuleNames; + host.resolveModuleNames = function (moduleNames: string[], containingFile: string, ...parameters) { + const results = baseResolveModuleNames.call(host, moduleNames, containingFile, ...parameters); + + const containingFilePath = normalizePath(containingFile); + for (const result of results) { + if (result) { + const containingFileDependencies = dependencies.get(containingFilePath); + if (containingFileDependencies) { + containingFileDependencies.add(result.resolvedFileName); + } else { + dependencies.set(containingFilePath, new Set([result.resolvedFileName])); + } + } + } + + return results; + }; + } else { + host.resolveModuleNames = function ( + moduleNames: string[], + containingFile: string, + _reusedNames: string[] | undefined, + redirectedReference: ts.ResolvedProjectReference | undefined, + options: ts.CompilerOptions, + ) { + return moduleNames.map((name) => { + const result = ts.resolveModuleName( + name, + containingFile, + options, + host, + moduleResolutionCache, + redirectedReference, + ).resolvedModule; + + if (result) { + const containingFilePath = normalizePath(containingFile); + const containingFileDependencies = dependencies.get(containingFilePath); + if (containingFileDependencies) { + containingFileDependencies.add(result.resolvedFileName); + } else { + dependencies.set(containingFilePath, new Set([result.resolvedFileName])); + } + } + + return result; + }); + }; + } +} + export function augmentHostWithNgcc( host: ts.CompilerHost, ngcc: NgccProcessor, diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index 0c5685cf6e33..1216a0ef3e87 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -24,6 +24,7 @@ import { SourceFileCache } from './cache'; import { DiagnosticsReporter, createDiagnosticsReporter } from './diagnostics'; import { augmentHostWithCaching, + augmentHostWithDependencyCollection, augmentHostWithNgcc, augmentHostWithReplacements, augmentHostWithResources, @@ -90,6 +91,7 @@ export class AngularWebpackPlugin { private builder?: ts.EmitAndSemanticDiagnosticsBuilderProgram; private sourceFileCache?: SourceFileCache; private buildTimestamp!: number; + private readonly fileDependencies = new Map>(); private readonly requiredFilesToEmit = new Set(); private readonly requiredFilesToEmitCache = new Map(); private readonly fileEmitHistory = new Map(); @@ -195,6 +197,11 @@ export class AngularWebpackPlugin { if (cache) { // Invalidate existing cache based on compiler file timestamps changedFiles = cache.invalidate(compiler.fileTimestamps, this.buildTimestamp); + + // Invalidate file dependencies of changed files + for (const changedFile of changedFiles) { + this.fileDependencies.delete(normalizePath(changedFile)); + } } else { // Initialize a new cache cache = new SourceFileCache(); @@ -212,6 +219,9 @@ export class AngularWebpackPlugin { compilerOptions, ); + // Setup source file dependency collection + augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache); + // Setup on demand ngcc augmentHostWithNgcc(host, ngccProcessor, moduleResolutionCache); @@ -589,7 +599,7 @@ export class AngularWebpackPlugin { } const dependencies = [ - ...program.getAllDependencies(sourceFile), + ...this.fileDependencies.get(filePath) || [], ...getExtraDependencies(sourceFile), ].map(externalizePath);