Skip to content

Commit e3c5b91

Browse files
clydinalan-agius4
authored andcommitted
fix(@angular-devkit/build-angular): automatically include known packages in vite prebundling
When using the Vite-based development server, the application build step already contains the list of known packages that would need to be prebundled. This information can be passed to Vite directly to avoid Vite needing to perform discovery on every output file that will be requested. This also avoids the Vite server behavior where Vite forces a reload of the page when it discovers a new dependency. This behavior can result in lost state during lazy loading of a route. (cherry picked from commit 9768c18)
1 parent 87425a7 commit e3c5b91

File tree

4 files changed

+60
-5
lines changed

4 files changed

+60
-5
lines changed

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ export async function executeBuild(
168168
return executionResult;
169169
}
170170

171+
// Analyze external imports if external options are enabled
172+
if (options.externalPackages || options.externalDependencies?.length) {
173+
// TODO: Filter externalImports to generate second argument to support wildcard externalDependency values
174+
executionResult.setExternalMetadata(
175+
[...bundlingResult.externalImports],
176+
options.externalDependencies,
177+
);
178+
}
179+
171180
const { metafile, initialFiles, outputFiles } = bundlingResult;
172181

173182
executionResult.outputFiles.push(...outputFiles);

packages/angular_devkit/build_angular/src/builders/dev-server/vite-server.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ export async function* serveWithVite(
115115
let hadError = false;
116116
const generatedFiles = new Map<string, OutputFileRecord>();
117117
const assetFiles = new Map<string, string>();
118+
const externalMetadata: { implicit: string[]; explicit: string[] } = {
119+
implicit: [],
120+
explicit: [],
121+
};
118122
const build =
119123
builderName === '@angular-devkit/build-angular:application'
120124
? buildApplicationInternal
@@ -166,6 +170,15 @@ export async function* serveWithVite(
166170
}
167171
}
168172

173+
// To avoid disconnecting the array objects from the option, these arrays need to be mutated
174+
// instead of replaced.
175+
if (result.externalMetadata.explicit) {
176+
externalMetadata.explicit.push(...result.externalMetadata.explicit);
177+
}
178+
if (result.externalMetadata.implicit) {
179+
externalMetadata.implicit.push(...result.externalMetadata.implicit);
180+
}
181+
169182
if (server) {
170183
handleUpdate(generatedFiles, server, serverOptions, context.logger);
171184
} else {
@@ -185,7 +198,7 @@ export async function* serveWithVite(
185198
generatedFiles,
186199
assetFiles,
187200
browserOptions.preserveSymlinks,
188-
browserOptions.externalDependencies,
201+
externalMetadata,
189202
!!browserOptions.ssr,
190203
prebundleTransformer,
191204
target,
@@ -336,7 +349,7 @@ export async function setupServer(
336349
outputFiles: Map<string, OutputFileRecord>,
337350
assets: Map<string, string>,
338351
preserveSymlinks: boolean | undefined,
339-
prebundleExclude: string[] | undefined,
352+
externalMetadata: { implicit: string[]; explicit: string[] },
340353
ssr: boolean,
341354
prebundleTransformer: JavaScriptTransformer,
342355
target: string[],
@@ -381,7 +394,7 @@ export async function setupServer(
381394
},
382395
ssr: {
383396
// Exclude any provided dependencies (currently build defined externals)
384-
external: prebundleExclude,
397+
external: externalMetadata.implicit,
385398
},
386399
plugins: [
387400
createAngularLocaleDataPlugin(),
@@ -598,8 +611,10 @@ export async function setupServer(
598611
optimizeDeps: {
599612
// Only enable with caching since it causes prebundle dependencies to be cached
600613
disabled: !serverOptions.cacheOptions.enabled,
601-
// Exclude any provided dependencies (currently build defined externals)
602-
exclude: prebundleExclude,
614+
// Exclude any explicitly defined dependencies (currently build defined externals)
615+
exclude: externalMetadata.explicit,
616+
// Include all implict dependencies from the external packages internal option
617+
include: externalMetadata.implicit,
603618
// Skip automatic file-based entry point discovery
604619
entries: [],
605620
// Add an esbuild plugin to run the Angular linker on dependencies

packages/angular_devkit/build_angular/src/tools/esbuild/bundler-context.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type BundleContextResult =
2929
metafile: Metafile;
3030
outputFiles: BuildOutputFile[];
3131
initialFiles: Map<string, InitialFileRecord>;
32+
externalImports: Set<string>;
3233
};
3334

3435
export interface InitialFileRecord {
@@ -105,6 +106,7 @@ export class BundlerContext {
105106
const warnings: Message[] = [];
106107
const metafile: Metafile = { inputs: {}, outputs: {} };
107108
const initialFiles = new Map<string, InitialFileRecord>();
109+
const externalImports = new Set<string>();
108110
const outputFiles = [];
109111
for (const result of individualResults) {
110112
warnings.push(...result.warnings);
@@ -122,6 +124,7 @@ export class BundlerContext {
122124

123125
result.initialFiles.forEach((value, key) => initialFiles.set(key, value));
124126
outputFiles.push(...result.outputFiles);
127+
result.externalImports.forEach((value) => externalImports.add(value));
125128
}
126129

127130
if (errors !== undefined) {
@@ -134,6 +137,7 @@ export class BundlerContext {
134137
metafile,
135138
initialFiles,
136139
outputFiles,
140+
externalImports,
137141
};
138142
}
139143

@@ -284,6 +288,20 @@ export class BundlerContext {
284288
}
285289
}
286290

291+
// Collect all external package names
292+
const externalImports = new Set<string>();
293+
for (const { imports } of Object.values(result.metafile.outputs)) {
294+
for (const importData of imports) {
295+
if (
296+
!importData.external ||
297+
(importData.kind !== 'import-statement' && importData.kind !== 'dynamic-import')
298+
) {
299+
continue;
300+
}
301+
externalImports.add(importData.path);
302+
}
303+
}
304+
287305
const outputFiles = result.outputFiles.map((file) => {
288306
let fileType: BuildOutputFileType;
289307
if (dirname(file.path) === 'media') {
@@ -303,6 +321,7 @@ export class BundlerContext {
303321
...result,
304322
outputFiles,
305323
initialFiles,
324+
externalImports,
306325
errors: undefined,
307326
};
308327
}

packages/angular_devkit/build_angular/src/tools/esbuild/bundler-execution-result.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class ExecutionResult {
3030
outputFiles: BuildOutputFile[] = [];
3131
assetFiles: BuildOutputAsset[] = [];
3232
errors: Message[] = [];
33+
externalMetadata?: { implicit: string[]; explicit?: string[] };
3334

3435
constructor(
3536
private rebuildContexts: BundlerContext[],
@@ -48,6 +49,16 @@ export class ExecutionResult {
4849
this.errors.push(...errors);
4950
}
5051

52+
/**
53+
* Add external JavaScript import metadata to the result. This is currently used
54+
* by the development server to optimize the prebundling process.
55+
* @param implicit External dependencies due to the external packages option.
56+
* @param explicit External dependencies due to explicit project configuration.
57+
*/
58+
setExternalMetadata(implicit: string[], explicit: string[] | undefined) {
59+
this.externalMetadata = { implicit, explicit };
60+
}
61+
5162
get output() {
5263
return {
5364
success: this.errors.length === 0,
@@ -60,6 +71,7 @@ export class ExecutionResult {
6071
outputFiles: this.outputFiles,
6172
assetFiles: this.assetFiles,
6273
errors: this.errors,
74+
externalMetadata: this.externalMetadata,
6375
};
6476
}
6577

0 commit comments

Comments
 (0)