diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index a757c79561f3..ba05db89aa65 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -55,7 +55,7 @@ export function generateAngularServerAppEngineManifest( i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], baseHref: string | undefined, ): string { - const entryPointsContent: string[] = []; + const entryPoints: Record = {}; if (i18nOptions.shouldInline) { for (const locale of i18nOptions.inlineLocales) { @@ -69,18 +69,22 @@ export function generateAngularServerAppEngineManifest( const end = localeWithBaseHref[localeWithBaseHref.length - 1] === '/' ? -1 : undefined; localeWithBaseHref = localeWithBaseHref.slice(start, end); - entryPointsContent.push(`['${localeWithBaseHref}', () => import('${importPath}')]`); + entryPoints[localeWithBaseHref] = `() => import('${importPath}')`; } } else { - entryPointsContent.push(`['', () => import('./${MAIN_SERVER_OUTPUT_FILENAME}')]`); + entryPoints[''] = `() => import('./${MAIN_SERVER_OUTPUT_FILENAME}')`; } const manifestContent = ` export default { basePath: '${baseHref ?? '/'}', - entryPoints: new Map([${entryPointsContent.join(', \n')}]), + entryPoints: { + ${Object.entries(entryPoints) + .map(([key, value]) => `'${key}': ${value}`) + .join(',\n ')} + }, }; - `; +`; return manifestContent; } @@ -122,7 +126,7 @@ export function generateAngularServerAppManifest( serverAssetsChunks: BuildOutputFile[]; } { const serverAssetsChunks: BuildOutputFile[] = []; - const serverAssetsContent: string[] = []; + const serverAssets: Record = {}; for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) { const extension = extname(file.path); if (extension === '.html' || (inlineCriticalCss && extension === '.css')) { @@ -135,9 +139,8 @@ export function generateAngularServerAppManifest( ), ); - serverAssetsContent.push( - `['${file.path}', {size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, - ); + serverAssets[file.path] = + `{size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`; } } @@ -146,9 +149,13 @@ export default { bootstrap: () => import('./main.server.mjs').then(m => m.default), inlineCriticalCss: ${inlineCriticalCss}, baseHref: '${baseHref}', - locale: ${locale !== undefined ? `'${locale}'` : undefined}, + locale: ${JSON.stringify(locale)}, routes: ${JSON.stringify(routes, undefined, 2)}, - assets: new Map([\n${serverAssetsContent.join(', \n')}\n]), + assets: { + ${Object.entries(serverAssets) + .map(([key, value]) => `'${key}': ${value}`) + .join(',\n ')} + }, }; `; diff --git a/packages/angular/ssr/src/app-engine.ts b/packages/angular/ssr/src/app-engine.ts index cda1754fcbdf..2b3b15ef7ea4 100644 --- a/packages/angular/ssr/src/app-engine.ts +++ b/packages/angular/ssr/src/app-engine.ts @@ -46,6 +46,11 @@ export class AngularAppEngine { */ private readonly manifest = getAngularAppEngineManifest(); + /** + * The number of entry points available in the server application's manifest. + */ + private readonly entryPointsCount = Object.keys(this.manifest.entryPoints).length; + /** * A cache that holds entry points, keyed by their potential locale string. */ @@ -113,7 +118,7 @@ export class AngularAppEngine { } const { entryPoints } = this.manifest; - const entryPoint = entryPoints.get(potentialLocale); + const entryPoint = entryPoints[potentialLocale]; if (!entryPoint) { return undefined; } @@ -136,8 +141,8 @@ export class AngularAppEngine { * @returns A promise that resolves to the entry point exports or `undefined` if not found. */ private getEntryPointExportsForUrl(url: URL): Promise | undefined { - const { entryPoints, basePath } = this.manifest; - if (entryPoints.size === 1) { + const { basePath } = this.manifest; + if (this.entryPointsCount === 1) { return this.getEntryPointExports(''); } diff --git a/packages/angular/ssr/src/assets.ts b/packages/angular/ssr/src/assets.ts index 5e72647b0bdc..934d0c7cd3ae 100644 --- a/packages/angular/ssr/src/assets.ts +++ b/packages/angular/ssr/src/assets.ts @@ -27,7 +27,7 @@ export class ServerAssets { * @throws Error - Throws an error if the asset does not exist. */ getServerAsset(path: string): ServerAsset { - const asset = this.manifest.assets.get(path); + const asset = this.manifest.assets[path]; if (!asset) { throw new Error(`Server asset '${path}' does not exist.`); } @@ -42,7 +42,7 @@ export class ServerAssets { * @returns A boolean indicating whether the asset exists. */ hasServerAsset(path: string): boolean { - return this.manifest.assets.has(path); + return !!this.manifest.assets[path]; } /** diff --git a/packages/angular/ssr/src/manifest.ts b/packages/angular/ssr/src/manifest.ts index f18aa01af4ea..2c0d642ec2ae 100644 --- a/packages/angular/ssr/src/manifest.ts +++ b/packages/angular/ssr/src/manifest.ts @@ -10,7 +10,7 @@ import type { SerializableRouteTreeNode } from './routes/route-tree'; import { AngularBootstrap } from './utils/ng'; /** - * Represents of a server asset stored in the manifest. + * Represents a server asset stored in the manifest. */ export interface ServerAsset { /** @@ -53,12 +53,12 @@ export interface EntryPointExports { */ export interface AngularAppEngineManifest { /** - * A map of entry points for the server application. - * Each entry in the map consists of: + * A readonly record of entry points for the server application. + * Each entry consists of: * - `key`: The base href for the entry point. * - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`. */ - readonly entryPoints: ReadonlyMap Promise>; + readonly entryPoints: Readonly Promise) | undefined>>; /** * The base path for the server application. @@ -78,12 +78,12 @@ export interface AngularAppManifest { readonly baseHref: string; /** - * A map of assets required by the server application. - * Each entry in the map consists of: + * A readonly record of assets required by the server application. + * Each entry consists of: * - `key`: The path of the asset. - * - `value`: A function returning a promise that resolves to the file contents of the asset. + * - `value`: An object of type `ServerAsset`. */ - readonly assets: ReadonlyMap; + readonly assets: Readonly>; /** * The bootstrap mechanism for the server application. diff --git a/packages/angular/ssr/test/app-engine_spec.ts b/packages/angular/ssr/test/app-engine_spec.ts index fd37fb5b27ee..df405703a5bf 100644 --- a/packages/angular/ssr/test/app-engine_spec.ts +++ b/packages/angular/ssr/test/app-engine_spec.ts @@ -18,6 +18,57 @@ import { setAngularAppEngineManifest } from '../src/manifest'; import { RenderMode } from '../src/routes/route-config'; import { setAngularAppTestingManifest } from './testing-utils'; +function createEntryPoint(locale: string) { + return async () => { + @Component({ + standalone: true, + selector: `app-ssr-${locale}`, + template: `SSR works ${locale.toUpperCase()}`, + }) + class SSRComponent {} + + @Component({ + standalone: true, + selector: `app-ssg-${locale}`, + template: `SSG works ${locale.toUpperCase()}`, + }) + class SSGComponent {} + + setAngularAppTestingManifest( + [ + { path: 'ssg', component: SSGComponent }, + { path: 'ssr', component: SSRComponent }, + ], + [ + { path: 'ssg', renderMode: RenderMode.Prerender }, + { path: '**', renderMode: RenderMode.Server }, + ], + '/' + locale, + { + 'ssg/index.html': { + size: 25, + hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', + text: async () => ` + + SSG page + + + + SSG works ${locale.toUpperCase()} + + + `, + }, + }, + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }; +} + describe('AngularAppEngine', () => { let appEngine: AngularAppEngine; @@ -28,59 +79,10 @@ describe('AngularAppEngine', () => { setAngularAppEngineManifest({ // Note: Although we are testing only one locale, we need to configure two or more // to ensure that we test a different code path. - entryPoints: new Map( - ['it', 'en'].map((locale) => [ - locale, - async () => { - @Component({ - standalone: true, - selector: `app-ssr-${locale}`, - template: `SSR works ${locale.toUpperCase()}`, - }) - class SSRComponent {} - - @Component({ - standalone: true, - selector: `app-ssg-${locale}`, - template: `SSG works ${locale.toUpperCase()}`, - }) - class SSGComponent {} - - setAngularAppTestingManifest( - [ - { path: 'ssg', component: SSGComponent }, - { path: 'ssr', component: SSRComponent }, - ], - [ - { path: 'ssg', renderMode: RenderMode.Prerender }, - { path: '**', renderMode: RenderMode.Server }, - ], - '/' + locale, - { - 'ssg/index.html': { - size: 25, - hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', - text: async () => ` - - SSG page - - - - SSG works ${locale.toUpperCase()} - - - `, - }, - }, - ); - - return { - ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, - ɵdestroyAngularServerApp: destroyAngularServerApp, - }; - }, - ]), - ), + entryPoints: { + it: createEntryPoint('it'), + en: createEntryPoint('en'), + }, basePath: '', }); @@ -143,29 +145,26 @@ describe('AngularAppEngine', () => { destroyAngularServerApp(); setAngularAppEngineManifest({ - entryPoints: new Map([ - [ - '', - async () => { - @Component({ - standalone: true, - selector: 'app-home', - template: `Home works`, - }) - class HomeComponent {} - - setAngularAppTestingManifest( - [{ path: 'home', component: HomeComponent }], - [{ path: '**', renderMode: RenderMode.Server }], - ); - - return { - ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, - ɵdestroyAngularServerApp: destroyAngularServerApp, - }; - }, - ], - ]), + entryPoints: { + '': async () => { + @Component({ + standalone: true, + selector: 'app-home', + template: `Home works`, + }) + class HomeComponent {} + + setAngularAppTestingManifest( + [{ path: 'home', component: HomeComponent }], + [{ path: '**', renderMode: RenderMode.Server }], + ); + + return { + ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, + ɵdestroyAngularServerApp: destroyAngularServerApp, + }; + }, + }, basePath: '', }); diff --git a/packages/angular/ssr/test/assets_spec.ts b/packages/angular/ssr/test/assets_spec.ts index fa794f4d9317..b5633cbad52d 100644 --- a/packages/angular/ssr/test/assets_spec.ts +++ b/packages/angular/ssr/test/assets_spec.ts @@ -15,20 +15,18 @@ describe('ServerAsset', () => { assetManager = new ServerAssets({ baseHref: '/', bootstrap: undefined as never, - assets: new Map( - Object.entries({ - 'index.server.html': { - text: async () => 'Index', - size: 18, - hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', - }, - 'index.other.html': { - text: async () => 'Other', - size: 18, - hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3', - }, - }), - ), + assets: { + 'index.server.html': { + text: async () => 'Index', + size: 18, + hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', + }, + 'index.other.html': { + text: async () => 'Other', + size: 18, + hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3', + }, + }, }); }); diff --git a/packages/angular/ssr/test/testing-utils.ts b/packages/angular/ssr/test/testing-utils.ts index 9c48479fe038..ff6803df58a8 100644 --- a/packages/angular/ssr/test/testing-utils.ts +++ b/packages/angular/ssr/test/testing-utils.ts @@ -32,40 +32,38 @@ export function setAngularAppTestingManifest( setAngularAppManifest({ inlineCriticalCss: false, baseHref, - assets: new Map( - Object.entries({ - ...additionalServerAssets, - 'index.server.html': { - size: 25, - hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', - text: async () => ` - - SSR page - - - - - - - `, - }, - 'index.csr.html': { - size: 25, - hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', - text: async () => - ` - - CSR page - - - - - - - `, - }, - }), - ), + assets: { + ...additionalServerAssets, + 'index.server.html': { + size: 25, + hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', + text: async () => ` + + SSR page + + + + + + + `, + }, + 'index.csr.html': { + size: 25, + hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde', + text: async () => + ` + + CSR page + + + + + + + `, + }, + }, bootstrap: async () => () => { @Component({ standalone: true,