Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { BundlerContext } from '../../tools/esbuild/bundler-context';
import { createGlobalScriptsBundleOptions } from '../../tools/esbuild/global-scripts';
import { createGlobalStylesBundleOptions } from '../../tools/esbuild/global-styles';
import { MemoryLoadResultCache } from '../../tools/esbuild/load-result-cache';
import { getSupportedNodeTargets } from '../../tools/esbuild/target';
import type { NormalizedApplicationBuildOptions } from './options';

Expand Down Expand Up @@ -94,11 +95,19 @@ export function setupBundlerContexts(

// Global Stylesheets
if (options.globalStyles.length > 0) {
const globalStylesCache = new MemoryLoadResultCache();
for (const initial of [true, false]) {
const bundleOptions = createGlobalStylesBundleOptions(options, target, initial);
if (bundleOptions) {
otherContexts.push(
new BundlerContext(workspaceRoot, watch, bundleOptions, true, () => initial),
new BundlerContext(
workspaceRoot,
watch,
bundleOptions,
true,
() => initial,
globalStylesCache,
),
);
}
}
Expand Down
76 changes: 48 additions & 28 deletions packages/angular/build/src/tools/esbuild/bundler-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
context,
} from 'esbuild';
import assert from 'node:assert';
import { basename, extname, join, relative } from 'node:path';
import { basename, extname, isAbsolute, join, normalize, relative } from 'node:path';
import { SERVER_GENERATED_EXTERNALS } from '../../utils/server-rendering/manifest';
import {
type BuildOutputFile,
Expand Down Expand Up @@ -79,16 +79,18 @@ export class BundlerContext {
#disposed = false;
#optionsFactory: BundlerOptionsFactory<BuildOptions & { metafile: true; write: false }>;
#shouldCacheResult: boolean;
#loadCache?: MemoryLoadResultCache;
#loadCache?: LoadResultCache;
readonly watchFiles = new Set<string>();

constructor(
private workspaceRoot: string,
private incremental: boolean,
options: BuildOptions | BundlerOptionsFactory,
private alwaysUseContext = false,
private useContext = incremental,
private initialFilter?: (initial: Readonly<InitialFileRecord>) => boolean,
sharedLoadCache?: LoadResultCache,
Comment thread
clydin marked this conversation as resolved.
) {
this.#loadCache = sharedLoadCache;
// To cache the results an option factory is needed to capture the full set of dependencies
this.#shouldCacheResult = incremental && typeof options === 'function';
this.#optionsFactory = (...args) => {
Expand Down Expand Up @@ -219,7 +221,7 @@ export class BundlerContext {
async #performBundle(): Promise<BundleContextResult> {
// Create esbuild options if not present
if (this.#esbuildOptions === undefined) {
if (this.incremental) {
if (this.incremental && !this.#loadCache) {
this.#loadCache = new MemoryLoadResultCache();
}
this.#esbuildOptions = this.#optionsFactory(this.#loadCache);
Expand All @@ -234,7 +236,7 @@ export class BundlerContext {
if (this.#esbuildContext) {
// Rebuild using the existing incremental build context
result = await this.#esbuildContext.rebuild();
} else if (this.incremental || this.alwaysUseContext) {
} else if (this.useContext) {
// Create a build context and perform the build.
// Context creation does not perform a build.
const esbuildContext = await context(this.#esbuildOptions);
Expand All @@ -258,23 +260,12 @@ export class BundlerContext {
// Build failures will throw an exception which contains errors/warnings
if (isEsBuildFailure(failure)) {
this.#addErrorsToWatch(failure);
this.#addLoadCacheFilesToWatch();

return failure;
} else {
throw failure;
}
} finally {
if (this.incremental) {
// When incremental always add any files from the load result cache
if (this.#loadCache) {
for (const file of this.#loadCache.watchFiles) {
if (!isInternalAngularFile(file)) {
// watch files are fully resolved paths
this.watchFiles.add(file);
}
}
}
}
}

// Update files that should be watched.
Expand All @@ -283,18 +274,38 @@ export class BundlerContext {
if (this.incremental) {
// Add input files except virtual angular files which do not exist on disk
for (const input of Object.keys(result.metafile.inputs)) {
if (isInternalAngularFile(input) || isInternalBundlerFile(input)) {
continue;
const isInternal = isInternalAngularFile(input) || isInternalBundlerFile(input);

// Input file paths are always relative to the workspace root unless already absolute
const normalizedAbsoluteInput = isAbsolute(input)
? normalize(input)
: join(this.workspaceRoot, input);

if (!isInternal) {
this.watchFiles.add(normalizedAbsoluteInput);
}

// Input file paths are always relative to the workspace root
this.watchFiles.add(join(this.workspaceRoot, input));
if (this.#loadCache) {
const cachedLoad = await (this.#loadCache.get(input) ??
this.#loadCache.get(input.replace(';', ':')) ??
this.#loadCache.get('file:' + normalizedAbsoluteInput));
if (cachedLoad?.watchFiles) {
for (const file of cachedLoad.watchFiles) {
if (!isInternalAngularFile(file)) {
this.watchFiles.add(
isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file),
);
}
}
}
}
}
}

// Return if the build encountered any errors
if (result.errors.length) {
this.#addErrorsToWatch(result);
this.#addLoadCacheFilesToWatch();

return {
errors: result.errors,
Expand Down Expand Up @@ -443,12 +454,22 @@ export class BundlerContext {
for (const error of result.errors) {
let file = error.location?.file;
if (file && !isInternalAngularFile(file)) {
this.watchFiles.add(join(this.workspaceRoot, file));
this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
}
for (const note of error.notes) {
file = note.location?.file;
if (file && !isInternalAngularFile(file)) {
this.watchFiles.add(join(this.workspaceRoot, file));
this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
}
}
}
}

#addLoadCacheFilesToWatch(): void {
if (this.incremental && this.#loadCache) {
for (const file of this.#loadCache.watchFiles) {
if (!isInternalAngularFile(file)) {
this.watchFiles.add(isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file));
}
}
}
Expand All @@ -468,12 +489,11 @@ export class BundlerContext {

let invalid = false;
for (const file of files) {
if (this.#loadCache?.invalidate(file)) {
invalid = true;
continue;
}
const normalizedFile = isAbsolute(file) ? normalize(file) : join(this.workspaceRoot, file);

this.#loadCache?.invalidate(normalizedFile);

invalid ||= this.watchFiles.has(file);
invalid ||= this.watchFiles.has(normalizedFile);
}

if (invalid) {
Expand Down
16 changes: 7 additions & 9 deletions packages/angular/build/src/tools/esbuild/load-result-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { normalize } from 'node:path';
export interface LoadResultCache {
get(path: string): OnLoadResult | Promise<OnLoadResult | undefined> | undefined;
put(path: string, result: OnLoadResult): Promise<void>;
invalidate(path: string): boolean;
readonly watchFiles: ReadonlyArray<string>;
}

Expand Down Expand Up @@ -108,18 +109,15 @@ export class MemoryLoadResultCache implements LoadResultCache {

invalidate(path: string): boolean {
const affectedPaths = this.#fileDependencies.get(path);
let found = false;
if (!affectedPaths) {
return false;
}

if (affectedPaths) {
for (const affected of affectedPaths) {
if (this.#loadResults.delete(affected)) {
found = true;
}
}
this.#fileDependencies.delete(path);
for (const affected of affectedPaths) {
this.#loadResults.delete(affected);
}

return found;
return true;
}

get watchFiles(): string[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ describe('MemoryLoadResultCache', () => {
// Invalidating new dependency should invalidate the cache
expect(cache.invalidate('/test/new-dep.json')).toBeTrue();
expect(cache.get('file:/test/styles.css')).toBeUndefined();
expect(cache.watchFiles).not.toContain('/test/new-dep.json');
// Invalidating a file marks its cached results stale, but preserves watch file tracking
// so the file watcher continues monitoring the dependency for subsequent changes until
// a new build pass (via put) updates the active dependencies.
expect(cache.watchFiles).toContain('/test/new-dep.json');
});
});