forked from angular/angular-cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmanifest.ts
More file actions
241 lines (216 loc) · 8.86 KB
/
manifest.ts
File metadata and controls
241 lines (216 loc) · 8.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import type { Metafile } from 'esbuild';
import { extname } from 'node:path';
import { runInThisContext } from 'node:vm';
import { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { createOutputFile } from '../../tools/esbuild/utils';
import { shouldOptimizeChunks } from '../environment-options';
export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs';
export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs';
interface FilesMapping {
path: string;
dynamicImport: boolean;
}
const MAIN_SERVER_OUTPUT_FILENAME = 'main.server.mjs';
/**
* A mapping of unsafe characters to their escaped Unicode equivalents.
*/
const UNSAFE_CHAR_MAP: Record<string, string> = {
'`': '\\`',
'$': '\\$',
'\\': '\\\\',
};
/**
* Escapes unsafe characters in a given string by replacing them with
* their Unicode escape sequences.
*
* @param str - The string to be escaped.
* @returns The escaped string where unsafe characters are replaced.
*/
function escapeUnsafeChars(str: string): string {
return str.replace(/[$`\\]/g, (c) => UNSAFE_CHAR_MAP[c]);
}
/**
* Generates the server manifest for the App Engine environment.
*
* This manifest is used to configure the server-side rendering (SSR) setup for the
* Angular application when deployed to Google App Engine. It includes the entry points
* for different locales and the base HREF for the application.
*
* @param i18nOptions - The internationalization options for the application build. This
* includes settings for inlining locales and determining the output structure.
* @param allowedHosts - A list of hosts that are allowed to access the server-side application.
* @param baseHref - The base HREF for the application. This is used to set the base URL
* for all relative URLs in the application.
*/
export function generateAngularServerAppEngineManifest(
i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'],
allowedHosts: string[],
baseHref: string | undefined,
): string {
const entryPoints: Record<string, string> = {};
const supportedLocales: Record<string, string> = {};
if (i18nOptions.shouldInline && !i18nOptions.flatOutput) {
for (const locale of i18nOptions.inlineLocales) {
const { subPath } = i18nOptions.locales[locale];
const importPath = `${subPath ? `${subPath}/` : ''}${MAIN_SERVER_OUTPUT_FILENAME}`;
entryPoints[subPath] = `() => import('./${importPath}')`;
supportedLocales[locale] = subPath;
}
} else {
entryPoints[''] = `() => import('./${MAIN_SERVER_OUTPUT_FILENAME}')`;
supportedLocales[i18nOptions.sourceLocale] = '';
}
// Remove trailing slash but retain leading slash.
let basePath = baseHref || '/';
if (basePath.length > 1 && basePath[basePath.length - 1] === '/') {
basePath = basePath.slice(0, -1);
}
const manifestContent = `
export default {
basePath: '${basePath}',
allowedHosts: ${JSON.stringify(allowedHosts, undefined, 2)},
supportedLocales: ${JSON.stringify(supportedLocales, undefined, 2)},
entryPoints: {
${Object.entries(entryPoints)
.map(([key, value]) => `'${key}': ${value}`)
.join(',\n ')}
},
};
`;
return manifestContent;
}
/**
* Generates the server manifest for the standard Node.js environment.
*
* This manifest is used to configure the server-side rendering (SSR) setup for the
* Angular application when running in a standard Node.js environment. It includes
* information about the bootstrap module, whether to inline critical CSS, and any
* additional HTML and CSS output files.
*
* @param additionalHtmlOutputFiles - A map of additional HTML output files generated
* during the build process, keyed by their file paths.
* @param outputFiles - An array of all output files from the build process, including
* JavaScript and CSS files.
* @param inlineCriticalCss - A boolean indicating whether critical CSS should be inlined
* in the server-side rendered pages.
* @param routes - An optional array of route definitions for the application, used for
* server-side rendering and routing.
* @param locale - An optional string representing the locale or language code to be used for
* the application, helping with localization and rendering content specific to the locale.
* @param baseHref - The base HREF for the application. This is used to set the base URL
* for all relative URLs in the application.
* @param initialFiles - A list of initial files that preload tags have already been added for.
* @param metafile - An esbuild metafile object.
* @param publicPath - The configured public path.
*
* @returns An object containing:
* - `manifestContent`: A string of the SSR manifest content.
* - `serverAssetsChunks`: An array of build output files containing the generated assets for the server.
*/
export function generateAngularServerAppManifest(
additionalHtmlOutputFiles: Map<string, BuildOutputFile>,
outputFiles: BuildOutputFile[],
inlineCriticalCss: boolean,
routes: readonly unknown[] | undefined,
locale: string | undefined,
baseHref: string,
initialFiles: Set<string>,
metafile: Metafile,
publicPath: string | undefined,
): {
manifestContent: string;
serverAssetsChunks: BuildOutputFile[];
} {
const serverAssetsChunks: BuildOutputFile[] = [];
const serverAssets: Record<string, string> = {};
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
const extension = extname(file.path);
if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
const jsChunkFilePath = `assets-chunks/${file.path.replace(/[./]/g, '_')}.mjs`;
const escapedContent = escapeUnsafeChars(file.text);
serverAssetsChunks.push(
createOutputFile(
jsChunkFilePath,
`export default \`${escapedContent}\`;`,
BuildOutputFileType.ServerApplication,
),
);
// This is needed because JavaScript engines script parser convert `\r\n` to `\n` in template literals,
// which can result in an incorrect byte length.
const size = runInThisContext(`new TextEncoder().encode(\`${escapedContent}\`).byteLength`);
serverAssets[file.path] =
`{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`;
}
}
// When routes have been extracted, mappings are no longer needed, as preloads will be included in the metadata.
// When shouldOptimizeChunks is enabled the metadata is no longer correct and thus we cannot generate the mappings.
const entryPointToBrowserMapping =
routes?.length || shouldOptimizeChunks
? undefined
: generateLazyLoadedFilesMappings(metafile, initialFiles, publicPath);
const manifestContent = `
export default {
bootstrap: () => import('./main.server.mjs').then(m => m.default),
inlineCriticalCss: ${inlineCriticalCss},
baseHref: '${baseHref}',
locale: ${JSON.stringify(locale)},
routes: ${JSON.stringify(routes, undefined, 2)},
entryPointToBrowserMapping: ${JSON.stringify(entryPointToBrowserMapping, undefined, 2)},
assets: {
${Object.entries(serverAssets)
.map(([key, value]) => `'${key}': ${value}`)
.join(',\n ')}
},
};
`;
return { manifestContent, serverAssetsChunks };
}
/**
* Maps entry points to their corresponding browser bundles for lazy loading.
*
* This function processes a metafile's outputs to generate a mapping between browser-side entry points
* and the associated JavaScript files that should be loaded in the browser. It includes the entry-point's
* own path and any valid imports while excluding initial files or external resources.
*/
function generateLazyLoadedFilesMappings(
metafile: Metafile,
initialFiles: Set<string>,
publicPath = '',
): Record<string, FilesMapping[]> {
const entryPointToBundles: Record<string, FilesMapping[]> = {};
for (const [fileName, { entryPoint, exports, imports }] of Object.entries(metafile.outputs)) {
// Skip files that don't have an entryPoint, no exports, or are not .js
if (!entryPoint || exports?.length < 1 || !fileName.endsWith('.js')) {
continue;
}
const importedPaths: FilesMapping[] = [
{
path: `${publicPath}${fileName}`,
dynamicImport: false,
},
];
for (const { kind, external, path } of imports) {
if (
external ||
initialFiles.has(path) ||
(kind !== 'dynamic-import' && kind !== 'import-statement')
) {
continue;
}
importedPaths.push({
path: `${publicPath}${path}`,
dynamicImport: kind === 'dynamic-import',
});
}
entryPointToBundles[entryPoint] = importedPaths;
}
return entryPointToBundles;
}