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 000000000000..5a408783788d --- /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 71bc698a5767..f0e12ec93a9e 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 000000000000..8dfe97b1481f --- /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 61f694532a96..b56e6a80bcda 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, ], },