From be298835ffd3a402f21fa818378af6fc9f196c1a Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 17 Feb 2026 10:45:37 +0100 Subject: [PATCH 1/5] fix: Added persistent cache --- .../src/lib/core/build-adapter.ts | 1 + .../src/lib/core/build-for-federation.ts | 13 +++-- .../lib/core/bundle-exposed-and-mappings.ts | 2 + .../src/utils/angular-esbuild-adapter.ts | 39 ++----------- .../utils/create-awaitable-compiler-plugin.ts | 57 ++++++++++--------- .../src/utils/create-compiler-options.ts | 4 +- 6 files changed, 49 insertions(+), 67 deletions(-) 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 b29cf30a..ddc31853 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; + cachePath?: string; signal?: AbortSignal; } 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 6b4e80a1..5a972e99 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 @@ -42,12 +42,19 @@ export async function buildForFederation( let artefactInfo: ArtefactInfo | undefined; + const cacheProjectFolder = resolveProjectName(config); + const pathToCache = getCachePath( + fedOptions.workspaceRoot, + cacheProjectFolder, + ); + if (!buildParams.skipMappingsAndExposed) { const start = process.hrtime(); artefactInfo = await bundleExposedAndMappings( config, fedOptions, externals, + pathToCache, signal, ); logger.measure( @@ -65,12 +72,6 @@ export async function buildForFederation( ? describeExposed(config, fedOptions) : artefactInfo.exposes; - const cacheProjectFolder = resolveProjectName(config); - const pathToCache = getCachePath( - fedOptions.workspaceRoot, - cacheProjectFolder, - ); - if (!buildParams.skipShared && sharedPackageInfoCache.length > 0) { logger.info('Checksum matched, re-using cached externals.'); } 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 3906d1fe..43e20f55 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 @@ -22,6 +22,7 @@ export async function bundleExposedAndMappings( config: NormalizedFederationConfig, fedOptions: FederationOptions, externals: string[], + cachePath: string, signal?: AbortSignal, ): Promise { if (signal?.aborted) { @@ -61,6 +62,7 @@ export async function bundleExposedAndMappings( kind: 'mapping-or-exposed', hash, optimizedMappings: config.features.ignoreUnusedDeps, + cachePath, signal, }); if (signal?.aborted) { diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index c87a26a1..6bd15bea 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -14,6 +14,7 @@ import { generateSearchDirectories, findTailwindConfiguration, loadPostcssConfiguration, + SourceFileCache, } from '@angular/build/private'; import { createCompilerPluginOptions } from './create-compiler-options'; @@ -72,6 +73,7 @@ export function createAngularBuildAdapter( dev, hash, platform, + cachePath, optimizedMappings, signal, } = options; @@ -96,6 +98,7 @@ export function createAngularBuildAdapter( undefined, platform, optimizedMappings, + cachePath, signal, ); @@ -109,37 +112,6 @@ export function createAngularBuildAdapter( } return files.map((fileName) => ({ fileName }) as BuildResult); - - // TODO: Do we still need rollup as esbuilt evolved? - // if (kind === 'shared-package') { - // await runRollup(entryPoint, external, outfile); - // } else { - - // if ( - // dev && - // kind === 'shared-package' && - // entryPoint.match(fesmFolderRegExp) - // ) { - // fs.copyFileSync(entryPoint, outfile); - // } else { - // await runEsbuild( - // builderOptions, - // context, - // entryPoint, - // external, - // outfile, - // tsConfigPath, - // mappedPaths, - // watch, - // rebuildRequested, - // dev, - // kind - // ); - // } - // if (kind === 'shared-package' && fs.existsSync(outfile)) { - // await link(outfile, dev); - // } - // } }; async function link(outfile: string, dev: boolean) { @@ -154,8 +126,6 @@ export function createAngularBuildAdapter( const result = await transformAsync(code, { filename: outfile, - // inputSourceMap: (useInputSourcemap ? undefined : false) as undefined, - // sourceMaps: pluginOptions.sourcemap ? 'inline' : false, compact: !dev, configFile: false, babelrc: false, @@ -196,6 +166,7 @@ async function runEsbuild( logLevel: esbuild.LogLevel = 'warning', platform?: 'browser' | 'node', optimizedMappings?: boolean, + cachePath?: string, signal?: AbortSignal, ) { if (signal?.aborted) { @@ -270,7 +241,7 @@ async function runEsbuild( postcssConfiguration, } as any, target, - undefined, + cachePath ? new SourceFileCache(cachePath) : undefined, ); const commonjsPluginModule = await import('@chialab/esbuild-plugin-commonjs'); diff --git a/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts index 1b3e125b..538d0d6e 100644 --- a/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts +++ b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts @@ -3,38 +3,43 @@ import { createCompilerPlugin } from '@angular/build/private'; type CreateCompilerPluginParams = Parameters; +let plugin: esbuild.Plugin | undefined = undefined; +let pluginPromise: Promise = new Promise(() => {}); + export function createAwaitableCompilerPlugin( pluginOptions: CreateCompilerPluginParams[0], styleOptions: CreateCompilerPluginParams[1], ): [esbuild.Plugin, Promise] { - const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); + if (!!plugin) { + const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); - let resolveDispose: () => void; - const onDisposePromise = new Promise((resolve) => { - resolveDispose = resolve; - }); + let resolveDispose: () => void; + pluginPromise = new Promise((resolve) => { + resolveDispose = resolve; + }); - const wrappedPlugin: esbuild.Plugin = { - ...originalPlugin, - setup(build: esbuild.PluginBuild) { - // Wrap the build object to intercept onDispose - const wrappedBuild = new Proxy(build, { - get(target, prop) { - if (prop === 'onDispose') { - return (callback: () => void | Promise) => { - return target.onDispose(() => { - callback(); - resolveDispose(); - }); - }; - } - return target[prop as keyof esbuild.PluginBuild]; - }, - }); + plugin = { + ...originalPlugin, + setup(build: esbuild.PluginBuild) { + // Wrap the build object to intercept onDispose + const wrappedBuild = new Proxy(build, { + get(target, prop) { + if (prop === 'onDispose') { + return (callback: () => void | Promise) => { + return target.onDispose(() => { + callback(); + resolveDispose(); + }); + }; + } + return target[prop as keyof esbuild.PluginBuild]; + }, + }); - return originalPlugin.setup(wrappedBuild); - }, - }; + return originalPlugin.setup(wrappedBuild); + }, + }; + } - return [wrappedPlugin, onDisposePromise]; + return [plugin, pluginPromise]; } diff --git a/libs/native-federation/src/utils/create-compiler-options.ts b/libs/native-federation/src/utils/create-compiler-options.ts index 3a090eeb..cd89dfbf 100644 --- a/libs/native-federation/src/utils/create-compiler-options.ts +++ b/libs/native-federation/src/utils/create-compiler-options.ts @@ -1,10 +1,12 @@ // Taken from https://github.com/angular/angular-cli/blob/main/packages/angular/build/src/tools/esbuild/compiler-plugin-options.ts // Currently, this type cannot be accessed from the outside +import { SourceFileCache } from '@angular/build/private'; + export function createCompilerPluginOptions( options: any, target: string[], - sourceFileCache?: any, + sourceFileCache?: SourceFileCache, ): { pluginOptions: any[0]; styleOptions: any[1]; From e1d84d2338bb27e34b2b0f22820d0dada48a1626 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 17 Feb 2026 10:53:13 +0100 Subject: [PATCH 2/5] fix: Remove memHandler --- .../src/builders/build/builder.ts | 35 +------------------ .../src/utils/angular-esbuild-adapter.ts | 34 ++---------------- 2 files changed, 4 insertions(+), 65 deletions(-) diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 2b608e31..7ead65bb 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -28,10 +28,7 @@ import { RebuildQueue, AbortedError, } from '@softarc/native-federation/build'; -import { - createAngularBuildAdapter, - setMemResultHandler, -} from '../../utils/angular-esbuild-adapter'; +import { createAngularBuildAdapter } from '../../utils/angular-esbuild-adapter'; import { JsonObject } from '@angular-devkit/core'; import { existsSync, mkdirSync, rmSync } from 'fs'; @@ -139,7 +136,6 @@ export async function* runBuilder( let serverOptions = null; - const write = true; const watch = nfOptions.watch; if (options['buildTarget']) { @@ -317,15 +313,6 @@ export async function* runBuilder( mkdirSync(fedOptions.outputPath, { recursive: true }); } - if (!write) { - setMemResultHandler((outFiles, outDir) => { - const fullOutDir = outDir - ? path.join(fedOptions.workspaceRoot, outDir) - : null; - memResults.add(outFiles.map((f) => new EsBuildResult(f, fullOutDir))); - }); - } - let federationResult: FederationInfo; try { const start = process.hrtime(); @@ -382,26 +369,6 @@ export async function* runBuilder( for await (const output of builderRun) { lastResult = output; - if (!write && output['outputFiles']) { - memResults.add( - output['outputFiles'].map((file) => new EsBuildResult(file)), - ); - } - - if (!write && output['assetFiles']) { - memResults.add( - output['assetFiles'].map((file) => new NgCliAssetResult(file)), - ); - } - - // if (write && !runServer && !nfOptions.skipHtmlTransform) { - // updateIndexHtml(fedOptions, nfOptions); - // } - - // if (!runServer) { - // yield output; - // } - if (!first && (nfOptions.dev || watch)) { rebuildQueue .enqueue(async (signal: AbortSignal) => { diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 6bd15bea..d40d7cb3 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -8,7 +8,6 @@ import { import * as esbuild from 'esbuild'; import { - createCompilerPlugin, transformSupportedBrowsersToTargets, getSupportedBrowsers, generateSearchDirectories, @@ -45,17 +44,6 @@ import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin'; -export type MemResultHandler = ( - outfiles: esbuild.OutputFile[], - outdir?: string, -) => void; - -let _memResultHandler: MemResultHandler; - -export function setMemResultHandler(handler: MemResultHandler): void { - _memResultHandler = handler; -} - export function createAngularBuildAdapter( builderOptions: ApplicationBuilderOptions, context: BuilderContext, @@ -272,7 +260,7 @@ async function runEsbuild( 'async-await': false, 'object-rest-spread': false, }, - splitting: true, //kind === 'mapping-or-exposed', + splitting: true, platform: platform ?? 'browser', format: 'esm', target: target, @@ -307,9 +295,7 @@ async function runEsbuild( const result = await ctx.rebuild(); - const memOnly = dev && kind === 'mapping-or-exposed' && !!_memResultHandler; - - const writtenFiles = writeResult(result, outdir, memOnly); + const writtenFiles = writeResult(result, outdir); if (watch) { registerForRebuilds( @@ -319,7 +305,6 @@ async function runEsbuild( entryPoints, outdir, hash, - memOnly, ); } else { if (signal) signal.removeEventListener('abort', abortHandler); @@ -419,27 +404,15 @@ function doesFileExistAndJsonEqual(path: string, content: string) { function writeResult( result: esbuild.BuildResult, outdir: string, - memOnly: boolean, ) { const writtenFiles: string[] = []; - if (memOnly) { - _memResultHandler(result.outputFiles, outdir); - } - for (const outFile of result.outputFiles) { const fileName = path.basename(outFile.path); const filePath = path.join(outdir, fileName); - if (!memOnly) { - fs.writeFileSync(filePath, outFile.text); - } writtenFiles.push(filePath); } - if (!memOnly) { - // for (const asset of result.outputFiles) - } - return writtenFiles; } @@ -450,12 +423,11 @@ function registerForRebuilds( entryPoints: EntryPoint[], outdir: string, hash: boolean, - memOnly: boolean, ) { if (kind !== 'shared-package') { rebuildRequested.rebuild.register(async () => { const result = await ctx.rebuild(); - writeResult(result, outdir, memOnly); + writeResult(result, outdir); }); } } From a0ee61845d2b4a04f8d06ac46343a5c2c3111834 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 18 Feb 2026 08:56:34 +0100 Subject: [PATCH 3/5] feat: Refactored to allow rebuilds using the Angular cache --- libs/native-federation-core/src/build.ts | 8 +- .../src/lib/core/build-adapter.ts | 45 +-- .../src/lib/core/build-for-federation.ts | 111 ++++--- .../lib/core/bundle-exposed-and-mappings.ts | 35 ++- .../src/lib/core/bundle-shared.ts | 11 +- .../src/lib/core/federation-builder.ts | 11 +- .../src/lib/utils/build-utils.ts | 6 - .../src/builders/build/builder.ts | 47 +-- .../src/utils/angular-esbuild-adapter.ts | 275 ++++++++++-------- .../utils/create-awaitable-compiler-plugin.ts | 6 +- .../src/utils/event-sorce.ts | 18 -- .../src/utils/rebuild-events.ts | 9 - 12 files changed, 297 insertions(+), 285 deletions(-) delete mode 100644 libs/native-federation-core/src/lib/utils/build-utils.ts delete mode 100644 libs/native-federation/src/utils/event-sorce.ts delete mode 100644 libs/native-federation/src/utils/rebuild-events.ts diff --git a/libs/native-federation-core/src/build.ts b/libs/native-federation-core/src/build.ts index 090846ef..69a5294d 100644 --- a/libs/native-federation-core/src/build.ts +++ b/libs/native-federation-core/src/build.ts @@ -7,13 +7,15 @@ export { export { withNativeFederation } from './lib/config/with-native-federation'; export { BuildAdapter, - BuildAdapterOptions, - BuildKind, BuildResult, EntryPoint, + SetupOptions, setBuildAdapter, } from './lib/core/build-adapter'; -export { buildForFederation } from './lib/core/build-for-federation'; +export { + buildForFederation, + rebuildForFederation, +} from './lib/core/build-for-federation'; export { bundleExposedAndMappings } from './lib/core/bundle-exposed-and-mappings'; export { FederationOptions } from './lib/core/federation-options'; export { getExternals } from './lib/core/get-externals'; 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 ddc31853..29d69f6c 100644 --- a/libs/native-federation-core/src/lib/core/build-adapter.ts +++ b/libs/native-federation-core/src/lib/core/build-adapter.ts @@ -1,53 +1,54 @@ import { logger } from '../utils/logger'; import { MappedPath } from '../utils/mapped-paths'; -let _buildAdapter: BuildAdapter = async () => { - // TODO: add logger - logger.error('Please set a BuildAdapter!'); - return []; -}; - -export type BuildKind = - | 'shared-package' - | 'shared-mapping' - | 'exposed' - | 'mapping-or-exposed'; +let _buildAdapter: BuildAdapter | null = null; + +// export type BuildKind = +// | 'shared-package' +// | 'shared-mapping' +// | 'exposed' +// | 'mapping-or-exposed'; export interface EntryPoint { fileName: string; outName: string; } -export interface BuildAdapterOptions { +export interface SetupOptions { entryPoints: EntryPoint[]; tsConfigPath?: string; - external: Array; + external: string[]; outdir: string; mappedPaths: MappedPath[]; - packageName?: string; - esm?: boolean; + bundleName: string; + isNodeModules: boolean; dev?: boolean; - watch?: boolean; - kind: BuildKind; - hash: boolean; + hash?: boolean; platform?: 'browser' | 'node'; optimizedMappings?: boolean; cachePath?: string; - signal?: AbortSignal; } export interface BuildResult { fileName: string; } -export type BuildAdapter = ( - options: BuildAdapterOptions, -) => Promise; +export interface BuildAdapter { + setup(options: SetupOptions): Promise; + + build(name: string, signal?: AbortSignal): Promise; + + dispose(name?: string): Promise; +} export function setBuildAdapter(buildAdapter: BuildAdapter): void { _buildAdapter = buildAdapter; } export function getBuildAdapter(): BuildAdapter { + if (!_buildAdapter) { + logger.error('Please set a BuildAdapter!'); + throw new Error('BuildAdapter not set'); + } return _buildAdapter; } 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 5a972e99..33011537 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 @@ -19,27 +19,14 @@ import { normalizePackageName } from '../utils/normalize'; import { AbortedError } from '../utils/errors'; import { resolveProjectName } from '../utils/config-utils'; -export interface BuildParams { - skipMappingsAndExposed: boolean; - skipShared: boolean; - signal?: AbortSignal; -} - -export const defaultBuildParams: BuildParams = { - skipMappingsAndExposed: false, - skipShared: false, -}; - const sharedPackageInfoCache: SharedInfo[] = []; export async function buildForFederation( config: NormalizedFederationConfig, fedOptions: FederationOptions, externals: string[], - buildParams = defaultBuildParams, + signal?: AbortSignal, ): Promise { - const signal = buildParams.signal; - let artefactInfo: ArtefactInfo | undefined; const cacheProjectFolder = resolveProjectName(config); @@ -48,35 +35,34 @@ export async function buildForFederation( cacheProjectFolder, ); - if (!buildParams.skipMappingsAndExposed) { - const start = process.hrtime(); - artefactInfo = await bundleExposedAndMappings( - config, - fedOptions, - externals, - pathToCache, - signal, - ); - logger.measure( - start, - '[build artifacts] - To bundle all mappings and exposed.', - ); + const start = process.hrtime(); + artefactInfo = await bundleExposedAndMappings( + config, + fedOptions, + externals, + pathToCache, + true, + signal, + ); + logger.measure( + start, + '[build artifacts] - To bundle all mappings and exposed.', + ); - if (signal?.aborted) - throw new AbortedError( - '[buildForFederation] After exposed-and-mappings bundle', - ); - } + if (signal?.aborted) + throw new AbortedError( + '[buildForFederation] After exposed-and-mappings bundle', + ); const exposedInfo = !artefactInfo ? describeExposed(config, fedOptions) : artefactInfo.exposes; - if (!buildParams.skipShared && sharedPackageInfoCache.length > 0) { + if (sharedPackageInfoCache.length > 0) { logger.info('Checksum matched, re-using cached externals.'); } - if (!buildParams.skipShared && sharedPackageInfoCache.length === 0) { + if (sharedPackageInfoCache.length === 0) { const { sharedBrowser, sharedServer, separateBrowser, separateServer } = splitShared(config.shared); @@ -193,6 +179,63 @@ export async function buildForFederation( return federationInfo; } +export async function rebuildForFederation( + config: NormalizedFederationConfig, + fedOptions: FederationOptions, + externals: string[], + signal?: AbortSignal, +): Promise { + const cacheProjectFolder = resolveProjectName(config); + const pathToCache = getCachePath( + fedOptions.workspaceRoot, + cacheProjectFolder, + ); + + const start = process.hrtime(); + let artefactInfo = await bundleExposedAndMappings( + config, + fedOptions, + externals, + pathToCache, + false, + signal, + ); + logger.measure( + start, + '[build artifacts] - To re-bundle all mappings and exposed.', + ); + + if (signal?.aborted) + throw new AbortedError( + '[buildForFederation] After rebuild-exposed-and-mappings bundle', + ); + + const exposedInfo = !artefactInfo + ? describeExposed(config, fedOptions) + : artefactInfo.exposes; + + const sharedMappingInfo = !artefactInfo + ? describeSharedMappings(config, fedOptions) + : artefactInfo.mappings; + + const sharedInfo = [...sharedPackageInfoCache, ...sharedMappingInfo]; + const buildNotificationsEndpoint = + fedOptions.buildNotifications?.enable && fedOptions.dev + ? fedOptions.buildNotifications?.endpoint + : undefined; + const federationInfo: FederationInfo = { + name: config.name, + shared: sharedInfo, + exposes: exposedInfo, + buildNotificationsEndpoint, + }; + + writeFederationInfo(federationInfo, fedOptions); + writeImportMap(sharedInfo, fedOptions); + + return federationInfo; +} + type SplitSharedResult = { sharedServer: Record; sharedBrowser: Record; 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 43e20f55..f046c746 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 @@ -7,11 +7,11 @@ import { createBuildResultMap, lookupInResultMap, } from '../utils/build-result-map'; -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'; +import { getBuildAdapter } from './build-adapter'; export interface ArtefactInfo { mappings: SharedInfo[]; @@ -23,6 +23,7 @@ export async function bundleExposedAndMappings( fedOptions: FederationOptions, externals: string[], cachePath: string, + setup: boolean, signal?: AbortSignal, ): Promise { if (signal?.aborted) { @@ -51,20 +52,24 @@ export async function bundleExposedAndMappings( let result; try { - result = await bundle({ - entryPoints, - outdir: fedOptions.outputPath, - tsConfigPath: fedOptions.tsConfig, - external: externals, - dev: !!fedOptions.dev, - watch: fedOptions.watch, - mappedPaths: config.sharedMappings, - kind: 'mapping-or-exposed', - hash, - optimizedMappings: config.features.ignoreUnusedDeps, - cachePath, - signal, - }); + if (setup) { + await getBuildAdapter().setup({ + entryPoints, + outdir: fedOptions.outputPath, + tsConfigPath: fedOptions.tsConfig, + external: externals, + dev: !!fedOptions.dev, + mappedPaths: config.sharedMappings, + bundleName: 'mapping-or-exposed', + isNodeModules: false, + hash, + optimizedMappings: config.features.ignoreUnusedDeps, + cachePath, + }); + } + + result = await getBuildAdapter().build('mapping-or-exposed', signal); + if (signal?.aborted) { throw new AbortedError( '[bundle-exposed-and-mappings] Aborted after bundle', diff --git a/libs/native-federation-core/src/lib/core/bundle-shared.ts b/libs/native-federation-core/src/lib/core/bundle-shared.ts index e69117c1..78ee6de0 100644 --- a/libs/native-federation-core/src/lib/core/bundle-shared.ts +++ b/libs/native-federation-core/src/lib/core/bundle-shared.ts @@ -4,14 +4,13 @@ import { NormalizedFederationConfig, NormalizedSharedConfig, } from '../config/federation-config'; -import { bundle } from '../utils/build-utils'; import { getPackageInfo, PackageInfo } from '../utils/package-info'; import { SharedInfo } from '@softarc/native-federation-runtime'; import { FederationOptions } from './federation-options'; import { logger } from '../utils/logger'; import crypto from 'crypto'; import { DEFAULT_EXTERNAL_LIST } from './default-external-list'; -import { BuildResult } from './build-adapter'; +import { BuildResult, getBuildAdapter } from './build-adapter'; import { deriveInternalName, isSourceFile, @@ -105,18 +104,22 @@ export async function bundleShared( let bundleResult: BuildResult[] | null = null; try { - bundleResult = await bundle({ + await getBuildAdapter().setup({ entryPoints, tsConfigPath: fedOptions.tsConfig, external: [...additionalExternals, ...externals], outdir: cacheOptions.pathToCache, mappedPaths: config.sharedMappings, dev: fedOptions.dev, - kind: 'shared-package', + bundleName: cacheOptions.bundleName, + isNodeModules: true, hash: false, platform, optimizedMappings: config.features.ignoreUnusedDeps, }); + bundleResult = await getBuildAdapter().build(cacheOptions.bundleName); + + await getBuildAdapter().dispose(cacheOptions.bundleName); const cachedFiles = bundleResult.map((br) => path.basename(br.fileName)); rewriteImports(cachedFiles, cacheOptions.pathToCache); diff --git a/libs/native-federation-core/src/lib/core/federation-builder.ts b/libs/native-federation-core/src/lib/core/federation-builder.ts index a0df749e..36128679 100644 --- a/libs/native-federation-core/src/lib/core/federation-builder.ts +++ b/libs/native-federation-core/src/lib/core/federation-builder.ts @@ -6,7 +6,7 @@ import { } from '../config/configuration-context'; import { NormalizedFederationConfig } from '../config/federation-config'; import { BuildAdapter, setBuildAdapter } from './build-adapter'; -import { buildForFederation, defaultBuildParams } from './build-for-federation'; +import { buildForFederation } from './build-for-federation'; import { FederationOptions } from './federation-options'; import { getExternals } from './get-externals'; import { loadFederationConfig } from './load-federation-config'; @@ -32,13 +32,8 @@ async function init(params: BuildHelperParams): Promise { externals = getExternals(config); } -async function build(buildParams = defaultBuildParams): Promise { - fedInfo = await buildForFederation( - config, - fedOptions, - externals, - buildParams, - ); +async function build(signal?: AbortSignal): Promise { + fedInfo = await buildForFederation(config, fedOptions, externals, signal); } export const federationBuilder = { diff --git a/libs/native-federation-core/src/lib/utils/build-utils.ts b/libs/native-federation-core/src/lib/utils/build-utils.ts deleted file mode 100644 index 51957501..00000000 --- a/libs/native-federation-core/src/lib/utils/build-utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { BuildAdapterOptions, getBuildAdapter } from '../core/build-adapter'; - -export async function bundle(options: BuildAdapterOptions) { - const adapter = getBuildAdapter(); - return await adapter(options); -} diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 7ead65bb..755be49a 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -19,6 +19,7 @@ import { normalizeOptions } from '@angular-devkit/build-angular/src/builders/dev import { buildForFederation, + rebuildForFederation, FederationOptions, getExternals, loadFederationConfig, @@ -33,15 +34,9 @@ import { createAngularBuildAdapter } from '../../utils/angular-esbuild-adapter'; import { JsonObject } from '@angular-devkit/core'; import { existsSync, mkdirSync, rmSync } from 'fs'; import { fstart } from '../../tools/fstart-as-data-url'; -import { - EsBuildResult, - MemResults, - NgCliAssetResult, -} from '../../utils/mem-resuts'; import { FederationInfo } from '@softarc/native-federation-runtime'; import { PluginBuild } from 'esbuild'; import { getI18nConfig, translateFederationArtefacts } from '../../utils/i18n'; -import { RebuildHubs } from '../../utils/rebuild-events'; import { createSharedMappingsPlugin } from '../../utils/shared-mappings-plugin'; import { updateScriptTags } from '../../utils/updateIndexHtml'; import { federationBuildNotifier } from './federation-build-notifier'; @@ -167,9 +162,7 @@ export async function* runBuilder( options.outputPath = nfOptions.outputPath; } - const rebuildEvents = new RebuildHubs(); - - const adapter = createAngularBuildAdapter(options, context, rebuildEvents); + const adapter = createAngularBuildAdapter(options, context); setBuildAdapter(adapter); setLogLevel(options.verbose ? 'verbose' : 'info'); @@ -218,7 +211,7 @@ export async function* runBuilder( federationConfig: inferConfigPath(options.tsConfig), tsConfig: options.tsConfig, verbose: options.verbose, - watch: false, // options.watch, + watch: nfOptions.dev || watch, dev: !!nfOptions.dev, entryPoint, buildNotifications: nfOptions.buildNotifications, @@ -300,8 +293,6 @@ export async function* runBuilder( }, ]; - const memResults = new MemResults(); - let first = true; let lastResult: { success: boolean } | undefined; @@ -396,15 +387,12 @@ export async function* runBuilder( } const start = process.hrtime(); - federationResult = await buildForFederation( + + federationResult = await rebuildForFederation( config, fedOptions, externals, - { - skipMappingsAndExposed: false, - skipShared: true, - signal, - }, + signal, ); if (signal?.aborted) { @@ -453,6 +441,7 @@ export async function* runBuilder( } } finally { rebuildQueue.dispose(); + await adapter.dispose(); if (isLocalDevelopment) { federationBuildNotifier.stopEventServer(); @@ -511,27 +500,5 @@ function transformIndexHtml( ); } -function addDebugInformation(fileName: string, rawBody: string): string { - if (fileName !== '/remoteEntry.json') { - return rawBody; - } - - const remoteEntry = JSON.parse(rawBody) as FederationInfo; - const shared = remoteEntry.shared; - - if (!shared) { - return rawBody; - } - - const sharedForVite = shared.map((s) => ({ - ...s, - packageName: `/@id/${s.packageName}`, - })); - - remoteEntry.shared = [...shared, ...sharedForVite]; - - return JSON.stringify(remoteEntry, null, 2); -} - // eslint-disable-next-line @typescript-eslint/no-explicit-any export default createBuilder(runBuilder) as any; diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index d40d7cb3..103157bc 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -1,6 +1,7 @@ import { AbortedError, BuildAdapter, + SetupOptions, logger, MappedPath, } from '@softarc/native-federation/build'; @@ -32,43 +33,81 @@ import * as path from 'path'; import { createSharedMappingsPlugin } from './shared-mappings-plugin'; import { PluginItem, transformAsync } from '@babel/core'; -import { - BuildKind, - BuildResult, - EntryPoint, -} from '@softarc/native-federation/build'; - -import { RebuildEvents, RebuildHubs } from './rebuild-events'; +import { BuildResult, EntryPoint } from '@softarc/native-federation/build'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin'; +interface CachedContext { + ctx: esbuild.BuildContext; + pluginDisposed: Promise; + outdir: string; + dev: boolean; + name: string; + isNodeModules: boolean; + sourceFileCache?: SourceFileCache; + entryPoints: EntryPoint[]; + workspaceRoot: string; +} + export function createAngularBuildAdapter( builderOptions: ApplicationBuilderOptions, context: BuilderContext, - rebuildRequested: RebuildEvents = new RebuildHubs(), ): BuildAdapter { - return async (options) => { + const contextCache = new Map(); + + const dispose = async (name?: string): Promise => { + if (name) { + if (!contextCache.has(name)) + throw new Error(`Could not dispose of non-existing build '${name}'`); + const entry = contextCache.get(name); + + await entry.ctx.dispose(); + await entry.pluginDisposed; + contextCache.delete(name); + return; + } + + // Dispose all if no specific build provided + + const disposals: Promise[] = []; + + for (const [, entry] of contextCache) { + disposals.push( + (async () => { + await entry.ctx.dispose(); + await entry.pluginDisposed; + })(), + ); + } + contextCache.clear(); + await Promise.all(disposals); + }; + + const setup = async (options: SetupOptions): Promise => { const { entryPoints, tsConfigPath, external, outdir, mappedPaths, - kind, - watch, + bundleName, + isNodeModules, dev, hash, platform, - cachePath, optimizedMappings, - signal, + cachePath, } = options; setNgServerMode(); - const files = await runEsbuild( + if (contextCache.has(bundleName)) { + return; + } + + const { ctx, pluginDisposed, sourceFileCache } = await createEsbuildContext( builderOptions, context, entryPoints, @@ -76,67 +115,111 @@ export function createAngularBuildAdapter( outdir, tsConfigPath, mappedPaths, - watch, - rebuildRequested, dev, - kind, + isNodeModules, hash, - undefined, - undefined, - undefined, platform, optimizedMappings, cachePath, - signal, ); - if (kind === 'shared-package') { - const scriptFiles = files.filter( - (f) => f.endsWith('.js') || f.endsWith('.mjs'), + contextCache.set(bundleName, { + ctx, + pluginDisposed, + outdir, + isNodeModules, + dev: !!dev, + name: bundleName, + sourceFileCache, + entryPoints, + workspaceRoot: context.workspaceRoot, + }); + }; + + const build = async ( + name: string, + signal?: AbortSignal, + ): Promise => { + const cached = contextCache.get(name); + if (!cached) { + throw new Error( + `No context found for build "${name}". Call setup() first.`, ); - for (const file of scriptFiles) { - link(file, dev); + } + + if (signal?.aborted) { + throw new AbortedError('[build] Aborted before rebuild'); + } + + // todo: tap into "modified files" to see what to invalidate + if (cached.sourceFileCache) { + for (const ep of cached.entryPoints) { + // const absolutePath = path.isAbsolute(ep.fileName) + // ? ep.fileName + // : path.join(cached.workspaceRoot, ep.fileName); + // cached.sourceFileCache.modifiedFiles.add(path.normalize(absolutePath)); } } - return files.map((fileName) => ({ fileName }) as BuildResult); + try { + const result = await cached.ctx.rebuild(); + const writtenFiles = writeResult(result, cached.outdir); + + if (cached.isNodeModules) { + const scriptFiles = writtenFiles.filter( + (f) => f.endsWith('.js') || f.endsWith('.mjs'), + ); + for (const file of scriptFiles) { + await link(file, cached.dev); + } + } + + return writtenFiles.map((fileName) => ({ fileName }) as BuildResult); + } catch (error) { + if (signal?.aborted && error?.message?.includes('canceled')) { + throw new AbortedError('[build] ESBuild rebuild was canceled.'); + } + throw error; + } }; - async function link(outfile: string, dev: boolean) { - const code = fs.readFileSync(outfile, 'utf-8'); + return { setup, build, dispose }; +} - try { - const linkerEsm = await loadEsmModule<{ default: PluginItem }>( - '@angular/compiler-cli/linker/babel', - ); +async function link(outfile: string, dev: boolean) { + const code = fs.readFileSync(outfile, 'utf-8'); - const linker = linkerEsm.default; + try { + const linkerEsm = await loadEsmModule<{ default: PluginItem }>( + '@angular/compiler-cli/linker/babel', + ); - const result = await transformAsync(code, { - filename: outfile, - compact: !dev, - configFile: false, - babelrc: false, - minified: !dev, - browserslistConfigFile: false, - plugins: [linker], - }); + const linker = linkerEsm.default; - fs.writeFileSync(outfile, result.code, 'utf-8'); - } catch (e) { - logger.error('error linking'); + const result = await transformAsync(code, { + filename: outfile, + compact: !dev, + configFile: false, + babelrc: false, + minified: !dev, + browserslistConfigFile: false, + plugins: [linker], + }); - if (fs.existsSync(`${outfile}.error`)) { - fs.unlinkSync(`${outfile}.error`); - } - fs.renameSync(outfile, `${outfile}.error`); + fs.writeFileSync(outfile, result.code, 'utf-8'); + } catch (e) { + logger.error('error linking'); - throw e; + if (fs.existsSync(`${outfile}.error`)) { + fs.unlinkSync(`${outfile}.error`); } + fs.renameSync(outfile, `${outfile}.error`); + + throw e; } } -async function runEsbuild( +async function createEsbuildContext( builderOptions: ApplicationBuilderOptions, context: BuilderContext, entryPoints: EntryPoint[], @@ -144,23 +227,17 @@ async function runEsbuild( outdir: string, tsConfigPath: string, mappedPaths: MappedPath[], - watch?: boolean, - rebuildRequested: RebuildEvents = new RebuildHubs(), dev?: boolean, - kind?: BuildKind, + isNodeModules?: boolean, hash = false, - plugins: esbuild.Plugin[] | null = null, - absWorkingDir: string | undefined = undefined, - logLevel: esbuild.LogLevel = 'warning', platform?: 'browser' | 'node', optimizedMappings?: boolean, cachePath?: string, - signal?: AbortSignal, -) { - if (signal?.aborted) { - throw new AbortedError('[angular-esbuild-adapter] Before building'); - } - +): Promise<{ + ctx: esbuild.BuildContext; + pluginDisposed: Promise; + sourceFileCache?: SourceFileCache; +}> { const workspaceRoot = context.workspaceRoot; const projectMetadata = await context.getProjectMetadata( @@ -211,6 +288,10 @@ async function runEsbuild( ); } + const sourceFileCache = cachePath + ? new SourceFileCache(cachePath) + : undefined; + const pluginOptions = createCompilerPluginOptions( { workspaceRoot, @@ -227,9 +308,10 @@ async function runEsbuild( jit: false, tailwindConfiguration, postcssConfiguration, + incremental: !isNodeModules, // Enable incremental mode for rebuild support } as any, target, - cachePath ? new SourceFileCache(cachePath) : undefined, + sourceFileCache, ); const commonjsPluginModule = await import('@chialab/esbuild-plugin-commonjs'); @@ -250,9 +332,8 @@ async function runEsbuild( outdir, entryNames: hash ? '[name]-[hash]' : '[name]', write: false, - absWorkingDir, external, - logLevel, + logLevel: 'warning', bundle: true, sourcemap: sourcemapOptions.scripts, minify: !dev, @@ -264,8 +345,8 @@ async function runEsbuild( platform: platform ?? 'browser', format: 'esm', target: target, - logLimit: kind === 'shared-package' ? 1 : 0, - plugins: (plugins as any) || [ + logLimit: isNodeModules ? 1 : 0, + plugins: [ compilerPlugin, ...(mappedPaths && mappedPaths.length > 0 ? [createSharedMappingsPlugin(mappedPaths)] @@ -282,44 +363,7 @@ async function runEsbuild( const ctx = await esbuild.context(config); - try { - const abortHandler = async () => { - await ctx.cancel(); - await ctx.dispose(); - await pluginDisposed; - }; - - if (signal) { - signal.addEventListener('abort', abortHandler, { once: true }); - } - - const result = await ctx.rebuild(); - - const writtenFiles = writeResult(result, outdir); - - if (watch) { - registerForRebuilds( - kind, - rebuildRequested, - ctx, - entryPoints, - outdir, - hash, - ); - } else { - if (signal) signal.removeEventListener('abort', abortHandler); - await ctx.dispose(); - await pluginDisposed; - } - return writtenFiles; - } catch (error) { - // 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.'); - } - throw error; - } + return { ctx, pluginDisposed, sourceFileCache }; } async function getTailwindConfig( @@ -410,28 +454,13 @@ function writeResult( for (const outFile of result.outputFiles) { const fileName = path.basename(outFile.path); const filePath = path.join(outdir, fileName); + fs.writeFileSync(filePath, outFile.text); writtenFiles.push(filePath); } return writtenFiles; } -function registerForRebuilds( - kind: BuildKind, - rebuildRequested: RebuildEvents, - ctx: esbuild.BuildContext, - entryPoints: EntryPoint[], - outdir: string, - hash: boolean, -) { - if (kind !== 'shared-package') { - rebuildRequested.rebuild.register(async () => { - const result = await ctx.rebuild(); - writeResult(result, outdir); - }); - } -} - export function loadEsmModule(modulePath: string | URL): Promise { return new Function('modulePath', `return import(modulePath);`)( modulePath, diff --git a/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts index 538d0d6e..d71f5412 100644 --- a/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts +++ b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts @@ -10,7 +10,7 @@ export function createAwaitableCompilerPlugin( pluginOptions: CreateCompilerPluginParams[0], styleOptions: CreateCompilerPluginParams[1], ): [esbuild.Plugin, Promise] { - if (!!plugin) { + if (!plugin) { const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); let resolveDispose: () => void; @@ -25,9 +25,9 @@ export function createAwaitableCompilerPlugin( const wrappedBuild = new Proxy(build, { get(target, prop) { if (prop === 'onDispose') { - return (callback: () => void | Promise) => { + return (originalCallback: () => void | Promise) => { return target.onDispose(() => { - callback(); + originalCallback(); resolveDispose(); }); }; diff --git a/libs/native-federation/src/utils/event-sorce.ts b/libs/native-federation/src/utils/event-sorce.ts deleted file mode 100644 index 78641171..00000000 --- a/libs/native-federation/src/utils/event-sorce.ts +++ /dev/null @@ -1,18 +0,0 @@ -export type EventHandler = () => Promise; - -export interface EventSource { - register(handler: EventHandler): void; -} - -export class EventHub implements EventSource { - private handlers: EventHandler[] = []; - - register(handler: EventHandler): void { - this.handlers.push(handler); - } - - async emit(): Promise { - const promises = this.handlers.map((h) => h()); - await Promise.all(promises); - } -} diff --git a/libs/native-federation/src/utils/rebuild-events.ts b/libs/native-federation/src/utils/rebuild-events.ts deleted file mode 100644 index 9c5d267e..00000000 --- a/libs/native-federation/src/utils/rebuild-events.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { EventHub, EventSource } from './event-sorce'; - -export interface RebuildEvents { - readonly rebuild: EventSource; -} - -export class RebuildHubs implements RebuildEvents { - readonly rebuild = new EventHub(); -} From be80e4110a817ef26bc0bc1230a3eedaa4648b9c Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 18 Feb 2026 11:32:20 +0100 Subject: [PATCH 4/5] fix: Pass on updated files --- .../src/lib/core/build-adapter.ts | 8 +++++++- .../src/lib/core/build-for-federation.ts | 5 +++-- .../lib/core/bundle-exposed-and-mappings.ts | 9 ++++++--- .../src/builders/build/builder.ts | 2 ++ .../src/utils/angular-esbuild-adapter.ts | 20 +++++++++---------- 5 files changed, 28 insertions(+), 16 deletions(-) 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 29d69f6c..c17eb763 100644 --- a/libs/native-federation-core/src/lib/core/build-adapter.ts +++ b/libs/native-federation-core/src/lib/core/build-adapter.ts @@ -36,7 +36,13 @@ export interface BuildResult { export interface BuildAdapter { setup(options: SetupOptions): Promise; - build(name: string, signal?: AbortSignal): Promise; + build( + name: string, + opts?: { + files?: string[]; + signal?: AbortSignal; + }, + ): Promise; dispose(name?: string): Promise; } 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 33011537..2cc665a8 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 @@ -41,7 +41,7 @@ export async function buildForFederation( fedOptions, externals, pathToCache, - true, + undefined, signal, ); logger.measure( @@ -183,6 +183,7 @@ export async function rebuildForFederation( config: NormalizedFederationConfig, fedOptions: FederationOptions, externals: string[], + modifiedFiles: string[], signal?: AbortSignal, ): Promise { const cacheProjectFolder = resolveProjectName(config); @@ -197,7 +198,7 @@ export async function rebuildForFederation( fedOptions, externals, pathToCache, - false, + modifiedFiles, signal, ); logger.measure( 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 f046c746..c3a46aa5 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 @@ -23,7 +23,7 @@ export async function bundleExposedAndMappings( fedOptions: FederationOptions, externals: string[], cachePath: string, - setup: boolean, + modifiedFiles?: string[], signal?: AbortSignal, ): Promise { if (signal?.aborted) { @@ -52,7 +52,7 @@ export async function bundleExposedAndMappings( let result; try { - if (setup) { + if (!modifiedFiles) { await getBuildAdapter().setup({ entryPoints, outdir: fedOptions.outputPath, @@ -68,7 +68,10 @@ export async function bundleExposedAndMappings( }); } - result = await getBuildAdapter().build('mapping-or-exposed', signal); + result = await getBuildAdapter().build('mapping-or-exposed', { + signal, + files: modifiedFiles, + }); if (signal?.aborted) { throw new AbortedError( diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 755be49a..cf2c8a05 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -5,6 +5,7 @@ import * as path from 'path'; import { ApplicationBuilderOptions, buildApplication } from '@angular/build'; import { buildApplicationInternal, + ResultKind, serveWithVite, } from '@angular/build/private'; @@ -392,6 +393,7 @@ export async function* runBuilder( config, fedOptions, externals, + [], // an string array of the files from _buildApplication signal, ); diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 103157bc..c1de40a2 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -138,7 +138,10 @@ export function createAngularBuildAdapter( const build = async ( name: string, - signal?: AbortSignal, + opts: { + files?: string[]; + signal?: AbortSignal; + } = {}, ): Promise => { const cached = contextCache.get(name); if (!cached) { @@ -147,18 +150,15 @@ export function createAngularBuildAdapter( ); } - if (signal?.aborted) { + if (opts?.signal?.aborted) { throw new AbortedError('[build] Aborted before rebuild'); } // todo: tap into "modified files" to see what to invalidate - if (cached.sourceFileCache) { - for (const ep of cached.entryPoints) { - // const absolutePath = path.isAbsolute(ep.fileName) - // ? ep.fileName - // : path.join(cached.workspaceRoot, ep.fileName); - // cached.sourceFileCache.modifiedFiles.add(path.normalize(absolutePath)); - } + if (cached.sourceFileCache && opts.files) { + opts.files.forEach((f) => { + cached.sourceFileCache.modifiedFiles.add(f); + }); } try { @@ -176,7 +176,7 @@ export function createAngularBuildAdapter( return writtenFiles.map((fileName) => ({ fileName }) as BuildResult); } catch (error) { - if (signal?.aborted && error?.message?.includes('canceled')) { + if (opts?.signal?.aborted && error?.message?.includes('canceled')) { throw new AbortedError('[build] ESBuild rebuild was canceled.'); } throw error; From f3b812e860347a590ef964f4e60042780726b862 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Fri, 20 Feb 2026 18:48:52 +0100 Subject: [PATCH 5/5] fix: Added shared cache between builder and plugin --- .../src/builders/build/builder.ts | 2 + .../src/utils/angular-esbuild-adapter.ts | 43 +++++++++++-------- .../src/utils/code-bundle-cache.ts | 12 ++++++ 3 files changed, 38 insertions(+), 19 deletions(-) create mode 100644 libs/native-federation/src/utils/code-bundle-cache.ts diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index cf2c8a05..0271cec3 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -43,6 +43,7 @@ import { updateScriptTags } from '../../utils/updateIndexHtml'; import { federationBuildNotifier } from './federation-build-notifier'; import { NfBuilderSchema } from './schema'; import { Schema as DevServerSchema } from '@angular-devkit/build-angular/src/builders/dev-server/schema'; +import { getCodeBundleCache } from '../../utils/code-bundle-cache'; const originalWrite = process.stderr.write.bind(process.stderr); @@ -76,6 +77,7 @@ function _buildApplication(options, context, pluginsOrExtensions) { } else { extensions = pluginsOrExtensions; } + options.codeBundleCache = getCodeBundleCache(); return buildApplicationInternal(options, context, extensions); } diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index c1de40a2..7f40c081 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -14,7 +14,6 @@ import { generateSearchDirectories, findTailwindConfiguration, loadPostcssConfiguration, - SourceFileCache, } from '@angular/build/private'; import { createCompilerPluginOptions } from './create-compiler-options'; @@ -38,6 +37,7 @@ import { BuildResult, EntryPoint } from '@softarc/native-federation/build'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin'; +import { getCodeBundleCache, setCodeBundleCache } from './code-bundle-cache'; interface CachedContext { ctx: esbuild.BuildContext; @@ -46,9 +46,9 @@ interface CachedContext { dev: boolean; name: string; isNodeModules: boolean; - sourceFileCache?: SourceFileCache; entryPoints: EntryPoint[]; workspaceRoot: string; + outFiles: string[]; } export function createAngularBuildAdapter( @@ -103,11 +103,13 @@ export function createAngularBuildAdapter( setNgServerMode(); + if (!!cachePath) setCodeBundleCache(cachePath); + if (contextCache.has(bundleName)) { return; } - const { ctx, pluginDisposed, sourceFileCache } = await createEsbuildContext( + const { ctx, pluginDisposed } = await createEsbuildContext( builderOptions, context, entryPoints, @@ -120,7 +122,6 @@ export function createAngularBuildAdapter( hash, platform, optimizedMappings, - cachePath, ); contextCache.set(bundleName, { @@ -130,9 +131,9 @@ export function createAngularBuildAdapter( isNodeModules, dev: !!dev, name: bundleName, - sourceFileCache, entryPoints, workspaceRoot: context.workspaceRoot, + outFiles: [], }); }; @@ -155,14 +156,23 @@ export function createAngularBuildAdapter( } // todo: tap into "modified files" to see what to invalidate - if (cached.sourceFileCache && opts.files) { - opts.files.forEach((f) => { - cached.sourceFileCache.modifiedFiles.add(f); - }); - } + // if (cached.sourceFileCache && opts.files) { + // opts.files.forEach((f) => { + // cached.sourceFileCache.modifiedFiles.add(f); + // }); + // } try { + cached.outFiles.forEach((e) => { + getCodeBundleCache().invalidate(e); + }); + const result = await cached.ctx.rebuild(); + + cached.outFiles = []; + result.outputFiles.forEach((e) => { + cached.outFiles.push(e.path); + }); const writtenFiles = writeResult(result, cached.outdir); if (cached.isNodeModules) { @@ -232,11 +242,9 @@ async function createEsbuildContext( hash = false, platform?: 'browser' | 'node', optimizedMappings?: boolean, - cachePath?: string, ): Promise<{ ctx: esbuild.BuildContext; pluginDisposed: Promise; - sourceFileCache?: SourceFileCache; }> { const workspaceRoot = context.workspaceRoot; @@ -288,16 +296,13 @@ async function createEsbuildContext( ); } - const sourceFileCache = cachePath - ? new SourceFileCache(cachePath) - : undefined; - const pluginOptions = createCompilerPluginOptions( { workspaceRoot, optimizationOptions, sourcemapOptions, tsconfig: tsConfigPath, + outputNames, fileReplacements, externalDependencies: external, @@ -308,10 +313,10 @@ async function createEsbuildContext( jit: false, tailwindConfiguration, postcssConfiguration, - incremental: !isNodeModules, // Enable incremental mode for rebuild support + incremental: !isNodeModules, } as any, target, - sourceFileCache, + getCodeBundleCache(), ); const commonjsPluginModule = await import('@chialab/esbuild-plugin-commonjs'); @@ -363,7 +368,7 @@ async function createEsbuildContext( const ctx = await esbuild.context(config); - return { ctx, pluginDisposed, sourceFileCache }; + return { ctx, pluginDisposed }; } async function getTailwindConfig( diff --git a/libs/native-federation/src/utils/code-bundle-cache.ts b/libs/native-federation/src/utils/code-bundle-cache.ts new file mode 100644 index 00000000..4269b21e --- /dev/null +++ b/libs/native-federation/src/utils/code-bundle-cache.ts @@ -0,0 +1,12 @@ +import { SourceFileCache } from '@angular/build/private'; + +let _codeBundleCache: SourceFileCache | undefined = undefined; + +export function setCodeBundleCache(path: string) { + if (!!_codeBundleCache) return; + _codeBundleCache = new SourceFileCache(path); +} + +export function getCodeBundleCache() { + return _codeBundleCache; +}