Skip to content

Commit 9e99820

Browse files
committed
fix(@angular/build): disable code splitting for unit test builds
Every spec file is its own entry point, so esbuild code splitting hoists any module reached from more than one spec into a chunk shared between them. A module placed in a shared chunk is wrapped in a lazy `__esm` initializer, so its exported value is only assigned once that initializer runs, and importing chunks read the export as a live ESM binding. The unit test runners load the generated output through a module runner rather than the browser's own ESM implementation, and that does not reliably preserve those bindings. An importing chunk can therefore observe the export as `undefined`. A component whose class field initializer reads a `const` exported from a module that was hoisted into a shared chunk fails with a `TypeError`, while the same value read later, or read from within the shared chunk itself, is correct. It only appears once a project has more than one spec file, because a single entry point inlines everything and never splits. Test bundles are never downloaded by a browser, so splitting has nothing to optimize here. This disables it for the unit test build only, via an internal option, leaving application builds unaffected.
1 parent 3039776 commit 9e99820

3 files changed

Lines changed: 28 additions & 0 deletions

File tree

packages/angular/build/src/builders/application/options.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ interface InternalOptions {
126126
* Suppress build summary and stats table.
127127
*/
128128
quiet?: boolean;
129+
130+
/**
131+
* Disables esbuild code splitting for the browser code bundle.
132+
*
133+
* Splitting emits shared chunks whose exports are read across chunk boundaries as live ESM
134+
* bindings. A module hoisted into a shared chunk is wrapped in a lazy initializer, so its exported
135+
* value is only assigned once that initializer runs. Runners that load the generated output
136+
* through a module runner rather than the browser's own ESM implementation do not reliably
137+
* preserve those bindings, and an importing chunk can observe the export as `undefined`.
138+
*
139+
* Test bundles are never downloaded by a browser, so there is nothing for splitting to optimize
140+
* there. Used exclusively for tests and shouldn't be used for other kinds of builds.
141+
*/
142+
disableCodeSplitting?: boolean;
129143
}
130144

131145
/** Full set of options for `application` builder. */
@@ -439,6 +453,7 @@ export async function normalizeOptions(
439453
partialSSRBuild = false,
440454
externalRuntimeStyles,
441455
instrumentForCoverage,
456+
disableCodeSplitting,
442457
} = options;
443458

444459
// Return all the normalized options
@@ -475,6 +490,7 @@ export async function normalizeOptions(
475490
watch,
476491
workspaceRoot,
477492
entryPoints,
493+
disableCodeSplitting,
478494
optimizationOptions,
479495
outputOptions,
480496
outExtension,

packages/angular/build/src/builders/unit-test/runners/vitest/build-options.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,12 @@ export async function getVitestBuildOptions(
257257
outputHashing: adjustOutputHashing(baseBuildOptions.outputHashing),
258258
optimization: false,
259259
entryPoints,
260+
// Every spec file is its own entry point, so splitting hoists any module shared between two
261+
// specs into a chunk whose exports are then read across a chunk boundary. Those reads rely on
262+
// live ESM bindings, and a module placed in a shared chunk is only assigned its exported value
263+
// when that chunk's lazy initializer runs, so an importing chunk can read `undefined`. Nothing
264+
// downloads these bundles, so there is no benefit to weigh against that.
265+
disableCodeSplitting: true,
260266
// Enable support for vitest browser prebundling. Excludes can be controlled with a runnerConfig
261267
// and the `optimizeDeps.exclude` option.
262268
externalPackages: true,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export function createBrowserCodeBundleOptions(
7171
supported: getFeatureSupport(zoneless),
7272
};
7373

74+
if (options.disableCodeSplitting) {
75+
// Splitting emits shared chunks that are read across chunk boundaries as live ESM bindings,
76+
// which the unit-test runners' module loading does not reliably preserve.
77+
buildOptions.splitting = false;
78+
}
79+
7480
buildOptions.plugins ??= [];
7581
buildOptions.plugins.push(
7682
createWasmPlugin({ allowAsync: zoneless, cache: loadCache }),

0 commit comments

Comments
 (0)