openvibers/openvibe.shared Commits

Shared ADR 0001 (proposed): one package with explicit subpath exports, not a split into @openvibe/* packages

OP OpenVibers committed 23 Sept 2026 · 6f61fbc35f · 3 files, +84 −2

Register item 26 asked for either a split of Shared into packages or an ADR accepting one package. The proposal keeps one package. Consumers pin one GitHub tag tarball, and that can only serve one package per repository. The browser files load each other at runtime, so versioning them separately would bring back skew. Subpath exports already give selective loading to sites with no bundler, and the package has one runtime dependency.

The charter's @openvibe/* names become groups of subpaths. The ADR lists what would justify splitting out a part later: a heavy or native dependency, an outside semver audience, or registry publication. Applied to package.json: the exports map is the public surface, with one explicit entry per subpath. test/package.test.js now refuses wildcard subpaths, and it already requires an entry for every module file. The README links the ADR.

Changes

ddiff --git a/README.md b/README.md
iindex cd99c39..4f2ea67 100644
---- a/README.md
++++ b/README.md
@@@ -27,8 +27,10 @@ were reconciled first.
| Schemas | `docs/schemas/analytics-event.v1.json` (`analytics/event.v1`, exported as `openvibe-shared/analytics/event.v1.json`) |
| Generators | `scripts/build-nav-icons.py` (Font Awesome glyphs → `nav-icons.js`, `ov-icons.js`), `scripts/build-navbar-icons.js` (navbar.js's built-in glyphs), `scripts/build-theme-loader.js`, `scripts/build-app-icons.js` |
--Every module file has an `exports` entry, so `require('openvibe-shared/navbar')`,
--`require.resolve('openvibe-shared/package.json')` and the rest all resolve.
++Every module file has an explicit `exports` entry (no wildcards), so `require('openvibe-shared/navbar')`,
++`require.resolve('openvibe-shared/package.json')` and the rest all resolve. It stays one package with
++subpath exports rather than a set of `@openvibe/*` packages:
++[docs/adr/0001-one-package-subpath-exports.md](docs/adr/0001-one-package-subpath-exports.md).
## Consuming it
ddiff --git a/docs/adr/0001-one-package-subpath-exports.md b/docs/adr/0001-one-package-subpath-exports.md
nnew file mode 100644
iindex 0000000..ab49992
---- /dev/null
++++ b/docs/adr/0001-one-package-subpath-exports.md
@@@ -0,0 +1,78 @@
++# Shared ADR 0001: one package with subpath exports
++
++**Status:** Proposed 2026-09-23, for review with v1.4.0. This is a Shared-local decision under
++network ADR-008 (OpenVibe.Contracts `docs/adr/ADR-008-shared-packages.md`). It answers roadmap
++register item 26: split Shared into packages, or accept one package in an ADR.
++
++## Context
++
++The charter names eleven logical packages: `@openvibe/tokens`, `ui`, `chrome`, `auth-ui`, `seo`,
++`legal`, `icons`, `release-client`, `web-runtime`, `server-web` and `testing-web`. Today everything
++ships as one package, `openvibe-shared`. The facts behind the decision:
++
++- **Distribution is a tag tarball.** Consumers pin
++ `codeload.github.com/OpenVibers/OpenVibe.Shared/tar.gz/refs/tags/vX.Y.Z`, and the repository root
++ is the package root. One repository can serve only one package this way. A split would need a
++ registry, or one repository per package.
++- **Consumers take most of it.** Each of the 19 consumers in the README pins one version, and
++ most use chrome, legal, SEO, icons and themes together. The browser files depend on each
++ other at runtime: `navbar.js` loads `nav-icons.js` and `release-watch.js` from its own directory,
++ and `theme-loader.js` must match the navbar's token names. Separate versions of these files
++ would reintroduce the skew ADR-008 was written to end.
++- **Selective loading already works.** `exports` gives each module its own subpath, and the sites
++ have no bundler. `require('openvibe-shared/legal')` loads only `legal.js` and its own
++ requires. Browser pages load only the files they name. A split would not reduce what a site
++ loads.
++- **The package is small.** Its one runtime dependency is `jsonwebtoken`. The analytics module
++ is dependency-free: the caller passes in `better-sqlite3`.
++- **There is one release process.** A split means N changelogs and N tags per change, and a
++ compatibility matrix between them. There is no second team or release cadence to justify
++ that.
++
++## Decision
++
++Keep **one package, `openvibe-shared`, with explicit subpath exports**:
++
++1. **`package.json` `exports` is the public surface.** Every entry is an explicit path. There
++ are no wildcards (`./*`), so publishing a subpath is always a deliberate choice. A file with
++ no entry is private. `test/package.test.js` checks that every module file, including
++ `analytics/*.js`, has an entry and that every entry resolves.
++2. **Related modules share a folder and a subpath prefix.** Example: `openvibe-shared/analytics`
++ plus `analytics/privacy`, `analytics/tracker`, `analytics/retention`, `analytics/schema`,
++ `analytics/event`, `analytics/prune-cli` and `analytics/event.v1.json`. Put new families the
++ same way, for example `openvibe-shared/seo/...`. Existing flat subpaths such as `./navbar`
++ and `./legal` stay where they are, because moving them would be a major release.
++3. **The charter's package names become groups of subpaths.** They are not separate packages.
++
++ | Group | Subpaths |
++ |---|---|
++ | tokens | `theme-sync`, `theme-loader`, `builtin-themes` |
++ | chrome / ui | `navbar`, `nav-icons`, `footer`, `chrome-ssr`, `panels`, `ui`, `island`, `tooltip`, `user-card`, `account-switcher`, `notification-ui`, `history` |
++ | icons | `icons` (= `ov-icons`), `ov-mark`, `app-icon` |
++ | seo | `seo` |
++ | legal | `legal` |
++ | auth-ui | `sso-client`, `auth-client`, `middleware` |
++ | release-client | `release`, `release-watch`, `openvibe-sw` |
++ | server-web | `metrics`, `ready`, `analytics/*`, `files`, `url-resolver`, `brand`, `notifications` |
++
++4. **Semver applies to the package as a whole.** Removing or renaming any subpath or export is
++ a major release. This is the existing compatibility policy.
++
++## When to revisit
++
++Split out one part, as its own repository and tag, only if one of these becomes true:
++
++- A module needs a heavy or native runtime dependency that most consumers would not want.
++ Example: analytics bundling `better-sqlite3` instead of receiving it from the caller.
++- A part gains an outside audience that needs its own semver line. Example: a published
++ third-party SDK.
++- Shared starts publishing to an npm registry. Scoped packages would then cost little, and a
++ split could be revisited as a whole.
++
++## Consequences
++
++- Consumers keep one pin and one lockfile entry. Moving a copied module into Shared, as with
++ analytics in v1.4.0, is one new subpath and not a new dependency.
++- The charter's `@openvibe/*` names describe how the package is organised. They are not
++ install targets. Update the charter and README text to match once this is accepted.
++- Rollback is to split later. The explicit `exports` map already lists each group's files.
ddiff --git a/test/package.test.js b/test/package.test.js
iindex 2698ddd..64d73ce 100644
---- a/test/package.test.js
++++ b/test/package.test.js
@@@ -11,6 +11,8 @@ const modules = fs.readdirSync(ROOT).filter((f) => f.endsWith('.js'))
.concat(fs.readdirSync(path.join(ROOT, 'analytics')).filter((f) => f.endsWith('.js')).map((f) => `analytics/${f}`));
const targets = new Set(Object.values(pkg.exports).map((t) => t.replace(/^\.\//, '')));
for (const f of modules) assert.ok(targets.has(f), `${f} is exported`);
++// Shared ADR 0001: the exports map is the public surface, one explicit entry per subpath, no patterns.
++for (const sub of Object.keys(pkg.exports)) assert.ok(!sub.includes('*'), `${sub}: no wildcard subpaths`);
for (const [sub, target] of Object.entries(pkg.exports)) {
assert.ok(fs.existsSync(path.join(ROOT, target)), `${sub} -> ${target} exists`);
assert.strictEqual(require.resolve(sub === '.' ? pkg.name : `${pkg.name}/${sub.slice(2)}`), path.join(ROOT, target), `${sub} resolves`);