Thanks for your interest in @dwk/workers — a pnpm-workspace monorepo of
composable npm packages that each implement an open web standard and run as
Cloudflare Workers on an end
user's own account. Start with the README for what the
project is; this document covers how to work on it.
- The specs are the requirements. Before changing behaviour, read the
package's spec under
spec/packages/and the cross-cutting documents (spec/composition-contract.md,spec/non-functional-requirements.md). If a change contradicts a spec, update the spec in the same PR or open an issue first — don't let code and spec drift apart. The broader requirements thread is issue #1. - Discuss significant changes first. For new packages, new dependencies, or architectural changes, open an issue before writing code.
- License: contributions are accepted under the project's ISC license.
- Node.js >= 22 (
@dwk/cf-shims' built-innode:sqliteshims need it; CI pins Node 24) - pnpm 10 (the exact version is pinned via
packageManagerinpackage.json;corepack enablewill pick it up)
git clone https://github.com/davidwkeith/workers.git
cd workers
pnpm install
pnpm build # tsc per package — required before tests (see below)
pnpm test # full vitest suite, Node + workerd projectsBuild before test. Package tests import sibling
@dwk/*workspace deps through theirexportsmaps, which point atdist/. A fresh clone (or a change to a dependency package) needspnpm buildbefore the dependent package's tests will pass.
Windows: enable symlinks. The repo tracks
AGENTS.mdas a symlink toCLAUDE.md. Withoutcore.symlinks=true(requires Developer Mode or an admingit clone), Windows checkouts silently materialize it as a plain text file containingCLAUDE.mdinstead of the real guidance.
-
Branch off
main. -
Read the spec for the package(s) you're touching.
-
Make the change with colocated tests. Every behaviour change needs a test in the package's
src/*.test.ts. -
Run the CI gate locally, in the same order CI does:
pnpm lint && pnpm format:check && pnpm typecheck && pnpm build && pnpm test
All five must pass —
.github/workflows/ci.ymlruns exactly this sequence.pnpm formatfixes formatting violations in place.This gate does not exercise the Docker image's bundling step, so a change to
@dwk/cf-shims's public exports (or how@dwk/serverresolves them at bundle time) can pass lint/typecheck/build/test and still breakpackages/server/scripts/bundle.mjs— e.g. an ESM/CJS resolution mismatch (require.resolve()can't see animport-onlyexportsentry) only surfaces there. If your change touches that surface, also run:pnpm --filter @dwk/server bundle
-
Record a changeset for any user-visible change to a publishable package:
pnpm changeset # pick affected packages + bump typeCommit the generated
.changeset/*.mdalongside the code (commit: falseis set — changesets does not auto-commit). Repo-only changes (CI, docs, scripts) don't need one. -
Write commit messages (and the PR title) in Conventional Commits style, matching the history:
<type>(<scope>): <subject>— lowercase type, subject not capitalized, no trailing period. Common types:feat,fix,chore,docs,debug. Scope is the package name minus the@dwk/prefix (fix(solid-pod): …), a comma-separated list for several packages (fix(store,solid-pod): …), or omitted for a repo-wide change (chore: …,docs: …).✅
fix(solid-pod): strip client-forged ldp:contains from container PUT❌Fix solid-pod: strip client-forged ldp:contains from container PUT— capitalized, and a bare colon instead of a(scope)❌Add AGENTS.md symlink to CLAUDE.md— no type prefix at allThis repo squash-merges PRs, so the PR title becomes the permanent history entry — get the title right, not just the commit messages on the branch.
-
Open a pull request from
.github/PULL_REQUEST_TEMPLATE.md. GitHub pre-fills this body automatically when a PR is opened against the repo; keep its section headings (Summary,Packages affected,Checklist) verbatim instead of renaming or collapsing them into free-form prose (e.g. don't renameChecklisttoVerification). Tick each checklist box, or leave it unchecked with a one-line reason if it doesn't apply — don't delete items that don't apply. Keep PRs focused and reference the issue they address. -
Resolve every review conversation before merging. Push a fix and resolve the thread, or reply explaining why no change is needed and then resolve it. A PR isn't ready to merge with outstanding unresolved threads.
This is a multi-project vitest setup — always scope with --project when
targeting a subset (a bare file/name filter errors against projects that don't
match):
# One package's tests, by its vitest project name
pnpm test --project @dwk/dpop
# A single test file (filename substring) within a package
pnpm test --project @dwk/rdf jsonld
# A single test by name
pnpm test --project @dwk/dpop -t "verifies a valid proof"
# Watch mode / coverage
pnpm test:watch
pnpm test:coverage
# Build or typecheck a single package
pnpm --filter @dwk/wac build
pnpm --filter @dwk/wac typecheckEach package's vitest.config.ts picks one of two environments — get this
right when adding or moving tests:
- Pure libs run under Node (
environment: "node"): they take plain-data inputs and must need no Workers runtime (e.g.@dwk/dpop,@dwk/rdf,@dwk/wac,@dwk/oauth,@dwk/http-signatures,@dwk/calendar). - Runtime/binding-bound packages run under
workerdvia@cloudflare/vitest-pool-workers(e.g.@dwk/store,@dwk/solid-pod,@dwk/activitypub,@dwk/atproto-pds). These keep their Miniflare setup in atest-harness.tsthat is excluded from the build and the publishedfiles.
- Formatting: Prettier — semicolons, double quotes, trailing commas
(
all), 80-column print width.pnpm format:checkis a CI gate. - TypeScript is strict (
tsconfig.base.json):strict,noUncheckedIndexedAccess,noUnusedLocals/noUnusedParameters,verbatimModuleSyntax,isolatedModules. Useimport typefor type-only imports. ESLint flags unused variables unless prefixed with_. - ESM-only packages, tree-shakeable, fully typed,
"sideEffects": false,exportsmaps pointing atdist/. Dependencies are minimized and pinned to exact versions; internal workspace deps use"workspace:*". src/index.tsis the public surface and carries a doc comment stating the package's role, whether it is pure/protocol-agnostic, and a@see spec/packages/<name>.mdpointer. Match this style.
The full rules live in spec/composition-contract.md
and spec/non-functional-requirements.md;
the ones most often relevant to a PR:
- Endpoint packages export a factory
createX(config): (request, env, ctx) => Promise<Response>mountable under a path prefix. Packages MUST NOT read the global environment directly — all config is injected — and MUST fail loudly at startup if a required binding is missing. - Cloudflare specifics are confined to
@dwk/storeand the endpoint packages. Cross-standard libs (@dwk/rdf,@dwk/dpop,@dwk/oauth, …) MUST stay free of IndieWeb/Solid assumptions and unit-test without a Workers runtime. This is a hard constraint, not a preference. - KV MUST NEVER be used for authz or anything where staleness is a correctness/security bug; authoritative state lives only in strongly-consistent stores (DO SQLite, R2, D1 with session consistency).
- Stay within the Worker runtime budget (128 MB memory, 3/10 MB script, 1 s startup). Stream R2 bodies through the Worker — never buffer a full blob in a Durable Object.
- Write (or update) its spec in
spec/packages/<name>.mdfirst. - Follow the standard per-package layout (see the tree in
.claude/skills/add-package/SKILL.md):src/with colocated*.test.ts,tsconfig.json+tsconfig.build.json,vitest.config.ts,README.md. - Pick the correct vitest environment (Node vs
workerd, above). - Give it a catalog decision: either a worker entry in
catalog.jsonor alibrariesexclusion —pnpm catalog:checkenforces this for every publishable package. Workerids are forever-stable once added. - Add its row to
conformance/status.json; the release gate (pnpm release:gate) cross-checks it before any stable publish.
You never publish from a laptop. Versioning is via Changesets pre mode
(tag beta), and publishing happens only through the gated Release GitHub
Actions workflow. All you need to do in a PR is record a changeset; see
RELEASING.md for the full runbook, including the dist-tag
gotchas.
Stable (>=1.0.0) releases are additionally gated on conformance: a package
cannot publish a stable version until its suites in
conformance/status.json are passing or not-applicable
(see spec/conformance-and-testing.md).
- Open a GitHub issue with reproduction steps and the package + version affected.
- For security vulnerabilities, please do not open a public issue — use GitHub private vulnerability reporting instead.