diff --git a/packages/angular/build/src/builders/library/builder.ts b/packages/angular/build/src/builders/library/builder.ts index b809c5de3342..67d5735d3f8c 100644 --- a/packages/angular/build/src/builders/library/builder.ts +++ b/packages/angular/build/src/builders/library/builder.ts @@ -107,14 +107,14 @@ export async function* executeLibraryBuilder( target, ); - // Track all referenced files for watch mode - const allWatchedFiles = new Set([ + // Track all referenced compilation files for watch mode + const watchedCompilationFiles = new Set([ toPosixPath(tsConfigPath), toPosixPath(packageJsonPath), ]); for (const entryPoint of normalizedOptions.entryPoints.values()) { - allWatchedFiles.add(toPosixPath(entryPoint.entryFilePath)); + watchedCompilationFiles.add(toPosixPath(entryPoint.entryFilePath)); } if (isWatchMode) { @@ -131,7 +131,7 @@ export async function* executeLibraryBuilder( poll, preserveSymlinks, signal, - watchFiles: allWatchedFiles, + watchFiles: watchedCompilationFiles, }); context.addTeardown?.(() => void watcher?.close()); @@ -143,7 +143,7 @@ export async function* executeLibraryBuilder( { options: normalizedOptions, stylesheetBundler, - allWatchedFiles, + watchedCompilationFiles, isWatchMode, context, buildState, @@ -162,7 +162,7 @@ export async function* executeLibraryBuilder( watcher, normalizedOptions, stylesheetBundler, - allWatchedFiles, + watchedCompilationFiles, context, withProgress, buildState, @@ -190,7 +190,7 @@ async function executeBuild( buildAction: typeof import('./pipeline/build-action').buildAction, ): Promise { const startTime = process.hrtime.bigint(); - const { context, allWatchedFiles, isWatchMode } = actionContext; + const { context, watchedCompilationFiles, isWatchMode } = actionContext; try { await withProgress(message, () => buildAction(actionContext)); @@ -206,7 +206,14 @@ async function executeBuild( return { success: false, error: error.message }; } finally { - watcher?.add(Array.from(allWatchedFiles)); + if (watcher) { + watcher.add(Array.from(watchedCompilationFiles)); + + const { assetsToEmit } = actionContext; + if (assetsToEmit?.length) { + watcher.add(assetsToEmit.map((asset) => asset.source)); + } + } } } @@ -217,7 +224,7 @@ async function* runWatchLoop( watcher: BuildWatcher, options: NormalizedLibraryOptions, stylesheetBundler: ReturnType, - allWatchedFiles: Set, + watchedCompilationFiles: Set, context: BuilderContext, withProgress: typeof withSpinner, buildState: SingleBuildState, @@ -225,7 +232,7 @@ async function* runWatchLoop( hasModifiedWatchedFile: typeof import('./pipeline/build-action').hasModifiedWatchedFile, signal?: AbortSignal, ): AsyncIterableIterator { - const { checkAssetChanges } = await import('./pipeline/assets'); + const { collectAssetsToEmit } = await import('./pipeline/assets'); const { workspaceRoot, packageJsonPath, assets, clearScreen } = options; const posixPackageJsonPath = toPosixPath(packageJsonPath); @@ -281,7 +288,7 @@ async function* runWatchLoop( packageJson, options, buildState, - allWatchedFiles, + watchedCompilationFiles, packageJsonPath, ); @@ -302,13 +309,15 @@ async function* runWatchLoop( !buildState.singleProgramCache || buildState.hasCompilationError || buildState.hasEntryPointsChanges || - hasModifiedWatchedFile(changedFiles, allWatchedFiles, posixPackageJsonPath); + hasModifiedWatchedFile(changedFiles, watchedCompilationFiles, posixPackageJsonPath); + + const assetsToEmit = await collectAssetsToEmit( + assets, + workspaceRoot, + buildState.hasEmittedAssets ? changedFiles : undefined, + ); - if ( - !hasSourceChanges && - !hasPackageJsonChanges && - !checkAssetChanges(assets, workspaceRoot, changedFiles) - ) { + if (!hasSourceChanges && !hasPackageJsonChanges && assetsToEmit.length === 0) { continue; } @@ -317,11 +326,12 @@ async function* runWatchLoop( { options, stylesheetBundler, - allWatchedFiles, + watchedCompilationFiles, isWatchMode: true, context, buildState, modifiedFiles: changedFiles, + assetsToEmit, }, withProgress, watcher, diff --git a/packages/angular/build/src/builders/library/pipeline/assets.ts b/packages/angular/build/src/builders/library/pipeline/assets.ts index 8cd85d4e8fae..e1e46f4b9f08 100644 --- a/packages/angular/build/src/builders/library/pipeline/assets.ts +++ b/packages/angular/build/src/builders/library/pipeline/assets.ts @@ -15,30 +15,23 @@ import type { NormalizedLibraryOptions } from '../types'; import { type DiskOutputFile, createDiskOutputFile } from './utils'; /** - * Resolves and collects configured library assets to be emitted to disk, - * and registers their source paths with the watch set. + * Resolves and collects configured library assets to be emitted to disk. * * @param assets The normalized asset patterns. * @param workspaceRoot The workspace root directory path. - * @param allWatchedFiles Set collecting all watched file paths for watch mode. * @param modifiedFiles Optional set of modified file paths for incremental copying in watch mode. * @returns An array of disk file emission descriptors. */ export async function collectAssetsToEmit( assets: NormalizedLibraryOptions['assets'], workspaceRoot: string, - allWatchedFiles: Set, modifiedFiles?: ReadonlySet, ): Promise { - if (assets.length === 0) { + if (assets.length === 0 || modifiedFiles?.size === 0) { return []; } if (modifiedFiles) { - if (modifiedFiles.size === 0) { - return []; - } - const matchers = createAssetMatchers(assets, workspaceRoot); const filesToEmit: DiskOutputFile[] = []; @@ -58,7 +51,6 @@ export async function collectAssetsToEmit( if (statSync(resolvedFile, { throwIfNoEntry: false })?.isFile()) { filesToEmit.push(createDiskOutputFile(resolvedFile, path.join(asset.output, relative))); - allWatchedFiles.add(posixFile); } } } @@ -67,50 +59,8 @@ export async function collectAssetsToEmit( } const resolvedAssets = await resolveAssets(assets, workspaceRoot); - const filesToEmit: DiskOutputFile[] = []; - - for (const { source, destination } of resolvedAssets) { - filesToEmit.push(createDiskOutputFile(source, destination)); - allWatchedFiles.add(toPosixPath(source)); - } - - return filesToEmit; -} - -/** - * Checks whether any configured library assets were modified. - * - * @param assets The normalized asset patterns. - * @param workspaceRoot The workspace root directory path. - * @param changedFiles Set of changed file paths. - * @returns True if any asset file was modified. - */ -export function checkAssetChanges( - assets: NormalizedLibraryOptions['assets'], - workspaceRoot: string, - changedFiles: ReadonlySet, -): boolean { - if (assets.length === 0 || changedFiles.size === 0) { - return false; - } - - const matchers = createAssetMatchers(assets, workspaceRoot); - - for (const file of changedFiles) { - const resolvedFile = path.isAbsolute(file) ? file : path.resolve(workspaceRoot, file); - const posixFile = toPosixPath(resolvedFile); - - for (const { posixInputPrefix, isMatch } of matchers) { - if (posixFile.startsWith(posixInputPrefix)) { - const relative = posixFile.slice(posixInputPrefix.length); - if (isMatch(relative)) { - return true; - } - } - } - } - return false; + return resolvedAssets.map(({ source, destination }) => createDiskOutputFile(source, destination)); } function createAssetMatchers(assets: NormalizedLibraryOptions['assets'], workspaceRoot: string) { diff --git a/packages/angular/build/src/builders/library/pipeline/build-action.ts b/packages/angular/build/src/builders/library/pipeline/build-action.ts index 93bbf0ba59a3..a2ce6d8546dc 100644 --- a/packages/angular/build/src/builders/library/pipeline/build-action.ts +++ b/packages/angular/build/src/builders/library/pipeline/build-action.ts @@ -9,6 +9,7 @@ import type { BuilderContext } from '@angular-devkit/architect'; import { constants, copyFile, mkdir, writeFile } from 'node:fs/promises'; import path from 'node:path'; +import type { ComponentStylesheetBundler } from '../../../tools/esbuild/angular/component-stylesheets'; import { emitFilesToDisk } from '../../../tools/esbuild/utils'; import { toPosixPath } from '../../../utils/path'; import type { NormalizedLibraryOptions, SingleBuildState } from '../types'; @@ -16,8 +17,7 @@ import { collectAssetsToEmit } from './assets'; import { type BundleEntryPointInput, bundleEntryPoints } from './bundler'; import { compileLibrary } from './compilation'; import { generatePackageManifests } from './package-manifests'; -import type { createComponentStylesheetBundlerForLibrary } from './stylesheet-bundler'; -import type { OutputFile } from './utils'; +import type { DiskOutputFile, OutputFile } from './utils'; /** * Creates a fresh {@link SingleBuildState} instance. @@ -35,13 +35,40 @@ export function createSingleBuildState(): SingleBuildState { * Context required to execute a single library build iteration. */ export interface BuildActionContext { + /** Normalized options for the library build. */ options: NormalizedLibraryOptions; + + /** The Architect builder context. */ context: BuilderContext; - stylesheetBundler: ReturnType; + + /** Bundler instance used to process component stylesheets. */ + stylesheetBundler: ComponentStylesheetBundler; + + /** Whether the builder is running in watch mode. */ isWatchMode: boolean; - allWatchedFiles: Set; + + /** + * Set of file paths tracked for compilation and configuration + * (including `tsconfig.json`, `package.json`, entry points, and referenced source, + * template, and stylesheet files). Updated during compilation and used to determine + * whether file changes require recompiling entry points, excluding asset files so + * asset-only changes do not trigger code compilation. + */ + watchedCompilationFiles: Set; + + /** State preserved across incremental builds in watch mode. */ buildState: SingleBuildState; + + /** Set of file paths modified since the last build iteration. */ modifiedFiles?: Set; + + /** + * Asset files to emit to disk for the current build iteration. + * Pre-collected in the watch loop to avoid redundant asset matching, or resolved and + * populated by `buildAction` when omitted (such as during the initial build) so the + * caller can register their source paths with the file watcher. + */ + assetsToEmit?: DiskOutputFile[]; } /** @@ -57,7 +84,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise 0 || pendingChangedDtsFiles.size > 0 || - hasModifiedWatchedFile(modifiedFiles, allWatchedFiles, posixPackageJsonPath); + hasModifiedWatchedFile(modifiedFiles, watchedCompilationFiles, posixPackageJsonPath); const shouldGenerateManifests = !buildState.hasEmittedManifests; if (shouldGenerateManifests) { @@ -107,7 +134,7 @@ export async function buildAction(actionContext: BuildActionContext): Promise(filesToEmit, async (file) => { const fullFilePath = path.join(options.outputPath, file.path); @@ -200,11 +225,11 @@ export async function buildAction(actionContext: BuildActionContext): Promise, - allWatchedFiles: ReadonlySet, + watchedCompilationFiles: ReadonlySet, posixPackageJsonPath: string, ): boolean { for (const file of modifiedFiles) { - if (file !== posixPackageJsonPath && allWatchedFiles.has(file)) { + if (file !== posixPackageJsonPath && watchedCompilationFiles.has(file)) { return true; } } diff --git a/packages/angular/build/src/builders/library/pipeline/entry-points.ts b/packages/angular/build/src/builders/library/pipeline/entry-points.ts index 76d647359184..93a96307d9e4 100644 --- a/packages/angular/build/src/builders/library/pipeline/entry-points.ts +++ b/packages/angular/build/src/builders/library/pipeline/entry-points.ts @@ -238,7 +238,7 @@ export function updateWatchedEntryPoints( packageJson: PackageJsonData, options: NormalizedLibraryOptions, buildState: SingleBuildState, - allWatchedFiles: Set, + watchedCompilationFiles: Set, packageJsonPath: string, ): void { const newEntryPoints = normalizeEntryPoints( @@ -250,7 +250,7 @@ export function updateWatchedEntryPoints( if (haveEntryPointsChanged(options.entryPoints, newEntryPoints)) { for (const entryPoint of newEntryPoints.values()) { - allWatchedFiles.add(toPosixPath(entryPoint.entryFilePath)); + watchedCompilationFiles.add(toPosixPath(entryPoint.entryFilePath)); } for (const name of buildState.previousBundleResults.keys()) { diff --git a/packages/angular/build/src/builders/library/pipeline/entry-points_spec.ts b/packages/angular/build/src/builders/library/pipeline/entry-points_spec.ts index 8472ec35ad34..834b7aec6835 100644 --- a/packages/angular/build/src/builders/library/pipeline/entry-points_spec.ts +++ b/packages/angular/build/src/builders/library/pipeline/entry-points_spec.ts @@ -530,7 +530,7 @@ describe('entry-points pipeline', () => { hasEntryPointsChanges: false, } as unknown as SingleBuildState; - const allWatchedFiles = new Set(); + const watchedCompilationFiles = new Set(); const newPackageJson: PackageJsonData = { name: packageName, @@ -544,7 +544,7 @@ describe('entry-points pipeline', () => { newPackageJson, options, buildState, - allWatchedFiles, + watchedCompilationFiles, packageJsonPath, ); @@ -553,9 +553,13 @@ describe('entry-points pipeline', () => { expect(options.entryPoints.has('old-feature')).toBeFalse(); expect(buildState.previousBundleResults.has('old-feature')).toBeFalse(); expect(buildState.previousBundleResults.has('.')).toBeTrue(); - expect(allWatchedFiles.has(toPosixPath(join(projectRoot, 'src/public-api.ts')))).toBeTrue(); expect( - allWatchedFiles.has(toPosixPath(join(projectRoot, 'new-feature/src/public-api.ts'))), + watchedCompilationFiles.has(toPosixPath(join(projectRoot, 'src/public-api.ts'))), + ).toBeTrue(); + expect( + watchedCompilationFiles.has( + toPosixPath(join(projectRoot, 'new-feature/src/public-api.ts')), + ), ).toBeTrue(); }); @@ -584,7 +588,7 @@ describe('entry-points pipeline', () => { hasEntryPointsChanges: false, } as unknown as SingleBuildState; - const allWatchedFiles = new Set(); + const watchedCompilationFiles = new Set(); const samePackageJson: PackageJsonData = { name: packageName, @@ -598,13 +602,13 @@ describe('entry-points pipeline', () => { samePackageJson, options, buildState, - allWatchedFiles, + watchedCompilationFiles, packageJsonPath, ); expect(buildState.hasEntryPointsChanges).toBeFalse(); expect(options.entryPoints).toBe(initialEntryPoints); - expect(allWatchedFiles).toHaveSize(0); + expect(watchedCompilationFiles).toHaveSize(0); expect(buildState.previousBundleResults).toHaveSize(2); }); @@ -631,7 +635,7 @@ describe('entry-points pipeline', () => { hasEntryPointsChanges: false, } as unknown as SingleBuildState; - const allWatchedFiles = new Set(); + const watchedCompilationFiles = new Set(); const updatedPackageJson: PackageJsonData = { name: packageName, @@ -644,7 +648,7 @@ describe('entry-points pipeline', () => { updatedPackageJson, options, buildState, - allWatchedFiles, + watchedCompilationFiles, packageJsonPath, ); @@ -653,7 +657,9 @@ describe('entry-points pipeline', () => { expect(options.entryPoints.get('.')?.entryFilePath).toBe( join(projectRoot, 'src/other-api.ts'), ); - expect(allWatchedFiles.has(toPosixPath(join(projectRoot, 'src/other-api.ts')))).toBeTrue(); + expect( + watchedCompilationFiles.has(toPosixPath(join(projectRoot, 'src/other-api.ts'))), + ).toBeTrue(); }); it('should prune previousBundleResults when an entry point bundleName changes', () => { @@ -679,7 +685,7 @@ describe('entry-points pipeline', () => { hasEntryPointsChanges: false, } as unknown as SingleBuildState; - const allWatchedFiles = new Set(); + const watchedCompilationFiles = new Set(); const updatedPackageJson: PackageJsonData = { name: '@renamed-scope/my-lib', @@ -694,7 +700,7 @@ describe('entry-points pipeline', () => { updatedPackageJson, options, buildState, - allWatchedFiles, + watchedCompilationFiles, packageJsonPath, );