refactor(@ngtools/webpack): remove NGCC integration

This commit removes usage of NGCC which was used to convert old View Engine libraries to Ivy.

BREAKING CHANGE: NGCC integration has been removed and as a result Angular View Engine libraries will no longer work.
This commit is contained in:
Alan Agius 2023-02-15 21:28:02 +00:00 committed by angular-robot[bot]
parent d2ef386f46
commit c8ac660d8b
3 changed files with 1 additions and 434 deletions

View File

@ -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<string, string>,

View File

@ -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<string>;
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<Compilation, FileEmitterCollection>();
@ -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(

View File

@ -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<Compiler['resolverFactory']['get']>;
// 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<string>();
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;
}
}