From 2a7a2f747fd780a7c155be9ad49e15296a716b83 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:24:20 -0400 Subject: [PATCH] fix(@angular/build): preserve duplicate bundle filenames across output file types When building applications with SSR and Service Worker enabled, bundle outputs (such as CSS chunks produced from JS imports) can share the same relative path name across different `BuildOutputFileType`s (e.g. `BuildOutputFileType.Browser` and `BuildOutputFileType.ServerApplication`). Previously, build results stored output files in a Record keyed by file path, which caused server files to overwrite browser files of the same name and prevented them from being emitted to the browser output directory. The build `Result` interface now maintains `files` as an array (`ResultFile[]`), and rebuild tracking keys output files using a compound type and path key, ensuring all files are preserved and emitted correctly regardless of type. Closes #33922 --- .../src/builders/application/build-action.ts | 108 +++++++++++------- .../build/src/builders/application/index.ts | 4 +- .../build/src/builders/application/results.ts | 5 +- .../tests/options/service-worker_spec.ts | 37 +++++- .../src/builders/dev-server/vite/index.ts | 20 +--- .../src/builders/dev-server/vite/utils.ts | 13 ++- .../extract-i18n/application-extraction.ts | 13 ++- .../src/builders/karma/application_builder.ts | 2 +- .../src/builders/karma/assets-middleware.ts | 11 +- .../src/builders/karma/progress-reporter.ts | 12 +- .../angular/build/src/builders/karma/utils.ts | 8 +- .../build/src/builders/unit-test/builder.ts | 2 +- .../unit-test/runners/vitest/executor.ts | 8 +- .../tools/esbuild/bundler-execution-result.ts | 9 +- .../angular/build/src/tools/esbuild/utils.ts | 2 +- .../angular/build/src/utils/test-files.ts | 11 +- .../extract-i18n/application-extraction.ts | 13 ++- 17 files changed, 167 insertions(+), 111 deletions(-) diff --git a/packages/angular/build/src/builders/application/build-action.ts b/packages/angular/build/src/builders/application/build-action.ts index e71663765274..3f8d24da6ecc 100644 --- a/packages/angular/build/src/builders/application/build-action.ts +++ b/packages/angular/build/src/builders/application/build-action.ts @@ -9,7 +9,11 @@ import { BuilderContext } from '@angular-devkit/architect'; import { existsSync } from 'node:fs'; import path from 'node:path'; -import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result'; +import { + BuildOutputAsset, + ExecutionResult, + RebuildState, +} from '../../tools/esbuild/bundler-execution-result'; import { BuildOutputFile, BuildOutputFileType } from '../../tools/esbuild/bundler-files'; import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language'; import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils'; @@ -279,34 +283,15 @@ function* emitOutputResults( // Use a full result if there is no rebuild state (no prior build result) if (!rebuildState || !changes) { - const result: FullResult = { - kind: ResultKind.Full, - warnings: warnings as ResultMessage[], - files: {}, - detail: { - externalMetadata, - htmlIndexPath, - htmlBaseHref, - outputOptions, - }, - }; - for (const file of assetFiles) { - result.files[file.destination] = { - type: BuildOutputFileType.Browser, - inputPath: file.source, - origin: 'disk', - }; - } - for (const file of outputFiles) { - result.files[file.path] = { - type: file.type, - contents: file.contents, - origin: 'memory', - hash: file.hash, - }; - } - - yield result; + yield createFullResult( + outputFiles, + assetFiles, + warnings, + outputOptions, + externalMetadata, + htmlIndexPath, + htmlBaseHref, + ); return; } @@ -326,7 +311,7 @@ function* emitOutputResults( added: [], removed: [], modified: [], - files: {}, + files: [], detail: { externalMetadata, htmlIndexPath, @@ -340,9 +325,10 @@ function* emitOutputResults( // Initially assume all previous output files have been removed const removedOutputFiles = new Map(previousOutputInfo); for (const file of outputFiles) { - removedOutputFiles.delete(file.path); + const key = `${file.type}:${file.path}`; + removedOutputFiles.delete(key); - const previousHash = previousOutputInfo.get(file.path)?.hash; + const previousHash = previousOutputInfo.get(key)?.hash; let needFile = false; if (previousHash === undefined) { needFile = true; @@ -359,12 +345,13 @@ function* emitOutputResults( incrementalResult.background = false; } - incrementalResult.files[file.path] = { + incrementalResult.files.push({ + path: file.path, type: file.type, contents: file.contents, origin: 'memory', hash: file.hash, - }; + }); } } @@ -385,11 +372,12 @@ function* emitOutputResults( hasCssUpdates ||= destination.endsWith('.css'); - incrementalResult.files[destination] = { + incrementalResult.files.push({ + path: destination, type: BuildOutputFileType.Browser, inputPath: source, origin: 'disk', - }; + }); } // Do not remove stale files yet if there are template updates. @@ -403,12 +391,12 @@ function* emitOutputResults( // Include the removed output and asset files incrementalResult.removed.push( - ...Array.from(removedOutputFiles, ([file, { type }]) => ({ - path: file, + ...Array.from(removedOutputFiles.values(), ({ type, path }) => ({ + path, type, })), - ...Array.from(removedAssetFiles.values(), (file) => ({ - path: file, + ...Array.from(removedAssetFiles.values(), (path) => ({ + path, type: BuildOutputFileType.Browser, })), ); @@ -425,9 +413,7 @@ function* emitOutputResults( added: incrementalResult.added.filter(isCssFilePath), removed: incrementalResult.removed.filter(({ path }) => isCssFilePath(path)), modified: incrementalResult.modified.filter(isCssFilePath), - files: Object.fromEntries( - Object.entries(incrementalResult.files).filter(([path]) => isCssFilePath(path)), - ), + files: incrementalResult.files.filter((file) => isCssFilePath(file.path)), }; yield styleResult; @@ -446,6 +432,42 @@ function* emitOutputResults( } } +function createFullResult( + outputFiles: readonly BuildOutputFile[], + assetFiles: readonly BuildOutputAsset[], + warnings: readonly unknown[], + outputOptions: NormalizedApplicationBuildOptions['outputOptions'], + externalMetadata: unknown, + htmlIndexPath: unknown, + htmlBaseHref: unknown, +): FullResult { + return { + kind: ResultKind.Full, + warnings: warnings as ResultMessage[], + files: [ + ...assetFiles.map(({ source, destination }) => ({ + path: destination, + type: BuildOutputFileType.Browser, + inputPath: source, + origin: 'disk' as const, + })), + ...outputFiles.map((file) => ({ + path: file.path, + type: file.type, + contents: file.contents, + origin: 'memory' as const, + hash: file.hash, + })), + ], + detail: { + externalMetadata, + htmlIndexPath, + htmlBaseHref, + outputOptions, + }, + }; +} + function isCssFilePath(filePath: string): boolean { return /\.css(?:\.map)?$/i.test(filePath); } diff --git a/packages/angular/build/src/builders/application/index.ts b/packages/angular/build/src/builders/application/index.ts index b31fc0a8f81b..59ded1cd8182 100644 --- a/packages/angular/build/src/builders/application/index.ts +++ b/packages/angular/build/src/builders/application/index.ts @@ -199,7 +199,7 @@ export async function* buildApplication( // Writes the output files to disk and ensures the containing directories are present const directoryExists = new Set(); try { - await emitFilesToDisk(Object.entries(result.files), async ([filePath, file]) => { + await emitFilesToDisk(result.files, async (file) => { if ( outputOptions.ignoreServer && (file.type === BuildOutputFileType.ServerApplication || @@ -208,7 +208,7 @@ export async function* buildApplication( return; } - const fullFilePath = generateFullPath(filePath, file.type, outputOptions); + const fullFilePath = generateFullPath(file.path, file.type, outputOptions); // Ensure output subdirectories exist const fileBasePath = path.dirname(fullFilePath); diff --git a/packages/angular/build/src/builders/application/results.ts b/packages/angular/build/src/builders/application/results.ts index 6fa23f9f19cd..8e95176639f5 100644 --- a/packages/angular/build/src/builders/application/results.ts +++ b/packages/angular/build/src/builders/application/results.ts @@ -31,7 +31,7 @@ export interface FailureResult extends BaseResult { export interface FullResult extends BaseResult { kind: ResultKind.Full; - files: Record; + files: ResultFile[]; } export interface IncrementalResult extends BaseResult { @@ -40,13 +40,14 @@ export interface IncrementalResult extends BaseResult { added: string[]; removed: { path: string; type: BuildOutputFileType }[]; modified: string[]; - files: Record; + files: ResultFile[]; } export type ResultFile = DiskFile | MemoryFile; export interface BaseResultFile { origin: 'memory' | 'disk'; + path: string; type: BuildOutputFileType; } diff --git a/packages/angular/build/src/builders/application/tests/options/service-worker_spec.ts b/packages/angular/build/src/builders/application/tests/options/service-worker_spec.ts index 958cd5007960..444babc37a02 100644 --- a/packages/angular/build/src/builders/application/tests/options/service-worker_spec.ts +++ b/packages/angular/build/src/builders/application/tests/options/service-worker_spec.ts @@ -19,7 +19,7 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { name: 'app', installMode: 'prefetch', resources: { - files: ['/favicon.ico', '/index.html'], + files: ['/favicon.ico', '/index.html', '/*.css', '/*.js'], }, }, { @@ -88,5 +88,40 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => { const config = await harness.readFile('dist/browser/ngsw.json'); expect(JSON.parse(config)).toEqual(jasmine.objectContaining({ index: '/index.csr.html' })); }); + + it('should write JS-imported CSS chunk to browser dist when SSR is enabled', async () => { + await harness.modifyFile('src/tsconfig.app.json', (content) => { + const tsConfig = JSON.parse(content); + tsConfig.files ??= []; + tsConfig.files.push('main.server.ts', 'server.ts', 'extra.d.ts'); + + return JSON.stringify(tsConfig); + }); + + await harness.writeFile('src/extra.d.ts', `declare module '*.css';`); + await harness.writeFile('src/server.ts', `console.log('Server!');`); + await harness.writeFile('src/extra.css', `body { color: red; }`); + await harness.modifyFile('src/main.ts', (content) => `import './extra.css';\n${content}`); + + harness.useTarget('build', { + ...BASE_OPTIONS, + server: 'src/main.server.ts', + ssr: { entry: 'src/server.ts' }, + serviceWorker: true, + }); + + const { result } = await harness.executeOnce(); + expect(result?.success).toBeTrue(); + + const config = JSON.parse(harness.readFile('dist/browser/ngsw.json')); + const hashTable = config.hashTable as Record; + + const cssChunkUrls = Object.keys(hashTable).filter((url) => url.endsWith('.css')); + expect(cssChunkUrls.length).toBeGreaterThan(0); + + for (const url of Object.keys(hashTable)) { + harness.expectFile(`dist/browser${url}`).toExist(); + } + }); }); }); diff --git a/packages/angular/build/src/builders/dev-server/vite/index.ts b/packages/angular/build/src/builders/dev-server/vite/index.ts index 94192ec41b8c..8c8f23c3a339 100644 --- a/packages/angular/build/src/builders/dev-server/vite/index.ts +++ b/packages/angular/build/src/builders/dev-server/vite/index.ts @@ -262,9 +262,8 @@ export async function* serveWithVite( componentStyles.clear(); generatedFiles.clear(); - for (const [outputPath, file] of Object.entries(result.files)) { + for (const file of result.files) { updateResultRecord( - outputPath, file, normalizePath, htmlIndexPath, @@ -292,22 +291,9 @@ export async function* serveWithVite( assetFiles.delete(filePath); } - for (const modified of result.modified) { + for (const file of result.files) { updateResultRecord( - modified, - result.files[modified], - normalizePath, - htmlIndexPath, - generatedFiles, - assetFiles, - componentStyles, - ); - } - - for (const added of result.added) { - updateResultRecord( - added, - result.files[added], + file, normalizePath, htmlIndexPath, generatedFiles, diff --git a/packages/angular/build/src/builders/dev-server/vite/utils.ts b/packages/angular/build/src/builders/dev-server/vite/utils.ts index e1e6b4f96847..bc2d4a513b5a 100644 --- a/packages/angular/build/src/builders/dev-server/vite/utils.ts +++ b/packages/angular/build/src/builders/dev-server/vite/utils.ts @@ -30,7 +30,6 @@ export interface DevServerExternalResultMetadata extends Omit string, htmlIndexPath: string, @@ -40,7 +39,7 @@ export function updateResultRecord( initial = false, ): void { if (file.origin === 'disk') { - assetFiles.set('/' + normalizePath(outputPath), { + assetFiles.set('/' + normalizePath(file.path), { source: normalizePath(file.inputPath), updated: !initial, }); @@ -49,12 +48,12 @@ export function updateResultRecord( } let filePath; - if (outputPath === htmlIndexPath) { + if (file.path === htmlIndexPath) { // Convert custom index output path to standard index path for dev-server usage. // This mimics the Webpack dev-server behavior. filePath = '/index.html'; } else { - filePath = '/' + normalizePath(outputPath); + filePath = '/' + normalizePath(file.path); } const servable = @@ -74,6 +73,12 @@ export function updateResultRecord( return; } + // Avoid overwriting a servable browser file with a non-servable server file of the same path (e.g. CSS chunks) + const existing = generatedFiles.get(filePath); + if (existing?.servable && !servable) { + return; + } + // New or updated file generatedFiles.set(filePath, { contents: file.contents, diff --git a/packages/angular/build/src/builders/extract-i18n/application-extraction.ts b/packages/angular/build/src/builders/extract-i18n/application-extraction.ts index e7f8bee10c32..ff223ba0bd16 100644 --- a/packages/angular/build/src/builders/extract-i18n/application-extraction.ts +++ b/packages/angular/build/src/builders/extract-i18n/application-extraction.ts @@ -65,12 +65,12 @@ export async function extractMessages( // Extract messages from each output JavaScript file. // Output files are only present on a successful build. - for (const filePath of Object.keys(builderResult.files)) { - if (!filePath.endsWith('.js')) { + for (const file of builderResult.files) { + if (!file.path.endsWith('.js')) { continue; } - const fileMessages = extractor.extractMessages(filePath); + const fileMessages = extractor.extractMessages(file.path); messages.push(...fileMessages); } @@ -88,9 +88,10 @@ export async function extractMessages( function setupLocalizeExtractor( extractorConstructor: typeof MessageExtractor, - files: Record, + files: readonly ResultFile[], context: BuilderContext, ): MessageExtractor { + const fileMap = new Map(files.map((file) => [file.path, file])); const textDecoder = new TextDecoder(); // Setup a virtual file system instance for the extractor // * MessageExtractor itself uses readFile, relative and resolve @@ -100,7 +101,7 @@ function setupLocalizeExtractor( // Output files are stored as relative to the workspace root const requestedPath = nodePath.relative(context.workspaceRoot, path); - const file = files[requestedPath]; + const file = fileMap.get(requestedPath); let content; if (file?.origin === 'memory') { content = textDecoder.decode(file.contents); @@ -123,7 +124,7 @@ function setupLocalizeExtractor( // Output files are stored as relative to the workspace root const requestedPath = nodePath.relative(context.workspaceRoot, path); - return files[requestedPath] !== undefined; + return fileMap.has(requestedPath); }, dirname(path: string): string { return nodePath.dirname(path); diff --git a/packages/angular/build/src/builders/karma/application_builder.ts b/packages/angular/build/src/builders/karma/application_builder.ts index d0adb6c3cf0e..674a7db0e4be 100644 --- a/packages/angular/build/src/builders/karma/application_builder.ts +++ b/packages/angular/build/src/builders/karma/application_builder.ts @@ -386,7 +386,7 @@ async function configureKarma( ); } - parsedKarmaConfig.plugins.push(AngularAssetsMiddleware.createPlugin(buildOutput)); + parsedKarmaConfig.plugins.push(AngularAssetsMiddleware.createPlugin(buildOutput.files)); parsedKarmaConfig.middleware ??= []; parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME); diff --git a/packages/angular/build/src/builders/karma/assets-middleware.ts b/packages/angular/build/src/builders/karma/assets-middleware.ts index fd6ce489e583..45483cb4ec41 100644 --- a/packages/angular/build/src/builders/karma/assets-middleware.ts +++ b/packages/angular/build/src/builders/karma/assets-middleware.ts @@ -25,7 +25,7 @@ interface ServeFileFunction { } export interface LatestBuildFiles { - files: Record; + files: Map; } const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles'; @@ -49,7 +49,7 @@ export class AngularAssetsMiddleware { pathname = pathname.replaceAll(path.posix.sep, path.win32.sep); } - const file = this.latestBuildFiles.files[pathname]; + const file = this.latestBuildFiles.files.get(pathname); if (!file) { next(); @@ -76,9 +76,12 @@ export class AngularAssetsMiddleware { } } - static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef { + static createPlugin(initialFiles: readonly ResultFile[]): InlinePluginDef { return { - [LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }], + [LATEST_BUILD_FILES_TOKEN]: [ + 'value', + { files: new Map(initialFiles.map((file) => [file.path, file])) }, + ], [`middleware:${AngularAssetsMiddleware.NAME}`]: [ 'factory', diff --git a/packages/angular/build/src/builders/karma/progress-reporter.ts b/packages/angular/build/src/builders/karma/progress-reporter.ts index 6fc92b6c74c8..e7d40be15cfd 100644 --- a/packages/angular/build/src/builders/karma/progress-reporter.ts +++ b/packages/angular/build/src/builders/karma/progress-reporter.ts @@ -76,12 +76,14 @@ export function injectKarmaReporter( buildOutput.kind === ResultKind.Full ) { if (buildOutput.kind === ResultKind.Full) { - this.latestBuildFiles.files = buildOutput.files; + this.latestBuildFiles.files.clear(); } else { - this.latestBuildFiles.files = { - ...this.latestBuildFiles.files, - ...buildOutput.files, - }; + for (const { path } of buildOutput.removed) { + this.latestBuildFiles.files.delete(path); + } + } + for (const file of buildOutput.files) { + this.latestBuildFiles.files.set(file.path, file); } await writeTestFiles(buildOutput.files, buildOptions.outputPath); this.emitter.refreshFiles(); diff --git a/packages/angular/build/src/builders/karma/utils.ts b/packages/angular/build/src/builders/karma/utils.ts index 9a8aa749c269..ecdda5516cbd 100644 --- a/packages/angular/build/src/builders/karma/utils.ts +++ b/packages/angular/build/src/builders/karma/utils.ts @@ -8,8 +8,8 @@ import type { BuilderContext } from '@angular-devkit/architect'; import { createRequire } from 'node:module'; -import { BuildOutputFileType } from '../../tools/esbuild/bundler-files'; import { getProjectRootPaths } from '../../utils/project-metadata'; +import type { ResultFile } from '../application/results'; import { findTests, getTestEntrypoints } from './find-tests'; import type { NormalizedKarmaBuilderOptions } from './options'; @@ -62,9 +62,9 @@ export async function collectEntrypoints( return getTestEntrypoints(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot }); } -export function hasChunkOrWorkerFiles(files: Record): boolean { - return Object.keys(files).some((filename) => { - return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(filename); +export function hasChunkOrWorkerFiles(files: readonly ResultFile[]): boolean { + return files.some((file) => { + return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(file.path); }); } diff --git a/packages/angular/build/src/builders/unit-test/builder.ts b/packages/angular/build/src/builders/unit-test/builder.ts index 146ce6654bd0..755b91c40544 100644 --- a/packages/angular/build/src/builders/unit-test/builder.ts +++ b/packages/angular/build/src/builders/unit-test/builder.ts @@ -227,7 +227,7 @@ export async function* execute( await using executor = await runner.createExecutor(context, normalizedOptions, undefined); yield* executor.execute({ kind: ResultKind.Full, - files: {}, + files: [], }); } catch (e) { assertIsError(e); diff --git a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts index ac2bfe20fa38..fc64e432e19f 100644 --- a/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts +++ b/packages/angular/build/src/builders/unit-test/runners/vitest/executor.ts @@ -104,8 +104,8 @@ export class VitestExecutor implements TestExecutor { if (buildResult.kind === ResultKind.Full) { this.buildResultFiles.clear(); - for (const [path, file] of Object.entries(buildResult.files)) { - this.buildResultFiles.set(this.normalizePath(path), file); + for (const file of buildResult.files) { + this.buildResultFiles.set(this.normalizePath(file.path), file); } this.debugLog( DebugLogLevel.Info, @@ -124,8 +124,8 @@ export class VitestExecutor implements TestExecutor { for (const file of buildResult.removed) { this.buildResultFiles.delete(this.normalizePath(file.path)); } - for (const [path, file] of Object.entries(buildResult.files)) { - this.buildResultFiles.set(this.normalizePath(path), file); + for (const file of buildResult.files) { + this.buildResultFiles.set(this.normalizePath(file.path), file); } } diff --git a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts index 53e17e7e427c..eeb7045dfb61 100644 --- a/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts +++ b/packages/angular/build/src/tools/esbuild/bundler-execution-result.ts @@ -27,7 +27,10 @@ export interface RebuildState { componentStyleBundler: ComponentStylesheetBundler; codeBundleCache?: SourceFileCache; fileChanges: ChangedFiles; - previousOutputInfo: ReadonlyMap; + previousOutputInfo: ReadonlyMap< + string, + { hash: string; type: BuildOutputFileType; path: string } + >; previousAssetsInfo: ReadonlyMap; templateUpdates?: Map; } @@ -171,7 +174,7 @@ export class ExecutionResult { componentStyleBundler: this.componentStyleBundler, fileChanges, previousOutputInfo: new Map( - this.outputFiles.map(({ path, hash, type }) => [path, { hash, type }]), + this.outputFiles.map(({ path, hash, type }) => [`${type}:${path}`, { hash, type, path }]), ), previousAssetsInfo: new Map( this.assetFiles.map(({ source, destination }) => [source, destination]), @@ -185,7 +188,7 @@ export class ExecutionResult { ): Set { const changed = new Set(); for (const file of this.outputFiles) { - const previousHash = previousOutputHashes.get(file.path)?.hash; + const previousHash = previousOutputHashes.get(`${file.type}:${file.path}`)?.hash; if (previousHash === undefined || previousHash !== file.hash) { changed.add(file.path); } diff --git a/packages/angular/build/src/tools/esbuild/utils.ts b/packages/angular/build/src/tools/esbuild/utils.ts index 7d9dce4a5522..3bbaabdd2400 100644 --- a/packages/angular/build/src/tools/esbuild/utils.ts +++ b/packages/angular/build/src/tools/esbuild/utils.ts @@ -214,7 +214,7 @@ export function getFeatureSupport(nativeAsyncAwait: boolean): BuildOptions['supp const MAX_CONCURRENT_WRITES = 64; export async function emitFilesToDisk( - files: T[], + files: readonly T[], writeFileCallback: (file: T) => Promise, ): Promise { // Write files in groups of MAX_CONCURRENT_WRITES to avoid too many open files diff --git a/packages/angular/build/src/utils/test-files.ts b/packages/angular/build/src/utils/test-files.ts index d5f0edb2815d..6790d007e220 100644 --- a/packages/angular/build/src/utils/test-files.ts +++ b/packages/angular/build/src/utils/test-files.ts @@ -17,21 +17,18 @@ import { emitFilesToDisk } from '../tools/esbuild/utils'; * This function handles both in-memory and on-disk files, creating subdirectories * as needed. * - * @param files A map of file paths to `ResultFile` objects, representing the build output. + * @param files A collection of `ResultFile` objects, representing the build output. * @param testDir The absolute path to the directory where the files should be written. */ -export async function writeTestFiles( - files: Record, - testDir: string, -): Promise { +export async function writeTestFiles(files: readonly ResultFile[], testDir: string): Promise { const directoryExists = new Set(); // Writes the test related output files to disk and ensures the containing directories are present - await emitFilesToDisk(Object.entries(files), async ([filePath, file]) => { + await emitFilesToDisk(files, async (file) => { if (file.type !== BuildOutputFileType.Browser && file.type !== BuildOutputFileType.Media) { return; } - const fullFilePath = path.join(testDir, filePath); + const fullFilePath = path.join(testDir, file.path); // Ensure output subdirectories exist const fileBasePath = path.dirname(fullFilePath); diff --git a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts index fb6e9138982e..8de8b6863aaf 100644 --- a/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts +++ b/packages/angular_devkit/build_angular/src/builders/extract-i18n/application-extraction.ts @@ -70,12 +70,12 @@ export async function extractMessages( // Extract messages from each output JavaScript file. // Output files are only present on a successful build. - for (const filePath of Object.keys(builderResult.files)) { - if (!filePath.endsWith('.js')) { + for (const file of builderResult.files) { + if (!file.path.endsWith('.js')) { continue; } - const fileMessages = extractor.extractMessages(filePath); + const fileMessages = extractor.extractMessages(file.path); messages.push(...fileMessages); } @@ -93,9 +93,10 @@ export async function extractMessages( function setupLocalizeExtractor( extractorConstructor: typeof MessageExtractor, - files: Record, + files: readonly ResultFile[], context: BuilderContext, ): MessageExtractor { + const fileMap = new Map(files.map((file) => [file.path, file])); const textDecoder = new TextDecoder(); // Setup a virtual file system instance for the extractor // * MessageExtractor itself uses readFile, relative and resolve @@ -105,7 +106,7 @@ function setupLocalizeExtractor( // Output files are stored as relative to the workspace root const requestedPath = nodePath.relative(context.workspaceRoot, path); - const file = files[requestedPath]; + const file = fileMap.get(requestedPath); let content; if (file?.origin === 'memory') { content = textDecoder.decode(file.contents); @@ -128,7 +129,7 @@ function setupLocalizeExtractor( // Output files are stored as relative to the workspace root const requestedPath = nodePath.relative(context.workspaceRoot, path); - return files[requestedPath] !== undefined; + return fileMap.has(requestedPath); }, dirname(path: string): string { return nodePath.dirname(path);