mirror of
https://github.com/angular/angular-cli.git
synced 2025-05-17 02:54:21 +08:00
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:
parent
480cba5fc5
commit
4db4dd4315
@ -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) {
|
||||
@ -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;
|
||||
}
|
||||
@ -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')) {
|
||||
@ -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)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 ')}
|
||||
},
|
||||
};
|
||||
`;
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
@ -113,7 +118,7 @@ export class AngularAppEngine {
|
||||
}
|
||||
|
||||
const { entryPoints } = this.manifest;
|
||||
const entryPoint = entryPoints.get(potentialLocale);
|
||||
const entryPoint = entryPoints[potentialLocale];
|
||||
if (!entryPoint) {
|
||||
return undefined;
|
||||
}
|
||||
@ -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('');
|
||||
}
|
||||
|
||||
|
@ -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.`);
|
||||
}
|
||||
@ -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];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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 {
|
||||
/**
|
||||
@ -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.
|
||||
@ -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.
|
||||
|
@ -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;
|
||||
|
||||
@ -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: '',
|
||||
});
|
||||
|
||||
@ -143,29 +145,26 @@ describe('AngularAppEngine', () => {
|
||||
destroyAngularServerApp();
|
||||
|
||||
setAngularAppEngineManifest({
|
||||
entryPoints: new Map([
|
||||
[
|
||||
'',
|
||||
async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-home',
|
||||
template: `Home works`,
|
||||
})
|
||||
class HomeComponent {}
|
||||
entryPoints: {
|
||||
'': async () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-home',
|
||||
template: `Home works`,
|
||||
})
|
||||
class HomeComponent {}
|
||||
|
||||
setAngularAppTestingManifest(
|
||||
[{ path: 'home', component: HomeComponent }],
|
||||
[{ path: '**', renderMode: RenderMode.Server }],
|
||||
);
|
||||
setAngularAppTestingManifest(
|
||||
[{ path: 'home', component: HomeComponent }],
|
||||
[{ path: '**', renderMode: RenderMode.Server }],
|
||||
);
|
||||
|
||||
return {
|
||||
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
|
||||
ɵdestroyAngularServerApp: destroyAngularServerApp,
|
||||
};
|
||||
},
|
||||
],
|
||||
]),
|
||||
return {
|
||||
ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp,
|
||||
ɵdestroyAngularServerApp: destroyAngularServerApp,
|
||||
};
|
||||
},
|
||||
},
|
||||
basePath: '',
|
||||
});
|
||||
|
||||
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -32,40 +32,38 @@ export function setAngularAppTestingManifest(
|
||||
setAngularAppManifest({
|
||||
inlineCriticalCss: false,
|
||||
baseHref,
|
||||
assets: new Map(
|
||||
Object.entries({
|
||||
...additionalServerAssets,
|
||||
'index.server.html': {
|
||||
size: 25,
|
||||
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
|
||||
text: async () => `<html>
|
||||
<head>
|
||||
<title>SSR page</title>
|
||||
<base href="${baseHref}" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
'index.csr.html': {
|
||||
size: 25,
|
||||
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
|
||||
text: async () =>
|
||||
`<html>
|
||||
<head>
|
||||
<title>CSR page</title>
|
||||
<base href="${baseHref}" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
}),
|
||||
),
|
||||
assets: {
|
||||
...additionalServerAssets,
|
||||
'index.server.html': {
|
||||
size: 25,
|
||||
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
|
||||
text: async () => `<html>
|
||||
<head>
|
||||
<title>SSR page</title>
|
||||
<base href="${baseHref}" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
'index.csr.html': {
|
||||
size: 25,
|
||||
hash: 'f799132d0a09e0fef93c68a12e443527700eb59e6f67fcb7854c3a60ff082fde',
|
||||
text: async () =>
|
||||
`<html>
|
||||
<head>
|
||||
<title>CSR page</title>
|
||||
<base href="${baseHref}" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
`,
|
||||
},
|
||||
},
|
||||
bootstrap: async () => () => {
|
||||
@Component({
|
||||
standalone: true,
|
||||
|
Loading…
x
Reference in New Issue
Block a user