This commit is contained in:
CHEVALLIER Abel
2025-11-13 16:23:22 +01:00
parent de9c515a47
commit cb235644dc
34924 changed files with 3811102 additions and 0 deletions
@@ -0,0 +1,18 @@
/**
* @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
*/
export interface ESMInMemoryFileLoaderWorkerData {
outputFiles: Record<string, string>;
workspaceRoot: string;
}
export declare function initialize(data: ESMInMemoryFileLoaderWorkerData): void;
export declare function resolve(specifier: string, context: {
parentURL: undefined | string;
}, nextResolve: Function): any;
export declare function load(url: string, context: {
format?: string | null;
}, nextLoad: Function): Promise<any>;
@@ -0,0 +1,135 @@
"use strict";
/**
* @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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialize = initialize;
exports.resolve = resolve;
exports.load = load;
const node_assert_1 = __importDefault(require("node:assert"));
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const node_url_1 = require("node:url");
const javascript_transformer_1 = require("../../../tools/esbuild/javascript-transformer");
/**
* @note For some unknown reason, setting `globalThis.ngServerMode = true` does not work when using ESM loader hooks.
*/
const NG_SERVER_MODE_INIT_BYTES = new TextEncoder().encode('var ngServerMode=true;');
/**
* Node.js ESM loader to redirect imports to in memory files.
* @see: https://nodejs.org/api/esm.html#loaders for more information about loaders.
*/
const MEMORY_URL_SCHEME = 'memory://';
let memoryVirtualRootUrl;
let outputFiles;
const javascriptTransformer = new javascript_transformer_1.JavaScriptTransformer(
// Always enable JIT linking to support applications built with and without AOT.
// In a development environment the additional scope information does not
// have a negative effect unlike production where final output size is relevant.
{ sourcemap: true, jit: true }, 1);
function initialize(data) {
// This path does not actually exist but is used to overlay the in memory files with the
// actual filesystem for resolution purposes.
// A custom URL schema (such as `memory://`) cannot be used for the resolve output because
// the in-memory files may use `import.meta.url` in ways that assume a file URL.
// `createRequire` is one example of this usage.
memoryVirtualRootUrl = (0, node_url_1.pathToFileURL)((0, node_path_1.join)(data.workspaceRoot, `.angular/prerender-root/${(0, node_crypto_1.randomUUID)()}/`)).href;
outputFiles = data.outputFiles;
}
function resolve(specifier, context, nextResolve) {
// In-memory files loaded from external code will contain a memory scheme
if (specifier.startsWith(MEMORY_URL_SCHEME)) {
let memoryUrl;
try {
memoryUrl = new URL(specifier);
}
catch {
node_assert_1.default.fail('External code attempted to use malformed memory scheme: ' + specifier);
}
// Resolve with a URL based from the virtual filesystem root
return {
format: 'module',
shortCircuit: true,
url: new URL(memoryUrl.pathname.slice(1), memoryVirtualRootUrl).href,
};
}
// Use next/default resolve if the parent is not from the virtual root
if (!context.parentURL?.startsWith(memoryVirtualRootUrl)) {
return nextResolve(specifier, context);
}
// Check for `./` and `../` relative specifiers
const isRelative = specifier[0] === '.' &&
(specifier[1] === '/' || (specifier[1] === '.' && specifier[2] === '/'));
// Relative specifiers from memory file should be based from the parent memory location
if (isRelative) {
let specifierUrl;
try {
specifierUrl = new URL(specifier, context.parentURL);
}
catch { }
if (specifierUrl?.pathname &&
Object.hasOwn(outputFiles, specifierUrl.href.slice(memoryVirtualRootUrl.length))) {
return {
format: 'module',
shortCircuit: true,
url: specifierUrl.href,
};
}
node_assert_1.default.fail(`In-memory ESM relative file should always exist: '${context.parentURL}' --> '${specifier}'`);
}
// Update the parent URL to allow for module resolution for the workspace.
// This handles bare specifiers (npm packages) and absolute paths.
// Defer to the next hook in the chain, which would be the
// Node.js default resolve if this is the last user-specified loader.
return nextResolve(specifier, {
...context,
parentURL: new URL('index.js', memoryVirtualRootUrl).href,
});
}
async function load(url, context, nextLoad) {
const { format } = context;
// Load the file from memory if the URL is based in the virtual root
if (url.startsWith(memoryVirtualRootUrl)) {
const source = outputFiles[url.slice(memoryVirtualRootUrl.length)];
(0, node_assert_1.default)(source !== undefined, 'Resolved in-memory ESM file should always exist: ' + url);
// In-memory files have already been transformer during bundling and can be returned directly
return {
format,
shortCircuit: true,
source,
};
}
// Only module files potentially require transformation. Angular libraries that would
// need linking are ESM only.
if (format === 'module' && isFileProtocol(url)) {
const filePath = (0, node_url_1.fileURLToPath)(url);
let source = await javascriptTransformer.transformFile(filePath);
if (filePath.includes('@angular/')) {
// Prepend 'var ngServerMode=true;' to the source.
source = Buffer.concat([NG_SERVER_MODE_INIT_BYTES, source]);
}
return {
format,
shortCircuit: true,
source,
};
}
// Let Node.js handle all other URLs.
return nextLoad(url);
}
function isFileProtocol(url) {
return url.startsWith('file://');
}
function handleProcessExit() {
void javascriptTransformer.close();
}
process.once('exit', handleProcessExit);
process.once('SIGINT', handleProcessExit);
process.once('uncaughtException', handleProcessExit);
@@ -0,0 +1,8 @@
/**
* @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
*/
export {};
@@ -0,0 +1,13 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_module_1 = require("node:module");
const node_url_1 = require("node:url");
const node_worker_threads_1 = require("node:worker_threads");
(0, node_module_1.register)('./loader-hooks.js', { parentURL: (0, node_url_1.pathToFileURL)(__filename), data: node_worker_threads_1.workerData });
@@ -0,0 +1,8 @@
/**
* @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
*/
export declare const IMPORT_EXEC_ARGV: string;
+13
View File
@@ -0,0 +1,13 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.IMPORT_EXEC_ARGV = void 0;
const node_path_1 = require("node:path");
const node_url_1 = require("node:url");
exports.IMPORT_EXEC_ARGV = '--import=' + (0, node_url_1.pathToFileURL)((0, node_path_1.join)(__dirname, 'register-hooks.js')).href;
+8
View File
@@ -0,0 +1,8 @@
/**
* @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
*/
export declare function patchFetchToLoadInMemoryAssets(baseURL: URL): void;
+63
View File
@@ -0,0 +1,63 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.patchFetchToLoadInMemoryAssets = patchFetchToLoadInMemoryAssets;
const mrmime_1 = require("mrmime");
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const node_worker_threads_1 = require("node:worker_threads");
/**
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { assetFiles } = node_worker_threads_1.workerData;
const assetsCache = new Map();
function patchFetchToLoadInMemoryAssets(baseURL) {
const originalFetch = globalThis.fetch;
const patchedFetch = async (input, init) => {
let url;
if (input instanceof URL) {
url = input;
}
else if (typeof input === 'string') {
url = new URL(input, baseURL);
}
else if (typeof input === 'object' && 'url' in input) {
url = new URL(input.url, baseURL);
}
else {
return originalFetch(input, init);
}
const { hostname } = url;
const pathname = decodeURIComponent(url.pathname);
if (hostname !== baseURL.hostname || !assetFiles[pathname]) {
// Only handle relative requests or files that are in assets.
return originalFetch(input, init);
}
const cachedAsset = assetsCache.get(pathname);
if (cachedAsset) {
const { content, headers } = cachedAsset;
return new Response(content, {
headers,
});
}
const extension = (0, node_path_1.extname)(pathname);
const mimeType = (0, mrmime_1.lookup)(extension);
const content = await (0, promises_1.readFile)(assetFiles[pathname]);
const headers = mimeType
? {
'Content-Type': mimeType,
}
: undefined;
assetsCache.set(pathname, { headers, content });
return new Response(content, {
headers,
});
};
globalThis.fetch = patchedFetch;
}
+14
View File
@@ -0,0 +1,14 @@
/**
* @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
*/
export declare const DEFAULT_URL: import("url").URL;
/**
* Launches a server that handles local requests.
*
* @returns A promise that resolves to the URL of the running server.
*/
export declare function launchServer(): Promise<URL>;
+63
View File
@@ -0,0 +1,63 @@
"use strict";
/**
* @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
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEFAULT_URL = void 0;
exports.launchServer = launchServer;
const node_assert_1 = __importDefault(require("node:assert"));
const node_http_1 = require("node:http");
const load_esm_1 = require("../load-esm");
const load_esm_from_memory_1 = require("./load-esm-from-memory");
const utils_1 = require("./utils");
exports.DEFAULT_URL = new URL('http://ng-localhost/');
/**
* Launches a server that handles local requests.
*
* @returns A promise that resolves to the URL of the running server.
*/
async function launchServer() {
const { reqHandler } = await (0, load_esm_from_memory_1.loadEsmModuleFromMemory)('./server.mjs');
const { createWebRequestFromNodeRequest, writeResponseToNodeResponse } = await (0, load_esm_1.loadEsmModule)('@angular/ssr/node');
if (!(0, utils_1.isSsrNodeRequestHandler)(reqHandler) && !(0, utils_1.isSsrRequestHandler)(reqHandler)) {
return exports.DEFAULT_URL;
}
const server = (0, node_http_1.createServer)((req, res) => {
(async () => {
// handle request
if ((0, utils_1.isSsrNodeRequestHandler)(reqHandler)) {
await reqHandler(req, res, (e) => {
throw e ?? new Error(`Unable to handle request: '${req.url}'.`);
});
}
else {
const webRes = await reqHandler(createWebRequestFromNodeRequest(req));
if (webRes) {
await writeResponseToNodeResponse(webRes, res);
}
else {
res.statusCode = 501;
res.end('Not Implemented.');
}
}
})().catch((e) => {
res.statusCode = 500;
res.end('Internal Server Error.');
// eslint-disable-next-line no-console
console.error(e);
});
});
server.unref();
await new Promise((resolve) => server.listen(0, 'localhost', resolve));
const serverAddress = server.address();
(0, node_assert_1.default)(serverAddress, 'Server address should be defined.');
(0, node_assert_1.default)(typeof serverAddress !== 'string', 'Server address should not be a string.');
return new URL(`http://localhost:${serverAddress.port}/`);
}
+27
View File
@@ -0,0 +1,27 @@
/**
* @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 { ApplicationRef, Type } from '@angular/core';
import type { BootstrapContext } from '@angular/platform-browser';
import type { ɵextractRoutesAndCreateRouteTree, ɵgetOrCreateAngularServerApp } from '@angular/ssr';
/**
* Represents the exports available from the main server bundle.
*/
interface MainServerBundleExports {
default: ((context: BootstrapContext) => Promise<ApplicationRef>) | Type<unknown>;
ɵextractRoutesAndCreateRouteTree: typeof ɵextractRoutesAndCreateRouteTree;
ɵgetOrCreateAngularServerApp: typeof ɵgetOrCreateAngularServerApp;
}
/**
* Represents the exports available from the server bundle.
*/
interface ServerBundleExports {
reqHandler?: unknown;
}
export declare function loadEsmModuleFromMemory(path: './main.server.mjs'): Promise<MainServerBundleExports>;
export declare function loadEsmModuleFromMemory(path: './server.mjs'): Promise<ServerBundleExports>;
export {};
+25
View File
@@ -0,0 +1,25 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadEsmModuleFromMemory = loadEsmModuleFromMemory;
const error_1 = require("../error");
const load_esm_1 = require("../load-esm");
function loadEsmModuleFromMemory(path) {
return (0, load_esm_1.loadEsmModule)(new URL(path, 'memory://')).catch((e) => {
(0, error_1.assertIsError)(e);
// While the error is an 'instanceof Error', it is extended with non transferable properties
// and cannot be transferred from a worker when using `--import`. This results in the error object
// displaying as '[Object object]' when read outside of the worker. Therefore, we reconstruct the error message here.
const error = new Error(e.message);
error.stack = e.stack;
error.name = e.name;
error.code = e.code;
throw error;
});
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @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 { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { type BuildOutputFile } from '../../tools/esbuild/bundler-context';
export declare const SERVER_APP_MANIFEST_FILENAME = "angular-app-manifest.mjs";
export declare const SERVER_APP_ENGINE_MANIFEST_FILENAME = "angular-app-engine-manifest.mjs";
/**
* 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 baseHref - The base HREF for the application. This is used to set the base URL
* for all relative URLs in the application.
*/
export declare function generateAngularServerAppEngineManifest(i18nOptions: NormalizedApplicationBuildOptions['i18nOptions'], baseHref: string | undefined): string;
/**
* 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 declare 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[];
};
+174
View File
@@ -0,0 +1,174 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SERVER_APP_ENGINE_MANIFEST_FILENAME = exports.SERVER_APP_MANIFEST_FILENAME = void 0;
exports.generateAngularServerAppEngineManifest = generateAngularServerAppEngineManifest;
exports.generateAngularServerAppManifest = generateAngularServerAppManifest;
const node_path_1 = require("node:path");
const node_vm_1 = require("node:vm");
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
const utils_1 = require("../../tools/esbuild/utils");
const environment_options_1 = require("../environment-options");
exports.SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs';
exports.SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs';
const MAIN_SERVER_OUTPUT_FILENAME = 'main.server.mjs';
/**
* A mapping of unsafe characters to their escaped Unicode equivalents.
*/
const UNSAFE_CHAR_MAP = {
'`': '\\`',
'$': '\\$',
'\\': '\\\\',
};
/**
* 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) {
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 baseHref - The base HREF for the application. This is used to set the base URL
* for all relative URLs in the application.
*/
function generateAngularServerAppEngineManifest(i18nOptions, baseHref) {
const entryPoints = {};
const supportedLocales = {};
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}',
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.
*/
function generateAngularServerAppManifest(additionalHtmlOutputFiles, outputFiles, inlineCriticalCss, routes, locale, baseHref, initialFiles, metafile, publicPath) {
const serverAssetsChunks = [];
const serverAssets = {};
for (const file of [...additionalHtmlOutputFiles.values(), ...outputFiles]) {
const extension = (0, node_path_1.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((0, utils_1.createOutputFile)(jsChunkFilePath, `export default \`${escapedContent}\`;`, bundler_context_1.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 = (0, node_vm_1.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 || environment_options_1.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, initialFiles, publicPath = '') {
const entryPointToBundles = {};
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 = [`${publicPath}${fileName}`];
for (const { kind, external, path } of imports) {
if (external || initialFiles.has(path) || kind !== 'import-statement') {
continue;
}
importedPaths.push(`${publicPath}${path}`);
}
entryPointToBundles[entryPoint] = importedPaths;
}
return entryPointToBundles;
}
+28
View File
@@ -0,0 +1,28 @@
/**
* @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 { RenderMode, ɵextractRoutesAndCreateRouteTree } from '@angular/ssr';
import { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
type Writeable<T extends readonly unknown[]> = T extends readonly (infer U)[] ? U[] : never;
export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData {
assetFiles: Record</** Destination */ string, /** Source */ string>;
}
export type SerializableRouteTreeNode = ReturnType<Awaited<ReturnType<typeof ɵextractRoutesAndCreateRouteTree>>['routeTree']['toObject']>;
export type WritableSerializableRouteTreeNode = Writeable<SerializableRouteTreeNode>;
export interface RoutersExtractorWorkerResult {
serializedRouteTree: SerializableRouteTreeNode;
appShellRoute?: string;
errors: string[];
}
/**
* Local copy of `RenderMode` exported from `@angular/ssr`.
* This constant is needed to handle interop between CommonJS (CJS) and ES Modules (ESM) formats.
*
* It maps `RenderMode` enum values to their corresponding numeric identifiers.
*/
export declare const RouteRenderMode: Record<keyof typeof RenderMode, RenderMode>;
export {};
+21
View File
@@ -0,0 +1,21 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RouteRenderMode = void 0;
/**
* Local copy of `RenderMode` exported from `@angular/ssr`.
* This constant is needed to handle interop between CommonJS (CJS) and ES Modules (ESM) formats.
*
* It maps `RenderMode` enum values to their corresponding numeric identifiers.
*/
exports.RouteRenderMode = {
Server: 0,
Client: 1,
Prerender: 2,
};
+39
View File
@@ -0,0 +1,39 @@
/**
* @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 { NormalizedApplicationBuildOptions } from '../../builders/application/options';
import { OutputMode } from '../../builders/application/schema';
import { BuildOutputFile } from '../../tools/esbuild/bundler-context';
import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
import { SerializableRouteTreeNode } from './models';
type PrerenderOptions = NormalizedApplicationBuildOptions['prerenderOptions'];
type AppShellOptions = NormalizedApplicationBuildOptions['appShellOptions'];
/**
* Represents the output of a prerendering process.
*
* The key is the file path, and the value is an object containing the following properties:
*
* - `content`: The HTML content or output generated for the corresponding file path.
* - `appShellRoute`: A boolean flag indicating whether the content is an app shell.
*
* @example
* {
* '/index.html': { content: '<html>...</html>', appShell: false },
* '/shell/index.html': { content: '<html>...</html>', appShellRoute: true }
* }
*/
type PrerenderOutput = Record<string, {
content: string;
appShellRoute: boolean;
}>;
export declare function prerenderPages(workspaceRoot: string, baseHref: string, appShellOptions: AppShellOptions | undefined, prerenderOptions: PrerenderOptions | undefined, outputFiles: Readonly<BuildOutputFile[]>, assets: Readonly<BuildOutputAsset[]>, outputMode: OutputMode | undefined, sourcemap?: boolean, maxThreads?: number): Promise<{
output: PrerenderOutput;
warnings: string[];
errors: string[];
serializableRouteTreeNode: SerializableRouteTreeNode;
}>;
export {};
+244
View File
@@ -0,0 +1,244 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.prerenderPages = prerenderPages;
const promises_1 = require("node:fs/promises");
const node_path_1 = require("node:path");
const schema_1 = require("../../builders/application/schema");
const bundler_context_1 = require("../../tools/esbuild/bundler-context");
const error_1 = require("../error");
const path_1 = require("../path");
const url_1 = require("../url");
const worker_pool_1 = require("../worker-pool");
const utils_1 = require("./esm-in-memory-loader/utils");
const manifest_1 = require("./manifest");
const models_1 = require("./models");
const utils_2 = require("./utils");
async function prerenderPages(workspaceRoot, baseHref, appShellOptions, prerenderOptions, outputFiles, assets, outputMode, sourcemap = false, maxThreads = 1) {
const outputFilesForWorker = {};
const serverBundlesSourceMaps = new Map();
const warnings = [];
const errors = [];
for (const { text, path, type } of outputFiles) {
if (type !== bundler_context_1.BuildOutputFileType.ServerApplication && type !== bundler_context_1.BuildOutputFileType.ServerRoot) {
continue;
}
// Contains the server runnable application code
if ((0, node_path_1.extname)(path) === '.map') {
serverBundlesSourceMaps.set(path.slice(0, -4), text);
}
else {
outputFilesForWorker[path] = text;
}
}
// Inline sourcemap into JS file. This is needed to make Node.js resolve sourcemaps
// when using `--enable-source-maps` when using in memory files.
for (const [filePath, map] of serverBundlesSourceMaps) {
const jsContent = outputFilesForWorker[filePath];
if (jsContent) {
outputFilesForWorker[filePath] =
jsContent +
`\n//# sourceMappingURL=` +
`data:application/json;base64,${Buffer.from(map).toString('base64')}`;
}
}
serverBundlesSourceMaps.clear();
const assetsReversed = {};
for (const { source, destination } of assets) {
assetsReversed[addLeadingSlash((0, path_1.toPosixPath)(destination))] = source;
}
// Get routes to prerender
const { errors: extractionErrors, serializedRouteTree: serializableRouteTreeNode, appShellRoute, } = await getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, assetsReversed, appShellOptions, prerenderOptions, sourcemap, outputMode).catch((err) => {
return {
errors: [`An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err}`],
serializedRouteTree: [],
appShellRoute: undefined,
};
});
errors.push(...extractionErrors);
const serializableRouteTreeNodeForPrerender = [];
for (const metadata of serializableRouteTreeNode) {
if (outputMode !== schema_1.OutputMode.Static && metadata.redirectTo) {
// Skip redirects if output mode is not static.
continue;
}
if (metadata.route.includes('*')) {
// Skip catch all routes from prerender.
continue;
}
switch (metadata.renderMode) {
case undefined: /* Legacy building mode */
case models_1.RouteRenderMode.Prerender:
serializableRouteTreeNodeForPrerender.push(metadata);
break;
case models_1.RouteRenderMode.Server:
if (outputMode === schema_1.OutputMode.Static) {
errors.push(`Route '${metadata.route}' is configured with server render mode, but the build 'outputMode' is set to 'static'.`);
}
break;
}
}
if (!serializableRouteTreeNodeForPrerender.length || errors.length > 0) {
return {
errors,
warnings,
output: {},
serializableRouteTreeNode,
};
}
// Add the extracted routes to the manifest file.
// We could re-generate it from the start, but that would require a number of options to be passed down.
const manifest = outputFilesForWorker[manifest_1.SERVER_APP_MANIFEST_FILENAME];
if (manifest) {
outputFilesForWorker[manifest_1.SERVER_APP_MANIFEST_FILENAME] = manifest.replace('routes: undefined,', `routes: ${JSON.stringify(serializableRouteTreeNodeForPrerender, undefined, 2)},`);
}
// Render routes
const { errors: renderingErrors, output } = await renderPages(baseHref, sourcemap, serializableRouteTreeNodeForPrerender, maxThreads, workspaceRoot, outputFilesForWorker, assetsReversed, outputMode, appShellRoute ?? appShellOptions?.route);
errors.push(...renderingErrors);
return {
errors,
warnings,
output,
serializableRouteTreeNode,
};
}
async function renderPages(baseHref, sourcemap, serializableRouteTreeNode, maxThreads, workspaceRoot, outputFilesForWorker, assetFilesForWorker, outputMode, appShellRoute) {
const output = {};
const errors = [];
const workerExecArgv = [utils_1.IMPORT_EXEC_ARGV];
if (sourcemap) {
workerExecArgv.push('--enable-source-maps');
}
const renderWorker = new worker_pool_1.WorkerPool({
filename: require.resolve('./render-worker'),
maxThreads: Math.min(serializableRouteTreeNode.length, maxThreads),
workerData: {
workspaceRoot,
outputFiles: outputFilesForWorker,
assetFiles: assetFilesForWorker,
outputMode,
hasSsrEntry: !!outputFilesForWorker['server.mjs'],
},
execArgv: workerExecArgv,
});
try {
const renderingPromises = [];
const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute);
const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname;
for (const { route, redirectTo } of serializableRouteTreeNode) {
// Remove the base href from the file output path.
const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefPathnameWithLeadingSlash)
? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length))
: route;
const outPath = node_path_1.posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
if (typeof redirectTo === 'string') {
output[outPath] = { content: (0, utils_2.generateRedirectStaticPage)(redirectTo), appShellRoute: false };
continue;
}
const render = renderWorker.run({ url: route });
const renderResult = render
.then((content) => {
if (content !== null) {
output[outPath] = {
content,
appShellRoute: appShellRouteWithLeadingSlash === routeWithoutBaseHref,
};
}
})
.catch((err) => {
errors.push(`An error occurred while prerendering route '${route}'.\n\n${err.message ?? err.stack ?? err.code ?? err}`);
void renderWorker.destroy();
});
renderingPromises.push(renderResult);
}
await Promise.all(renderingPromises);
}
finally {
void renderWorker.destroy();
}
return {
errors,
output,
};
}
async function getAllRoutes(workspaceRoot, baseHref, outputFilesForWorker, assetFilesForWorker, appShellOptions, prerenderOptions, sourcemap, outputMode) {
const { routesFile, discoverRoutes } = prerenderOptions ?? {};
const routes = [];
let appShellRoute;
if (appShellOptions) {
appShellRoute = (0, url_1.urlJoin)(baseHref, appShellOptions.route);
routes.push({
renderMode: models_1.RouteRenderMode.Prerender,
route: appShellRoute,
});
}
if (routesFile) {
const routesFromFile = (await (0, promises_1.readFile)(routesFile, 'utf8')).split(/\r?\n/);
for (const route of routesFromFile) {
routes.push({
renderMode: models_1.RouteRenderMode.Prerender,
route: (0, url_1.urlJoin)(baseHref, route.trim()),
});
}
}
if (!discoverRoutes) {
return { errors: [], appShellRoute, serializedRouteTree: routes };
}
const workerExecArgv = [utils_1.IMPORT_EXEC_ARGV];
if (sourcemap) {
workerExecArgv.push('--enable-source-maps');
}
const renderWorker = new worker_pool_1.WorkerPool({
filename: require.resolve('./routes-extractor-worker'),
maxThreads: 1,
workerData: {
workspaceRoot,
outputFiles: outputFilesForWorker,
assetFiles: assetFilesForWorker,
outputMode,
hasSsrEntry: !!outputFilesForWorker['server.mjs'],
},
execArgv: workerExecArgv,
});
try {
const { serializedRouteTree, appShellRoute, errors } = await renderWorker.run({});
if (!routes.length) {
return { errors, appShellRoute, serializedRouteTree };
}
// Merge the routing trees
const uniqueRoutes = new Map();
for (const item of [...routes, ...serializedRouteTree]) {
if (!uniqueRoutes.has(item.route)) {
uniqueRoutes.set(item.route, item);
}
}
return { errors, serializedRouteTree: Array.from(uniqueRoutes.values()) };
}
catch (err) {
(0, error_1.assertIsError)(err);
return {
errors: [
`An error occurred while extracting routes.\n\n${err.message ?? err.stack ?? err.code ?? err}`,
],
serializedRouteTree: [],
};
}
finally {
void renderWorker.destroy();
}
}
function addLeadingSlash(value) {
return value[0] === '/' ? value : '/' + value;
}
function addTrailingSlash(url) {
return url[url.length - 1] === '/' ? url : `${url}/`;
}
function removeLeadingSlash(value) {
return value[0] === '/' ? value.slice(1) : value;
}
+23
View File
@@ -0,0 +1,23 @@
/**
* @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 { OutputMode } from '../../builders/application/schema';
import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
assetFiles: Record</** Destination */ string, /** Source */ string>;
outputMode: OutputMode | undefined;
hasSsrEntry: boolean;
}
export interface RenderOptions {
url: string;
}
/**
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
*/
declare function renderPage({ url }: RenderOptions): Promise<string | null>;
declare const _default: Promise<typeof renderPage>;
export default _default;
+42
View File
@@ -0,0 +1,42 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_worker_threads_1 = require("node:worker_threads");
const fetch_patch_1 = require("./fetch-patch");
const launch_server_1 = require("./launch-server");
const load_esm_from_memory_1 = require("./load-esm-from-memory");
const utils_1 = require("./utils");
/**
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { outputMode, hasSsrEntry } = node_worker_threads_1.workerData;
let serverURL = launch_server_1.DEFAULT_URL;
/**
* Renders each route in routes and writes them to <outputPath>/<route>/index.html.
*/
async function renderPage({ url }) {
const { ɵgetOrCreateAngularServerApp: getOrCreateAngularServerApp } = await (0, load_esm_from_memory_1.loadEsmModuleFromMemory)('./main.server.mjs');
const angularServerApp = getOrCreateAngularServerApp({
allowStaticRouteRender: true,
});
const response = await angularServerApp.handle(new Request(new URL(url, serverURL), { signal: AbortSignal.timeout(30_000) }));
if (!response) {
return null;
}
const location = response.headers.get('Location');
return location ? (0, utils_1.generateRedirectStaticPage)(location) : response.text();
}
async function initialize() {
if (outputMode !== undefined && hasSsrEntry) {
serverURL = await (0, launch_server_1.launchServer)();
}
(0, fetch_patch_1.patchFetchToLoadInMemoryAssets)(serverURL);
return renderPage;
}
exports.default = initialize();
+16
View File
@@ -0,0 +1,16 @@
/**
* @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 { OutputMode } from '../../builders/application/schema';
import { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loader-hooks';
import { RoutersExtractorWorkerResult } from './models';
export interface ExtractRoutesWorkerData extends ESMInMemoryFileLoaderWorkerData {
outputMode: OutputMode | undefined;
}
/** Renders an application based on a provided options. */
declare function extractRoutes(): Promise<RoutersExtractorWorkerResult>;
export default extractRoutes;
+36
View File
@@ -0,0 +1,36 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
const node_worker_threads_1 = require("node:worker_threads");
const schema_1 = require("../../builders/application/schema");
const fetch_patch_1 = require("./fetch-patch");
const launch_server_1 = require("./launch-server");
const load_esm_from_memory_1 = require("./load-esm-from-memory");
/**
* This is passed as workerData when setting up the worker via the `piscina` package.
*/
const { outputMode, hasSsrEntry } = node_worker_threads_1.workerData;
/** Renders an application based on a provided options. */
async function extractRoutes() {
const serverURL = outputMode !== undefined && hasSsrEntry ? await (0, launch_server_1.launchServer)() : launch_server_1.DEFAULT_URL;
(0, fetch_patch_1.patchFetchToLoadInMemoryAssets)(serverURL);
const { ɵextractRoutesAndCreateRouteTree: extractRoutesAndCreateRouteTree } = await (0, load_esm_from_memory_1.loadEsmModuleFromMemory)('./main.server.mjs');
const { routeTree, appShellRoute, errors } = await extractRoutesAndCreateRouteTree({
url: serverURL,
invokeGetPrerenderParams: outputMode !== undefined,
includePrerenderFallbackRoutes: outputMode === schema_1.OutputMode.Server,
signal: AbortSignal.timeout(30_000),
});
return {
errors,
appShellRoute,
serializedRouteTree: routeTree.toObject(),
};
}
exports.default = extractRoutes;
+21
View File
@@ -0,0 +1,21 @@
/**
* @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 { createRequestHandler } from '@angular/ssr';
import type { createNodeRequestHandler } from '@angular/ssr/node';
export declare function isSsrNodeRequestHandler(value: unknown): value is ReturnType<typeof createNodeRequestHandler>;
export declare function isSsrRequestHandler(value: unknown): value is ReturnType<typeof createRequestHandler>;
/**
* Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL.
*
* This function creates a simple HTML page that performs a redirect using a meta tag.
* It includes a fallback link in case the meta-refresh doesn't work.
*
* @param url - The URL to which the page should redirect.
* @returns The HTML content of the static redirect page.
*/
export declare function generateRedirectStaticPage(url: string): string;
+42
View File
@@ -0,0 +1,42 @@
"use strict";
/**
* @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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.isSsrNodeRequestHandler = isSsrNodeRequestHandler;
exports.isSsrRequestHandler = isSsrRequestHandler;
exports.generateRedirectStaticPage = generateRedirectStaticPage;
function isSsrNodeRequestHandler(value) {
return typeof value === 'function' && '__ng_node_request_handler__' in value;
}
function isSsrRequestHandler(value) {
return typeof value === 'function' && '__ng_request_handler__' in value;
}
/**
* Generates a static HTML page with a meta refresh tag to redirect the user to a specified URL.
*
* This function creates a simple HTML page that performs a redirect using a meta tag.
* It includes a fallback link in case the meta-refresh doesn't work.
*
* @param url - The URL to which the page should redirect.
* @returns The HTML content of the static redirect page.
*/
function generateRedirectStaticPage(url) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting</title>
<meta http-equiv="refresh" content="0; url=${url}">
</head>
<body>
<pre>Redirecting to <a href="${url}">${url}</a></pre>
</body>
</html>
`.trim();
}