From c4565ccf28ba067b18896f44501cb2b0d7e521c3 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 21 Apr 2020 18:24:00 -0400 Subject: [PATCH 1/5] refactor(@ngtools/webpack): initial ivy-only Webpack plugin/loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduces a new Ivy-only Webpack plugin. The plugin is based on the design introduced by `tsc_wrapped` within Bazel’s `rules_typescript` and leverages the newly added ngtsc plugin from within the `@angular/compiler-cli` package. By leveraging the same plugin interface that it used by Bazel, a common interface can be used to access the Angular compiler. This has benefits to both major build systems and dramatically reduces the necessary code infrastructure to integrate the Angular compiler. The plugin also simplifies and reduces the amount of code within the plugin by leveraging newer TypeScript features and capabilities. The need for the virtual filesystem has also been removed. The file replacements capability was the primary driver for the previous need for the virtual filesystem. File replacements are now implemented using a two-pronged approach. The first, for TypeScript, is to hook TypeScript module resolution and adjust the resolved modules based on the configured file replacements. This is similar in behavior to TypeScript path mapping. The second, for Webpack, is the use of the `NormalModuleReplacementPlugin` to facilitate bundling of the configured file replacements. An advantage to this approach is that the build system (both TypeScript and Webpack) are now aware of the replacements and can operate without augmenting multiple aspects of system as was needed previously. The plugin also introduces the use of TypeScript’s builder programs. The current primary benefit is more accurate and simplified dependency discovery. Further, they also provide for the potential future introduction of incremental build support and incremental type checking. NOTE: The deprecated string format for lazy routes is not supported by this plugin. Dynamic imports are recommended for use with Ivy and are required when using the new plugin. --- packages/ngtools/webpack/src/index.ts | 2 + .../ngtools/webpack/src/ivy/diagnostics.ts | 27 + packages/ngtools/webpack/src/ivy/host.ts | 246 +++++++++ packages/ngtools/webpack/src/ivy/index.ts | 11 + packages/ngtools/webpack/src/ivy/loader.ts | 77 +++ packages/ngtools/webpack/src/ivy/plugin.ts | 486 ++++++++++++++++++ packages/ngtools/webpack/src/ivy/symbol.ts | 16 + packages/ngtools/webpack/src/ivy/system.ts | 99 ++++ .../ngtools/webpack/src/ivy/transformation.ts | 141 +++++ .../ngtools/webpack/src/resource_loader.ts | 31 +- 10 files changed, 1134 insertions(+), 2 deletions(-) create mode 100644 packages/ngtools/webpack/src/ivy/diagnostics.ts create mode 100644 packages/ngtools/webpack/src/ivy/host.ts create mode 100644 packages/ngtools/webpack/src/ivy/index.ts create mode 100644 packages/ngtools/webpack/src/ivy/loader.ts create mode 100644 packages/ngtools/webpack/src/ivy/plugin.ts create mode 100644 packages/ngtools/webpack/src/ivy/symbol.ts create mode 100644 packages/ngtools/webpack/src/ivy/system.ts create mode 100644 packages/ngtools/webpack/src/ivy/transformation.ts diff --git a/packages/ngtools/webpack/src/index.ts b/packages/ngtools/webpack/src/index.ts index bba971febb90..35de314a7db3 100644 --- a/packages/ngtools/webpack/src/index.ts +++ b/packages/ngtools/webpack/src/index.ts @@ -14,3 +14,5 @@ export const NgToolsLoader = __filename; // We shouldn't need to export this, but webpack-rollup-loader uses it. export type { VirtualFileSystemDecorator } from './virtual_file_system_decorator'; + +export * as ivy from './ivy'; diff --git a/packages/ngtools/webpack/src/ivy/diagnostics.ts b/packages/ngtools/webpack/src/ivy/diagnostics.ts new file mode 100644 index 000000000000..4ddd0b739ba9 --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/diagnostics.ts @@ -0,0 +1,27 @@ +/** + * @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 { Diagnostics, formatDiagnostics } from '@angular/compiler-cli'; +import { DiagnosticCategory } from 'typescript'; +import { addError, addWarning } from '../webpack-diagnostics'; + +export type DiagnosticsReporter = (diagnostics: Diagnostics) => void; + +export function createDiagnosticsReporter( + compilation: import('webpack').compilation.Compilation, +): DiagnosticsReporter { + return (diagnostics) => { + for (const diagnostic of diagnostics) { + const text = formatDiagnostics([diagnostic]); + if (diagnostic.category === DiagnosticCategory.Error) { + addError(compilation, text); + } else { + addWarning(compilation, text); + } + } + }; +} diff --git a/packages/ngtools/webpack/src/ivy/host.ts b/packages/ngtools/webpack/src/ivy/host.ts new file mode 100644 index 000000000000..b85cc279151d --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/host.ts @@ -0,0 +1,246 @@ +/** + * @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 { 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 { forwardSlashPath } from '../utils'; + +export function augmentHostWithResources( + host: ts.CompilerHost, + resourceLoader: WebpackResourceLoader, + options: { directTemplateLoading?: boolean } = {}, +) { + const resourceHost = host as CompilerHost; + + resourceHost.readResource = function (fileName: string) { + const filePath = forwardSlashPath(fileName); + + if ( + options.directTemplateLoading && + (filePath.endsWith('.html') || filePath.endsWith('.svg')) + ) { + const content = this.readFile(filePath); + if (content === undefined) { + throw new Error('Unable to locate component resource: ' + fileName); + } + + resourceLoader.setAffectedResources(filePath, [filePath]); + + return content; + } else { + return resourceLoader.get(filePath); + } + }; + + resourceHost.resourceNameToFileName = function (resourceName: string, containingFile: string) { + return forwardSlashPath(path.join(path.dirname(containingFile), resourceName)); + }; + + resourceHost.getModifiedResourceFiles = function () { + return resourceLoader.getModifiedResourceFiles(); + }; +} + +function augmentResolveModuleNames( + host: ts.CompilerHost, + resolvedModuleModifier: ( + resolvedModule: ts.ResolvedModule | undefined, + moduleName: string, + ) => ts.ResolvedModule | undefined, + moduleResolutionCache?: ts.ModuleResolutionCache, +): void { + if (host.resolveModuleNames) { + const baseResolveModuleNames = host.resolveModuleNames; + host.resolveModuleNames = function (moduleNames: string[], ...parameters) { + return moduleNames.map((name) => { + const result = baseResolveModuleNames.call(host, [name], ...parameters); + + return resolvedModuleModifier(result[0], name); + }); + }; + } else { + host.resolveModuleNames = function ( + moduleNames: string[], + containingFile: string, + _reusedNames: string[] | undefined, + redirectedReference: ts.ResolvedProjectReference | undefined, + options: ts.CompilerOptions, + ) { + return moduleNames.map((name) => { + const result = ts.resolveModuleName( + name, + containingFile, + options, + host, + moduleResolutionCache, + redirectedReference, + ).resolvedModule; + + return resolvedModuleModifier(result, name); + }); + }; + } +} + +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[], ...parameters) { + return names.map((name) => { + const result = baseResolveTypeReferenceDirectives.call(host, [name], ...parameters); + + if (result[0] && ngcc) { + ngcc.processModule(name, result[0]); + } + + return result[0]; + }); + }; + } else { + host.resolveTypeReferenceDirectives = function ( + moduleNames: string[], + containingFile: string, + redirectedReference: ts.ResolvedProjectReference | undefined, + options: ts.CompilerOptions, + ) { + return moduleNames.map((name) => { + const result = ts.resolveTypeReferenceDirective( + name, + containingFile, + options, + host, + redirectedReference, + ).resolvedTypeReferenceDirective; + + if (result && ngcc) { + ngcc.processModule(name, result); + } + + return result; + }); + }; + } +} + +export function augmentHostWithReplacements( + host: ts.CompilerHost, + replacements: Record, + moduleResolutionCache?: ts.ModuleResolutionCache, +): void { + if (Object.keys(replacements).length === 0) { + return; + } + + const tryReplace = (resolvedModule: ts.ResolvedModule | undefined) => { + const replacement = resolvedModule && replacements[resolvedModule.resolvedFileName]; + if (replacement) { + return { + resolvedFileName: replacement, + isExternalLibraryImport: /[\/\\]node_modules[\/\\]/.test(replacement), + }; + } else { + return resolvedModule; + } + }; + + augmentResolveModuleNames(host, tryReplace, moduleResolutionCache); +} + +export function augmentHostWithSubstitutions( + host: ts.CompilerHost, + substitutions: Record, +): void { + const regexSubstitutions: [RegExp, string][] = []; + for (const [key, value] of Object.entries(substitutions)) { + regexSubstitutions.push([new RegExp(`\\b${key}\\b`, 'g'), value]); + } + + if (regexSubstitutions.length === 0) { + return; + } + + const baseReadFile = host.readFile; + host.readFile = function (...parameters) { + let file: string | undefined = baseReadFile.call(host, ...parameters); + if (file) { + for (const entry of regexSubstitutions) { + file = file.replace(entry[0], entry[1]); + } + } + + return file; + }; +} + +export function augmentHostWithVersioning(host: ts.CompilerHost): void { + const baseGetSourceFile = host.getSourceFile; + host.getSourceFile = function (...parameters) { + const file: (ts.SourceFile & { version?: string }) | undefined = baseGetSourceFile.call( + host, + ...parameters, + ); + if (file && file.version === undefined) { + file.version = createHash('sha256').update(file.text).digest('hex'); + } + + return file; + }; +} + +export function augmentHostWithCaching( + host: ts.CompilerHost, + cache: Map, +): void { + const baseGetSourceFile = host.getSourceFile; + host.getSourceFile = function ( + fileName, + languageVersion, + onError, + shouldCreateNewSourceFile, + // tslint:disable-next-line: trailing-comma + ...parameters + ) { + if (!shouldCreateNewSourceFile && cache.has(fileName)) { + return cache.get(fileName); + } + + const file = baseGetSourceFile.call( + host, + fileName, + languageVersion, + onError, + true, + ...parameters, + ); + + if (file) { + cache.set(fileName, file); + } + + return file; + }; +} diff --git a/packages/ngtools/webpack/src/ivy/index.ts b/packages/ngtools/webpack/src/ivy/index.ts new file mode 100644 index 000000000000..b5e2985745ad --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/index.ts @@ -0,0 +1,11 @@ +/** + * @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 + */ +export { angularWebpackLoader as default } from './loader'; +export { AngularPluginOptions, AngularWebpackPlugin } from './plugin'; + +export const AngularWebpackLoaderPath = __filename; diff --git a/packages/ngtools/webpack/src/ivy/loader.ts b/packages/ngtools/webpack/src/ivy/loader.ts new file mode 100644 index 000000000000..11a09a4dbbf0 --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/loader.ts @@ -0,0 +1,77 @@ +/** + * @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 path from 'path'; +import { AngularPluginSymbol, FileEmitter } from './symbol'; + +export function angularWebpackLoader( + this: import('webpack').loader.LoaderContext, + content: string, + // Source map types are broken in the webpack type definitions + // tslint:disable-next-line: no-any + map: any, +) { + if (this.loaderIndex !== this.loaders.length - 1) { + this.emitWarning('The Angular Webpack loader does not support chaining prior to the loader.'); + } + + const callback = this.async(); + if (!callback) { + throw new Error('Invalid webpack version'); + } + + const emitFile = this._compilation[AngularPluginSymbol] as FileEmitter; + if (typeof emitFile !== 'function') { + if (this.resourcePath.endsWith('.js')) { + // Passthrough for JS files when no plugin is used + this.callback(undefined, content, map); + + return; + } + + callback(new Error('The Angular Webpack loader requires the AngularWebpackPlugin.')); + + return; + } + + emitFile(this.resourcePath) + .then((result) => { + if (!result) { + if (this.resourcePath.endsWith('.js')) { + // Return original content for JS files if not compiled by TypeScript ("allowJs") + this.callback(undefined, content, map); + } else { + // File is not part of the compilation + const message = + `${this.resourcePath} is missing from the TypeScript compilation. ` + + `Please make sure it is in your tsconfig via the 'files' or 'include' property.`; + callback(new Error(message)); + } + + return; + } + + result.dependencies.forEach((dependency) => this.addDependency(dependency)); + + let resultContent = result.content || ''; + let resultMap; + if (result.map) { + resultContent = resultContent.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); + resultMap = JSON.parse(result.map); + resultMap.sources = resultMap.sources.map((source: string) => + path.join(path.dirname(this.resourcePath), source), + ); + } + + callback(undefined, resultContent, resultMap); + }) + .catch((err) => { + callback(err); + }); +} + +export { angularWebpackLoader as default }; diff --git a/packages/ngtools/webpack/src/ivy/plugin.ts b/packages/ngtools/webpack/src/ivy/plugin.ts new file mode 100644 index 000000000000..d810cbd90c3f --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/plugin.ts @@ -0,0 +1,486 @@ +/** + * @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 { + CompilerHost, + CompilerOptions, + NgTscPlugin, + readConfiguration, +} from '@angular/compiler-cli'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { + Compiler, + ContextReplacementPlugin, + NormalModuleReplacementPlugin, + compilation, +} from 'webpack'; +import { NgccProcessor } from '../ngcc_processor'; +import { TypeScriptPathsPlugin } from '../paths-plugin'; +import { WebpackResourceLoader } from '../resource_loader'; +import { forwardSlashPath } from '../utils'; +import { addError, addWarning } from '../webpack-diagnostics'; +import { DiagnosticsReporter, createDiagnosticsReporter } from './diagnostics'; +import { + augmentHostWithCaching, + augmentHostWithNgcc, + augmentHostWithReplacements, + augmentHostWithResources, + augmentHostWithSubstitutions, + augmentHostWithVersioning, +} from './host'; +import { AngularPluginSymbol, FileEmitter } from './symbol'; +import { createWebpackSystem } from './system'; +import { createAotTransformers, createJitTransformers, mergeTransformers } from './transformation'; + +export interface AngularPluginOptions { + tsconfig: string; + compilerOptions?: CompilerOptions; + fileReplacements: Record; + substitutions: Record; + directTemplateLoading: boolean; + emitClassMetadata: boolean; + emitNgModuleScope: boolean; +} + +// Add support for missing properties in Webpack types as well as the loader's file emitter +interface WebpackCompilation extends compilation.Compilation { + compilationDependencies: Set; + [AngularPluginSymbol]: FileEmitter; +} + +function initializeNgccProcessor( + compiler: Compiler, + tsconfig: string, +): { processor: NgccProcessor; errors: string[]; warnings: string[] } { + const { inputFileSystem, options: webpackOptions } = compiler; + const mainFields = ([] as string[]).concat(...(webpackOptions.resolve?.mainFields || [])); + + const fileWatchPurger = (path: string) => { + if (inputFileSystem.purge) { + // Webpack typings do not contain the string parameter overload for purge + (inputFileSystem as { purge(path: string): void }).purge(path); + } + }; + + const errors: string[] = []; + const warnings: string[] = []; + const processor = new NgccProcessor( + mainFields, + fileWatchPurger, + warnings, + errors, + compiler.context, + tsconfig, + ); + + return { processor, errors, warnings }; +} + +const PLUGIN_NAME = 'angular-compiler'; + +export class AngularWebpackPlugin { + private readonly pluginOptions: AngularPluginOptions; + private watchMode?: boolean; + private ngtscNextProgram?: ts.Program; + private builder?: ts.EmitAndSemanticDiagnosticsBuilderProgram; + private sourceFileCache?: Map; + private buildTimestamp!: number; + + constructor(options: Partial = {}) { + this.pluginOptions = { + emitClassMetadata: false, + emitNgModuleScope: false, + fileReplacements: {}, + substitutions: {}, + directTemplateLoading: true, + tsconfig: 'tsconfig.json', + ...options, + }; + } + + get options(): AngularPluginOptions { + return this.pluginOptions; + } + + apply(compiler: Compiler & { watchMode?: boolean }): void { + // Setup file replacements with webpack + for (const [key, value] of Object.entries(this.pluginOptions.fileReplacements)) { + new NormalModuleReplacementPlugin( + new RegExp('^' + key.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&') + '$'), + value, + ).apply(compiler); + } + + // Mimic VE plugin's systemjs module loader resource location for consistency + new ContextReplacementPlugin( + /\@angular[\\\/]core[\\\/]/, + path.join(compiler.context, '$$_lazy_route_resource'), + ).apply(compiler); + + // Set resolver options + const pathsPlugin = new TypeScriptPathsPlugin(); + compiler.hooks.afterResolvers.tap('angular-compiler', (compiler) => { + // 'resolverFactory' is not present in the Webpack typings + // tslint:disable-next-line: no-any + const resolverFactoryHooks = (compiler as any).resolverFactory.hooks; + + // 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 + resolverFactoryHooks.resolveOptions + .for('normal') + .tap(PLUGIN_NAME, (resolveOptions: { mainFields: string[] }) => { + const originalMainFields = resolveOptions.mainFields; + const ivyMainFields = originalMainFields.map((f) => `${f}_ivy_ngcc`); + + return { + ...resolveOptions, + mainFields: [...ivyMainFields, ...originalMainFields], + }; + }); + + resolverFactoryHooks.resolver.for('normal').tap(PLUGIN_NAME, (resolver: {}) => { + pathsPlugin.apply(resolver); + }); + }); + + let ngccProcessor: NgccProcessor | undefined; + const resourceLoader = new WebpackResourceLoader(); + let previousUnused: Set | undefined; + compiler.hooks.thisCompilation.tap(PLUGIN_NAME, (thisCompilation) => { + const compilation = thisCompilation as WebpackCompilation; + + // Store watch mode; assume true if not present (webpack < 4.23.0) + this.watchMode = compiler.watchMode ?? true; + + // Initialize and process eager ngcc if not already setup + if (!ngccProcessor) { + const { processor, errors, warnings } = initializeNgccProcessor( + compiler, + this.pluginOptions.tsconfig, + ); + + processor.process(); + warnings.forEach((warning) => addWarning(compilation, warning)); + errors.forEach((error) => addError(compilation, error)); + + ngccProcessor = processor; + } + + // Setup and read TypeScript and Angular compiler configuration + const { options: compilerOptions, rootNames, errors } = readConfiguration( + this.pluginOptions.tsconfig, + this.pluginOptions.compilerOptions, + ); + compilerOptions.noEmitOnError = false; + compilerOptions.suppressOutputPathCheck = true; + compilerOptions.outDir = undefined; + compilerOptions.inlineSources = compilerOptions.sourceMap; + compilerOptions.inlineSourceMap = false; + compilerOptions.mapRoot = undefined; + compilerOptions.sourceRoot = undefined; + compilerOptions.allowEmptyCodegenFiles = false; + compilerOptions.annotationsAs = 'decorators'; + compilerOptions.enableResourceInlining = false; + + // Create diagnostics reporter and report configuration file errors + const diagnosticsReporter = createDiagnosticsReporter(compilation); + diagnosticsReporter(errors); + + // Update TypeScript path mapping plugin with new configuration + pathsPlugin.update(compilerOptions); + + // Create a Webpack-based TypeScript compiler host + const system = createWebpackSystem( + compiler.inputFileSystem, + forwardSlashPath(compiler.context), + ); + const host = ts.createIncrementalCompilerHost(compilerOptions, system); + + // Setup source file caching and reuse cache from previous compilation if present + let cache = this.sourceFileCache; + if (cache) { + // Invalidate existing cache based on compilation file timestamps + for (const [file, time] of compilation.fileTimestamps) { + if (this.buildTimestamp < time) { + cache.delete(forwardSlashPath(file)); + } + } + } else { + // Initialize a new cache + cache = new Map(); + // Only store cache if in watch mode + if (this.watchMode) { + this.sourceFileCache = cache; + } + } + this.buildTimestamp = Date.now(); + augmentHostWithCaching(host, cache); + + const moduleResolutionCache = ts.createModuleResolutionCache( + host.getCurrentDirectory(), + host.getCanonicalFileName.bind(host), + compilerOptions, + ); + + // Setup on demand ngcc + augmentHostWithNgcc(host, ngccProcessor, moduleResolutionCache); + + // Setup resource loading + resourceLoader.update(compilation); + augmentHostWithResources(host, resourceLoader, { + directTemplateLoading: this.pluginOptions.directTemplateLoading, + }); + + // Setup source file adjustment options + augmentHostWithReplacements(host, this.pluginOptions.fileReplacements, moduleResolutionCache); + augmentHostWithSubstitutions(host, this.pluginOptions.substitutions); + + // Create the file emitter used by the webpack loader + const { fileEmitter, builder, internalFiles } = compilerOptions.skipTemplateCodegen + ? this.updateJitProgram(compilerOptions, rootNames, host, diagnosticsReporter) + : this.updateAotProgram( + compilerOptions, + rootNames, + host, + diagnosticsReporter, + resourceLoader, + ); + + const allProgramFiles = builder + .getSourceFiles() + .filter((sourceFile) => !internalFiles || !internalFiles.has(sourceFile)) + .map((sourceFile) => sourceFile.fileName); + + // Ensure all program files are considered part of the compilation and will be watched + allProgramFiles.forEach((sourceFile) => compilation.compilationDependencies.add(sourceFile)); + + // Analyze program for unused files + compilation.hooks.finishModules.tap(PLUGIN_NAME, (modules) => { + if (compilation.errors.length > 0) { + return; + } + + const currentUnused = new Set( + allProgramFiles.filter((sourceFile) => !sourceFile.endsWith('.d.ts')), + ); + modules.forEach((module) => { + const { resource } = module as { resource?: string }; + const sourceFile = resource && builder.getSourceFile(forwardSlashPath(resource)); + if (!sourceFile) { + return; + } + + builder.getAllDependencies(sourceFile).forEach((dep) => currentUnused.delete(dep)); + }); + for (const unused of currentUnused) { + if (previousUnused && previousUnused.has(unused)) { + continue; + } + compilation.warnings.push( + `${unused} is part of the TypeScript compilation but it's unused.\n` + + `Add only entry points to the 'files' or 'include' properties in your tsconfig.`, + ); + } + previousUnused = currentUnused; + }); + + // Store file emitter for loader usage + compilation[AngularPluginSymbol] = fileEmitter; + }); + } + + private updateAotProgram( + compilerOptions: CompilerOptions, + rootNames: string[], + host: CompilerHost, + diagnosticsReporter: DiagnosticsReporter, + resourceLoader: WebpackResourceLoader, + ) { + // Create an ngtsc plugin instance used to initialize a TypeScript host and program + const ngtsc = new NgTscPlugin(compilerOptions); + + const wrappedHost = ngtsc.wrapHost(host, rootNames, compilerOptions); + + // SourceFile versions are required for builder programs. + // The wrapped host adds additional files that will not have versions + augmentHostWithVersioning(wrappedHost); + + const oldProgram = this.ngtscNextProgram; + const program = ts.createProgram({ + options: compilerOptions, + oldProgram, + rootNames: wrappedHost.inputFiles, + host: wrappedHost, + }); + + // The `ignoreForEmit` return value can be safely ignored when emitting. Only files + // that will be bundled (requested by Webpack) will be emitted. Combined with TypeScript's + // eliding of type only imports, this will cause type only files to be automatically ignored. + // Internal Angular type check files are also not resolvable by the bundler. Even if they + // were somehow errantly imported, the bundler would error before an emit was attempted. + // Diagnostics are still collected for all files which requires using `ignoreForDiagnostics`. + const { ignoreForDiagnostics, ignoreForEmit } = ngtsc.setupCompilation(program, oldProgram); + + const builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram( + program, + wrappedHost, + this.builder, + ); + + // Save for next rebuild + if (this.watchMode) { + this.ngtscNextProgram = ngtsc.getNextProgram(); + this.builder = builder; + } + + // Update semantic diagnostics cache + while (true) { + const result = builder.getSemanticDiagnosticsOfNextAffectedFile(undefined, (sourceFile) => + ignoreForDiagnostics.has(sourceFile), + ); + if (!result) { + break; + } + } + + // Collect non-semantic diagnostics + const diagnostics = [ + ...ngtsc.getOptionDiagnostics(), + ...builder.getOptionsDiagnostics(), + ...builder.getGlobalDiagnostics(), + ...builder.getSyntacticDiagnostics(), + ]; + diagnosticsReporter(diagnostics); + + // Collect semantic diagnostics + for (const sourceFile of builder.getSourceFiles()) { + if (!ignoreForDiagnostics.has(sourceFile)) { + diagnosticsReporter(builder.getSemanticDiagnostics(sourceFile)); + } + } + + const transformers = createAotTransformers(builder, this.pluginOptions); + + const getDependencies = (sourceFile: ts.SourceFile) => { + // TODO: Integrate getModuleDependencies once implemented + const dependencies = []; + for (const resourceDependency of ngtsc.compiler.getResourceDependencies(sourceFile)) { + const resourcePath = forwardSlashPath(resourceDependency); + dependencies.push( + resourcePath, + // Retrieve all dependencies of the resource (stylesheet imports, etc.) + ...resourceLoader.getResourceDependencies(resourcePath), + ); + } + + return dependencies; + }; + + // Required to support asynchronous resource loading + // Must be done before creating transformers or getting template diagnostics + const pendingAnalysis = ngtsc.compiler.analyzeAsync().then(() => { + // Collect Angular template diagnostics + for (const sourceFile of builder.getSourceFiles()) { + if (!ignoreForDiagnostics.has(sourceFile)) { + diagnosticsReporter(ngtsc.getDiagnostics(sourceFile)); + } + } + + return this.createFileEmitter( + builder, + mergeTransformers(ngtsc.createTransformers(), transformers), + getDependencies, + ); + }); + const analyzingFileEmitter: FileEmitter = async (file) => { + const innerFileEmitter = await pendingAnalysis; + + return innerFileEmitter(file); + }; + + return { + fileEmitter: analyzingFileEmitter, + builder, + internalFiles: ignoreForEmit, + }; + } + + private updateJitProgram( + compilerOptions: CompilerOptions, + rootNames: readonly string[], + host: CompilerHost, + diagnosticsReporter: DiagnosticsReporter, + ) { + const builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram( + rootNames, + compilerOptions, + host, + this.builder, + ); + + // Save for next rebuild + if (this.watchMode) { + this.builder = builder; + } + + const diagnostics = [ + ...builder.getOptionsDiagnostics(), + ...builder.getGlobalDiagnostics(), + ...builder.getSyntacticDiagnostics(), + // Gather incremental semantic diagnostics + ...builder.getSemanticDiagnostics(), + ]; + diagnosticsReporter(diagnostics); + + const transformers = createJitTransformers(builder, this.pluginOptions); + + return { + fileEmitter: this.createFileEmitter(builder, transformers, () => []), + builder, + internalFiles: undefined, + }; + } + + private createFileEmitter( + program: ts.BuilderProgram, + transformers: ts.CustomTransformers = {}, + getExtraDependencies: (sourceFile: ts.SourceFile) => Iterable, + ): FileEmitter { + return async (file: string) => { + const sourceFile = program.getSourceFile(forwardSlashPath(file)); + if (!sourceFile) { + return undefined; + } + + let content: string | undefined = undefined; + let map: string | undefined = undefined; + program.emit( + sourceFile, + (filename, data) => { + if (filename.endsWith('.map')) { + map = data; + } else if (filename.endsWith('.js')) { + content = data; + } + }, + undefined, + undefined, + transformers, + ); + + const dependencies = [ + ...program.getAllDependencies(sourceFile), + ...getExtraDependencies(sourceFile), + ]; + + return { content, map, dependencies }; + }; + } +} diff --git a/packages/ngtools/webpack/src/ivy/symbol.ts b/packages/ngtools/webpack/src/ivy/symbol.ts new file mode 100644 index 000000000000..b3dd0faafe68 --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/symbol.ts @@ -0,0 +1,16 @@ +/** + * @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 + */ +export const AngularPluginSymbol = Symbol.for('@angular-devkit/build-angular[angular-compiler]'); + +export interface EmitFileResult { + content?: string; + map?: string; + dependencies: readonly string[]; +} + +export type FileEmitter = (file: string) => Promise; diff --git a/packages/ngtools/webpack/src/ivy/system.ts b/packages/ngtools/webpack/src/ivy/system.ts new file mode 100644 index 000000000000..ac51e8d546b1 --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/system.ts @@ -0,0 +1,99 @@ +/** + * @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 ts from 'typescript'; +import { InputFileSystem } from 'webpack'; + +function shouldNotWrite(): never { + throw new Error('Webpack TypeScript System should not write.'); +} + +// 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) +// directory separator internally which is also supported with Windows system APIs. However, +// if file operations are performed with the non-default directory separator, the Webpack cache +// will contain a key that will not be purged. +function createToSystemPath(): (path: string) => string { + if (process.platform === 'win32') { + const cache = new Map(); + + return (path) => { + let value = cache.get(path); + if (value === undefined) { + value = path.replace(/\//g, '\\'); + cache.set(path, value); + } + + return value; + }; + } + + // POSIX-like platforms retain the existing directory separator + return (path) => path; +} + +export function createWebpackSystem(input: InputFileSystem, currentDirectory: string): ts.System { + const toSystemPath = createToSystemPath(); + + const system: ts.System = { + ...ts.sys, + readFile(path: string) { + let data; + try { + data = input.readFileSync(toSystemPath(path)); + } catch { + return undefined; + } + + // Strip BOM if present + let start = 0; + if (data.length > 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) { + start = 3; + } + + return data.toString('utf8', start); + }, + getFileSize(path: string) { + try { + return input.statSync(toSystemPath(path)).size; + } catch { + return 0; + } + }, + fileExists(path: string) { + try { + return input.statSync(toSystemPath(path)).isFile(); + } catch { + return false; + } + }, + directoryExists(path: string) { + try { + return input.statSync(toSystemPath(path)).isDirectory(); + } catch { + return false; + } + }, + getModifiedTime(path: string) { + try { + return input.statSync(toSystemPath(path)).mtime; + } catch { + return undefined; + } + }, + getCurrentDirectory() { + return currentDirectory; + }, + writeFile: shouldNotWrite, + createDirectory: shouldNotWrite, + deleteFile: shouldNotWrite, + setModifiedTime: shouldNotWrite, + }; + + return system; +} diff --git a/packages/ngtools/webpack/src/ivy/transformation.ts b/packages/ngtools/webpack/src/ivy/transformation.ts new file mode 100644 index 000000000000..a76f3ccd0d0d --- /dev/null +++ b/packages/ngtools/webpack/src/ivy/transformation.ts @@ -0,0 +1,141 @@ +/** + * @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 { constructorParametersDownlevelTransform } from '@angular/compiler-cli'; +import * as ts from 'typescript'; +import { elideImports } from '../transformers/elide_imports'; +import { removeIvyJitSupportCalls } from '../transformers/remove-ivy-jit-support-calls'; +import { replaceResources } from '../transformers/replace_resources'; + +export function createAotTransformers( + builder: ts.BuilderProgram, + options: { emitClassMetadata?: boolean; emitNgModuleScope?: boolean }, +): ts.CustomTransformers { + const getTypeChecker = () => builder.getProgram().getTypeChecker(); + const transformers: ts.CustomTransformers = { + before: [replaceBootstrap(getTypeChecker)], + after: [], + }; + + const removeClassMetadata = !options.emitClassMetadata; + const removeNgModuleScope = !options.emitNgModuleScope; + if (removeClassMetadata || removeNgModuleScope) { + // tslint:disable-next-line: no-non-null-assertion + transformers.after!.push( + removeIvyJitSupportCalls(removeClassMetadata, removeNgModuleScope, getTypeChecker), + ); + } + + return transformers; +} + +export function createJitTransformers( + builder: ts.BuilderProgram, + options: { directTemplateLoading?: boolean }, +): ts.CustomTransformers { + const getTypeChecker = () => builder.getProgram().getTypeChecker(); + + return { + before: [ + replaceResources(() => true, getTypeChecker, options.directTemplateLoading), + constructorParametersDownlevelTransform(builder.getProgram()), + ], + }; +} + +export function mergeTransformers( + first: ts.CustomTransformers, + second: ts.CustomTransformers, +): ts.CustomTransformers { + const result: ts.CustomTransformers = {}; + + if (first.before || second.before) { + result.before = [...(first.before || []), ...(second.before || [])]; + } + + if (first.after || second.after) { + result.after = [...(first.after || []), ...(second.after || [])]; + } + + if (first.afterDeclarations || second.afterDeclarations) { + result.afterDeclarations = [ + ...(first.afterDeclarations || []), + ...(second.afterDeclarations || []), + ]; + } + + return result; +} + +export function replaceBootstrap( + getTypeChecker: () => ts.TypeChecker, +): ts.TransformerFactory { + return (context: ts.TransformationContext) => { + let bootstrapImport: ts.ImportDeclaration | undefined; + let bootstrapNamespace: ts.Identifier | undefined; + const replacedNodes: ts.Node[] = []; + + const visitNode: ts.Visitor = (node: ts.Node) => { + if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) { + const target = node.expression; + if (target.text === 'platformBrowserDynamic') { + if (!bootstrapNamespace) { + bootstrapNamespace = ts.createUniqueName('__NgCli_bootstrap_'); + bootstrapImport = ts.createImportDeclaration( + undefined, + undefined, + ts.createImportClause(undefined, ts.createNamespaceImport(bootstrapNamespace)), + ts.createLiteral('@angular/platform-browser'), + ); + } + replacedNodes.push(target); + + return ts.updateCall( + node, + ts.createPropertyAccess(bootstrapNamespace, 'platformBrowser'), + node.typeArguments, + node.arguments, + ); + } + } + + return ts.visitEachChild(node, visitNode, context); + }; + + return (sourceFile: ts.SourceFile) => { + let updatedSourceFile = ts.visitEachChild(sourceFile, visitNode, context); + + if (bootstrapImport) { + // Remove any unused platform browser dynamic imports + const removals = elideImports( + updatedSourceFile, + replacedNodes, + getTypeChecker, + context.getCompilerOptions(), + ).map((op) => op.target); + if (removals.length > 0) { + updatedSourceFile = ts.visitEachChild( + updatedSourceFile, + (node) => (removals.includes(node) ? undefined : node), + context, + ); + } + + // Add new platform browser import + return ts.updateSourceFileNode( + updatedSourceFile, + ts.setTextRange( + ts.createNodeArray([bootstrapImport, ...updatedSourceFile.statements]), + sourceFile.statements, + ), + ); + } else { + return updatedSourceFile; + } + }; + }; +} diff --git a/packages/ngtools/webpack/src/resource_loader.ts b/packages/ngtools/webpack/src/resource_loader.ts index cb3cbc234cdb..978eecddf029 100644 --- a/packages/ngtools/webpack/src/resource_loader.ts +++ b/packages/ngtools/webpack/src/resource_loader.ts @@ -32,11 +32,34 @@ export class WebpackResourceLoader { private _cachedSources = new Map(); private _cachedEvaluatedSources = new Map(); - constructor() {} + private buildTimestamp?: number; + public changedFiles = new Set(); - update(parentCompilation: any) { + update(parentCompilation: import('webpack').compilation.Compilation) { this._parentCompilation = parentCompilation; this._context = parentCompilation.context; + + // Update changed file list + if (this.buildTimestamp !== undefined) { + this.changedFiles.clear(); + for (const [file, time] of parentCompilation.fileTimestamps) { + if (this.buildTimestamp < time) { + this.changedFiles.add(file); + } + } + } + this.buildTimestamp = Date.now(); + } + + getModifiedResourceFiles() { + const modifiedResources = new Set(); + for (const changedFile of this.changedFiles) { + this.getAffectedResources( + changedFile, + ).forEach((affected: string) => modifiedResources.add(affected)); + } + + return modifiedResources; } getResourceDependencies(filePath: string) { @@ -47,6 +70,10 @@ export class WebpackResourceLoader { return this._reverseDependencies.get(file) || []; } + setAffectedResources(file: string, resources: Iterable) { + this._reverseDependencies.set(file, new Set(resources)); + } + private async _compile(filePath: string): Promise { if (!this._parentCompilation) { From aa5e1a9590e30b80b4b7eff7db7a107094062114 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Tue, 21 Apr 2020 18:24:37 -0400 Subject: [PATCH 2/5] refactor(@angular-devkit/build-angular): integrate ivy-only compiler plugin --- .../src/webpack/configs/typescript.ts | 137 ++++++++++++++++-- 1 file changed, 122 insertions(+), 15 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts index 6e854baf8130..ba49905e4ec1 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts @@ -13,9 +13,78 @@ import { AngularCompilerPlugin, AngularCompilerPluginOptions, NgToolsLoader, - PLATFORM + PLATFORM, + ivy, } from '@ngtools/webpack'; import { WebpackConfigOptions, BuildOptions } from '../../utils/build-options'; +import { CompilerOptions } from '@angular/compiler-cli'; +import { RuleSetLoader } from 'webpack'; + +function canUseIvyPlugin(wco: WebpackConfigOptions): boolean { + // Can only be used with Ivy + if (!wco.tsConfig.options.enableIvy) { + return false; + } + + // Allow fallback to legacy build system via environment variable ('NG_BUILD_IVY_LEGACY=1') + const flag = process.env['NG_BUILD_IVY_LEGACY']; + if (flag !== undefined && flag !== '0' && flag.toLowerCase() !== 'false') { + wco.logger.warn( + '"NG_BUILD_IVY_LEGACY" environment variable detected. Using legacy Ivy build system.', + ); + + return false; + } + + // Lazy modules option uses the deprecated string format for lazy routes which is not supported + if (wco.buildOptions.lazyModules && wco.buildOptions.lazyModules.length > 0) { + wco.logger.warn( + '"lazyModules" option is deprecated and not supported by the new Ivy build system. ' + + 'Using legacy Ivy build system.' + ); + + return false; + } + + // This pass relies on internals of the original plugin + if (wco.buildOptions.experimentalRollupPass) { + return false; + } + + return true; +} + +function createIvyPlugin( + wco: WebpackConfigOptions, + aot: boolean, + tsconfig: string, +): ivy.AngularWebpackPlugin { + const { buildOptions } = wco; + const optimize = buildOptions.optimization.scripts; + + const compilerOptions: CompilerOptions = { + skipTemplateCodegen: !aot, + sourceMap: buildOptions.sourceMap.scripts, + }; + + if (buildOptions.preserveSymlinks !== undefined) { + compilerOptions.preserveSymlinks = buildOptions.preserveSymlinks; + } + + const fileReplacements: Record = {}; + if (buildOptions.fileReplacements) { + for (const replacement of buildOptions.fileReplacements) { + fileReplacements[replacement.replace] = replacement.with; + } + } + + return new ivy.AngularWebpackPlugin({ + tsconfig, + compilerOptions, + fileReplacements, + emitNgModuleScope: !optimize, + }); +} function _pluginOptionsOverrides( buildOptions: BuildOptions, @@ -103,40 +172,78 @@ function _createAotPlugin( export function getNonAotConfig(wco: WebpackConfigOptions) { const { tsConfigPath } = wco; + const useIvyOnlyPlugin = canUseIvyPlugin(wco); return { - module: { rules: [{ test: /\.tsx?$/, loader: NgToolsLoader }] }, - plugins: [_createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true })] + module: { + rules: [ + { + test: useIvyOnlyPlugin ? /\.[jt]sx?$/ : /\.tsx?$/, + loader: useIvyOnlyPlugin + ? ivy.AngularWebpackLoaderPath + : NgToolsLoader, + }, + ], + }, + plugins: [ + useIvyOnlyPlugin + ? createIvyPlugin(wco, false, tsConfigPath) + : _createAotPlugin(wco, { tsConfigPath, skipCodeGeneration: true }), + ], }; } export function getAotConfig(wco: WebpackConfigOptions, i18nExtract = false) { const { tsConfigPath, buildOptions } = wco; + const optimize = buildOptions.optimization.scripts; + const useIvyOnlyPlugin = canUseIvyPlugin(wco) && !i18nExtract; - const loaders: any[] = [NgToolsLoader]; + let buildOptimizerRules: RuleSetLoader[] = []; if (buildOptions.buildOptimizer) { - loaders.unshift({ + buildOptimizerRules = [{ loader: buildOptimizerLoaderPath, options: { sourceMap: buildOptions.sourceMap.scripts } - }); + }]; } - const test = /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/; - const optimize = wco.buildOptions.optimization.scripts; - return { - module: { rules: [{ test, use: loaders }] }, + module: { + rules: [ + { + test: useIvyOnlyPlugin ? /\.tsx?$/ : /(?:\.ngfactory\.js|\.ngstyle\.js|\.tsx?)$/, + use: [ + ...buildOptimizerRules, + useIvyOnlyPlugin ? ivy.AngularWebpackLoaderPath : NgToolsLoader, + ], + }, + // "allowJs" support with ivy plugin - ensures build optimizer is not run twice + ...(useIvyOnlyPlugin + ? [ + { + test: /\.jsx?$/, + use: [ivy.AngularWebpackLoaderPath], + }, + ] + : []), + ], + }, plugins: [ - _createAotPlugin( - wco, - { tsConfigPath, emitClassMetadata: !optimize, emitNgModuleScope: !optimize }, - i18nExtract, - ), + useIvyOnlyPlugin + ? createIvyPlugin(wco, true, tsConfigPath) + : _createAotPlugin( + wco, + { tsConfigPath, emitClassMetadata: !optimize, emitNgModuleScope: !optimize }, + i18nExtract, + ), ], }; } export function getTypescriptWorkerPlugin(wco: WebpackConfigOptions, workerTsConfigPath: string) { + if (canUseIvyPlugin(wco)) { + return createIvyPlugin(wco, false, workerTsConfigPath); + } + const { buildOptions } = wco; let pluginOptions: AngularCompilerPluginOptions = { From 9092df35586b66ff515cb34e95cf4afee1ad4472 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 13 May 2020 23:14:26 -0400 Subject: [PATCH 3/5] test(@angular-devkit/build-angular): disable lazy routing string format tests The new Ivy-only Webpack plugin no longer supports the deprecate string format for lazy routes. --- .../src/browser/specs/lazy-module_spec.ts | 12 ++++++++++++ tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts index ffb07abf2026..72d4932bec04 100644 --- a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts +++ b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts @@ -61,6 +61,12 @@ describe('Browser Builder lazy modules', () => { for (const [name, imports] of cases) { describe(`Load children ${name} syntax`, () => { it('supports lazy bundle for lazy routes with JIT', async () => { + if (name === 'string' && !veEnabled) { + pending('Does not apply to Ivy.'); + + return; + } + host.writeMultipleFiles(lazyModuleFiles); host.writeMultipleFiles(imports); @@ -73,6 +79,12 @@ describe('Browser Builder lazy modules', () => { }); it('supports lazy bundle for lazy routes with AOT', async () => { + if (name === 'string' && !veEnabled) { + pending('Does not apply to Ivy.'); + + return; + } + host.writeMultipleFiles(lazyModuleFiles); host.writeMultipleFiles(imports); addLazyLoadedModulesInTsConfig(host, lazyModuleFiles); diff --git a/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts b/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts index 8426c8a354d0..0754ec3ba54b 100644 --- a/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts +++ b/tests/legacy-cli/e2e/tests/build/lazy-load-syntax.ts @@ -86,8 +86,13 @@ export default async function () { await ng('e2e'); await ng('e2e', '--prod'); + if (!(getGlobalVariable('argv')['ve'])) { + // Only applicable to VE. Does not apply to Ivy. + return; + } + // Test string import. - // Both Ivy and View Engine should support it. + // View Engine should support it. await updateJsonFile('tsconfig.app.json', tsConfig => { tsConfig.files.push('src/app/lazy/lazy.module.ts'); }); From 9ddc55989f3706bbaa21eabf88c2a5dbf3b9684a Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 6 Sep 2020 13:23:15 -0400 Subject: [PATCH 4/5] refactor(@ngtools/webpack): allow paths plugin to update compiler options This change allows the compiler options used by the TypeScript paths plugin to be updated if the TypeScript configuration file is changed during a rebuild. --- packages/ngtools/webpack/src/paths-plugin.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/ngtools/webpack/src/paths-plugin.ts b/packages/ngtools/webpack/src/paths-plugin.ts index 46bf71ebe1cb..4b461cf91f1d 100644 --- a/packages/ngtools/webpack/src/paths-plugin.ts +++ b/packages/ngtools/webpack/src/paths-plugin.ts @@ -16,14 +16,14 @@ export interface TypeScriptPathsPluginOptions extends Pick { return new Promise((resolve, reject) => { @@ -46,6 +46,10 @@ export class TypeScriptPathsPlugin { resolver.getHook('described-resolve').tapPromise( 'TypeScriptPathsPlugin', async (request: NormalModuleFactoryRequest, resolveContext: {}) => { + if (!this.options) { + throw new Error('TypeScriptPathsPlugin options were not provided.'); + } + if (!request || request.typescriptPathMapped) { return; } @@ -70,11 +74,11 @@ export class TypeScriptPathsPlugin { return; } - const replacements = findReplacements(originalRequest, this._options.paths || {}); + const replacements = findReplacements(originalRequest, this.options.paths || {}); for (const potential of replacements) { const potentialRequest = { ...request, - request: path.resolve(this._options.baseUrl || '', potential), + request: path.resolve(this.options.baseUrl || '', potential), typescriptPathMapped: true, }; const result = await resolveAsync(potentialRequest, resolveContext); From 409c5d375ec9a821c2393ab80d81bf394258b51f Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Sun, 11 Oct 2020 14:17:25 -0400 Subject: [PATCH 5/5] refactor(@angular-devkit/build-angular): gate Ivy-only plugin behind feature flag The new Ivy Webpack plugin is considered experimental and can only be used by enabling the `NG_BUILD_IVY_EXPERIMENTAL` environment variable. --- .circleci/config.yml | 8 +++++ .../src/webpack/configs/typescript.ts | 35 ++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 56a6364fe701..2a6ea47ef7e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -204,6 +204,14 @@ jobs: command: | rm -rf /mnt/ramdisk/*test-project PATH=~/.npm-global/bin:$PATH node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.ve >>--ve<> <<# parameters.snapshots >>--ng-snapshots<> --yarn --tmpdir=/mnt/ramdisk --glob="{tests/basic/**,tests/update/**}" + - unless: + condition: << parameters.ve >> + steps: + - run: + name: Execute CLI E2E Test Subset for Ivy-only plugin + command: | + rm -rf /mnt/ramdisk/*test-project + PATH=~/.npm-global/bin:$PATH NG_BUILD_IVY_EXPERIMENTAL=1 node ./tests/legacy-cli/run_e2e --nb-shards=${CIRCLE_NODE_TOTAL} --shard=${CIRCLE_NODE_INDEX} <<# parameters.snapshots >>--ng-snapshots<> --tmpdir=/mnt/ramdisk --glob="{tests/basic/**,tests/build/**}" e2e-cli-node-10: executor: diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts index ba49905e4ec1..cd09ade41f08 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/typescript.ts @@ -26,21 +26,29 @@ function canUseIvyPlugin(wco: WebpackConfigOptions): boolean { return false; } - // Allow fallback to legacy build system via environment variable ('NG_BUILD_IVY_LEGACY=1') - const flag = process.env['NG_BUILD_IVY_LEGACY']; - if (flag !== undefined && flag !== '0' && flag.toLowerCase() !== 'false') { - wco.logger.warn( - '"NG_BUILD_IVY_LEGACY" environment variable detected. Using legacy Ivy build system.', - ); - + // Allow new ivy build system via environment variable ('NG_BUILD_IVY_EXPERIMENTAL=1') + // TODO: Remove this section and adjust warnings below once the Ivy plugin is the default + const flag = process.env['NG_BUILD_IVY_EXPERIMENTAL']; + if (flag === undefined || flag === '0' || flag.toLowerCase() === 'false') { return false; } + // Allow fallback to legacy build system via environment variable ('NG_BUILD_IVY_LEGACY=1') + // TODO: Enable this section once the Ivy plugin is the default + // const flag = process.env['NG_BUILD_IVY_LEGACY']; + // if (flag !== undefined && flag !== '0' && flag.toLowerCase() !== 'false') { + // wco.logger.warn( + // '"NG_BUILD_IVY_LEGACY" environment variable detected. Using legacy Ivy build system.', + // ); + + // return false; + // } + // Lazy modules option uses the deprecated string format for lazy routes which is not supported if (wco.buildOptions.lazyModules && wco.buildOptions.lazyModules.length > 0) { wco.logger.warn( - '"lazyModules" option is deprecated and not supported by the new Ivy build system. ' + - 'Using legacy Ivy build system.' + '"lazyModules" option is deprecated and not supported by the experimental Ivy build system. ' + + 'Using original Ivy build system.' ); return false; @@ -48,9 +56,18 @@ function canUseIvyPlugin(wco: WebpackConfigOptions): boolean { // This pass relies on internals of the original plugin if (wco.buildOptions.experimentalRollupPass) { + wco.logger.warn( + 'The experimental rollup pass is not supported by the experimental Ivy build system. ' + + 'Using original Ivy build system.' + ); + return false; } + wco.logger.warn( + '"NG_BUILD_IVY_EXPERIMENTAL" environment variable detected. Using experimental Ivy build system.', + ); + return true; }