@danieljvdm/dev-kit 0.11.3 → 0.12.0
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 +70 -76
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/dev-kit/SKILL.md +58 -46
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +49 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +0 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +16 -14
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +170 -120
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
package/README.md
CHANGED
|
@@ -198,9 +198,9 @@ tool versions. A project-local process lock also prevents concurrent applies.
|
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
- `dev-kit` installs guidance for operating the toolkit itself.
|
|
201
|
-
- `effect` expands to the `effect-ts`
|
|
202
|
-
|
|
203
|
-
|
|
201
|
+
- `effect` expands to the package-guidance `effect-ts` bootstrap, the
|
|
202
|
+
opinionated `effect-architecture-audit`, and `build-effect-apis` for shared
|
|
203
|
+
HTTP contracts, Effect Atom clients, TanStack Start, and Cloudflare Workers.
|
|
204
204
|
- Prefer individual external skills such as `workers-best-practices` and
|
|
205
205
|
`wrangler`, selected after scanning the project for relevant technologies.
|
|
206
206
|
- `serve-sim` selects the approved Evan Bacon simulator skill directly.
|
|
@@ -284,9 +284,35 @@ worktree converges its own copy during install while the project-owned
|
|
|
284
284
|
|
|
285
285
|
## Vite+ quality setup
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
The repository always owns `vite.config.ts`. Compose Dev Kit's quality defaults
|
|
288
|
+
from that project-owned config, then opt into the hardened GitHub Actions
|
|
288
289
|
workflow independently:
|
|
289
290
|
|
|
291
|
+
```ts
|
|
292
|
+
import { createRecommendedVitePlusConfig } from "@danieljvdm/dev-kit/vite-plus";
|
|
293
|
+
import { defineConfig } from "vite-plus";
|
|
294
|
+
|
|
295
|
+
const recommended = createRecommendedVitePlusConfig({
|
|
296
|
+
ignorePatterns: ["apps/api/worker-configuration.d.ts", "apps/web/src/routeTree.gen.ts"],
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export default defineConfig({
|
|
300
|
+
...recommended,
|
|
301
|
+
// Project-owned Vite, test, build, and framework options stay local.
|
|
302
|
+
server: { port: 5173 },
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Spread the returned top-level config before local options. When overriding a
|
|
307
|
+
`fmt`, `lint`, `run`, or `staged` block, spread that returned block as well so
|
|
308
|
+
its defaults remain composed.
|
|
309
|
+
|
|
310
|
+
The factory configures `vp staged`, matching Oxlint/Oxfmt ignores for Dev Kit's
|
|
311
|
+
tool-owned paths, and separate `vp run check` and pure `vp run typecheck` tasks.
|
|
312
|
+
Project and framework-generated paths belong in `ignorePatterns` as shown;
|
|
313
|
+
custom harness target paths belong there too. Dev Kit does not grow a global
|
|
314
|
+
framework ignore list.
|
|
315
|
+
|
|
290
316
|
```jsonc
|
|
291
317
|
{
|
|
292
318
|
"include": ["dev-kit", "effect"],
|
|
@@ -295,7 +321,6 @@ workflow independently:
|
|
|
295
321
|
"vitePlus": {
|
|
296
322
|
"hooks": { "enabled": true },
|
|
297
323
|
"quality": {
|
|
298
|
-
"config": { "enabled": true },
|
|
299
324
|
"workflow": { "enabled": true },
|
|
300
325
|
},
|
|
301
326
|
},
|
|
@@ -303,47 +328,28 @@ workflow independently:
|
|
|
303
328
|
}
|
|
304
329
|
```
|
|
305
330
|
|
|
306
|
-
`quality.
|
|
307
|
-
|
|
308
|
-
config, scripts, or TypeScript topology. Both require direct
|
|
331
|
+
`quality.workflow.enabled` owns `.github/workflows/check.yml` but never reads,
|
|
332
|
+
rewrites, adopts, or removes `vite.config.ts`. Workflow setup requires direct
|
|
309
333
|
`@danieljvdm/dev-kit`, `vite-plus`, `effect`, `@effect/tsgo`, and native
|
|
310
334
|
TypeScript dependencies with `setup.effectTsgo.enabled`. The installed Vite+
|
|
311
335
|
must satisfy Dev Kit's peer range.
|
|
312
336
|
|
|
313
|
-
|
|
314
|
-
`vp staged`, and defines separate `vp run check` and pure `vp run typecheck`
|
|
315
|
-
tasks. It rejects conflicting root scripts. The default `single-project`
|
|
316
|
-
strategy requires a root `tsconfig.json` without project references. Workspaces
|
|
317
|
-
can instead select Vite Task orchestration explicitly:
|
|
337
|
+
Workspaces select bounded, dependency-ordered typechecking in their config:
|
|
318
338
|
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
"enabled": true,
|
|
327
|
-
"typecheck": {
|
|
328
|
-
"strategy": "workspace",
|
|
329
|
-
"concurrency": 4,
|
|
330
|
-
"packages": ["apps/web", "packages/core"],
|
|
331
|
-
},
|
|
332
|
-
},
|
|
333
|
-
},
|
|
339
|
+
```ts
|
|
340
|
+
export default defineConfig(
|
|
341
|
+
createRecommendedVitePlusConfig({
|
|
342
|
+
typecheck: {
|
|
343
|
+
strategy: "workspace",
|
|
344
|
+
concurrency: 4,
|
|
345
|
+
packages: ["apps/web", "packages/core"],
|
|
334
346
|
},
|
|
335
|
-
},
|
|
336
|
-
|
|
347
|
+
}),
|
|
348
|
+
);
|
|
337
349
|
```
|
|
338
350
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
dependency order, caching, `--fail-if-no-match`, and bounded concurrency.
|
|
342
|
-
Project-reference builds remain custom because their `.tsbuildinfo` inputs and
|
|
343
|
-
outputs are repository-specific.
|
|
344
|
-
|
|
345
|
-
Repositories with a custom Vite config can still adopt only the hardened CI
|
|
346
|
-
workflow and declare repository-specific preparation and typecheck commands:
|
|
351
|
+
Each listed package must expose a pure `typecheck` script. Repositories can
|
|
352
|
+
also declare workflow-specific preparation and typecheck commands:
|
|
347
353
|
|
|
348
354
|
```jsonc
|
|
349
355
|
{
|
|
@@ -370,16 +376,12 @@ workflow and declare repository-specific preparation and typecheck commands:
|
|
|
370
376
|
}
|
|
371
377
|
```
|
|
372
378
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
Bun version, follows the maintained `setup-bun@v2` tag, and names the current
|
|
380
|
-
`setup-vp` release because that action's `v1` tag is frozen. Existing
|
|
381
|
-
workflows remain user-owned until their rendered content matches exactly—Dev
|
|
382
|
-
Kit never merges YAML. See the primary
|
|
379
|
+
The workflow performs one frozen, script-suppressed install, runs
|
|
380
|
+
`dev-kit apply --locked`, and only then runs preparation, formatting, linting,
|
|
381
|
+
tests, and typechecking. Its default typecheck command is `vp run typecheck`;
|
|
382
|
+
`workflow.typecheck` replaces it. Existing workflows remain user-owned until
|
|
383
|
+
their rendered content matches exactly—Dev Kit never merges YAML. See the
|
|
384
|
+
primary
|
|
383
385
|
[`setup-vp` versioning guidance](https://github.com/voidzero-dev/setup-vp#versioning),
|
|
384
386
|
[Vite+ install guide](https://viteplus.dev/guide/install), and
|
|
385
387
|
[Vite Task run guide](https://viteplus.dev/guide/run) when maintaining the
|
|
@@ -427,7 +429,7 @@ Pin the compatible packages in the consuming project:
|
|
|
427
429
|
{
|
|
428
430
|
"devDependencies": {
|
|
429
431
|
"@danieljvdm/dev-kit": "^0.2.0",
|
|
430
|
-
"@effect/tsgo": "0.
|
|
432
|
+
"@effect/tsgo": "0.33.0",
|
|
431
433
|
"typescript": "7.0.2",
|
|
432
434
|
},
|
|
433
435
|
}
|
|
@@ -477,8 +479,16 @@ bun x dev-kit add @tanstack/ai#ai-core
|
|
|
477
479
|
That writes `@tanstack/ai#ai-core` to `dev-kit.jsonc` and, unless
|
|
478
480
|
`--no-apply` is passed, installs it through the normal ownership-safe sync
|
|
479
481
|
path. The qualifier prevents ambiguity when two dependencies publish the same
|
|
480
|
-
skill name
|
|
481
|
-
|
|
482
|
+
skill name, and the installed output carries it too: the copied directory is
|
|
483
|
+
named by flattening the package name (drop `@`, turn every other
|
|
484
|
+
non-alphanumeric run into one dash) and appending the skill name, so
|
|
485
|
+
`@tanstack/ai#ai-core` installs as `tanstack-ai-ai-core`. Agent harnesses
|
|
486
|
+
identify a project skill by its directory name, so the copied `SKILL.md`
|
|
487
|
+
frontmatter `name:` is rewritten to that same install name; all other content
|
|
488
|
+
is copied verbatim. Symlink-mode targets link straight into `node_modules`, so
|
|
489
|
+
only the link itself carries the qualified name while the linked frontmatter
|
|
490
|
+
keeps the upstream bare name. Two selected skills that would both write the
|
|
491
|
+
same destination are rejected before any output is changed.
|
|
482
492
|
|
|
483
493
|
The initial compatibility boundary is intentionally small and deterministic:
|
|
484
494
|
|
|
@@ -496,9 +506,9 @@ and frontmatter-name invariants expected by Agent Skills targets. Dev Kit does
|
|
|
496
506
|
not rewrite nested names or ask Intent to manage agent configuration.
|
|
497
507
|
|
|
498
508
|
The project `dev-kit.lock.json` records the selected package name, installed
|
|
499
|
-
version, skill name, and
|
|
500
|
-
package-version or skill-content drift.
|
|
501
|
-
package or substitutes a registry version.
|
|
509
|
+
version, original bare skill name, and the `node_modules` content digest.
|
|
510
|
+
`apply --locked` therefore rejects package-version or skill-content drift.
|
|
511
|
+
Dev Kit never downloads a missing package or substitutes a registry version.
|
|
502
512
|
|
|
503
513
|
See TanStack's
|
|
504
514
|
[Agent Skills documentation](https://tanstack.com/ai/latest/docs/getting-started/agent-skills)
|
|
@@ -554,34 +564,18 @@ approved Git content.
|
|
|
554
564
|
|
|
555
565
|
## Oxlint and Oxfmt configurations
|
|
556
566
|
|
|
557
|
-
Dev Kit exports
|
|
558
|
-
|
|
559
|
-
in `vite.config.ts`:
|
|
567
|
+
Dev Kit exports typed Oxlint/Oxfmt presets for standalone Oxc projects. The
|
|
568
|
+
Vite+ factory composes both presets and keeps their tool-path ignores aligned:
|
|
560
569
|
|
|
561
570
|
```ts
|
|
562
|
-
import {
|
|
563
|
-
import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
|
|
571
|
+
import { createRecommendedVitePlusConfig } from "@danieljvdm/dev-kit/vite-plus";
|
|
564
572
|
import { defineConfig } from "vite-plus";
|
|
565
573
|
|
|
566
|
-
export default defineConfig(
|
|
567
|
-
fmt: {
|
|
568
|
-
...recommendedOxfmtConfig,
|
|
569
|
-
},
|
|
570
|
-
lint: {
|
|
571
|
-
extends: [recommendedOxlintConfig],
|
|
572
|
-
rules: {
|
|
573
|
-
// Add repository-specific rules here.
|
|
574
|
-
},
|
|
575
|
-
},
|
|
576
|
-
});
|
|
574
|
+
export default defineConfig(createRecommendedVitePlusConfig());
|
|
577
575
|
```
|
|
578
576
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
spread its configuration before project-local formatter options. The shared
|
|
582
|
-
lint preset enables `typeAware` for semantic lint rules but leaves `typeCheck`
|
|
583
|
-
disabled. Managed quality config uses this composition; custom Vite configs
|
|
584
|
-
compose the exports manually. Effect TypeScript-Go projects run:
|
|
577
|
+
The shared lint preset enables `typeAware` for semantic lint rules but leaves
|
|
578
|
+
`typeCheck` disabled. Effect TypeScript-Go projects run:
|
|
585
579
|
|
|
586
580
|
```sh
|
|
587
581
|
vp fmt --check
|
package/dev-kit.example.jsonc
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danieljvdm/dev-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Declarative project development toolkit with portable agent skills.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"types": "./src/oxfmt.ts",
|
|
39
39
|
"default": "./src/oxfmt.js"
|
|
40
40
|
},
|
|
41
|
+
"./vite-plus": {
|
|
42
|
+
"types": "./src/vite-plus.ts",
|
|
43
|
+
"default": "./src/vite-plus.js"
|
|
44
|
+
},
|
|
41
45
|
"./oxlint-plugin-effect": {
|
|
42
46
|
"types": "./src/oxlint-plugin-effect.d.ts",
|
|
43
47
|
"default": "./src/oxlint-plugin-effect.js"
|
|
@@ -65,17 +69,17 @@
|
|
|
65
69
|
"catalog:check": "./bin/dev-kit.mjs catalog verify"
|
|
66
70
|
},
|
|
67
71
|
"dependencies": {
|
|
68
|
-
"@effect/platform-bun": "4.0.0-beta.
|
|
72
|
+
"@effect/platform-bun": "4.0.0-beta.105",
|
|
69
73
|
"@stylistic/eslint-plugin": "5.10.0",
|
|
70
|
-
"effect": "4.0.0-beta.
|
|
74
|
+
"effect": "4.0.0-beta.105",
|
|
71
75
|
"jsonc-parser": "3.3.1",
|
|
72
76
|
"semver": "7.8.5"
|
|
73
77
|
},
|
|
74
78
|
"devDependencies": {
|
|
75
79
|
"@changesets/cli": "3.0.0-next.10",
|
|
76
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
77
|
-
"@effect/tsgo": "0.
|
|
78
|
-
"@effect/vitest": "4.0.0-beta.
|
|
80
|
+
"@effect/platform-node": "4.0.0-beta.105",
|
|
81
|
+
"@effect/tsgo": "0.33.0",
|
|
82
|
+
"@effect/vitest": "4.0.0-beta.105",
|
|
79
83
|
"@types/node": "25.9.1",
|
|
80
84
|
"@types/semver": "7.8.0",
|
|
81
85
|
"oxfmt": "0.60.0",
|
|
@@ -161,32 +161,15 @@
|
|
|
161
161
|
}
|
|
162
162
|
},
|
|
163
163
|
"vitePlusQualitySetup": {
|
|
164
|
-
"description": "
|
|
164
|
+
"description": "Manage the Vite+ quality workflow while the repository owns and extends vite.config.ts.",
|
|
165
165
|
"type": "object",
|
|
166
166
|
"additionalProperties": false,
|
|
167
167
|
"properties": {
|
|
168
|
-
"config": {
|
|
169
|
-
"$ref": "#/$defs/vitePlusQualityConfigSetup"
|
|
170
|
-
},
|
|
171
168
|
"workflow": {
|
|
172
169
|
"$ref": "#/$defs/vitePlusQualityWorkflowSetup"
|
|
173
170
|
}
|
|
174
171
|
}
|
|
175
172
|
},
|
|
176
|
-
"vitePlusQualityConfigSetup": {
|
|
177
|
-
"description": "Manage the canonical root Vite config without requiring Dev Kit to own CI.",
|
|
178
|
-
"type": "object",
|
|
179
|
-
"additionalProperties": false,
|
|
180
|
-
"properties": {
|
|
181
|
-
"enabled": {
|
|
182
|
-
"type": "boolean",
|
|
183
|
-
"default": false
|
|
184
|
-
},
|
|
185
|
-
"typecheck": {
|
|
186
|
-
"$ref": "#/$defs/vitePlusQualityTypecheckSetup"
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
173
|
"vitePlusQualityWorkflowSetup": {
|
|
191
174
|
"description": "Manage the canonical GitHub Actions check workflow without requiring Dev Kit to own the Vite config.",
|
|
192
175
|
"type": "object",
|
|
@@ -235,34 +218,6 @@
|
|
|
235
218
|
}
|
|
236
219
|
}
|
|
237
220
|
},
|
|
238
|
-
"vitePlusQualityTypecheckSetup": {
|
|
239
|
-
"description": "Select the managed typecheck topology. Single-project is the default; workspace mode runs explicitly scoped package scripts through Vite Task.",
|
|
240
|
-
"type": "object",
|
|
241
|
-
"additionalProperties": false,
|
|
242
|
-
"properties": {
|
|
243
|
-
"strategy": {
|
|
244
|
-
"enum": ["single-project", "workspace"],
|
|
245
|
-
"default": "single-project"
|
|
246
|
-
},
|
|
247
|
-
"concurrency": {
|
|
248
|
-
"description": "Maximum concurrent workspace typecheck tasks.",
|
|
249
|
-
"type": "integer",
|
|
250
|
-
"minimum": 1,
|
|
251
|
-
"maximum": 32,
|
|
252
|
-
"default": 4
|
|
253
|
-
},
|
|
254
|
-
"packages": {
|
|
255
|
-
"description": "Project-relative workspace package directories whose typecheck scripts should run.",
|
|
256
|
-
"type": "array",
|
|
257
|
-
"items": {
|
|
258
|
-
"type": "string",
|
|
259
|
-
"minLength": 1
|
|
260
|
-
},
|
|
261
|
-
"uniqueItems": true,
|
|
262
|
-
"default": []
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
},
|
|
266
221
|
"target": {
|
|
267
222
|
"type": "object",
|
|
268
223
|
"additionalProperties": false,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: build-effect-apis
|
|
3
|
+
description: Build and consume contract-first Effect HTTP APIs. Use when defining shared HttpApiEndpoint/HttpApiGroup contracts, implementing HttpApiBuilder or HttpApiServer handlers and middleware, deriving HttpApiClient or Effect Atom clients, or integrating those APIs with TanStack Start and Cloudflare Workers/effect-cf.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Build Effect APIs
|
|
7
|
+
|
|
8
|
+
Treat the shared `HttpApi` value as the **contract spine**: schemas, server,
|
|
9
|
+
OpenAPI, and clients all derive from it. Keep transport contracts isomorphic;
|
|
10
|
+
keep runtime behavior in handlers, services, layers, and client state modules.
|
|
11
|
+
|
|
12
|
+
Effect HTTP and reactivity APIs are version-sensitive. Read the target
|
|
13
|
+
repository's manifests and lockfile, inspect its existing imports, and confirm
|
|
14
|
+
exact signatures from the installed package declarations before editing. In
|
|
15
|
+
current Effect v4 betas the server module is `HttpApiBuilder`; a request that
|
|
16
|
+
mentions `HttpApiServer` may refer to the same server-building responsibility
|
|
17
|
+
from another version.
|
|
18
|
+
|
|
19
|
+
## Build the contract spine
|
|
20
|
+
|
|
21
|
+
1. Inventory the existing API root, groups, endpoints, schemas, transport
|
|
22
|
+
errors, middleware, handler layers, client construction, and runtime edge.
|
|
23
|
+
Finish when every existing contract consumer and provider is identified.
|
|
24
|
+
2. Read [shared-contracts.md](references/shared-contracts.md), then change the
|
|
25
|
+
shared domain package first: schemas and errors, one endpoint contract,
|
|
26
|
+
group composition, root API composition, and public exports. Finish when
|
|
27
|
+
server and client can import the same API value without runtime-specific
|
|
28
|
+
dependencies entering the domain package.
|
|
29
|
+
3. Read [server-and-middleware.md](references/server-and-middleware.md), then
|
|
30
|
+
implement middleware and every changed group handler. Keep handlers as
|
|
31
|
+
decoded boundary adapters into application services and assemble all
|
|
32
|
+
requirements at the runtime edge. Finish when each endpoint identifier has
|
|
33
|
+
exactly one handler and every declared middleware has a provided layer.
|
|
34
|
+
4. Choose the consumer branch:
|
|
35
|
+
- For React server state, read
|
|
36
|
+
[effect-atom-client.md](references/effect-atom-client.md) and
|
|
37
|
+
[effect-atom-lifecycle.md](references/effect-atom-lifecycle.md), then
|
|
38
|
+
derive one `AtomHttpApi.Service`, stable query atoms or families, mutation
|
|
39
|
+
atoms, and one reactivity-key vocabulary.
|
|
40
|
+
- For non-React Effect code, use the direct `HttpApiClient` branch in
|
|
41
|
+
[effect-atom-client.md](references/effect-atom-client.md).
|
|
42
|
+
|
|
43
|
+
Finish when consumers call the shared contract rather than redefining wire
|
|
44
|
+
types or using ad hoc `fetch` for declared endpoints.
|
|
45
|
+
|
|
46
|
+
5. Read [verification.md](references/verification.md). Run the repository's
|
|
47
|
+
format, lint, typecheck, and test commands. Finish when changed schemas
|
|
48
|
+
round-trip, middleware failures use declared error channels, server and
|
|
49
|
+
client agree on every request shape, and changed atom lifecycles have
|
|
50
|
+
deterministic coverage.
|
|
51
|
+
|
|
52
|
+
## Optional branches
|
|
53
|
+
|
|
54
|
+
- Read [runtime-assembly.md](references/runtime-assembly.md) when wiring a
|
|
55
|
+
conventional Node/Bun server, generated API docs, process entrypoint, or
|
|
56
|
+
serverless web handler.
|
|
57
|
+
- Read [effect-atom-testing.md](references/effect-atom-testing.md) when changing
|
|
58
|
+
Atom cache retention, SWR, polling, invalidation, cancellation, aggregation,
|
|
59
|
+
provider placement, SSR, or hydration behavior.
|
|
60
|
+
- Read [tanstack-start.md](references/tanstack-start.md) when the client is
|
|
61
|
+
TanStack Start, SSR, hydration, `ClientOnly`, loaders, server functions, or a
|
|
62
|
+
proxied separate API.
|
|
63
|
+
- Read [cloudflare-workers.md](references/cloudflare-workers.md) when the server
|
|
64
|
+
runs on Cloudflare Workers or uses `effect-cf`, bindings, Durable Objects,
|
|
65
|
+
Queues, WebSockets, streaming, or raw byte routes.
|
|
66
|
+
|
|
67
|
+
## Boundary rules
|
|
68
|
+
|
|
69
|
+
- Let schemas own wire validation, encoding, status metadata, and branded IDs.
|
|
70
|
+
- Let endpoint files own route inputs, success, and expected transport errors.
|
|
71
|
+
- Let middleware own cross-cutting request behavior and request-scoped services.
|
|
72
|
+
- Let handlers own transport-to-application mapping and boundary invariants.
|
|
73
|
+
- Let application services own orchestration, persistence, retries, and
|
|
74
|
+
transactions.
|
|
75
|
+
- Let client data modules own API services, query identity, cache policy,
|
|
76
|
+
invalidation keys, and mutation atoms; let UI action owners own navigation,
|
|
77
|
+
toasts, optimistic presentation, and form reset.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Cloudflare Workers and effect-cf
|
|
2
|
+
|
|
3
|
+
Use `effect-cf` to turn Worker bindings and entrypoints into Effect services and
|
|
4
|
+
layers. Confirm its compatibility with the repository's exact Effect v4 beta.
|
|
5
|
+
|
|
6
|
+
## Runtime edge
|
|
7
|
+
|
|
8
|
+
Assemble application services, `HttpApiBuilder` routes, the Effect HTTP
|
|
9
|
+
platform/router requirements, and binding layers once, then hand the final
|
|
10
|
+
layer and fetch effect to `Worker.make` or another effect-cf definition.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Effect, Layer } from "effect";
|
|
14
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
15
|
+
import { Worker } from "effect-cf";
|
|
16
|
+
|
|
17
|
+
const WorkerLive = Layer.mergeAll(ApplicationServicesLive, ApiRoutesLive);
|
|
18
|
+
|
|
19
|
+
const fetch = Effect.gen(function* () {
|
|
20
|
+
const request = yield* Worker.NativeRequest;
|
|
21
|
+
const router = yield* HttpRouter.HttpRouter;
|
|
22
|
+
return yield* router.asHttpEffect();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export default Worker.make(WorkerLive, { fetch });
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The concrete Effect HTTP platform prerequisites differ by version and runtime.
|
|
29
|
+
Let the `HttpApiBuilder.layer` requirements drive the layer graph and verify it
|
|
30
|
+
with typecheck plus a Worker startup/dry-run command.
|
|
31
|
+
|
|
32
|
+
## Typed bindings
|
|
33
|
+
|
|
34
|
+
Represent D1, R2, KV, Queues, Durable Object namespaces, and configuration as
|
|
35
|
+
services/layers at the Worker boundary. Application services depend on those
|
|
36
|
+
tags, not on a raw `env` object. Keep Wrangler binding names synchronized with
|
|
37
|
+
the layers and generated Worker types.
|
|
38
|
+
|
|
39
|
+
`effect-cf` typed Durable Object and Queue definitions can serve both as the
|
|
40
|
+
contract and as the producer/namespace service. Use deterministic Durable
|
|
41
|
+
Object names for domain ownership, keep RPC errors typed, and design Queue
|
|
42
|
+
consumers for at-least-once delivery.
|
|
43
|
+
|
|
44
|
+
## Raw route escape hatches
|
|
45
|
+
|
|
46
|
+
JSON request/response routes fit `HttpApi`. WebSocket upgrades, streaming
|
|
47
|
+
responses, passthrough `Response` values, and large or direct byte transfers may
|
|
48
|
+
need a raw adapter at the Worker edge.
|
|
49
|
+
|
|
50
|
+
Dispatch raw routes before `router.asHttpEffect()`, decode every path/query/
|
|
51
|
+
header value with shared schemas, then fall through to the generated API router.
|
|
52
|
+
Mirror all relevant security, tenancy, tracing, and rate-limit behavior because
|
|
53
|
+
`HttpApi` middleware does not run for routes that bypass it.
|
|
54
|
+
|
|
55
|
+
For uploads, verify declared size, maximum size, media type, capability, and
|
|
56
|
+
checksum before storage. For WebSockets, validate the upgrade request and
|
|
57
|
+
identity before forwarding it to a Durable Object or session service.
|
|
58
|
+
|
|
59
|
+
Apply runtime-wide CORS around both raw and `HttpApi` routes. Include every
|
|
60
|
+
custom request header and method, and test preflight behavior. Treat OpenAPI as
|
|
61
|
+
documentation only for declared `HttpApi` endpoints; document raw routes
|
|
62
|
+
separately when clients need them.
|
|
63
|
+
|
|
64
|
+
## Operational checks
|
|
65
|
+
|
|
66
|
+
- Generate Worker binding types after Wrangler binding changes.
|
|
67
|
+
- Run local migrations before local server tests.
|
|
68
|
+
- Run the repository's startup or deployment dry-run check after layer changes.
|
|
69
|
+
- Keep remote resource creation, migrations, and deployment as explicit
|
|
70
|
+
user-authorized operations.
|
|
71
|
+
- Test Queue idempotency, Durable Object naming, and raw-route middleware parity.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Effect API clients
|
|
2
|
+
|
|
3
|
+
Derive clients from the shared `HttpApi`; keep request types, response types,
|
|
4
|
+
and expected errors owned by the contract.
|
|
5
|
+
|
|
6
|
+
- [Inventory the client boundary](#inventory-the-client-boundary)
|
|
7
|
+
- [Build one Atom API service](#build-one-atom-api-service)
|
|
8
|
+
- [Define queries](#define-queries)
|
|
9
|
+
- [Define mutations and invalidation](#define-mutations-and-invalidation)
|
|
10
|
+
- [Use a direct client outside React](#use-a-direct-client-outside-react)
|
|
11
|
+
|
|
12
|
+
## Inventory the client boundary
|
|
13
|
+
|
|
14
|
+
Before changing Atom code, locate every `RegistryProvider`, runtime factory,
|
|
15
|
+
`AtomHttpApi.Service`, query atom, mutation atom, reactivity-key constructor,
|
|
16
|
+
and route aggregate. Confirm the installed `effect` and `@effect/atom-react`
|
|
17
|
+
versions before copying signatures.
|
|
18
|
+
|
|
19
|
+
## Build one Atom API service
|
|
20
|
+
|
|
21
|
+
Create one module-scoped runtime factory and one `AtomHttpApi.Service`. Share a
|
|
22
|
+
memo map when multiple client services must reuse layers.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { Effect, Layer } from "effect";
|
|
26
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
27
|
+
import { HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
28
|
+
import { Atom, AtomHttpApi } from "effect/unstable/reactivity";
|
|
29
|
+
import { ApplicationApi, Authenticate } from "@app/domain";
|
|
30
|
+
|
|
31
|
+
export const AuthenticateClient = HttpApiMiddleware.layerClient(
|
|
32
|
+
Authenticate,
|
|
33
|
+
Effect.fn("AuthenticateClient")(function* ({ next, request }) {
|
|
34
|
+
const token = yield* readAccessToken;
|
|
35
|
+
return yield* next(HttpClientRequest.bearerToken(request, token));
|
|
36
|
+
}),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const ApiHttpClient = Layer.mergeAll(FetchHttpClient.layer, AuthenticateClient);
|
|
40
|
+
|
|
41
|
+
export const appRuntime = Atom.context({
|
|
42
|
+
memoMap: Layer.makeMemoMapUnsafe(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export const ApiClient = AtomHttpApi.Service()("ApiClient", {
|
|
46
|
+
api: ApplicationApi,
|
|
47
|
+
httpClient: ApiHttpClient,
|
|
48
|
+
runtime: appRuntime,
|
|
49
|
+
baseUrl: "/api",
|
|
50
|
+
transformClient: (client) =>
|
|
51
|
+
HttpClient.mapRequest(client, (request) =>
|
|
52
|
+
HttpClientRequest.setHeader(request, "x-request-id", requestId()),
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Define the runtime, service, key constructors, queries, and mutations outside
|
|
58
|
+
React renders. Keep browser-only identity access behind the client boundary.
|
|
59
|
+
When contract middleware sets `requiredForClient`, satisfy it with
|
|
60
|
+
`HttpApiMiddleware.layerClient` in the `httpClient` layer. Reserve
|
|
61
|
+
`transformClient` for transport-wide behavior such as correlation headers,
|
|
62
|
+
base URL changes, tracing, or retries; do not bypass declared API security with
|
|
63
|
+
an unrelated raw header transform.
|
|
64
|
+
|
|
65
|
+
## Define queries
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
export const projectKeys = {
|
|
69
|
+
collection: ["projects"] as const,
|
|
70
|
+
project: (id: ProjectId) => [`project:${id}`] as const,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const projectAtom = Atom.family((projectId: ProjectId) =>
|
|
74
|
+
ApiClient.query("projects", "getProject", {
|
|
75
|
+
params: { projectId },
|
|
76
|
+
timeToLive: "5 minutes",
|
|
77
|
+
reactivityKeys: projectKeys.project(projectId),
|
|
78
|
+
}).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
|
|
79
|
+
);
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The
|
|
83
|
+
service memoizes encoded request keys internally; a public `Atom.family` still
|
|
84
|
+
expresses domain ownership and cache policy through a stable scalar or Effect
|
|
85
|
+
`Hash`/`Equal` key.
|
|
86
|
+
|
|
87
|
+
Query options have distinct jobs:
|
|
88
|
+
|
|
89
|
+
- `timeToLive` controls idle registry retention;
|
|
90
|
+
- `reactivityKeys` registers invalidation subscriptions;
|
|
91
|
+
- `serializationKey` enables decoded-only hydration serialization and is not
|
|
92
|
+
the runtime cache key;
|
|
93
|
+
- `responseMode` changes the response and error shape.
|
|
94
|
+
|
|
95
|
+
Keep secrets out of serialization keys, URL state, hydration payloads, and
|
|
96
|
+
client-visible layers.
|
|
97
|
+
|
|
98
|
+
## Define mutations and invalidation
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
export const updateProjectMutation = ApiClient.mutation("projects", "updateProject");
|
|
102
|
+
|
|
103
|
+
const updateProject = useAtomSet(updateProjectMutation, { mode: "promise" });
|
|
104
|
+
|
|
105
|
+
await updateProject({
|
|
106
|
+
params: { projectId },
|
|
107
|
+
payload: patch,
|
|
108
|
+
reactivityKeys: [...projectKeys.collection, ...projectKeys.project(projectId)],
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Successful mutations invalidate matching keys; failed mutations do not.
|
|
113
|
+
Centralize reactivity-key constructors because mismatched strings fail silently.
|
|
114
|
+
When one mutation affects several exact keys, flatten their arrays; an array
|
|
115
|
+
containing key arrays registers those nested arrays as different keys.
|
|
116
|
+
|
|
117
|
+
Exact array keys invalidate only exact matches. Record-form keys are
|
|
118
|
+
hierarchical in current Effect v4 implementations: a property registers both
|
|
119
|
+
its broad name and each `property:id` combination. Confirm this against the
|
|
120
|
+
installed version before relying on it.
|
|
121
|
+
|
|
122
|
+
Choose invalidation breadth from the server write:
|
|
123
|
+
|
|
124
|
+
- invalidate an entity key when only its detail changed;
|
|
125
|
+
- invalidate the collection when membership, ordering, totals, or filters can
|
|
126
|
+
change;
|
|
127
|
+
- invalidate every affected namespace when a write crosses aggregates.
|
|
128
|
+
|
|
129
|
+
Combine invalidation with manual refresh only when two requests are intended.
|
|
130
|
+
Keep navigation, toasts, form reset, and presentation-level optimistic state
|
|
131
|
+
with the initiating UI.
|
|
132
|
+
|
|
133
|
+
## Use a direct client outside React
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { Effect } from "effect";
|
|
137
|
+
import { FetchHttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
138
|
+
import { HttpApiClient, HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
139
|
+
import { ApplicationApi, Authenticate } from "@app/domain";
|
|
140
|
+
import { AccessToken, AccessTokenLive } from "./AccessToken";
|
|
141
|
+
|
|
142
|
+
const AuthenticateClient = HttpApiMiddleware.layerClient(
|
|
143
|
+
Authenticate,
|
|
144
|
+
Effect.fn("AuthenticateClient")(function* ({ next, request }) {
|
|
145
|
+
const token = yield* AccessToken;
|
|
146
|
+
return yield* next(HttpClientRequest.bearerToken(request, token.value));
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const program = Effect.gen(function* () {
|
|
151
|
+
const client = yield* HttpApiClient.make(ApplicationApi, {
|
|
152
|
+
baseUrl: "https://api.example.com",
|
|
153
|
+
});
|
|
154
|
+
return yield* client.projects.getProject({ params: { projectId } });
|
|
155
|
+
}).pipe(Effect.provide([FetchHttpClient.layer, AuthenticateClient, AccessTokenLive]));
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Use the direct client in services, scripts, tests, and non-React applications.
|
|
159
|
+
Provide every required contract middleware at the client composition root.
|
|
160
|
+
Transform the underlying `HttpClient` once for non-contract headers, tracing,
|
|
161
|
+
retry, or base behavior rather than repeating it at every call site.
|