From 893a1a7fd5bf6b6e207839cb7618222f9d6240b3 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Fri, 6 Sep 2024 07:25:50 +0000 Subject: [PATCH] fix(@angular/ssr): resolve circular dependency issue from main.server.js reference in manifest The issue was addressed by changing the top-level import to a dynamic import. Closes #28358 --- .../src/utils/server-rendering/manifest.ts | 6 ++-- packages/angular/ssr/src/app.ts | 11 ++++-- packages/angular/ssr/src/manifest.ts | 5 +-- packages/angular/ssr/src/routes/ng-routes.ts | 2 +- packages/angular/ssr/test/testing-utils.ts | 2 +- .../build/ssr/express-engine-csp-nonce.ts | 29 ++++++++------- .../build/ssr/express-engine-ngmodule.ts | 35 ++++++++++--------- .../build/ssr/express-engine-standalone.ts | 26 ++++++++------ 8 files changed, 66 insertions(+), 50 deletions(-) diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index d3a5519adf77..89a6f92f3387 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -89,17 +89,15 @@ export function generateAngularServerAppManifest( if ( file.path === INDEX_HTML_SERVER || file.path === INDEX_HTML_CSR || - file.path.endsWith('.css') + (inlineCriticalCss && file.path.endsWith('.css')) ) { serverAssetsContent.push(`['${file.path}', async () => ${JSON.stringify(file.text)}]`); } } const manifestContent = ` -import bootstrap from './main.server.mjs'; - export default { - bootstrap: () => bootstrap, + bootstrap: () => import('./main.server.mjs').then(m => m.default), inlineCriticalCss: ${inlineCriticalCss}, routes: ${JSON.stringify(routes, undefined, 2)}, assets: new Map([${serverAssetsContent.join(', \n')}]), diff --git a/packages/angular/ssr/src/app.ts b/packages/angular/ssr/src/app.ts index ec64dcdbbf75..4fd81077ec8e 100644 --- a/packages/angular/ssr/src/app.ts +++ b/packages/angular/ssr/src/app.ts @@ -15,7 +15,7 @@ import { getAngularAppManifest } from './manifest'; import { ServerRouter } from './routes/router'; import { REQUEST, REQUEST_CONTEXT, RESPONSE_INIT } from './tokens'; import { InlineCriticalCssProcessor } from './utils/inline-critical-css'; -import { renderAngular } from './utils/ng'; +import { AngularBootstrap, renderAngular } from './utils/ng'; /** * Enum representing the different contexts in which server rendering can occur. @@ -58,6 +58,11 @@ export class AngularServerApp { */ private inlineCriticalCssProcessor: InlineCriticalCssProcessor | undefined; + /** + * The bootstrap mechanism for the server application. + */ + private boostrap: AngularBootstrap | undefined; + /** * Renders a response for the given HTTP request using the server application. * @@ -176,7 +181,9 @@ export class AngularServerApp { html = await hooks.run('html:transform:pre', { html }); } - html = await renderAngular(html, manifest.bootstrap(), new URL(request.url), platformProviders); + this.boostrap ??= await manifest.bootstrap(); + + html = await renderAngular(html, this.boostrap, new URL(request.url), platformProviders); if (manifest.inlineCriticalCss) { // Optionally inline critical CSS. diff --git a/packages/angular/ssr/src/manifest.ts b/packages/angular/ssr/src/manifest.ts index 0ab6e5f439d5..4e29d8f76b8d 100644 --- a/packages/angular/ssr/src/manifest.ts +++ b/packages/angular/ssr/src/manifest.ts @@ -71,9 +71,10 @@ export interface AngularAppManifest { /** * The bootstrap mechanism for the server application. - * A function that returns a reference to an NgModule or a function returning a promise that resolves to an ApplicationRef. + * A function that returns a promise that resolves to an `NgModule` or a function + * returning a promise that resolves to an `ApplicationRef`. */ - readonly bootstrap: () => AngularBootstrap; + readonly bootstrap: () => Promise; /** * Indicates whether critical CSS should be inlined into the HTML. diff --git a/packages/angular/ssr/src/routes/ng-routes.ts b/packages/angular/ssr/src/routes/ng-routes.ts index d2ef31608752..2c3484d3f404 100644 --- a/packages/angular/ssr/src/routes/ng-routes.ts +++ b/packages/angular/ssr/src/routes/ng-routes.ts @@ -274,7 +274,7 @@ export async function extractRoutesAndCreateRouteTree( const routeTree = new RouteTree(); const document = await new ServerAssets(manifest).getIndexServerHtml(); const { baseHref, routes } = await getRoutesFromAngularRouterConfig( - manifest.bootstrap(), + await manifest.bootstrap(), document, url, ); diff --git a/packages/angular/ssr/test/testing-utils.ts b/packages/angular/ssr/test/testing-utils.ts index dea7b2f9f80b..7eb0ddf7683d 100644 --- a/packages/angular/ssr/test/testing-utils.ts +++ b/packages/angular/ssr/test/testing-utils.ts @@ -38,7 +38,7 @@ export function setAngularAppTestingManifest(routes: Routes, baseHref = ''): voi `, }), ), - bootstrap: () => () => { + bootstrap: async () => () => { @Component({ standalone: true, selector: 'app-root', diff --git a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-csp-nonce.ts index fcdc210fbc25..612407cbfb11 100644 --- a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-csp-nonce.ts @@ -136,25 +136,30 @@ export default async function () { `, }); - async function ngDevSsr(): Promise { + async function spawnServer(): Promise { const port = await findFreePort(); - const useWebpackBuilder = !getGlobalVariable('argv')['esbuild']; - const validBundleRegEx = useWebpackBuilder ? /Compiled successfully\./ : /complete\./; + + const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; await execAndWaitForOutputToMatch( - 'ng', - [ - 'run', - `test-project:${useWebpackBuilder ? 'serve-ssr' : 'serve'}:production`, - '--port', - String(port), - ], - validBundleRegEx, + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + 'PORT': String(port), + }, ); return port; } - const port = await ngDevSsr(); + await ng('build'); + + if (useWebpackBuilder) { + // Build server code + await ng('run', 'test-project:server'); + } + + const port = await spawnServer(); await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); } diff --git a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-ngmodule.ts index 22cb864e1a8b..47d6044408ad 100644 --- a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-ngmodule.ts @@ -141,29 +141,30 @@ export default async function () { `, }); - async function ngDevSsr(): Promise { + async function spawnServer(): Promise { const port = await findFreePort(); - const validBundleRegEx = useWebpackBuilder ? /Compiled successfully\./ : /complete\./; + + const runCommand = useWebpackBuilder ? 'serve:ssr' : `serve:ssr:test-project-two`; await execAndWaitForOutputToMatch( - 'ng', - [ - 'run', - `test-project-two:${useWebpackBuilder ? 'serve-ssr' : 'serve'}:production`, - '--port', - String(port), - ], - validBundleRegEx, + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + 'PORT': String(port), + }, ); return port; } - const port = await ngDevSsr(); - await ng( - 'e2e', - 'test-project-two', - `--base-url=http://localhost:${port}`, - '--dev-server-target=', - ); + await ng('build'); + + if (useWebpackBuilder) { + // Build server code + await ng('run', `test-project-two:server`); + } + + const port = await spawnServer(); + await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); } diff --git a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-standalone.ts index 23abf7dfcdb9..cb51de4e6ac8 100644 --- a/tests/legacy-cli/e2e/tests/build/ssr/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/build/ssr/express-engine-standalone.ts @@ -106,24 +106,28 @@ export default async function () { `, }); - async function ngDevSsr(): Promise { + async function spawnServer(): Promise { const port = await findFreePort(); - const validBundleRegEx = useWebpackBuilder ? /Compiled successfully\./ : /complete\./; + const runCommand = useWebpackBuilder ? 'serve:ssr' : 'serve:ssr:test-project'; await execAndWaitForOutputToMatch( - 'ng', - [ - 'run', - `test-project:${useWebpackBuilder ? 'serve-ssr' : 'serve'}:production`, - '--port', - String(port), - ], - validBundleRegEx, + 'npm', + ['run', runCommand], + /Node Express server listening on/, + { + 'PORT': String(port), + }, ); return port; } - const port = await ngDevSsr(); + await ng('build'); + if (useWebpackBuilder) { + // Build server code + await ng('run', `test-project:server`); + } + + const port = await spawnServer(); await ng('e2e', `--base-url=http://localhost:${port}`, '--dev-server-target='); }