Skip to content

Commit e375edb

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 ca757c9 commit e375edb

File tree

4 files changed

+169
-135
lines changed

4 files changed

+169
-135
lines changed

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

Lines changed: 18 additions & 11 deletions
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/manifest.ts

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,20 @@ 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+
* Utility type that replaces all properties of type `Record<K, V>` with `ReadonlyMap<K, V>`.
14+
*/
15+
type TransformRecordWithReadonlyMap<T> = {
16+
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown // Check if it's a function
17+
? T[K] // Leave function properties untouched
18+
: T[K] extends Readonly<Record<string, unknown>> // Check if it's a Readonly Record
19+
? T[K] extends Readonly<Record<infer RK, infer RV>>
20+
? ReadonlyMap<RK, RV> // Convert Readonly Record to ReadonlyMap
21+
: never
22+
: T[K]; // Leave other properties unchanged
23+
};
24+
25+
/**
26+
* Represents a server asset stored in the manifest.
1427
*/
1528
export interface ServerAsset {
1629
/**
@@ -51,14 +64,14 @@ export interface EntryPointExports {
5164
/**
5265
* Manifest for the Angular server application engine, defining entry points.
5366
*/
54-
export interface AngularAppEngineManifest {
67+
interface AngularAppEngineManifestConfig {
5568
/**
56-
* A map of entry points for the server application.
57-
* Each entry in the map consists of:
69+
* A readonly record of entry points for the server application.
70+
* Each entry consists of:
5871
* - `key`: The base href for the entry point.
5972
* - `value`: A function that returns a promise resolving to an object of type `EntryPointExports`.
6073
*/
61-
readonly entryPoints: ReadonlyMap<string, () => Promise<EntryPointExports>>;
74+
readonly entryPoints: Readonly<Record<string, () => Promise<EntryPointExports>>>;
6275

6376
/**
6477
* The base path for the server application.
@@ -67,23 +80,29 @@ export interface AngularAppEngineManifest {
6780
readonly basePath: string;
6881
}
6982

83+
/**
84+
* Manifest for the Angular server application engine, defining entry points.
85+
*/
86+
export type AngularAppEngineManifest =
87+
TransformRecordWithReadonlyMap<AngularAppEngineManifestConfig>;
88+
7089
/**
7190
* Manifest for a specific Angular server application, defining assets and bootstrap logic.
7291
*/
73-
export interface AngularAppManifest {
92+
interface AngularAppManifestConfig {
7493
/**
7594
* The base href for the application.
7695
* This is used to determine the root path of the application.
7796
*/
7897
readonly baseHref: string;
7998

8099
/**
81-
* A map of assets required by the server application.
82-
* Each entry in the map consists of:
100+
* A readonly record of assets required by the server application.
101+
* Each entry consists of:
83102
* - `key`: The path of the asset.
84-
* - `value`: A function returning a promise that resolves to the file contents of the asset.
103+
* - `value`: An object of type `ServerAsset`.
85104
*/
86-
readonly assets: ReadonlyMap<string, ServerAsset>;
105+
readonly assets: Readonly<Record<string, ServerAsset>>;
87106

88107
/**
89108
* The bootstrap mechanism for the server application.
@@ -112,6 +131,11 @@ export interface AngularAppManifest {
112131
readonly locale?: string;
113132
}
114133

134+
/**
135+
* Manifest for a specific Angular server application, defining assets and bootstrap logic.
136+
*/
137+
export type AngularAppManifest = TransformRecordWithReadonlyMap<AngularAppManifestConfig>;
138+
115139
/**
116140
* The Angular app manifest object.
117141
* This is used internally to store the current Angular app manifest.
@@ -123,8 +147,11 @@ let angularAppManifest: AngularAppManifest | undefined;
123147
*
124148
* @param manifest - The manifest object to set for the Angular application.
125149
*/
126-
export function setAngularAppManifest(manifest: AngularAppManifest): void {
127-
angularAppManifest = manifest;
150+
export function setAngularAppManifest(manifest: AngularAppManifestConfig): void {
151+
angularAppManifest = {
152+
...manifest,
153+
assets: new Map(Object.entries(manifest.assets)),
154+
};
128155
}
129156

130157
/**
@@ -155,8 +182,11 @@ let angularAppEngineManifest: AngularAppEngineManifest | undefined;
155182
*
156183
* @param manifest - The engine manifest object to set.
157184
*/
158-
export function setAngularAppEngineManifest(manifest: AngularAppEngineManifest): void {
159-
angularAppEngineManifest = manifest;
185+
export function setAngularAppEngineManifest(manifest: AngularAppEngineManifestConfig): void {
186+
angularAppEngineManifest = {
187+
...manifest,
188+
entryPoints: new Map(Object.entries(manifest.entryPoints)),
189+
};
160190
}
161191

162192
/**

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

Lines changed: 75 additions & 76 deletions
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

0 commit comments

Comments
 (0)