diff --git a/libs/native-federation/src/builders/build/builder.ts b/libs/native-federation/src/builders/build/builder.ts index 2b608e31..4c706e32 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,18 @@ 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 = { + ...options, + buildTarget: options['buildTarget'], + port: nfOptions.port || options['port'], + watch: watch, + } as DevServerBuilderOptions; target = targetFromTargetString(options['buildTarget'] as string); targetOptions = (await context.getTargetOptions( @@ -355,22 +342,18 @@ export async function* runBuilder( options.deleteOutputPath = false; - const appBuilderName = '@angular/build:application'; + await new Promise((resolve) => { + setTimeout(resolve, nfOptions.initialDelay ?? 0); + }); 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/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; diff --git a/libs/native-federation/src/utils/angular-esbuild-adapter.ts b/libs/native-federation/src/utils/angular-esbuild-adapter.ts index ffed4544..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, @@ -18,11 +17,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'; @@ -42,6 +36,7 @@ import { RebuildEvents, RebuildHubs } from './rebuild-events'; import JSON5 from 'json5'; import { isDeepStrictEqual } from 'node:util'; +import { createAwaitableCompilerPlugin } from './create-awaitable-compiler-plugin'; export type MemResultHandler = ( outfiles: esbuild.OutputFile[], @@ -270,6 +265,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, @@ -294,10 +294,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)] : []), @@ -317,6 +314,7 @@ async function runEsbuild( const abortHandler = async () => { await ctx.cancel(); await ctx.dispose(); + await compilerPluginDispose; }; if (signal) { @@ -342,6 +340,7 @@ async function runEsbuild( } else { if (signal) signal.removeEventListener('abort', abortHandler); await ctx.dispose(); + await compilerPluginDispose; } return writtenFiles; } catch (error) { @@ -486,6 +485,89 @@ 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: OptimizationInput = false, +): NormalizedOptimization { + if (typeof optimization === 'boolean') { + return { + scripts: optimization, + styles: { minify: optimization, inlineCritical: optimization }, + fonts: { inline: optimization }, + }; + } + + 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: normalizeStyles(optimization.styles), + fonts: normalizeFonts(optimization.fonts), + }; +} + +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 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]; +}