Skip to content

Commit 58f0583

Browse files
committed
perf(@angular/build): avoid duplicate Domino bundling in server bundles
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.
1 parent 5bb7e5c commit 58f0583

3 files changed

Lines changed: 180 additions & 1 deletion

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { buildApplication } from '../../index';
10+
import { OutputMode, Platform } from '../../schema';
11+
import { APPLICATION_BUILDER_INFO, BASE_OPTIONS, describeBuilder } from '../setup';
12+
13+
/**
14+
* `@angular/platform-server/init` (bundled into the server polyfills) and
15+
* `@angular/platform-server` (bundled into the main server bundle) both import this file.
16+
*/
17+
const DOMINO_BUNDLE = 'third_party/domino/bundled-domino.mjs';
18+
19+
/** The property on globalThis where the Domino instance is stored. */
20+
const DOMINO_GLOBAL_PROPERTY = /(?:ɵ|\\u0275)domino/;
21+
22+
describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
23+
describe('Behavior: "Domino is bundled once for the server"', () => {
24+
beforeEach(async () => {
25+
await harness.modifyFile('src/tsconfig.app.json', (content) => {
26+
const tsConfig = JSON.parse(content);
27+
tsConfig.files ??= [];
28+
tsConfig.files.push('main.server.ts', 'server.ts');
29+
30+
return JSON.stringify(tsConfig);
31+
});
32+
33+
await harness.writeFile('src/server.ts', `console.log('Hello!');`);
34+
35+
// The default template uses i18n which requires `@angular/localize` to prerender.
36+
await harness.writeFile('src/app/app.component.html', `<p>Hello!</p>`);
37+
});
38+
39+
it('shares the server polyfills copy of Domino with the main server bundle', async () => {
40+
harness.useTarget('build', {
41+
...BASE_OPTIONS,
42+
server: 'src/main.server.ts',
43+
ssr: true,
44+
});
45+
46+
const { result } = await harness.executeOnce();
47+
expect(result?.success).toBeTrue();
48+
49+
harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
50+
harness
51+
.expectFile('dist/server/polyfills.server.mjs')
52+
.content.toMatch(DOMINO_GLOBAL_PROPERTY);
53+
54+
harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
55+
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
56+
});
57+
58+
it('shares the server polyfills copy of Domino when "outputMode" is "server"', async () => {
59+
harness.useTarget('build', {
60+
...BASE_OPTIONS,
61+
server: 'src/main.server.ts',
62+
ssr: { entry: 'src/server.ts' },
63+
outputMode: OutputMode.Server,
64+
});
65+
66+
const { result } = await harness.executeOnce();
67+
expect(result?.success).toBeTrue();
68+
69+
harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
70+
harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
71+
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
72+
harness.expectFile('dist/server/server.mjs').content.not.toContain(DOMINO_BUNDLE);
73+
});
74+
75+
it('shares the server polyfills copy of Domino when "ssr.platform" is "neutral"', async () => {
76+
harness.useTarget('build', {
77+
...BASE_OPTIONS,
78+
server: 'src/main.server.ts',
79+
ssr: { entry: 'src/server.ts', platform: Platform.Neutral },
80+
outputMode: OutputMode.Server,
81+
});
82+
83+
const { result } = await harness.executeOnce();
84+
expect(result?.success).toBeTrue();
85+
86+
harness.expectFile('dist/server/polyfills.server.mjs').content.toContain(DOMINO_BUNDLE);
87+
harness
88+
.expectFile('dist/server/polyfills.server.mjs')
89+
.content.toMatch(DOMINO_GLOBAL_PROPERTY);
90+
harness.expectFile('dist/server/main.server.mjs').content.not.toContain(DOMINO_BUNDLE);
91+
harness.expectFile('dist/server/main.server.mjs').content.toMatch(DOMINO_GLOBAL_PROPERTY);
92+
});
93+
});
94+
});

‎packages/angular/build/src/tools/esbuild/application-code-bundle.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ import { createExternalPackagesPlugin } from './external-packages-plugin';
3030
import { createAngularLocaleDataPlugin } from './i18n-locale-plugin';
3131
import type { LoadResultCache } from './load-result-cache';
3232
import { createLoaderImportAttributePlugin } from './loader-import-attribute-plugin';
33+
import {
34+
createPlatformServerDominoExposePlugin,
35+
createPlatformServerDominoPlugin,
36+
} from './platform-server-domino-plugin';
3337
import { createRxjsEsmResolutionPlugin } from './rxjs-esm-resolution-plugin';
3438
import { createServerBundleMetadata } from './server-bundle-metadata-plugin';
3539
import { createSourcemapIgnorelistPlugin } from './sourcemap-ignorelist-plugin';
@@ -237,7 +241,7 @@ export function createServerPolyfillBundleOptions(
237241
};
238242

239243
buildOptions.plugins ??= [];
240-
buildOptions.plugins.push(createServerBundleMetadata());
244+
buildOptions.plugins.push(createServerBundleMetadata(), createPlatformServerDominoExposePlugin());
241245

242246
return () => buildOptions;
243247
}
@@ -321,6 +325,7 @@ export function createServerMainCodeBundleOptions(
321325

322326
buildOptions.plugins.push(
323327
createServerBundleMetadata(),
328+
createPlatformServerDominoPlugin(),
324329
createVirtualModulePlugin({
325330
namespace: mainServerInjectManifestNamespace,
326331
cache: loadResultCache,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { Plugin } from 'esbuild';
10+
import { dirname, resolve } from 'node:path';
11+
import { toPosixPath } from '../../utils/path';
12+
13+
/**
14+
* Filter matching the Domino bundle import inside `@angular/platform-server`.
15+
*/
16+
const BUNDLED_DOMINO_FILTER = /(?:^|[\\/])bundled-domino(?:\.[cm]?js)?$/;
17+
18+
/**
19+
* The property name on `globalThis` where the Domino instance is stored.
20+
*/
21+
const DOMINO_GLOBAL_PROPERTY = 'ɵdomino';
22+
23+
/**
24+
* Creates an esbuild plugin that attaches the Domino instance to `globalThis` when
25+
* `@angular/platform-server/init` is bundled into the server polyfills bundle.
26+
*
27+
* @returns An esbuild plugin.
28+
*/
29+
export function createPlatformServerDominoExposePlugin(): Plugin {
30+
return {
31+
name: 'angular-platform-server-domino-expose',
32+
setup(build) {
33+
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
34+
args.namespace !== 'angular:domino-expose' && args.kind === 'import-statement'
35+
? {
36+
path: resolve(args.resolveDir, args.path),
37+
namespace: 'angular:domino-expose',
38+
}
39+
: null,
40+
);
41+
42+
build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => ({
43+
contents: [
44+
`import domino from ${JSON.stringify(toPosixPath(args.path))};`,
45+
`globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`,
46+
`export default domino;`,
47+
].join('\n'),
48+
loader: 'js',
49+
resolveDir: dirname(args.path),
50+
}));
51+
},
52+
};
53+
}
54+
55+
/**
56+
* Creates an esbuild plugin that redirects imports of Domino in `@angular/platform-server`
57+
* to the single copy attached to `globalThis` by the server polyfills bundle.
58+
*
59+
* @returns An esbuild plugin.
60+
*/
61+
export function createPlatformServerDominoPlugin(): Plugin {
62+
return {
63+
name: 'angular-platform-server-domino-consume',
64+
setup(build) {
65+
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
66+
args.kind === 'import-statement'
67+
? {
68+
path: 'angular:platform-server-domino',
69+
namespace: 'angular:platform-server-domino',
70+
}
71+
: null,
72+
);
73+
74+
build.onLoad({ filter: /./, namespace: 'angular:platform-server-domino' }, () => ({
75+
contents: `export default globalThis['${DOMINO_GLOBAL_PROPERTY}'];`,
76+
loader: 'js',
77+
}));
78+
},
79+
};
80+
}

0 commit comments

Comments
 (0)