Turn the single @stainless-code/persist package (subpaths + optional-peer soup) into a layers-shaped bun-workspaces monorepo: private root, zero-peer @stainless-code/persist in packages/core, opt-in packages wherever an npm peer exists. Skills already name the units (skills/<name>/); this plan maps them to publishable packages.
When this ships: lift durable bits into docs/architecture.md + README story + docs-governance README surfaces; delete this file (per docs-governance § Closing a plan).
- Install burden — consumers must not carry peers/deps for adapters they never import. Optional peers on one package still tax the graph and optics.
- Product tenets — open/agnostic core; adapters layered and opt-in; predictable package scope.
- Naming — framework packages are
<fw>-persist; concern packages arepersist-<leaf>; core compose subpaths are flat/<leaf>(skill leaf without thepersist-prefix). Seams stay in docs/src, not in the public path. - Skills lead —
skills/is already split 1:1 with intended namespaces (v0.4.1). Package layout should match. - Workspace infra — match layers: private root,
packages/*, per-package build/skills/README.
- Core has zero peers — no
peerDependencies/peerDependenciesMetaon@stainless-code/persist. - Npm peer ⇒ own package — that peer is required on the satellite (not optional). Zero-peer compose (platform globals, vendored types, Node built-ins) stays on core as flat subpaths (
./encrypted, not./backends/encrypted) — skill ≠ package. Includesnode-fs(node:fsis not an npm peer). - Satellites depend on core —
dependencies: { "@stainless-code/persist": "workspace:*" }(like layers). Not a peerDependency; Rule 5 is about import composition, not a secondbun add. - Skill ships with the package that owns the code — move root
skills/intopackages/*/skills/; delete the root bag. - No core re-export from satellites — consumers compose ≥2 import surfaces (source + storage ± gate). Unlike layers adapters, satellites do not re-export core.
- Framework packages are hydration bindings only —
useHydrated/HydrationController/ etc.; not a UI kit. Name:<fw>-persist. - Breaking cut at 0.5.0 — hard cut: peel peer-backed entries and flatten zero-peer core subpaths; CHANGELOG + migration path table; no codemod (pre-1.0). No forever mega-package that re-exports every satellite. 1.0.0 later = stability seal only, not another package-shape break.
- Independent versions — changesets
fixed: [];updateInternalDependencies: "patch". First publish wave: all packages at 0.5.0; independent afterward. - No breaking npm until the full set is ready — infra may land early if the
0.4.xpublish surface is unchanged; do not publish a half-split.
| Subpath | Skill | Symbols |
|---|---|---|
. |
persist |
persistSource, storage/codec factories, hydration signal APIs, types |
./encrypted |
persist-encrypted |
createEncryptedStorage |
./compressed |
persist-compressed |
createCompressedStorage |
./node-fs |
persist-node-fs |
nodeFsStateStorage |
./crosstab |
persist-crosstab |
createBroadcastCrossTab |
./standard-schema |
persist-standard-schema |
withStandardSchema*, createStandardSchemaStorage (vendored ~standard — no peer) |
Deps/peers: none. Public path = skill leaf (drop persist- prefix). Seam folders (backends/ / codecs/ / transport/) are maintainer layout / docs vocabulary only — not part of the import.
Each: dependencies: { "@stainless-code/persist": "workspace:*" }, entry ., no core re-export. Peer ranges: carry forward from today's root peerDependencies.
| Package | Skill | Old path | Required peer |
|---|---|---|---|
@stainless-code/persist-seroval |
persist-seroval |
./codecs/seroval |
seroval |
@stainless-code/persist-idb |
persist-idb |
./backends/idb |
idb-keyval |
@stainless-code/persist-async-storage |
persist-async-storage |
./backends/async-storage |
@react-native-async-storage/async-storage |
@stainless-code/persist-mmkv |
persist-mmkv |
./backends/mmkv |
react-native-mmkv |
@stainless-code/persist-secure-store |
persist-secure-store |
./backends/secure-store |
expo-secure-store |
@stainless-code/persist-tanstack-store |
persist-tanstack-store |
./sources/tanstack-store |
@tanstack/store |
@stainless-code/persist-zustand |
persist-zustand |
./sources/zustand |
zustand |
@stainless-code/persist-jotai |
persist-jotai |
./sources/jotai |
jotai |
@stainless-code/persist-valtio |
persist-valtio |
./sources/valtio |
valtio |
@stainless-code/persist-mobx |
persist-mobx |
./sources/mobx |
mobx |
@stainless-code/persist-pinia |
persist-pinia |
./sources/pinia |
pinia |
@stainless-code/persist-redux |
persist-redux |
./sources/redux |
redux |
Hydration gate only. Each: dep on @stainless-code/persist, required framework peer, no core re-export.
| Package | Skill | Old path | Required peer | Export |
|---|---|---|---|---|
@stainless-code/react-persist |
react-persist |
./frameworks/react |
react |
useHydrated |
@stainless-code/preact-persist |
preact-persist |
./frameworks/preact |
preact |
useHydrated |
@stainless-code/vue-persist |
vue-persist |
./frameworks/vue |
vue |
useHydrated |
@stainless-code/solid-persist |
solid-persist |
./frameworks/solid |
solid-js |
useHydrated |
@stainless-code/angular-persist |
angular-persist |
./frameworks/angular |
@angular/core |
useHydrated |
@stainless-code/lit-persist |
lit-persist |
./frameworks/lit |
lit |
HydrationController |
@stainless-code/alpine-persist |
alpine-persist |
./frameworks/alpine |
alpinejs |
plugin / useHydrated / $hydrated |
@stainless-code/svelte-persist |
svelte-persist |
./frameworks/svelte |
svelte |
hydratedRune |
@stainless-code/svelte-store-persist |
svelte-store-persist |
./frameworks/svelte-store |
svelte |
hydratedStore |
Skills already split Svelte runes vs stores into two packages — keep that (not svelte-persist/store).
Today src/testing/ is already omitted from dist / typedoc. Do not publish @stainless-code/persist-testing.
| Home | Contents | Why |
|---|---|---|
packages/test-utils/ ("private": true) |
MemoryStorage, createMockSource, waitForHydration, isolation assert |
Shared by core + every satellite test; devDependency: workspace:* |
Not a public export on core. Not copied per package. Generalize itImportsOnlyFromCore → “imports only @stainless-code/persist + own peer”.
Published count: 1 core + 12 concern + 9 framework = 22. Plus one private workspace package for tests.
| Old | New |
|---|---|
@stainless-code/persist |
same |
…/backends/encrypted |
@stainless-code/persist/encrypted |
…/backends/compressed |
@stainless-code/persist/compressed |
…/backends/node-fs |
@stainless-code/persist/node-fs |
…/transport/crosstab |
@stainless-code/persist/crosstab |
…/codecs/standard-schema |
@stainless-code/persist/standard-schema |
…/codecs/seroval |
@stainless-code/persist-seroval |
…/backends/idb |
@stainless-code/persist-idb |
…/backends/async-storage |
@stainless-code/persist-async-storage |
…/backends/mmkv |
@stainless-code/persist-mmkv |
…/backends/secure-store |
@stainless-code/persist-secure-store |
…/sources/tanstack-store |
@stainless-code/persist-tanstack-store |
…/sources/zustand |
@stainless-code/persist-zustand |
…/sources/jotai |
@stainless-code/persist-jotai |
…/sources/valtio |
@stainless-code/persist-valtio |
…/sources/mobx |
@stainless-code/persist-mobx |
…/sources/pinia |
@stainless-code/persist-pinia |
…/sources/redux |
@stainless-code/persist-redux |
…/frameworks/react |
@stainless-code/react-persist |
…/frameworks/preact |
@stainless-code/preact-persist |
…/frameworks/vue |
@stainless-code/vue-persist |
…/frameworks/solid |
@stainless-code/solid-persist |
…/frameworks/angular |
@stainless-code/angular-persist |
…/frameworks/lit |
@stainless-code/lit-persist |
…/frameworks/alpine |
@stainless-code/alpine-persist |
…/frameworks/svelte |
@stainless-code/svelte-persist |
…/frameworks/svelte-store |
@stainless-code/svelte-store-persist |
package.json # private monorepo root (layers-monorepo shape)
packages/
core/ # @stainless-code/persist
skills/{persist,persist-encrypted,persist-compressed,persist-node-fs,persist-crosstab,persist-standard-schema}/
seroval/ # @stainless-code/persist-seroval + skill
idb/
async-storage/
mmkv/
secure-store/
tanstack-store/
zustand/
jotai/
valtio/
mobx/
pinia/
redux/
react/ # @stainless-code/react-persist + skill
preact/
vue/
solid/
angular/
lit/
alpine/
svelte/
svelte-store/ # @stainless-code/svelte-store-persist + skill
test-utils/ # private — MemoryStorage, mocks, isolation assert (not published)
Workspace tooling (match layers): bun workspaces apps/* + packages/*, @stainless-code/source export condition, sherif/knip, changesets, per-package tsdown + size-limit. Isolation assert: each adapter imports only @stainless-code/persist + its peer.
- Root
README.md— monorepo index (package table + install pointers), not the sole npm blurb. packages/*/README.md— npm landing per publishable (install, peer, short taste).- Update docs-governance § README surfaces from “root README = npm landing” to this layers model.
apps/docsremains canonical public docs.
# headless + localStorage (core alone)
bun add @stainless-code/persist
# zustand + idb + React hydrate gate (core comes transitively; listing it is fine)
bun add @stainless-code/persist-zustand @stainless-code/persist-idb @stainless-code/react-persist
# RN + mmkv
bun add @stainless-code/persist-mmkv- Baseline: freeze last single-package
0.4.x. - Ship multi-package as
0.5.0across all 22 publishables: move the 21 peer-backed entries; keep zero-peer compose on core but flatten their public subpaths (./backends/encrypted→./encrypted, etc.). - No codemod — CHANGELOG breaking section + old→new table (above) + docs install paths.
- No runtime shim / core re-export of satellites (recreates peer soup).
- Update
apps/docs, per-package READMEs, skilllibrary:metadata, typedoc entry points,sync-skill-versionsfor multi-package.
- One package per zero-peer wrapper (
persist-encrypted/persist-node-fsas npm) — skill yes, package no. - Keeping seam folders in public core exports (
./backends/*,./codecs/*,./transport/*) — rejected; flatten at 0.5. - Umbrella / suite meta-package that re-aggregates all peers.
- Renaming framework packages to
persist-react— rejected; keep<fw>-persist. - Folding
svelte-store-persistintosvelte-persist/store— skills already chose two packages. - Codemod / calling the split 1.0.0 — deferred; v1 is a later stability seal.
- Core re-export from satellites (layers does this; persist does not).
- Workspace builds/publishes all 22 packages at
0.5.0; corepeerDependenciesempty; root package private. - Each satellite declares exactly one required peer (svelte pair: both peer
svelte) +dependencieson core. - Skills live under owning package; root
skills/gone;librarymetadata points at that package. - Isolation + zero-dep core gates green; sherif/knip green.
- CHANGELOG + docs cover old→new table (peer peels and core subpath flatten); no half-split publish.
- Root README = monorepo index; per-package npm READMEs; docs-governance README surfaces updated.
- Happy-path installs above typecheck without unrelated peers in the lockfile noise.