Skip to content

feat(@ngtools/webpack): support Webpack 5 #20037

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Feb 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ngtools/webpack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,12 @@
"peerDependencies": {
"@angular/compiler-cli": "^12.0.0-next",
"typescript": "~4.0.0 || ~4.1.0",
"webpack": "^4.0.0"
"webpack": "^4.0.0 || ^5.20.0"
},
"devDependencies": {
"@angular/compiler": "12.0.0-next.0",
"@angular/compiler-cli": "12.0.0-next.0",
"typescript": "4.1.5",
"webpack": "4.44.2"
"webpack": "5.21.2"
}
}
35 changes: 17 additions & 18 deletions packages/ngtools/webpack/src/angular_compiler_plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { ChildProcess, ForkOptions, fork } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as ts from 'typescript';
import { Compiler, compilation } from 'webpack';
import { Compiler, WebpackFourCompiler, compilation } from 'webpack';
import { time, timeEnd } from './benchmark';
import { WebpackCompilerHost } from './compiler_host';
import { DiagnosticMode, gatherDiagnostics, hasErrors, reportDiagnostics } from './diagnostics';
Expand Down Expand Up @@ -75,10 +75,6 @@ import {
VirtualFileSystemDecorator,
VirtualWatchFileSystemDecorator,
} from './virtual_file_system_decorator';
import {
NodeWatchFileSystemInterface,
NormalModuleFactoryRequest,
} from './webpack';
import { addError, addWarning } from './webpack-diagnostics';
import { createWebpackInputHost } from './webpack-input-host';
import { isWebpackFiveOrHigher, mergeResolverMainFields } from './webpack-version';
Expand Down Expand Up @@ -686,7 +682,10 @@ export class AngularCompilerPlugin {
};

// Go over all the modules in the webpack compilation and remove them from the sets.
compilation.modules.forEach(m => m.resource ? removeSourceFile(m.resource, true) : null);
// tslint:disable-next-line: no-any
compilation.modules.forEach((m: compilation.Module & { resource?: string }) =>
m.resource ? removeSourceFile(m.resource, true) : null,
);

// Anything that remains is unused, because it wasn't referenced directly or transitively
// on the files in the compilation.
Expand All @@ -712,7 +711,12 @@ export class AngularCompilerPlugin {

// Registration hook for webpack plugin.
// tslint:disable-next-line:no-big-function
apply(compiler: Compiler & { watchMode?: boolean, parentCompilation?: compilation.Compilation }) {
apply(webpackCompiler: Compiler | WebpackFourCompiler) {
const compiler = webpackCompiler as Compiler & {
watchMode?: boolean;
parentCompilation?: compilation.Compilation;
watchFileSystem?: unknown;
};
// The below is require by NGCC processor
// since we need to know which fields we need to process
compiler.hooks.environment.tap('angular-compiler', () => {
Expand Down Expand Up @@ -748,13 +752,8 @@ export class AngularCompilerPlugin {
// Decorate inputFileSystem to serve contents of CompilerHost.
// Use decorated inputFileSystem in watchFileSystem.
compiler.hooks.environment.tap('angular-compiler', () => {
// The webpack types currently do not include these
const compilerWithFileSystems = compiler as Compiler & {
watchFileSystem: NodeWatchFileSystemInterface,
};

let host: virtualFs.Host<fs.Stats> = this._options.host || createWebpackInputHost(
compilerWithFileSystems.inputFileSystem,
compiler.inputFileSystem,
);

let replacements: Map<Path, Path> | ((path: Path) => Path) | undefined;
Expand Down Expand Up @@ -791,7 +790,7 @@ export class AngularCompilerPlugin {
this._errors,
this._basePath,
this._tsConfigPath,
compilerWithFileSystems.inputFileSystem,
compiler.inputFileSystem,
compiler.options.resolve?.symlinks,
);

Expand Down Expand Up @@ -830,11 +829,11 @@ export class AngularCompilerPlugin {
}

const inputDecorator = new VirtualFileSystemDecorator(
compilerWithFileSystems.inputFileSystem,
compiler.inputFileSystem,
this._compilerHost,
);
compilerWithFileSystems.inputFileSystem = inputDecorator;
compilerWithFileSystems.watchFileSystem = new VirtualWatchFileSystemDecorator(
compiler.inputFileSystem = inputDecorator;
compiler.watchFileSystem = new VirtualWatchFileSystemDecorator(
inputDecorator,
replacements,
);
Expand Down Expand Up @@ -956,7 +955,7 @@ export class AngularCompilerPlugin {
// when the issuer is a `.ts` or `.ngfactory.js` file.
nmf.hooks.beforeResolve.tapPromise(
'angular-compiler',
async (request?: NormalModuleFactoryRequest) => {
async (request) => {
if (this.done && request) {
const name = request.request;
const issuer = request.contextInfo.issuer;
Expand Down
17 changes: 13 additions & 4 deletions packages/ngtools/webpack/src/ivy/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,23 @@ import { normalizePath } from './paths';

export class SourceFileCache extends Map<string, ts.SourceFile> {
invalidate(
fileTimestamps: Map<string, number | { timestamp: number } | null>,
fileTimestamps: Map<string, 'ignore' | number | { safeTime: number } | null>,
buildTimestamp: number,
): Set<string> {
const changedFiles = new Set<string>();
for (const [file, timeOrEntry] of fileTimestamps) {
const time =
timeOrEntry && (typeof timeOrEntry === 'number' ? timeOrEntry : timeOrEntry.timestamp);
if (time === null || buildTimestamp < time) {
if (timeOrEntry === 'ignore') {
continue;
}

let time;
if (typeof timeOrEntry === 'number') {
time = timeOrEntry;
} else if (timeOrEntry) {
time = timeOrEntry.safeTime;
}

if (!time || time >= buildTimestamp) {
// Cache stores paths using the POSIX directory separator
const normalizedFile = normalizePath(file);
this.delete(normalizedFile);
Expand Down
32 changes: 20 additions & 12 deletions packages/ngtools/webpack/src/ivy/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Compiler,
ContextReplacementPlugin,
NormalModuleReplacementPlugin,
WebpackFourCompiler,
compilation,
} from 'webpack';
import { NgccProcessor } from '../ngcc_processor';
Expand All @@ -33,7 +34,7 @@ import {
} from './host';
import { externalizePath, normalizePath } from './paths';
import { AngularPluginSymbol, EmitFileResult, FileEmitter } from './symbol';
import { createWebpackSystem } from './system';
import { InputFileSystemSync, createWebpackSystem } from './system';
import { createAotTransformers, createJitTransformers, mergeTransformers } from './transformation';

export interface AngularPluginOptions {
Expand All @@ -50,7 +51,8 @@ export interface AngularPluginOptions {

// Add support for missing properties in Webpack types as well as the loader's file emitter
interface WebpackCompilation extends compilation.Compilation {
compilationDependencies: Set<string>;
// tslint:disable-next-line: no-any
compilationDependencies: { add(item: string): any };
rebuildModule(module: compilation.Module, callback: () => void): void;
[AngularPluginSymbol]: FileEmitter;
}
Expand Down Expand Up @@ -113,7 +115,8 @@ export class AngularWebpackPlugin {
return this.pluginOptions;
}

apply(compiler: Compiler & { watchMode?: boolean }): void {
apply(webpackCompiler: Compiler | WebpackFourCompiler): void {
const compiler = webpackCompiler as Compiler & { watchMode?: boolean };
// Setup file replacements with webpack
for (const [key, value] of Object.entries(this.pluginOptions.fileReplacements)) {
new NormalModuleReplacementPlugin(
Expand Down Expand Up @@ -188,7 +191,11 @@ export class AngularWebpackPlugin {
pathsPlugin.update(compilerOptions);

// Create a Webpack-based TypeScript compiler host
const system = createWebpackSystem(compiler.inputFileSystem, normalizePath(compiler.context));
const system = createWebpackSystem(
// Webpack lacks an InputFileSytem type definition with sync functions
compiler.inputFileSystem as InputFileSystemSync,
normalizePath(compiler.context),
);
const host = ts.createIncrementalCompilerHost(compilerOptions, system);

// Setup source file caching and reuse cache from previous compilation if present
Expand Down Expand Up @@ -267,7 +274,7 @@ export class AngularWebpackPlugin {
.filter((sourceFile) => !sourceFile.isDeclarationFile)
.map((sourceFile) => sourceFile.fileName),
);
modules.forEach(({ resource }: compilation.Module & { resource?: string }) => {
Array.from(modules).forEach(({ resource }: compilation.Module & { resource?: string }) => {
const sourceFile = resource && builder.getSourceFile(resource);
if (!sourceFile) {
return;
Expand Down Expand Up @@ -303,7 +310,7 @@ export class AngularWebpackPlugin {
}

const rebuild = (webpackModule: compilation.Module) =>
new Promise<void>((resolve) => compilation.rebuildModule(webpackModule, resolve));
new Promise<void>((resolve) => compilation.rebuildModule(webpackModule, () => resolve()));

const filesToRebuild = new Set<string>();
for (const requiredFile of this.requiredFilesToEmit) {
Expand Down Expand Up @@ -485,7 +492,7 @@ export class AngularWebpackPlugin {
!ignoreForEmit.has(sourceFile) &&
!angularCompiler.incrementalDriver.safeToSkipEmit(sourceFile)
) {
this.requiredFilesToEmit.add(sourceFile.fileName);
this.requiredFilesToEmit.add(normalizePath(sourceFile.fileName));
}
}

Expand All @@ -500,7 +507,7 @@ export class AngularWebpackPlugin {
mergeTransformers(angularCompiler.prepareEmit().transformers, transformers),
getDependencies,
(sourceFile) => {
this.requiredFilesToEmit.delete(sourceFile.fileName);
this.requiredFilesToEmit.delete(normalizePath(sourceFile.fileName));
angularCompiler.incrementalDriver.recordSuccessfulEmit(sourceFile);
},
);
Expand Down Expand Up @@ -589,11 +596,12 @@ export class AngularWebpackPlugin {
onAfterEmit?: (sourceFile: ts.SourceFile) => void,
): FileEmitter {
return async (file: string) => {
if (this.requiredFilesToEmitCache.has(file)) {
return this.requiredFilesToEmitCache.get(file);
const filePath = normalizePath(file);
if (this.requiredFilesToEmitCache.has(filePath)) {
return this.requiredFilesToEmitCache.get(filePath);
}

const sourceFile = program.getSourceFile(file);
const sourceFile = program.getSourceFile(filePath);
if (!sourceFile) {
return undefined;
}
Expand All @@ -620,7 +628,7 @@ export class AngularWebpackPlugin {
if (content !== undefined && this.watchMode) {
// Capture emit history info for Angular rebuild analysis
hash = hashContent(content);
this.fileEmitHistory.set(file, { length: content.length, hash });
this.fileEmitHistory.set(filePath, { length: content.length, hash });
}

const dependencies = [
Expand Down
10 changes: 9 additions & 1 deletion packages/ngtools/webpack/src/ivy/system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,19 @@ import * as ts from 'typescript';
import { InputFileSystem } from 'webpack';
import { externalizePath } from './paths';

export interface InputFileSystemSync extends InputFileSystem {
readFileSync(path: string): Buffer;
statSync(path: string): { size: number; mtime: Date; isDirectory(): boolean; isFile(): boolean };
}

function shouldNotWrite(): never {
throw new Error('Webpack TypeScript System should not write.');
}

export function createWebpackSystem(input: InputFileSystem, currentDirectory: string): ts.System {
export function createWebpackSystem(
input: InputFileSystemSync,
currentDirectory: string,
): ts.System {
// Webpack's CachedInputFileSystem uses the default directory separator in the paths it uses
// for keys to its cache. If the keys do not match then the file watcher will not purge outdated
// files and cause stale data to be used in the next rebuild. TypeScript always uses a `/` (POSIX)
Expand Down
8 changes: 7 additions & 1 deletion packages/ngtools/webpack/src/paths-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
*/
import * as path from 'path';
import { CompilerOptions, MapLike } from 'typescript';
import { NormalModuleFactoryRequest } from './webpack';

const getInnerRequest = require('enhanced-resolve/lib/getInnerRequest');

interface NormalModuleFactoryRequest {
request: string;
context: { issuer: string };
contextInfo: { issuer: string };
typescriptPathMapped?: boolean;
}

export interface TypeScriptPathsPluginOptions extends Pick<CompilerOptions, 'paths' | 'baseUrl'> {

}
Expand Down
21 changes: 18 additions & 3 deletions packages/ngtools/webpack/src/virtual_file_system_decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,17 @@ import { FileDoesNotExistException, Path, getSystemPath, normalize } from '@angu
import { Stats } from 'fs';
import { InputFileSystem } from 'webpack';
import { WebpackCompilerHost } from './compiler_host';
import { NodeWatchFileSystemInterface } from './webpack';
import { isWebpackFiveOrHigher } from './webpack-version';

interface NodeWatchFileSystemInterface {
inputFileSystem: InputFileSystem;
new(inputFileSystem: InputFileSystem): NodeWatchFileSystemInterface;
// tslint:disable-next-line:no-any
watch(files: any, dirs: any, missing: any, startTime: any, options: any, callback: any,
// tslint:disable-next-line:no-any
callbackUndelayed: any): any;
}

export const NodeWatchFileSystem: NodeWatchFileSystemInterface = require(
'webpack/lib/node/NodeWatchFileSystem');

Expand Down Expand Up @@ -61,7 +69,12 @@ export class VirtualFileSystemDecorator implements InputFileSystem {
(this._inputFileSystem as any).readJson(path, callback);
}

readlink(path: string, callback: (err: Error | null | undefined, linkString: string) => void): void {
readlink(
path: string,
// Callback types differ between Webpack 4 and 5
// tslint:disable-next-line: no-any
callback: (err: any, linkString: any) => void,
): void {
this._inputFileSystem.readlink(path, callback);
}

Expand Down Expand Up @@ -89,7 +102,9 @@ export class VirtualFileSystemDecorator implements InputFileSystem {
}

readlinkSync(path: string): string {
return this._inputFileSystem.readlinkSync(path);
// Synchronous functions are missing from the Webpack typings
// tslint:disable-next-line: no-any
return (this._inputFileSystem as any).readlinkSync(path);
}

purge(changes?: string[] | string): void {
Expand Down
8 changes: 6 additions & 2 deletions packages/ngtools/webpack/src/webpack-input-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ export function createWebpackInputHost(inputFileSystem: InputFileSystem) {
},

read(path): virtualFs.FileBuffer {
const data = inputFileSystem.readFileSync(getSystemPath(path));
// Synchronous functions are missing from the Webpack typings
// tslint:disable-next-line: no-any
const data = (inputFileSystem as any).readFileSync(getSystemPath(path));

return new Uint8Array(data).buffer as ArrayBuffer;
},
Expand Down Expand Up @@ -53,7 +55,9 @@ export function createWebpackInputHost(inputFileSystem: InputFileSystem) {

stat(path): Stats | null {
try {
return inputFileSystem.statSync(getSystemPath(path));
// Synchronous functions are missing from the Webpack typings
// tslint:disable-next-line: no-any
return (inputFileSystem as any).statSync(getSystemPath(path));
} catch (e) {
if (e.code === 'ENOENT') {
return null;
Expand Down
25 changes: 25 additions & 0 deletions packages/ngtools/webpack/src/webpack.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright Google Inc. 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 * as webpack from 'webpack';
import { Compiler as webpack4Compiler, loader as webpack4Loader } from '@types/webpack';

// Webpack 5 transition support types
declare module 'webpack' {
export type WebpackFourCompiler = webpack4Compiler;

export type InputFileSystem = webpack.Compiler['inputFileSystem'];

export namespace compilation {
export type Compilation = webpack.Compilation;
export type Module = webpack.Module;
}

export namespace loader {
export type LoaderContext = webpack4Loader.LoaderContext;
}
}
Loading