Skip to content

Commit cda2465

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. (cherry picked from commit f15c8c6)
1 parent af80160 commit cda2465

3 files changed

Lines changed: 190 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+
createPlatformServerDominoRedirectPlugin,
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+
createPlatformServerDominoRedirectPlugin(),
324329
createVirtualModulePlugin({
325330
namespace: mainServerInjectManifestNamespace,
326331
cache: loadResultCache,
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 { toPosixPath } from '../../utils/path';
11+
12+
/**
13+
* Filter matching the Domino bundle import inside `@angular/platform-server`.
14+
*/
15+
const BUNDLED_DOMINO_FILTER = /[\\/]third_party[\\/]domino[\\/]bundled-domino\.mjs$/;
16+
17+
/**
18+
* The property name on `globalThis` where the Domino instance is stored.
19+
*/
20+
const DOMINO_GLOBAL_PROPERTY = 'ɵdomino';
21+
22+
/**
23+
* Creates an esbuild plugin that attaches the Domino instance to `globalThis` when
24+
* `@angular/platform-server/init` is bundled into the server polyfills bundle.
25+
*
26+
* @returns An esbuild plugin.
27+
*/
28+
export function createPlatformServerDominoExposePlugin(): Plugin {
29+
return {
30+
name: 'angular-platform-server-domino-expose',
31+
setup(build) {
32+
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
33+
args.namespace !== 'angular:domino-expose' && args.kind === 'import-statement'
34+
? {
35+
path: 'angular:platform-server-domino',
36+
namespace: 'angular:domino-expose',
37+
pluginData: {
38+
importPath: args.path,
39+
resolveDir: args.resolveDir,
40+
},
41+
}
42+
: null,
43+
);
44+
45+
build.onLoad({ filter: /./, namespace: 'angular:domino-expose' }, (args) => {
46+
const { importPath, resolveDir } = args.pluginData as {
47+
importPath: string;
48+
resolveDir: string;
49+
};
50+
51+
return {
52+
contents: [
53+
`import domino from ${JSON.stringify(toPosixPath(importPath))};`,
54+
`globalThis['${DOMINO_GLOBAL_PROPERTY}'] = domino;`,
55+
`export default domino;`,
56+
].join('\n'),
57+
loader: 'js',
58+
resolveDir,
59+
};
60+
});
61+
},
62+
};
63+
}
64+
65+
/**
66+
* Creates an esbuild plugin that redirects imports of Domino in `@angular/platform-server`
67+
* to the single copy attached to `globalThis` by the server polyfills bundle.
68+
*
69+
* @returns An esbuild plugin.
70+
*/
71+
export function createPlatformServerDominoRedirectPlugin(): Plugin {
72+
return {
73+
name: 'angular-platform-server-domino-redirect',
74+
setup(build) {
75+
build.onResolve({ filter: BUNDLED_DOMINO_FILTER }, (args) =>
76+
args.kind === 'import-statement'
77+
? {
78+
path: 'angular:platform-server-domino',
79+
namespace: 'angular:platform-server-domino',
80+
}
81+
: null,
82+
);
83+
84+
build.onLoad({ filter: /./, namespace: 'angular:platform-server-domino' }, () => ({
85+
contents: `export default globalThis['${DOMINO_GLOBAL_PROPERTY}'];`,
86+
loader: 'js',
87+
}));
88+
},
89+
};
90+
}

0 commit comments

Comments
 (0)