Skip to content

Commit 1f299ff

Browse files
committed
fix(@angular-devkit/build-angular): prebundle dependencies for SSR when using Vite
The commit introduces dependencies prebundling and optimisation for SSR dependencies. This is primarily needed for Angular linking and async/await downlevelling. To enable this, we need to use the undocumented `optimizeDeps` setting under the `ssr` option. This is because, the top level `optimizeDeps` vite config option only controls browser dependencies. For the above mentioned option to take effect and transform node packages, we also need to use `noExternal` and use a catch all `RegExp`. Note: setting this option to `true` has a different effect from a catch all `RegExp`, as the former will cause the `external` option to be ignored. Additionally together with `externalMetadata.explicit` we add Node.js build-ins as `external`. Closes: #26192 (cherry picked from commit 4c1265a)
1 parent fd2c4c3 commit 1f299ff

File tree

1 file changed

+81
-27
lines changed
  • packages/angular_devkit/build_angular/src/builders/dev-server

1 file changed

+81
-27
lines changed

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

Lines changed: 81 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ import assert from 'node:assert';
1515
import { randomUUID } from 'node:crypto';
1616
import { readFile } from 'node:fs/promises';
1717
import { ServerResponse } from 'node:http';
18-
import type { AddressInfo } from 'node:net';
1918
import path from 'node:path';
20-
import type { Connect, InlineConfig, ViteDevServer } from 'vite';
19+
import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from 'vite';
2120
import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
2221
import { JavaScriptTransformer } from '../../tools/esbuild/javascript-transformer';
22+
import { createRxjsEsmResolutionPlugin } from '../../tools/esbuild/rxjs-esm-resolution-plugin';
2323
import { getFeatureSupport, transformSupportedBrowsersToTargets } from '../../tools/esbuild/utils';
2424
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
2525
import { renderPage } from '../../utils/server-rendering/render-page';
@@ -173,6 +173,8 @@ export async function* serveWithVite(
173173

174174
// To avoid disconnecting the array objects from the option, these arrays need to be mutated
175175
// instead of replaced.
176+
// TODO: split explicit imports by platform to avoid having Vite optimize server-only/browser-only
177+
// dependencies twice when SSR is enabled.
176178
if (result.externalMetadata) {
177179
if (result.externalMetadata.explicit) {
178180
externalMetadata.explicit.push(...result.externalMetadata.explicit);
@@ -379,6 +381,8 @@ export async function setupServer(
379381
path.join(serverOptions.workspaceRoot, `.angular/vite-root/${randomUUID()}/`),
380382
);
381383

384+
const { builtinModules } = await import('node:module');
385+
382386
const configuration: InlineConfig = {
383387
configFile: false,
384388
envFile: false,
@@ -412,8 +416,21 @@ export async function setupServer(
412416
preTransformRequests: externalMetadata.explicit.length === 0,
413417
},
414418
ssr: {
415-
// Exclude any provided dependencies (currently build defined externals)
416-
external: externalMetadata.explicit,
419+
// Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored.
420+
noExternal: /.*/,
421+
// Exclude any Node.js built in module and provided dependencies (currently build defined externals)
422+
external: [...builtinModules, ...externalMetadata.explicit],
423+
optimizeDeps: getDepOptimizationConfig({
424+
// Only enable with caching since it causes prebundle dependencies to be cached
425+
disabled: !serverOptions.cacheOptions.enabled,
426+
// Exclude any explicitly defined dependencies (currently build defined externals)
427+
exclude: externalMetadata.explicit,
428+
// Include all implict dependencies from the external packages internal option
429+
include: externalMetadata.implicit,
430+
ssr: true,
431+
prebundleTransformer,
432+
target,
433+
}),
417434
},
418435
plugins: [
419436
createAngularLocaleDataPlugin(),
@@ -645,35 +662,18 @@ export async function setupServer(
645662
},
646663
},
647664
],
648-
optimizeDeps: {
665+
// Browser only optimizeDeps. (This does not run for SSR dependencies).
666+
optimizeDeps: getDepOptimizationConfig({
649667
// Only enable with caching since it causes prebundle dependencies to be cached
650668
disabled: !serverOptions.cacheOptions.enabled,
651669
// Exclude any explicitly defined dependencies (currently build defined externals)
652670
exclude: externalMetadata.explicit,
653671
// Include all implict dependencies from the external packages internal option
654672
include: externalMetadata.implicit,
655-
// Skip automatic file-based entry point discovery
656-
entries: [],
657-
// Add an esbuild plugin to run the Angular linker on dependencies
658-
esbuildOptions: {
659-
// Set esbuild supported targets.
660-
target,
661-
supported: getFeatureSupport(target),
662-
plugins: [
663-
{
664-
name: 'angular-vite-optimize-deps',
665-
setup(build) {
666-
build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => {
667-
return {
668-
contents: await prebundleTransformer.transformFile(args.path),
669-
loader: 'js',
670-
};
671-
});
672-
},
673-
},
674-
],
675-
},
676-
},
673+
ssr: false,
674+
prebundleTransformer,
675+
target,
676+
}),
677677
};
678678

679679
if (serverOptions.ssl) {
@@ -728,3 +728,57 @@ function pathnameWithoutServePath(url: string, serverOptions: NormalizedDevServe
728728

729729
return pathname;
730730
}
731+
732+
type ViteEsBuildPlugin = NonNullable<
733+
NonNullable<DepOptimizationConfig['esbuildOptions']>['plugins']
734+
>[0];
735+
736+
function getDepOptimizationConfig({
737+
disabled,
738+
exclude,
739+
include,
740+
target,
741+
prebundleTransformer,
742+
ssr,
743+
}: {
744+
disabled: boolean;
745+
exclude: string[];
746+
include: string[];
747+
target: string[];
748+
prebundleTransformer: JavaScriptTransformer;
749+
ssr: boolean;
750+
}): DepOptimizationConfig {
751+
const plugins: ViteEsBuildPlugin[] = [
752+
{
753+
name: `angular-vite-optimize-deps${ssr ? '-ssr' : ''}`,
754+
setup(build) {
755+
build.onLoad({ filter: /\.[cm]?js$/ }, async (args) => {
756+
return {
757+
contents: await prebundleTransformer.transformFile(args.path),
758+
loader: 'js',
759+
};
760+
});
761+
},
762+
},
763+
];
764+
765+
if (ssr) {
766+
plugins.unshift(createRxjsEsmResolutionPlugin() as ViteEsBuildPlugin);
767+
}
768+
769+
return {
770+
// Only enable with caching since it causes prebundle dependencies to be cached
771+
disabled,
772+
// Exclude any explicitly defined dependencies (currently build defined externals)
773+
exclude,
774+
// Include all implict dependencies from the external packages internal option
775+
include,
776+
// Add an esbuild plugin to run the Angular linker on dependencies
777+
esbuildOptions: {
778+
// Set esbuild supported targets.
779+
target,
780+
supported: getFeatureSupport(target),
781+
plugins,
782+
},
783+
};
784+
}

0 commit comments

Comments
 (0)