From cf1844bcce4716a300c73a38a1e07e39e09e2701 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Wed, 5 Aug 2026 09:31:47 +0000 Subject: [PATCH] refactor(devtools): use JavaScriptTransformer for Angular optimization esbuild plugin Refactor the devtools optimization esbuild plugin to use JavaScriptTransformer from @angular/build/private. --- .../tools/angular-optimization/BUILD.bazel | 3 - .../ensure-no-linker-decl.mjs | 36 ----- .../angular-optimization/esbuild-plugin.d.ts | 10 +- .../angular-optimization/esbuild-plugin.mjs | 108 ++++--------- .../tools/esbuild/esbuild-base.config.mts | 24 +-- package.json | 2 +- packages/compiler/BUILD.bazel | 1 - pnpm-lock.yaml | 152 +++++++++++++++++- 8 files changed, 182 insertions(+), 154 deletions(-) delete mode 100644 devtools/tools/angular-optimization/ensure-no-linker-decl.mjs diff --git a/devtools/tools/angular-optimization/BUILD.bazel b/devtools/tools/angular-optimization/BUILD.bazel index ca087bddb8b..8b6496af518 100644 --- a/devtools/tools/angular-optimization/BUILD.bazel +++ b/devtools/tools/angular-optimization/BUILD.bazel @@ -5,7 +5,6 @@ package(default_visibility = ["//visibility:public"]) copy_to_bin( name = "js_lib_files", srcs = [ - "ensure-no-linker-decl.mjs", "esbuild-plugin.d.ts", "esbuild-plugin.mjs", ], @@ -16,7 +15,5 @@ js_library( srcs = [":js_lib_files"], deps = [ "//:node_modules/@angular/build", - "//:node_modules/@babel/core", - "//packages/compiler-cli", ], ) diff --git a/devtools/tools/angular-optimization/ensure-no-linker-decl.mjs b/devtools/tools/angular-optimization/ensure-no-linker-decl.mjs deleted file mode 100644 index 7f3969f0302..00000000000 --- a/devtools/tools/angular-optimization/ensure-no-linker-decl.mjs +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @license - * Copyright Google LLC - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -/** Naively checks whether this node path resolves to an Angular declare invocation. */ -function isNgDeclareCallExpression(nodePath) { - if (!nodePath.node.name.startsWith('ɵɵngDeclare')) { - return false; - } - - // Expect the `ngDeclare` identifier to be used as part of a property access that - // is invoked within a call expression. e.g. `i0.ɵɵngDeclare<>`. - return ( - nodePath.parentPath?.type === 'MemberExpression' && - nodePath.parentPath.parentPath?.type === 'CallExpression' - ); -} - -/** Asserts that the given AST does not contain any Angular partial declaration. */ -export async function assertNoPartialDeclaration(filePath, ast, traverseFn) { - // Naively check if there are any Angular declarations left that haven't been linked. - traverseFn(ast, { - Identifier: (astPath) => { - if (isNgDeclareCallExpression(astPath)) { - throw astPath.buildCodeFrameError( - `Found Angular declaration that has not been linked. ${filePath}`, - Error, - ); - } - }, - }); -} diff --git a/devtools/tools/angular-optimization/esbuild-plugin.d.ts b/devtools/tools/angular-optimization/esbuild-plugin.d.ts index 0867317edcd..902aa26d9cf 100644 --- a/devtools/tools/angular-optimization/esbuild-plugin.d.ts +++ b/devtools/tools/angular-optimization/esbuild-plugin.d.ts @@ -6,17 +6,11 @@ * found in the LICENSE file at https://angular.dev/license */ -import {PluginItem} from '@babel/core'; - export interface OptimizationOptions { - enableLinker?: { - ensureNoPartialDeclaration: boolean; - filterPaths?: RegExp; - linkerOptions?: object; - }; optimize?: { isSideEffectFree?: (absoluteDiskPath: string) => boolean; }; + enableLinker?: boolean; } -export function createEsbuildAngularOptimizePlugin(opts: OptimizationOptions, additionalBabelPlugins?: PluginItem[]): Promise \ No newline at end of file +export function createEsbuildAngularOptimizePlugin(opts: OptimizationOptions): Promise \ No newline at end of file diff --git a/devtools/tools/angular-optimization/esbuild-plugin.mjs b/devtools/tools/angular-optimization/esbuild-plugin.mjs index ff731c19568..e6476d645dd 100644 --- a/devtools/tools/angular-optimization/esbuild-plugin.mjs +++ b/devtools/tools/angular-optimization/esbuild-plugin.mjs @@ -6,96 +6,42 @@ * found in the LICENSE file at https://angular.dev/license */ -import fs from 'fs'; - -import {transformAsync, traverse} from '@babel/core'; -import {assertNoPartialDeclaration} from './ensure-no-linker-decl.mjs'; - /** - * Creates an ESBuild plugin that configures various Angular optimization Babel plugins. - * The Babel plugins configured usually run in the Angular CLI compilation pipeline. + * Creates an ESBuild plugin that configures various Angular optimization. + * The optimization plugins usually run in the Angular CLI compilation pipeline. * * @param {import('./esbuild-plugin').OptimizationOptions} opts Options - * @param additionalBabelPlugins List of additional Babel plugins that should run as part - * of this ESBuild plugin. This is primarily supported for reducing the amount of ESBuild - * load plugins needed (as they can impact performance significantly). */ -export async function createEsbuildAngularOptimizePlugin(opts, additionalBabelPlugins = []) { - let linkerCreator = { - compiler: null, - babel: null, - }; - - if (opts.enableLinker) { - linkerCreator = { - compiler: await import('@angular/compiler-cli'), - babel: await import('@angular/compiler-cli/linker/babel'), - }; - } - - const {adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata, markTopLevelPure} = ( - await import('@angular/build/private') - ).default; - +export function createEsbuildAngularOptimizePlugin(opts) { return { - name: 'ng-babel-optimize-esbuild', - setup: (build) => { + name: 'ng-optimize-esbuild', + setup: async (build) => { + const {JavaScriptTransformer} = (await import('@angular/build/private')).default; + const javascriptTransformer = new JavaScriptTransformer( + { + jit: false, + advancedOptimizations: !!opts.optimize, + sourcemap: !!build.initialOptions.sourcemap, + }, + /** maxWorkers */ 2, + ); + build.onLoad({filter: /\.[cm]?js$/}, async (args) => { - const filePath = args.path; - const content = await fs.promises.readFile(filePath, 'utf8'); - const plugins = [...additionalBabelPlugins]; + const sideEffects = opts.optimize?.isSideEffectFree + ? !opts.optimize.isSideEffectFree(args.path) + : true; - if (opts.optimize) { - plugins.push(adjustStaticMembers, adjustTypeScriptEnums, elideAngularMetadata); + const contents = await javascriptTransformer.transformFile( + args.path, + /** skipLinker */ !opts.enableLinker, + sideEffects, + ); - // If the current file is denoted as explicit side effect free, add the pure - // top-level functions optimization plugin for this file. - if (opts.optimize.isSideEffectFree && opts.optimize.isSideEffectFree(args.path)) { - plugins.push(markTopLevelPure); - } - } + return {contents}; + }); - const shouldRunLinker = - opts.enableLinker && - (opts.enableLinker.filterPaths == null || opts.enableLinker.filterPaths.test(args.path)); - - if (shouldRunLinker) { - plugins.push( - linkerCreator.babel.createEs2015LinkerPlugin({ - ...(opts.enableLinker.linkerOptions ?? {}), - fileSystem: new linkerCreator.compiler.NodeJSFileSystem(), - logger: new linkerCreator.compiler.ConsoleLogger( - linkerCreator.compiler.LogLevel.warn, - ), - // Workaround for https://github.com/angular/angular/issues/42769 and https://github.com/angular/angular-cli/issues/22647. - sourceMapping: false, - }), - ); - } - - // If no plugins are enabled, return the original code and save time. - if (plugins.length === 0) { - return {contents: content}; - } - - const ensureNoPartialDeclaration = - opts.enableLinker && opts.enableLinker.ensureNoPartialDeclaration; - const {code, ast} = await transformAsync(content, { - filename: filePath, - filenameRelative: filePath, - plugins: plugins, - // Sourcemaps are generated inline so that ESBuild can process them. - sourceMaps: 'inline', - compact: false, - // AST is needed when we want to ensure no partial declarations later. - ast: ensureNoPartialDeclaration, - }); - - if (ensureNoPartialDeclaration) { - assertNoPartialDeclaration(filePath, ast, traverse); - } - - return {contents: code}; + build.onDispose(() => { + void javascriptTransformer.close(); }); }, }; diff --git a/devtools/tools/esbuild/esbuild-base.config.mts b/devtools/tools/esbuild/esbuild-base.config.mts index aa6f8283805..8a7e9f258db 100644 --- a/devtools/tools/esbuild/esbuild-base.config.mts +++ b/devtools/tools/esbuild/esbuild-base.config.mts @@ -36,31 +36,11 @@ export default async function createConfig({ conditions: ['es2020', 'es2015', 'module'], define: optimize ? convertObjectToStringDictionary(GLOBAL_DEFS_FOR_TERSER_WITH_AOT) : undefined, plugins: [ - await createEsbuildAngularOptimizePlugin({ + createEsbuildAngularOptimizePlugin({ optimize: { isSideEffectFree: undefined, }, - enableLinker: enableLinker - ? { - ensureNoPartialDeclaration: false, - // Only run the linker on fesm2020 and fesm2022 bundles. This should not have an effect on - // the bundle output, but helps speeding up ESBuild when it visits other modules. - filterPaths: /fesm2020|fesm2022/, - linkerOptions: { - // DevTools relies on angular framework packages that are consumed, - // locally via bazel. These packages have a version of 0.0.0-PLACEHOLDER. - // DevTools also relies on Angular CDK and Material packages that are consumed via npm. - // Because of this, we set unknownDeclarationVersionHandling to ignore so that we bypass - // selecting a linker for our CDK and Material dependencies based on our local framework - // version (0.0.0-PLACEHOLDER). - // Instead this option defaults to the latest linker version, which should - // be correct, except for the small time interval where we rollout a new - // declaration version and target a material release that hasn't been compiled - // with that version yet. - unknownDeclarationVersionHandling: 'ignore', - }, - } - : undefined, + enableLinker, }), ], }; diff --git a/package.json b/package.json index 4426e5684d0..694402d03b0 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@angular/animations": "workspace:*", "@angular/aria": "22.1.0-rc.0", "@angular/benchpress": "workspace: *", - "@angular/build": "22.2.0-next.0", + "@angular/build": "22.2.0-next.1", "@angular/cdk": "22.1.0-rc.0", "@angular/cli": "22.2.0-next.0", "@angular/common": "workspace:*", diff --git a/packages/compiler/BUILD.bazel b/packages/compiler/BUILD.bazel index c457ea2dcdf..7a1333dec6b 100644 --- a/packages/compiler/BUILD.bazel +++ b/packages/compiler/BUILD.bazel @@ -34,7 +34,6 @@ ng_package( # Dependencies on the full npm_package cause long re-builds. visibility = [ "//adev:__pkg__", - "//devtools/tools/angular-optimization:__subpackages__", "//integration:__subpackages__", "//modules/ssr-benchmarks:__subpackages__", "//packages/compiler-cli/integrationtest:__pkg__", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 042b7e8008c..5e0095f0b4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,8 +28,8 @@ importers: specifier: 'workspace: *' version: link:packages/benchpress '@angular/build': - specifier: 22.2.0-next.0 - version: 22.2.0-next.0(a32c4cb6cdd76a7ab914156bb758f065) + specifier: 22.2.0-next.1 + version: 22.2.0-next.1(a32c4cb6cdd76a7ab914156bb758f065) '@angular/cdk': specifier: 22.1.0-rc.0 version: 22.1.0-rc.0(@angular/common@packages+common)(@angular/core@packages+core)(@angular/platform-browser@packages+platform-browser)(rxjs@7.8.2) @@ -1493,6 +1493,11 @@ packages: engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true + '@angular-devkit/architect@0.2202.0-next.1': + resolution: {integrity: sha512-P+oXENhfY/xyr2T/2cwa3jIZAwGQCWLS0//CuHKqjDq0FbCHau45Bc64oTfcQib5ROrc7gHMXqK/DYPIfmycvQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + '@angular-devkit/core@22.2.0-next.0': resolution: {integrity: sha512-VsmT/WaZWKw0cU/QbVJdV/mgoAtDvvjFD1jejXHwC4kK/sqBVmxNjon96N110/aXwOzDmu+MPlTYKyviA1LWQQ==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -1502,6 +1507,15 @@ packages: chokidar: optional: true + '@angular-devkit/core@22.2.0-next.1': + resolution: {integrity: sha512-SBWUvFYxp6xeXQhLFv8PYPSUjh/j94YrIbaEw7Sok6VEJkm4cQfDJAq1vMGwDXbrLmjgR9RaP+XPjfjUDid18A==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^5.0.0 + peerDependenciesMeta: + chokidar: + optional: true + '@angular-devkit/schematics@22.2.0-next.0': resolution: {integrity: sha512-6Ok5cZ/i/ox7xkGYCLpDRgMEVfR6N7DrZ/qUXM7jVPOQXXayqfrMCCl1nmXPMu/g+C6nk81TRWfSzDJactBxmA==} engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} @@ -1564,6 +1578,58 @@ packages: vitest: optional: true + '@angular/build@22.2.0-next.1': + resolution: {integrity: sha512-u5Fhud9XR5z0r57xMWlAJnabuSgPV+uswI9NEKRm29kmltPcoTB2e61p7WT9gwQ2kKg2wIKD6YOcxB8f+2VzpQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + '@angular/compiler': ^22.0.0 || ^22.2.0-next.0 + '@angular/compiler-cli': ^22.0.0 || ^22.2.0-next.0 + '@angular/core': ^22.0.0 || ^22.2.0-next.0 + '@angular/localize': ^22.0.0 || ^22.2.0-next.0 + '@angular/platform-browser': ^22.0.0 || ^22.2.0-next.0 + '@angular/platform-server': ^22.0.0 || ^22.2.0-next.0 + '@angular/service-worker': ^22.0.0 || ^22.2.0-next.0 + '@angular/ssr': ^22.2.0-next.1 + istanbul-lib-instrument: ^6.0.0 + karma: ^6.4.0 + less: ^4.2.0 + ng-packagr: ^22.0.0 || ^22.2.0-next.0 + postcss: ^8.4.0 + rollup: ^4.0.0 + tailwindcss: ^2.0.0 || ^3.0.0 || ^4.0.0 + tslib: ^2.3.0 + typescript: '>=6.0 <6.1' + vitest: ^4.0.8 + peerDependenciesMeta: + '@angular/core': + optional: true + '@angular/localize': + optional: true + '@angular/platform-browser': + optional: true + '@angular/platform-server': + optional: true + '@angular/service-worker': + optional: true + '@angular/ssr': + optional: true + istanbul-lib-instrument: + optional: true + karma: + optional: true + less: + optional: true + ng-packagr: + optional: true + postcss: + optional: true + rollup: + optional: true + tailwindcss: + optional: true + vitest: + optional: true + '@angular/cdk@22.1.0-rc.0': resolution: {integrity: sha512-E0IbkeugB0fjXFHT2fjb3Cicqz8wt8SEAY/8GqtmP/CC5U9sXuzWLnH8JjkR/96i05WgNT0PfBnycYZEkI59Pw==} peerDependencies: @@ -11603,6 +11669,13 @@ snapshots: transitivePeerDependencies: - chokidar + '@angular-devkit/architect@0.2202.0-next.1(chokidar@5.0.0)': + dependencies: + '@angular-devkit/core': 22.2.0-next.1(chokidar@5.0.0) + rxjs: 7.8.2 + transitivePeerDependencies: + - chokidar + '@angular-devkit/core@22.2.0-next.0(chokidar@5.0.0)': dependencies: ajv: 8.20.0 @@ -11614,6 +11687,17 @@ snapshots: optionalDependencies: chokidar: 5.0.0 + '@angular-devkit/core@22.2.0-next.1(chokidar@5.0.0)': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1 + jsonc-parser: 3.3.1 + picomatch: 4.0.5 + rxjs: 7.8.2 + source-map: 0.8.0 + optionalDependencies: + chokidar: 5.0.0 + '@angular-devkit/schematics@22.2.0-next.0(chokidar@5.0.0)': dependencies: '@angular-devkit/core': 22.2.0-next.0(chokidar@5.0.0) @@ -11754,6 +11838,66 @@ snapshots: - tsx - yaml + '@angular/build@22.2.0-next.1(a32c4cb6cdd76a7ab914156bb758f065)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2202.0-next.1(chokidar@5.0.0) + '@angular/compiler': link:packages/compiler + '@angular/compiler-cli': link:packages/compiler-cli + '@babel/core': 8.0.1 + '@inquirer/confirm': 6.1.1(@types/node@20.19.43) + '@vitejs/plugin-basic-ssl': 2.3.0(vite@8.2.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@1.21.7)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0)) + beasties: 0.4.3 + browserslist: 4.28.7 + esbuild: 0.28.1 + https-proxy-agent: 9.1.0(supports-color@11.0.0) + jsonc-parser: 3.3.1 + listr2: 11.0.0 + magic-string: 1.1.0 + mrmime: 2.0.1 + oxc-parser: 0.142.0 + parse5-html-rewriting-stream: 8.0.1 + picomatch: 4.0.5 + piscina: 5.3.0 + rolldown: 1.2.1 + sass: 1.102.0 + semver: 7.8.5 + source-map-support: 0.5.21 + tinyglobby: 0.2.17 + tslib: 2.8.1 + typescript: 6.0.3 + vite: 8.2.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@1.21.7)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0) + watchpack: 2.5.2 + optionalDependencies: + '@angular/core': link:packages/core + '@angular/localize': link:packages/localize + '@angular/platform-browser': link:packages/platform-browser + '@angular/platform-server': link:packages/platform-server + '@angular/service-worker': link:packages/service-worker + '@angular/ssr': 22.2.0-next.0(@angular/common@packages+common)(@angular/core@packages+core)(@angular/platform-server@packages+platform-server)(@angular/router@packages+router) + istanbul-lib-instrument: 6.0.3(supports-color@11.0.0) + karma: 6.4.4(bufferutil@4.1.0)(debug@4.4.3(supports-color@11.0.0))(supports-color@11.0.0)(utf-8-validate@6.0.6) + less: 4.8.1(supports-color@11.0.0) + lmdb: 3.5.6 + ng-packagr: 22.2.0-next.2(@angular/compiler-cli@packages+compiler-cli)(supports-color@11.0.0)(tailwindcss@3.4.19(tsx@4.23.1)(yaml@2.9.0))(tslib@2.8.1)(typescript@6.0.3) + postcss: 8.5.25 + rollup: 4.62.3 + tailwindcss: 3.4.19(tsx@4.23.1)(yaml@2.9.0) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@20.19.43)(esbuild@0.28.1)(jiti@1.21.7)(jsdom@30.0.1)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0) + transitivePeerDependencies: + - '@types/node' + - '@vitejs/devtools' + - chokidar + - jiti + - kerberos + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + '@angular/cdk@22.1.0-rc.0(@angular/common@packages+common)(@angular/core@packages+core)(@angular/platform-browser@packages+platform-browser)(rxjs@7.8.2)': dependencies: '@angular/common': link:packages/common @@ -15296,6 +15440,10 @@ snapshots: dependencies: vite: 8.1.5(@types/node@24.13.3)(esbuild@0.28.1)(jiti@1.21.7)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0) + '@vitejs/plugin-basic-ssl@2.3.0(vite@8.2.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@1.21.7)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0))': + dependencies: + vite: 8.2.0(@types/node@20.19.43)(esbuild@0.28.1)(jiti@1.21.7)(less@4.8.1(supports-color@11.0.0))(sass@1.102.0)(tsx@4.23.1)(yaml@2.9.0) + '@vitest/expect@4.1.10': dependencies: '@standard-schema/spec': 1.1.0