Skip to content

Commit 6e4981e

Browse files
committed
feat(@angular/build): utilize ssr.entry in Vite dev-server when available
When `ssr.entry` (`server.ts`) is defined, Vite will now use it in the dev-server. This feature requires the new `@angular/ssr` APIs, which are currently in developer preview.
1 parent c0315fb commit 6e4981e

File tree

10 files changed

+700
-115
lines changed

10 files changed

+700
-115
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,15 +88,15 @@ export async function* buildApplicationInternal(
8888

8989
yield* runEsBuildBuildAction(
9090
async (rebuildState) => {
91-
const { serverEntryPoint, jsonLogs } = normalizedOptions;
91+
const { serverEntryPoint, jsonLogs, disableFullServerManifestGeneration } = normalizedOptions;
9292

9393
const startTime = process.hrtime.bigint();
9494
const result = await executeBuild(normalizedOptions, context, rebuildState);
9595

9696
if (jsonLogs) {
9797
result.addLog(await createJsonBuildManifest(result, normalizedOptions));
9898
} else {
99-
if (serverEntryPoint) {
99+
if (serverEntryPoint && !disableFullServerManifestGeneration) {
100100
const prerenderedRoutesLength = Object.keys(result.prerenderedRoutes).length;
101101
let prerenderMsg = `Prerendered ${prerenderedRoutesLength} static route`;
102102
prerenderMsg += prerenderedRoutesLength !== 1 ? 's.' : '.';

packages/angular/build/src/builders/dev-server/vite-server.ts

Lines changed: 52 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import type { Connect, DepOptimizationConfig, InlineConfig, ViteDevServer } from
1717
import { createAngularMemoryPlugin } from '../../tools/vite/angular-memory-plugin';
1818
import { createAngularLocaleDataPlugin } from '../../tools/vite/i18n-locale-plugin';
1919
import { createRemoveIdPrefixPlugin } from '../../tools/vite/id-prefix-plugin';
20+
import {
21+
ServerSsrMode,
22+
createAngularSetupMiddlewaresPlugin,
23+
} from '../../tools/vite/setup-middlewares-plugin';
24+
import { createAngularSsrServerPlugin } from '../../tools/vite/ssr-server-plugin';
2025
import { loadProxyConfiguration, normalizeSourceMaps } from '../../utils';
2126
import { loadEsmModule } from '../../utils/load-esm';
2227
import { Result, ResultFile, ResultKind } from '../application/results';
@@ -313,14 +318,25 @@ export async function* serveWithVite(
313318
? browserOptions.polyfills
314319
: [browserOptions.polyfills];
315320

321+
let ssrMode: ServerSsrMode = ServerSsrMode.NoSsr;
322+
if (
323+
browserOptions.outputMode &&
324+
typeof browserOptions.ssr === 'object' &&
325+
browserOptions.ssr.entry
326+
) {
327+
ssrMode = ServerSsrMode.ExternalSsrMiddleware;
328+
} else if (browserOptions.server) {
329+
ssrMode = ServerSsrMode.InternalSsrMiddleware;
330+
}
331+
316332
// Setup server and start listening
317333
const serverConfiguration = await setupServer(
318334
serverOptions,
319335
generatedFiles,
320336
assetFiles,
321337
browserOptions.preserveSymlinks,
322338
externalMetadata,
323-
!!browserOptions.ssr,
339+
ssrMode,
324340
prebundleTransformer,
325341
target,
326342
isZonelessApp(polyfills),
@@ -337,7 +353,10 @@ export async function* serveWithVite(
337353
if (browserOptions.ssr && serverOptions.prebundle !== false) {
338354
// Warm up the SSR request and begin optimizing dependencies.
339355
// Without this, Vite will only start optimizing SSR modules when the first request is made.
340-
void server.warmupRequest('./main.server.mjs', { ssr: true });
356+
void Promise.allSettled([
357+
server.warmupRequest('./server.mjs', { ssr: true }),
358+
server.warmupRequest('./main.server.mjs', { ssr: true }),
359+
]);
341360
}
342361

343362
const urls = server.resolvedUrls;
@@ -385,34 +404,37 @@ async function handleUpdate(
385404
usedComponentStyles: Map<string, string[]>,
386405
): Promise<void> {
387406
const updatedFiles: string[] = [];
388-
let isServerFileUpdated = false;
407+
let destroyAngularServerAppCalled = false;
389408

390409
// Invalidate any updated files
391-
for (const [file, record] of generatedFiles) {
392-
if (record.updated) {
393-
updatedFiles.push(file);
394-
isServerFileUpdated ||= record.type === BuildOutputFileType.ServerApplication;
410+
for (const [file, { updated, type }] of generatedFiles) {
411+
if (!updated) {
412+
continue;
413+
}
395414

396-
const updatedModules = server.moduleGraph.getModulesByFile(
397-
normalizePath(join(server.config.root, file)),
398-
);
399-
updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
415+
if (type === BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
416+
// Clear the server app cache
417+
// This must be done before module invalidation.
418+
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
419+
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
420+
};
421+
422+
ɵdestroyAngularServerApp();
423+
destroyAngularServerAppCalled = true;
400424
}
425+
426+
updatedFiles.push(file);
427+
428+
const updatedModules = server.moduleGraph.getModulesByFile(
429+
normalizePath(join(server.config.root, file)),
430+
);
431+
updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m));
401432
}
402433

403434
if (!updatedFiles.length) {
404435
return;
405436
}
406437

407-
// clean server apps cache
408-
if (isServerFileUpdated) {
409-
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
410-
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
411-
};
412-
413-
ɵdestroyAngularServerApp();
414-
}
415-
416438
if (serverOptions.liveReload || serverOptions.hmr) {
417439
if (updatedFiles.every((f) => f.endsWith('.css'))) {
418440
const timestamp = Date.now();
@@ -534,7 +556,7 @@ export async function setupServer(
534556
assets: Map<string, string>,
535557
preserveSymlinks: boolean | undefined,
536558
externalMetadata: DevServerExternalResultMetadata,
537-
ssr: boolean,
559+
ssrMode: ServerSsrMode,
538560
prebundleTransformer: JavaScriptTransformer,
539561
target: string[],
540562
zoneless: boolean,
@@ -637,19 +659,22 @@ export async function setupServer(
637659
},
638660
plugins: [
639661
createAngularLocaleDataPlugin(),
640-
createAngularMemoryPlugin({
641-
workspaceRoot: serverOptions.workspaceRoot,
642-
virtualProjectRoot,
662+
createAngularSetupMiddlewaresPlugin({
643663
outputFiles,
644664
assets,
645-
ssr,
646-
external: externalMetadata.explicitBrowser,
647665
indexHtmlTransformer,
648666
extensionMiddleware,
649-
normalizePath,
650667
usedComponentStyles,
668+
ssrMode,
651669
}),
652670
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
671+
await createAngularSsrServerPlugin(serverOptions.workspaceRoot),
672+
await createAngularMemoryPlugin({
673+
workspaceRoot: serverOptions.workspaceRoot,
674+
virtualProjectRoot,
675+
outputFiles,
676+
external: externalMetadata.explicitBrowser,
677+
}),
653678
],
654679
// Browser only optimizeDeps. (This does not run for SSR dependencies).
655680
optimizeDeps: getDepOptimizationConfig({

packages/angular/build/src/tools/vite/angular-memory-plugin.ts

Lines changed: 19 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,25 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import remapping, { SourceMapInput } from '@ampproject/remapping';
109
import assert from 'node:assert';
1110
import { readFile } from 'node:fs/promises';
1211
import { dirname, join, relative } from 'node:path';
13-
import type { Connect, Plugin } from 'vite';
14-
import {
15-
angularHtmlFallbackMiddleware,
16-
createAngularAssetsMiddleware,
17-
createAngularHeadersMiddleware,
18-
createAngularIndexHtmlMiddleware,
19-
createAngularSSRMiddleware,
20-
} from './middlewares';
12+
import type { Plugin } from 'vite';
13+
import { loadEsmModule } from '../../utils/load-esm';
2114
import { AngularMemoryOutputFiles } from './utils';
2215

2316
export interface AngularMemoryPluginOptions {
2417
workspaceRoot: string;
2518
virtualProjectRoot: string;
2619
outputFiles: AngularMemoryOutputFiles;
27-
assets: Map<string, string>;
28-
ssr: boolean;
2920
external?: string[];
30-
extensionMiddleware?: Connect.NextHandleFunction[];
31-
indexHtmlTransformer?: (content: string) => Promise<string>;
32-
normalizePath: (path: string) => string;
33-
usedComponentStyles: Map<string, string[]>;
3421
}
3522

36-
export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Plugin {
37-
const {
38-
workspaceRoot,
39-
virtualProjectRoot,
40-
outputFiles,
41-
assets,
42-
external,
43-
ssr,
44-
extensionMiddleware,
45-
indexHtmlTransformer,
46-
normalizePath,
47-
usedComponentStyles,
48-
} = options;
23+
export async function createAngularMemoryPlugin(
24+
options: AngularMemoryPluginOptions,
25+
): Promise<Plugin> {
26+
const { virtualProjectRoot, outputFiles, external, workspaceRoot } = options;
27+
const { normalizePath } = await loadEsmModule<typeof import('vite')>('vite');
4928

5029
return {
5130
name: 'vite:angular-memory',
@@ -59,12 +38,19 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
5938
return source;
6039
}
6140

62-
if (importer && source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
63-
// Remove query if present
64-
const [importerFile] = importer.split('?', 1);
41+
if (importer) {
42+
let normalizedSource: string | undefined;
43+
if (source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
44+
// Remove query if present
45+
const [importerFile] = importer.split('?', 1);
46+
normalizedSource = join(dirname(relative(virtualProjectRoot, importerFile)), source);
47+
} else if (normalizePath(source).startsWith(workspaceRoot)) {
48+
normalizedSource = relative(workspaceRoot, source);
49+
}
6550

66-
source =
67-
'/' + normalizePath(join(dirname(relative(virtualProjectRoot, importerFile)), source));
51+
if (normalizedSource) {
52+
source = '/' + normalizePath(normalizedSource);
53+
}
6854
}
6955

7056
const [file] = source.split('?', 1);
@@ -92,54 +78,6 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
9278
map: mapContents && Buffer.from(mapContents).toString('utf-8'),
9379
};
9480
},
95-
// eslint-disable-next-line max-lines-per-function
96-
configureServer(server) {
97-
const originalssrTransform = server.ssrTransform;
98-
server.ssrTransform = async (code, map, url, originalCode) => {
99-
const result = await originalssrTransform(code, null, url, originalCode);
100-
if (!result || !result.map || !map) {
101-
return result;
102-
}
103-
104-
const remappedMap = remapping(
105-
[result.map as SourceMapInput, map as SourceMapInput],
106-
() => null,
107-
);
108-
109-
// Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
110-
remappedMap.sourceRoot = normalizePath(workspaceRoot) + '/';
111-
112-
return {
113-
...result,
114-
map: remappedMap as (typeof result)['map'],
115-
};
116-
};
117-
118-
server.middlewares.use(createAngularHeadersMiddleware(server));
119-
120-
// Assets and resources get handled first
121-
server.middlewares.use(
122-
createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles),
123-
);
124-
125-
if (extensionMiddleware?.length) {
126-
extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
127-
}
128-
129-
// Returning a function, installs middleware after the main transform middleware but
130-
// before the built-in HTML middleware
131-
return () => {
132-
if (ssr) {
133-
server.middlewares.use(createAngularSSRMiddleware(server, indexHtmlTransformer));
134-
}
135-
136-
server.middlewares.use(angularHtmlFallbackMiddleware);
137-
138-
server.middlewares.use(
139-
createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransformer),
140-
);
141-
};
142-
},
14381
};
14482
}
14583

packages/angular/build/src/tools/vite/middlewares/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,8 @@
99
export { createAngularAssetsMiddleware } from './assets-middleware';
1010
export { angularHtmlFallbackMiddleware } from './html-fallback-middleware';
1111
export { createAngularIndexHtmlMiddleware } from './index-html-middleware';
12-
export { createAngularSSRMiddleware } from './ssr-middleware';
12+
export {
13+
createAngularSsrExternalMiddleware,
14+
createAngularSsrInternalMiddleware,
15+
} from './ssr-middleware';
1316
export { createAngularHeadersMiddleware } from './headers-middleware';

0 commit comments

Comments
 (0)