Sharing Dependencies

Applies to

Shared dependencies are the mechanism that lets hosts and remotes load the same library once and reuse it at runtime. This page covers the share and shareAll helpers and the options that govern each shared entry.

shareAll

The shareAll helper shares every production dependency declared in your package.json. Pass a single options object — these options are applied to every discovered dependency:

import { withNativeFederation, shareAll } from '@softarc/native-federation/config';

export default withNativeFederation({
  name: 'host',
  shared: {
    ...shareAll({
      singleton: true,
      strictVersion: true,
      requiredVersion: 'auto',
      includeSecondaries: false,
    }),
  },
});

Per-Package Overrides

Since v21.1, shareAll accepts an overrides option to deviate from the defaults for specific packages:

...shareAll(
  { singleton: true, strictVersion: true, requiredVersion: 'auto' },
  {
    overrides: {
      'package-a/themes/xyz': {
        singleton: true,
        strictVersion: true,
        requiredVersion: 'auto',
        includeSecondaries: { skip: '@package-a/themes/xyz/*' },
        build: 'package',
      },
      'package-b': {
        singleton: false,
        strictVersion: true,
        requiredVersion: 'auto',
        includeSecondaries: { skip: 'package-b/icons/*' },
        build: 'package',
      },
    },
  }
)

share

Use share when you want to hand-pick which dependencies are shared and configure each one individually:

import { share } from '@softarc/native-federation/config';

shared: share({
  'package-a': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: true,
  },
})

Per-Package Options

Option Type Default Description
singleton boolean false Only one instance of this package is ever loaded at runtime. Required for libraries with internal state (Angular, React, zone.js, …).
strictVersion boolean false Throw at runtime instead of falling back when a version mismatch is detected.
requiredVersion string | 'auto' 'auto' The required semver range. 'auto' reads the actual version from the closest package.json.
version string inferred The version that is being shared. Usually inferred from package.json.
includeSecondaries boolean | { skip?, resolveGlob?, keepAll? } true Also share the package's secondary entry points. See below.
platform 'browser' | 'node' config default Target platform for this shared bundle.
build 'default' | 'separate' | 'package' 'default' How the shared external is bundled. 'default' groups all default shared externals into one build step. 'separate' builds the entry on its own. 'package' builds the entry plus its secondaries as an isolated package bundle — required when you want per-package chunks settings to take effect.
chunks boolean config default Enable or disable code-splitting for this specific package.
shareScope string config default Optional share-scope override for this package.

requiredVersion: 'auto'

With 'auto', the helper looks up the version in the closest package.json. This helps resolve unmet peer dependencies and is the recommended default.

Secondary Entry Points

Many packages expose more than one entry point (e.g. @angular/common also ships @angular/common/http, @angular/common/testing, …). includeSecondaries controls how they are handled.

true — include all secondaries

Every directory under the package that contains a package.json or that is listed in exports becomes its own shared entry:

shared: share({
  '@angular/common': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: true,
  },
})

{ skip: ... } — include but filter

shared: share({
  '@angular/common': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: {
      skip: ['@angular/common/http/testing'],
    },
  },
})

{ resolveGlob: true } — expand glob exports

Some packages declare wildcard exports in their package.json — for example RxJS exposes ./internal/*. By default, the share helper only emits a bundle for exact entry points, so an import like rxjs/internal/observable/of crashes at runtime (the import map only resolves exact matches, never glob paths). Since v21 you can opt in to glob resolution:

shared: share({
  'rxjs': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: { resolveGlob: true },
  },
})

The helper walks the glob and produces a shared bundle for every file it matches — that's every valid file under the glob, recursively. For RxJS this can easily add 300+ entries to remoteEntry.json. Only use resolveGlob together with the ignoreUnusedDeps feature flag so the builder prunes anything the entry points don't actually import. You can also narrow the scope yourself with skip, which accepts wildcards:

includeSecondaries: {
  resolveGlob: true,
  skip: ['rxjs/internal/testing/*'],
}

Why this happens. Native Federation treats a shared package as an opaque external for your app bundler. The app bundler sees rxjs/internal/observable/of, marks it external, and hands the exact specifier to the import map at runtime. The import map has no wildcard semantics, so unless the exact specifier is listed, the browser throws a module-not-found error. resolveGlob fixes this by pre-materializing every match into remoteEntry.json.

{ keepAll: true } — opt out of unused-dep removal

When ignoreUnusedDeps is active and you want all secondaries of a package to survive — to guarantee a single, consistent version across every remote — use keepAll:

shared: share({
  '@angular/core': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: { keepAll: true },
  },
})

See Downsides of treeshaking shared packages below for the scenario this protects against.

Skipping Dependencies

"Skip" is not the same as "don't load". A skipped package is not excluded from the micro-frontend — otherwise the app couldn't run on its own. Skip only prevents the package from being extracted into a shared bundle. The package itself is still inlined into the micro-frontend or its shared externals.

Use the top-level skip option to opt out of sharing specific entries — including mapped paths from your tsconfig:

export default withNativeFederation({
  skip: [
    'rxjs/ajax',
    'rxjs/fetch',
    'rxjs/testing',
    'rxjs/webSocket',
    /^@org\/internal-/,
  ],
  shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },
});

Entries accept three forms — matched against the full package name including any secondary entry point:

The skip list you provide is merged with DEFAULT_SKIP_LIST, which already excludes the Native Federation packages themselves, es-module-shims, tslib/ and everything under @types/.

Pseudo-treeshaking via deep imports

Once a dependency is shared, the bundler can no longer tree-shake it: the shared bundle has to contain every symbol that any consumer might ever import at runtime. For a library like RxJS that's a lot of bytes when all you actually use is of.

A pragmatic workaround is to share only the specific deep entry point you use — the rest of the library never enters a shared bundle and stays local to the micro-frontend, where it can be tree-shaken:

// app code
import { of } from 'rxjs/internal/observable/of';

Pair this with includeSecondaries: { resolveGlob: true } and ignoreUnusedDeps: true so only the deep entries you actually touch make it into remoteEntry.json. The rule of thumb: measure before committing. If only one remote imports a couple of symbols, sharing the package at all may cost more than it saves.

Downsides of treeshaking shared packages

ignoreUnusedDeps is enabled by default and almost always the right choice — but there is a failure mode to be aware of, especially for core framework libraries.

Imagine two remotes, each sharing @angular/core: mfe1 on 21.0.2 and mfe2 on 21.0.1. The orchestrator picks 21.0.2 as the winning version. But if mfe1 never imports @angular/core/rxjs-interop, that secondary is pruned from its remoteEntry.json. The orchestrator then falls back to mfe2's copy of rxjs-interop — at version 21.0.1. Now Angular is split across two versions, which is exactly the class of bug shared dependencies are supposed to prevent.

Use keepAll: true on such packages to force all secondaries to be shared regardless of what the entry points touch:

shared: share({
  '@angular/core': {
    singleton: true,
    strictVersion: true,
    requiredVersion: 'auto',
    includeSecondaries: { keepAll: true },
  },
})

As a rule of thumb, opt into keepAll for tightly-coupled framework packages (Angular, React ecosystems, your own design system) and leave it off for utility libraries where secondaries are genuinely independent.

Code-Splitting

By default, large shared libraries are split into chunks that load on demand. Control this at two levels:

Global

export default withNativeFederation({
  chunks: false,   // disable code-splitting for every shared entry
  shared: { ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }) },
});

Per-Package

export default withNativeFederation({
  shared: {
    ...shareAll(
      { singleton: true, strictVersion: true, requiredVersion: 'auto' },
      {
        overrides: {
          'large-lib': {
            singleton: true,
            strictVersion: true,
            requiredVersion: 'auto',
            chunks: false,
            build: 'package',  // required for per-package chunk settings to take effect
          },
        },
      }
    ),
  },
});

Per-package chunks settings are only honored when build is set to 'package' (or 'separate'). 'default' externals share one build step, so their chunks value is overridden by the top-level config.

Dense Chunking

Enable features.denseChunking to group chunks by bundle name in remoteEntry.json. Each shared entry then references its chunk bundle by name rather than listing every chunk individually, producing a smaller and more cache-friendly manifest:

features: { denseChunking: true }