Skip to content

Commit bbc2901

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 allows API and routes defined in `server.ts` to be accessible during development. This feature requires the new `@angular/ssr` APIs, which are currently in developer preview.
1 parent ad014c7 commit bbc2901

File tree

11 files changed

+718
-139
lines changed

11 files changed

+718
-139
lines changed

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

+2-2
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

+50-32
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),
@@ -334,12 +350,6 @@ export async function* serveWithVite(
334350
server = await createServer(serverConfiguration);
335351
await server.listen();
336352

337-
if (browserOptions.ssr && serverOptions.prebundle !== false) {
338-
// Warm up the SSR request and begin optimizing dependencies.
339-
// Without this, Vite will only start optimizing SSR modules when the first request is made.
340-
void server.warmupRequest('./main.server.mjs', { ssr: true });
341-
}
342-
343353
const urls = server.resolvedUrls;
344354
if (urls && (urls.local.length || urls.network.length)) {
345355
serverUrl = new URL(urls.local[0] ?? urls.network[0]);
@@ -385,34 +395,37 @@ async function handleUpdate(
385395
usedComponentStyles: Map<string, string[]>,
386396
): Promise<void> {
387397
const updatedFiles: string[] = [];
388-
let isServerFileUpdated = false;
398+
let destroyAngularServerAppCalled = false;
389399

390400
// 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;
401+
for (const [file, { updated, type }] of generatedFiles) {
402+
if (!updated) {
403+
continue;
404+
}
395405

396-
const updatedModules = server.moduleGraph.getModulesByFile(
397-
normalizePath(join(server.config.root, file)),
398-
);
399-
updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
406+
if (type === BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
407+
// Clear the server app cache
408+
// This must be done before module invalidation.
409+
const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
410+
ɵdestroyAngularServerApp: typeof destroyAngularServerApp;
411+
};
412+
413+
ɵdestroyAngularServerApp();
414+
destroyAngularServerAppCalled = true;
400415
}
416+
417+
updatedFiles.push(file);
418+
419+
const updatedModules = server.moduleGraph.getModulesByFile(
420+
normalizePath(join(server.config.root, file)),
421+
);
422+
updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m));
401423
}
402424

403425
if (!updatedFiles.length) {
404426
return;
405427
}
406428

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-
416429
if (serverOptions.liveReload || serverOptions.hmr) {
417430
if (updatedFiles.every((f) => f.endsWith('.css'))) {
418431
const timestamp = Date.now();
@@ -534,7 +547,7 @@ export async function setupServer(
534547
assets: Map<string, string>,
535548
preserveSymlinks: boolean | undefined,
536549
externalMetadata: DevServerExternalResultMetadata,
537-
ssr: boolean,
550+
ssrMode: ServerSsrMode,
538551
prebundleTransformer: JavaScriptTransformer,
539552
target: string[],
540553
zoneless: boolean,
@@ -587,6 +600,9 @@ export async function setupServer(
587600
preserveSymlinks,
588601
},
589602
server: {
603+
warmup: {
604+
ssrFiles: ['./main.server.mjs', './server.mjs'],
605+
},
590606
port: serverOptions.port,
591607
strictPort: true,
592608
host: serverOptions.host,
@@ -637,19 +653,21 @@ export async function setupServer(
637653
},
638654
plugins: [
639655
createAngularLocaleDataPlugin(),
640-
createAngularMemoryPlugin({
641-
workspaceRoot: serverOptions.workspaceRoot,
642-
virtualProjectRoot,
656+
createAngularSetupMiddlewaresPlugin({
643657
outputFiles,
644658
assets,
645-
ssr,
646-
external: externalMetadata.explicitBrowser,
647659
indexHtmlTransformer,
648660
extensionMiddleware,
649-
normalizePath,
650661
usedComponentStyles,
662+
ssrMode,
651663
}),
652664
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
665+
await createAngularSsrServerPlugin(serverOptions.workspaceRoot),
666+
await createAngularMemoryPlugin({
667+
virtualProjectRoot,
668+
outputFiles,
669+
external: externalMetadata.explicitBrowser,
670+
}),
653671
],
654672
// Browser only optimizeDeps. (This does not run for SSR dependencies).
655673
optimizeDeps: getDepOptimizationConfig({

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

+22-84
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,26 @@
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';
12-
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';
11+
import { basename, dirname, join, relative } from 'node:path';
12+
import type { Plugin } from 'vite';
13+
import { loadEsmModule } from '../../utils/load-esm';
2114
import { AngularMemoryOutputFiles } from './utils';
2215

2316
export interface AngularMemoryPluginOptions {
24-
workspaceRoot: string;
2517
virtualProjectRoot: string;
2618
outputFiles: AngularMemoryOutputFiles;
27-
assets: Map<string, string>;
28-
ssr: boolean;
2919
external?: string[];
30-
extensionMiddleware?: Connect.NextHandleFunction[];
31-
indexHtmlTransformer?: (content: string) => Promise<string>;
32-
normalizePath: (path: string) => string;
33-
usedComponentStyles: Map<string, string[]>;
3420
}
3521

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;
22+
export async function createAngularMemoryPlugin(
23+
options: AngularMemoryPluginOptions,
24+
): Promise<Plugin> {
25+
const { virtualProjectRoot, outputFiles, external } = options;
26+
const { normalizePath } = await loadEsmModule<typeof import('vite')>('vite');
27+
// See: https://github.com/vitejs/vite/blob/a34a73a3ad8feeacc98632c0f4c643b6820bbfda/packages/vite/src/node/server/pluginContainer.ts#L331-L334
28+
const defaultImporter = join(virtualProjectRoot, 'index.html');
4929

5030
return {
5131
name: 'vite:angular-memory',
@@ -59,12 +39,18 @@ export function createAngularMemoryPlugin(options: AngularMemoryPluginOptions):
5939
return source;
6040
}
6141

62-
if (importer && source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
63-
// Remove query if present
64-
const [importerFile] = importer.split('?', 1);
65-
66-
source =
67-
'/' + normalizePath(join(dirname(relative(virtualProjectRoot, importerFile)), source));
42+
if (importer) {
43+
let normalizedSource: string | undefined;
44+
if (source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) {
45+
// Remove query if present
46+
const [importerFile] = importer.split('?', 1);
47+
normalizedSource = join(dirname(relative(virtualProjectRoot, importerFile)), source);
48+
} else if (source[0] === '/' && importer === defaultImporter) {
49+
normalizedSource = basename(source);
50+
}
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

+4-1
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)