Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading