From 58f0583764e93126b90b1f19cb2960c8b44cf41e Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 26 Oct 2026 09:10:30 +0000 Subject: [PATCH 1/3] perf(@angular/build): avoid duplicate Domino bundling in server bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Avoid bundling duplicate copies of Domino in both the server polyfills bundle and the main server bundle. The server polyfills bundle exposes the Domino instance attached to globalThis['ɵdomino'] when @angular/platform-server/init is bundled, and the main server bundle redirects Domino imports to this shared instance. --- .../behavior/platform-server-domino_spec.ts | 94 +++++++++++++++++++ .../tools/esbuild/application-code-bundle.ts | 7 +- .../esbuild/platform-server-domino-plugin.ts | 80 ++++++++++++++++ 3 files changed, 180 insertions(+), 1 deletion(-) create mode 100644 packages/angular/build/src/builders/application/tests/behavior/platform-server-domino_spec.ts create mode 100644 packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts diff --git a/packages/angular/build/src/builders/application/tests/behavior/platform-server-domino_spec.ts b/packages/angular/build/src/builders/application/tests/behavior/platform-server-domino_spec.ts new file mode 100644 index 000000000000..df58f1c833c0 --- /dev/null +++ b/packages/angular/build/src/builders/application/tests/behavior/platform-server-domino_spec.ts @@ -0,0 +1,94 @@ +/** + * @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.dev/license + */ + +import { buildApplication } from '../../index'; +import { OutputMode, Platform } from '../../schema'; +import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup'; + +/** + * `@angular/platform-server/init` (bundled into the server polyfills) and + * `@angular/platform-server` (bundled into the main server bundle) both import this file. + */ +const DOMINO_BUNDLE = 'third_party/domino/bundled-domino.mjs'; + +/** The property on globalThis where the Domino instance is stored. */ +const DOMINO_GLOBAL_PROPERTY = /(?:ɵ|\\u0275)domino/; + +describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { + describe('Behavior: "Domino is bundled once for the server"', () => { + beforeEach(async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts', 'server.ts'); + + return JSON.stringify(tsConfig); + }); + + await harness.writeFile('src/server.ts', `console.log('Hello!');`); + + // The default template uses i18n which requires `@angular/localize` to prerender. + await harness.writeFile('src/app/app.component.html', `
Hello!
`); + }); + + it('shares the server polyfills copy of Domino with the main server bundle', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE); + harness + .expectFile('dist/server/polyfills.server.mjs') + .content.toMatch(DOMINO_GLOBAL_PROPERTY); + + harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE); + harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY); + }); + + it('shares the server polyfills copy of Domino when "outputMode" is "server"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: { entry: 'src/server.ts' }, + outputMode: OutputMode.Server, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE); + harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE); + harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY); + harness.expectFile('dist/server/server.mjs').content.not.toContain(DOMINO_BUNDLE); + }); + + it('shares the server polyfills copy of Domino when "ssr.platform" is "neutral"', async () => { + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: { entry: 'src/server.ts', platform: Platform.Neutral }, + outputMode: OutputMode.Server, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE); + harness + .expectFile('dist/server/polyfills.server.mjs') + .content.toMatch(DOMINO_GLOBAL_PROPERTY); + harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE); + harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY); + }); + }); +}); diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index c98ad3bcae28..a51e0fb1c333 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -30,6 +30,10 @@ import { createExternalPackagesPlugin } from './external-packages-plugin'; import { createAngularLocaleDataPlugin } from './i18n-locale-plugin'; import type { LoadResultCache } from './load-result-cache'; import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin'; +import { + createPlatformServerDominoExposePlugin, + createPlatformServerDominoPlugin, +} from './platform-server-domino-plugin'; import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createServerBundleMetadata } from './server-bundle-metadata-plugin'; import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin'; @@ -237,7 +241,7 @@ export function createServerPolyfillBundleOptions( }; buildOptions.plugins ??= []; - buildOptions.plugins.push(createServerBundleMetadata()); + buildOptions.plugins.push(createServerBundleMetadata(), createPlatformServerDominoExposePlugin()); return () => buildOptions; } @@ -321,6 +325,7 @@ export function createServerMainCodeBundleOptions( buildOptions.plugins.push( createServerBundleMetadata(), + createPlatformServerDominoPlugin(), createVirtualModulePlugin({ namespace: mainServerInjectManifestNamespace, cache: loadResultCache, diff --git a/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts new file mode 100644 index 000000000000..31c9b3ccba11 --- /dev/null +++ b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts @@ -0,0 +1,80 @@ +/** + * @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.dev/license + */ + +import type { Plugin } from 'esbuild'; +import { dirname, resolve } from 'node:path'; +import { toPosixPath } from '../../utils/path'; + +/** + * Filter matching the Domino bundle import inside `@angular/platform-server`. + */ +const BUNDLED_DOMINO_FILTER = /(?:^|[\\/])bundled-domino(?:\.[cm]?js)?$/; + +/** + * The property name on `globalThis` where the Domino instance is stored. + */ +const DOMINO_GLOBAL_PROPERTY = 'ɵdomino'; + +/** + * Creates an esbuild plugin that attaches the Domino instance to `globalThis` when + * `@angular/platform-server/init` is bundled into the server polyfills bundle. + * + * @returns An esbuild plugin. + */ +export function createPlatformServerDominoExposePlugin(): Plugin { + return { + name: 'angular-platform-server-domino-expose', + setup(build) { + build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) => + args.namespace !== 'angular:domino-expose' && args.kind === 'import-statement' + ? { + path: resolve(args.resolveDir, args.path), + namespace: 'angular:domino-expose', + } + : null, + ); + + build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => ({ + contents: [ + `import domino from ${JSON.stringify(toPosixPath(args.path))};`, + `globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`, + `export default domino;`, + ].join('\n'), + loader: 'js', + resolveDir: dirname(args.path), + })); + }, + }; +} + +/** + * Creates an esbuild plugin that redirects imports of Domino in `@angular/platform-server` + * to the single copy attached to `globalThis` by the server polyfills bundle. + * + * @returns An esbuild plugin. + */ +export function createPlatformServerDominoPlugin(): Plugin { + return { + name: 'angular-platform-server-domino-consume', + setup(build) { + build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) => + args.kind === 'import-statement' + ? { + path: 'angular:platform-server-domino', + namespace: 'angular:platform-server-domino', + } + : null, + ); + + build.onLoad({ filter: /./, namespace: 'angular:platform-server-domino' }, () => ({ + contents: `export default globalThis['${DOMINO_GLOBAL_PROPERTY}'];`, + loader: 'js', + })); + }, + }; +} From 5c31f57ce08759aaa80f1e4cf0c9f85b00457b00 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:00:09 +0000 Subject: [PATCH 2/3] fixup! perf(@angular/build): avoid duplicate Domino bundling in server bundles --- .../tools/esbuild/application-code-bundle.ts | 4 +-- .../esbuild/platform-server-domino-plugin.ts | 36 ++++++++++++------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts index a51e0fb1c333..6cd2b09f5a67 100644 --- a/packages/angular/build/src/tools/esbuild/application-code-bundle.ts +++ b/packages/angular/build/src/tools/esbuild/application-code-bundle.ts @@ -32,7 +32,7 @@ import type { LoadResultCache } from './load-result-cache'; import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin'; import { createPlatformServerDominoExposePlugin, - createPlatformServerDominoPlugin, + createPlatformServerDominoRedirectPlugin, } from './platform-server-domino-plugin'; import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin'; import { createServerBundleMetadata } from './server-bundle-metadata-plugin'; @@ -325,7 +325,7 @@ export function createServerMainCodeBundleOptions( buildOptions.plugins.push( createServerBundleMetadata(), - createPlatformServerDominoPlugin(), + createPlatformServerDominoRedirectPlugin(), createVirtualModulePlugin({ namespace: mainServerInjectManifestNamespace, cache: loadResultCache, diff --git a/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts index 31c9b3ccba11..2019cd5b1c65 100644 --- a/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts @@ -7,7 +7,6 @@ */ import type { Plugin } from 'esbuild'; -import { dirname, resolve } from 'node:path'; import { toPosixPath } from '../../utils/path'; /** @@ -33,21 +32,32 @@ export function createPlatformServerDominoExposePlugin(): Plugin { build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) => args.namespace !== 'angular:domino-expose' && args.kind === 'import-statement' ? { - path: resolve(args.resolveDir, args.path), + path: 'angular:platform-server-domino', namespace: 'angular:domino-expose', + pluginData: { + importPath: args.path, + resolveDir: args.resolveDir, + }, } : null, ); - build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => ({ - contents: [ - `import domino from ${JSON.stringify(toPosixPath(args.path))};`, - `globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`, - `export default domino;`, - ].join('\n'), - loader: 'js', - resolveDir: dirname(args.path), - })); + build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => { + const { importPath, resolveDir } = args.pluginData as { + importPath: string; + resolveDir: string; + }; + + return { + contents: [ + `import domino from ${JSON.stringify(toPosixPath(importPath))};`, + `globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`, + `export default domino;`, + ].join('\n'), + loader: 'js', + resolveDir, + }; + }); }, }; } @@ -58,9 +68,9 @@ export function createPlatformServerDominoExposePlugin(): Plugin { * * @returns An esbuild plugin. */ -export function createPlatformServerDominoPlugin(): Plugin { +export function createPlatformServerDominoRedirectPlugin(): Plugin { return { - name: 'angular-platform-server-domino-consume', + name: 'angular-platform-server-domino-redirect', setup(build) { build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) => args.kind === 'import-statement' From d9ad768b6fa3bdfd371aea9f5a72afa857f25543 Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Mon, 26 Oct 2026 18:04:06 +0000 Subject: [PATCH 3/3] fixup! perf(@angular/build): avoid duplicate Domino bundling in server bundles --- .../build/src/tools/esbuild/platform-server-domino-plugin.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts index 2019cd5b1c65..198d44f0c6c4 100644 --- a/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts +++ b/packages/angular/build/src/tools/esbuild/platform-server-domino-plugin.ts @@ -12,7 +12,7 @@ import { toPosixPath } from '../../utils/path'; /** * Filter matching the Domino bundle import inside `@angular/platform-server`. */ -const BUNDLED_DOMINO_FILTER = /(?:^|[\\/])bundled-domino(?:\.[cm]?js)?$/; +const BUNDLED_DOMINO_FILTER = /[\\/]third_party[\\/]domino[\\/]bundled-domino\.mjs$/; /** * The property name on `globalThis` where the Domino instance is stored.