Build Adapters

Applies to

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:

export interface NFBuildAdapter {
  setup(name: string, options: NFBuildAdapterOptions): Promise<void>;

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

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

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).

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.
outdirstringAbsolute output directory.
mappedPathsPathToImportResolved tsconfig path mappings relevant to this build.
isMappingOrExposedbooleantrue when compiling exposed modules or shared mapped paths; false when bundling shared npm externals.
platform'browser' | 'node'Target platform.
tsConfigPathstringPath to the tsconfig.json to use.
devbooleanDevelopment build (source maps, no minify).
watchbooleanOpt in to watch-mode behavior.
chunksbooleanEnable code-splitting.
optimizedMappingsbooleanHint for incremental compile of mapped paths.
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 entry point:

export interface NFBuildAdapterResult {
  fileName: string;  // absolute path to the emitted file
}

Registering an Adapter

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

await federationBuilder.init({
  options: { /* ... */ },
  adapter: esBuildAdapter,
});

… 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);
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 the platform and isMappingOrExposed flags — shared npm externals and exposed modules often need different bundler settings (e.g. different format or resolve.extensions).