Skip to content

Commit 6ec1432

Browse files
clydinalan-agius4
authored andcommitted
refactor(@angular-devkit/build-angular): bundle polyfills independent of main code in application builder
Any provided or required polyfills (the later mainly related to i18n) will now be bundled in a separate concurrent bundling action. This has several benefits including allowing different bundling options for the polyfills and minimizing rebundling of polyfills (which rarely change) in watch/serve mode. Along with this change, the polyfill bundling options have been adjusted to not externalize packages when in development server mode. This allows the zone.js globals to be more easily used as well as minimizing the need for Vite to process a typically unchanging output file.
1 parent 9ad99c1 commit 6ec1432

File tree

2 files changed

+116
-62
lines changed

2 files changed

+116
-62
lines changed

packages/angular_devkit/build_angular/src/builders/application/execute-build.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { BuilderContext } from '@angular-devkit/architect';
1010
import { SourceFileCache } from '../../tools/esbuild/angular/source-file-cache';
1111
import {
1212
createBrowserCodeBundleOptions,
13+
createBrowserPolyfillBundleOptions,
1314
createServerCodeBundleOptions,
1415
} from '../../tools/esbuild/application-code-bundle';
1516
import { generateBudgetStats } from '../../tools/esbuild/budget-stats';
@@ -80,6 +81,18 @@ export async function executeBuild(
8081
),
8182
);
8283

84+
// Browser polyfills code
85+
const polyfillBundleOptions = createBrowserPolyfillBundleOptions(
86+
options,
87+
target,
88+
codeBundleCache,
89+
);
90+
if (polyfillBundleOptions) {
91+
bundlerContexts.push(
92+
new BundlerContext(workspaceRoot, !!options.watch, polyfillBundleOptions),
93+
);
94+
}
95+
8396
// Global Stylesheets
8497
if (options.globalStyles.length > 0) {
8598
for (const initial of [true, false]) {
@@ -110,7 +123,7 @@ export async function executeBuild(
110123
}
111124

112125
// Server application code
113-
// Skip server build when non of the features are enabled.
126+
// Skip server build when none of the features are enabled.
114127
if (serverEntryPoint && (prerenderOptions || appShellOptions || ssrOptions)) {
115128
const nodeTargets = getSupportedNodeTargets();
116129
bundlerContexts.push(

packages/angular_devkit/build_angular/src/tools/esbuild/application-code-bundle.ts

Lines changed: 102 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export function createBrowserCodeBundleOptions(
2828
target: string[],
2929
sourceFileCache?: SourceFileCache,
3030
): BuildOptions {
31-
const { workspaceRoot, entryPoints, outputNames, jit } = options;
31+
const { entryPoints, outputNames } = options;
3232

3333
const { pluginOptions, styleOptions } = createCompilerPluginOptions(
3434
options,
@@ -63,6 +63,50 @@ export function createBrowserCodeBundleOptions(
6363
buildOptions.packages = 'external';
6464
}
6565

66+
if (options.plugins) {
67+
buildOptions.plugins?.push(...options.plugins);
68+
}
69+
70+
return buildOptions;
71+
}
72+
73+
export function createBrowserPolyfillBundleOptions(
74+
options: NormalizedApplicationBuildOptions,
75+
target: string[],
76+
sourceFileCache?: SourceFileCache,
77+
): BuildOptions | undefined {
78+
const { workspaceRoot, outputNames, jit } = options;
79+
80+
const { pluginOptions, styleOptions } = createCompilerPluginOptions(
81+
options,
82+
target,
83+
sourceFileCache,
84+
);
85+
86+
const buildOptions: BuildOptions = {
87+
...getEsBuildCommonOptions(options),
88+
platform: 'browser',
89+
// Note: `es2015` is needed for RxJS v6. If not specified, `module` would
90+
// match and the ES5 distribution would be bundled and ends up breaking at
91+
// runtime with the RxJS testing library.
92+
// More details: https://github.com/angular/angular-cli/issues/25405.
93+
mainFields: ['es2020', 'es2015', 'browser', 'module', 'main'],
94+
entryNames: outputNames.bundles,
95+
target,
96+
splitting: false,
97+
supported: getFeatureSupport(target),
98+
plugins: [
99+
createSourcemapIgnorelistPlugin(),
100+
createCompilerPlugin(
101+
// JS/TS options
102+
{ ...pluginOptions, noopTypeScriptCompilation: true },
103+
// Component stylesheet options are unused for polyfills but required by the plugin
104+
styleOptions,
105+
),
106+
],
107+
};
108+
buildOptions.plugins ??= [];
109+
66110
const polyfills = options.polyfills ? [...options.polyfills] : [];
67111

68112
// Angular JIT mode requires the runtime compiler
@@ -103,71 +147,68 @@ export function createBrowserCodeBundleOptions(
103147
buildOptions.plugins?.push(createAngularLocaleDataPlugin());
104148
}
105149

106-
// Add polyfill entry point if polyfills are present
107-
if (polyfills.length) {
108-
const namespace = 'angular:polyfills';
109-
buildOptions.entryPoints = {
110-
...buildOptions.entryPoints,
111-
'polyfills': namespace,
112-
};
150+
if (polyfills.length === 0) {
151+
return;
152+
}
113153

114-
buildOptions.plugins?.unshift(
115-
createVirtualModulePlugin({
116-
namespace,
117-
loadContent: async (_, build) => {
118-
let hasLocalizePolyfill = false;
119-
const polyfillPaths = await Promise.all(
120-
polyfills.map(async (path) => {
121-
hasLocalizePolyfill ||= path.startsWith('@angular/localize');
122-
123-
if (path.startsWith('zone.js') || !extname(path)) {
124-
return path;
125-
}
126-
127-
const potentialPathRelative = './' + path;
128-
const result = await build.resolve(potentialPathRelative, {
129-
kind: 'import-statement',
130-
resolveDir: workspaceRoot,
131-
});
132-
133-
return result.path ? potentialPathRelative : path;
134-
}),
135-
);
154+
// Add polyfill entry point if polyfills are present
155+
const namespace = 'angular:polyfills';
156+
buildOptions.entryPoints = {
157+
'polyfills': namespace,
158+
};
136159

137-
if (!options.i18nOptions.shouldInline && !hasLocalizePolyfill) {
138-
// Cannot use `build.resolve` here since it does not allow overriding the external options
139-
// and the actual presence of the `@angular/localize` package needs to be checked here.
140-
const workspaceRequire = createRequire(workspaceRoot + '/');
141-
try {
142-
workspaceRequire.resolve('@angular/localize');
143-
// The resolve call above will throw if not found
144-
polyfillPaths.push('@angular/localize/init');
145-
} catch {}
146-
}
147-
148-
// Generate module contents with an import statement per defined polyfill
149-
let contents = polyfillPaths
150-
.map((file) => `import '${file.replace(/\\/g, '/')}';`)
151-
.join('\n');
160+
buildOptions.plugins?.unshift(
161+
createVirtualModulePlugin({
162+
namespace,
163+
loadContent: async (_, build) => {
164+
let hasLocalizePolyfill = false;
165+
const polyfillPaths = await Promise.all(
166+
polyfills.map(async (path) => {
167+
hasLocalizePolyfill ||= path.startsWith('@angular/localize');
168+
169+
if (path.startsWith('zone.js') || !extname(path)) {
170+
return path;
171+
}
172+
173+
const potentialPathRelative = './' + path;
174+
const result = await build.resolve(potentialPathRelative, {
175+
kind: 'import-statement',
176+
resolveDir: workspaceRoot,
177+
});
178+
179+
return result.path ? potentialPathRelative : path;
180+
}),
181+
);
182+
183+
if (!options.i18nOptions.shouldInline && !hasLocalizePolyfill) {
184+
// Cannot use `build.resolve` here since it does not allow overriding the external options
185+
// and the actual presence of the `@angular/localize` package needs to be checked here.
186+
const workspaceRequire = createRequire(workspaceRoot + '/');
187+
try {
188+
workspaceRequire.resolve('@angular/localize');
189+
// The resolve call above will throw if not found
190+
polyfillPaths.push('@angular/localize/init');
191+
} catch {}
192+
}
152193

153-
// If not inlining translations and source locale is defined, inject the locale specifier
154-
if (!options.i18nOptions.shouldInline && options.i18nOptions.hasDefinedSourceLocale) {
155-
contents += `(globalThis.$localize ??= {}).locale = "${options.i18nOptions.sourceLocale}";\n`;
156-
}
194+
// Generate module contents with an import statement per defined polyfill
195+
let contents = polyfillPaths
196+
.map((file) => `import '${file.replace(/\\/g, '/')}';`)
197+
.join('\n');
157198

158-
return {
159-
contents,
160-
loader: 'js',
161-
resolveDir: workspaceRoot,
162-
};
163-
},
164-
}),
165-
);
166-
}
199+
// If not inlining translations and source locale is defined, inject the locale specifier
200+
if (!options.i18nOptions.shouldInline && options.i18nOptions.hasDefinedSourceLocale) {
201+
contents += `(globalThis.$localize ??= {}).locale = "${options.i18nOptions.sourceLocale}";\n`;
202+
}
167203

168-
if (options.plugins) {
169-
buildOptions.plugins?.push(...options.plugins);
170-
}
204+
return {
205+
contents,
206+
loader: 'js',
207+
resolveDir: workspaceRoot,
208+
};
209+
},
210+
}),
211+
);
171212

172213
return buildOptions;
173214
}

0 commit comments

Comments
 (0)