Skip to content

Commit 4db4dd4

Browse files
committed
refactor(@angular/ssr): replace Map with Record in SSR manifest
Replaced `Map` with `Record` in SSR manifest to simplify structure and improve testing/setup.
1 parent 480cba5 commit 4db4dd4

File tree

7 files changed

+155
-148
lines changed

7 files changed

+155
-148
lines changed

‎packages/angular/build/src/utils/server-rendering/manifest.ts

+18-11
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export function generateAngularServerAppEngineManifest(
5555
i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'],
5656
baseHref: string | undefined,
5757
): string {
58-
const entryPointsContent: string[] = [];
58+
const entryPoints: Record<string, string> = {};
5959

6060
if (i18nOptions.shouldInline) {
6161
for (const locale of i18nOptions.inlineLocales) {
@@ -69,18 +69,22 @@ export function generateAngularServerAppEngineManifest(
6969
const end = localeWithBaseHref[localeWithBaseHref.length - 1] === '/' ? -1 : undefined;
7070
localeWithBaseHref = localeWithBaseHref.slice(start, end);
7171

72-
entryPointsContent.push(`['${localeWithBaseHref}', () => import('${importPath}')]`);
72+
entryPoints[localeWithBaseHref] = `() => import('${importPath}')`;
7373
}
7474
} else {
75-
entryPointsContent.push(`['', () => import('./${MAIN_SERVER_OUTPUT_FILENAME}')]`);
75+
entryPoints[''] = `() => import('./${MAIN_SERVER_OUTPUT_FILENAME}')`;
7676
}
7777

7878
const manifestContent = `
7979
export default {
8080
basePath: '${baseHref ?? '/'}',
81-
entryPoints: new Map([${entryPointsContent.join(', \n')}]),
81+
entryPoints: {
82+
${Object.entries(entryPoints)
83+
.map(([key, value]) => `'${key}': ${value}`)
84+
.join(',\n ')}
85+
},
8286
};
83-
`;
87+
`;
8488

8589
return manifestContent;
8690
}
@@ -122,7 +126,7 @@ export function generateAngularServerAppManifest(
122126
serverAssetsChunks: BuildOutputFile[];
123127
} {
124128
const serverAssetsChunks: BuildOutputFile[] = [];
125-
const serverAssetsContent: string[] = [];
129+
const serverAssets: Record<string, string> = {};
126130
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
127131
const extension = extname(file.path);
128132
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
@@ -135,9 +139,8 @@ export function generateAngularServerAppManifest(
135139
),
136140
);
137141

138-
serverAssetsContent.push(
139-
`['${file.path}', {size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`,
140-
);
142+
serverAssets[file.path] =
143+
`{size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`;
141144
}
142145
}
143146

@@ -146,9 +149,13 @@ export default {
146149
bootstrap: () => import('./main.server.mjs').then(m => m.default),
147150
inlineCriticalCss: ${inlineCriticalCss},
148151
baseHref: '${baseHref}',
149-
locale: ${locale !== undefined ? `'${locale}'` : undefined},
152+
locale: ${JSON.stringify(locale)},
150153
routes: ${JSON.stringify(routes, undefined, 2)},
151-
assets: new Map([\n${serverAssetsContent.join(', \n')}\n]),
154+
assets: {
155+
${Object.entries(serverAssets)
156+
.map(([key, value]) => `'${key}': ${value}`)
157+
.join(',\n ')}
158+
},
152159
};
153160
`;
154161

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export class AngularAppEngine {
4646
*/
4747
private readonly manifest = getAngularAppEngineManifest();
4848

49+
/**
50+
* The number of entry points available in the server application's manifest.
51+
*/
52+
private readonly entryPointsCount = Object.keys(this.manifest.entryPoints).length;
53+
4954
/**
5055
* A cache that holds entry points, keyed by their potential locale string.
5156
*/
@@ -113,7 +118,7 @@ export class AngularAppEngine {
113118
}
114119

115120
const { entryPoints } = this.manifest;
116-
const entryPoint = entryPoints.get(potentialLocale);
121+
const entryPoint = entryPoints[potentialLocale];
117122
if (!entryPoint) {
118123
return undefined;
119124
}
@@ -136,8 +141,8 @@ export class AngularAppEngine {
136141
* @returns A promise that resolves to the entry point exports or `undefined` if not found.
137142
*/
138143
private getEntryPointExportsForUrl(url: URL): Promise<EntryPointExports> | undefined {
139-
const { entryPoints, basePath } = this.manifest;
140-
if (entryPoints.size === 1) {
144+
const { basePath } = this.manifest;
145+
if (this.entryPointsCount === 1) {
141146
return this.getEntryPointExports('');
142147
}
143148

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export class ServerAssets {
2727
* @throws Error - Throws an error if the asset does not exist.
2828
*/
2929
getServerAsset(path: string): ServerAsset {
30-
const asset = this.manifest.assets.get(path);
30+
const asset = this.manifest.assets[path];
3131
if (!asset) {
3232
throw new Error(`Server asset '${path}' does not exist.`);
3333
}
@@ -42,7 +42,7 @@ export class ServerAssets {
4242
* @returns A boolean indicating whether the asset exists.
4343
*/
4444
hasServerAsset(path: string): boolean {
45-
return this.manifest.assets.has(path);
45+
return !!this.manifest.assets[path];
4646
}
4747

4848
/**

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type { SerializableRouteTreeNode } from './routes/route-tree';
1010
import { AngularBootstrap } from './utils/ng';
1111

1212
/**
13-
* Represents of a server asset stored in the manifest.
13+
* Represents a server asset stored in the manifest.
1414
*/
1515
export interface ServerAsset {
1616
/**
@@ -53,12 +53,12 @@ export interface EntryPointExports {
5353
*/
5454
export interface AngularAppEngineManifest {
5555
/**
56-
* A map of entry points for the server application.
57-
* Each entry in the map consists of:
56+
* A readonly record of entry points for the server application.
57+
* Each entry consists of:
5858
* - `key`: The base href for the entry point.
5959
* - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`.
6060
*/
61-
readonly entryPoints: ReadonlyMap<string, () => Promise<EntryPointExports>>;
61+
readonly entryPoints: Readonly<Record<string, (() => Promise<EntryPointExports>) | undefined>>;
6262

6363
/**
6464
* The base path for the server application.
@@ -78,12 +78,12 @@ export interface AngularAppManifest {
7878
readonly baseHref: string;
7979

8080
/**
81-
* A map of assets required by the server application.
82-
* Each entry in the map consists of:
81+
* A readonly record of assets required by the server application.
82+
* Each entry consists of:
8383
* - `key`: The path of the asset.
84-
* - `value`: A function returning a promise that resolves to the file contents of the asset.
84+
* - `value`: An object of type `ServerAsset`.
8585
*/
86-
readonly assets: ReadonlyMap<string, ServerAsset>;
86+
readonly assets: Readonly<Record<string, ServerAsset | undefined>>;
8787

8888
/**
8989
* The bootstrap mechanism for the server application.

‎packages/angular/ssr/test/app-engine_spec.ts

+75-76
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,57 @@ import { setAngularAppEngineManifest } from '../src/manifest';
1818
import { RenderMode } from '../src/routes/route-config';
1919
import { setAngularAppTestingManifest } from './testing-utils';
2020

21+
function createEntryPoint(locale: string) {
22+
return async () => {
23+
@Component({
24+
standalone: true,
25+
selector: `app-ssr-${locale}`,
26+
template: `SSR works ${locale.toUpperCase()}`,
27+
})
28+
class SSRComponent {}
29+
30+
@Component({
31+
standalone: true,
32+
selector: `app-ssg-${locale}`,
33+
template: `SSG works ${locale.toUpperCase()}`,
34+
})
35+
class SSGComponent {}
36+
37+
setAngularAppTestingManifest(
38+
[
39+
{ path: 'ssg', component: SSGComponent },
40+
{ path: 'ssr', component: SSRComponent },
41+
],
42+
[
43+
{ path: 'ssg', renderMode: RenderMode.Prerender },
44+
{ path: '**', renderMode: RenderMode.Server },
45+
],
46+
'/' + locale,
47+
{
48+
'ssg/index.html': {
49+
size: 25,
50+
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
51+
text: async () => `<html>
52+
<head>
53+
<title>SSG page</title>
54+
<base href="/${locale}" />
55+
</head>
56+
<body>
57+
SSG works ${locale.toUpperCase()}
58+
</body>
59+
</html>
60+
`,
61+
},
62+
},
63+
);
64+
65+
return {
66+
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
67+
ɵdestroyAngularServerApp: destroyAngularServerApp,
68+
};
69+
};
70+
}
71+
2172
describe('AngularAppEngine', () => {
2273
let appEngine: AngularAppEngine;
2374

@@ -28,59 +79,10 @@ describe('AngularAppEngine', () => {
2879
setAngularAppEngineManifest({
2980
// Note: Although we are testing only one locale, we need to configure two or more
3081
// to ensure that we test a different code path.
31-
entryPoints: new Map(
32-
['it', 'en'].map((locale) => [
33-
locale,
34-
async () => {
35-
@Component({
36-
standalone: true,
37-
selector: `app-ssr-${locale}`,
38-
template: `SSR works ${locale.toUpperCase()}`,
39-
})
40-
class SSRComponent {}
41-
42-
@Component({
43-
standalone: true,
44-
selector: `app-ssg-${locale}`,
45-
template: `SSG works ${locale.toUpperCase()}`,
46-
})
47-
class SSGComponent {}
48-
49-
setAngularAppTestingManifest(
50-
[
51-
{ path: 'ssg', component: SSGComponent },
52-
{ path: 'ssr', component: SSRComponent },
53-
],
54-
[
55-
{ path: 'ssg', renderMode: RenderMode.Prerender },
56-
{ path: '**', renderMode: RenderMode.Server },
57-
],
58-
'/' + locale,
59-
{
60-
'ssg/index.html': {
61-
size: 25,
62-
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
63-
text: async () => `<html>
64-
<head>
65-
<title>SSG page</title>
66-
<base href="/${locale}" />
67-
</head>
68-
<body>
69-
SSG works ${locale.toUpperCase()}
70-
</body>
71-
</html>
72-
`,
73-
},
74-
},
75-
);
76-
77-
return {
78-
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
79-
ɵdestroyAngularServerApp: destroyAngularServerApp,
80-
};
81-
},
82-
]),
83-
),
82+
entryPoints: {
83+
it: createEntryPoint('it'),
84+
en: createEntryPoint('en'),
85+
},
8486
basePath: '',
8587
});
8688

@@ -143,29 +145,26 @@ describe('AngularAppEngine', () => {
143145
destroyAngularServerApp();
144146

145147
setAngularAppEngineManifest({
146-
entryPoints: new Map([
147-
[
148-
'',
149-
async () => {
150-
@Component({
151-
standalone: true,
152-
selector: 'app-home',
153-
template: `Home works`,
154-
})
155-
class HomeComponent {}
156-
157-
setAngularAppTestingManifest(
158-
[{ path: 'home', component: HomeComponent }],
159-
[{ path: '**', renderMode: RenderMode.Server }],
160-
);
161-
162-
return {
163-
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
164-
ɵdestroyAngularServerApp: destroyAngularServerApp,
165-
};
166-
},
167-
],
168-
]),
148+
entryPoints: {
149+
'': async () => {
150+
@Component({
151+
standalone: true,
152+
selector: 'app-home',
153+
template: `Home works`,
154+
})
155+
class HomeComponent {}
156+
157+
setAngularAppTestingManifest(
158+
[{ path: 'home', component: HomeComponent }],
159+
[{ path: '**', renderMode: RenderMode.Server }],
160+
);
161+
162+
return {
163+
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
164+
ɵdestroyAngularServerApp: destroyAngularServerApp,
165+
};
166+
},
167+
},
169168
basePath: '',
170169
});
171170

‎packages/angular/ssr/test/assets_spec.ts

+12-14
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,18 @@ describe('ServerAsset', () => {
1515
assetManager = new ServerAssets({
1616
baseHref: '/',
1717
bootstrap: undefined as never,
18-
assets: new Map(
19-
Object.entries({
20-
'index.server.html': {
21-
text: async () => '<html>Index</html>',
22-
size: 18,
23-
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
24-
},
25-
'index.other.html': {
26-
text: async () => '<html>Other</html>',
27-
size: 18,
28-
hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3',
29-
},
30-
}),
31-
),
18+
assets: {
19+
'index.server.html': {
20+
text: async () => '<html>Index</html>',
21+
size: 18,
22+
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
23+
},
24+
'index.other.html': {
25+
text: async () => '<html>Other</html>',
26+
size: 18,
27+
hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3',
28+
},
29+
},
3230
});
3331
});
3432

0 commit comments

Comments
 (0)