Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion modules/testing/builder/src/builder-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export class BuilderHarness<T> {
}
}

private resolvePath(path: string): string {
resolvePath(path: string): string {
return join(getSystemPath(this.host.root()), path);
}

Expand Down
75 changes: 10 additions & 65 deletions packages/angular/build/src/builders/application/build-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
*/

import { BuilderContext } from '@angular-devkit/architect';
import { existsSync } from 'node:fs';
import path from 'node:path';
import {
BuildOutputAsset,
ExecutionResult,
Expand All @@ -21,10 +19,8 @@ import {
} 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 { initializeHash } from '../../utils/hash';
import { NormalizedCachedOptions } from '../../utils/normalize-cache';
import { toPosixPath } from '../../utils/path';
import { NormalizedApplicationBuildOptions, NormalizedOutputOptions } from './options';
import {
ComponentUpdateResult,
Expand All @@ -35,20 +31,6 @@ import {
ResultMessage,
} from './results';

// Watch workspace for package manager changes
const packageWatchFiles = [
// manifest can affect module resolution
'package.json',
// npm lock file
'package-lock.json',
// pnpm lock file
'pnpm-lock.yaml',
// yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
'yarn.lock',
'.pnp.cjs',
'.pnp.data.json',
];

// eslint-disable-next-line max-lines-per-function
export async function* runEsBuildBuildAction(
action: (rebuildState?: RebuildState) => Promise<ExecutionResult>,
Expand Down Expand Up @@ -115,55 +97,18 @@ export async function* runEsBuildBuildAction(
logger.info('Watch mode enabled. Watching for file changes...');
}

const normalizedOutputBase = toPosixPath(outputOptions.base);
const normalizedCacheBase = toPosixPath(cacheOptions.basePath);
const ignored: string[] = [
// Ignore the output and cache paths to avoid infinite rebuild cycles
normalizedOutputBase,
`${normalizedOutputBase}/**`,
normalizedCacheBase,
`${normalizedCacheBase}/**`,
`${toPosixPath(workspaceRoot)}/**/.*/**`,
];

if (cacheOptions.localBasePath && cacheOptions.localBasePath !== cacheOptions.basePath) {
const normalizedLocalCacheBase = toPosixPath(cacheOptions.localBasePath);
ignored.push(normalizedLocalCacheBase, `${normalizedLocalCacheBase}/**`);
}

// Setup a watcher
const { createWatcher } = await import('../../tools/esbuild/watcher');
watcher = await createWatcher({
polling: typeof poll === 'number',
interval: poll,
followSymlinks: preserveSymlinks,
ignored,
cwd: workspaceRoot,
const { setupWatcher } = await import('../../tools/esbuild/watcher');
watcher = await setupWatcher({
workspaceRoot,
projectRoot,
outputPath: outputOptions.base,
cacheOptions,
poll,
preserveSymlinks,
signal: options.signal,
watchFiles: result.watchFiles,
});

// Setup abort support
options.signal?.addEventListener('abort', () => void watcher?.close());

// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
if (shouldWatchRoot) {
if (!preserveSymlinks) {
// Ignore all node modules directories to avoid excessive file watchers.
// Package changes are handled below by watching manifest and lock files.
// NOTE: this is not enable when preserveSymlinks is true as this would break `npm link` usages.
ignored.push('**/node_modules/**');

watcher.add(
packageWatchFiles
.map((file) => path.join(workspaceRoot, file))
.filter((file) => existsSync(file)),
);
}

watcher.add(projectRoot);
}

// Watch locations provided by the initial build result
watcher.add(result.watchFiles);
}

// Output the first build results after setting up the watcher to ensure that any code executed
Expand Down
36 changes: 2 additions & 34 deletions packages/angular/build/src/builders/application/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import type { BuilderContext } from '@angular-devkit/architect';
import type { Plugin } from 'esbuild';
import { access, constants, readFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { normalizeAssetPatterns, normalizeOptimization, normalizeSourceMaps } from '../../utils';
import { supportColor } from '../../utils/color';
Expand All @@ -19,9 +18,8 @@ import { IndexHtmlTransform } from '../../utils/index-file/index-html-generator'
import { normalizeCacheOptions } from '../../utils/normalize-cache';
import { canonicalizePath } from '../../utils/path';
import {
SearchDirectory,
findTailwindConfiguration,
generateSearchDirectories,
getTailwindConfig,
loadPostcssConfiguration,
} from '../../utils/postcss-configuration';
import { getProjectRootPaths, normalizeDirectoryPath } from '../../utils/project-metadata';
Expand Down Expand Up @@ -280,7 +278,7 @@ export async function normalizeOptions(
// Skip tailwind configuration if postcss is customized
const tailwindConfiguration = postcssConfiguration
? undefined
: await getTailwindConfig(searchDirectories, workspaceRoot, context);
: await getTailwindConfig(searchDirectories, workspaceRoot, context.logger);

let serverEntryPoint: string | undefined;
if (typeof options.server === 'string') {
Expand Down Expand Up @@ -538,36 +536,6 @@ export async function normalizeOptions(
};
}

async function getTailwindConfig(
searchDirectories: SearchDirectory[],
workspaceRoot: string,
context: BuilderContext,
): Promise<{ file: string; package: string } | undefined> {
const tailwindConfigurationPath = findTailwindConfiguration(searchDirectories);

if (!tailwindConfigurationPath) {
return undefined;
}

// Create a node resolver from the configuration file
const resolver = createRequire(tailwindConfigurationPath);
try {
return {
file: tailwindConfigurationPath,
package: resolver.resolve('tailwindcss'),
};
} catch {
const relativeTailwindConfigPath = path.relative(workspaceRoot, tailwindConfigurationPath);
context.logger.warn(
`Tailwind CSS configuration file found (${relativeTailwindConfigPath})` +
` but the 'tailwindcss' package is not installed.` +
` To enable Tailwind CSS, please install the 'tailwindcss' package.`,
);
}

return undefined;
}

/**
* Normalize entry point options. To maintain compatibility with the legacy browser builder, we need a single `browser`
* option which defines a single entry point. However, we also want to support multiple entry points as an internal option.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import type { Loader, OnLoadResult, PartialMessage } from 'esbuild';
import { readFile, stat } from 'node:fs/promises';
import { isAbsolute } from 'node:path';
import { fileURLToPath } from 'node:url';
import { mapConcurrent, runConcurrent } from '../../utils/concurrency';
import { calculateHash, createContentHash } from '../../utils/hash';
import type { Cache as PersistentCacheStore } from './cache';
import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache';
Expand Down Expand Up @@ -114,29 +115,6 @@ export function extractDiskFilePath(path: string): string | undefined {
/** Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. */
const MAX_CONCURRENT_READS = 16;

/**
* Maps an array asynchronously with a sliding worker pool to maintain full concurrency saturation.
*/
async function mapConcurrent<T, R>(
items: T[],
limit: number,
fn: (item: T) => Promise<R>,
): Promise<R[]> {
const results: R[] = new Array(items.length);
let index = 0;

const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
while (index < items.length) {
const i = index++;
results[i] = await fn(items[i]);
}
});

await Promise.all(workers);

return results;
}

/**
* Validates that all imported watch files exist on disk and their contents match.
* Performs a fast-path metadata check (mtime + size) first, falling back to content hashing.
Expand Down Expand Up @@ -214,7 +192,7 @@ async function computeMetadataForWatchFiles(
): Promise<Record<string, CachedDependencyMetadata>> {
const watchFilesMetadata: Record<string, CachedDependencyMetadata> = {};

await mapConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => {
await runConcurrent(watchFiles, MAX_CONCURRENT_READS, async (filePath) => {
try {
const knownContent = knownContents?.get(filePath);
const [content, stats] = await Promise.all([
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface BundleStylesheetOptions {
workspaceRoot: string;
optimization: boolean;
inlineFonts: boolean;
dataurl?: boolean;
preserveSymlinks?: boolean;
sourcemap: boolean | 'external' | 'inline' | 'linked';
sourcesContent?: boolean;
Expand Down Expand Up @@ -62,7 +63,7 @@ export function createStylesheetBundleOptions(
pluginFactory.create(SassStylesheetLanguage),
pluginFactory.create(LessStylesheetLanguage),
pluginFactory.create(CssStylesheetLanguage),
createCssResourcePlugin(cache),
createCssResourcePlugin(cache, options.dataurl),
];

if (options.inlineFonts) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ const CSS_RESOURCE_RESOLUTION = Symbol('CSS_RESOURCE_RESOLUTION');
* and types to be supported without needing to manually specify all extensions
* within the build configuration.
*
* @param cache An optional load result cache.
* @param dataurl If true, resources will be loaded with the 'dataurl' loader to inline them as base64 data URIs.
* @returns An esbuild {@link Plugin} instance.
*/
export function createCssResourcePlugin(cache?: LoadResultCache): Plugin {
export function createCssResourcePlugin(cache?: LoadResultCache, dataurl?: boolean): Plugin {
return {
name: 'angular-css-resource',
setup(build: PluginBuild): void {
Expand Down Expand Up @@ -119,7 +121,7 @@ export function createCssResourcePlugin(cache?: LoadResultCache): Plugin {

return {
contents: await readFile(resourcePath),
loader: 'file',
loader: dataurl ? 'dataurl' : 'file',
watchFiles: [resourcePath],
};
}),
Expand Down
95 changes: 95 additions & 0 deletions packages/angular/build/src/tools/esbuild/watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type * as Chokidar from 'chokidar';
import * as fs from 'node:fs';
import * as path from 'node:path';
import picomatch from 'picomatch';
import { shouldWatchRoot } from '../../utils/environment-options';
import { toPosixPath } from '../../utils/path';

export class ChangedFiles {
Expand Down Expand Up @@ -47,6 +48,100 @@ export interface WatcherOptions {
cwd?: string;
}

// Watch workspace for package manager changes
const packageWatchFiles = [
// manifest can affect module resolution
'package.json',
// npm lock file
'package-lock.json',
// pnpm lock file
'pnpm-lock.yaml',
// yarn lock file including Yarn PnP manifest files (https://yarnpkg.com/advanced/pnp-spec/)
'yarn.lock',
'.pnp.cjs',
'.pnp.data.json',
];

export interface SetupWatcherOptions {
workspaceRoot: string;
projectRoot: string;
outputPath: string;
cacheOptions: { basePath: string; localBasePath?: string };
poll?: number;
preserveSymlinks?: boolean;
signal?: AbortSignal;
watchFiles?: Iterable<string>;
}

/**
* Sets up and initializes a file watcher with proper ignore patterns for build outputs and caches.
*/
export async function setupWatcher(options: SetupWatcherOptions): Promise<BuildWatcher> {
const {
workspaceRoot,
projectRoot,
outputPath,
cacheOptions,
poll,
preserveSymlinks,
signal,
watchFiles,
} = options;

const normalizedOutputBase = toPosixPath(outputPath);
const normalizedCacheBase = toPosixPath(cacheOptions.basePath);
const ignored: string[] = [
// Ignore the output and cache paths to avoid infinite rebuild cycles
normalizedOutputBase,
`${normalizedOutputBase}/**`,
normalizedCacheBase,
`${normalizedCacheBase}/**`,
`${toPosixPath(workspaceRoot)}/**/.*/**`,
];

if (cacheOptions.localBasePath && cacheOptions.localBasePath !== cacheOptions.basePath) {
const normalizedLocalCacheBase = toPosixPath(cacheOptions.localBasePath);
ignored.push(normalizedLocalCacheBase, `${normalizedLocalCacheBase}/**`);
}

if (shouldWatchRoot && !preserveSymlinks) {
// Ignore all node modules directories to avoid excessive file watchers.
// Package changes are handled below by watching manifest and lock files.
// NOTE: this is not enabled when preserveSymlinks is true as this would break `npm link` usages.
ignored.push('**/node_modules/**');
}

const watcher = await createWatcher({
polling: typeof poll === 'number',
interval: poll,
followSymlinks: preserveSymlinks,
ignored,
cwd: workspaceRoot,
});

// Setup abort support
signal?.addEventListener('abort', () => void watcher.close());

// Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
if (shouldWatchRoot) {
if (!preserveSymlinks) {
watcher.add(
packageWatchFiles
.map((file) => path.join(workspaceRoot, file))
.filter((file) => fs.existsSync(file)),
);
}

watcher.add(projectRoot);
}

if (watchFiles) {
watcher.add(Array.isArray(watchFiles) ? watchFiles : Array.from(watchFiles));
}

return watcher;
}

/**
* Probes the filesystem at the specified target directory to determine whether it is case-sensitive.
*/
Expand Down
Loading