mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-22 15:02:11 +08:00
This commit enables server-side rendering (SSR) in Vite when prerendering is turned off. It also imports `@angular/compiler` in the SSR middleware to resolve the following issue: ``` [vite] Internal server error: The injectable 'PlatformNavigation' needs to be compiled using the JIT compiler, but '@angular/compiler' is not available. The injectable is part of a library that has been partially compiled. However, the Angular Linker has not processed the library to utilize JIT compilation as a fallback. Ideally, the library should be processed with the Angular Linker for complete AOT compilation. ``` Closes #28523
148 lines
4.9 KiB
TypeScript
148 lines
4.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.dev/license
|
|
*/
|
|
|
|
import type {
|
|
AngularAppEngine as SSRAngularAppEngine,
|
|
ɵgetOrCreateAngularServerApp as getOrCreateAngularServerApp,
|
|
} from '@angular/ssr';
|
|
import type { ServerResponse } from 'node:http';
|
|
import type { Connect, ViteDevServer } from 'vite';
|
|
import { loadEsmModule } from '../../../utils/load-esm';
|
|
import {
|
|
isSsrNodeRequestHandler,
|
|
isSsrRequestHandler,
|
|
} from '../../../utils/server-rendering/utils';
|
|
|
|
export function createAngularSsrInternalMiddleware(
|
|
server: ViteDevServer,
|
|
indexHtmlTransformer?: (content: string) => Promise<string>,
|
|
): Connect.NextHandleFunction {
|
|
let cachedAngularServerApp: ReturnType<typeof getOrCreateAngularServerApp> | undefined;
|
|
|
|
return function angularSsrMiddleware(
|
|
req: Connect.IncomingMessage,
|
|
res: ServerResponse,
|
|
next: Connect.NextFunction,
|
|
) {
|
|
if (req.url === undefined) {
|
|
return next();
|
|
}
|
|
|
|
(async () => {
|
|
// Load the compiler because `@angular/ssr/node` depends on `@angular/` packages,
|
|
// which must be processed by the runtime linker, even if they are not used.
|
|
await loadEsmModule('@angular/compiler');
|
|
const { writeResponseToNodeResponse, createWebRequestFromNodeRequest } =
|
|
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
|
|
|
|
const { ɵgetOrCreateAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs')) as {
|
|
ɵgetOrCreateAngularServerApp: typeof getOrCreateAngularServerApp;
|
|
};
|
|
|
|
const angularServerApp = ɵgetOrCreateAngularServerApp();
|
|
// Only Add the transform hook only if it's a different instance.
|
|
if (cachedAngularServerApp !== angularServerApp) {
|
|
angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
|
|
const processedHtml = await server.transformIndexHtml(url.pathname, html);
|
|
|
|
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
|
|
});
|
|
|
|
cachedAngularServerApp = angularServerApp;
|
|
}
|
|
|
|
const webReq = new Request(createWebRequestFromNodeRequest(req), {
|
|
signal: AbortSignal.timeout(30_000),
|
|
});
|
|
const webRes = await angularServerApp.render(webReq);
|
|
if (!webRes) {
|
|
return next();
|
|
}
|
|
|
|
return writeResponseToNodeResponse(webRes, res);
|
|
})().catch(next);
|
|
};
|
|
}
|
|
|
|
export async function createAngularSsrExternalMiddleware(
|
|
server: ViteDevServer,
|
|
indexHtmlTransformer?: (content: string) => Promise<string>,
|
|
): Promise<Connect.NextHandleFunction> {
|
|
let fallbackWarningShown = false;
|
|
let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
|
|
let angularSsrInternalMiddleware:
|
|
| ReturnType<typeof createAngularSsrInternalMiddleware>
|
|
| undefined;
|
|
|
|
// Load the compiler because `@angular/ssr/node` depends on `@angular/` packages,
|
|
// which must be processed by the runtime linker, even if they are not used.
|
|
await loadEsmModule('@angular/compiler');
|
|
|
|
const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } =
|
|
await loadEsmModule<typeof import('@angular/ssr/node')>('@angular/ssr/node');
|
|
|
|
return function angularSsrExternalMiddleware(
|
|
req: Connect.IncomingMessage,
|
|
res: ServerResponse,
|
|
next: Connect.NextFunction,
|
|
) {
|
|
(async () => {
|
|
const { default: handler, AngularAppEngine } = (await server.ssrLoadModule(
|
|
'./server.mjs',
|
|
)) as {
|
|
default?: unknown;
|
|
AngularAppEngine: typeof SSRAngularAppEngine;
|
|
};
|
|
|
|
if (!isSsrNodeRequestHandler(handler) && !isSsrRequestHandler(handler)) {
|
|
if (!fallbackWarningShown) {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(
|
|
`The default export in 'server.ts' does not provide a Node.js request handler. ` +
|
|
'Using the internal SSR middleware instead.',
|
|
);
|
|
fallbackWarningShown = true;
|
|
}
|
|
|
|
angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware(
|
|
server,
|
|
indexHtmlTransformer,
|
|
);
|
|
|
|
angularSsrInternalMiddleware(req, res, next);
|
|
|
|
return;
|
|
}
|
|
|
|
if (cachedAngularAppEngine !== AngularAppEngine) {
|
|
AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
|
|
const processedHtml = await server.transformIndexHtml(url.pathname, html);
|
|
|
|
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
|
|
});
|
|
|
|
cachedAngularAppEngine = AngularAppEngine;
|
|
}
|
|
|
|
// Forward the request to the middleware in server.ts
|
|
if (isSsrNodeRequestHandler(handler)) {
|
|
await handler(req, res, next);
|
|
} else {
|
|
const webRes = await handler(createWebRequestFromNodeRequest(req));
|
|
if (!webRes) {
|
|
next();
|
|
|
|
return;
|
|
}
|
|
|
|
await writeResponseToNodeResponse(webRes, res);
|
|
}
|
|
})().catch(next);
|
|
};
|
|
}
|