Skip to main content

Defer ESM adoption to v3

  • Status: Accepted
  • Authors: Tony Braasch
  • Deciders: Abyss Team
  • Date: 2026-07-14

Context

Story US10652906 decoupled Abyss build tooling from @uhg-abyss/core into a private abyss-internal package (abyss-internal build replacing abyss prepublish). As part of that work we prototyped shipping @uhg-abyss/web as a dual-format package so consumers on modern bundlers could benefit from tree-shaking. The prototype added:

  1. A custom abyss-esm export condition and per-file .mjs emission, so ESM would be opt-in within a minor release.
  2. "sideEffects": false, to unlock module-level dead-code elimination.

Validating the prototype against the docs site (which bundles abyss-web from source with webpack) surfaced two blocking problems:

  • sideEffects: false is unsafe with the current source. Abyss compound components (Table, Accordion, Modal, DataTable, and ~29 others) build themselves by mutating the base component at module top level, e.g. V1Table.Container = StyledTable. These assignments are real side effects. Declaring the package side-effect-free lets bundlers drop or reorder them, so a consumer module that reads Table.Container can evaluate before the assignment runs — producing styled(undefined) and a runtime crash. This affects any consumer whose bundler performs production dead-code elimination, not just consumers who opt into ESM.

  • Without sideEffects: false, the opt-in ESM flag delivers little. Abyss already enforces deep imports through its exports map, so consumers only pull the components they reference regardless of module format. The main remaining win — whole-module DCE — is gated on removing the mutation pattern.

Separately, making ESM the default (via the standard import/require conditions) is conventionally a major-version change. Shipping the non-standard abyss-esm flag in a minor would create migration churn for early adopters when v3 later switches to standard conditions.

Decision

Defer adopting ESM for @uhg-abyss/web until the v3 major release.

On the US10652906 branch we keep only the non-breaking build-tooling work and strip the abyss-web ESM prototype:

Kept (non-breaking):

  • abyss-internal build replaces abyss prepublish for abyss-web, abyss-mobile, abyss-api, and abyss-parcels, with byte-identical CJS output.
  • Internal build commands (build-tokens, changelog, watch-tokens) moved to abyss-internal.
  • The dual-emit capability stays in abyss-internal, gated so .mjs is emitted only when a package's exports map references a .mjs target. It lies dormant — no shipped package emits ESM on this branch.

Not shipped for abyss-web:

  • The abyss-esm opt-in export condition.
  • Per-file .mjs emission (abyss-web's exports map is CJS-only).
  • "sideEffects": false.

The complete v3 implementation plan — including the compound-component refactor, enabling sideEffects, switching to default ESM, and validation — is captured in V3 ESM adoption plan.

Alternatives Considered

Ship opt-in ESM now (without sideEffects: false)

  • Pros: Lets early adopters experiment; establishes the ESM path incrementally.
  • Cons: Delivers little benefit without whole-module DCE; adds a non-standard condition, dual-emit artifacts, and docs surface; creates migration churn when v3 moves to standard conditions.

Ship sideEffects: false with a curated allowlist

  • Pros: Retains some module-level tree-shaking now.
  • Cons: Fragile — the allowlist must enumerate every compound module with side effects across the library; missing one is a silent consumer crash. Unacceptable risk for a backward-compatibility-critical change.

Do the compound-component refactor now, on this branch

  • Pros: Would unblock sideEffects: false immediately.
  • Cons: ~29 components, broad regression surface, and requires full visual/component-test validation across the browser matrix — out of scope for a build-tooling story and better sequenced with a major release.

Consequences

Positive

  • The US10652906 branch is fully non-breaking: current CJS consumers are unaffected and abyss-web output is byte-identical to the previous abyss prepublish.
  • Build tooling is decoupled from @uhg-abyss/core as intended.
  • The dual-emit capability is built, tested, and ready to enable in v3.
  • The root-cause finding (compound mutation vs. sideEffects: false) is captured so v3 starts from a known, sequenced plan rather than rediscovering it.

Negative

  • Consumers do not yet receive ESM or whole-module tree-shaking benefits.
  • The tree-shaking win is deferred to a major release.

Future Considerations

  • Execute the V3 ESM adoption plan: refactor compound components off top-level mutation, enable sideEffects, make ESM the default via standard conditions, and validate across the browser matrix.
  • Revisit possible adoption of ESM for @uhg-abyss/mobile and @uhg-abyss/shared alongside abyss-web in v3.
  • Re-evaluate a library-level singleton-context helper. It was prototyped and reverted here because ESM was opt-in (the dual-package hazard was theoretical). Making ESM the default in v3 turns that hazard live, so v3 is the point to decide — defaulting to the consumer-side fix unless validation shows real duplicate-context breakage (see the plan, §7.4).

References

Revision History

  • 07/14/2026: Initial decision — defer abyss-web ESM to v3; keep build-tooling decoupling on the US10652906 branch.
Table of Contents