From 95aa2b8f925ee295b8edf659b5d8e706d122ffec Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 10 Mar 2021 20:39:38 -0500 Subject: [PATCH] perf(@ngtools/webpack): avoid adding transitive dependencies to Webpack's dependency graph This change augments a TypeScript Compiler Host's resolveModuleNames function to collect dependencies of the containing file based on the module names passed to the resolveModuleNames function. This process assumes that consumers of the Compiler Host will call resolveModuleNames with modules that are actually present in a containing file. The TypeScript compiler exhibits such behavior making this process effective at generating a set of all direct dependencies for a given source 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 especially in larger programs. --- packages/ngtools/webpack/src/ivy/host.ts | 72 ++++++++++++++++++++++ packages/ngtools/webpack/src/ivy/plugin.ts | 12 +++- 2 files changed, 83 insertions(+), 1 deletion(-) 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);