- 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:
- A custom
abyss-esmexport condition and per-file.mjsemission, so ESM would be opt-in within a minor release. "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: falseis 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 readsTable.Containercan evaluate before the assignment runs — producingstyled(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 itsexportsmap, 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 buildreplacesabyss prepublishfor 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
.mjsis emitted only when a package'sexportsmap references a.mjstarget. It lies dormant — no shipped package emits ESM on this branch.
Not shipped for abyss-web:
- The
abyss-esmopt-in export condition. - Per-file
.mjsemission (abyss-web'sexportsmap 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: falseimmediately. - 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/coreas 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/mobileand@uhg-abyss/sharedalongside 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
- V3 ESM adoption plan
- Build tooling:
products/abyss-internal/src/scripts/commands/abyss-build/ - Reference dual-emit implementation (dormant capability): the abyss-internal
sample-libtest fixture andstrategies/babel.js - Node.js: Conditional exports, Dual-package hazard
Revision History
- 07/14/2026: Initial decision — defer abyss-web ESM to v3; keep build-tooling decoupling on the US10652906 branch.