Skip to content
3 changes: 2 additions & 1 deletion libs/native-federation-core/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
1 change: 1 addition & 0 deletions libs/native-federation-core/src/lib/core/build-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface BuildAdapterOptions {
hash: boolean;
platform?: 'browser' | 'node';
optimizedMappings?: boolean;
signal?: AbortSignal;
}

export interface BuildResult {
Expand Down
23 changes: 23 additions & 0 deletions libs/native-federation-core/src/lib/core/build-for-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@ 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;
skipShared: boolean;
signal?: AbortSignal;
}

export const defaultBuildParams: BuildParams = {
Expand All @@ -35,6 +37,8 @@ export async function buildForFederation(
externals: string[],
buildParams = defaultBuildParams,
): Promise<FederationInfo> {
const signal = buildParams.signal;

let artefactInfo: ArtefactInfo | undefined;

if (!buildParams.skipMappingsAndExposed) {
Expand All @@ -43,11 +47,17 @@ export async function buildForFederation(
config,
fedOptions,
externals,
signal,
);
logger.measure(
start,
'[build artifacts] - To bundle all mappings and exposed.',
);

if (signal?.aborted)
throw new AbortedError(
'[buildForFederation] After exposed-and-mappings bundle',
);
}

const exposedInfo = !artefactInfo
Expand Down Expand Up @@ -77,6 +87,10 @@ export async function buildForFederation(
Object.keys(sharedBrowser).forEach((packageName) =>
cachedSharedPackages.add(packageName),
);
if (signal?.aborted)
throw new AbortedError(
'[buildForFederation] After shared-browser bundle',
);
}

if (Object.keys(sharedServer).length > 0) {
Expand All @@ -96,6 +110,8 @@ export async function buildForFederation(
Object.keys(sharedServer).forEach((packageName) =>
cachedSharedPackages.add(packageName),
);
if (signal?.aborted)
throw new AbortedError('[buildForFederation] After shared-node bundle');
}

if (Object.keys(separateBrowser).length > 0) {
Expand All @@ -115,6 +131,10 @@ export async function buildForFederation(
Object.keys(separateBrowser).forEach((packageName) =>
cachedSharedPackages.add(packageName),
);
if (signal?.aborted)
throw new AbortedError(
'[buildForFederation] After separate-browser bundle',
);
}

if (Object.keys(separateServer).length > 0) {
Expand All @@ -135,6 +155,9 @@ export async function buildForFederation(
cachedSharedPackages.add(packageName),
);
}

if (signal?.aborted)
throw new AbortedError('[buildForFederation] After separate-node bundle');
}

const sharedMappingInfo = !artefactInfo
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -21,7 +22,14 @@ export async function bundleExposedAndMappings(
config: NormalizedFederationConfig,
fedOptions: FederationOptions,
externals: string[],
signal?: AbortSignal,
): Promise<ArtefactInfo> {
if (signal?.aborted) {
throw new AbortedError(
'[bundle-exposed-and-mappings] Aborted before bundling',
);
}

const shared = config.sharedMappings.map((sm) => {
const entryPoint = sm.path;
const tmp = sm.key.replace(/[^A-Za-z0-9]/g, '_');
Expand Down Expand Up @@ -53,9 +61,17 @@ export async function bundleExposedAndMappings(
kind: 'mapping-or-exposed',
hash,
optimizedMappings: config.features.ignoreUnusedDeps,
signal,
});
if (signal?.aborted) {
throw new AbortedError(
'[bundle-exposed-and-mappings] Aborted after bundle',
);
}
} catch (error) {
logger.error('Error building federation artefacts');
if (!(error instanceof AbortedError)) {
logger.error('Error building federation artefacts');
}
throw error;
}

Expand Down
7 changes: 7 additions & 0 deletions libs/native-federation-core/src/lib/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export class AbortedError extends Error {
constructor(message: string) {
super(message);
this.name = 'AbortedError';
Object.setPrototypeOf(this, AbortedError.prototype);
}
}
60 changes: 60 additions & 0 deletions libs/native-federation-core/src/lib/utils/rebuild-queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { logger } from './logger';

interface BuildControl {
controller: AbortController;
buildFinished: { resolve: () => void; promise: Promise<void> };
}

export class RebuildQueue {
private activeBuilds: Map<number, BuildControl> = new Map();
private buildCounter = 0;

async enqueue(
rebuildFn: (signal: AbortSignal) => Promise<void>,
): Promise<void> {
const buildId = ++this.buildCounter;

const pendingCancellations = Array.from(this.activeBuilds.values()).map(
(buildInfo) => {
buildInfo.controller.abort();
return buildInfo.buildFinished.promise;
},
);

if (pendingCancellations.length > 0) {
logger.info(`Aborting ${pendingCancellations.length} bundling task(s)..`);
}

if (pendingCancellations.length > 0) {
await Promise.all(pendingCancellations);
}

let buildFinished: () => void;
const completionPromise = new Promise<void>((resolve) => {
buildFinished = resolve;
});

const control: BuildControl = {
controller: new AbortController(),
buildFinished: {
resolve: buildFinished!,
promise: completionPromise,
},
};
this.activeBuilds.set(buildId, control);

try {
await rebuildFn(control.controller.signal);
} finally {
control.buildFinished.resolve();
this.activeBuilds.delete(buildId);
}
}

dispose(): void {
for (const [_, buildInfo] of this.activeBuilds) {
buildInfo.controller.abort();
}
this.activeBuilds.clear();
}
}
4 changes: 3 additions & 1 deletion libs/native-federation-node/src/lib/utils/fstart.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,9 @@ function resolveAndComposeImportMap(parsed) {
);
if (invalidKeys.length > 0) {
console.warn(
`Invalid top-level key${invalidKeys.length > 0 ? 's' : ''} in import map - ${invalidKeys.join(', ')}`,
`Invalid top-level key${
invalidKeys.length > 0 ? 's' : ''
} in import map - ${invalidKeys.join(', ')}`,
);
}
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ export const BUILD_NOTIFICATIONS_ENDPOINT =
export enum BuildNotificationType {
COMPLETED = 'federation-rebuild-complete',
ERROR = 'federation-rebuild-error',
CANCELLED = 'federation-rebuild-cancelled',
}
70 changes: 57 additions & 13 deletions libs/native-federation/src/builders/build/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
logger,
setBuildAdapter,
setLogLevel,
RebuildQueue,
AbortedError,
} from '@softarc/native-federation/build';
import {
createAngularBuildAdapter,
Expand Down Expand Up @@ -373,8 +375,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;

Expand All @@ -399,8 +402,31 @@ export async function* runBuilder(
// }

if (!first && (nfOptions.dev || watch)) {
setTimeout(async () => {
try {
rebuildQueue
.enqueue(async (signal: AbortSignal) => {
if (signal?.aborted) {
throw new AbortedError('Build canceled before starting');
}

await new Promise((resolve, reject) => {
const timeout = setTimeout(
resolve,
Math.max(10, nfOptions.rebuildDelay),
);

if (signal) {
const abortHandler = () => {
clearTimeout(timeout);
reject(new AbortedError('[builder] During delay.'));
};
signal.addEventListener('abort', abortHandler, { once: true });
}
});

if (signal?.aborted) {
throw new AbortedError('[builder] Before federation build.');
}

const start = process.hrtime();
federationResult = await buildForFederation(
config,
Expand All @@ -409,9 +435,14 @@ export async function* runBuilder(
{
skipMappingsAndExposed: false,
skipShared: true,
signal,
},
);

if (signal?.aborted) {
throw new AbortedError('[builder] After federation build.');
}

if (hasLocales && localeFilter) {
translateFederationArtefacts(
i18n,
Expand All @@ -421,27 +452,40 @@ export async function* runBuilder(
);
}

if (signal?.aborted) {
throw new AbortedError(
'[builder] After federation translations.',
);
}

logger.info('Done!');

// Notifies about build completion
if (isLocalDevelopment) {
federationBuildNotifier.broadcastBuildCompletion();
}
logger.measure(start, 'To rebuild nf.');
} catch (error) {
logger.error('Federation rebuild failed!');

// Notifies about build failure
if (isLocalDevelopment) {
federationBuildNotifier.broadcastBuildError(error);
logger.measure(start, 'To rebuild the federation artifacts.');
})
.catch((error) => {
if (error instanceof AbortedError) {
logger.verbose(
'Rebuild was canceled. Cancellation point: ' + error?.message,
);
federationBuildNotifier.broadcastBuildCancellation();
} else {
logger.error('Federation rebuild failed!');
if (options.verbose) console.error(error);
if (isLocalDevelopment) {
federationBuildNotifier.broadcastBuildError(error);
}
}
}
}, nfOptions.rebuildDelay);
});
}

first = false;
}
} finally {
rebuildQueue.dispose();

if (isLocalDevelopment) {
federationBuildNotifier.stopEventServer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
2 changes: 1 addition & 1 deletion libs/native-federation/src/schematics/init/schematic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ function updateWorkspaceConfig(
builder: '@angular-architects/native-federation:build',
options: {
target: `${projectName}:serve-original:development`,
rebuildDelay: 0,
rebuildDelay: 500,
dev: true,
port: 0,
},
Expand Down
Loading