From 7ab718feef7d420428abda134a1587457565f27a Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Thu, 6 Nov 2025 10:58:51 +0100 Subject: [PATCH 01/10] fix: Added AbortController to abort build if next one started fix: Added quque fix: Cleanup fix(nf): Added rebuild-queue --- libs/native-federation-core/src/build.ts | 2 +- .../src/lib/core/build-adapter.ts | 1 + .../src/lib/core/build-for-federation.ts | 18 ++- .../lib/core/bundle-exposed-and-mappings.ts | 14 ++- .../src/lib/utils/rebuild-queue.ts | 46 ++++++++ .../src/builders/build/builder.ts | 111 ++++++++++++------ .../src/utils/angular-esbuild-adapter.ts | 110 +++++++++++++---- 7 files changed, 244 insertions(+), 58 deletions(-) create mode 100644 libs/native-federation-core/src/lib/utils/rebuild-queue.ts diff --git a/libs/native-federation-core/src/build.ts b/libs/native-federation-core/src/build.ts index 7dd03abe..5a2f895d 100644 --- a/libs/native-federation-core/src/build.ts +++ b/libs/native-federation-core/src/build.ts @@ -21,7 +21,7 @@ export { loadFederationConfig } from './lib/core/load-federation-config'; export { writeFederationInfo } from './lib/core/write-federation-info'; export { writeImportMap } from './lib/core/write-import-map'; export { MappedPath } from './lib/utils/mapped-paths'; - +export { RebuildQueue } from './lib/utils/rebuild-queue'; export { findRootTsConfigJson, share, diff --git a/libs/native-federation-core/src/lib/core/build-adapter.ts b/libs/native-federation-core/src/lib/core/build-adapter.ts index 44fc7fbf..b423f8af 100644 --- a/libs/native-federation-core/src/lib/core/build-adapter.ts +++ b/libs/native-federation-core/src/lib/core/build-adapter.ts @@ -32,6 +32,7 @@ export interface BuildAdapterOptions { hash: boolean; platform?: 'browser' | 'node'; optimizedMappings?: boolean; + signal?: AbortSignal; } export interface BuildResult { diff --git a/libs/native-federation-core/src/lib/core/build-for-federation.ts b/libs/native-federation-core/src/lib/core/build-for-federation.ts index b624c638..72b6b604 100644 --- a/libs/native-federation-core/src/lib/core/build-for-federation.ts +++ b/libs/native-federation-core/src/lib/core/build-for-federation.ts @@ -18,6 +18,7 @@ import { logger } from '../utils/logger'; export interface BuildParams { skipMappingsAndExposed: boolean; skipShared: boolean; + signal?: AbortSignal; } export const defaultBuildParams: BuildParams = { @@ -35,15 +36,30 @@ export async function buildForFederation( externals: string[], buildParams = defaultBuildParams ): Promise { + const signal = buildParams.signal; + + const checkAbort = () => { + if (signal?.aborted) { + const error = new Error('Build aborted'); + error.name = 'AbortError'; + throw error; + } + }; + let artefactInfo: ArtefactInfo | undefined; if (!buildParams.skipMappingsAndExposed) { + checkAbort(); + const start = process.hrtime(); artefactInfo = await bundleExposedAndMappings( config, fedOptions, - externals + externals, + signal ); + checkAbort(); + logger.measure( start, '[build artifacts] - To bundle all mappings and exposed.' diff --git a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts index 2087acae..9ab6ef27 100644 --- a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts +++ b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts @@ -20,8 +20,13 @@ export interface ArtefactInfo { export async function bundleExposedAndMappings( config: NormalizedFederationConfig, fedOptions: FederationOptions, - externals: string[] + externals: string[], + signal?: AbortSignal ): Promise { + if (signal?.aborted) { + throw new Error('Aborted before bundling'); + } + const shared = config.sharedMappings.map((sm) => { const entryPoint = sm.path; const tmp = sm.key.replace(/[^A-Za-z0-9]/g, '_'); @@ -54,7 +59,14 @@ export async function bundleExposedAndMappings( hash, optimizedMappings: config.features.ignoreUnusedDeps, }); + if (signal?.aborted) { + throw new Error('Aborted after bundle'); + } } catch (error) { + if (signal?.aborted) { + logger.info('Bundle operation was aborted'); + throw error; + } logger.error('Error building federation artefacts'); throw error; } diff --git a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts new file mode 100644 index 00000000..a2be3071 --- /dev/null +++ b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts @@ -0,0 +1,46 @@ +export class RebuildQueue { + private activeBuilds: Map = new Map(); + private buildCounter = 0; + + async enqueue(rebuildFn: () => Promise): Promise { + const buildId = ++this.buildCounter; + + for (const [id, controller] of this.activeBuilds) { + controller.abort(); + } + this.activeBuilds.clear(); + + const controller = new AbortController(); + this.activeBuilds.set(buildId, controller); + + try { + await rebuildFn(); + } catch (error) { + if (controller.signal.aborted) { + throw new Error(`Build ${buildId} cancelled`); + } else { + throw error; + } + } finally { + this.activeBuilds.delete(buildId); + } + } + + abort(): void { + for (const [_, controller] of this.activeBuilds) { + controller.abort(); + } + this.activeBuilds.clear(); + } + + get signal(): AbortSignal | undefined { + if (this.activeBuilds.size === 0) return undefined; + + const latestBuildId = Math.max(...this.activeBuilds.keys()); + return this.activeBuilds.get(latestBuildId)?.signal; + } + + get activeBuildCount(): number { + return this.activeBuilds.size; + } +} diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 95abc0fa..9a0b7cca 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -25,6 +25,7 @@ import { logger, setBuildAdapter, setLogLevel, + RebuildQueue, } from '@softarc/native-federation/build'; import { createAngularBuildAdapter, @@ -373,8 +374,9 @@ export async function* runBuilder( indexHtmlTransformer: transformIndexHtml(nfOptions), }); + const rebuildQueue = new RebuildQueue(); + try { - // builderRun.output.subscribe(async (output) => { for await (const output of builderRun) { lastResult = output; @@ -399,49 +401,92 @@ export async function* runBuilder( // } if (!first && (nfOptions.dev || watch)) { - setTimeout(async () => { - try { - const start = process.hrtime(); - federationResult = await buildForFederation( - config, - fedOptions, - externals, - { - skipMappingsAndExposed: false, - skipShared: true, - } - ); - - if (hasLocales && localeFilter) { - translateFederationArtefacts( - i18n, - localeFilter, - outputOptions.base, - federationResult - ); + rebuildQueue + .enqueue(async () => { + const signal = rebuildQueue.signal; + if (signal?.aborted) { + throw new Error('Build cancelled before starting'); } - logger.info('Done!'); + await new Promise((resolve, reject) => { + const timeout = setTimeout(resolve, nfOptions.rebuildDelay); + + if (signal) { + const abortHandler = () => { + clearTimeout(timeout); + reject(new Error('Build cancelled during delay')); + }; + signal.addEventListener('abort', abortHandler, { once: true }); + } + }); - // Notifies about build completion - if (isLocalDevelopment) { - federationBuildNotifier.broadcastBuildCompletion(); + if (signal?.aborted) { + throw new Error('Build cancelled after delay'); } - logger.measure(start, 'To rebuild nf.'); - } catch (error) { - logger.error('Federation rebuild failed!'); - // Notifies about build failure - if (isLocalDevelopment) { - federationBuildNotifier.broadcastBuildError(error); + try { + const start = process.hrtime(); + federationResult = await buildForFederation( + config, + fedOptions, + externals, + { + skipMappingsAndExposed: false, + skipShared: true, + signal, + } + ); + + if (signal?.aborted) { + throw new Error('Build cancelled after federation build'); + } + + if (hasLocales && localeFilter) { + if (signal?.aborted) { + throw new Error('Build cancelled before i18n'); + } + + translateFederationArtefacts( + i18n, + localeFilter, + outputOptions.base, + federationResult + ); + } + + logger.info('Done!'); + + if (isLocalDevelopment) { + federationBuildNotifier.broadcastBuildCompletion(); + } + logger.measure(start, 'To rebuild nf.'); + } catch (error) { + if (signal?.aborted || error.message?.includes('cancelled')) { + throw error; // Propagate cancellation + } else { + logger.error('Federation rebuild failed!'); + if (options.verbose) console.error(error); + if (isLocalDevelopment) { + federationBuildNotifier.broadcastBuildError(error); + } + throw error; + } } - } - }, nfOptions.rebuildDelay); + }) + .catch((error) => { + // Only log non-cancellation errors + if (!error.message?.includes('cancelled')) { + logger.error('Rebuild error:'); + if (options.verbose) console.error(error); + } + }); } first = false; } } finally { + rebuildQueue.abort(); + if (isLocalDevelopment) { federationBuildNotifier.stopEventServer(); } diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 42efcb0a..15912b6c 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -71,6 +71,7 @@ export function createAngularBuildAdapter( hash, platform, optimizedMappings, + signal, } = options; setNgServerMode(); @@ -191,8 +192,13 @@ async function runEsbuild( absWorkingDir: string | undefined = undefined, logLevel: esbuild.LogLevel = 'warning', platform?: 'browser' | 'node', - optimizedMappings?: boolean + optimizedMappings?: boolean, + signal?: AbortSignal ) { + if (signal?.aborted) { + throw new Error('Build aborted before esbuild start'); + } + const projectRoot = path.dirname(tsConfigPath); const browsers = getSupportedBrowsers(projectRoot, context.logger as any); const target = transformSupportedBrowsersToTargets(browsers); @@ -304,27 +310,48 @@ async function runEsbuild( }; const ctx = await esbuild.context(config); - const result = await ctx.rebuild(); - const memOnly = dev && kind === 'mapping-or-exposed' && !!_memResultHandler; + const abortHandler = () => { + ctx.cancel(); + ctx.dispose(); + }; - const writtenFiles = writeResult(result, outdir, memOnly); + if (signal) { + signal.addEventListener('abort', abortHandler, { once: true }); + } - if (watch) { - registerForRebuilds( - kind, - rebuildRequested, - ctx, - entryPoints, - outdir, - hash, - memOnly - ); - } else { + try { + const result = await ctx.rebuild(); + + if (signal?.aborted) { + throw new Error('Build aborted after esbuild completion'); + } + + const memOnly = dev && kind === 'mapping-or-exposed' && !!_memResultHandler; + + const writtenFiles = writeResult(result, outdir, memOnly); + + if (watch) { + registerForRebuilds( + kind, + rebuildRequested, + ctx, + entryPoints, + outdir, + hash, + memOnly, + signal + ); + } else { + ctx.dispose(); + } + return writtenFiles; + } catch (error) { ctx.dispose(); + throw error; + } finally { + if (signal) signal.removeEventListener('abort', abortHandler); } - - return writtenFiles; } async function getTailwindConfig( @@ -448,13 +475,52 @@ function registerForRebuilds( entryPoints: EntryPoint[], outdir: string, hash: boolean, - memOnly: boolean + memOnly: boolean, + signal?: AbortSignal ) { if (kind !== 'shared-package') { - rebuildRequested.rebuild.register(async () => { - const result = await ctx.rebuild(); - writeResult(result, outdir, memOnly); - }); + if (signal?.aborted) { + logger.info('Skipping rebuild registration due to abort signal'); + ctx.dispose(); + return; + } + + const rebuilder = async () => { + if (signal?.aborted) { + logger.info('Skipping rebuild due to abort signal'); + return; + } + + try { + const result = await ctx.rebuild(); + + if (signal?.aborted) { + logger.info('Rebuild completed but was aborted'); + return; + } + + writeResult(result, outdir, memOnly); + } catch (error) { + if (signal?.aborted) { + logger.info('Rebuild was aborted'); + return; + } + throw error; + } + }; + + rebuildRequested.rebuild.register(rebuilder); + + if (signal) { + signal.addEventListener( + 'abort', + async () => { + await ctx.cancel(); + ctx.dispose(); + }, + { once: true } + ); + } } } From ce4f1b4ea966003196e774aaf11c04258374453d Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Fri, 7 Nov 2025 08:48:43 +0100 Subject: [PATCH 02/10] fix: Updated messaging --- .../src/lib/core/bundle-exposed-and-mappings.ts | 1 - libs/native-federation-core/src/lib/utils/rebuild-queue.ts | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts index 9ab6ef27..8585daa0 100644 --- a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts +++ b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts @@ -64,7 +64,6 @@ export async function bundleExposedAndMappings( } } catch (error) { if (signal?.aborted) { - logger.info('Bundle operation was aborted'); throw error; } logger.error('Error building federation artefacts'); diff --git a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts index a2be3071..46f831b9 100644 --- a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts +++ b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts @@ -1,3 +1,5 @@ +import { logger } from './logger'; + export class RebuildQueue { private activeBuilds: Map = new Map(); private buildCounter = 0; @@ -8,6 +10,8 @@ export class RebuildQueue { for (const [id, controller] of this.activeBuilds) { controller.abort(); } + if (this.activeBuildCount > 0) + logger.info(`Aborted ${this.activeBuildCount} previous bundling task(s)`); this.activeBuilds.clear(); const controller = new AbortController(); From b63262448119d91d7f4fe5c16cf6a1d7e94ac45d Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 12 Nov 2025 16:00:10 +0100 Subject: [PATCH 03/10] fix(abort): Added AbortError --- libs/native-federation-core/src/build.ts | 1 + .../src/lib/core/build-for-federation.ts | 16 ++-- .../src/lib/utils/errors.ts | 8 ++ .../src/builders/build/builder.ts | 94 +++++++++---------- .../src/utils/angular-esbuild-adapter.ts | 40 ++++---- 5 files changed, 80 insertions(+), 79 deletions(-) create mode 100644 libs/native-federation-core/src/lib/utils/errors.ts diff --git a/libs/native-federation-core/src/build.ts b/libs/native-federation-core/src/build.ts index 5a2f895d..090846ef 100644 --- a/libs/native-federation-core/src/build.ts +++ b/libs/native-federation-core/src/build.ts @@ -33,4 +33,5 @@ export { } from './lib/core/federation-builder'; export * from './lib/utils/build-result-map'; export { hashFile } from './lib/utils/hash-file'; +export * from './lib/utils/errors'; export { logger, setLogLevel } from './lib/utils/logger'; diff --git a/libs/native-federation-core/src/lib/core/build-for-federation.ts b/libs/native-federation-core/src/lib/core/build-for-federation.ts index 72b6b604..fa18b535 100644 --- a/libs/native-federation-core/src/lib/core/build-for-federation.ts +++ b/libs/native-federation-core/src/lib/core/build-for-federation.ts @@ -14,6 +14,7 @@ import { FederationOptions } from './federation-options'; import { writeFederationInfo } from './write-federation-info'; import { writeImportMap } from './write-import-map'; import { logger } from '../utils/logger'; +import { AbortedError } from '../utils/errors'; export interface BuildParams { skipMappingsAndExposed: boolean; @@ -38,9 +39,9 @@ export async function buildForFederation( ): Promise { const signal = buildParams.signal; - const checkAbort = () => { + const checkIfBuildAborted = () => { if (signal?.aborted) { - const error = new Error('Build aborted'); + const error = new AbortedError('Abort signal was called'); error.name = 'AbortError'; throw error; } @@ -49,8 +50,6 @@ export async function buildForFederation( let artefactInfo: ArtefactInfo | undefined; if (!buildParams.skipMappingsAndExposed) { - checkAbort(); - const start = process.hrtime(); artefactInfo = await bundleExposedAndMappings( config, @@ -58,12 +57,11 @@ export async function buildForFederation( externals, signal ); - checkAbort(); - logger.measure( start, '[build artifacts] - To bundle all mappings and exposed.' ); + checkIfBuildAborted(); } const exposedInfo = !artefactInfo @@ -93,6 +91,7 @@ export async function buildForFederation( Object.keys(sharedBrowser).forEach((packageName) => cachedSharedPackages.add(packageName) ); + checkIfBuildAborted(); } if (Object.keys(sharedServer).length > 0) { @@ -112,6 +111,7 @@ export async function buildForFederation( Object.keys(sharedServer).forEach((packageName) => cachedSharedPackages.add(packageName) ); + checkIfBuildAborted(); } if (Object.keys(separateBrowser).length > 0) { @@ -131,6 +131,7 @@ export async function buildForFederation( Object.keys(separateBrowser).forEach((packageName) => cachedSharedPackages.add(packageName) ); + checkIfBuildAborted(); } if (Object.keys(separateServer).length > 0) { @@ -151,6 +152,8 @@ export async function buildForFederation( cachedSharedPackages.add(packageName) ); } + + checkIfBuildAborted(); } const sharedMappingInfo = !artefactInfo @@ -169,6 +172,7 @@ export async function buildForFederation( buildNotificationsEndpoint, }; + checkIfBuildAborted(); writeFederationInfo(federationInfo, fedOptions); writeImportMap(sharedInfo, fedOptions); diff --git a/libs/native-federation-core/src/lib/utils/errors.ts b/libs/native-federation-core/src/lib/utils/errors.ts new file mode 100644 index 00000000..0cfa0a1d --- /dev/null +++ b/libs/native-federation-core/src/lib/utils/errors.ts @@ -0,0 +1,8 @@ +export class AbortedError extends Error { + constructor(message: string) { + super(message); // Call the constructor of the base class `Error` + this.name = 'AbortedError'; // Set the error name to your custom error class name + // Set the prototype explicitly to maintain the correct prototype chain + Object.setPrototypeOf(this, AbortedError.prototype); + } +} diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 9a0b7cca..56ea4ff3 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -26,6 +26,7 @@ import { setBuildAdapter, setLogLevel, RebuildQueue, + AbortedError, } from '@softarc/native-federation/build'; import { createAngularBuildAdapter, @@ -405,7 +406,7 @@ export async function* runBuilder( .enqueue(async () => { const signal = rebuildQueue.signal; if (signal?.aborted) { - throw new Error('Build cancelled before starting'); + throw new AbortedError('Build cancelled before starting'); } await new Promise((resolve, reject) => { @@ -414,70 +415,65 @@ export async function* runBuilder( if (signal) { const abortHandler = () => { clearTimeout(timeout); - reject(new Error('Build cancelled during delay')); + reject(new AbortedError('[builder] During delay.')); }; signal.addEventListener('abort', abortHandler, { once: true }); } }); if (signal?.aborted) { - throw new Error('Build cancelled after delay'); + throw new AbortedError('[builder] Before federation build.'); } - try { - const start = process.hrtime(); - federationResult = await buildForFederation( - config, - fedOptions, - externals, - { - skipMappingsAndExposed: false, - skipShared: true, - signal, - } - ); - - if (signal?.aborted) { - throw new Error('Build cancelled after federation build'); + const start = process.hrtime(); + federationResult = await buildForFederation( + config, + fedOptions, + externals, + { + skipMappingsAndExposed: false, + skipShared: true, + signal, } + ); - if (hasLocales && localeFilter) { - if (signal?.aborted) { - throw new Error('Build cancelled before i18n'); - } - - translateFederationArtefacts( - i18n, - localeFilter, - outputOptions.base, - federationResult - ); - } + if (signal?.aborted) { + throw new AbortedError('[builder] After federation build.'); + } - logger.info('Done!'); + if (hasLocales && localeFilter) { + translateFederationArtefacts( + i18n, + localeFilter, + outputOptions.base, + federationResult + ); + } - if (isLocalDevelopment) { - federationBuildNotifier.broadcastBuildCompletion(); - } - logger.measure(start, 'To rebuild nf.'); - } catch (error) { - if (signal?.aborted || error.message?.includes('cancelled')) { - throw error; // Propagate cancellation - } else { - logger.error('Federation rebuild failed!'); - if (options.verbose) console.error(error); - if (isLocalDevelopment) { - federationBuildNotifier.broadcastBuildError(error); - } - throw error; - } + if (signal?.aborted) { + throw new AbortedError( + '[builder] After federation translations.' + ); + } + + logger.info('Done!'); + + if (isLocalDevelopment) { + federationBuildNotifier.broadcastBuildCompletion(); } + logger.measure(start, 'To rebuild nf.'); }) .catch((error) => { - // Only log non-cancellation errors - if (!error.message?.includes('cancelled')) { - logger.error('Rebuild error:'); + if (error instanceof AbortedError) { + logger.warn('Rebuild was cancelled.'); + if (options.verbose) + logger.warn('Cancellation point: ' + error?.message); + } else { + logger.error('Federation rebuild failed!'); if (options.verbose) console.error(error); + if (isLocalDevelopment) { + federationBuildNotifier.broadcastBuildError(error); + } } }); } diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 15912b6c..7f6c9f03 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -1,4 +1,5 @@ import { + AbortedError, BuildAdapter, logger, MappedPath, @@ -93,7 +94,8 @@ export function createAngularBuildAdapter( undefined, undefined, platform, - optimizedMappings + optimizedMappings, + signal ); if (kind === 'shared-package') { @@ -196,7 +198,7 @@ async function runEsbuild( signal?: AbortSignal ) { if (signal?.aborted) { - throw new Error('Build aborted before esbuild start'); + throw new AbortedError('[angular-esbuild-adapter] Before building'); } const projectRoot = path.dirname(tsConfigPath); @@ -324,7 +326,7 @@ async function runEsbuild( const result = await ctx.rebuild(); if (signal?.aborted) { - throw new Error('Build aborted after esbuild completion'); + throw new AbortedError('[angular-esbuild-adapter] After building.'); } const memOnly = dev && kind === 'mapping-or-exposed' && !!_memResultHandler; @@ -480,47 +482,37 @@ function registerForRebuilds( ) { if (kind !== 'shared-package') { if (signal?.aborted) { - logger.info('Skipping rebuild registration due to abort signal'); - ctx.dispose(); - return; + throw new AbortedError( + '[angular-esbuild-adapter] Before rebuild register' + ); } const rebuilder = async () => { if (signal?.aborted) { - logger.info('Skipping rebuild due to abort signal'); - return; + throw new AbortedError('[angular-esbuild-adapter] Rebuild aborted'); } try { const result = await ctx.rebuild(); if (signal?.aborted) { - logger.info('Rebuild completed but was aborted'); - return; + throw new AbortedError( + '[angular-esbuild-adapter] Aborted after rebuild' + ); } writeResult(result, outdir, memOnly); } catch (error) { - if (signal?.aborted) { - logger.info('Rebuild was aborted'); - return; + if (signal?.aborted && !(error instanceof AbortedError)) { + throw new AbortedError( + '[angular-esbuild-adapter] Rebuild interrupted' + ); } throw error; } }; rebuildRequested.rebuild.register(rebuilder); - - if (signal) { - signal.addEventListener( - 'abort', - async () => { - await ctx.cancel(); - ctx.dispose(); - }, - { once: true } - ); - } } } From 7ea4e1933a005587565d48fdd017efe451b2be8f Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 12 Nov 2025 16:30:26 +0100 Subject: [PATCH 04/10] fix(signal): Removed AbortSignal on watch --- .../src/utils/angular-esbuild-adapter.ts | 44 +++---------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 7f6c9f03..4b89cf57 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -341,18 +341,17 @@ async function runEsbuild( entryPoints, outdir, hash, - memOnly, - signal + memOnly ); } else { ctx.dispose(); + if (signal) signal.removeEventListener('abort', abortHandler); } return writtenFiles; } catch (error) { ctx.dispose(); throw error; } finally { - if (signal) signal.removeEventListener('abort', abortHandler); } } @@ -477,42 +476,13 @@ function registerForRebuilds( entryPoints: EntryPoint[], outdir: string, hash: boolean, - memOnly: boolean, - signal?: AbortSignal + memOnly: boolean ) { if (kind !== 'shared-package') { - if (signal?.aborted) { - throw new AbortedError( - '[angular-esbuild-adapter] Before rebuild register' - ); - } - - const rebuilder = async () => { - if (signal?.aborted) { - throw new AbortedError('[angular-esbuild-adapter] Rebuild aborted'); - } - - try { - const result = await ctx.rebuild(); - - if (signal?.aborted) { - throw new AbortedError( - '[angular-esbuild-adapter] Aborted after rebuild' - ); - } - - writeResult(result, outdir, memOnly); - } catch (error) { - if (signal?.aborted && !(error instanceof AbortedError)) { - throw new AbortedError( - '[angular-esbuild-adapter] Rebuild interrupted' - ); - } - throw error; - } - }; - - rebuildRequested.rebuild.register(rebuilder); + rebuildRequested.rebuild.register(async () => { + const result = await ctx.rebuild(); + writeResult(result, outdir, memOnly); + }); } } From 75fb0718c24047c1e61600248912eef9efd4747f Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 12 Nov 2025 17:03:54 +0100 Subject: [PATCH 05/10] fix(abort): Consistent errors --- .../src/lib/core/bundle-exposed-and-mappings.ts | 6 ++++-- libs/native-federation-core/src/lib/utils/rebuild-queue.ts | 6 ------ libs/native-federation/src/builders/build/builder.ts | 6 +++--- libs/native-federation/src/utils/angular-esbuild-adapter.ts | 3 +++ 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts index 8585daa0..a85fdd17 100644 --- a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts +++ b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts @@ -11,6 +11,7 @@ import { bundle } from '../utils/build-utils'; import { logger } from '../utils/logger'; import { normalize } from '../utils/normalize'; import { FederationOptions } from './federation-options'; +import { AbortedError } from '../utils/errors'; export interface ArtefactInfo { mappings: SharedInfo[]; @@ -24,7 +25,7 @@ export async function bundleExposedAndMappings( signal?: AbortSignal ): Promise { if (signal?.aborted) { - throw new Error('Aborted before bundling'); + throw new AbortedError('Aborted before bundling'); } const shared = config.sharedMappings.map((sm) => { @@ -58,9 +59,10 @@ export async function bundleExposedAndMappings( kind: 'mapping-or-exposed', hash, optimizedMappings: config.features.ignoreUnusedDeps, + signal, }); if (signal?.aborted) { - throw new Error('Aborted after bundle'); + throw new AbortedError('Aborted after bundle'); } } catch (error) { if (signal?.aborted) { diff --git a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts index 46f831b9..a7d7f433 100644 --- a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts +++ b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts @@ -19,12 +19,6 @@ export class RebuildQueue { try { await rebuildFn(); - } catch (error) { - if (controller.signal.aborted) { - throw new Error(`Build ${buildId} cancelled`); - } else { - throw error; - } } finally { this.activeBuilds.delete(buildId); } diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 56ea4ff3..5d7cfacf 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -406,7 +406,7 @@ export async function* runBuilder( .enqueue(async () => { const signal = rebuildQueue.signal; if (signal?.aborted) { - throw new AbortedError('Build cancelled before starting'); + throw new AbortedError('Build canceled before starting'); } await new Promise((resolve, reject) => { @@ -461,11 +461,11 @@ export async function* runBuilder( if (isLocalDevelopment) { federationBuildNotifier.broadcastBuildCompletion(); } - logger.measure(start, 'To rebuild nf.'); + logger.measure(start, 'To rebuild the federation artifacts.'); }) .catch((error) => { if (error instanceof AbortedError) { - logger.warn('Rebuild was cancelled.'); + logger.warn('Rebuild was canceled.'); if (options.verbose) logger.warn('Cancellation point: ' + error?.message); } else { diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 4b89cf57..bf545ac4 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -350,6 +350,9 @@ async function runEsbuild( return writtenFiles; } catch (error) { ctx.dispose(); + if (signal?.aborted && error?.message?.includes('canceled')) { + throw new AbortedError('[runEsbuild] ESBuild was canceled.'); + } throw error; } finally { } From d0259191d7247255638dd5f7ede30a35b23f954d Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 12 Nov 2025 17:18:10 +0100 Subject: [PATCH 06/10] fix(abort): Final cleanup --- .../src/lib/core/build-for-federation.ts | 31 ++++++++++--------- .../lib/core/bundle-exposed-and-mappings.ts | 13 +++++--- .../src/lib/utils/errors.ts | 5 ++- .../src/lib/utils/rebuild-queue.ts | 4 ++- .../src/utils/angular-esbuild-adapter.ts | 1 - 5 files changed, 30 insertions(+), 24 deletions(-) diff --git a/libs/native-federation-core/src/lib/core/build-for-federation.ts b/libs/native-federation-core/src/lib/core/build-for-federation.ts index fa18b535..31558067 100644 --- a/libs/native-federation-core/src/lib/core/build-for-federation.ts +++ b/libs/native-federation-core/src/lib/core/build-for-federation.ts @@ -39,14 +39,6 @@ export async function buildForFederation( ): Promise { const signal = buildParams.signal; - const checkIfBuildAborted = () => { - if (signal?.aborted) { - const error = new AbortedError('Abort signal was called'); - error.name = 'AbortError'; - throw error; - } - }; - let artefactInfo: ArtefactInfo | undefined; if (!buildParams.skipMappingsAndExposed) { @@ -61,7 +53,11 @@ export async function buildForFederation( start, '[build artifacts] - To bundle all mappings and exposed.' ); - checkIfBuildAborted(); + + if (signal?.aborted) + throw new AbortedError( + '[buildForFederation] After exposed-and-mappings bundle' + ); } const exposedInfo = !artefactInfo @@ -91,7 +87,10 @@ export async function buildForFederation( Object.keys(sharedBrowser).forEach((packageName) => cachedSharedPackages.add(packageName) ); - checkIfBuildAborted(); + if (signal?.aborted) + throw new AbortedError( + '[buildForFederation] After shared-browser bundle' + ); } if (Object.keys(sharedServer).length > 0) { @@ -111,7 +110,8 @@ export async function buildForFederation( Object.keys(sharedServer).forEach((packageName) => cachedSharedPackages.add(packageName) ); - checkIfBuildAborted(); + if (signal?.aborted) + throw new AbortedError('[buildForFederation] After shared-node bundle'); } if (Object.keys(separateBrowser).length > 0) { @@ -131,7 +131,10 @@ export async function buildForFederation( Object.keys(separateBrowser).forEach((packageName) => cachedSharedPackages.add(packageName) ); - checkIfBuildAborted(); + if (signal?.aborted) + throw new AbortedError( + '[buildForFederation] After separate-browser bundle' + ); } if (Object.keys(separateServer).length > 0) { @@ -153,7 +156,8 @@ export async function buildForFederation( ); } - checkIfBuildAborted(); + if (signal?.aborted) + throw new AbortedError('[buildForFederation] After separate-node bundle'); } const sharedMappingInfo = !artefactInfo @@ -172,7 +176,6 @@ export async function buildForFederation( buildNotificationsEndpoint, }; - checkIfBuildAborted(); writeFederationInfo(federationInfo, fedOptions); writeImportMap(sharedInfo, fedOptions); diff --git a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts index a85fdd17..bca837a4 100644 --- a/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts +++ b/libs/native-federation-core/src/lib/core/bundle-exposed-and-mappings.ts @@ -25,7 +25,9 @@ export async function bundleExposedAndMappings( signal?: AbortSignal ): Promise { if (signal?.aborted) { - throw new AbortedError('Aborted before bundling'); + throw new AbortedError( + '[bundle-exposed-and-mappings] Aborted before bundling' + ); } const shared = config.sharedMappings.map((sm) => { @@ -62,13 +64,14 @@ export async function bundleExposedAndMappings( signal, }); if (signal?.aborted) { - throw new AbortedError('Aborted after bundle'); + throw new AbortedError( + '[bundle-exposed-and-mappings] Aborted after bundle' + ); } } catch (error) { - if (signal?.aborted) { - throw error; + if (!(error instanceof AbortedError)) { + logger.error('Error building federation artefacts'); } - logger.error('Error building federation artefacts'); throw error; } diff --git a/libs/native-federation-core/src/lib/utils/errors.ts b/libs/native-federation-core/src/lib/utils/errors.ts index 0cfa0a1d..1f843ec4 100644 --- a/libs/native-federation-core/src/lib/utils/errors.ts +++ b/libs/native-federation-core/src/lib/utils/errors.ts @@ -1,8 +1,7 @@ export class AbortedError extends Error { constructor(message: string) { - super(message); // Call the constructor of the base class `Error` - this.name = 'AbortedError'; // Set the error name to your custom error class name - // Set the prototype explicitly to maintain the correct prototype chain + super(message); + this.name = 'AbortedError'; Object.setPrototypeOf(this, AbortedError.prototype); } } diff --git a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts index a7d7f433..3587487e 100644 --- a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts +++ b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts @@ -11,7 +11,9 @@ export class RebuildQueue { controller.abort(); } if (this.activeBuildCount > 0) - logger.info(`Aborted ${this.activeBuildCount} previous bundling task(s)`); + logger.debug( + `Aborted ${this.activeBuildCount} previous bundling task(s)` + ); this.activeBuilds.clear(); const controller = new AbortController(); diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index bf545ac4..917b2671 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -354,7 +354,6 @@ async function runEsbuild( throw new AbortedError('[runEsbuild] ESBuild was canceled.'); } throw error; - } finally { } } From bd58781270b29ca20333897a7243f429161092e6 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Thu, 20 Nov 2025 11:35:50 +0100 Subject: [PATCH 07/10] feat(971): Updated messages --- .../src/lib/utils/rebuild-queue.ts | 7 ++-- .../lib/model/build-notifications-options.ts | 1 + .../src/builders/build/builder.ts | 7 ++-- .../build/federation-build-notifier.ts | 10 ++++++ .../src/utils/angular-esbuild-adapter.ts | 33 +++++++------------ 5 files changed, 29 insertions(+), 29 deletions(-) diff --git a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts index 3587487e..071d3aa4 100644 --- a/libs/native-federation-core/src/lib/utils/rebuild-queue.ts +++ b/libs/native-federation-core/src/lib/utils/rebuild-queue.ts @@ -7,13 +7,12 @@ export class RebuildQueue { async enqueue(rebuildFn: () => Promise): Promise { const buildId = ++this.buildCounter; - for (const [id, controller] of this.activeBuilds) { + for (const [_, controller] of this.activeBuilds) { controller.abort(); } if (this.activeBuildCount > 0) - logger.debug( - `Aborted ${this.activeBuildCount} previous bundling task(s)` - ); + logger.info(`Aborted ${this.activeBuildCount} previous bundling task(s)`); + this.activeBuilds.clear(); const controller = new AbortController(); diff --git a/libs/native-federation-runtime/src/lib/model/build-notifications-options.ts b/libs/native-federation-runtime/src/lib/model/build-notifications-options.ts index 0da24f2e..abbac525 100644 --- a/libs/native-federation-runtime/src/lib/model/build-notifications-options.ts +++ b/libs/native-federation-runtime/src/lib/model/build-notifications-options.ts @@ -9,4 +9,5 @@ export const BUILD_NOTIFICATIONS_ENDPOINT = export enum BuildNotificationType { COMPLETED = 'federation-rebuild-complete', ERROR = 'federation-rebuild-error', + CANCELLED = 'federation-rebuild-cancelled', } diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 5d7cfacf..d6fc07af 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -465,9 +465,10 @@ export async function* runBuilder( }) .catch((error) => { if (error instanceof AbortedError) { - logger.warn('Rebuild was canceled.'); - if (options.verbose) - logger.warn('Cancellation point: ' + error?.message); + logger.verbose( + 'Rebuild was canceled. Cancellation point: ' + error?.message + ); + federationBuildNotifier.broadcastBuildCancellation(); } else { logger.error('Federation rebuild failed!'); if (options.verbose) console.error(error); diff --git a/libs/native-federation/src/builders/build/federation-build-notifier.ts b/libs/native-federation/src/builders/build/federation-build-notifier.ts index 09347956..00fd3b26 100644 --- a/libs/native-federation/src/builders/build/federation-build-notifier.ts +++ b/libs/native-federation/src/builders/build/federation-build-notifier.ts @@ -192,6 +192,16 @@ export class FederationBuildNotifier { }); } + /** + * Notifies about cancellation of a federation rebuild + */ + public broadcastBuildCancellation(): void { + this._broadcastEvent({ + type: BuildNotificationType.CANCELLED, + timestamp: Date.now(), + }); + } + /** * Notifies about failed federation rebuild */ diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 917b2671..350a12ba 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -313,22 +313,18 @@ async function runEsbuild( const ctx = await esbuild.context(config); - const abortHandler = () => { - ctx.cancel(); - ctx.dispose(); - }; - - if (signal) { - signal.addEventListener('abort', abortHandler, { once: true }); - } - try { - const result = await ctx.rebuild(); + const abortHandler = async () => { + await ctx.cancel(); + await ctx.dispose(); + }; - if (signal?.aborted) { - throw new AbortedError('[angular-esbuild-adapter] After building.'); + if (signal) { + signal.addEventListener('abort', abortHandler, { once: true }); } + const result = await ctx.rebuild(); + const memOnly = dev && kind === 'mapping-or-exposed' && !!_memResultHandler; const writtenFiles = writeResult(result, outdir, memOnly); @@ -344,12 +340,13 @@ async function runEsbuild( memOnly ); } else { - ctx.dispose(); if (signal) signal.removeEventListener('abort', abortHandler); + await ctx.dispose(); } return writtenFiles; } catch (error) { - ctx.dispose(); + // ESBuild throws an error if the request is cancelled. + // if it is, it's changed to an 'AbortedError' if (signal?.aborted && error?.message?.includes('canceled')) { throw new AbortedError('[runEsbuild] ESBuild was canceled.'); } @@ -436,14 +433,6 @@ function doesFileExistAndJsonEqual(path: string, content: string) { } } -function doesFileExist(path: string, content: string): boolean { - if (!fs.existsSync(path)) { - return false; - } - const currentContent = fs.readFileSync(path, 'utf-8'); - return currentContent === content; -} - function writeResult( result: esbuild.BuildResult, outdir: string, From 8b25cafd21e3ef57ec52f0fa6a936b62454ba532 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 26 Nov 2025 08:28:04 +0100 Subject: [PATCH 08/10] fix: Formatting issues --- apps/mfe1/src/app/nx-welcome.component.ts | 119 +++++++++++++++--- apps/mfe1/src/index.html | 2 +- apps/mfe2/src/app/nx-welcome.component.ts | 119 +++++++++++++++--- apps/mfe2/src/index.html | 2 +- .../tests/native-federation.spec.ts | 12 +- apps/playground/src/app/app.component.spec.ts | 2 +- apps/playground/src/index.html | 2 +- apps/playground/src/test-setup.ts | 2 +- .../enhanced/src/lib/init-federation.ts | 6 +- .../enhanced/src/lib/loadRemoteModule.ts | 8 +- .../src/lib/loader/dynamic-federation.ts | 28 ++--- libs/mf-tools/README.md | 6 +- libs/mf-tools/src/lib/utils/global-state.ts | 2 +- .../src/lib/web-components/bootstrap-utils.ts | 4 +- libs/mf/post-build.js | 2 +- libs/mf/src/builders/build/builder.ts | 4 +- libs/mf/src/generators/mf/generator.ts | 4 +- libs/mf/src/rspack/with-federation.ts | 2 +- .../mf/src/schematics/boot-async/schematic.ts | 4 +- .../src/schematics/init-rspack/schematic.ts | 32 ++--- .../src/schematics/init-webpack/schematic.ts | 36 +++--- libs/mf/src/schematics/init/schematic.ts | 6 +- .../src/schematics/nguniversal/schematic.ts | 4 +- libs/mf/src/schematics/patch/schematic.ts | 2 +- libs/mf/src/schematics/remove/schematic.ts | 4 +- libs/mf/src/server/colors.ts | 2 +- libs/mf/src/server/mf-dev-server.ts | 2 +- libs/mf/src/server/workspace.ts | 4 +- libs/mf/src/universal/create-fetch.ts | 2 +- libs/mf/src/utils/create-config.ts | 2 +- libs/mf/src/utils/modify-entry-plugin.ts | 4 +- libs/mf/src/utils/share-utils.ts | 28 ++--- libs/mf/src/utils/shared-mappings.ts | 8 +- libs/mf/src/utils/skip-list.ts | 2 +- libs/mf/src/utils/with-mf-plugin.ts | 6 +- libs/mf/tutorial/tutorial.md | 21 +--- libs/native-federation-core/README.md | 10 +- .../src/lib/config/share-utils.ts | 54 ++++---- .../src/lib/config/with-native-federation.ts | 18 +-- .../src/lib/core/build-adapter.d.ts | 2 +- .../src/lib/core/build-adapter.ts | 2 +- .../src/lib/core/build-for-federation.ts | 46 +++---- .../lib/core/bundle-exposed-and-mappings.ts | 14 +-- .../src/lib/core/bundle-shared.ts | 40 +++--- .../src/lib/core/default-skip-list.ts | 4 +- .../src/lib/core/federation-builder.ts | 2 +- .../src/lib/core/load-federation-config.ts | 8 +- .../src/lib/core/remove-unused-deps.ts | 18 +-- .../src/lib/core/write-federation-info.ts | 4 +- .../src/lib/core/write-import-map.ts | 4 +- .../src/lib/utils/build-result-map.ts | 4 +- .../src/lib/utils/get-external-imports.ts | 6 +- .../src/lib/utils/logger.ts | 2 +- .../src/lib/utils/mapped-paths.ts | 4 +- .../src/lib/utils/package-info.ts | 18 +-- .../src/lib/utils/resolve-glob.ts | 6 +- .../src/lib/utils/resolve-wildcard-keys.ts | 2 +- .../src/lib/utils/rewrite-chunk-imports.ts | 12 +- .../src/lib/adapter.ts | 14 +-- .../src/lib/node/init-node-federation.ts | 10 +- .../src/lib/utils/fstart.mjs | 32 ++--- .../src/lib/utils/import-map-loader.js | 28 ++--- .../src/lib/get-shared.ts | 2 +- .../src/lib/init-federation.ts | 22 ++-- .../src/lib/load-remote-module.ts | 14 +-- .../src/lib/utils/add-import-map.ts | 2 +- libs/native-federation/README.md | 13 +- libs/native-federation/post-build.js | 2 +- .../src/builders/build/builder.ts | 44 +++---- .../build/federation-build-notifier.ts | 16 +-- .../src/executors/build/executor.ts | 2 +- .../generators/native-federation/generator.ts | 6 +- .../src/patch-angular-build.ts | 2 +- .../src/plugin/dev-externals-mixin.ts | 2 +- libs/native-federation/src/plugin/index.ts | 6 +- .../src/schematics/appbuilder/schematic.ts | 20 +-- .../src/schematics/init/schematic.ts | 39 +++--- .../src/schematics/remove/schematic.ts | 20 +-- .../src/utils/angular-esbuild-adapter.ts | 43 ++++--- .../src/utils/angular-locales.ts | 2 +- .../src/utils/create-compiler-options.ts | 2 +- libs/native-federation/src/utils/i18n.ts | 16 +-- .../native-federation/src/utils/mem-resuts.ts | 5 +- .../src/utils/patch-angular-build.ts | 4 +- .../src/utils/shared-mappings-plugin.ts | 2 +- .../src/utils/updateIndexHtml.ts | 12 +- migration-guide-14.md | 9 +- tools/scripts/publish-utils.mjs | 14 +-- tools/scripts/publish.mjs | 2 +- tools/scripts/start-local-registry.ts | 2 +- 90 files changed, 654 insertions(+), 530 deletions(-) diff --git a/apps/mfe1/src/app/nx-welcome.component.ts b/apps/mfe1/src/app/nx-welcome.component.ts index 6d7ebbcd..95134e5b 100644 --- a/apps/mfe1/src/app/nx-welcome.component.ts +++ b/apps/mfe1/src/app/nx-welcome.component.ts @@ -15,9 +15,20 @@ import { Component, OnInit, ViewEncapsulation } from '@angular/core';