refactor(@angular/ssr): replace Map with Record in SSR manifest

Replaced `Map` with `Record` in SSR manifest to simplify structure and improve testing/setup.
This commit is contained in:
Alan Agius 2024-12-04 08:49:37 +00:00 committed by Alan Agius
parent 480cba5fc5
commit 4db4dd4315
7 changed files with 153 additions and 146 deletions

View File

@ -55,7 +55,7 @@ export function generateAngularServerAppEngineManifest(
i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'],
baseHref: string | undefined, baseHref: string | undefined,
): string { ): string {
const entryPointsContent: string[] = []; const entryPoints: Record<string, string> = {};
if (i18nOptions.shouldInline) { if (i18nOptions.shouldInline) {
for (const locale of i18nOptions.inlineLocales) { for (const locale of i18nOptions.inlineLocales) {
@ -69,16 +69,20 @@ export function generateAngularServerAppEngineManifest(
const end = localeWithBaseHref[localeWithBaseHref.length - 1] === '/' ? -1 : undefined; const end = localeWithBaseHref[localeWithBaseHref.length - 1] === '/' ? -1 : undefined;
localeWithBaseHref = localeWithBaseHref.slice(start, end); localeWithBaseHref = localeWithBaseHref.slice(start, end);
entryPointsContent.push(`['${localeWithBaseHref}', () => import('${importPath}')]`); entryPoints[localeWithBaseHref] = `() => import('${importPath}')`;
} }
} else { } else {
entryPointsContent.push(`['', () => import('./${MAIN_SERVER_OUTPUT_FILENAME}')]`); entryPoints[''] = `() => import('./${MAIN_SERVER_OUTPUT_FILENAME}')`;
} }
const manifestContent = ` const manifestContent = `
export default { export default {
basePath: '${baseHref ?? '/'}', basePath: '${baseHref ?? '/'}',
entryPoints: new Map([${entryPointsContent.join(', \n')}]), entryPoints: {
${Object.entries(entryPoints)
.map(([key, value]) => `'${key}': ${value}`)
.join(',\n ')}
},
}; };
`; `;
@ -122,7 +126,7 @@ export function generateAngularServerAppManifest(
serverAssetsChunks: BuildOutputFile[]; serverAssetsChunks: BuildOutputFile[];
} { } {
const serverAssetsChunks: BuildOutputFile[] = []; const serverAssetsChunks: BuildOutputFile[] = [];
const serverAssetsContent: string[] = []; const serverAssets: Record<string, string> = {};
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) { for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
const extension = extname(file.path); const extension = extname(file.path);
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) { if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
@ -135,9 +139,8 @@ export function generateAngularServerAppManifest(
), ),
); );
serverAssetsContent.push( serverAssets[file.path] =
`['${file.path}', {size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, `{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), bootstrap: () => import('./main.server.mjs').then(m => m.default),
inlineCriticalCss: ${inlineCriticalCss}, inlineCriticalCss: ${inlineCriticalCss},
baseHref: '${baseHref}', baseHref: '${baseHref}',
locale: ${locale !== undefined ? `'${locale}'` : undefined}, locale: ${JSON.stringify(locale)},
routes: ${JSON.stringify(routes, undefined, 2)}, 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 ')}
},
}; };
`; `;

View File

@ -46,6 +46,11 @@ export class AngularAppEngine {
*/ */
private readonly manifest = getAngularAppEngineManifest(); 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. * A cache that holds entry points, keyed by their potential locale string.
*/ */
@ -113,7 +118,7 @@ export class AngularAppEngine {
} }
const { entryPoints } = this.manifest; const { entryPoints } = this.manifest;
const entryPoint = entryPoints.get(potentialLocale); const entryPoint = entryPoints[potentialLocale];
if (!entryPoint) { if (!entryPoint) {
return undefined; return undefined;
} }
@ -136,8 +141,8 @@ export class AngularAppEngine {
* @returns A promise that resolves to the entry point exports or `undefined` if not found. * @returns A promise that resolves to the entry point exports or `undefined` if not found.
*/ */
private getEntryPointExportsForUrl(url: URL): Promise<EntryPointExports> | undefined { private getEntryPointExportsForUrl(url: URL): Promise<EntryPointExports> | undefined {
const { entryPoints, basePath } = this.manifest; const { basePath } = this.manifest;
if (entryPoints.size === 1) { if (this.entryPointsCount === 1) {
return this.getEntryPointExports(''); return this.getEntryPointExports('');
} }

View File

@ -27,7 +27,7 @@ export class ServerAssets {
* @throws Error - Throws an error if the asset does not exist. * @throws Error - Throws an error if the asset does not exist.
*/ */
getServerAsset(path: string): ServerAsset { getServerAsset(path: string): ServerAsset {
const asset = this.manifest.assets.get(path); const asset = this.manifest.assets[path];
if (!asset) { if (!asset) {
throw new Error(`Server asset '${path}' does not exist.`); throw new Error(`Server asset '${path}' does not exist.`);
} }
@ -42,7 +42,7 @@ export class ServerAssets {
* @returns A boolean indicating whether the asset exists. * @returns A boolean indicating whether the asset exists.
*/ */
hasServerAsset(path: string): boolean { hasServerAsset(path: string): boolean {
return this.manifest.assets.has(path); return !!this.manifest.assets[path];
} }
/** /**

View File

@ -10,7 +10,7 @@ import type { SerializableRouteTreeNode } from './routes/route-tree';
import { AngularBootstrap } from './utils/ng'; 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 { export interface ServerAsset {
/** /**
@ -53,12 +53,12 @@ export interface EntryPointExports {
*/ */
export interface AngularAppEngineManifest { export interface AngularAppEngineManifest {
/** /**
* A map of entry points for the server application. * A readonly record of entry points for the server application.
* Each entry in the map consists of: * Each entry consists of:
* - `key`: The base href for the entry point. * - `key`: The base href for the entry point.
* - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`. * - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`.
*/ */
readonly entryPoints: ReadonlyMap<string, () => Promise<EntryPointExports>>; readonly entryPoints: Readonly<Record<string, (() => Promise<EntryPointExports>) | undefined>>;
/** /**
* The base path for the server application. * The base path for the server application.
@ -78,12 +78,12 @@ export interface AngularAppManifest {
readonly baseHref: string; readonly baseHref: string;
/** /**
* A map of assets required by the server application. * A readonly record of assets required by the server application.
* Each entry in the map consists of: * Each entry consists of:
* - `key`: The path of the asset. * - `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<string, ServerAsset>; readonly assets: Readonly<Record<string, ServerAsset | undefined>>;
/** /**
* The bootstrap mechanism for the server application. * The bootstrap mechanism for the server application.

View File

@ -18,20 +18,8 @@ import { setAngularAppEngineManifest } from '../src/manifest';
import { RenderMode } from '../src/routes/route-config'; import { RenderMode } from '../src/routes/route-config';
import { setAngularAppTestingManifest } from './testing-utils'; import { setAngularAppTestingManifest } from './testing-utils';
describe('AngularAppEngine', () => { function createEntryPoint(locale: string) {
let appEngine: AngularAppEngine; return async () => {
describe('Localized app', () => {
beforeAll(() => {
destroyAngularServerApp();
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({ @Component({
standalone: true, standalone: true,
selector: `app-ssr-${locale}`, selector: `app-ssr-${locale}`,
@ -78,9 +66,23 @@ describe('AngularAppEngine', () => {
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp, ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
ɵdestroyAngularServerApp: destroyAngularServerApp, ɵdestroyAngularServerApp: destroyAngularServerApp,
}; };
};
}
describe('AngularAppEngine', () => {
let appEngine: AngularAppEngine;
describe('Localized app', () => {
beforeAll(() => {
destroyAngularServerApp();
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: {
it: createEntryPoint('it'),
en: createEntryPoint('en'),
}, },
]),
),
basePath: '', basePath: '',
}); });
@ -143,10 +145,8 @@ describe('AngularAppEngine', () => {
destroyAngularServerApp(); destroyAngularServerApp();
setAngularAppEngineManifest({ setAngularAppEngineManifest({
entryPoints: new Map([ entryPoints: {
[ '': async () => {
'',
async () => {
@Component({ @Component({
standalone: true, standalone: true,
selector: 'app-home', selector: 'app-home',
@ -164,8 +164,7 @@ describe('AngularAppEngine', () => {
ɵdestroyAngularServerApp: destroyAngularServerApp, ɵdestroyAngularServerApp: destroyAngularServerApp,
}; };
}, },
], },
]),
basePath: '', basePath: '',
}); });

View File

@ -15,8 +15,7 @@ describe('ServerAsset', () => {
assetManager = new ServerAssets({ assetManager = new ServerAssets({
baseHref: '/', baseHref: '/',
bootstrap: undefined as never, bootstrap: undefined as never,
assets: new Map( assets: {
Object.entries({
'index.server.html': { 'index.server.html': {
text: async () => '<html>Index</html>', text: async () => '<html>Index</html>',
size: 18, size: 18,
@ -27,8 +26,7 @@ describe('ServerAsset', () => {
size: 18, size: 18,
hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3', hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3',
}, },
}), },
),
}); });
}); });

View File

@ -32,8 +32,7 @@ export function setAngularAppTestingManifest(
setAngularAppManifest({ setAngularAppManifest({
inlineCriticalCss: false, inlineCriticalCss: false,
baseHref, baseHref,
assets: new Map( assets: {
Object.entries({
...additionalServerAssets, ...additionalServerAssets,
'index.server.html': { 'index.server.html': {
size: 25, size: 25,
@ -64,8 +63,7 @@ export function setAngularAppTestingManifest(
</html> </html>
`, `,
}, },
}), },
),
bootstrap: async () => () => { bootstrap: async () => () => {
@Component({ @Component({
standalone: true, standalone: true,