Skip to content

Commit a0c5d91

Browse files
committed
fix(@angular/build): support incremental asset updates and output cleanup in library watch mode
Ensure the library builder properly handles created, modified, and deleted assets during watch mode rebuilds, and removes stale bundle and manifest output files when entry points or exports change. Also normalize paths across the library builder pipeline to POSIX format.
1 parent 0b9a52c commit a0c5d91

14 files changed

Lines changed: 640 additions & 194 deletions

File tree

‎packages/angular/build/src/builders/library/builder.ts‎

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ import { getSupportedBrowsers } from '../../utils/supported-browsers';
2424
import { assertCompatibleAngularVersion } from '../../utils/version';
2525
import type { BuildWatcher } from '../../utils/watcher';
2626
import { normalizeLibraryOptions } from './options';
27-
import type { BuildActionResult } from './pipeline/build-action';
27+
import type {
28+
BuildActionContext,
29+
BuildActionResult,
30+
buildAction,
31+
hasModifiedWatchedFile,
32+
} from './pipeline/build-action';
2833
import { updateWatchedEntryPoints } from './pipeline/entry-points';
2934
import type { createComponentStylesheetBundlerForLibrary } from './pipeline/stylesheet-bundler';
3035
import { loadPackageJson } from './pipeline/utils';
@@ -111,13 +116,10 @@ export async function* executeLibraryBuilder(
111116
);
112117

113118
// Track all referenced compilation files for watch mode
114-
const watchedCompilationFiles = new Set<string>([
115-
toPosixPath(tsConfigPath),
116-
toPosixPath(packageJsonPath),
117-
]);
119+
const watchedCompilationFiles = new Set<string>([tsConfigPath, packageJsonPath]);
118120

119121
for (const entryPoint of normalizedOptions.entryPoints.values()) {
120-
watchedCompilationFiles.add(toPosixPath(entryPoint.entryFilePath));
122+
watchedCompilationFiles.add(entryPoint.entryFilePath);
121123
}
122124

123125
if (isWatchMode) {
@@ -130,7 +132,10 @@ export async function* executeLibraryBuilder(
130132
poll,
131133
preserveSymlinks,
132134
signal,
133-
watchFiles: watchedCompilationFiles,
135+
watchFiles: [
136+
...watchedCompilationFiles,
137+
...normalizedOptions.assets.map((asset) => asset.input),
138+
],
134139
});
135140

136141
context.addTeardown?.(() => void watcher?.close());
@@ -194,21 +199,21 @@ export async function* executeLibraryBuilder(
194199
* @param actionContext The build action context and state.
195200
* @param withProgress Progress wrapper function (`withSpinner` or `withNoProgress`).
196201
* @param watcher Optional file watcher for watch mode.
197-
* @param buildAction The library build action function to execute.
202+
* @param buildActionFn The library build action function to execute.
198203
* @returns A promise resolving to the Architect `BuilderOutput`.
199204
*/
200205
async function executeBuild(
201206
message: string,
202-
actionContext: import('./pipeline/build-action').BuildActionContext,
207+
actionContext: BuildActionContext,
203208
withProgress: typeof withSpinner,
204209
watcher: BuildWatcher | undefined,
205-
buildAction: typeof import('./pipeline/build-action').buildAction,
210+
buildActionFn: typeof buildAction,
206211
): Promise<BuilderOutput> {
207212
const startTime = process.hrtime.bigint();
208213
const { context, watchedCompilationFiles, isWatchMode, options } = actionContext;
209214

210215
try {
211-
const result = await withProgress(message, () => buildAction(actionContext));
216+
const result = await withProgress(message, () => buildActionFn(actionContext));
212217
logMessages(context.logger, result);
213218

214219
const success = result.errors.length === 0;
@@ -227,16 +232,27 @@ async function executeBuild(
227232
if (watcher) {
228233
watcher.add(Array.from(watchedCompilationFiles));
229234

230-
const { assetsToEmit } = actionContext;
231-
if (assetsToEmit?.length) {
232-
watcher.add(assetsToEmit.map((asset) => asset.source));
235+
const { collectedAssets } = actionContext;
236+
if (collectedAssets?.filesToEmit.length) {
237+
watcher.add(collectedAssets.filesToEmit.map((asset) => asset.source));
233238
}
234239
}
235240
}
236241
}
237242

238243
/**
239244
* Runs the watch loop, rebuilding the library as watched files are modified.
245+
*
246+
* @param watcher The file watcher instance.
247+
* @param options The normalized library builder options with POSIX paths.
248+
* @param stylesheetBundler The component stylesheet bundler instance.
249+
* @param watchedCompilationFiles Set of POSIX file paths tracked for compilation.
250+
* @param context The Architect builder context.
251+
* @param withProgress Progress wrapper function (`withSpinner` or `withNoProgress`).
252+
* @param buildState The state preserved across incremental builds.
253+
* @param buildActionFn The library build action function to execute.
254+
* @param hasModifiedWatchedFileFn Helper to check whether watched compilation files changed.
255+
* @param signal Optional abort signal to terminate the watch loop.
240256
*/
241257
async function* runWatchLoop(
242258
watcher: BuildWatcher,
@@ -246,14 +262,13 @@ async function* runWatchLoop(
246262
context: BuilderContext,
247263
withProgress: typeof withSpinner,
248264
buildState: SingleBuildState,
249-
buildAction: typeof import('./pipeline/build-action').buildAction,
250-
hasModifiedWatchedFile: typeof import('./pipeline/build-action').hasModifiedWatchedFile,
265+
buildActionFn: typeof buildAction,
266+
hasModifiedWatchedFileFn: typeof hasModifiedWatchedFile,
251267
signal?: AbortSignal,
252268
): AsyncIterableIterator<BuilderOutput> {
253269
const { collectAssetsToEmit } = await import('./pipeline/assets');
254270

255271
const { workspaceRoot, packageJsonPath, assets, clearScreen } = options;
256-
const posixPackageJsonPath = toPosixPath(packageJsonPath);
257272

258273
for await (const changes of watcher) {
259274
if (signal?.aborted) {
@@ -293,13 +308,13 @@ async function* runWatchLoop(
293308

294309
// Check if package.json was modified
295310
let hasPackageJsonChanges = false;
296-
if (changedFiles.has(posixPackageJsonPath)) {
311+
if (changedFiles.has(packageJsonPath)) {
297312
try {
298313
const packageJson = await loadPackageJson(packageJsonPath);
299-
options.packageJson = packageJson;
300314
if (!packageJson.name) {
301315
throw new Error(`The package.json at '${packageJsonPath}' must contain a 'name'.`);
302316
}
317+
options.packageJson = packageJson;
303318
options.packageName = packageJson.name;
304319

305320
updateWatchedEntryPoints(
@@ -328,15 +343,21 @@ async function* runWatchLoop(
328343
!buildState.singleProgramCache ||
329344
buildState.hasCompilationError ||
330345
buildState.hasEntryPointsChanges ||
331-
hasModifiedWatchedFile(changedFiles, watchedCompilationFiles, posixPackageJsonPath);
346+
hasModifiedWatchedFileFn(changedFiles, watchedCompilationFiles, packageJsonPath);
332347

333-
const assetsToEmit = await collectAssetsToEmit(
348+
const collectedAssets = await collectAssetsToEmit(
334349
assets,
335350
workspaceRoot,
336351
buildState.hasEmittedAssets ? changedFiles : undefined,
352+
buildState.emittedAssets,
337353
);
338354

339-
if (!hasSourceChanges && !hasPackageJsonChanges && assetsToEmit.length === 0) {
355+
if (
356+
!hasSourceChanges &&
357+
!hasPackageJsonChanges &&
358+
collectedAssets.filesToEmit.length === 0 &&
359+
collectedAssets.filesToDelete.length === 0
360+
) {
340361
continue;
341362
}
342363

@@ -350,11 +371,11 @@ async function* runWatchLoop(
350371
context,
351372
buildState,
352373
modifiedFiles: changedFiles,
353-
assetsToEmit,
374+
collectedAssets,
354375
},
355376
withProgress,
356377
watcher,
357-
buildAction,
378+
buildActionFn,
358379
);
359380
}
360381
}
@@ -388,7 +409,7 @@ function logMessages(
388409
* @param logger The Architect logger API.
389410
* @param startTime High-resolution timestamp recorded when the build iteration started.
390411
* @param success Whether the build iteration succeeded without errors.
391-
* @param outputPath Optional output directory path to log on successful builds.
412+
* @param outputPath Optional POSIX output directory path to log on successful builds.
392413
*/
393414
function logBuildResult(
394415
logger: logging.LoggerApi,

‎packages/angular/build/src/builders/library/options.ts‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { normalizeAssetPatterns } from '../../utils';
1313
import { supportColor } from '../../utils/color';
1414
import { assertIsError } from '../../utils/error';
1515
import { normalizeCacheOptions } from '../../utils/normalize-cache';
16+
import { toPosixPath } from '../../utils/path';
1617
import {
1718
generateSearchDirectories,
1819
getTailwindConfig,
@@ -24,14 +25,25 @@ import { loadPackageJson } from './pipeline/utils';
2425
import type { Schema as LibraryBuilderOptions } from './schema';
2526
import type { NormalizedLibraryOptions, PackageJsonData } from './types';
2627

28+
/**
29+
* Normalizes raw library builder options into {@link NormalizedLibraryOptions} with all
30+
* file and directory paths normalized to POSIX format.
31+
*
32+
* @param context The Architect builder context.
33+
* @param projectName The name of the library project.
34+
* @param options The raw library builder schema options.
35+
* @returns A promise resolving to the normalized library options.
36+
*/
2737
export async function normalizeLibraryOptions(
2838
context: BuilderContext,
2939
projectName: string,
3040
options: LibraryBuilderOptions,
3141
): Promise<NormalizedLibraryOptions> {
32-
const { workspaceRoot } = context;
42+
const workspaceRoot = toPosixPath(context.workspaceRoot);
3343
const projectMetadata = await context.getProjectMetadata(projectName);
34-
const { projectRoot, projectSourceRoot } = getProjectRootPaths(workspaceRoot, projectMetadata);
44+
const projectRootPaths = getProjectRootPaths(workspaceRoot, projectMetadata);
45+
const projectRoot = toPosixPath(projectRootPaths.projectRoot);
46+
const projectSourceRoot = toPosixPath(projectRootPaths.projectSourceRoot);
3547

3648
const {
3749
tsConfig,
@@ -50,8 +62,8 @@ export async function normalizeLibraryOptions(
5062
clearScreen,
5163
} = options;
5264

53-
const resolvedTsConfigPath = path.resolve(workspaceRoot, tsConfig);
54-
const packageJsonPath = path.join(projectRoot, 'package.json');
65+
const resolvedTsConfigPath = toPosixPath(path.resolve(workspaceRoot, tsConfig));
66+
const packageJsonPath = path.posix.join(projectRoot, 'package.json');
5567

5668
let packageJson: PackageJsonData;
5769
try {
@@ -99,7 +111,7 @@ export async function normalizeLibraryOptions(
99111
}
100112
defaultAssets.push({
101113
glob: 'README.md',
102-
input: path.dirname(entryPoint.entryFilePath),
114+
input: path.posix.dirname(entryPoint.entryFilePath),
103115
output: entryPoint.name,
104116
});
105117
}
@@ -109,7 +121,11 @@ export async function normalizeLibraryOptions(
109121
workspaceRoot,
110122
projectRoot,
111123
projectSourceRoot,
112-
);
124+
).map((asset) => ({
125+
...asset,
126+
input: toPosixPath(path.resolve(workspaceRoot, asset.input)),
127+
output: toPosixPath(asset.output),
128+
}));
113129

114130
const cacheOptions = normalizeCacheOptions(
115131
projectMetadata,
@@ -119,7 +135,7 @@ export async function normalizeLibraryOptions(
119135
);
120136

121137
const styleIncludePaths = (stylePreprocessorOptions?.includePaths ?? []).map((p: string) =>
122-
path.resolve(workspaceRoot, p),
138+
toPosixPath(path.resolve(workspaceRoot, p)),
123139
);
124140

125141
const searchDirectories = await generateSearchDirectories([projectRoot, workspaceRoot]);
@@ -133,7 +149,9 @@ export async function normalizeLibraryOptions(
133149
projectRoot,
134150
packageName,
135151
packageJson,
136-
outputPath: path.resolve(workspaceRoot, options.outputPath ?? path.join('dist', projectName)),
152+
outputPath: toPosixPath(
153+
path.resolve(workspaceRoot, options.outputPath ?? path.posix.join('dist', projectName)),
154+
),
137155
deleteOutputPath,
138156
packageJsonPath,
139157
tsConfigPath: resolvedTsConfigPath,

0 commit comments

Comments
 (0)