mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-16 18:43:42 +08:00
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:
parent
d2ef386f46
commit
c8ac660d8b
@ -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>,
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user