Orchestrator 4.8.0: one build per package, no more stitching.Read the announcement and share your feedback →

Build Adapters

The NFBuildAdapter contract — how Native Federation Core delegates the actual bundling step to a pluggable, bundler-specific adapter.

The core library doesn't ship a bundler. Instead, it defines a contract — NFBuildAdapter — and delegates the actual compilation to an adapter. That's what makes Native Federation bundler-agnostic.

The Contract

A build adapter is an object implementing three methods, plus a few optional members:

export interface NFBuildAdapter {
  readonly externalsCacheKey?: ExternalsCacheKey; // { adapter: string; options?: Record<string, string | number | boolean> }
  readonly externals?: readonly string[];

  setup(name: string, options: NFBuildAdapterOptions): Promise<void>;

  build(
    name: string,
    opts?: { modifiedFiles?: string[]; signal?: AbortSignal }
  ): Promise<NFBuildAdapterResult[]>;

  dispose(name?: string): Promise<void>;

  cacheData?(name: string): unknown | Promise<unknown>;
}

The core calls these in phases. Each phase uses a unique name — for example 'browser-shared', 'node-shared', 'browser-<pkg>', or 'exposed' — so an adapter can maintain one persistent context per phase (useful for watch mode and incremental compile).

externalsCacheKey names the adapter and the options that change its shared bundles, and takes part in the externals cache checksum. externals lists imports the adapter adds itself (a dev client, a polyfill), so users don't have to list them, and cacheData stores data alongside a cached shared bundle — see Build Your Own Adapter.

NFBuildAdapterOptions

The options object tells the adapter exactly what to compile in a given phase:

FieldTypeDescription
entryPointsEntryPoint[]Entries to compile. Each entry has a fileName, an outName and an optional key.
externalstring[]Packages that must be left as external imports. A name also covers its subpaths and * is a wildcard.
outdirstringAbsolute output directory.
mappedPathsPathToImportResolved tsconfig path mappings relevant to this build.
kind'shared' | 'mapping' | 'exposed'Which kind of bundle this is: shared npm externals, shared mapped paths or exposed modules.
isMappingOrExposedbooleanDeprecated — read kind. true when compiling exposed modules or shared mapped paths. Removed in v5.
platform'browser' | 'node'Target platform.
tsConfigPathstringPath to the tsconfig.json to use, as configured. A relative path is relative to workspaceRoot.
devbooleanDevelopment build (source maps, no minify).
watchbooleanOpt in to watch-mode behavior.
chunksbooleanEnable code-splitting.
optimizedMappingsbooleanDeprecated and unused; removed in v5.
hashbooleanAppend a content hash to output filenames.
cacheFederationCacheShared federation cache — adapters may store bundler-specific state on its bundlerCache slot.

NFBuildAdapterResult

build() returns one result per file it wrote — entries, chunks, source maps and assets — on a rebuild too:

export interface NFBuildAdapterResult {
  fileName: string;  // absolute path to the emitted file
  entry?: string;    // the EntryPoint.outName this file was built from, on the entry's own script
}

Files go flat into outdir. Setting entry tells the core which entry each output belongs to, instead of having it match file names.

Registering an Adapter

Adapters are registered either by passing them to federationBuilder.init:

await federationBuilder.init({
  options: { /* ... */ },
  adapter: createEsBuildAdapter({ plugins: [] }),
});

… or imperatively via setBuildAdapter, when you drive the lower-level API directly:

import { setBuildAdapter, buildForFederation, normalizeFederationOptions, getExternals } from '@softarc/native-federation';

setBuildAdapter(myAdapter);

const { config, options } = await normalizeFederationOptions(fedOptions, undefined, {
  externals: myAdapter.externals,
});
const externals = getExternals(config);
await buildForFederation(config, options, externals);

Adapter Anatomy

Every production adapter tends to follow the same shape:

Reference Adapters

Writing Your Own Adapter

A minimal custom adapter for a hypothetical bundler looks like this:

import type { NFBuildAdapter } from '@softarc/native-federation/domain';

const myAdapter: NFBuildAdapter = {
  contexts: new Map(),

  async setup(name, options) {
    const ctx = await myBundler.createContext({
      entries: options.entryPoints,
      external: options.external,
      outdir: options.outdir,
      platform: options.platform ?? 'browser',
      sourcemap: options.dev,
    });
    this.contexts.set(name, ctx);
  },

  async build(name, opts) {
    const ctx = this.contexts.get(name);
    const result = await ctx.rebuild({
      modifiedFiles: opts?.modifiedFiles,
      signal: opts?.signal,
    });
    return result.outputs.map(o => ({ fileName: o.path }));
  },

  async dispose(name) {
    if (name) {
      await this.contexts.get(name)?.dispose();
      this.contexts.delete(name);
    } else {
      for (const ctx of this.contexts.values()) await ctx.dispose();
      this.contexts.clear();
    }
  },
};

When implementing an adapter, look carefully at platform and kind — shared npm externals and exposed modules often need different bundler settings (e.g. different format or resolve.extensions).