From a247a7a0e8be327b0ffeeba4b25e0113607f2b2e Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Mon, 1 Dec 2025 08:43:53 +0100 Subject: [PATCH 1/8] chore: Switch to executeDevServerBuilder --- .../src/builders/build/builder.ts | 71 ++++++++----------- .../src/utils/angular-esbuild-adapter.ts | 65 +++++++++++++++-- 2 files changed, 91 insertions(+), 45 deletions(-) diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 2b608e31..f7fcbaca 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -1,12 +1,13 @@ +import * as path from 'path'; import * as fs from 'fs'; import * as mrmime from 'mrmime'; -import * as path from 'path'; -import { ApplicationBuilderOptions, buildApplication } from '@angular/build'; import { - buildApplicationInternal, - serveWithVite, -} from '@angular/build/private'; + ApplicationBuilderOptions, + buildApplication, + executeDevServerBuilder, + DevServerBuilderOptions, +} from '@angular/build'; import { BuilderContext, @@ -15,8 +16,6 @@ import { targetFromTargetString, } from '@angular-devkit/architect'; -import { normalizeOptions } from '@angular-devkit/build-angular/src/builders/dev-server/options'; - import { buildForFederation, FederationOptions, @@ -49,7 +48,6 @@ import { createSharedMappingsPlugin } from '../../utils/shared-mappings-plugin'; 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'; const originalWrite = process.stderr.write.bind(process.stderr); @@ -74,18 +72,6 @@ process.stderr.write = function ( return originalWrite(chunk, encodingOrCallback as BufferEncoding, callback); }; -function _buildApplication(options, context, pluginsOrExtensions) { - let extensions; - if (pluginsOrExtensions && Array.isArray(pluginsOrExtensions)) { - extensions = { - codePlugins: pluginsOrExtensions, - }; - } else { - extensions = pluginsOrExtensions; - } - return buildApplicationInternal(options, context, extensions); -} - export async function* runBuilder( nfOptions: NfBuilderSchema, context: BuilderContext, @@ -137,17 +123,30 @@ export async function* runBuilder( builder, )) as JsonObject & ApplicationBuilderOptions; - let serverOptions = null; + let serverOptions: DevServerBuilderOptions | null = null; const write = true; const watch = nfOptions.watch; if (options['buildTarget']) { - serverOptions = await normalizeOptions( - context, - context.target.project, - options as unknown as DevServerSchema, - ); + serverOptions = { + buildTarget: options['buildTarget'] as string, + port: nfOptions.port || options['port'], + host: options['host'] as string, + watch: watch, + verbose: options.verbose, + ...(options['ssl'] && { ssl: options['ssl'] as boolean }), + ...(options['sslCert'] && { sslCert: options['sslCert'] as string }), + ...(options['sslKey'] && { sslKey: options['sslKey'] as string }), + ...(options['proxyConfig'] && { + proxyConfig: options['proxyConfig'] as string, + }), + ...(options['open'] && { open: options['open'] as boolean }), + ...(options['liveReload'] !== undefined && { + liveReload: options['liveReload'] as boolean, + }), + ...(options['hmr'] !== undefined && { hmr: options['hmr'] as boolean }), + } as DevServerBuilderOptions; target = targetFromTargetString(options['buildTarget'] as string); targetOptions = (await context.getTargetOptions( @@ -355,22 +354,14 @@ export async function* runBuilder( options.deleteOutputPath = false; - const appBuilderName = '@angular/build:application'; - const builderRun = runServer - ? serveWithVite( - serverOptions, - appBuilderName, - _buildApplication, - context, - nfOptions.skipHtmlTransform + ? executeDevServerBuilder(serverOptions, context, { + buildPlugins: plugins as any, + middleware, + ...(nfOptions.skipHtmlTransform ? {} - : { indexHtml: transformIndexHtml(nfOptions) }, - { - buildPlugins: plugins as any, - middleware, - }, - ) + : { indexHtmlTransformer: transformIndexHtml(nfOptions) }), + }) : buildApplication(options, context, { codePlugins: plugins as any, indexHtmlTransformer: transformIndexHtml(nfOptions), diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index ffed4544..bcfbca82 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -18,11 +18,6 @@ import { import { createCompilerPluginOptions } from './create-compiler-options'; import { BuilderContext } from '@angular-devkit/architect'; - -import { - normalizeOptimization, - normalizeSourceMaps, -} from '@angular-devkit/build-angular/src/utils'; import { createRequire } from 'node:module'; import { ApplicationBuilderOptions } from '@angular/build'; @@ -486,6 +481,66 @@ export function loadEsmModule(modulePath: string | URL): Promise { ) as Promise; } +function normalizeOptimization( + optimization: + | boolean + | { + scripts?: boolean; + styles?: boolean | { minify?: boolean; inlineCritical?: boolean }; + fonts?: boolean | { inline?: boolean }; + } = false, +) { + if (typeof optimization === 'boolean') { + return { + scripts: optimization, + styles: { minify: optimization, inlineCritical: optimization }, + fonts: { inline: optimization }, + }; + } + + return { + scripts: optimization.scripts ?? false, + styles: + typeof optimization.styles === 'boolean' + ? { minify: optimization.styles, inlineCritical: optimization.styles } + : { + minify: optimization.styles?.minify ?? false, + inlineCritical: optimization.styles?.inlineCritical ?? false, + }, + fonts: + typeof optimization.fonts === 'boolean' + ? { inline: optimization.fonts } + : { inline: optimization.fonts?.inline ?? false }, + }; +} + +function normalizeSourceMaps( + sourceMap: + | boolean + | { + scripts?: boolean; + styles?: boolean; + vendor?: boolean; + hidden?: boolean; + } = false, +) { + if (typeof sourceMap === 'boolean') { + return { + scripts: sourceMap, + styles: sourceMap, + vendor: sourceMap, + hidden: false, + }; + } + + return { + scripts: sourceMap.scripts ?? false, + styles: sourceMap.styles ?? false, + vendor: sourceMap.vendor ?? false, + hidden: sourceMap.hidden ?? false, + }; +} + // // Usually, ngServerMode is set during bundling. However, we need to infer this // value at runtime as we are using the same shared bundle for @angular/core From 62115082ea255919c39b7d9e5dea5b47469f661b Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Mon, 8 Dec 2025 09:25:34 +0100 Subject: [PATCH 2/8] fix: Added 5s initial wait --- libs/native-federation/src/builders/build/builder.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index f7fcbaca..2b5e51de 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -354,6 +354,10 @@ export async function* runBuilder( options.deleteOutputPath = false; + await new Promise((resolve) => { + setTimeout(resolve, 5000); + }); + const builderRun = runServer ? executeDevServerBuilder(serverOptions, context, { buildPlugins: plugins as any, From 33fbaeb74d4189121434fbe4b0d3ce20ecb50a23 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Mon, 8 Dec 2025 09:50:51 +0100 Subject: [PATCH 3/8] fix: Initial delay configurable --- libs/native-federation/src/builders/build/builder.ts | 2 +- libs/native-federation/src/builders/build/schema.d.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 2b5e51de..94c01a16 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -355,7 +355,7 @@ export async function* runBuilder( options.deleteOutputPath = false; await new Promise((resolve) => { - setTimeout(resolve, 5000); + setTimeout(resolve, nfOptions.initialDelay ?? 0); }); const builderRun = runServer diff --git a/libs/native-federation/src/builders/build/schema.d.ts b/libs/native-federation/src/builders/build/schema.d.ts index 8250f5a5..15f76076 100644 --- a/libs/native-federation/src/builders/build/schema.d.ts +++ b/libs/native-federation/src/builders/build/schema.d.ts @@ -8,6 +8,7 @@ export interface NfBuilderSchema extends JsonObject { port: number; open: boolean; rebuildDelay: number; + initialDelay?: number; buildNotifications?: BuildNotificationOptions; shell: string; watch: boolean; From 714899ff5b15fc7d494a9d768e5ee35f9ab9db4f Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Mon, 8 Dec 2025 07:25:50 +0100 Subject: [PATCH 4/8] fix: test if issue is resolved when awaitable --- .../src/utils/angular-esbuild-adapter.ts | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index bcfbca82..bf160967 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -8,7 +8,7 @@ import { import * as esbuild from 'esbuild'; import { - createCompilerPlugin, + createCompilerPlugin as createCompilerPlugin, transformSupportedBrowsersToTargets, getSupportedBrowsers, generateSearchDirectories, @@ -38,6 +38,46 @@ import { RebuildEvents, RebuildHubs } from './rebuild-events'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; +function createAwaitableCompilerPlugin( + pluginOptions: any, + styleOptions: any, +): [esbuild.Plugin, Promise] { + const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); + + let resolveDispose: () => void; + const onDisposePromise = new Promise((resolve) => { + resolveDispose = resolve; + }); + + const wrappedPlugin: esbuild.Plugin = { + ...originalPlugin, + setup(build: esbuild.PluginBuild) { + let onDisposeCallback: (() => void | Promise) | undefined; + + // Wrap the build object to intercept onDispose + const wrappedBuild = new Proxy(build, { + get(target, prop) { + if (prop === 'onDispose') { + return (callback: () => void | Promise) => { + onDisposeCallback = callback; + return target.onDispose(async () => { + await callback(); + resolveDispose(); + }); + }; + } + return target[prop as keyof esbuild.PluginBuild]; + }, + }); + + // Call original setup with wrapped build + return originalPlugin.setup(wrappedBuild); + }, + }; + + return [wrappedPlugin, onDisposePromise]; +} + export type MemResultHandler = ( outfiles: esbuild.OutputFile[], outdir?: string, @@ -265,6 +305,11 @@ async function runEsbuild( pluginOptions.styleOptions.externalDependencies = []; + const [compilerPlugin, compilerPluginDispose] = createAwaitableCompilerPlugin( + pluginOptions.pluginOptions, + pluginOptions.styleOptions, + ); + const config: esbuild.BuildOptions = { entryPoints: entryPoints.map((ep) => ({ in: ep.fileName, @@ -289,10 +334,7 @@ async function runEsbuild( target: target, logLimit: kind === 'shared-package' ? 1 : 0, plugins: (plugins as any) || [ - createCompilerPlugin( - pluginOptions.pluginOptions, - pluginOptions.styleOptions, - ), + compilerPlugin, ...(mappedPaths && mappedPaths.length > 0 ? [createSharedMappingsPlugin(mappedPaths)] : []), @@ -311,6 +353,7 @@ async function runEsbuild( try { const abortHandler = async () => { await ctx.cancel(); + await compilerPluginDispose; await ctx.dispose(); }; @@ -336,6 +379,8 @@ async function runEsbuild( ); } else { if (signal) signal.removeEventListener('abort', abortHandler); + + await compilerPluginDispose; await ctx.dispose(); } return writtenFiles; From 2f4adaa55bd87790f2c271aa01269e2126ffe8b3 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Mon, 8 Dec 2025 13:10:41 +0100 Subject: [PATCH 5/8] Added promise wrapper --- libs/native-federation/src/utils/angular-esbuild-adapter.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index bf160967..88530d41 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -353,8 +353,8 @@ async function runEsbuild( try { const abortHandler = async () => { await ctx.cancel(); - await compilerPluginDispose; await ctx.dispose(); + await compilerPluginDispose; }; if (signal) { @@ -379,9 +379,8 @@ async function runEsbuild( ); } else { if (signal) signal.removeEventListener('abort', abortHandler); - - await compilerPluginDispose; await ctx.dispose(); + await compilerPluginDispose; } return writtenFiles; } catch (error) { From 93f7be8532c704d013c532f94c38bca3517b1ffa Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 9 Dec 2025 21:16:02 +0100 Subject: [PATCH 6/8] fix: Refactored code --- .../src/builders/build/builder.ts | 16 +---- .../src/utils/angular-esbuild-adapter.ts | 63 +++++++++++++------ .../src/utils/federation-build-worker.ts | 13 ++++ 3 files changed, 58 insertions(+), 34 deletions(-) create mode 100644 libs/native-federation/src/utils/federation-build-worker.ts diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 94c01a16..4c706e32 100644 --- a/libs/native-federation/src/builders/build/builder.ts +++ b/libs/native-federation/src/builders/build/builder.ts @@ -130,22 +130,10 @@ export async function* runBuilder( if (options['buildTarget']) { serverOptions = { - buildTarget: options['buildTarget'] as string, + ...options, + buildTarget: options['buildTarget'], port: nfOptions.port || options['port'], - host: options['host'] as string, watch: watch, - verbose: options.verbose, - ...(options['ssl'] && { ssl: options['ssl'] as boolean }), - ...(options['sslCert'] && { sslCert: options['sslCert'] as string }), - ...(options['sslKey'] && { sslKey: options['sslKey'] as string }), - ...(options['proxyConfig'] && { - proxyConfig: options['proxyConfig'] as string, - }), - ...(options['open'] && { open: options['open'] as boolean }), - ...(options['liveReload'] !== undefined && { - liveReload: options['liveReload'] as boolean, - }), - ...(options['hmr'] !== undefined && { hmr: options['hmr'] as boolean }), } as DevServerBuilderOptions; target = targetFromTargetString(options['buildTarget'] as string); diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 88530d41..4f2eab77 100644 --- a/libs/native-federation/src/utils/angular-esbuild-adapter.ts +++ b/libs/native-federation/src/utils/angular-esbuild-adapter.ts @@ -8,7 +8,7 @@ import { import * as esbuild from 'esbuild'; import { - createCompilerPlugin as createCompilerPlugin, + createCompilerPlugin, transformSupportedBrowsersToTargets, getSupportedBrowsers, generateSearchDirectories, @@ -525,15 +525,23 @@ export function loadEsmModule(modulePath: string | URL): Promise { ) as Promise; } +type OptimizationInput = + | boolean + | { + scripts?: boolean; + styles?: boolean | { minify?: boolean; inlineCritical?: boolean }; + fonts?: boolean | { inline?: boolean }; + }; + +interface NormalizedOptimization { + scripts: boolean; + styles: { minify: boolean; inlineCritical: boolean }; + fonts: { inline: boolean }; +} + function normalizeOptimization( - optimization: - | boolean - | { - scripts?: boolean; - styles?: boolean | { minify?: boolean; inlineCritical?: boolean }; - fonts?: boolean | { inline?: boolean }; - } = false, -) { + optimization: OptimizationInput = false, +): NormalizedOptimization { if (typeof optimization === 'boolean') { return { scripts: optimization, @@ -542,19 +550,34 @@ function normalizeOptimization( }; } + const normalizeStyles = ( + styles: + | boolean + | { minify?: boolean; inlineCritical?: boolean } + | undefined, + ) => { + if (typeof styles === 'boolean') { + return { minify: styles, inlineCritical: styles }; + } + return { + minify: styles?.minify ?? false, + inlineCritical: styles?.inlineCritical ?? false, + }; + }; + + const normalizeFonts = ( + fonts: boolean | { inline?: boolean } | undefined, + ) => { + if (typeof fonts === 'boolean') { + return { inline: fonts }; + } + return { inline: fonts?.inline ?? false }; + }; + return { scripts: optimization.scripts ?? false, - styles: - typeof optimization.styles === 'boolean' - ? { minify: optimization.styles, inlineCritical: optimization.styles } - : { - minify: optimization.styles?.minify ?? false, - inlineCritical: optimization.styles?.inlineCritical ?? false, - }, - fonts: - typeof optimization.fonts === 'boolean' - ? { inline: optimization.fonts } - : { inline: optimization.fonts?.inline ?? false }, + styles: normalizeStyles(optimization.styles), + fonts: normalizeFonts(optimization.fonts), }; } diff --git a/libs/native-federation/src/utils/federation-build-worker.ts b/libs/native-federation/src/utils/federation-build-worker.ts new file mode 100644 index 00000000..d25bd818 --- /dev/null +++ b/libs/native-federation/src/utils/federation-build-worker.ts @@ -0,0 +1,13 @@ +import { parentPort, workerData } from 'worker_threads'; +import { runEsbuild } from './angular-esbuild-adapter'; + +async function run() { + try { + const result = await runEsbuild(workerData.options); + parentPort?.postMessage({ success: true, result }); + } catch (error) { + parentPort?.postMessage({ success: false, error: error.message }); + } +} + +run(); From ca12b2f1a75a816f292b66ee1280f45bc2db8e86 Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Tue, 9 Dec 2025 21:32:42 +0100 Subject: [PATCH 7/8] fix: removed redundant file --- .../src/utils/federation-build-worker.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 libs/native-federation/src/utils/federation-build-worker.ts diff --git a/libs/native-federation/src/utils/federation-build-worker.ts b/libs/native-federation/src/utils/federation-build-worker.ts deleted file mode 100644 index d25bd818..00000000 --- a/libs/native-federation/src/utils/federation-build-worker.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { parentPort, workerData } from 'worker_threads'; -import { runEsbuild } from './angular-esbuild-adapter'; - -async function run() { - try { - const result = await runEsbuild(workerData.options); - parentPort?.postMessage({ success: true, result }); - } catch (error) { - parentPort?.postMessage({ success: false, error: error.message }); - } -} - -run(); From 541167a52cfd29224afdf5f53a49c3f58532a9bb Mon Sep 17 00:00:00 2001 From: Aukevanoost Date: Wed, 10 Dec 2025 18:35:49 +0100 Subject: [PATCH 8/8] fix: added awaitable compiler plugin --- .../src/utils/angular-esbuild-adapter.ts | 42 +------------------ .../utils/create-awaitable-compiler-plugin.ts | 42 +++++++++++++++++++ 2 files changed, 43 insertions(+), 41 deletions(-) create mode 100644 libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index 4f2eab77..2fa5f015 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, @@ -37,46 +36,7 @@ import { RebuildEvents, RebuildHubs } from './rebuild-events'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; - -function createAwaitableCompilerPlugin( - pluginOptions: any, - styleOptions: any, -): [esbuild.Plugin, Promise] { - const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); - - let resolveDispose: () => void; - const onDisposePromise = new Promise((resolve) => { - resolveDispose = resolve; - }); - - const wrappedPlugin: esbuild.Plugin = { - ...originalPlugin, - setup(build: esbuild.PluginBuild) { - let onDisposeCallback: (() => void | Promise) | undefined; - - // Wrap the build object to intercept onDispose - const wrappedBuild = new Proxy(build, { - get(target, prop) { - if (prop === 'onDispose') { - return (callback: () => void | Promise) => { - onDisposeCallback = callback; - return target.onDispose(async () => { - await callback(); - resolveDispose(); - }); - }; - } - return target[prop as keyof esbuild.PluginBuild]; - }, - }); - - // Call original setup with wrapped build - return originalPlugin.setup(wrappedBuild); - }, - }; - - return [wrappedPlugin, onDisposePromise]; -} +import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin'; export type MemResultHandler = ( outfiles: esbuild.OutputFile[], diff --git a/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts new file mode 100644 index 00000000..8baf87be --- /dev/null +++ b/libs/native-federation/src/utils/create-awaitable-compiler-plugin.ts @@ -0,0 +1,42 @@ +import * as esbuild from 'esbuild'; +import { createCompilerPlugin } from '@angular/build/private'; + +export function createAwaitableCompilerPlugin( + pluginOptions: any, + styleOptions: any, +): [esbuild.Plugin, Promise] { + const originalPlugin = createCompilerPlugin(pluginOptions, styleOptions); + + let resolveDispose: () => void; + const onDisposePromise = new Promise((resolve) => { + resolveDispose = resolve; + }); + + const wrappedPlugin: esbuild.Plugin = { + ...originalPlugin, + setup(build: esbuild.PluginBuild) { + let onDisposeCallback: (() => void | Promise) | undefined; + + // Wrap the build object to intercept onDispose + const wrappedBuild = new Proxy(build, { + get(target, prop) { + if (prop === 'onDispose') { + return (callback: () => void | Promise) => { + onDisposeCallback = callback; + return target.onDispose(async () => { + await callback(); + resolveDispose(); + }); + }; + } + return target[prop as keyof esbuild.PluginBuild]; + }, + }); + + // Call original setup with wrapped build + return originalPlugin.setup(wrappedBuild); + }, + }; + + return [wrappedPlugin, onDisposePromise]; +}