Skip to content

Commit 8fa682e

Browse files
committedJan 10, 2025
fix(@angular/build): remove deleted assets from output during watch mode
This commit ensures that assets deleted from the source are also removed from the output directory while in watch mode. Previously, deleted assets could persist in the output folder, potentially causing inconsistencies or outdated files to be served. This fix improves the accuracy of the build output by maintaining synchronization between the source and the output directory during development. (cherry picked from commit 6ebce49)
1 parent 834c2dc commit 8fa682e

File tree

3 files changed

+84
-18
lines changed

3 files changed

+84
-18
lines changed
 

‎packages/angular/build/src/builders/application/build-action.ts

+33-16
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
1313
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
1414
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
1515
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
16+
import { ChangedFiles } from '../../tools/esbuild/watcher';
1617
import { shouldWatchRoot } from '../../utils/environment-options';
1718
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
1819
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
@@ -199,7 +200,8 @@ export async function* runEsBuildBuildAction(
199200
for (const outputResult of emitOutputResults(
200201
result,
201202
outputOptions,
202-
incrementalResults ? rebuildState.previousOutputInfo : undefined,
203+
changes,
204+
incrementalResults ? rebuildState : undefined,
203205
)) {
204206
yield outputResult;
205207
}
@@ -224,7 +226,8 @@ function* emitOutputResults(
224226
templateUpdates,
225227
}: ExecutionResult,
226228
outputOptions: NormalizedApplicationBuildOptions['outputOptions'],
227-
previousOutputInfo?: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>,
229+
changes?: ChangedFiles,
230+
rebuildState?: RebuildState,
228231
): Iterable<Result> {
229232
if (errors.length > 0) {
230233
yield {
@@ -255,7 +258,9 @@ function* emitOutputResults(
255258
}
256259

257260
// Use an incremental result if previous output information is available
258-
if (previousOutputInfo) {
261+
if (rebuildState && changes) {
262+
const { previousAssetsInfo, previousOutputInfo } = rebuildState;
263+
259264
const incrementalResult: IncrementalResult = {
260265
kind: ResultKind.Incremental,
261266
warnings: warnings as ResultMessage[],
@@ -273,7 +278,6 @@ function* emitOutputResults(
273278

274279
// Initially assume all previous output files have been removed
275280
const removedOutputFiles = new Map(previousOutputInfo);
276-
277281
for (const file of outputFiles) {
278282
removedOutputFiles.delete(file.path);
279283

@@ -304,24 +308,37 @@ function* emitOutputResults(
304308
}
305309
}
306310

307-
// Include the removed output files
311+
// Initially assume all previous assets files have been removed
312+
const removedAssetFiles = new Map(previousAssetsInfo);
313+
for (const { source, destination } of assetFiles) {
314+
removedAssetFiles.delete(source);
315+
316+
if (changes.modified.has(source)) {
317+
incrementalResult.modified.push(destination);
318+
} else if (!previousAssetsInfo.has(source)) {
319+
incrementalResult.added.push(destination);
320+
} else {
321+
continue;
322+
}
323+
324+
incrementalResult.files[destination] = {
325+
type: BuildOutputFileType.Browser,
326+
inputPath: source,
327+
origin: 'disk',
328+
};
329+
}
330+
331+
// Include the removed output and asset files
308332
incrementalResult.removed.push(
309333
...Array.from(removedOutputFiles, ([file, { type }]) => ({
310334
path: file,
311335
type,
312336
})),
313-
);
314-
315-
// Always consider asset files as added to ensure new/modified assets are available.
316-
// TODO: Consider more comprehensive asset analysis.
317-
for (const file of assetFiles) {
318-
incrementalResult.added.push(file.destination);
319-
incrementalResult.files[file.destination] = {
337+
...Array.from(removedAssetFiles.values(), (file) => ({
338+
path: file,
320339
type: BuildOutputFileType.Browser,
321-
inputPath: file.source,
322-
origin: 'disk',
323-
};
324-
}
340+
})),
341+
);
325342

326343
yield incrementalResult;
327344

‎packages/angular/build/src/builders/application/tests/behavior/rebuild-assets_spec.ts

+45
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,50 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {
6161

6262
expect(buildCount).toBe(2);
6363
});
64+
65+
it('remove deleted asset from output', async () => {
66+
await Promise.all([
67+
harness.writeFile('public/asset-two.txt', 'bar'),
68+
harness.writeFile('public/asset-one.txt', 'foo'),
69+
]);
70+
71+
harness.useTarget('build', {
72+
...BASE_OPTIONS,
73+
assets: [
74+
{
75+
glob: '**/*',
76+
input: 'public',
77+
},
78+
],
79+
watch: true,
80+
});
81+
82+
const buildCount = await harness
83+
.execute({ outputLogsOnFailure: false })
84+
.pipe(
85+
timeout(BUILD_TIMEOUT),
86+
concatMap(async ({ result }, index) => {
87+
switch (index) {
88+
case 0:
89+
expect(result?.success).toBeTrue();
90+
harness.expectFile('dist/browser/asset-one.txt').toExist();
91+
harness.expectFile('dist/browser/asset-two.txt').toExist();
92+
93+
await harness.removeFile('public/asset-two.txt');
94+
break;
95+
case 1:
96+
expect(result?.success).toBeTrue();
97+
harness.expectFile('dist/browser/asset-one.txt').toExist();
98+
harness.expectFile('dist/browser/asset-two.txt').toNotExist();
99+
break;
100+
}
101+
}),
102+
take(2),
103+
count(),
104+
)
105+
.toPromise();
106+
107+
expect(buildCount).toBe(2);
108+
});
64109
});
65110
});

‎packages/angular/build/src/tools/esbuild/bundler-execution-result.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export interface RebuildState {
2727
componentStyleBundler: ComponentStylesheetBundler;
2828
codeBundleCache?: SourceFileCache;
2929
fileChanges: ChangedFiles;
30-
previousOutputInfo: Map<string, { hash: string; type: BuildOutputFileType }>;
30+
previousOutputInfo: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>;
31+
previousAssetsInfo: ReadonlyMap<string, string>;
3132
templateUpdates?: Map<string, string>;
3233
}
3334

@@ -172,12 +173,15 @@ export class ExecutionResult {
172173
previousOutputInfo: new Map(
173174
this.outputFiles.map(({ path, hash, type }) => [path, { hash, type }]),
174175
),
176+
previousAssetsInfo: new Map(
177+
this.assetFiles.map(({ source, destination }) => [source, destination]),
178+
),
175179
templateUpdates: this.templateUpdates,
176180
};
177181
}
178182

179183
findChangedFiles(
180-
previousOutputHashes: Map<string, { hash: string; type: BuildOutputFileType }>,
184+
previousOutputHashes: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>,
181185
): Set<string> {
182186
const changed = new Set<string>();
183187
for (const file of this.outputFiles) {

0 commit comments

Comments
 (0)
Please sign in to comment.