diff --git a/packages/ngtools/webpack/src/ivy/host.ts b/packages/ngtools/webpack/src/ivy/host.ts index fe33df7f5f73..0b44c8bb15a9 100644 --- a/packages/ngtools/webpack/src/ivy/host.ts +++ b/packages/ngtools/webpack/src/ivy/host.ts @@ -11,7 +11,6 @@ import type { CompilerHost } from '@angular/compiler-cli'; import { createHash } from 'crypto'; import * as path from 'path'; import * as ts from 'typescript'; -import { NgccProcessor } from '../ngcc_processor'; import { WebpackResourceLoader } from '../resource_loader'; import { normalizePath } from './paths'; @@ -191,67 +190,6 @@ export function augmentHostWithDependencyCollection( } } -export function augmentHostWithNgcc( - host: ts.CompilerHost, - ngcc: NgccProcessor, - moduleResolutionCache?: ts.ModuleResolutionCache, -): void { - augmentResolveModuleNames( - host, - (resolvedModule, moduleName) => { - if (resolvedModule && ngcc) { - ngcc.processModule(moduleName, resolvedModule); - } - - return resolvedModule; - }, - moduleResolutionCache, - ); - - if (host.resolveTypeReferenceDirectives) { - const baseResolveTypeReferenceDirectives = host.resolveTypeReferenceDirectives; - host.resolveTypeReferenceDirectives = function ( - names: string[] | ts.FileReference[], - ...parameters - ) { - return names.map((name) => { - const fileName = typeof name === 'string' ? name : name.fileName; - const result = baseResolveTypeReferenceDirectives.call(host, [fileName], ...parameters); - - if (result[0] && ngcc) { - ngcc.processModule(fileName, result[0]); - } - - return result[0]; - }); - }; - } else { - host.resolveTypeReferenceDirectives = function ( - moduleNames: string[] | ts.FileReference[], - containingFile: string, - redirectedReference: ts.ResolvedProjectReference | undefined, - options: ts.CompilerOptions, - ) { - return moduleNames.map((name) => { - const fileName = typeof name === 'string' ? name : name.fileName; - const result = ts.resolveTypeReferenceDirective( - fileName, - containingFile, - options, - host, - redirectedReference, - ).resolvedTypeReferenceDirective; - - if (result && ngcc) { - ngcc.processModule(fileName, result); - } - - return result; - }); - }; - } -} - export function augmentHostWithReplacements( host: ts.CompilerHost, replacements: Record, diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts index 0d51e5580695..3153d3276a94 100644 --- a/packages/ngtools/webpack/src/ivy/plugin.ts +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -10,7 +10,6 @@ import type { CompilerHost, CompilerOptions, NgtscProgram } from '@angular/compi import { strict as assert } from 'assert'; import * as ts from 'typescript'; import type { Compilation, Compiler, Module, NormalModule } from 'webpack'; -import { NgccProcessor } from '../ngcc_processor'; import { TypeScriptPathsPlugin } from '../paths-plugin'; import { WebpackResourceLoader } from '../resource_loader'; import { SourceFileCache } from './cache'; @@ -23,7 +22,6 @@ import { import { augmentHostWithCaching, augmentHostWithDependencyCollection, - augmentHostWithNgcc, augmentHostWithReplacements, augmentHostWithResources, augmentHostWithSubstitutions, @@ -57,48 +55,11 @@ export interface AngularWebpackPluginOptions { * The Angular compilation state that is maintained across each Webpack compilation. */ interface AngularCompilationState { - ngccProcessor?: NgccProcessor; resourceLoader?: WebpackResourceLoader; previousUnused?: Set; pathsPlugin: TypeScriptPathsPlugin; } -function initializeNgccProcessor( - compiler: Compiler, - tsconfig: string, - compilerNgccModule: typeof import('@angular/compiler-cli/ngcc') | undefined, -): { processor: NgccProcessor; errors: string[]; warnings: string[] } { - const { inputFileSystem, options: webpackOptions } = compiler; - const mainFields = webpackOptions.resolve?.mainFields?.flat() ?? []; - - const errors: string[] = []; - const warnings: string[] = []; - const resolver = compiler.resolverFactory.get('normal', { - // Caching must be disabled because it causes the resolver to become async after a rebuild - cache: false, - extensions: ['.json'], - useSyncFileSystemCalls: true, - }); - - // The compilerNgccModule field is guaranteed to be defined during a compilation - // due to the `beforeCompile` hook. Usage of this property accessor prior to the - // hook execution is an implementation error. - assert.ok(compilerNgccModule, `'@angular/compiler-cli/ngcc' used prior to Webpack compilation.`); - - const processor = new NgccProcessor( - compilerNgccModule, - mainFields, - warnings, - errors, - compiler.context, - tsconfig, - inputFileSystem, - resolver, - ); - - return { processor, errors, warnings }; -} - const PLUGIN_NAME = 'angular-compiler'; const compilationFileEmitters = new WeakMap(); @@ -110,7 +71,6 @@ interface FileEmitHistoryItem { export class AngularWebpackPlugin { private readonly pluginOptions: AngularWebpackPluginOptions; private compilerCliModule?: typeof import('@angular/compiler-cli'); - private compilerNgccModule?: typeof import('@angular/compiler-cli/ngcc'); private watchMode?: boolean; private ngtscNextProgram?: NgtscProgram; private builder?: ts.EmitAndSemanticDiagnosticsBuilderProgram; @@ -163,21 +123,13 @@ export class AngularWebpackPlugin { // Set resolver options const pathsPlugin = new TypeScriptPathsPlugin(); compiler.hooks.afterResolvers.tap(PLUGIN_NAME, (compiler) => { - // When Ivy is enabled we need to add the fields added by NGCC - // to take precedence over the provided mainFields. - // NGCC adds fields in package.json suffixed with '_ivy_ngcc' - // Example: module -> module__ivy_ngcc compiler.resolverFactory.hooks.resolveOptions .for('normal') .tap(PLUGIN_NAME, (resolveOptions) => { - const originalMainFields = resolveOptions.mainFields; - const ivyMainFields = originalMainFields?.flat().map((f) => `${f}_ivy_ngcc`) ?? []; - resolveOptions.plugins ??= []; resolveOptions.plugins.push(pathsPlugin); - // https://github.com/webpack/webpack/issues/11635#issuecomment-707016779 - return util.cleverMerge(resolveOptions, { mainFields: [...ivyMainFields, '...'] }); + return resolveOptions; }); }); @@ -216,21 +168,6 @@ export class AngularWebpackPlugin { state.resourceLoader = new WebpackResourceLoader(this.watchMode); } - // Initialize and process eager ngcc if not already setup - if (!state.ngccProcessor) { - const { processor, errors, warnings } = initializeNgccProcessor( - compiler, - this.pluginOptions.tsconfig, - this.compilerNgccModule, - ); - - processor.process(); - warnings.forEach((warning) => addWarning(compilation, warning)); - errors.forEach((error) => addError(compilation, error)); - - state.ngccProcessor = processor; - } - // Setup and read TypeScript and Angular compiler configuration const { compilerOptions, rootNames, errors } = this.loadConfiguration(); @@ -284,9 +221,6 @@ export class AngularWebpackPlugin { // Setup source file dependency collection augmentHostWithDependencyCollection(host, this.fileDependencies, moduleResolutionCache); - // Setup on demand ngcc - augmentHostWithNgcc(host, state.ngccProcessor, moduleResolutionCache); - // Setup resource loading state.resourceLoader.update(compilation, changedFiles); augmentHostWithResources(host, state.resourceLoader, { @@ -760,7 +694,6 @@ export class AngularWebpackPlugin { // Once TypeScript provides support for keeping the dynamic import this workaround can // be dropped. this.compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)(); - this.compilerNgccModule = await new Function(`return import('@angular/compiler-cli/ngcc');`)(); } private async addFileEmitHistory( diff --git a/packages/ngtools/webpack/src/ngcc_processor.ts b/packages/ngtools/webpack/src/ngcc_processor.ts deleted file mode 100644 index 03dd89114997..000000000000 --- a/packages/ngtools/webpack/src/ngcc_processor.ts +++ /dev/null @@ -1,304 +0,0 @@ -/** - * @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.io/license - */ - -import type { LogLevel, Logger } from '@angular/compiler-cli/ngcc'; -import { spawnSync } from 'child_process'; -import { createHash } from 'crypto'; -import { accessSync, constants, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; -import * as path from 'path'; -import * as ts from 'typescript'; -import type { Compiler } from 'webpack'; -import { time, timeEnd } from './benchmark'; -import { InputFileSystem } from './ivy/system'; - -// Extract Resolver type from Webpack types since it is not directly exported -type ResolverWithOptions = ReturnType; - -// We cannot create a plugin for this, because NGTSC requires addition type -// information which ngcc creates when processing a package which was compiled with NGC. - -// Example of such errors: -// ERROR in node_modules/@angular/platform-browser/platform-browser.d.ts(42,22): -// error TS-996002: Appears in the NgModule.imports of AppModule, -// but could not be resolved to an NgModule class - -// We now transform a package and it's typings when NGTSC is resolving a module. - -export class NgccProcessor { - private _processedModules = new Set(); - private _logger: NgccLogger; - private _nodeModulesDirectory: string | null; - - constructor( - private readonly compilerNgcc: typeof import('@angular/compiler-cli/ngcc'), - private readonly propertiesToConsider: string[], - private readonly compilationWarnings: (Error | string)[], - private readonly compilationErrors: (Error | string)[], - private readonly basePath: string, - private readonly tsConfigPath: string, - private readonly inputFileSystem: InputFileSystem, - private readonly resolver: ResolverWithOptions, - ) { - this._logger = new NgccLogger( - this.compilationWarnings, - this.compilationErrors, - compilerNgcc.LogLevel.info, - ); - this._nodeModulesDirectory = this.findNodeModulesDirectory(this.basePath); - } - - /** Process the entire node modules tree. */ - process() { - // Under Bazel when running in sandbox mode parts of the filesystem is read-only, or when using - // Yarn PnP there may not be a node_modules directory. ngcc can't run in those cases, so the - // processing is skipped. - if (process.env.BAZEL_TARGET || !this._nodeModulesDirectory) { - return; - } - - // Skip if node_modules are read-only - const corePackage = this.tryResolvePackage('@angular/core', this._nodeModulesDirectory); - if (corePackage && isReadOnlyFile(corePackage)) { - return; - } - - // Perform a ngcc run check to determine if an initial execution is required. - // If a run hash file exists that matches the current package manager lock file and the - // project's tsconfig, then an initial ngcc run has already been performed. - let skipProcessing = false; - let runHashFilePath: string | undefined; - const runHashBasePath = path.join(this._nodeModulesDirectory, '.cli-ngcc'); - const projectBasePath = path.join(this._nodeModulesDirectory, '..'); - try { - let ngccConfigData; - try { - ngccConfigData = readFileSync(path.join(projectBasePath, 'ngcc.config.js')); - } catch { - ngccConfigData = ''; - } - - const relativeTsconfigPath = path.relative(projectBasePath, this.tsConfigPath); - const tsconfigData = readFileSync(this.tsConfigPath); - const { lockFileData, lockFilePath } = this.findPackageManagerLockFile(projectBasePath); - - // Generate a hash that represents the state of the package lock file and used tsconfig - const runHash = createHash('sha256') - .update(lockFileData) - .update(lockFilePath) - .update(ngccConfigData) - .update(tsconfigData) - .update(relativeTsconfigPath) - .digest('hex'); - - // The hash is used directly in the file name to mitigate potential read/write race - // conditions as well as to only require a file existence check - runHashFilePath = path.join(runHashBasePath, runHash + '.lock'); - - // If the run hash lock file exists, then ngcc was already run against this project state - if (existsSync(runHashFilePath)) { - skipProcessing = true; - } - } catch { - // Any error means an ngcc execution is needed - } - - if (skipProcessing) { - return; - } - - const timeLabel = 'NgccProcessor.process'; - time(timeLabel); - - // We spawn instead of using the API because: - // - NGCC Async uses clustering which is problematic when used via the API which means - // that we cannot setup multiple cluster masters with different options. - // - We will not be able to have concurrent builds otherwise Ex: App-Shell, - // as NGCC will create a lock file for both builds and it will cause builds to fails. - const originalProcessTitle = process.title; - try { - const { status, error } = spawnSync( - process.execPath, - [ - this.compilerNgcc.ngccMainFilePath, - '--source' /** basePath */, - this._nodeModulesDirectory, - '--properties' /** propertiesToConsider */, - ...this.propertiesToConsider, - '--first-only' /** compileAllFormats */, - '--create-ivy-entry-points' /** createNewEntryPointFormats */, - '--async', - '--tsconfig' /** tsConfigPath */, - this.tsConfigPath, - '--use-program-dependencies', - ], - { - stdio: ['inherit', process.stderr, process.stderr], - }, - ); - - if (status !== 0) { - const errorMessage = error?.message || ''; - throw new Error(errorMessage + `NGCC failed${errorMessage ? ', see above' : ''}.`); - } - } finally { - process.title = originalProcessTitle; - } - - timeEnd(timeLabel); - - // ngcc was successful so if a run hash was generated, write it for next time - if (runHashFilePath) { - try { - if (!existsSync(runHashBasePath)) { - mkdirSync(runHashBasePath, { recursive: true }); - } - writeFileSync(runHashFilePath, ''); - } catch { - // Errors are non-fatal - } - } - } - - /** Process a module and its dependencies. */ - processModule( - moduleName: string, - resolvedModule: ts.ResolvedModule | ts.ResolvedTypeReferenceDirective, - ): void { - const resolvedFileName = resolvedModule.resolvedFileName; - if ( - !this._nodeModulesDirectory || - !resolvedFileName || - moduleName.startsWith('.') || - this._processedModules.has(resolvedFileName) - ) { - // Skip when module_modules directory is not present, module is unknown, relative or the - // NGCC compiler is not found or already processed. - return; - } - - const packageJsonPath = this.tryResolvePackage(moduleName, resolvedFileName); - // If the package.json is read only we should skip calling NGCC. - // With Bazel when running under sandbox the filesystem is read-only. - if (!packageJsonPath || isReadOnlyFile(packageJsonPath)) { - // add it to processed so the second time round we skip this. - this._processedModules.add(resolvedFileName); - - return; - } - - const timeLabel = `NgccProcessor.processModule.ngcc.process+${moduleName}`; - time(timeLabel); - this.compilerNgcc.process({ - basePath: this._nodeModulesDirectory, - targetEntryPointPath: path.dirname(packageJsonPath), - propertiesToConsider: this.propertiesToConsider, - compileAllFormats: false, - createNewEntryPointFormats: true, - logger: this._logger, - tsConfigPath: this.tsConfigPath, - }); - timeEnd(timeLabel); - - // Purge this file from cache, since NGCC add new mainFields. Ex: module_ivy_ngcc - // which are unknown in the cached file. - this.inputFileSystem.purge?.(packageJsonPath); - - this._processedModules.add(resolvedFileName); - } - - invalidate(fileName: string) { - this._processedModules.delete(fileName); - } - - /** - * Try resolve a package.json file from the resolved .d.ts file. - */ - private tryResolvePackage(moduleName: string, resolvedFileName: string): string | undefined { - try { - const resolvedPath = this.resolver.resolveSync( - {}, - resolvedFileName, - `${moduleName}/package.json`, - ); - - return resolvedPath || undefined; - } catch { - // Ex: @angular/compiler/src/i18n/i18n_ast/package.json - // or local libraries which don't reside in node_modules - const packageJsonPath = path.resolve(resolvedFileName, '../package.json'); - - return existsSync(packageJsonPath) ? packageJsonPath : undefined; - } - } - - private findNodeModulesDirectory(startPoint: string): string | null { - let current = startPoint; - while (path.dirname(current) !== current) { - const nodePath = path.join(current, 'node_modules'); - if (existsSync(nodePath)) { - return nodePath; - } - - current = path.dirname(current); - } - - return null; - } - - private findPackageManagerLockFile(projectBasePath: string): { - lockFilePath: string; - lockFileData: Buffer; - } { - for (const lockFile of ['yarn.lock', 'pnpm-lock.yaml', 'package-lock.json']) { - const lockFilePath = path.join(projectBasePath, lockFile); - - try { - return { - lockFilePath, - lockFileData: readFileSync(lockFilePath), - }; - } catch {} - } - - throw new Error('Cannot locate a package manager lock file.'); - } -} - -class NgccLogger implements Logger { - constructor( - private readonly compilationWarnings: (Error | string)[], - private readonly compilationErrors: (Error | string)[], - public level: LogLevel, - ) {} - - // eslint-disable-next-line @typescript-eslint/no-empty-function - debug() {} - - info(...args: string[]) { - // Log to stderr because it's a progress-like info message. - process.stderr.write(`\n${args.join(' ')}\n`); - } - - warn(...args: string[]) { - this.compilationWarnings.push(args.join(' ')); - } - - error(...args: string[]) { - this.compilationErrors.push(new Error(args.join(' '))); - } -} - -function isReadOnlyFile(fileName: string): boolean { - try { - accessSync(fileName, constants.W_OK); - - return false; - } catch { - return true; - } -}