From 4dcd137c791848e6a23c43e02e6a1bb774269a03 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Sat, 2 Nov 2024 11:15:37 +0000 Subject: [PATCH 1/2] refactor(@angular/build): split SSR server assets into separate chunks This commit refactors the build process for server-side rendering (SSR) by dividing server assets into separate, importable chunks rather than bundling them into a single output file. --- goldens/circular-deps/packages.json | 9 ++-- .../application/execute-post-bundle.ts | 43 ++++++++++++------- .../vite/plugins/angular-memory-plugin.ts | 12 +----- .../src/utils/server-rendering/manifest.ts | 30 +++++++++---- 4 files changed, 57 insertions(+), 37 deletions(-) diff --git a/goldens/circular-deps/packages.json b/goldens/circular-deps/packages.json index 0d4d97ad1edd..96a53f7a1040 100644 --- a/goldens/circular-deps/packages.json +++ b/goldens/circular-deps/packages.json @@ -7,7 +7,6 @@ "packages/angular/build/src/tools/esbuild/angular/component-stylesheets.ts", "packages/angular/build/src/tools/esbuild/bundler-context.ts", "packages/angular/build/src/tools/esbuild/utils.ts", - "packages/angular/build/src/utils/server-rendering/manifest.ts", "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" ], [ @@ -17,16 +16,18 @@ [ "packages/angular/build/src/tools/esbuild/bundler-context.ts", "packages/angular/build/src/tools/esbuild/utils.ts", - "packages/angular/build/src/utils/server-rendering/manifest.ts" + "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" ], [ "packages/angular/build/src/tools/esbuild/bundler-context.ts", "packages/angular/build/src/tools/esbuild/utils.ts", - "packages/angular/build/src/utils/server-rendering/manifest.ts", - "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts" + "packages/angular/build/src/utils/server-rendering/manifest.ts" ], [ "packages/angular/build/src/tools/esbuild/bundler-execution-result.ts", + "packages/angular/build/src/tools/esbuild/utils.ts" + ], + [ "packages/angular/build/src/tools/esbuild/utils.ts", "packages/angular/build/src/utils/server-rendering/manifest.ts" ], diff --git a/packages/angular/build/src/builders/application/execute-post-bundle.ts b/packages/angular/build/src/builders/application/execute-post-bundle.ts index bb2fb2e17b4d..bf23df37688b 100644 --- a/packages/angular/build/src/builders/application/execute-post-bundle.ts +++ b/packages/angular/build/src/builders/application/execute-post-bundle.ts @@ -40,6 +40,7 @@ import { OutputMode } from './schema'; * @param initialFiles A map containing initial file information for the executed build. * @param locale A language locale to insert in the index.html. */ +// eslint-disable-next-line max-lines-per-function export async function executePostBundleSteps( options: NormalizedApplicationBuildOptions, outputFiles: BuildOutputFile[], @@ -107,16 +108,19 @@ export async function executePostBundleSteps( // Create server manifest if (serverEntryPoint) { + const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest( + additionalHtmlOutputFiles, + outputFiles, + optimizationOptions.styles.inlineCritical ?? false, + undefined, + locale, + ); + additionalOutputFiles.push( + ...serverAssetsChunks, createOutputFile( SERVER_APP_MANIFEST_FILENAME, - generateAngularServerAppManifest( - additionalHtmlOutputFiles, - outputFiles, - optimizationOptions.styles.inlineCritical ?? false, - undefined, - locale, - ), + manifestContent, BuildOutputFileType.ServerApplication, ), ); @@ -194,15 +198,24 @@ export async function executePostBundleSteps( const manifest = additionalOutputFiles.find((f) => f.path === SERVER_APP_MANIFEST_FILENAME); assert(manifest, `${SERVER_APP_MANIFEST_FILENAME} was not found in output files.`); - manifest.contents = new TextEncoder().encode( - generateAngularServerAppManifest( - additionalHtmlOutputFiles, - outputFiles, - optimizationOptions.styles.inlineCritical ?? false, - serializableRouteTreeNodeForManifest, - locale, - ), + const { manifestContent, serverAssetsChunks } = generateAngularServerAppManifest( + additionalHtmlOutputFiles, + outputFiles, + optimizationOptions.styles.inlineCritical ?? false, + serializableRouteTreeNodeForManifest, + locale, ); + + for (const chunk of serverAssetsChunks) { + const idx = additionalOutputFiles.findIndex(({ path }) => path === chunk.path); + if (idx === -1) { + additionalOutputFiles.push(chunk); + } else { + additionalOutputFiles[idx] = chunk; + } + } + + manifest.contents = new TextEncoder().encode(manifestContent); } } diff --git a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts index 9d6510588ffa..1f5012b028f3 100644 --- a/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts +++ b/packages/angular/build/src/tools/vite/plugins/angular-memory-plugin.ts @@ -8,7 +8,7 @@ import assert from 'node:assert'; import { readFile } from 'node:fs/promises'; -import { basename, dirname, join, relative } from 'node:path'; +import { dirname, join, relative } from 'node:path'; import type { Plugin } from 'vite'; import { loadEsmModule } from '../../../utils/load-esm'; import { AngularMemoryOutputFiles } from '../utils'; @@ -24,8 +24,6 @@ export async function createAngularMemoryPlugin( ): Promise { const { virtualProjectRoot, outputFiles, external } = options; const { normalizePath } = await loadEsmModule('vite'); - // See: https://github.com/vitejs/vite/blob/a34a73a3ad8feeacc98632c0f4c643b6820bbfda/packages/vite/src/node/server/pluginContainer.ts#L331-L334 - const defaultImporter = join(virtualProjectRoot, 'index.html'); return { name: 'vite:angular-memory', @@ -40,16 +38,10 @@ export async function createAngularMemoryPlugin( } if (importer) { - let normalizedSource: string | undefined; if (source[0] === '.' && normalizePath(importer).startsWith(virtualProjectRoot)) { // Remove query if present const [importerFile] = importer.split('?', 1); - normalizedSource = join(dirname(relative(virtualProjectRoot, importerFile)), source); - } else if (source[0] === '/' && importer === defaultImporter) { - normalizedSource = basename(source); - } - if (normalizedSource) { - source = '/' + normalizePath(normalizedSource); + source = '/' + join(dirname(relative(virtualProjectRoot, importerFile)), source); } } diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index 1265bd110915..eb13be07e5d1 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -11,8 +11,8 @@ import { NormalizedApplicationBuildOptions, getLocaleBaseHref, } from '../../builders/application/options'; -import type { BuildOutputFile } from '../../tools/esbuild/bundler-context'; -import type { PrerenderedRoutesRecord } from '../../tools/esbuild/bundler-execution-result'; +import { type BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-context'; +import { createOutputFile } from '../../tools/esbuild/utils'; export const SERVER_APP_MANIFEST_FILENAME = 'angular-app-manifest.mjs'; export const SERVER_APP_ENGINE_MANIFEST_FILENAME = 'angular-app-engine-manifest.mjs'; @@ -104,8 +104,9 @@ export default { * @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. * - * @returns A string representing the content of the SSR server manifest for the Node.js - * environment. + * @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, @@ -113,13 +114,26 @@ export function generateAngularServerAppManifest( inlineCriticalCss: boolean, routes: readonly unknown[] | undefined, locale: string | undefined, -): string { +): { + manifestContent: string; + serverAssetsChunks: BuildOutputFile[]; +} { + const serverAssetsChunks: BuildOutputFile[] = []; const serverAssetsContent: 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`; + serverAssetsChunks.push( + createOutputFile( + jsChunkFilePath, + `export default \`${escapeUnsafeChars(file.text)}\`;`, + BuildOutputFileType.ServerApplication, + ), + ); + serverAssetsContent.push( - `['${file.path}', { size: ${file.size}, hash: '${file.hash}', text: async () => \`${escapeUnsafeChars(file.text)}\`}]`, + `['${file.path}', {size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, ); } } @@ -129,10 +143,10 @@ export default { bootstrap: () => import('./main.server.mjs').then(m => m.default), inlineCriticalCss: ${inlineCriticalCss}, routes: ${JSON.stringify(routes, undefined, 2)}, - assets: new Map([${serverAssetsContent.join(', \n')}]), + assets: new Map([\n${serverAssetsContent.join(', \n')}\n]), locale: ${locale !== undefined ? `'${locale}'` : undefined}, }; `; - return manifestContent; + return { manifestContent, serverAssetsChunks }; } From b37a47233efb97c0ace5e8ca0c4c7feb97ae011f Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 4 Nov 2024 07:58:51 +0000 Subject: [PATCH 2/2] fix(@angular/build): ensure accurate content size in server asset metadata Updated the calculation to use `Buffer.byteLength()` for determining the length of escaped file content. This change ensures that the `size` property in server asset metadata accurately represents the length of the escaped content. --- .../angular/build/src/utils/server-rendering/manifest.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/angular/build/src/utils/server-rendering/manifest.ts b/packages/angular/build/src/utils/server-rendering/manifest.ts index eb13be07e5d1..505eeb0ed516 100644 --- a/packages/angular/build/src/utils/server-rendering/manifest.ts +++ b/packages/angular/build/src/utils/server-rendering/manifest.ts @@ -124,16 +124,19 @@ export function generateAngularServerAppManifest( 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 \`${escapeUnsafeChars(file.text)}\`;`, + `export default \`${escapedContent}\`;`, BuildOutputFileType.ServerApplication, ), ); + const contentLength = Buffer.byteLength(escapedContent); serverAssetsContent.push( - `['${file.path}', {size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, + `['${file.path}', {size: ${contentLength}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, ); } }