refactor(devtools): use JavaScriptTransformer for Angular optimization esbuild plugin

Refactor the devtools optimization esbuild plugin to use JavaScriptTransformer from @angular/build/private.
This commit is contained in:
Alan Agius
2026-08-05 09:31:47 +00:00
committed by Alon Mishne
parent 573145afb5
commit cf1844bcce
8 changed files with 182 additions and 154 deletions
@@ -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",
],
)
@@ -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,
);
}
},
});
}
+2 -8
View File
@@ -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<any>
export function createEsbuildAngularOptimizePlugin(opts: OptimizationOptions): Promise<any>
@@ -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();
});
},
};
+2 -22
View File
@@ -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,
}),
],
};
+1 -1
View File
@@ -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:*",
-1
View File
@@ -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__",
+150 -2
View File
@@ -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