Dependency sharing

If every pilet bundled its own copy of React, your users would download React many times over — and worse, you'd have several different React instances on the page, which breaks hooks, context, and anything that relies on a singleton. Dependency sharing solves this. The mechanism is the importmap, and the key thing to understand is that it works in two different places, with two different meanings.

What an importmap is

An importmap is a field in a package.json that lists packages which should be shared rather than bundled:

{
  "name": "my-app-shell",
  "importmap": {
    "imports": {
      "react": "",
      "react-dom": "",
      "react-router-dom": "",
      "my-design-system": ""
    }
  }
}

The value is a version specifier. An empty string means "use the version installed in this project" — the most common case, because it keeps the shared version in lockstep with your normal dependency management. You can also pin a range ("recharts": "^2.10.0"), or point a name at an explicit URL when you host a prebuilt shared bundle yourself.

At build time, Piral reads the importmap and treats those packages specially instead of inlining them into the output bundle.

Two importmaps, two behaviors

The same field behaves differently depending on whether it lives in the app shell or in a pilet.

Where is the dependency declared? App shell importmap central · e.g. react, design system Pure external — provided by the shell, never bundled by any pilet, one instance Pilet importmap · first to resolve e.g. recharts in pilet A Loaded as a side-bundle and published to the shared registry for later pilets Pilet importmap · already resolved e.g. recharts in pilet B Reuses the existing instance — no second download, no duplication
The same importmap field means "external" in the shell and "shared side-bundle" in a pilet.

Central sharing — the app shell importmap

Packages in the app shell's importmap become pure externals. The shell loads them once and provides the running instance to every pilet. Pilets compile against them but never include them in their bundle.

Use this for things that must be a single instance or that everyone needs:

  • Singletons — React and React DOM. Two React copies on one page is a bug, not an optimization.
  • The framework contract — React Router, so all pilets navigate through the same router.
  • Your design system — one shared component library, one consistent UI.

Because the shell owns these, it also owns their version. Bump React in the shell and every pilet gets the new version on the next shell release — pilets don't each declare a React version.

Distributed sharing — the pilet importmap

Packages in a pilet's importmap become side-bundles. The first pilet that needs a given package loads it and registers it in a shared runtime registry; any later pilet that declares the same name reuses that instance instead of downloading its own.

Use this for domain libraries that only some teams need — a charting library, a date utility, an i18n package. The team that introduces the dependency owns the declaration; no coordination with the shell team is required.

{
  "name": "pilet-analytics",
  "importmap": {
    "imports": {
      "recharts": "",
      "date-fns": ""
    }
  }
}

How resolution works at runtime

When a pilet's code imports a shared name, Piral resolves it in this order:

  1. Is it a central external? If the app shell declared it, the pilet uses the shell's instance. Done.
  2. Is it already in the shared registry? If an earlier pilet side-bundled it, the pilet reuses that instance.
  3. Otherwise, the pilet loads its own side-bundle and registers it so later pilets can reuse it.

"First to resolve wins" means load order matters for which build of a side-bundled package is used. For libraries where multiple versions can safely coexist, that's fine. For anything that must be a singleton, promote it to the central importmap so there's exactly one source of truth.

Choosing where a dependency belongs

Put it in…WhenExamples
App shell importmap (external)Everyone needs it, or it must be a singletonreact, react-dom, react-router-dom, design system
Pilet importmap (side-bundle)Only some pilets need it; versions can varyrecharts, date-fns, i18next, a domain SDK
Bundled (no importmap)Small, pilet-private, no benefit to sharingone-off helpers, tiny utilities
Keep singletons central

Anything that breaks with multiple instances — React, state libraries, a global event bus — belongs in the app shell importmap. Side-bundles allow multiple versions to coexist, which is exactly what you don't want for a singleton.

See App shell for the shell side and Pilets for the pilet side.