Skip to content

Commit

Permalink
feat(@ngtools/webpack): support Webpack 5
Browse files Browse the repository at this point in the history
The `@ngtools/webpack` package now officially supports Webpack 5.
It is also now built against Webpack 5 types.  Webpack 4 support is temporarily maintained while the remainder of the tooling is transitioned.
  • Loading branch information
clydin authored and filipesilva committed Feb 17, 2021
1 parent 73a5610 commit 789e05d
Show file tree
Hide file tree
Showing 11 changed files with 334 additions and 73 deletions.
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
19 changes: 13 additions & 6 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
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;
}
}
26 changes: 0 additions & 26 deletions packages/ngtools/webpack/src/webpack.ts

This file was deleted.

Loading

0 comments on commit 789e05d

Please sign in to comment.