Contributing
Contributing
Monorepo Structure
workkit/ packages/ # Core packages types/ # @workkit/types -- shared types, no deps errors/ # @workkit/errors -- error hierarchy, no deps env/ # @workkit/env -- env validation d1/ # @workkit/d1 -- D1 database wrapper kv/ # @workkit/kv -- KV store wrapper r2/ # @workkit/r2 -- R2 object storage wrapper queue/ # @workkit/queue -- Queue producer/consumer cache/ # @workkit/cache -- Cache API wrapper do/ # @workkit/do -- Durable Objects helpers cron/ # @workkit/cron -- Cron task router crypto/ # @workkit/crypto -- WebCrypto wrappers auth/ # @workkit/auth -- JWT, sessions, passwords ratelimit/ # @workkit/ratelimit -- Rate limiting strategies ai/ # @workkit/ai -- Workers AI client ai-gateway/ # @workkit/ai-gateway -- Multi-provider gateway api/ # @workkit/api -- API definition and routing testing/ # @workkit/testing -- Mock bindings cli/ # @workkit/cli -- CLI tool integrations/ # Framework adapters hono/ # @workkit/hono astro/ # @workkit/astro remix/ # @workkit/remix tooling/ # Internal build tools docs/ # Documentation e2e/ # End-to-end testsPrerequisites
- Bun >= 1.2.0 (package manager and runtime)
- Node.js >= 18 (for some tooling)
- Git
Getting Started
# Clone the repositorygit clone https://github.com/your-org/workkit.gitcd workkit
# Install dependenciesbun install
# Build all packagesbun run build
# Run all testsbun run test
# Type checkbun run typecheck
# Lintbun run lintDevelopment Workflow
Running a Single Package
# Build one packagecd packages/kvbun run build
# Test one packagecd packages/kvbun testUsing Turbo
The monorepo uses Turborepo for build orchestration:
# Build everything (with caching)bun run build
# Test everythingbun run test
# Type check everythingbun run typecheck
# These respect the dependency graph -- types and errors build firstRunning E2E Tests
bun run test:e2ePackage Conventions
File Structure
Every package follows this layout:
packages/my-package/ src/ index.ts # Public API -- re-exports only types.ts # Type definitions my-feature.ts # Implementation tests/ my-feature.test.ts package.json tsconfig.jsonindex.ts is the Public API
The index.ts file should only contain re-exports. All implementation goes in separate files:
// Good -- index.tsexport { myFunction } from './my-feature'export type { MyType } from './types'
// Bad -- don't put implementation in index.tsType-Only Exports
Use export type for types to enable proper tree-shaking:
export type { MyType, MyOptions } from './types'export { myFunction } from './my-feature'Error Handling
- Use errors from
@workkit/errors— do not create ad-hoc error classes - All binding wrappers should throw
BindingNotFoundErrorfor null bindings - Classify errors with context when possible
import { BindingNotFoundError } from '@workkit/errors'
export function myWrapper(binding: SomeBinding) { if (!binding) { throw new BindingNotFoundError('SomeBinding binding is null or undefined') } // ...}Testing
- Use Vitest for all tests
- Use
@workkit/testingmocks for integration tests - Test both success and error paths
- Test edge cases (null bindings, empty inputs, boundary values)
import { describe, it, expect } from 'vitest'import { createMockKV } from '@workkit/testing'
describe('myFeature', () => { it('handles the happy path', async () => { // ... })
it('throws on null binding', () => { expect(() => myWrapper(null as any)).toThrow('BindingNotFoundError') })})Documentation
- Every exported function should have a JSDoc comment with
@example - Types should have doc comments explaining their purpose
- Keep the API reference (
docs/api-reference.md) up to date when adding exports
Adding a New Package
- Create the directory:
mkdir -p packages/my-package/src- Create
package.json:
{ "name": "@workkit/my-package", "version": "0.0.0", "type": "module", "main": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "scripts": { "build": "bunup src/index.ts --dts", "test": "vitest run", "typecheck": "tsc --noEmit" }, "peerDependencies": { "@workkit/types": "workspace:*", "@workkit/errors": "workspace:*" }}- Create
tsconfig.json:
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "dist", "rootDir": "src" }, "include": ["src"]}-
Create
src/index.tswith your public API. -
Add the package to the workspace (it is auto-detected from
packages/*in the rootpackage.json). -
Update
docs/api-reference.mdwith the new package’s exports.
Changesets
We use Changesets for versioning:
# Create a changesetbun run changeset
# Version packages (CI usually does this)bun run version-packages
# Publish (CI usually does this)bun run releaseWhen creating a changeset:
patch— Bug fixes, documentationminor— New features, non-breaking additionsmajor— Breaking changes
Manually backfilling a missing GitHub release
The release workflow creates GitHub releases serially in its own step (so the multi-package race that bit us in #77 can’t recur). The step is idempotent — re-running the Release workflow on the same merge commit only fills in releases that don’t yet exist.
If you ever need to create a single release by hand (e.g. the gh release create call inside the workflow failed for one package due to a transient API hiccup):
PKG=@workkit/agentVER=0.2.0TAG="${PKG}@${VER}"
# 1. Push the tag if `changeset publish` didn't (rare — it normally does).if ! git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then git tag "$TAG" "$(git rev-parse HEAD)" git push origin "$TAG"fi
# 2. Pull just this version's section out of the package CHANGELOG.PKGDIR=packages/agent # or integrations/<name>NOTES=$(mktemp)awk -v ver="$VER" ' /^## / { if (in_section) exit; if ($2 == ver) { in_section = 1; next } } in_section' "$PKGDIR/CHANGELOG.md" > "$NOTES"
# 3. Create the release.gh release create "$TAG" --title "$TAG" --notes-file "$NOTES"Code Style
- Biome for linting and formatting:
bun run lint - No semicolons (Biome config)
- Single quotes
- Tabs for indentation
- Explicit return types on exported functions
- Prefer
constassertions and discriminated unions
Design Principles
-
Each package is independent. No circular dependencies. Packages at the same layer should not depend on each other.
-
Standard Schema over vendor lock-in. Env validation accepts any Standard Schema validator. We do not re-export or depend on Zod, Valibot, or ArkType.
-
Factory + Options pattern. Every binding wrapper uses a factory function that returns a typed client. Options are always optional with sensible defaults.
-
Errors carry retry guidance. Never force consumers to guess whether an error is retryable or what strategy to use.
-
Raw access escape hatch. Every wrapper exposes
.rawfor the underlying Cloudflare binding. We do not hide the platform. -
Tree-shakeable. No barrel files that pull in everything. Import what you use.