Skip to content

Commit b59f663

Browse files
committed
feat(@angular-devkit/build-angular): allow control of Vite-based development server prebundling
Previously, the Vite-based development server that is automatically used with the `application` and `browser-esbuild` builders would always use prebundling if the Angular CLI caching was enabled. The development server now has a specific `prebundle` option to allow more control over prebundling while still allowing other forms of caching within the Angular CLI. The `prebundle` option can be a boolean value of `true` or `false` that will enable or disable prebundling, respectively. Additionally, the option also has an object long-form. This long-form enables prebundling and currently contains one property named `exclude`. The `exclude` property supports cases where a package should not be prebundled and rather should be bundled directly into the application code. These cases are not common but can happen based on project specific requirements. If the `prebundle` option is enabled when using the `browser` builder or any other Webpack-based builder, it will be ignored as the Webpack-based development server does not contain such functionality.
1 parent 5e6f1a9 commit b59f663

File tree

7 files changed

+153
-11
lines changed

7 files changed

+153
-11
lines changed

goldens/public-api/angular_devkit/build_angular/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ export interface DevServerBuilderOptions {
182182
open?: boolean;
183183
poll?: number;
184184
port?: number;
185+
prebundle?: PrebundleUnion;
185186
proxyConfig?: string;
186187
publicHost?: string;
187188
servePath?: string;

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

+22-5
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,27 @@ export function execute(
6969
);
7070
}
7171

72+
// Warn if the initial options provided by the user enable prebundling but caching is disabled
73+
if (options.prebundle && !normalizedOptions.cacheOptions.enabled) {
74+
context.logger.warn(
75+
`Prebundling has been configured but will not be used because caching has been disabled.`,
76+
);
77+
}
78+
7279
return defer(() => import('./vite-server')).pipe(
7380
switchMap(({ serveWithVite }) =>
7481
serveWithVite(normalizedOptions, builderName, context, transforms, extensions),
7582
),
7683
);
7784
}
7885

86+
// Warn if the initial options provided by the user enable prebundling with Webpack-based builders
87+
if (options.prebundle) {
88+
context.logger.warn(
89+
`Prebundling has been configured but will not be used because it is not supported by the "${builderName}" builder.`,
90+
);
91+
}
92+
7993
if (extensions?.buildPlugins?.length) {
8094
throw new Error('Only the `application` and `browser-esbuild` builders support plugins.');
8195
}
@@ -105,7 +119,13 @@ async function initialize(
105119
await purgeStaleBuildCache(context);
106120

107121
const normalizedOptions = await normalizeOptions(context, projectName, initialOptions);
108-
const builderName = await context.getBuilderNameForTarget(normalizedOptions.buildTarget);
122+
const builderName = builderSelector(
123+
{
124+
builderName: await context.getBuilderNameForTarget(normalizedOptions.buildTarget),
125+
forceEsbuild: !!normalizedOptions.forceEsbuild,
126+
},
127+
context.logger,
128+
);
109129

110130
if (
111131
!normalizedOptions.disableHostCheck &&
@@ -133,10 +153,7 @@ case.
133153
normalizedOptions.port = await checkPort(normalizedOptions.port, normalizedOptions.host);
134154

135155
return {
136-
builderName: builderSelector(
137-
{ builderName, forceEsbuild: !!normalizedOptions.forceEsbuild },
138-
context.logger,
139-
),
156+
builderName,
140157
normalizedOptions,
141158
};
142159
}

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

+3
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ export async function normalizeOptions(
5858
sslCert,
5959
sslKey,
6060
forceEsbuild,
61+
prebundle,
6162
} = options;
6263

6364
// Return all the normalized options
@@ -84,5 +85,7 @@ export async function normalizeOptions(
8485
sslCert,
8586
sslKey,
8687
forceEsbuild,
88+
// Prebundling defaults to true but requires caching to function
89+
prebundle: cacheOptions.enabled && (prebundle ?? true),
8790
};
8891
}

packages/angular_devkit/build_angular/src/builders/dev-server/schema.json

+18
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,24 @@
106106
"type": "boolean",
107107
"description": "Force the development server to use the 'browser-esbuild' builder when building. This is a developer preview option for the esbuild-based build system.",
108108
"default": false
109+
},
110+
"prebundle": {
111+
"description": "Enable and control the Vite-based development server's prebundling capabilities. To enable prebundling, the Angular CLI cache must also be enabled. This option has no effect when using the 'browser' or other Webpack-based builders.",
112+
"oneOf": [
113+
{ "type": "boolean" },
114+
{
115+
"type": "object",
116+
"properties": {
117+
"exclude": {
118+
"description": "List of package imports that should not be prebundled by the development server. The packages will be bundled into the application code itself.",
119+
"type": "array",
120+
"items": { "type": "string" }
121+
}
122+
},
123+
"additionalProperties": false,
124+
"required": ["exclude"]
125+
}
126+
]
109127
}
110128
},
111129
"additionalProperties": false,

packages/angular_devkit/build_angular/src/builders/dev-server/tests/execute-fetch.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,24 @@ export async function executeOnceAndFetch<T>(
1818
harness: BuilderHarness<T>,
1919
url: string,
2020
options?: Partial<BuilderHarnessExecutionOptions> & { request?: RequestInit },
21-
): Promise<BuilderHarnessExecutionResult & { response?: Response }> {
21+
): Promise<BuilderHarnessExecutionResult & { response?: Response; content?: string }> {
2222
return lastValueFrom(
2323
harness.execute().pipe(
2424
timeout(30000),
2525
mergeMap(async (executionResult) => {
2626
let response = undefined;
27+
let content = undefined;
2728
if (executionResult.result?.success) {
2829
let baseUrl = `${executionResult.result.baseUrl}`;
2930
baseUrl = baseUrl[baseUrl.length - 1] === '/' ? baseUrl : `${baseUrl}/`;
3031
const resolvedUrl = new URL(url, baseUrl);
31-
response = await fetch(resolvedUrl, options?.request);
32+
const originalResponse = await fetch(resolvedUrl, options?.request);
33+
response = originalResponse.clone();
34+
// Ensure all data is available before stopping server
35+
content = await originalResponse.text();
3236
}
3337

34-
return { ...executionResult, response };
38+
return { ...executionResult, response, content };
3539
}),
3640
take(1),
3741
),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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.io/license
7+
*/
8+
9+
import { executeDevServer } from '../../index';
10+
import { executeOnceAndFetch } from '../execute-fetch';
11+
import { describeServeBuilder } from '../jasmine-helpers';
12+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
13+
14+
// TODO: Temporarily disabled pending investigation into test-only Vite not stopping when caching is enabled
15+
describeServeBuilder(
16+
executeDevServer,
17+
DEV_SERVER_BUILDER_INFO,
18+
(harness, setupTarget, isViteRun) => {
19+
// prebundling is not available in webpack
20+
(isViteRun ? xdescribe : xdescribe)('option: "prebundle"', () => {
21+
beforeEach(async () => {
22+
setupTarget(harness);
23+
24+
harness.useProject('test', {
25+
cli: {
26+
cache: {
27+
enabled: true,
28+
},
29+
},
30+
});
31+
32+
// Application code is not needed for these tests
33+
await harness.writeFile(
34+
'src/main.ts',
35+
`
36+
import { VERSION as coreVersion } from '@angular/core';
37+
import { VERSION as platformVersion } from '@angular/platform-browser';
38+
39+
console.log(coreVersion);
40+
console.log(platformVersion);
41+
`,
42+
);
43+
});
44+
45+
it('should prebundle dependencies when option is not present', async () => {
46+
harness.useTarget('serve', {
47+
...BASE_OPTIONS,
48+
});
49+
50+
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
51+
52+
expect(result?.success).toBeTrue();
53+
expect(content).toContain('vite/deps/@angular_core.js');
54+
expect(content).not.toContain('node_modules/@angular/core/');
55+
});
56+
57+
it('should prebundle dependencies when option is set to true', async () => {
58+
harness.useTarget('serve', {
59+
...BASE_OPTIONS,
60+
prebundle: true,
61+
});
62+
63+
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
64+
65+
expect(result?.success).toBeTrue();
66+
expect(content).toContain('vite/deps/@angular_core.js');
67+
expect(content).not.toContain('node_modules/@angular/core/');
68+
});
69+
70+
it('should not prebundle dependencies when option is set to false', async () => {
71+
harness.useTarget('serve', {
72+
...BASE_OPTIONS,
73+
prebundle: false,
74+
});
75+
76+
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
77+
78+
expect(result?.success).toBeTrue();
79+
expect(content).not.toContain('vite/deps/@angular_core.js');
80+
expect(content).toContain('node_modules/@angular/core/');
81+
});
82+
83+
it('should not prebundle specified dependency if added to exclude list', async () => {
84+
harness.useTarget('serve', {
85+
...BASE_OPTIONS,
86+
prebundle: { exclude: ['@angular/platform-browser'] },
87+
});
88+
89+
const { result, content } = await executeOnceAndFetch(harness, '/main.js');
90+
91+
expect(result?.success).toBeTrue();
92+
expect(content).toContain('vite/deps/@angular_core.js');
93+
expect(content).not.toContain('node_modules/@angular/core/');
94+
expect(content).not.toContain('vite/deps/@angular_platform-browser.js');
95+
expect(content).toContain('node_modules/@angular/platform-browser/');
96+
});
97+
});
98+
},
99+
);

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ export async function* serveWithVite(
8080
}
8181

8282
// Set all packages as external to support Vite's prebundle caching
83-
browserOptions.externalPackages = serverOptions.cacheOptions.enabled;
83+
browserOptions.externalPackages = serverOptions.prebundle as json.JsonValue;
8484

8585
const baseHref = browserOptions.baseHref;
8686
if (serverOptions.servePath === undefined && baseHref !== undefined) {
@@ -513,7 +513,7 @@ export async function setupServer(
513513
*/
514514

515515
// Only enable with caching since it causes prebundle dependencies to be cached
516-
disabled: true, // !serverOptions.cacheOptions.enabled,
516+
disabled: true, // serverOptions.prebundle === false,
517517
// Exclude any explicitly defined dependencies (currently build defined externals and node.js built-ins)
518518
exclude: serverExplicitExternal,
519519
// Include all implict dependencies from the external packages internal option
@@ -543,7 +543,7 @@ export async function setupServer(
543543
// Browser only optimizeDeps. (This does not run for SSR dependencies).
544544
optimizeDeps: getDepOptimizationConfig({
545545
// Only enable with caching since it causes prebundle dependencies to be cached
546-
disabled: !serverOptions.cacheOptions.enabled,
546+
disabled: serverOptions.prebundle === false,
547547
// Exclude any explicitly defined dependencies (currently build defined externals)
548548
exclude: externalMetadata.explicit,
549549
// Include all implict dependencies from the external packages internal option

0 commit comments

Comments
 (0)