From 35d8adf02002115f5d6c2964c261f9336297abb8 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 16 Dec 2020 10:44:42 -0500 Subject: [PATCH] feat(@angular-devkit/build-angular): integrate Angular compiler linker The newly introduced library linker mode that removes the need for ngcc is integrated into the Angular CLI's build system. This allows libraries that are built in linker mode to be used when building an application. --- .../build_angular/src/babel/babel-loader.d.ts | 24 ++++ .../src/babel/presets/application.ts | 41 +++++- .../build_angular/src/babel/webpack-loader.ts | 120 ++++++++++++++++++ .../src/webpack/configs/common.ts | 38 ++---- 4 files changed, 195 insertions(+), 28 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts create mode 100644 packages/angular_devkit/build_angular/src/babel/webpack-loader.ts diff --git a/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts new file mode 100644 index 0000000000..5a40878378 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts @@ -0,0 +1,24 @@ +/** + * @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 + */ +declare module 'babel-loader' { + type BabelLoaderCustomizer = ( + babel: typeof import('@babel/core'), + ) => { + customOptions?( + this: import('webpack').loader.LoaderContext, + loaderOptions: Record, + loaderArguments: { source: string; map?: unknown }, + ): Promise<{ custom?: T; loader: Record }>; + config?( + this: import('webpack').loader.LoaderContext, + configuration: import('@babel/core').PartialConfig, + loaderArguments: { source: string; map?: unknown; customOptions: T }, + ): import('@babel/core').TransformOptions; + }; + function custom(customizer: BabelLoaderCustomizer): import('webpack').loader.Loader; +} diff --git a/packages/angular_devkit/build_angular/src/babel/presets/application.ts b/packages/angular_devkit/build_angular/src/babel/presets/application.ts index 71bc698a57..f0e12ec93a 100644 --- a/packages/angular_devkit/build_angular/src/babel/presets/application.ts +++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts @@ -5,9 +5,10 @@ * 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 fs from 'fs'; import * as path from 'path'; -export type DiagnosticReporter = (type: 'error' | 'warning', message: string) => void; +export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: string) => void; export interface ApplicationPresetOptions { i18n?: { locale: string; @@ -15,6 +16,8 @@ export interface ApplicationPresetOptions { translation?: unknown; }; + angularLinker?: boolean; + forceES5?: boolean; forceAsyncTransformation?: boolean; @@ -98,11 +101,47 @@ function createI18nPlugins( return plugins; } +function createNgtscLogger( + reporter: DiagnosticReporter | undefined, +): import('@angular/compiler-cli/src/ngtsc/logging').Logger { + return { + level: 1, // Info level + debug(...args: string[]) {}, + info(...args: string[]) { + reporter?.('info', args.join()); + }, + warn(...args: string[]) { + reporter?.('warning', args.join()); + }, + error(...args: string[]) { + reporter?.('error', args.join()); + }, + }; +} + export default function (api: unknown, options: ApplicationPresetOptions) { const presets = []; const plugins = []; let needRuntimeTransform = false; + if (options.angularLinker) { + // Babel currently is synchronous so import cannot be used + const { + createEs2015LinkerPlugin, + } = require('@angular/compiler-cli/linker/babel'); + + plugins.push(createEs2015LinkerPlugin({ + logger: createNgtscLogger(options.diagnosticReporter), + fileSystem: { + resolve: path.resolve, + exists: fs.existsSync, + dirname: path.dirname, + relative: path.relative, + readFile: fs.readFileSync, + }, + })); + } + if (options.forceES5) { presets.push([ require('@babel/preset-env').default, diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts new file mode 100644 index 0000000000..8dfe97b148 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -0,0 +1,120 @@ +/** + * @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 { custom } from 'babel-loader'; + +interface AngularCustomOptions { + forceES5: boolean; + shouldLink: boolean; +} + +/** + * Cached linker check utility function + * + * If undefined, not yet been imported + * If null, attempted import failed and no linker support + * If function, import succeeded and linker supported + */ +let needsLinking: undefined | null | typeof import('@angular/compiler-cli/linker').needsLinking; + +async function checkLinking( + path: string, + source: string, +): Promise<{ hasLinkerSupport?: boolean; requiresLinking: boolean }> { + // @angular/core and @angular/compiler will cause false positives + if (/[\\\/]@angular[\\\/](?:compiler|core)/.test(path)) { + return { requiresLinking: false }; + } + + if (needsLinking !== null) { + try { + if (needsLinking === undefined) { + needsLinking = (await import('@angular/compiler-cli/linker')).needsLinking; + } + + // If the linker entry point is present then there is linker support + return { hasLinkerSupport: true, requiresLinking: needsLinking(path, source) }; + } catch { + needsLinking = null; + } + } + + // Fallback for Angular versions less than 11.1.0 with no linker support. + // This information is used to issue errors if a partially compiled library is used when unsupported. + return { + hasLinkerSupport: false, + requiresLinking: + source.includes('ɵɵngDeclareDirective') || source.includes('ɵɵngDeclareComponent'), + }; +} + +export default custom(() => { + const baseOptions = Object.freeze({ + babelrc: false, + configFile: false, + compact: false, + cacheCompression: false, + sourceType: 'unambiguous', + }); + + return { + async customOptions({ forceES5, ...loaderOptions }, { source }) { + let shouldProcess = forceES5; + + let shouldLink = false; + const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source); + if (requiresLinking && !hasLinkerSupport) { + // Cannot link if there is no linker support + this.emitError( + 'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.', + ); + } else { + shouldLink = requiresLinking; + } + shouldProcess ||= shouldLink; + + const options: Record = { + ...baseOptions, + ...loaderOptions, + }; + + if (!shouldProcess) { + // Force the current file to be ignored + options.ignore = [() => true]; + } + + return { custom: { forceES5: !!forceES5, shouldLink }, loader: options }; + }, + config(configuration, { customOptions }) { + return { + ...configuration.options, + presets: [ + ...(configuration.options.presets || []), + [ + require('./presets/application').default, + { + angularLinker: customOptions.shouldLink, + forceES5: customOptions.forceES5, + diagnosticReporter: (type, message) => { + switch (type) { + case 'error': + this.emitError(message); + break; + case 'info': + // Webpack does not currently have an informational diagnostic + case 'warning': + this.emitWarning(message); + break; + } + }, + } as import('./presets/application').ApplicationPresetOptions, + ], + ], + }; + }, + }; +}); diff --git a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts index 61f694532a..b56e6a80bc 100644 --- a/packages/angular_devkit/build_angular/src/webpack/configs/common.ts +++ b/packages/angular_devkit/build_angular/src/webpack/configs/common.ts @@ -544,35 +544,19 @@ export function getCommonConfig(wco: WebpackConfigOptions): Configuration { sideEffects: true, }, { - test: /\.m?js$/, + test: /\.[cm]?js$/, exclude: [/[\/\\](?:core-js|\@babel|tslib|web-animations-js)[\/\\]/, /(ngfactory|ngstyle)\.js$/], use: [ - ...(wco.supportES2015 - ? [] - : [ - { - loader: require.resolve('babel-loader'), - options: { - babelrc: false, - configFile: false, - compact: false, - cacheCompression: false, - cacheDirectory: findCachePath('babel-webpack'), - cacheIdentifier: JSON.stringify({ - buildAngular: require('../../../package.json').version, - }), - sourceType: 'unambiguous', - presets: [ - [ - require.resolve('../../babel/presets/application'), - { - forceES5: true, - } as import('../../babel/presets/application').ApplicationPresetOptions, - ], - ], - }, - }, - ]), + { + loader: require.resolve('../../babel/webpack-loader'), + options: { + cacheDirectory: findCachePath('babel-webpack'), + cacheIdentifier: JSON.stringify({ + buildAngular: require('../../../package.json').version, + }), + forceES5: !wco.supportES2015, + }, + }, ...buildOptimizerUseRule, ], },