Skip to content

Commit bbc2901

Browse files
committedSep 23, 2024
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';

‎packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts

+64-4
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import type { ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp } from '@angular/ssr';
9+
import type {
10+
AngularAppEngine as SSRAngularAppEngine,
11+
ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp,
12+
} from '@angular/ssr';
1013
import type { ServerResponse } from 'node:http';
1114
import type { Connect, ViteDevServer } from 'vite';
1215
import { loadEsmModule } from '../../../utils/load-esm';
1316

14-
export function createAngularSSRMiddleware(
17+
export function createAngularSsrInternalMiddleware(
1518
server: ViteDevServer,
1619
indexHtmlTransformer?: (content: string) => Promise<string>,
1720
): Connect.NextHandleFunction {
1821
let cachedAngularServerApp: ReturnType<typeof getOrCreateAngularServerApp> | undefined;
1922

20-
return function angularSSRMiddleware(
23+
return function angularSsrMiddleware(
2124
req: Connect.IncomingMessage,
2225
res: ServerResponse,
2326
next: Connect.NextFunction,
@@ -41,7 +44,7 @@ export function createAngularSSRMiddleware(
4144
const angularServerApp = ɵgetOrCreateAngularServerApp();
4245
// Only Add the transform hook only if it's a different instance.
4346
if (cachedAngularServerApp !== angularServerApp) {
44-
angularServerApp.hooks.on('html:transform:pre', async ({ html }) => {
47+
angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
4548
const processedHtml = await server.transformIndexHtml(url.pathname, html);
4649

4750
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
@@ -62,3 +65,60 @@ export function createAngularSSRMiddleware(
6265
})().catch(next);
6366
};
6467
}
68+
69+
export function createAngularSsrExternalMiddleware(
70+
server: ViteDevServer,
71+
indexHtmlTransformer?: (content: string) => Promise<string>,
72+
): Connect.NextHandleFunction {
73+
let fallbackWarningShown = false;
74+
let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
75+
let angularSsrInternalMiddleware:
76+
| ReturnType<typeof createAngularSsrInternalMiddleware>
77+
| undefined;
78+
79+
return function angularSsrExternalMiddleware(
80+
req: Connect.IncomingMessage,
81+
res: ServerResponse,
82+
next: Connect.NextFunction,
83+
) {
84+
(async () => {
85+
const { default: handler, AngularAppEngine } = (await server.ssrLoadModule(
86+
'./server.mjs',
87+
)) as {
88+
default?: unknown;
89+
AngularAppEngine: typeof SSRAngularAppEngine;
90+
};
91+
92+
if (typeof handler !== 'function' || !('__ng_node_next_handler__' in handler)) {
93+
if (!fallbackWarningShown) {
94+
// eslint-disable-next-line no-console
95+
console.warn(
96+
`The default export in 'server.ts' does not provide a Node.js request handler. ` +
97+
'Using the internal SSR middleware instead.',
98+
);
99+
fallbackWarningShown = true;
100+
}
101+
102+
angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware(
103+
server,
104+
indexHtmlTransformer,
105+
);
106+
107+
return angularSsrInternalMiddleware(req, res, next);
108+
}
109+
110+
if (cachedAngularAppEngine !== AngularAppEngine) {
111+
AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
112+
const processedHtml = await server.transformIndexHtml(url.pathname, html);
113+
114+
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
115+
});
116+
117+
cachedAngularAppEngine = AngularAppEngine;
118+
}
119+
120+
// Forward the request to the middleware in server.ts
121+
return (handler as unknown as Connect.NextHandleFunction)(req, res, next);
122+
})().catch(next);
123+
};
124+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.dev/license
7+
*/
8+
9+
import type { Connect, Plugin } from 'vite';
10+
import {
11+
angularHtmlFallbackMiddleware,
12+
createAngularAssetsMiddleware,
13+
createAngularHeadersMiddleware,
14+
createAngularIndexHtmlMiddleware,
15+
createAngularSsrExternalMiddleware,
16+
createAngularSsrInternalMiddleware,
17+
} from './middlewares';
18+
import { AngularMemoryOutputFiles } from './utils';
19+
20+
export enum ServerSsrMode {
21+
/**
22+
* No SSR
23+
*/
24+
NoSsr,
25+
26+
/**
27+
* Internal server-side rendering (SSR) is handled through the built-in middleware.
28+
*
29+
* In this mode, the SSR process is managed internally by the dev-server's middleware.
30+
* The server automatically renders pages on the server without requiring external
31+
* middleware or additional configuration from the developer.
32+
*/
33+
InternalSsrMiddleware,
34+
35+
/**
36+
* External server-side rendering (SSR) is handled by a custom middleware defined in server.ts.
37+
*
38+
* This mode allows developers to define custom SSR behavior by providing a middleware in the
39+
* `server.ts` file. It gives more flexibility for handling SSR, such as integrating with other
40+
* frameworks or customizing the rendering pipeline.
41+
*/
42+
ExternalSsrMiddleware,
43+
}
44+
45+
export interface AngularSetupMiddlewaresPluginOptions {
46+
outputFiles: AngularMemoryOutputFiles;
47+
assets: Map<string, string>;
48+
extensionMiddleware?: Connect.NextHandleFunction[];
49+
indexHtmlTransformer?: (content: string) => Promise<string>;
50+
usedComponentStyles: Map<string, string[]>;
51+
ssrMode: ServerSsrMode;
52+
}
53+
54+
export function createAngularSetupMiddlewaresPlugin(
55+
options: AngularSetupMiddlewaresPluginOptions,
56+
): Plugin {
57+
return {
58+
name: 'vite:angular-setup-middlewares',
59+
enforce: 'pre',
60+
configureServer(server) {
61+
const {
62+
indexHtmlTransformer,
63+
outputFiles,
64+
extensionMiddleware,
65+
assets,
66+
usedComponentStyles,
67+
ssrMode,
68+
} = options;
69+
70+
// Headers, assets and resources get handled first
71+
server.middlewares.use(createAngularHeadersMiddleware(server));
72+
server.middlewares.use(
73+
createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles),
74+
);
75+
76+
extensionMiddleware?.forEach((middleware) => server.middlewares.use(middleware));
77+
78+
// Returning a function, installs middleware after the main transform middleware but
79+
// before the built-in HTML middleware
80+
return () => {
81+
if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
82+
server.middlewares.use(createAngularSsrExternalMiddleware(server, indexHtmlTransformer));
83+
84+
return;
85+
}
86+
87+
if (ssrMode === ServerSsrMode.InternalSsrMiddleware) {
88+
server.middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer));
89+
}
90+
91+
server.middlewares.use(angularHtmlFallbackMiddleware);
92+
server.middlewares.use(
93+
createAngularIndexHtmlMiddleware(server, outputFiles, indexHtmlTransformer),
94+
);
95+
};
96+
},
97+
};
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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.dev/license
7+
*/
8+
9+
import remapping, { SourceMapInput } from '@ampproject/remapping';
10+
import type { Plugin } from 'vite';
11+
import { loadEsmModule } from '../../utils/load-esm';
12+
13+
export async function createAngularSsrServerPlugin(workspaceRoot: string): Promise<Plugin> {
14+
const { normalizePath } = await loadEsmModule<typeof import('vite')>('vite');
15+
16+
return {
17+
name: 'vite:angular-ssr-server',
18+
enforce: 'pre',
19+
async configureServer(server) {
20+
const originalssrTransform = server.ssrTransform;
21+
22+
server.ssrTransform = async (code, map, url, originalCode) => {
23+
const result = await originalssrTransform(code, null, url, originalCode);
24+
if (!result || !result.map || !map) {
25+
return result;
26+
}
27+
28+
const remappedMap = remapping(
29+
[result.map as SourceMapInput, map as SourceMapInput],
30+
() => null,
31+
);
32+
33+
// Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
34+
remappedMap.sourceRoot = normalizePath(workspaceRoot) + '/';
35+
36+
return {
37+
...result,
38+
map: remappedMap as (typeof result)['map'],
39+
};
40+
};
41+
},
42+
};
43+
}

‎packages/angular/ssr/src/app.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -178,25 +178,26 @@ export class AngularServerApp {
178178
}),
179179
};
180180

181-
if (renderMode === RenderMode.Client) {
181+
if (renderMode === RenderMode.Server) {
182+
// Configure platform providers for request and response only for SSR.
183+
platformProviders.push(
184+
{
185+
provide: REQUEST,
186+
useValue: request,
187+
},
188+
{
189+
provide: REQUEST_CONTEXT,
190+
useValue: requestContext,
191+
},
192+
{
193+
provide: RESPONSE_INIT,
194+
useValue: responseInit,
195+
},
196+
);
197+
} else if (renderMode === RenderMode.Client) {
182198
// Serve the client-side rendered version if the route is configured for CSR.
183199
return new Response(await this.assets.getServerAsset('index.csr.html'), responseInit);
184200
}
185-
186-
platformProviders.push(
187-
{
188-
provide: REQUEST,
189-
useValue: request,
190-
},
191-
{
192-
provide: REQUEST_CONTEXT,
193-
useValue: requestContext,
194-
},
195-
{
196-
provide: RESPONSE_INIT,
197-
useValue: responseInit,
198-
},
199-
);
200201
}
201202

202203
const {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import assert from 'node:assert';
2+
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
3+
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
4+
import { installWorkspacePackages, uninstallPackage } from '../../utils/packages';
5+
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { getGlobalVariable } from '../../utils/env';
7+
8+
export default async function () {
9+
assert(
10+
getGlobalVariable('argv')['esbuild'],
11+
'This test should not be called in the Webpack suite.',
12+
);
13+
14+
// Forcibly remove in case another test doesn't clean itself up.
15+
await uninstallPackage('@angular/ssr');
16+
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
17+
await useSha();
18+
await installWorkspacePackages();
19+
20+
// Update angular.json
21+
await updateJsonFile('angular.json', (workspaceJson) => {
22+
const appArchitect = workspaceJson.projects['test-project'].architect;
23+
const options = appArchitect.build.options;
24+
options.outputMode = 'server';
25+
});
26+
27+
await writeMultipleFiles({
28+
// Replace the template of app.component.html as it makes it harder to debug
29+
'src/app/app.component.html': '<router-outlet />',
30+
'src/app/app.config.server.ts': `
31+
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
32+
import { provideServerRendering } from '@angular/platform-server';
33+
import { provideServerRoutesConfig } from '@angular/ssr';
34+
import { routes } from './app.routes.server';
35+
import { appConfig } from './app.config';
36+
37+
const serverConfig: ApplicationConfig = {
38+
providers: [
39+
provideServerRoutesConfig(routes),
40+
provideServerRendering()
41+
]
42+
};
43+
44+
export const config = mergeApplicationConfig(appConfig, serverConfig);
45+
`,
46+
'src/app/app.routes.ts': `
47+
import { Routes } from '@angular/router';
48+
import { HomeComponent } from './home/home.component';
49+
50+
export const routes: Routes = [
51+
{ path: 'home', component: HomeComponent }
52+
];
53+
`,
54+
'src/app/app.routes.server.ts': `
55+
import { RenderMode, ServerRoute } from '@angular/ssr';
56+
57+
export const routes: ServerRoute[] = [
58+
{ path: '**', renderMode: RenderMode.Server }
59+
];
60+
`,
61+
'server.ts': `
62+
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, defineNodeNextHandler } from '@angular/ssr/node';
63+
import express from 'express';
64+
import { fileURLToPath } from 'node:url';
65+
import { dirname, resolve } from 'node:path';
66+
67+
export function app(): express.Express {
68+
const server = express();
69+
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
70+
const browserDistFolder = resolve(serverDistFolder, '../browser');
71+
const angularNodeAppEngine = new AngularNodeAppEngine();
72+
73+
server.use('/api/**', (req, res) => res.json({ hello: 'foo' }));
74+
75+
server.get('**', express.static(browserDistFolder, {
76+
maxAge: '1y',
77+
index: 'index.html'
78+
}));
79+
80+
server.get('**', (req, res, next) => {
81+
angularNodeAppEngine.render(req)
82+
.then((response) => response ? writeResponseToNodeResponse(response, res) : next())
83+
.catch(next);
84+
});
85+
86+
return server;
87+
}
88+
89+
const server = app();
90+
if (isMainModule(import.meta.url)) {
91+
const port = process.env['PORT'] || 4000;
92+
server.listen(port, () => {
93+
console.log(\`Node Express server listening on http://localhost:\${port}\`);
94+
});
95+
}
96+
97+
export default defineNodeNextHandler(server);
98+
`,
99+
});
100+
101+
await silentNg('generate', 'component', 'home');
102+
103+
const port = await ngServe();
104+
105+
// Verify the server is running and the API response is correct.
106+
await validateResponse('/main.js', /bootstrapApplication/);
107+
await validateResponse('/api/test', /foo/);
108+
await validateResponse('/home', /home works/);
109+
110+
// Modify the home component and validate the change.
111+
await modifyFileAndWaitUntilUpdated(
112+
'src/app/home/home.component.html',
113+
'home works',
114+
'yay home works!!!',
115+
);
116+
await validateResponse('/api/test', /foo/);
117+
await validateResponse('/home', /yay home works/);
118+
119+
// Modify the API response and validate the change.
120+
await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
121+
await validateResponse('/api/test', /bar/);
122+
await validateResponse('/home', /yay home works/);
123+
124+
async function validateResponse(pathname: string, match: RegExp) {
125+
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
126+
const text = await response.text();
127+
assert.match(text, match);
128+
assert.equal(response.status, 200);
129+
}
130+
}
131+
132+
async function modifyFileAndWaitUntilUpdated(
133+
filePath: string,
134+
searchValue: string,
135+
replaceValue: string,
136+
) {
137+
await Promise.all([
138+
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
139+
replaceInFile(filePath, searchValue, replaceValue),
140+
]);
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import assert from 'node:assert';
2+
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
3+
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
4+
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
5+
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { getGlobalVariable } from '../../utils/env';
7+
8+
export default async function () {
9+
assert(
10+
getGlobalVariable('argv')['esbuild'],
11+
'This test should not be called in the Webpack suite.',
12+
);
13+
14+
// Forcibly remove in case another test doesn't clean itself up.
15+
await uninstallPackage('@angular/ssr');
16+
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
17+
await useSha();
18+
await installWorkspacePackages();
19+
await installPackage('fastify@5');
20+
21+
// Update angular.json
22+
await updateJsonFile('angular.json', (workspaceJson) => {
23+
const appArchitect = workspaceJson.projects['test-project'].architect;
24+
const options = appArchitect.build.options;
25+
options.outputMode = 'server';
26+
});
27+
28+
await writeMultipleFiles({
29+
// Replace the template of app.component.html as it makes it harder to debug
30+
'src/app/app.component.html': '<router-outlet />',
31+
'src/app/app.config.server.ts': `
32+
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
33+
import { provideServerRendering } from '@angular/platform-server';
34+
import { provideServerRoutesConfig } from '@angular/ssr';
35+
import { routes } from './app.routes.server';
36+
import { appConfig } from './app.config';
37+
38+
const serverConfig: ApplicationConfig = {
39+
providers: [
40+
provideServerRoutesConfig(routes),
41+
provideServerRendering()
42+
]
43+
};
44+
45+
export const config = mergeApplicationConfig(appConfig, serverConfig);
46+
`,
47+
'src/app/app.routes.ts': `
48+
import { Routes } from '@angular/router';
49+
import { HomeComponent } from './home/home.component';
50+
51+
export const routes: Routes = [
52+
{ path: 'home', component: HomeComponent }
53+
];
54+
`,
55+
'src/app/app.routes.server.ts': `
56+
import { RenderMode, ServerRoute } from '@angular/ssr';
57+
58+
export const routes: ServerRoute[] = [
59+
{ path: '**', renderMode: RenderMode.Server }
60+
];
61+
`,
62+
'server.ts': `
63+
import { AngularNodeAppEngine, writeResponseToNodeResponse, isMainModule, defineNodeNextHandler } from '@angular/ssr/node';
64+
import fastify from 'fastify';
65+
66+
export function app() {
67+
const server = fastify();
68+
const angularNodeAppEngine = new AngularNodeAppEngine();
69+
server.get('/api/*', (req, reply) => reply.send({ hello: 'foo' }));
70+
server.get('*', async (req, reply) => {
71+
try {
72+
const response = await angularNodeAppEngine.render(req.raw);
73+
if (response) {
74+
await writeResponseToNodeResponse(response, reply.raw);
75+
} else {
76+
reply.callNotFound();
77+
}
78+
} catch (error) {
79+
reply.send(error);
80+
}
81+
});
82+
83+
return server;
84+
}
85+
86+
const server = app();
87+
if (isMainModule(import.meta.url)) {
88+
const port = +(process.env['PORT'] || 4000);
89+
server.listen({ port }, () => {
90+
console.log(\`Fastify server listening on http://localhost:\${port}\`);
91+
});
92+
}
93+
94+
export default defineNodeNextHandler(async (req, res) => {
95+
await server.ready();
96+
server.server.emit('request', req, res);
97+
});
98+
`,
99+
});
100+
101+
await silentNg('generate', 'component', 'home');
102+
103+
const port = await ngServe();
104+
105+
// Verify the server is running and the API response is correct.
106+
await validateResponse('/main.js', /bootstrapApplication/);
107+
await validateResponse('/api/test', /foo/);
108+
await validateResponse('/home', /home works/);
109+
110+
// Modify the home component and validate the change.
111+
await modifyFileAndWaitUntilUpdated(
112+
'src/app/home/home.component.html',
113+
'home works',
114+
'yay home works!!!',
115+
);
116+
await validateResponse('/api/test', /foo/);
117+
await validateResponse('/home', /yay home works/);
118+
119+
// Modify the API response and validate the change.
120+
await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
121+
await validateResponse('/api/test', /bar/);
122+
await validateResponse('/home', /yay home works/);
123+
124+
async function validateResponse(pathname: string, match: RegExp) {
125+
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
126+
const text = await response.text();
127+
assert.match(text, match);
128+
assert.equal(response.status, 200);
129+
}
130+
}
131+
132+
async function modifyFileAndWaitUntilUpdated(
133+
filePath: string,
134+
searchValue: string,
135+
replaceValue: string,
136+
) {
137+
await Promise.all([
138+
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
139+
replaceInFile(filePath, searchValue, replaceValue),
140+
]);
141+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import assert from 'node:assert';
2+
import { replaceInFile, writeMultipleFiles } from '../../utils/fs';
3+
import { ng, silentNg, waitForAnyProcessOutputToMatch } from '../../utils/process';
4+
import { installPackage, installWorkspacePackages, uninstallPackage } from '../../utils/packages';
5+
import { ngServe, updateJsonFile, useSha } from '../../utils/project';
6+
import { getGlobalVariable } from '../../utils/env';
7+
8+
export default async function () {
9+
assert(
10+
getGlobalVariable('argv')['esbuild'],
11+
'This test should not be called in the Webpack suite.',
12+
);
13+
14+
// Forcibly remove in case another test doesn't clean itself up.
15+
await uninstallPackage('@angular/ssr');
16+
await ng('add', '@angular/ssr', '--skip-confirmation', '--skip-install');
17+
await useSha();
18+
await installWorkspacePackages();
19+
await installPackage('hono@4');
20+
21+
// Update angular.json
22+
await updateJsonFile('angular.json', (workspaceJson) => {
23+
const appArchitect = workspaceJson.projects['test-project'].architect;
24+
const options = appArchitect.build.options;
25+
options.outputMode = 'server';
26+
});
27+
28+
await writeMultipleFiles({
29+
// Replace the template of app.component.html as it makes it harder to debug
30+
'src/app/app.component.html': '<router-outlet />',
31+
'src/app/app.config.server.ts': `
32+
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
33+
import { provideServerRendering } from '@angular/platform-server';
34+
import { provideServerRoutesConfig } from '@angular/ssr';
35+
import { routes } from './app.routes.server';
36+
import { appConfig } from './app.config';
37+
38+
const serverConfig: ApplicationConfig = {
39+
providers: [
40+
provideServerRoutesConfig(routes),
41+
provideServerRendering()
42+
]
43+
};
44+
45+
export const config = mergeApplicationConfig(appConfig, serverConfig);
46+
`,
47+
'src/app/app.routes.ts': `
48+
import { Routes } from '@angular/router';
49+
import { HomeComponent } from './home/home.component';
50+
51+
export const routes: Routes = [
52+
{ path: 'home', component: HomeComponent }
53+
];
54+
`,
55+
'src/app/app.routes.server.ts': `
56+
import { RenderMode, ServerRoute } from '@angular/ssr';
57+
58+
export const routes: ServerRoute[] = [
59+
{ path: '**', renderMode: RenderMode.Server }
60+
];
61+
`,
62+
'server.ts': `
63+
import { AngularAppEngine } from '@angular/ssr';
64+
import { createWebRequestFromNodeRequest, writeResponseToNodeResponse, defineNodeNextHandler } from '@angular/ssr/node';
65+
import { Hono } from 'hono';
66+
67+
export function app() {
68+
const server = new Hono();
69+
const angularAppEngine = new AngularAppEngine();
70+
71+
server.get('/api/*', (c) => c.json({ hello: 'foo' }));
72+
server.get('/*', async (c) => {
73+
const res = await angularAppEngine.render(c.req.raw);
74+
return res || undefined
75+
});
76+
77+
return server;
78+
}
79+
80+
const server = app();
81+
export default defineNodeNextHandler(async (req, res, next) => {
82+
try {
83+
const webRes = await server.fetch(createWebRequestFromNodeRequest(req));
84+
if (webRes) {
85+
await writeResponseToNodeResponse(webRes, res);
86+
} else {
87+
next();
88+
}
89+
} catch (error) {
90+
next(error);
91+
}
92+
});
93+
`,
94+
});
95+
96+
await silentNg('generate', 'component', 'home');
97+
98+
const port = await ngServe();
99+
100+
// Verify the server is running and the API response is correct.
101+
await validateResponse('/main.js', /bootstrapApplication/);
102+
await validateResponse('/api/test', /foo/);
103+
await validateResponse('/home', /home works/);
104+
105+
// Modify the home component and validate the change.
106+
await modifyFileAndWaitUntilUpdated(
107+
'src/app/home/home.component.html',
108+
'home works',
109+
'yay home works!!!',
110+
);
111+
await validateResponse('/api/test', /foo/);
112+
await validateResponse('/home', /yay home works/);
113+
114+
// Modify the API response and validate the change.
115+
await modifyFileAndWaitUntilUpdated('server.ts', `{ hello: 'foo' }`, `{ hello: 'bar' }`);
116+
await validateResponse('/api/test', /bar/);
117+
await validateResponse('/home', /yay home works/);
118+
119+
async function validateResponse(pathname: string, match: RegExp) {
120+
const response = await fetch(new URL(pathname, `http://localhost:${port}`));
121+
const text = await response.text();
122+
assert.match(text, match);
123+
assert.equal(response.status, 200);
124+
}
125+
}
126+
127+
async function modifyFileAndWaitUntilUpdated(
128+
filePath: string,
129+
searchValue: string,
130+
replaceValue: string,
131+
) {
132+
await Promise.all([
133+
waitForAnyProcessOutputToMatch(/Application bundle generation complete./),
134+
replaceInFile(filePath, searchValue, replaceValue),
135+
]);
136+
}

0 commit comments

Comments
 (0)
Please sign in to comment.