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..505eeb0ed516 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,29 @@ 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`; + const escapedContent = escapeUnsafeChars(file.text); + + serverAssetsChunks.push( + createOutputFile( + jsChunkFilePath, + `export default \`${escapedContent}\`;`, + BuildOutputFileType.ServerApplication, + ), + ); + + const contentLength = Buffer.byteLength(escapedContent); serverAssetsContent.push( - `['${file.path}', { size: ${file.size}, hash: '${file.hash}', text: async () => \`${escapeUnsafeChars(file.text)}\`}]`, + `['${file.path}', {size: ${contentLength}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}]`, ); } } @@ -129,10 +146,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 }; }