From 7a44399cd260401f319936057d51f143b2573464 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Mon, 1 Mar 2021 11:19:59 -0500 Subject: [PATCH] refactor(@angular-devkit/build-angular): use custom babel loader for i18n dev-server support The custom babel loader allows files to be conditionally processed by the i18n inlining transforms based on both file path and content. By allowing content based checks, the entire parse/transform/print process can be skipped for files that do not contain localizations. --- .../build_angular/src/babel/webpack-loader.ts | 63 ++++++++++++------- .../build_angular/src/dev-server/index.ts | 52 +++------------ 2 files changed, 50 insertions(+), 65 deletions(-) diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts index bf6821b3b0f5..6283c2d0ac1e 100644 --- a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts +++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts @@ -7,11 +7,13 @@ */ import { custom } from 'babel-loader'; import { ScriptTarget } from 'typescript'; +import { ApplicationPresetOptions } from './presets/application'; interface AngularCustomOptions { forceAsyncTransformation: boolean; forceES5: boolean; shouldLink: boolean; + i18n: ApplicationPresetOptions['i18n']; } /** @@ -65,12 +67,18 @@ export default custom(() => { }); return { - async customOptions({ scriptTarget, ...loaderOptions }, { source }) { + async customOptions({ i18n, scriptTarget, ...rawOptions }, { source }) { // Must process file if plugins are added - let shouldProcess = Array.isArray(loaderOptions.plugins) && loaderOptions.plugins.length > 0; + let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0; + + const customOptions: AngularCustomOptions = { + forceAsyncTransformation: false, + forceES5: false, + shouldLink: false, + i18n: undefined, + }; // Analyze file for linking - let shouldLink = false; const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source); if (requiresLinking && !hasLinkerSupport) { // Cannot link if there is no linker support @@ -78,43 +86,51 @@ export default custom(() => { 'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.', ); } else { - shouldLink = requiresLinking; + customOptions.shouldLink = requiresLinking; } - shouldProcess ||= shouldLink; + shouldProcess ||= customOptions.shouldLink; // Analyze for ES target processing - let forceES5 = false; - let forceAsyncTransformation = false; - const esTarget = scriptTarget as ScriptTarget; - if (esTarget < ScriptTarget.ES2015) { - // TypeScript files will have already been downlevelled - forceES5 = !/\.tsx?$/.test(this.resourcePath); - } else if (esTarget >= ScriptTarget.ES2017) { - forceAsyncTransformation = source.includes('async'); + const esTarget = scriptTarget as ScriptTarget | undefined; + if (esTarget !== undefined) { + if (esTarget < ScriptTarget.ES2015) { + // TypeScript files will have already been downlevelled + customOptions.forceES5 = !/\.tsx?$/.test(this.resourcePath); + } else if (esTarget >= ScriptTarget.ES2017) { + customOptions.forceAsyncTransformation = source.includes('async'); + } + shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5; + } + + // Analyze for i18n inlining + if ( + i18n && + !/[\\\/]@angular[\\\/](?:compiler|localize)/.test(this.resourcePath) && + source.includes('$localize') + ) { + customOptions.i18n = i18n as ApplicationPresetOptions['i18n']; + shouldProcess = true; } - shouldProcess ||= forceAsyncTransformation || forceES5; // Add provided loader options to default base options - const options: Record = { + const loaderOptions: Record = { ...baseOptions, - ...loaderOptions, + ...rawOptions, cacheIdentifier: JSON.stringify({ buildAngular: require('../../package.json').version, - forceAsyncTransformation, - forceES5, - shouldLink, + customOptions, baseOptions, - loaderOptions, + rawOptions, }), }; // Skip babel processing if no actions are needed if (!shouldProcess) { // Force the current file to be ignored - options.ignore = [() => true]; + loaderOptions.ignore = [() => true]; } - return { custom: { forceAsyncTransformation, forceES5, shouldLink }, loader: options }; + return { custom: customOptions, loader: loaderOptions }; }, config(configuration, { customOptions }) { return { @@ -127,6 +143,7 @@ export default custom(() => { angularLinker: customOptions.shouldLink, forceES5: customOptions.forceES5, forceAsyncTransformation: customOptions.forceAsyncTransformation, + i18n: customOptions.i18n, diagnosticReporter: (type, message) => { switch (type) { case 'error': @@ -139,7 +156,7 @@ export default custom(() => { break; } }, - } as import('./presets/application').ApplicationPresetOptions, + } as ApplicationPresetOptions, ], ], }; diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts index e48a48331e7f..c25eac0344f1 100644 --- a/packages/angular_devkit/build_angular/src/dev-server/index.ts +++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts @@ -354,7 +354,6 @@ async function setupLocalize( webpackConfig: webpack.Configuration, ) { const localeDescription = i18n.locales[locale]; - const i18nDiagnostics: { type: string, message: string }[] = []; // Modify main entrypoint to include locale data if ( @@ -378,37 +377,25 @@ async function setupLocalize( translation = {}; } + const i18nLoaderOptions = { + locale, + missingTranslationBehavior, + translation: i18n.shouldInline ? translation : undefined, + }; + const i18nRule: webpack.RuleSetRule = { - test: /\.(?:m?js|ts)$/, + test: /\.(?:[cm]?js|ts)$/, enforce: 'post', use: [ { - loader: require.resolve('babel-loader'), + loader: require.resolve('../babel/webpack-loader'), options: { - babelrc: false, - configFile: false, - compact: false, - cacheCompression: false, - cacheDirectory: findCachePath('babel-loader'), + cacheDirectory: findCachePath('babel-dev-server-i18n'), cacheIdentifier: JSON.stringify({ - buildAngular: require('../../package.json').version, locale, translationIntegrity: localeDescription?.files.map((file) => file.integrity), }), - sourceType: 'unambiguous', - presets: [ - [ - require.resolve('../babel/presets/application'), - { - i18n: { - locale, - translation: i18n.shouldInline ? translation : undefined, - missingTranslationBehavior, - }, - diagnosticReporter: (type, message) => i18nDiagnostics.push({ type, message }), - } as import('../babel/presets/application').ApplicationPresetOptions, - ], - ], + i18n: i18nLoaderOptions, }, }, ], @@ -423,25 +410,6 @@ async function setupLocalize( } rules.push(i18nRule); - - // Add a plugin to inject the i18n diagnostics - // tslint:disable-next-line: no-non-null-assertion - webpackConfig.plugins!.push({ - apply: (compiler: webpack.Compiler) => { - compiler.hooks.thisCompilation.tap('build-angular', compilation => { - compilation.hooks.finishModules.tap('build-angular', () => { - for (const diagnostic of i18nDiagnostics) { - if (diagnostic.type === 'error') { - addError(compilation, diagnostic.message); - } else { - addWarning(compilation, diagnostic.message); - } - } - i18nDiagnostics.length = 0; - }); - }); - }, - }); } export default createBuilder(serveWebpackBrowser);