Skip to content

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 tests

Prerequisites

  • Bun >= 1.2.0 (package manager and runtime)
  • Node.js >= 18 (for some tooling)
  • Git

Getting Started

Terminal window
# Clone the repository
git clone https://github.com/your-org/workkit.git
cd workkit
# Install dependencies
bun install
# Build all packages
bun run build
# Run all tests
bun run test
# Type check
bun run typecheck
# Lint
bun run lint

Development Workflow

Running a Single Package

Terminal window
# Build one package
cd packages/kv
bun run build
# Test one package
cd packages/kv
bun test

Using Turbo

The monorepo uses Turborepo for build orchestration:

Terminal window
# Build everything (with caching)
bun run build
# Test everything
bun run test
# Type check everything
bun run typecheck
# These respect the dependency graph -- types and errors build first

Running E2E Tests

Terminal window
bun run test:e2e

Package 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.json

index.ts is the Public API

The index.ts file should only contain re-exports. All implementation goes in separate files:

// Good -- index.ts
export { myFunction } from './my-feature'
export type { MyType } from './types'
// Bad -- don't put implementation in index.ts

Type-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 BindingNotFoundError for 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/testing mocks 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

  1. Create the directory:
Terminal window
mkdir -p packages/my-package/src
  1. 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:*"
}
}
  1. Create tsconfig.json:
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}
  1. Create src/index.ts with your public API.

  2. Add the package to the workspace (it is auto-detected from packages/* in the root package.json).

  3. Update docs/api-reference.md with the new package’s exports.

Changesets

We use Changesets for versioning:

Terminal window
# Create a changeset
bun run changeset
# Version packages (CI usually does this)
bun run version-packages
# Publish (CI usually does this)
bun run release

When creating a changeset:

  • patch — Bug fixes, documentation
  • minor — New features, non-breaking additions
  • major — 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):

Terminal window
PKG=@workkit/agent
VER=0.2.0
TAG="${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 const assertions and discriminated unions

Design Principles

  1. Each package is independent. No circular dependencies. Packages at the same layer should not depend on each other.

  2. 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.

  3. Factory + Options pattern. Every binding wrapper uses a factory function that returns a typed client. Options are always optional with sensible defaults.

  4. Errors carry retry guidance. Never force consumers to guess whether an error is retryable or what strategy to use.

  5. Raw access escape hatch. Every wrapper exposes .raw for the underlying Cloudflare binding. We do not hide the platform.

  6. Tree-shakeable. No barrel files that pull in everything. Import what you use.