From 47f0892594a8227a52080b278c13d28c8efe4f36 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:57:23 -0400 Subject: [PATCH] refactor(@angular-devkit/build-angular): directly configure internal babel plugins for application builder The linker and build optimizer related babel plugins are now directly imported when needed within the JavaScript transformer worker code. This lowers the number of transitive modules that must be loaded for each worker instance. It also removes the use of `require` from the initialization code which provides support for full ESM output in the future. --- .../src/tools/babel/plugins/index.ts | 12 ++ .../esbuild/javascript-transformer-worker.ts | 135 +++++++++++++----- 2 files changed, 115 insertions(+), 32 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/tools/babel/plugins/index.ts diff --git a/packages/angular_devkit/build_angular/src/tools/babel/plugins/index.ts b/packages/angular_devkit/build_angular/src/tools/babel/plugins/index.ts new file mode 100644 index 000000000000..7f1bff038f49 --- /dev/null +++ b/packages/angular_devkit/build_angular/src/tools/babel/plugins/index.ts @@ -0,0 +1,12 @@ +/** + * @license + * Copyright Google LLC 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 { default as adjustStaticMembers } from './adjust-static-class-members'; +export { default as adjustTypeScriptEnums } from './adjust-typescript-enums'; +export { default as elideAngularMetadata } from './elide-angular-metadata'; +export { default as markTopLevelPure } from './pure-toplevel-functions'; diff --git a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts index b4fe58828e3a..cc2db729c653 100644 --- a/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts +++ b/packages/angular_devkit/build_angular/src/tools/esbuild/javascript-transformer-worker.ts @@ -6,9 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import { transformAsync } from '@babel/core'; +import { type PluginItem, transformAsync } from '@babel/core'; +import fs from 'node:fs'; +import path from 'node:path'; import Piscina from 'piscina'; -import angularApplicationPreset, { requiresLinking } from '../../tools/babel/presets/application'; import { loadEsmModule } from '../../utils/load-esm'; interface JavaScriptTransformRequest { @@ -37,10 +38,18 @@ export default async function transformJavaScript( return Piscina.move(textEncoder.encode(transformedData)); } +/** + * Cached instance of the compiler-cli linker's createEs2015LinkerPlugin function. + */ let linkerPluginCreator: | typeof import('@angular/compiler-cli/linker/babel').createEs2015LinkerPlugin | undefined; +/** + * Cached instance of the compiler-cli linker's needsLinking function. + */ +let needsLinking: typeof import('@angular/compiler-cli/linker').needsLinking | undefined; + async function transformWithBabel( filename: string, data: string, @@ -51,22 +60,36 @@ async function transformWithBabel( options.sourcemap && (!!options.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename)); - // If no additional transformations are needed, return the data directly - if (!options.advancedOptimizations && !shouldLink) { - // Strip sourcemaps if they should not be used - return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); - } - - const sideEffectFree = options.sideEffects === false; - const safeAngularPackage = sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); + const plugins: PluginItem[] = []; // Lazy load the linker plugin only when linking is required if (shouldLink) { - linkerPluginCreator ??= ( - await loadEsmModule( - '@angular/compiler-cli/linker/babel', - ) - ).createEs2015LinkerPlugin; + const linkerPlugin = await createLinkerPlugin(options); + plugins.push(linkerPlugin); + } + + if (options.advancedOptimizations) { + const sideEffectFree = options.sideEffects === false; + const safeAngularPackage = + sideEffectFree && /[\\/]node_modules[\\/]@angular[\\/]/.test(filename); + + const { adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure } = + await import('../babel/plugins'); + + if (safeAngularPackage) { + plugins.push(markTopLevelPure); + } + + plugins.push(elideAngularMetadata, adjustTypeScriptEnums, [ + adjustStaticMembers, + { wrapDecorators: sideEffectFree }, + ]); + } + + // If no additional transformations are needed, return the data directly + if (plugins.length === 0) { + // Strip sourcemaps if they should not be used + return useInputSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); } const result = await transformAsync(data, { @@ -77,23 +100,7 @@ async function transformWithBabel( configFile: false, babelrc: false, browserslistConfigFile: false, - plugins: [], - presets: [ - [ - angularApplicationPreset, - { - angularLinker: linkerPluginCreator && { - shouldLink, - jitMode: options.jit, - linkerPluginCreator, - }, - optimize: options.advancedOptimizations && { - pureTopLevel: safeAngularPackage, - wrapDecorators: sideEffectFree, - }, - }, - ], - ], + plugins, }); const outputCode = result?.code ?? data; @@ -104,3 +111,67 @@ async function transformWithBabel( ? outputCode : outputCode.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''); } + +async function requiresLinking(path: string, source: string): Promise { + // @angular/core and @angular/compiler will cause false positives + // Also, TypeScript files do not require linking + if (/[\\/]@angular[\\/](?:compiler|core)|\.tsx?$/.test(path)) { + return false; + } + + if (!needsLinking) { + // Load ESM `@angular/compiler-cli/linker` using the TypeScript dynamic import workaround. + // Once TypeScript provides support for keeping the dynamic import this workaround can be + // changed to a direct dynamic import. + const linkerModule = await loadEsmModule( + '@angular/compiler-cli/linker', + ); + needsLinking = linkerModule.needsLinking; + } + + return needsLinking(path, source); +} + +async function createLinkerPlugin(options: Omit) { + linkerPluginCreator ??= ( + await loadEsmModule( + '@angular/compiler-cli/linker/babel', + ) + ).createEs2015LinkerPlugin; + + const linkerPlugin = linkerPluginCreator({ + linkerJitMode: options.jit, + // This is a workaround until https://github.com/angular/angular/issues/42769 is fixed. + sourceMapping: false, + logger: { + level: 1, // Info level + debug(...args: string[]) { + // eslint-disable-next-line no-console + console.debug(args); + }, + info(...args: string[]) { + // eslint-disable-next-line no-console + console.info(args); + }, + warn(...args: string[]) { + // eslint-disable-next-line no-console + console.warn(args); + }, + error(...args: string[]) { + // eslint-disable-next-line no-console + console.error(args); + }, + }, + fileSystem: { + resolve: path.resolve, + exists: fs.existsSync, + dirname: path.dirname, + relative: path.relative, + readFile: fs.readFileSync, + // Node.JS types don't overlap the Compiler types. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + }); + + return linkerPlugin; +}