@variantlab/core 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1209 -39
- package/docs/API.md +692 -0
- package/docs/ARCHITECTURE.md +430 -0
- package/docs/CONTRIBUTING.md +264 -0
- package/docs/ROADMAP.md +292 -0
- package/docs/SECURITY.md +323 -0
- package/docs/design/api-philosophy.md +347 -0
- package/docs/design/config-format.md +442 -0
- package/docs/design/design-principles.md +212 -0
- package/docs/design/targeting-dsl.md +433 -0
- package/docs/features/codegen.md +351 -0
- package/docs/features/crash-rollback.md +399 -0
- package/docs/features/debug-overlay.md +328 -0
- package/docs/features/hmac-signing.md +330 -0
- package/docs/features/killer-features.md +308 -0
- package/docs/features/multivariate.md +339 -0
- package/docs/features/qr-sharing.md +372 -0
- package/docs/features/targeting.md +481 -0
- package/docs/features/time-travel.md +306 -0
- package/docs/features/value-experiments.md +487 -0
- package/docs/phases/phase-2-expansion.md +307 -0
- package/docs/phases/phase-3-ecosystem.md +289 -0
- package/docs/phases/phase-4-advanced.md +306 -0
- package/docs/phases/phase-5-v1-stable.md +350 -0
- package/docs/research/bundle-size-analysis.md +279 -0
- package/docs/research/competitors.md +327 -0
- package/docs/research/framework-ssr-quirks.md +394 -0
- package/docs/research/naming-rationale.md +238 -0
- package/docs/research/origin-story.md +179 -0
- package/docs/research/security-threats.md +312 -0
- package/package.json +2 -1
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document describes the monorepo layout, build tooling, runtime architecture, data flow, and size budgets for variantlab.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Design goals](#design-goals)
|
|
8
|
+
- [Monorepo layout](#monorepo-layout)
|
|
9
|
+
- [Package boundaries](#package-boundaries)
|
|
10
|
+
- [Runtime data flow](#runtime-data-flow)
|
|
11
|
+
- [Build tooling](#build-tooling)
|
|
12
|
+
- [Size budgets](#size-budgets)
|
|
13
|
+
- [Dependency policy](#dependency-policy)
|
|
14
|
+
- [Versioning and release strategy](#versioning-and-release-strategy)
|
|
15
|
+
- [Testing strategy](#testing-strategy)
|
|
16
|
+
- [CI/CD pipeline](#cicd-pipeline)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Design goals
|
|
21
|
+
|
|
22
|
+
1. **Core runs anywhere.** Any ECMAScript 2020 environment — Node 18+, Deno, Bun, browsers, React Native Hermes, Cloudflare Workers, Vercel Edge, AWS Lambda@Edge. Zero platform APIs in core.
|
|
23
|
+
2. **Adapters are trivially small.** Each framework adapter should be < 200 source LOC and < 2 KB gzipped.
|
|
24
|
+
3. **Tree-shakeable everything.** Every export lives in its own module file. Unused code is eliminated at build time.
|
|
25
|
+
4. **No implicit IO.** Core never reads from disk, network, or global storage on its own. All IO happens through injected adapters (Storage, Fetcher, Telemetry).
|
|
26
|
+
5. **Deterministic at hash boundaries.** User bucketing uses a stable hash. The same `userId` + experiment produces the same variant across every machine, every runtime, every language.
|
|
27
|
+
6. **Forward-compatible config schema.** `experiments.json` has a `version` field. The engine refuses to load a config from a newer major version, warns on a newer minor, and upgrades older configs in memory.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Monorepo layout
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
variantlab/
|
|
35
|
+
├── README.md
|
|
36
|
+
├── ARCHITECTURE.md
|
|
37
|
+
├── API.md
|
|
38
|
+
├── SECURITY.md
|
|
39
|
+
├── ROADMAP.md
|
|
40
|
+
├── LICENSE
|
|
41
|
+
├── CONTRIBUTING.md
|
|
42
|
+
├── experiments.schema.json
|
|
43
|
+
├── package.json # pnpm workspace root
|
|
44
|
+
├── pnpm-workspace.yaml
|
|
45
|
+
├── tsconfig.base.json
|
|
46
|
+
├── .changeset/ # changesets for versioning
|
|
47
|
+
├── .github/
|
|
48
|
+
│ └── workflows/
|
|
49
|
+
│ ├── ci.yml # lint, test, size-limit
|
|
50
|
+
│ ├── release.yml # changesets publish
|
|
51
|
+
│ ├── sigstore.yml # sigstore signing
|
|
52
|
+
│ └── compat-matrix.yml # test against Node 18/20/22, React 18/19
|
|
53
|
+
├── docs/
|
|
54
|
+
│ ├── research/
|
|
55
|
+
│ ├── design/
|
|
56
|
+
│ ├── features/
|
|
57
|
+
│ ├── phases/
|
|
58
|
+
│ └── adapters/
|
|
59
|
+
├── packages/
|
|
60
|
+
│ ├── core/ # @variantlab/core
|
|
61
|
+
│ │ ├── src/
|
|
62
|
+
│ │ │ ├── engine.ts
|
|
63
|
+
│ │ │ ├── types.ts
|
|
64
|
+
│ │ │ ├── storage.ts
|
|
65
|
+
│ │ │ ├── targeting.ts
|
|
66
|
+
│ │ │ ├── assignment.ts
|
|
67
|
+
│ │ │ ├── hash.ts
|
|
68
|
+
│ │ │ ├── schema.ts
|
|
69
|
+
│ │ │ ├── crypto.ts
|
|
70
|
+
│ │ │ ├── errors.ts
|
|
71
|
+
│ │ │ └── index.ts
|
|
72
|
+
│ │ ├── test/
|
|
73
|
+
│ │ ├── package.json
|
|
74
|
+
│ │ └── tsup.config.ts
|
|
75
|
+
│ ├── react/ # @variantlab/react
|
|
76
|
+
│ ├── react-native/ # @variantlab/react-native
|
|
77
|
+
│ ├── next/ # @variantlab/next
|
|
78
|
+
│ ├── remix/ # @variantlab/remix (phase 2)
|
|
79
|
+
│ ├── vue/ # @variantlab/vue (phase 2)
|
|
80
|
+
│ ├── vanilla/ # @variantlab/vanilla (phase 2)
|
|
81
|
+
│ ├── svelte/ # @variantlab/svelte (phase 3)
|
|
82
|
+
│ ├── solid/ # @variantlab/solid (phase 3)
|
|
83
|
+
│ ├── astro/ # @variantlab/astro (phase 3)
|
|
84
|
+
│ ├── nuxt/ # @variantlab/nuxt (phase 3)
|
|
85
|
+
│ ├── storybook/ # @variantlab/storybook (phase 3)
|
|
86
|
+
│ ├── eslint-plugin/ # @variantlab/eslint-plugin (phase 3)
|
|
87
|
+
│ ├── test-utils/ # @variantlab/test-utils (phase 3)
|
|
88
|
+
│ ├── devtools/ # @variantlab/devtools (phase 2, browser ext)
|
|
89
|
+
│ └── cli/ # @variantlab/cli
|
|
90
|
+
├── apps/
|
|
91
|
+
│ ├── docs/ # Astro Starlight docs site
|
|
92
|
+
│ ├── playground/ # browser sandbox (paste config, see it work)
|
|
93
|
+
│ └── examples/
|
|
94
|
+
│ ├── next-app-router/
|
|
95
|
+
│ ├── next-pages/
|
|
96
|
+
│ ├── expo-router/
|
|
97
|
+
│ ├── react-native-cli/
|
|
98
|
+
│ ├── remix/
|
|
99
|
+
│ ├── vite-react/
|
|
100
|
+
│ ├── nuxt/
|
|
101
|
+
│ ├── sveltekit/
|
|
102
|
+
│ ├── solid-start/
|
|
103
|
+
│ └── astro/
|
|
104
|
+
└── tools/
|
|
105
|
+
├── size-limit.config.js
|
|
106
|
+
└── scripts/
|
|
107
|
+
├── check-deps.js # verify 0 runtime deps
|
|
108
|
+
└── check-bundle-size.js
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Package boundaries
|
|
114
|
+
|
|
115
|
+
### `@variantlab/core`
|
|
116
|
+
|
|
117
|
+
The engine. Zero dependencies. Exports:
|
|
118
|
+
|
|
119
|
+
- `VariantEngine` class — the runtime state machine
|
|
120
|
+
- `createEngine(config, options)` factory
|
|
121
|
+
- `Storage` interface — pluggable persistence
|
|
122
|
+
- `Fetcher` interface — optional remote config loader
|
|
123
|
+
- `Telemetry` interface — optional event sink
|
|
124
|
+
- `Targeting` utilities — predicate evaluation
|
|
125
|
+
- `Assignment` strategies — default, random, sticky-hash, weighted
|
|
126
|
+
- Type exports: `Experiment`, `Variant`, `VariantContext`, `ExperimentsConfig`
|
|
127
|
+
|
|
128
|
+
**Allowed globals**: `crypto` (Web Crypto API), `Date`, `Math`. Nothing else. No `document`, no `window`, no `localStorage`, no `fetch`, no `process`.
|
|
129
|
+
|
|
130
|
+
**Runtime target**: ES2020. No `??=`, no top-level await, no decorators.
|
|
131
|
+
|
|
132
|
+
### `@variantlab/react`
|
|
133
|
+
|
|
134
|
+
React bindings. Depends on `@variantlab/core` and nothing else (React is a peer dep). Exports:
|
|
135
|
+
|
|
136
|
+
- `<VariantLabProvider config={...}>` — context provider
|
|
137
|
+
- `<Variant experimentId="...">` — render-prop switcher
|
|
138
|
+
- `<VariantValue experimentId="...">` — render-prop for value experiments
|
|
139
|
+
- `useVariant(id)` — hook returning the current variant ID
|
|
140
|
+
- `useVariantValue<T>(id)` — hook returning the variant's value
|
|
141
|
+
- `useExperiment(id)` — hook returning `{ variant, value, track }`
|
|
142
|
+
- `useSetVariant()` — hook to imperatively override a variant (dev only by default)
|
|
143
|
+
- `<VariantDebugOverlay />` — floating debug picker (tree-shaken in production builds)
|
|
144
|
+
- `<VariantErrorBoundary experimentId="...">` — error boundary that reports crashes to the engine
|
|
145
|
+
|
|
146
|
+
**React version**: 18.2+ and 19. Uses `useSyncExternalStore` for concurrent-mode safety.
|
|
147
|
+
|
|
148
|
+
### `@variantlab/react-native`
|
|
149
|
+
|
|
150
|
+
React Native + Expo bindings. Depends on `@variantlab/core`, peer-depends on `react-native` and optionally `@react-native-async-storage/async-storage`, `react-native-mmkv`, `expo-secure-store`. Exports the same surface as `@variantlab/react` plus:
|
|
151
|
+
|
|
152
|
+
- Default `AsyncStorageAdapter`, `MMKVStorageAdapter`, `SecureStoreAdapter`
|
|
153
|
+
- `<VariantDebugOverlay>` with RN-native UI (floating button, bottom sheet, QR modal, shake-to-open)
|
|
154
|
+
- `useRouteAwareExperiments()` — filters experiments by current Expo Router / React Navigation route
|
|
155
|
+
- Deep-link handler for `myapp://variantlab?set=...`
|
|
156
|
+
|
|
157
|
+
### `@variantlab/next`
|
|
158
|
+
|
|
159
|
+
Next.js 14 and 15 bindings. Supports both App Router and Pages Router. Exports:
|
|
160
|
+
|
|
161
|
+
- `createVariantLabServer(config)` — SSR-aware engine factory
|
|
162
|
+
- `<VariantLabProvider>` — client component version (re-exports from `@variantlab/react`)
|
|
163
|
+
- `getVariantSSR(experimentId, req)` — server helper for App Router loaders and Pages `getServerSideProps`
|
|
164
|
+
- `variantLabMiddleware(config)` — Next.js middleware that sets a sticky cookie
|
|
165
|
+
- React Server Component support for reading variants on the server
|
|
166
|
+
|
|
167
|
+
### `@variantlab/cli`
|
|
168
|
+
|
|
169
|
+
Command-line tool. Dev dependency only, never ships to production. Exports binary `variantlab`:
|
|
170
|
+
|
|
171
|
+
- `variantlab init` — scaffold `experiments.json` + install recommended adapter
|
|
172
|
+
- `variantlab generate` — codegen `.d.ts` from `experiments.json`
|
|
173
|
+
- `variantlab validate` — validate config against schema + check for orphaned IDs
|
|
174
|
+
- `variantlab scaffold <experiment-id>` — scaffold boilerplate for a new experiment
|
|
175
|
+
|
|
176
|
+
**Runtime**: Node 18+. The CLI is allowed dependencies (`commander`, `chalk`) — they do not affect the runtime packages.
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Runtime data flow
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
184
|
+
│ Application code (framework) │
|
|
185
|
+
│ │
|
|
186
|
+
│ useVariant("x") <Variant experimentId="x"> track(...) │
|
|
187
|
+
└──────────┬──────────────────────┬──────────────────────┬────────┘
|
|
188
|
+
│ │ │
|
|
189
|
+
│ │ │
|
|
190
|
+
┌──────────▼──────────────────────▼──────────────────────▼────────┐
|
|
191
|
+
│ Framework adapter │
|
|
192
|
+
│ (@variantlab/react, /next, ...) │
|
|
193
|
+
│ │
|
|
194
|
+
│ React Context │ Hooks │ SSR helpers │ Debug overlay │
|
|
195
|
+
└──────────┬──────────────────────────────────────────────────────┘
|
|
196
|
+
│
|
|
197
|
+
│ subscribe / getVariant / setVariant / trackEvent
|
|
198
|
+
│
|
|
199
|
+
┌──────────▼──────────────────────────────────────────────────────┐
|
|
200
|
+
│ @variantlab/core │
|
|
201
|
+
│ │
|
|
202
|
+
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
|
|
203
|
+
│ │ Engine │──│ Targeting │──│ Assignment │──│ Schema │ │
|
|
204
|
+
│ │ │ │ │ │ │ │ validator│ │
|
|
205
|
+
│ └─────┬──────┘ └────────────┘ └────────────┘ └──────────┘ │
|
|
206
|
+
│ │ │
|
|
207
|
+
│ │ │
|
|
208
|
+
│ ┌─────▼──────┐ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
|
|
209
|
+
│ │ Storage │ │ Fetcher │ │ Telemetry │ │ Crypto │ │
|
|
210
|
+
│ │ (injected) │ │ (injected) │ │ (injected) │ │ (WebAPI) │ │
|
|
211
|
+
│ └────────────┘ └────────────┘ └────────────┘ └──────────┘ │
|
|
212
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Resolve variant (hot path)
|
|
216
|
+
|
|
217
|
+
Called on every `useVariant()` read. Must be O(1).
|
|
218
|
+
|
|
219
|
+
1. Adapter calls `engine.getVariant(experimentId, context)`.
|
|
220
|
+
2. Engine checks in-memory override map (dev/debug overrides win).
|
|
221
|
+
3. Engine checks Storage for a persisted assignment.
|
|
222
|
+
4. If none, engine evaluates targeting predicates against `context`.
|
|
223
|
+
5. If targeting passes, engine runs the assignment strategy (sticky-hash/weighted/default).
|
|
224
|
+
6. Engine writes the result to Storage and memoizes.
|
|
225
|
+
7. Engine emits an `onAssignment` event to Telemetry (first time only per session).
|
|
226
|
+
8. Returns variant ID.
|
|
227
|
+
|
|
228
|
+
### Load config (cold start)
|
|
229
|
+
|
|
230
|
+
1. App mounts provider with inline config or async `Fetcher`.
|
|
231
|
+
2. Engine validates config against `experiments.schema.json` (hand-rolled validator, no zod).
|
|
232
|
+
3. If HMAC signature is present, engine verifies via Web Crypto `crypto.subtle.verify`.
|
|
233
|
+
4. Engine hydrates Storage — reads all previously persisted assignments.
|
|
234
|
+
5. Engine emits `onReady` event.
|
|
235
|
+
|
|
236
|
+
### Override flow (dev / QA)
|
|
237
|
+
|
|
238
|
+
1. User taps a variant in `<VariantDebugOverlay>`, or deep link fires, or QR is scanned.
|
|
239
|
+
2. Adapter calls `engine.setVariant(experimentId, variantId)`.
|
|
240
|
+
3. Engine writes override to Storage with priority flag.
|
|
241
|
+
4. Engine emits `onVariantChanged`.
|
|
242
|
+
5. All subscribed components re-render via `useSyncExternalStore`.
|
|
243
|
+
|
|
244
|
+
### Crash rollback flow
|
|
245
|
+
|
|
246
|
+
1. `<VariantErrorBoundary experimentId="x">` catches an error.
|
|
247
|
+
2. Adapter calls `engine.reportCrash(experimentId, error)`.
|
|
248
|
+
3. Engine increments a crash counter in Storage scoped to `(experimentId, variantId, sessionId)`.
|
|
249
|
+
4. If counter exceeds `rollbackThreshold` within `rollbackWindow`, engine:
|
|
250
|
+
- Clears the persisted assignment
|
|
251
|
+
- Forces the user to the experiment's `default` variant
|
|
252
|
+
- Marks this variant as "quarantined" for the session
|
|
253
|
+
- Emits `onRollback` event
|
|
254
|
+
|
|
255
|
+
Full details in [`docs/features/crash-rollback.md`](./docs/features/crash-rollback.md).
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Build tooling
|
|
260
|
+
|
|
261
|
+
| Tool | Purpose | Why |
|
|
262
|
+
|---|---|---|
|
|
263
|
+
| **pnpm** | Package manager + workspace | Fast, disk-efficient, strict hoisting |
|
|
264
|
+
| **tsup** | Bundle libraries | Fast (esbuild), dual ESM+CJS, `.d.ts` generation |
|
|
265
|
+
| **TypeScript 5.6+** | Type checking | Latest language features, strict mode |
|
|
266
|
+
| **Vitest** | Unit + integration tests | Vite-native, fast, TypeScript-first, works in Node + browser |
|
|
267
|
+
| **Playwright** | E2E tests in example apps | Cross-browser, cross-platform |
|
|
268
|
+
| **size-limit** | Bundle size enforcement | CI-blocking on budget violations |
|
|
269
|
+
| **Changesets** | Versioning + changelog | Per-package semver with provenance |
|
|
270
|
+
| **Biome** | Lint + format | 30x faster than ESLint/Prettier, single binary |
|
|
271
|
+
| **Astro Starlight** | Docs site | Fast, SEO-friendly, native MDX |
|
|
272
|
+
| **sigstore** | Release signing | Supply-chain integrity |
|
|
273
|
+
|
|
274
|
+
### Why not Turbopack / Nx / Rush?
|
|
275
|
+
|
|
276
|
+
- **Turbopack** — not stable for libraries yet
|
|
277
|
+
- **Nx** — too heavy for our use case; we don't need task graphs across dozens of apps
|
|
278
|
+
- **Rush** — unnecessary ceremony for a small monorepo
|
|
279
|
+
- **Bun workspaces** — too young, not yet compatible with `changesets`
|
|
280
|
+
|
|
281
|
+
pnpm + tsup + changesets is the sweet spot for publishing libraries in 2025.
|
|
282
|
+
|
|
283
|
+
### Why Biome over ESLint + Prettier?
|
|
284
|
+
|
|
285
|
+
- One binary, one config, 30x faster
|
|
286
|
+
- Zero JS plugin runtime (everything is Rust-native)
|
|
287
|
+
- Reduces CI minutes significantly in a monorepo of 10+ packages
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## Size budgets
|
|
292
|
+
|
|
293
|
+
Enforced in CI via `size-limit`. PRs that exceed the budget are blocked.
|
|
294
|
+
|
|
295
|
+
| Package | Budget (gzipped) | Notes |
|
|
296
|
+
|---|---:|---|
|
|
297
|
+
| `@variantlab/core` | **3.0 KB** | Zero dependencies, pure TS |
|
|
298
|
+
| `@variantlab/react` | **1.5 KB** | Excludes core |
|
|
299
|
+
| `@variantlab/react-native` | **4.0 KB** | Includes debug overlay (tree-shakable in production) |
|
|
300
|
+
| `@variantlab/next` | **2.0 KB** | Excludes core, excludes React |
|
|
301
|
+
| `@variantlab/remix` | **1.5 KB** | |
|
|
302
|
+
| `@variantlab/vue` | **1.5 KB** | |
|
|
303
|
+
| `@variantlab/svelte` | **1.0 KB** | Svelte compiles away most of the runtime |
|
|
304
|
+
| `@variantlab/solid` | **1.0 KB** | |
|
|
305
|
+
| `@variantlab/vanilla` | **0.5 KB** | Just the hook + engine re-export |
|
|
306
|
+
|
|
307
|
+
**Debug overlay is always tree-shaken in production builds.** We use a dev-only import pattern:
|
|
308
|
+
|
|
309
|
+
```ts
|
|
310
|
+
// @variantlab/react-native exports:
|
|
311
|
+
export { VariantDebugOverlay } from "./debug/overlay";
|
|
312
|
+
|
|
313
|
+
// Production usage — bundler drops the whole module:
|
|
314
|
+
import { VariantDebugOverlay } from "@variantlab/react-native";
|
|
315
|
+
{process.env.NODE_ENV !== "production" && <VariantDebugOverlay />}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Additional strategy: the overlay lives in its own entry point `@variantlab/react-native/debug` so users can import it only in dev.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Dependency policy
|
|
323
|
+
|
|
324
|
+
### Runtime dependencies
|
|
325
|
+
|
|
326
|
+
- **Core package**: **zero** runtime dependencies, forever. Enforced by `tools/scripts/check-deps.js` in CI.
|
|
327
|
+
- **Adapter packages**: may list `@variantlab/core` as the only runtime dep. Everything else is peer.
|
|
328
|
+
- **Peer dependencies**: framework itself (React, Vue, etc.) and optional integrations (`react-native-mmkv`, `@react-native-async-storage/async-storage`, etc.).
|
|
329
|
+
|
|
330
|
+
### Dev dependencies
|
|
331
|
+
|
|
332
|
+
- Build tooling (`tsup`, `typescript`, `biome`) is shared at the workspace root.
|
|
333
|
+
- Per-package dev deps are allowed when framework-specific (e.g., `@testing-library/react` in `react`).
|
|
334
|
+
|
|
335
|
+
### Why this matters
|
|
336
|
+
|
|
337
|
+
Every runtime dependency is a supply-chain attack vector, a potential version conflict, and added bundle size. By refusing runtime deps we:
|
|
338
|
+
|
|
339
|
+
- Reduce audit surface to zero for `@variantlab/core`
|
|
340
|
+
- Eliminate dependency version conflicts
|
|
341
|
+
- Enable safe use in any JavaScript runtime
|
|
342
|
+
- Prevent transitive bloat
|
|
343
|
+
|
|
344
|
+
See [`docs/research/bundle-size-analysis.md`](./docs/research/bundle-size-analysis.md) and [`SECURITY.md`](./SECURITY.md).
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Versioning and release strategy
|
|
349
|
+
|
|
350
|
+
- **Semver strict.** Breaking API changes require a major version bump in every affected package.
|
|
351
|
+
- **Changesets** manages per-package versioning. Each PR with a public API change must include a changeset.
|
|
352
|
+
- **Lock-step releases** for `@variantlab/core` and all adapters during 0.x, so users don't mismatch versions.
|
|
353
|
+
- **Provenance**: every npm publish is signed via `npm publish --provenance` and logged to the public Sigstore transparency log.
|
|
354
|
+
- **SBOM**: a CycloneDX SBOM is generated on release and attached to the GitHub release.
|
|
355
|
+
|
|
356
|
+
### Release cadence
|
|
357
|
+
|
|
358
|
+
- **Patch**: as needed, within 48 hours of a verified bug fix
|
|
359
|
+
- **Minor**: every 2-4 weeks in the 0.x phase, then monthly post-1.0
|
|
360
|
+
- **Major**: only with a 30-day RFC period and a migration guide
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Testing strategy
|
|
365
|
+
|
|
366
|
+
| Layer | Tool | Coverage target |
|
|
367
|
+
|---|---|---|
|
|
368
|
+
| Unit (core engine) | Vitest | 95%+ |
|
|
369
|
+
| Unit (adapters) | Vitest + framework testing libs | 90%+ |
|
|
370
|
+
| Integration (example apps) | Playwright | Smoke tests pass |
|
|
371
|
+
| Property-based (hash, assignment) | fast-check (dev only) | Invariants hold |
|
|
372
|
+
| Fuzz (schema validator) | Custom fuzzer | No crashes on malformed input |
|
|
373
|
+
| Compat matrix | GitHub Actions | Node 18/20/22, React 18/19, RN 0.74+ |
|
|
374
|
+
| Bundle size | size-limit | All packages under budget |
|
|
375
|
+
| Type coverage | `tsc --strict` + `typescript-coverage-report` | 100% |
|
|
376
|
+
|
|
377
|
+
**Test philosophy**: every bug fix must come with a regression test. Every public API must have an integration test in an example app.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## CI/CD pipeline
|
|
382
|
+
|
|
383
|
+
### `ci.yml` (on every PR)
|
|
384
|
+
|
|
385
|
+
1. Install pnpm + dependencies
|
|
386
|
+
2. Lint + format check (Biome)
|
|
387
|
+
3. Type check (`tsc --noEmit`)
|
|
388
|
+
4. Unit tests (Vitest)
|
|
389
|
+
5. Build all packages (tsup)
|
|
390
|
+
6. Check bundle sizes (size-limit)
|
|
391
|
+
7. Check zero-dep policy (custom script)
|
|
392
|
+
8. Run example app smoke tests (Playwright)
|
|
393
|
+
9. Generate coverage report
|
|
394
|
+
|
|
395
|
+
### `release.yml` (on merge to main)
|
|
396
|
+
|
|
397
|
+
1. Detect pending changesets
|
|
398
|
+
2. Open or update "Version Packages" PR
|
|
399
|
+
3. On merge, publish with `npm publish --provenance`
|
|
400
|
+
4. Sign release with sigstore
|
|
401
|
+
5. Generate SBOM
|
|
402
|
+
6. Create GitHub release with changelog
|
|
403
|
+
7. Update docs site
|
|
404
|
+
|
|
405
|
+
### `compat-matrix.yml` (nightly)
|
|
406
|
+
|
|
407
|
+
Tests against:
|
|
408
|
+
|
|
409
|
+
- Node 18, 20, 22
|
|
410
|
+
- React 18.2, 18.3, 19.0
|
|
411
|
+
- React Native 0.74, 0.75, 0.76
|
|
412
|
+
- Next.js 14, 15
|
|
413
|
+
- Vue 3.4, 3.5
|
|
414
|
+
- Svelte 4, 5
|
|
415
|
+
- Solid 1.8, 1.9
|
|
416
|
+
|
|
417
|
+
Fails the build if any combination breaks. Prevents silent regressions.
|
|
418
|
+
|
|
419
|
+
### `sigstore.yml` (on release tag)
|
|
420
|
+
|
|
421
|
+
Signs all published packages and updates provenance attestations.
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## See also
|
|
426
|
+
|
|
427
|
+
- [`API.md`](./API.md) — complete TypeScript API surface
|
|
428
|
+
- [`SECURITY.md`](./SECURITY.md) — threat model and mitigations
|
|
429
|
+
- [`docs/research/bundle-size-analysis.md`](./docs/research/bundle-size-analysis.md) — how we hit the size budgets
|
|
430
|
+
- [`docs/design/api-philosophy.md`](./docs/design/api-philosophy.md) — API design decisions
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# Contributing to variantlab
|
|
2
|
+
|
|
3
|
+
Thank you for considering a contribution. variantlab is currently in Phase 0 (Foundation) — we are actively seeking contributors who want to shape the API before a single line of code is written.
|
|
4
|
+
|
|
5
|
+
## Table of contents
|
|
6
|
+
|
|
7
|
+
- [Current phase](#current-phase)
|
|
8
|
+
- [Ways to contribute](#ways-to-contribute)
|
|
9
|
+
- [Code of conduct](#code-of-conduct)
|
|
10
|
+
- [Before you start coding](#before-you-start-coding)
|
|
11
|
+
- [Development setup](#development-setup)
|
|
12
|
+
- [Pull request process](#pull-request-process)
|
|
13
|
+
- [Commit conventions](#commit-conventions)
|
|
14
|
+
- [Changesets](#changesets)
|
|
15
|
+
- [Documentation changes](#documentation-changes)
|
|
16
|
+
- [Security disclosures](#security-disclosures)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Current phase
|
|
21
|
+
|
|
22
|
+
**We are in Phase 0: Foundation.** This means:
|
|
23
|
+
|
|
24
|
+
- **No production code exists yet.** If you open a PR adding a `src/` file, we will close it with a pointer to this document.
|
|
25
|
+
- All work is on documentation, design, and API surface.
|
|
26
|
+
- The most valuable contributions right now are reviews, critiques, and proposals on the existing docs.
|
|
27
|
+
|
|
28
|
+
When Phase 1 begins, this document will be updated to describe the coding workflow.
|
|
29
|
+
|
|
30
|
+
See [`ROADMAP.md`](./ROADMAP.md) for the current phase and [`docs/phases/phase-0-foundation.md`](./docs/phases/phase-0-foundation.md) for detailed Phase 0 work.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Ways to contribute
|
|
35
|
+
|
|
36
|
+
### 1. Review the API surface
|
|
37
|
+
|
|
38
|
+
Read [`API.md`](./API.md) and open a GitHub discussion with:
|
|
39
|
+
|
|
40
|
+
- Parts that feel awkward
|
|
41
|
+
- Missing functionality
|
|
42
|
+
- Naming concerns
|
|
43
|
+
- Type-safety gaps
|
|
44
|
+
- Comparisons to how other tools solve the same problem
|
|
45
|
+
|
|
46
|
+
### 2. Review the threat model
|
|
47
|
+
|
|
48
|
+
Read [`SECURITY.md`](./SECURITY.md) and open a discussion if you spot:
|
|
49
|
+
|
|
50
|
+
- Missing threats
|
|
51
|
+
- Weak mitigations
|
|
52
|
+
- Better alternatives to the proposed designs
|
|
53
|
+
- Privacy concerns we missed
|
|
54
|
+
|
|
55
|
+
### 3. Propose a framework adapter
|
|
56
|
+
|
|
57
|
+
If your favorite framework is not yet planned, open a discussion with:
|
|
58
|
+
|
|
59
|
+
- Framework name and version
|
|
60
|
+
- How experiments would be consumed (hooks, components, composables, signals)
|
|
61
|
+
- SSR considerations
|
|
62
|
+
- Sample code using the planned API
|
|
63
|
+
- Estimated bundle size
|
|
64
|
+
|
|
65
|
+
We will evaluate and potentially promote it to a future phase.
|
|
66
|
+
|
|
67
|
+
### 4. Contribute research
|
|
68
|
+
|
|
69
|
+
Add new content to `docs/research/`:
|
|
70
|
+
|
|
71
|
+
- Additional competitor analyses
|
|
72
|
+
- Framework-specific SSR quirks we missed
|
|
73
|
+
- Novel debug overlay patterns from other tools
|
|
74
|
+
- Security research papers relevant to client-side experimentation
|
|
75
|
+
|
|
76
|
+
### 5. Test the proposed APIs by hand
|
|
77
|
+
|
|
78
|
+
The best feedback comes from trying to write real application code against the proposed API. Open a discussion with a code sample showing what works and what doesn't.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Code of conduct
|
|
83
|
+
|
|
84
|
+
We follow a simple principle: **be kind, be specific, be useful**. Disrespectful, condescending, or harassing behavior will not be tolerated. We follow the [Contributor Covenant v2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
|
|
85
|
+
|
|
86
|
+
Report violations to `conduct@variantlab.dev`.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Before you start coding
|
|
91
|
+
|
|
92
|
+
**Phase 0 has no coding.** Once Phase 1 begins:
|
|
93
|
+
|
|
94
|
+
1. Check the [issue tracker](https://github.com/variantlab/variantlab/issues) for an open issue matching your idea, or create one
|
|
95
|
+
2. Wait for a maintainer to label it `accepted` before you start work
|
|
96
|
+
3. Comment on the issue to claim it
|
|
97
|
+
4. Open a draft PR early so maintainers can guide you
|
|
98
|
+
|
|
99
|
+
This process prevents wasted effort on changes we can't merge.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Development setup
|
|
104
|
+
|
|
105
|
+
*(Phase 1+ only — ignored during Phase 0.)*
|
|
106
|
+
|
|
107
|
+
### Prerequisites
|
|
108
|
+
|
|
109
|
+
- Node 18.17+ (we test 18, 20, 22)
|
|
110
|
+
- pnpm 9+
|
|
111
|
+
- Git
|
|
112
|
+
|
|
113
|
+
### Clone and install
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
git clone https://github.com/variantlab/variantlab.git
|
|
117
|
+
cd variantlab
|
|
118
|
+
pnpm install
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Build everything
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pnpm build
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Run tests
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
pnpm test # all packages
|
|
131
|
+
pnpm test --filter=@variantlab/core # one package
|
|
132
|
+
pnpm test:watch # watch mode
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Check bundle sizes
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
pnpm size
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Lint + format
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
pnpm check # biome check
|
|
145
|
+
pnpm fix # biome check --apply
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Run example app
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
pnpm --filter=example-expo-router start
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Pull request process
|
|
157
|
+
|
|
158
|
+
1. **Fork** the repo and create a feature branch from `main`
|
|
159
|
+
2. **Write tests** for your change. Every bug fix needs a regression test. Every feature needs at least one integration test.
|
|
160
|
+
3. **Run the full test suite locally** before pushing: `pnpm build && pnpm test && pnpm size && pnpm check`
|
|
161
|
+
4. **Add a changeset** if your PR touches public APIs: `pnpm changeset`
|
|
162
|
+
5. **Update documentation** — relevant `API.md`, `README.md`, `docs/` files
|
|
163
|
+
6. **Open a PR** with:
|
|
164
|
+
- A clear title (matches a commit convention — see below)
|
|
165
|
+
- A description explaining *what* and *why*
|
|
166
|
+
- Links to related issues/discussions
|
|
167
|
+
- Screenshots or GIFs for UI changes
|
|
168
|
+
- A checklist of changed packages
|
|
169
|
+
7. **Respond to review feedback** promptly
|
|
170
|
+
8. **Rebase on main** (do not merge main into your branch)
|
|
171
|
+
9. Once approved, a maintainer will merge via squash
|
|
172
|
+
|
|
173
|
+
### What we look for in review
|
|
174
|
+
|
|
175
|
+
- **API consistency** — does this match patterns elsewhere in variantlab?
|
|
176
|
+
- **Bundle size impact** — is the change within budget?
|
|
177
|
+
- **Type safety** — are the new types as strict as possible?
|
|
178
|
+
- **Test quality** — are the tests isolated, deterministic, fast?
|
|
179
|
+
- **Documentation** — is the change documented where users would look?
|
|
180
|
+
- **Security implications** — does this change any assumption in `SECURITY.md`?
|
|
181
|
+
- **SSR correctness** — does this change break any SSR guarantee?
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## Commit conventions
|
|
186
|
+
|
|
187
|
+
We follow [Conventional Commits](https://www.conventionalcommits.org/):
|
|
188
|
+
|
|
189
|
+
```
|
|
190
|
+
<type>(<scope>): <description>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Types:
|
|
194
|
+
|
|
195
|
+
- `feat` — new feature
|
|
196
|
+
- `fix` — bug fix
|
|
197
|
+
- `docs` — documentation changes
|
|
198
|
+
- `refactor` — code change that neither adds a feature nor fixes a bug
|
|
199
|
+
- `perf` — performance improvement
|
|
200
|
+
- `test` — adding or updating tests
|
|
201
|
+
- `chore` — build, CI, tooling
|
|
202
|
+
- `security` — security fix (always shipped in the next patch)
|
|
203
|
+
|
|
204
|
+
Scopes:
|
|
205
|
+
|
|
206
|
+
- `core`, `react`, `react-native`, `next`, `remix`, `vue`, `svelte`, `solid`, `astro`, `nuxt`, `cli`, `devtools`, `docs`, `ci`
|
|
207
|
+
|
|
208
|
+
Examples:
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
feat(react): add useRouteExperiments hook
|
|
212
|
+
fix(core): handle missing userId in stickyHash
|
|
213
|
+
docs(api): document VariantErrorBoundary props
|
|
214
|
+
security(core): prevent prototype pollution in schema validator
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## Changesets
|
|
220
|
+
|
|
221
|
+
Every PR that changes public APIs must include a changeset. Run:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
pnpm changeset
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Follow the prompts:
|
|
228
|
+
|
|
229
|
+
1. Select changed packages (space to toggle, enter to confirm)
|
|
230
|
+
2. Select bump type (patch, minor, major)
|
|
231
|
+
3. Write a description (will appear in CHANGELOG)
|
|
232
|
+
|
|
233
|
+
Commit the generated file with your PR.
|
|
234
|
+
|
|
235
|
+
PRs that only change docs, tests, or internal tooling do not need changesets.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Documentation changes
|
|
240
|
+
|
|
241
|
+
Documentation lives in two places:
|
|
242
|
+
|
|
243
|
+
- **Root markdown files** (`README.md`, `API.md`, etc.) — authoritative reference
|
|
244
|
+
- **Docs site** (`apps/docs/`) — long-form guides, tutorials, recipes
|
|
245
|
+
|
|
246
|
+
Changes to the API must first land in `API.md`. Changes to features must update the relevant `docs/features/*.md` spec. Changes to architecture must update `ARCHITECTURE.md`.
|
|
247
|
+
|
|
248
|
+
We prefer small, focused doc PRs over large reorganizations.
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Security disclosures
|
|
253
|
+
|
|
254
|
+
**Do not open public issues for security vulnerabilities.** See [`SECURITY.md`](./SECURITY.md) for the private reporting process.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Questions?
|
|
259
|
+
|
|
260
|
+
Open a GitHub discussion or reach out on our community channel (once established). We read every message.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
Thank you for helping build variantlab. Every contribution — code, docs, reviews, ideas — makes the project better.
|