Skip to content

Commit

Permalink
refactor(@angular/ssr): replace Map with Record in SSR manifest
Browse files Browse the repository at this point in the history
Replaced `Map` with `Record` in SSR manifest to simplify structure and improve testing/setup.

(cherry picked from commit 4db4dd4)
  • Loading branch information
alan-agius4 committed Dec 4, 2024
1 parent 775e6f7 commit 75b4e92
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 148 deletions.
29 changes: 18 additions & 11 deletions packages/angular/build/src/utils/server-rendering/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function generateAngularServerAppEngineManifest(
i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'],
baseHref: string | undefined,
): string {
const entryPointsContent: string[] = [];
const entryPoints: Record<string, string> = {};

if (i18nOptions.shouldInline) {
for (const locale of i18nOptions.inlineLocales) {
Expand All @@ -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;
}
Expand Down Expand Up @@ -122,7 +126,7 @@ export function generateAngularServerAppManifest(
serverAssetsChunks: BuildOutputFile[];
} {
const serverAssetsChunks: BuildOutputFile[] = [];
const serverAssetsContent: string[] = [];
const serverAssets: Record<string, string> = {};
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
const extension = extname(file.path);
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
Expand All @@ -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)}`;
}
}

Expand All @@ -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 ')}
},
};
`;

Expand Down
11 changes: 8 additions & 3 deletions packages/angular/ssr/src/app-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -113,7 +118,7 @@ export class AngularAppEngine {
}

const { entryPoints } = this.manifest;
const entryPoint = entryPoints.get(potentialLocale);
const entryPoint = entryPoints[potentialLocale];
if (!entryPoint) {
return undefined;
}
Expand All @@ -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<EntryPointExports> | undefined {
const { entryPoints, basePath } = this.manifest;
if (entryPoints.size === 1) {
const { basePath } = this.manifest;
if (this.entryPointsCount === 1) {
return this.getEntryPointExports('');
}

Expand Down
4 changes: 2 additions & 2 deletions packages/angular/ssr/src/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.`);
}
Expand All @@ -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];
}

/**
Expand Down
16 changes: 8 additions & 8 deletions packages/angular/ssr/src/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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<string, () => Promise<EntryPointExports>>;
readonly entryPoints: Readonly<Record<string, (() => Promise<EntryPointExports>) | undefined>>;

/**
* The base path for the server application.
Expand All @@ -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<string, ServerAsset>;
readonly assets: Readonly<Record<string, ServerAsset | undefined>>;

/**
* The bootstrap mechanism for the server application.
Expand Down
151 changes: 75 additions & 76 deletions packages/angular/ssr/test/app-engine_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => `<html>
<head>
<title>SSG page</title>
<base href="/${locale}" />
</head>
<body>
SSG works ${locale.toUpperCase()}
</body>
</html>
`,
},
},
);

return {
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
ɵdestroyAngularServerApp: destroyAngularServerApp,
};
};
}

describe('AngularAppEngine', () => {
let appEngine: AngularAppEngine;

Expand All @@ -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 () => `<html>
<head>
<title>SSG page</title>
<base href="/${locale}" />
</head>
<body>
SSG works ${locale.toUpperCase()}
</body>
</html>
`,
},
},
);

return {
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
ɵdestroyAngularServerApp: destroyAngularServerApp,
};
},
]),
),
entryPoints: {
it: createEntryPoint('it'),
en: createEntryPoint('en'),
},
basePath: '',
});

Expand Down Expand Up @@ -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: '',
});

Expand Down
26 changes: 12 additions & 14 deletions packages/angular/ssr/test/assets_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,18 @@ describe('ServerAsset', () => {
assetManager = new ServerAssets({
baseHref: '/',
bootstrap: undefined as never,
assets: new Map(
Object.entries({
'index.server.html': {
text: async () => '<html>Index</html>',
size: 18,
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
},
'index.other.html': {
text: async () => '<html>Other</html>',
size: 18,
hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3',
},
}),
),
assets: {
'index.server.html': {
text: async () => '<html>Index</html>',
size: 18,
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
},
'index.other.html': {
text: async () => '<html>Other</html>',
size: 18,
hash: '4a455a99366921d396f5d51c7253c4678764f5e9487f2c27baaa0f33553c8ce3',
},
},
});
});

Expand Down
Loading

0 comments on commit 75b4e92

Please sign in to comment.