Skip to content

Commit

Permalink
fix(@angular/build): remove deleted assets from output during watch mode
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
alan-agius4 committed Jan 10, 2025
1 parent 834c2dc commit 8fa682e
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 18 deletions.
49 changes: 33 additions & 16 deletions packages/angular/build/src/builders/application/build-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { BuildOutputFileType } from '../../tools/esbuild/bundler-context';
import { ExecutionResult, RebuildState } from '../../tools/esbuild/bundler-execution-result';
import { shutdownSassWorkerPool } from '../../tools/esbuild/stylesheets/sass-language';
import { logMessages, withNoProgress, withSpinner } from '../../tools/esbuild/utils';
import { ChangedFiles } from '../../tools/esbuild/watcher';
import { shouldWatchRoot } from '../../utils/environment-options';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
Expand Down Expand Up @@ -199,7 +200,8 @@ export async function* runEsBuildBuildAction(
for (const outputResult of emitOutputResults(
result,
outputOptions,
incrementalResults ? rebuildState.previousOutputInfo : undefined,
changes,
incrementalResults ? rebuildState : undefined,
)) {
yield outputResult;
}
Expand All @@ -224,7 +226,8 @@ function* emitOutputResults(
templateUpdates,
}: ExecutionResult,
outputOptions: NormalizedApplicationBuildOptions['outputOptions'],
previousOutputInfo?: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>,
changes?: ChangedFiles,
rebuildState?: RebuildState,
): Iterable<Result> {
if (errors.length > 0) {
yield {
Expand Down Expand Up @@ -255,7 +258,9 @@ function* emitOutputResults(
}

// Use an incremental result if previous output information is available
if (previousOutputInfo) {
if (rebuildState && changes) {
const { previousAssetsInfo, previousOutputInfo } = rebuildState;

const incrementalResult: IncrementalResult = {
kind: ResultKind.Incremental,
warnings: warnings as ResultMessage[],
Expand All @@ -273,7 +278,6 @@ 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);

Expand Down Expand Up @@ -304,24 +308,37 @@ function* emitOutputResults(
}
}

// Include the removed output files
// Initially assume all previous assets files have been removed
const removedAssetFiles = new Map(previousAssetsInfo);
for (const { source, destination } of assetFiles) {
removedAssetFiles.delete(source);

if (changes.modified.has(source)) {
incrementalResult.modified.push(destination);
} else if (!previousAssetsInfo.has(source)) {
incrementalResult.added.push(destination);
} else {
continue;
}

incrementalResult.files[destination] = {
type: BuildOutputFileType.Browser,
inputPath: source,
origin: 'disk',
};
}

// Include the removed output and asset files
incrementalResult.removed.push(
...Array.from(removedOutputFiles, ([file, { type }]) => ({
path: file,
type,
})),
);

// Always consider asset files as added to ensure new/modified assets are available.
// TODO: Consider more comprehensive asset analysis.
for (const file of assetFiles) {
incrementalResult.added.push(file.destination);
incrementalResult.files[file.destination] = {
...Array.from(removedAssetFiles.values(), (file) => ({
path: file,
type: BuildOutputFileType.Browser,
inputPath: file.source,
origin: 'disk',
};
}
})),
);

yield incrementalResult;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,50 @@ describeBuilder(buildApplication, APPLICATION_BUILDER_INFO, (harness) => {

expect(buildCount).toBe(2);
});

it('remove deleted asset from output', async () => {
await Promise.all([
harness.writeFile('public/asset-two.txt', 'bar'),
harness.writeFile('public/asset-one.txt', 'foo'),
]);

harness.useTarget('build', {
...BASE_OPTIONS,
assets: [
{
glob: '**/*',
input: 'public',
},
],
watch: true,
});

const buildCount = await harness
.execute({ outputLogsOnFailure: false })
.pipe(
timeout(BUILD_TIMEOUT),
concatMap(async ({ result }, index) => {
switch (index) {
case 0:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toExist();

await harness.removeFile('public/asset-two.txt');
break;
case 1:
expect(result?.success).toBeTrue();
harness.expectFile('dist/browser/asset-one.txt').toExist();
harness.expectFile('dist/browser/asset-two.txt').toNotExist();
break;
}
}),
take(2),
count(),
)
.toPromise();

expect(buildCount).toBe(2);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export interface RebuildState {
componentStyleBundler: ComponentStylesheetBundler;
codeBundleCache?: SourceFileCache;
fileChanges: ChangedFiles;
previousOutputInfo: Map<string, { hash: string; type: BuildOutputFileType }>;
previousOutputInfo: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>;
previousAssetsInfo: ReadonlyMap<string, string>;
templateUpdates?: Map<string, string>;
}

Expand Down Expand Up @@ -172,12 +173,15 @@ export class ExecutionResult {
previousOutputInfo: new Map(
this.outputFiles.map(({ path, hash, type }) => [path, { hash, type }]),
),
previousAssetsInfo: new Map(
this.assetFiles.map(({ source, destination }) => [source, destination]),
),
templateUpdates: this.templateUpdates,
};
}

findChangedFiles(
previousOutputHashes: Map<string, { hash: string; type: BuildOutputFileType }>,
previousOutputHashes: ReadonlyMap<string, { hash: string; type: BuildOutputFileType }>,
): Set<string> {
const changed = new Set<string>();
for (const file of this.outputFiles) {
Expand Down

0 comments on commit 8fa682e

Please sign in to comment.