@craft-ts/mcp 0.7.0-beta.18 → 0.7.0-beta.20
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.
|
@@ -8,12 +8,46 @@ Public docs: https://craft-ts.github.io/craft
|
|
|
8
8
|
LLM index: https://craft-ts.github.io/craft/llms.txt
|
|
9
9
|
Full dump: https://craft-ts.github.io/craft/llms-full.txt
|
|
10
10
|
|
|
11
|
-
For a new framework-independent app, start
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
For a new framework-independent app, start by asking the user only:
|
|
12
|
+
|
|
13
|
+
> What kind of application are you building, and what are its main features?
|
|
14
|
+
> Keep the answer high-level for now; we will refine the details afterwards.
|
|
15
|
+
|
|
16
|
+
Do not begin by asking whether to use EffectTS. From the application type and
|
|
17
|
+
the main features, infer whether a backend is needed. If it is, propose the
|
|
18
|
+
stack explicitly and ask for confirmation:
|
|
19
|
+
|
|
20
|
+
> I suggest CraftTS for the frontend and EffectTS v4 for the backend. EffectTS
|
|
21
|
+
> fits this project particularly well because its typed services, Layers and
|
|
22
|
+
> errors align with CraftTS's dependency graph and typed server boundary. Is
|
|
23
|
+
> that stack OK?
|
|
24
|
+
|
|
25
|
+
When the user confirms the proposal, use EffectTS for the backend by default.
|
|
26
|
+
If the user rejects it or names another backend, do not silently scaffold an
|
|
27
|
+
EffectTS backend; honour the requested choice, or leave the backend out when
|
|
28
|
+
no alternative is specified.
|
|
29
|
+
|
|
30
|
+
Once the stack is confirmed, create an empty, domain-ready starter with the
|
|
31
|
+
quality surfaces enabled from the beginning:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx --yes --package @craft-ts/dev-tools@beta craft create <directory> \
|
|
35
|
+
--yes --no-demos --domain app \
|
|
36
|
+
--frontend-runtime=plain --backend-runtime=effect \
|
|
37
|
+
--i18n=strict --design-system=basic --typed-css \
|
|
38
|
+
--references=all --agents=codex
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Replace `--backend-runtime=effect` with the user's explicit backend choice,
|
|
42
|
+
or `--backend-runtime=none` when they decline a backend. When no Effect
|
|
43
|
+
runtime is selected, use `--references=craft-ts` instead of `--references=all`.
|
|
44
|
+
Keep
|
|
45
|
+
`--no-demos`, `--i18n=strict`, `--design-system=basic` and `--typed-css` by
|
|
46
|
+
default for agent-created projects. The starter contains the CraftTS
|
|
47
|
+
architecture and tooling contract, but no prefilled product pages or demo
|
|
48
|
+
content. CraftTS source references are always enabled, and EffectTS references
|
|
49
|
+
are included whenever either runtime uses EffectTS; there is no final reference
|
|
50
|
+
confirmation question.
|
|
17
51
|
|
|
18
52
|
When MCP tools are available, call `get_best_practices` once, then `search_documentation` / `get_skill` instead of inventing APIs.
|
|
19
53
|
|
package/content/docs-index.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"path": "/guide/advanced/effect",
|
|
14
14
|
"title": "Using Effect with CraftTS",
|
|
15
|
-
"body": "# Using Effect with CraftTS\n\nEffect belongs in CraftTS when the problem is a **domain program**: composing\nservices, modelling typed failures, controlling resources, or running an\noperation that crosses an I/O boundary. CraftTS remains responsible for\ncomponents, fine-grained rendering, reactive state and resource lifecycles.\n\nThe integration is deliberately a boundary, not a second UI runtime:\n\n```text\nCraft component / template\n ↓\nCraft primitive or generator\n ↓\nEffect<A, E, R>\n ↓\nLayer<R> provided by the Craft injector\n```\n\nIf a value is only local UI state, keep it in Craft. If it is a domain operation\nwith typed errors or services, define it as an Effect and adapt it at the Craft\nboundary.\n\n## The short decision\n\n| Need | Use | Why |\n| -------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------- |\n| Toggle, draft, selection or other local UI value | `state` | Craft owns reactive UI state |\n| Read data with an Effect loader | `queryEffect` | loading, caching, cancellation and exceptions are Craft concerns |\n| Derive a reactive value from a synchronous Effect | `computedEffect` | runs a `SyncOp` Effect in place — a value, not a resource |\n| Expose a synchronous Effect as a callable method | `methodEffect` | the Effect counterpart of `craftMethod` |\n| Run a synchronous Effect in a lower-level position | `syncEffect` | `params`, a `craftMethod`, a `state` updater |\n| Write data with an Effect loader | `mutationEffect` | explicit writes and mutation reactions |\n| Run an explicit command | `asyncProcessEffect` | export, refresh, share action or other non-resource process |\n| Provide Effect services | `provideLayer` | app and route injectors own Layer scope |\n| Select a service from a Craft factory | `effectService` | records the Effect service dependency and selected members |\n| Yield one Effect in a Craft generator | `runEffect` | low-level bridge with typed Craft exceptions |\n| Validate data with Effect Schema | `Schema.toStandardSchemaV1(...)` | uses Craft's schema boundary without coupling core to Effect |\n\nThere is intentionally no `stateEffect`. A reactive value is not made better by\nbeing an Effect. Use `state` for the value, and use Effect for the computation\nthat loads or changes it.\n\n## Install the packages\n\n```shell\nnpm i @craft-ts/core@beta @craft-ts/component@beta @craft-ts/effect@beta\nnpm i effect@rc\n```\n\nKeep the three Craft packages on the same version. `@craft-ts/effect` declares\n`effect` as a peer dependency.\n\n## Run Effect diagnostics\n\nFor an Effect-enabled project, make the diagnostics command part of the normal\nfeedback loop:\n\n```shell\nnpm run effect-check\n# or, from the repository root:\nnode tools/run-effect-tsgo.mjs diagnostics \\\n --project apps/demo-effect/tsconfig.json \\\n --severity error,warning,message\n```\n\nThe repository wrapper prints the resolved project scope, the TypeScript\nprogram file count and source candidates excluded by the project configuration,\nthen asks EffectTS to print per-file progress. It refuses a zero-file program,\nwhich catches a wrong `--project`, an over-broad `exclude`, or an `include` that\nmisses the intended source tree before the check can look green.\n\nReview warnings in two groups: the existing baseline that is accepted for the\nproject, and warnings introduced by the current change. Keep the command's\nscope explicit in CI and link a diagnostic back to this page when handing the\nresult to an agent. `--project=...` and `-p ...` are both supported.\n\n## Install the bridge once\n\nThe bridge teaches Craft's generator driver how to execute a yielded Effect.\nInstall it at application bootstrap:\n\n```typescript\nimport { provideAppInitializer } from '@craft-ts/core';\nimport { installCraftEffectBridge } from '@craft-ts/effect';\n\nexport const appConfig = craftAppConfig({\n providers: [\n provideAppInitializer(() => {\n installCraftEffectBridge();\n }),\n ],\n});\n```\n\nIn a test, install it in `beforeEach` and call the returned disposer in\n`afterEach`. Do not install a new bridge in every loader or component.\n\n## Keep components in Craft\n\nA Craft component still has a generator factory and a typed template. The\ncomponent should call a domain operation, not resolve its repository or start a\nfiber from a click handler:\n\n```typescript\nimport { button, craftComponent, p } from '@craft-ts/component';\nimport { queryEffect } from '@craft-ts/effect';\nimport { loadUserProfile } from './profile-domain';\n\nexport const Profile = craftComponent(\n 'Profile',\n {},\n function* () {\n const profile = yield* queryEffect('profile', {\n params: () => 'user-ada',\n loader: ({ params }) => loadUserProfile(params),\n });\n\n return { profile };\n },\n ({ profile }) => [\n p(function* () {\n const user = yield* profile.value();\n return user?.name ?? 'Loading…';\n }),\n button(\n 'reload',\n {\n *click() {\n yield* profile.reload();\n },\n },\n 'Reload',\n ),\n ],\n);\n```\n\nThe template consumes Craft readers. It does not subscribe to an Effect, call\n`Effect.runPromise`, or convert a Promise into a signal manually.\n\n## Define the domain in Effect\n\nUse Effect for domain contracts and implementations. Tagged errors are values in\nthe `E` channel:\n\n```typescript\nimport { Context, Data, Effect, Layer } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport type UserRepository = {\n readonly byId: (userId: string) => Effect.Effect<User, UserNotFound>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport const UserRepositoryLive = Layer.sync(UserRepositoryService)(() => ({\n byId: (userId) => findUserInDatabase(userId),\n}));\n\nexport function loadUserProfile(userId: string) {\n return Effect.gen(function* () {\n const repository = yield* UserRepositoryService;\n return yield* repository.byId(userId);\n });\n}\n```\n\nThe resulting program carries its success value, its typed failures and its\nrequirements. A Craft component only needs `loadUserProfile`; it does not need\nto know which Layer implements `UserRepositoryService`.\n\n## Choose the right adapter\n\n### `queryEffect`: Effect-backed reads\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) => listUsers(params),\n });\n```\n\nUse it when the result is server or domain state. Craft owns `status`, loading,\nprevious value, cancellation and reloading. The loader returns\n`Effect<Value, Error, Requirements>`.\n\nThe `params` factory and `method` are synchronous. They may read Craft\ndependencies, but must not create an Effect or read an Effect service. The\nloader is the only Effect-aware callback:\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: function* () {\n const input = yield* searchInput();\n return resolveSearchParams(input);\n },\n loader: ({ params }) => listUsers(params),\n });\n```\n\nThe Effect ESLint rule enforces this boundary. A **declared-synchronous** Effect\nis allowed here through `syncEffect(...)`; for an input that has to suspend, use\na `queryEffect` and feed its settled value to this one.\n\n### `mutationEffect`: Effect-backed writes\n\n```typescript\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n method: (input: UserInput) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nTrigger it with `yield* saveUser.mutate(input)`. Use the normal Craft\n`insertReactOnMutation` insertion to reload a query or apply an optimistic patch.\nThe mutation `method` only maps its arguments to synchronous params. The\n`loader` is the only Effect-aware callback.\n\n### `asyncProcessEffect`: explicit commands\n\n```typescript\nconst exportUsers =\n yield *\n asyncProcessEffect('exportUsers', {\n method: (filter: Filter) => filter,\n loader: ({ params }) => exportUsersEffect(params),\n });\n\nyield * exportUsers.method(currentFilter);\n```\n\nThe `asyncProcessEffect` method follows the same rule: it returns plain params;\nthe loader owns the asynchronous Effect program.\n\nUse it for an operation with a lifecycle but without a query cache or mutation\nrelationship.\n\n### `methodEffect`: synchronous callable methods\n\nUse `methodEffect` when a domain operation is a synchronous Effect and should be\nexposed as a callable method rather than as a resource:\n\n```typescript\nconst formatPrice = methodEffect('formatPrice', (cents: number) =>\n Effect.gen(function* () {\n yield* SyncOp;\n return `${(cents / 100).toFixed(2)} €`;\n }),\n);\n\nformatPrice(1499); // '14.99 €'\n```\n\nIt is the Effect-aware convenience form of `craftMethod`. The `SyncOp`\nrequirement is mandatory because the method returns immediately. For an Effect\nthat can suspend, use `asyncProcessEffect`, `mutationEffect`, or\n`queryEffect`.\n\n### `computedEffect`: a derived value, not a resource\n\n`computedEffect` runs a **synchronous** Effect in place and hands back a value.\nIt is the adapter for a derivation — a formatted price, a validity flag — where\n`queryEffect` would wrap the answer in a resource with a loading state nothing\ncan ever be in:\n\n```typescript\nimport { computedEffect } from '@craft-ts/effect';\n\nconst totalLabel = computedEffect('totalLabel', function* () {\n const lines = yield* cartLines();\n return cartTotalLabel(lines); // returns the Effect, never runs it\n});\n```\n\nThe factory reads Craft dependencies with `yield*` and **returns** an Effect;\nthe adapter runs it in place against the nearest `provideLayer(...)`. Read the\nresult like any `craftComputed` — no `value`, no `isLoading`, no\n`pendingNode`.\n\nThe Effect it returns must be declared synchronous — `Effect<A, E, SyncOp>` —\nfor the same reason `syncEffect` requires it: a computation is asked for its\nvalue now and cannot suspend to produce it, so one whose `R` does not carry\n`SyncOp` is refused at the call site. See [Run a synchronous member from a\ncomputed](#run-a-synchronous-member-from-a-computed) for what `SyncOp` is and\nthe three mechanisms that check the claim.\n\nUse `syncEffect` instead when the synchronous Effect is not the whole\nderivation — inside a `craftMethod`, a `params`, or a `state` updater.\n\n### `runEffect`: the low-level form\n\nUse `runEffect` when an Effect is yielded directly by a guard, resolver or Craft\nprogram and you need its typed errors to be visible to Craft:\n\n```typescript\nimport { runEffect } from '@craft-ts/effect';\n\nconst user = yield * runEffect(loadUserProfile(userId));\n```\n\nThe adapter is the right choice for most component resources. A bare\n`yield* someEffect` may execute at runtime, but it does not advertise the\nEffect's `E` channel to Craft's route-exception analysis. `runEffect` does.\n\n## Provide services with `Layer`\n\n`provideLayer` attaches a built Effect context to a Craft injector:\n\n```typescript\nexport const appConfig = craftAppConfig({\n providers: [provideLayer(Layer.mergeAll(UserRepositoryLive, SessionLive))],\n});\n```\n\nUse one merged Layer per injector level. A route can add a narrower Layer:\n\n```typescript\nconst routes = craftRoutes('app', [\n {\n path: 'team',\n ...loadCraftComponent(() => import('./team'), [\n provideLayer(TeamContextLive),\n ] as const),\n },\n]);\n```\n\nThe parent context is reused and the child Layer is added for that route. Its\nEffect scope is closed with the route injector.\n\nFor compile-time coverage, compare the program's `Effect.Services<...>` with\nthe values provided by the app and route:\n\n```typescript\ntype Check = EffectRequirementsCheckedDI<\n Effect.Services<typeof loadTeamOverview>,\n | AppProvidedEffectServices\n | ProvidedEffectServicesOfRoute<typeof routes._routes, 'team'>\n>;\ntype CanRunCheck = CanRun<Check>;\n```\n\nSee [route-scoped Layers in the Learn path](/learn-effect/06-layers-routing) for\nthe full `AppProvidedDependencyValuesOf` setup.\n\n## Understand the error mapping\n\nThe bridge keeps Effect's distinctions intact:\n\n| Effect outcome | Craft outcome | Handle it with |\n| -------------------------- | ------------------------------------- | --------------------------------------- |\n| `Effect.succeed(value)` | resource value / generator result | normal rendering |\n| typed `Effect.fail(error)` | Craft exception keyed by `error._tag` | `matchNode`, `catchTag`, route handlers |\n| `Effect.die(defect)` | technical error | error boundary / monitoring |\n| interruption | cancellation | normally no user-facing handler |\n\nUse exhaustive matching for business errors:\n\n```typescript\nmatchNode.exhaustive(resource.exception, '_tag', {\n UserNotFound: () => p('No user was found.'),\n Unauthorized: () => p('Your session has expired.'),\n});\n```\n\nThe error union is only visible to the compiler when the Effect crosses through\n`queryEffect`, `mutationEffect`, `asyncProcessEffect` or `runEffect`.\n\n## Use Effect Schema at data boundaries\n\n`@craft-ts/core` accepts Standard Schema. Effect Schema participates through one\nconversion call:\n\n```typescript\nimport { Schema } from 'effect';\n\nconst UserInput = Schema.toStandardSchemaV1(\n Schema.Struct({\n name: Schema.String,\n email: Schema.String,\n }),\n);\n\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n methodSchema: UserInput,\n method: (input) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nThis schema interop does not require `@craft-ts/effect`; it follows the Standard\nSchema contract. Use the [schema validation guide](/guide/state/schema-validation#effect-schema)\nfor async decoding and loader result validation.\n\n## Select an Effect service from Craft\n\nMost components should consume a domain operation. A Craft service or adapter\nthat really needs an Effect service can select only the members it uses:\n\n```typescript\nconst { byId } =\n yield * effectService(UserRepositoryService, ({ byId }) => ({ byId }));\n```\n\nThe selection narrows the graph and keeps generic member signatures intact. It\ndoes not replace `Layer`; the service still comes from the nearest\n`provideLayer(...)`.\n\n## Run a synchronous member from a computed\n\n`params`, `craftComputed(...)` and `craftMethod(...)` run on Craft's synchronous\ndriver: they complete on one tick and cannot wait. `Effect<A, E, R>` does not say\nwhether running an Effect will suspend, and a service member makes it worse — a\n`Layer` closes over its dependencies at construction, so a network call and a\npure calculation both surface as `R = never`.\n\nDeclare the difference in `R`, the one channel Effect accumulates:\n\n```typescript\nexport type CartPricingShape = {\n readonly fetchCatalog: (skus: readonly string[]) => Effect.Effect<Catalog>;\n readonly lineTotal: (line: CartLine) => Effect.Effect<number, never, SyncOp>;\n};\n```\n\n`SyncOp` is a phantom requirement: nothing provides it, it costs nothing at\nruntime, and `Effect<A, E, never>` is assignable to `Effect<A, E, SyncOp>` — so\ndeclaring it in the shape is enough, the implementation needs no ceremony. Where\n`R` is inferred (a standalone `Effect.gen` calling nothing already marked), add\n`yield* SyncOp` to the body.\n\nRun it with `syncEffect(...)`, which resolves in place instead of suspending:\n\n```typescript\nconst totalLabel = craftComputed('totalLabel', function* () {\n const cents = yield* syncEffect(cartTotal(yield* lines()));\n return yield* syncEffect(formatPrice(cents));\n});\n```\n\nRequirements other than `SyncOp` travel through untouched — the level in force\nsatisfies them exactly as it does for a loader. The only thing checked at the\ntype level is that `SyncOp` is among them.\n\nThe declaration is a claim, and three independent mechanisms check it: the type\nrefuses an undeclared Effect at the call; `craft-ts/sync-effect-body` reads the\nbody, every branch at once, and rejects one that yields something async; and at\nruntime `syncEffect` goes through `Effect.runSyncExitWith`, which cannot suspend\n— a broken promise throws `CraftEffectNotSynchronous` at the first call rather\nthan freezing the UI.\n\nFull walkthrough: [Declare a synchronous member](/learn-effect/03-effect-domain#declare-a-synchronous-member).\n\n## Testing\n\nUse `mockEffectService` for a focused Layer:\n\n```typescript\nconst repository = mockEffectService(UserRepositoryService, {\n byId: () => Effect.succeed(expectedUser),\n});\n```\n\nCombine it with Craft's register-based tests. The Effect mock covers the Effect\nservice; the Craft register covers every Craft dependency and boundary. An\nunstubbed member fails with `UnstubbedEffectMember` instead of silently returning\nan incomplete value.\n\nSee [testing with Effect](/learn-effect/08-testing) and [browser\nboundaries](/guide/testing/browser-boundaries).\n\n## Package map\n\n| Package | Responsibility |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@craft-ts/component` | functional Craft components and typed templates |\n| `@craft-ts/core` | Craft primitives, services, routing, forms, testing and the current server-function registry |\n| `@craft-ts/effect` | Effect bridge, `Layer` providers, Effect-aware primitives, service selection, mocks and server execution helpers |\n| `@craft-ts/i18n-effect` | the Effect adapter over an `@craft-ts/i18n` runtime: `provideI18nRuntime`, `translateEffect`, `I18nEffectService` — see [i18n with Effect](/guide/i18n/effect) |\n| `effect` | `Effect`, `Context.Service`, `Layer`, `Schema`, tagged errors and the Effect runtime |\n| `@effect/platform-*` | Effect-native platform adapters; used by the current server-function experiment |\n| `@craft-ts/dev-tools` | generators, migration tools, graph and architecture checks |\n\nInstall only the packages needed by the layer you are building. For example,\nEffect Schema validation can be used with `@craft-ts/core` alone; the bridge and\nEffect-aware resource adapters require `@craft-ts/effect`.\n\n## Server functions: current POC\n\nThe current server-function integration is a **proof of concept**, not a final\nAPI. It currently combines:\n\n- `serverFunction` and `createServerFunctionClient` from `@craft-ts/core`;\n- `executeEffect` and `effectServerMiddleware` from `@craft-ts/effect`;\n- `Effect`/`Layer` on the server;\n- a local HTTP transport and `@effect/platform-node` in the demo.\n\nThe client must import only the server function's type, while the server owns\nthe implementation and server-only Layers. Authentication and authorization\nmust be checked again on the server; a client Layer is never a security boundary.\n\nSee the [server functions POC chapter](/learn-effect/09-server-functions) and\nthe [running demo](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function).\nExpect the transport, file conventions, middleware API and production\nintegration to change before this becomes a stable feature.\n\n## Common mistakes\n\n- **Putting every value in Effect:** keep local UI state in `state` and URL state\n in `queryParams`.\n- **Subscribing in a component:** return an Effect from a resource adapter and\n let Craft own loading and cancellation.\n- **Using `Effect.die` for a business case:** use a tagged error in `E` so the UI\n can handle it exhaustively.\n- **Providing a Layer inside a loader:** provide it at app or route scope so its\n lifetime and requirements are visible.\n- **Trusting client context in a server function:** treat it as a claim and\n verify it on the server.\n- **Using a bare `yield* effect` in a route program:** use `runEffect` so Craft\n sees the typed exception union.\n- **Declaring a member `SyncOp` to get it into a computed:** the marker states a\n fact, it does not create one. If the member can suspend, move the work to a\n loader — the runtime will refuse it anyway.\n\n## See also\n\n- [Learn CraftTS with Effect](/learn-effect/)\n- [Which primitive should I use?](/guide/concepts/choose-primitive)\n- [Exceptions as values](/guide/concepts/exceptions)\n- [Program operators](/guide/advanced/program-operators)\n- [Effect Schema](/guide/state/schema-validation#effect-schema)\n- [Effect integration tests](/learn-effect/08-testing)\n"
|
|
15
|
+
"body": "# Using Effect with CraftTS\n\nEffect belongs in CraftTS when the problem is a **domain program**: composing\nservices, modelling typed failures, controlling resources, or running an\noperation that crosses an I/O boundary. CraftTS remains responsible for\ncomponents, fine-grained rendering, reactive state and resource lifecycles.\n\nThe integration is deliberately a boundary, not a second UI runtime:\n\n```text\nCraft component / template\n ↓\nCraft primitive or generator\n ↓\nEffect<A, E, R>\n ↓\nLayer<R> provided by the Craft injector\n```\n\nIf a value is only local UI state, keep it in Craft. If it is a domain operation\nwith typed errors or services, define it as an Effect and adapt it at the Craft\nboundary.\n\n## The short decision\n\n| Need | Use | Why |\n| -------------------------------------------------- | -------------------------------- | ---------------------------------------------------------------- |\n| Toggle, draft, selection or other local UI value | `state` | Craft owns reactive UI state |\n| Read data with an Effect loader | `queryEffect` | loading, caching, cancellation and exceptions are Craft concerns |\n| Derive a reactive value from a synchronous Effect | `computedEffect` | runs a `SyncOp` Effect in place — a value, not a resource |\n| Expose a synchronous Effect as a callable method | `methodEffect` | the Effect counterpart of `craftMethod` |\n| Run a synchronous Effect in a lower-level position | `syncEffect` | `params`, a `craftMethod`, a `state` updater |\n| Write data with an Effect loader | `mutationEffect` | explicit writes and mutation reactions |\n| Run an explicit command | `asyncProcessEffect` | export, refresh, share action or other non-resource process |\n| Provide Effect services | `provideLayer` | app and route injectors own Layer scope |\n| Select a service from a Craft factory | `effectService` | records the Effect service dependency and selected members |\n| Yield one Effect in a Craft generator | `runEffect` | low-level bridge with typed Craft exceptions |\n| Validate data with Effect Schema | `Schema.toStandardSchemaV1(...)` | uses Craft's schema boundary without coupling core to Effect |\n\nThere is intentionally no `stateEffect`. A reactive value is not made better by\nbeing an Effect. Use `state` for the value, and use Effect for the computation\nthat loads or changes it.\n\n## Install the packages\n\n```shell\nnpm i @craft-ts/core@beta @craft-ts/component@beta @craft-ts/effect@beta\nnpm i effect@rc\n```\n\nKeep the three Craft packages on the same version. `@craft-ts/effect` declares\n`effect` as a peer dependency.\n\n## Run Effect diagnostics\n\nFor an Effect-enabled project, make the diagnostics command part of the normal\nfeedback loop:\n\n```shell\nnpm run effect-check\n# or, from the repository root:\nnode tools/run-effect-tsgo.mjs diagnostics \\\n --project apps/demo-effect/tsconfig.json \\\n --severity error,warning,message\n```\n\nThe repository wrapper prints the resolved project scope, the TypeScript\nprogram file count and source candidates excluded by the project configuration,\nthen asks EffectTS to print per-file progress. It refuses a zero-file program,\nwhich catches a wrong `--project`, an over-broad `exclude`, or an `include` that\nmisses the intended source tree before the check can look green.\n\nReview warnings in two groups: the existing baseline that is accepted for the\nproject, and warnings introduced by the current change. Keep the command's\nscope explicit in CI and link a diagnostic back to this page when handing the\nresult to an agent. `--project=...` and `-p ...` are both supported.\n\n## Install the bridge once\n\nThe bridge teaches Craft's generator driver how to execute a yielded Effect.\nInstall it at application bootstrap:\n\n```typescript\nimport { provideAppInitializer } from '@craft-ts/core';\nimport { installCraftEffectBridge } from '@craft-ts/effect';\n\nexport const appConfig = craftAppConfig({\n providers: [\n provideAppInitializer(() => {\n installCraftEffectBridge();\n }),\n ],\n});\n```\n\nIn a test, install it in `beforeEach` and call the returned disposer in\n`afterEach`. Do not install a new bridge in every loader or component.\n\n## Keep components in Craft\n\nA Craft component still has a generator factory and a typed template. The\ncomponent should call a domain operation, not resolve its repository or start a\nfiber from a click handler:\n\n```typescript\nimport { button, craftComponent, p } from '@craft-ts/component';\nimport { queryEffect } from '@craft-ts/effect';\nimport { loadUserProfile } from './profile-domain';\n\nexport const Profile = craftComponent(\n 'Profile',\n {},\n function* () {\n const profile = yield* queryEffect('profile', {\n params: () => 'user-ada',\n loader: ({ params }) => loadUserProfile(params),\n });\n\n return { profile };\n },\n ({ profile }) => [\n p(function* () {\n const user = yield* profile.value();\n return user?.name ?? 'Loading…';\n }),\n button(\n 'reload',\n {\n *click() {\n yield* profile.reload();\n },\n },\n 'Reload',\n ),\n ],\n);\n```\n\nThe template consumes Craft readers. It does not subscribe to an Effect, call\n`Effect.runPromise`, or convert a Promise into a signal manually.\n\n## Define the domain in Effect\n\nUse Effect for domain contracts and implementations. Tagged errors are values in\nthe `E` channel:\n\n```typescript\nimport { Context, Data, Effect, Layer } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport type UserRepository = {\n readonly byId: (userId: string) => Effect.Effect<User, UserNotFound>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport const UserRepositoryLive = Layer.sync(UserRepositoryService)(() => ({\n byId: (userId) => findUserInDatabase(userId),\n}));\n\nexport const loadUserProfile = Effect.fnUntraced(function* (userId: string) {\n const repository = yield* UserRepositoryService;\n return yield* repository.byId(userId);\n});\n```\n\nThe resulting program carries its success value, its typed failures and its\nrequirements. A Craft component only needs `loadUserProfile`; it does not need\nto know which Layer implements `UserRepositoryService`.\n\n## Choose the right adapter\n\n### `queryEffect`: Effect-backed reads\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: () => ({ filter: search() }),\n loader: ({ params }) => listUsers(params),\n });\n```\n\nUse it when the result is server or domain state. Craft owns `status`, loading,\nprevious value, cancellation and reloading. The loader returns\n`Effect<Value, Error, Requirements>`.\n\nThe `params` factory and `method` are synchronous. They may read Craft\ndependencies, but must not create an Effect or read an Effect service. The\nloader is the only Effect-aware callback:\n\n```typescript\nconst users =\n yield *\n queryEffect('users', {\n params: function* () {\n const input = yield* searchInput();\n return resolveSearchParams(input);\n },\n loader: ({ params }) => listUsers(params),\n });\n```\n\nThe Effect ESLint rule enforces this boundary. A **declared-synchronous** Effect\nis allowed here through `syncEffect(...)`; for an input that has to suspend, use\na `queryEffect` and feed its settled value to this one.\n\n### `mutationEffect`: Effect-backed writes\n\n```typescript\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n method: (input: UserInput) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nTrigger it with `yield* saveUser.mutate(input)`. Use the normal Craft\n`insertReactOnMutation` insertion to reload a query or apply an optimistic patch.\nThe mutation `method` only maps its arguments to synchronous params. The\n`loader` is the only Effect-aware callback.\n\n### `asyncProcessEffect`: explicit commands\n\n```typescript\nconst exportUsers =\n yield *\n asyncProcessEffect('exportUsers', {\n method: (filter: Filter) => filter,\n loader: ({ params }) => exportUsersEffect(params),\n });\n\nyield * exportUsers.method(currentFilter);\n```\n\nThe `asyncProcessEffect` method follows the same rule: it returns plain params;\nthe loader owns the asynchronous Effect program.\n\nUse it for an operation with a lifecycle but without a query cache or mutation\nrelationship.\n\n### `methodEffect`: synchronous callable methods\n\nUse `methodEffect` when a domain operation is a synchronous Effect and should be\nexposed as a callable method rather than as a resource:\n\n```typescript\nconst formatPrice = methodEffect('formatPrice', (cents: number) =>\n Effect.gen(function* () {\n yield* SyncOp;\n return `${(cents / 100).toFixed(2)} €`;\n }),\n);\n\nformatPrice(1499); // '14.99 €'\n```\n\nIt is the Effect-aware convenience form of `craftMethod`. The `SyncOp`\nrequirement is mandatory because the method returns immediately. For an Effect\nthat can suspend, use `asyncProcessEffect`, `mutationEffect`, or\n`queryEffect`.\n\n### `computedEffect`: a derived value, not a resource\n\n`computedEffect` runs a **synchronous** Effect in place and hands back a value.\nIt is the adapter for a derivation — a formatted price, a validity flag — where\n`queryEffect` would wrap the answer in a resource with a loading state nothing\ncan ever be in:\n\n```typescript\nimport { computedEffect } from '@craft-ts/effect';\n\nconst totalLabel = computedEffect('totalLabel', function* () {\n const lines = yield* cartLines();\n return cartTotalLabel(lines); // returns the Effect, never runs it\n});\n```\n\nThe factory reads Craft dependencies with `yield*` and **returns** an Effect;\nthe adapter runs it in place against the nearest `provideLayer(...)`. Read the\nresult like any `craftComputed` — no `value`, no `isLoading`, no\n`pendingNode`.\n\nThe Effect it returns must be declared synchronous — `Effect<A, E, SyncOp>` —\nfor the same reason `syncEffect` requires it: a computation is asked for its\nvalue now and cannot suspend to produce it, so one whose `R` does not carry\n`SyncOp` is refused at the call site. See [Run a synchronous member from a\ncomputed](#run-a-synchronous-member-from-a-computed) for what `SyncOp` is and\nthe three mechanisms that check the claim.\n\nUse `syncEffect` instead when the synchronous Effect is not the whole\nderivation — inside a `craftMethod`, a `params`, or a `state` updater.\n\n### `runEffect`: the low-level form\n\nUse `runEffect` when an Effect is yielded directly by a guard, resolver or Craft\nprogram and you need its typed errors to be visible to Craft:\n\n```typescript\nimport { runEffect } from '@craft-ts/effect';\n\nconst user = yield * runEffect(loadUserProfile(userId));\n```\n\nThe adapter is the right choice for most component resources. A bare\n`yield* someEffect` may execute at runtime, but it does not advertise the\nEffect's `E` channel to Craft's route-exception analysis. `runEffect` does.\n\n## Provide services with `Layer`\n\n`provideLayer` attaches a built Effect context to a Craft injector:\n\n```typescript\nexport const appConfig = craftAppConfig({\n providers: [provideLayer(Layer.mergeAll(UserRepositoryLive, SessionLive))],\n});\n```\n\nUse one merged Layer per injector level. A route can add a narrower Layer:\n\n```typescript\nconst routes = craftRoutes('app', [\n {\n path: 'team',\n ...loadCraftComponent(() => import('./team'), [\n provideLayer(TeamContextLive),\n ] as const),\n },\n]);\n```\n\nThe parent context is reused and the child Layer is added for that route. Its\nEffect scope is closed with the route injector.\n\nFor compile-time coverage, compare the program's `Effect.Services<...>` with\nthe values provided by the app and route:\n\n```typescript\ntype Check = EffectRequirementsCheckedDI<\n Effect.Services<typeof loadTeamOverview>,\n | AppProvidedEffectServices\n | ProvidedEffectServicesOfRoute<typeof routes._routes, 'team'>\n>;\ntype CanRunCheck = CanRun<Check>;\n```\n\nSee [route-scoped Layers in the Learn path](/learn-effect/06-layers-routing) for\nthe full `AppProvidedDependencyValuesOf` setup.\n\n## Understand the error mapping\n\nThe bridge keeps Effect's distinctions intact:\n\n| Effect outcome | Craft outcome | Handle it with |\n| -------------------------- | ------------------------------------- | --------------------------------------- |\n| `Effect.succeed(value)` | resource value / generator result | normal rendering |\n| typed `Effect.fail(error)` | Craft exception keyed by `error._tag` | `matchNode`, `catchTag`, route handlers |\n| `Effect.die(defect)` | technical error | error boundary / monitoring |\n| interruption | cancellation | normally no user-facing handler |\n\nUse exhaustive matching for business errors:\n\n```typescript\nmatchNode.exhaustive(resource.exception, '_tag', {\n UserNotFound: () => p('No user was found.'),\n Unauthorized: () => p('Your session has expired.'),\n});\n```\n\nThe error union is only visible to the compiler when the Effect crosses through\n`queryEffect`, `mutationEffect`, `asyncProcessEffect` or `runEffect`.\n\n## Use Effect Schema at data boundaries\n\n`@craft-ts/core` accepts Standard Schema. Effect Schema participates through one\nconversion call:\n\n```typescript\nimport { Schema } from 'effect';\n\nconst UserInput = Schema.toStandardSchemaV1(\n Schema.Struct({\n name: Schema.String,\n email: Schema.String,\n }),\n);\n\nconst saveUser =\n yield *\n mutationEffect('saveUser', {\n methodSchema: UserInput,\n method: (input) => input,\n loader: ({ params }) => saveUserEffect(params),\n });\n```\n\nThis schema interop does not require `@craft-ts/effect`; it follows the Standard\nSchema contract. Use the [schema validation guide](/guide/state/schema-validation#effect-schema)\nfor async decoding and loader result validation.\n\n## Select an Effect service from Craft\n\nMost components should consume a domain operation. A Craft service or adapter\nthat really needs an Effect service can select only the members it uses:\n\n```typescript\nconst { byId } =\n yield * effectService(UserRepositoryService, ({ byId }) => ({ byId }));\n```\n\nThe selection narrows the graph and keeps generic member signatures intact. It\ndoes not replace `Layer`; the service still comes from the nearest\n`provideLayer(...)`.\n\n## Run a synchronous member from a computed\n\n`params`, `craftComputed(...)` and `craftMethod(...)` run on Craft's synchronous\ndriver: they complete on one tick and cannot wait. `Effect<A, E, R>` does not say\nwhether running an Effect will suspend, and a service member makes it worse — a\n`Layer` closes over its dependencies at construction, so a network call and a\npure calculation both surface as `R = never`.\n\nDeclare the difference in `R`, the one channel Effect accumulates:\n\n```typescript\nexport type CartPricingShape = {\n readonly fetchCatalog: (skus: readonly string[]) => Effect.Effect<Catalog>;\n readonly lineTotal: (line: CartLine) => Effect.Effect<number, never, SyncOp>;\n};\n```\n\n`SyncOp` is a phantom requirement: nothing provides it, it costs nothing at\nruntime, and `Effect<A, E, never>` is assignable to `Effect<A, E, SyncOp>` — so\ndeclaring it in the shape is enough, the implementation needs no ceremony. Where\n`R` is inferred (a standalone `Effect.gen` calling nothing already marked), add\n`yield* SyncOp` to the body.\n\nRun it with `syncEffect(...)`, which resolves in place instead of suspending:\n\n```typescript\nconst totalLabel = craftComputed('totalLabel', function* () {\n const cents = yield* syncEffect(cartTotal(yield* lines()));\n return yield* syncEffect(formatPrice(cents));\n});\n```\n\nRequirements other than `SyncOp` travel through untouched — the level in force\nsatisfies them exactly as it does for a loader. The only thing checked at the\ntype level is that `SyncOp` is among them.\n\nThe declaration is a claim, and three independent mechanisms check it: the type\nrefuses an undeclared Effect at the call; `craft-ts/sync-effect-body` reads the\nbody, every branch at once, and rejects one that yields something async; and at\nruntime `syncEffect` goes through `Effect.runSyncExitWith`, which cannot suspend\n— a broken promise throws `CraftEffectNotSynchronous` at the first call rather\nthan freezing the UI.\n\nFull walkthrough: [Declare a synchronous member](/learn-effect/03-effect-domain#declare-a-synchronous-member).\n\n## Testing\n\nUse `mockEffectService` for a focused Layer:\n\n```typescript\nconst repository = mockEffectService(UserRepositoryService, {\n byId: () => Effect.succeed(expectedUser),\n});\n```\n\nCombine it with Craft's register-based tests. The Effect mock covers the Effect\nservice; the Craft register covers every Craft dependency and boundary. An\nunstubbed member fails with `UnstubbedEffectMember` instead of silently returning\nan incomplete value.\n\nSee [testing with Effect](/learn-effect/08-testing) and [browser\nboundaries](/guide/testing/browser-boundaries).\n\n## Package map\n\n| Package | Responsibility |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `@craft-ts/component` | functional Craft components and typed templates |\n| `@craft-ts/core` | Craft primitives, services, routing, forms, testing and the current server-function registry |\n| `@craft-ts/effect` | Effect bridge, `Layer` providers, Effect-aware primitives, service selection, mocks and server execution helpers |\n| `@craft-ts/i18n-effect` | the Effect adapter over an `@craft-ts/i18n` runtime: `provideI18nRuntime`, `translateEffect`, `I18nEffectService` — see [i18n with Effect](/guide/i18n/effect) |\n| `effect` | `Effect`, `Context.Service`, `Layer`, `Schema`, tagged errors and the Effect runtime |\n| `@effect/platform-*` | Effect-native platform adapters; used by the current server-function experiment |\n| `@craft-ts/dev-tools` | generators, migration tools, graph and architecture checks |\n\nInstall only the packages needed by the layer you are building. For example,\nEffect Schema validation can be used with `@craft-ts/core` alone; the bridge and\nEffect-aware resource adapters require `@craft-ts/effect`.\n\n## Server functions: current POC\n\nThe current server-function integration is a **proof of concept**, not a final\nAPI. It currently combines:\n\n- `serverFunction` and `createServerFunctionClient` from `@craft-ts/core`;\n- `executeEffect` and `effectServerMiddleware` from `@craft-ts/effect`;\n- `Effect`/`Layer` on the server;\n- a local HTTP transport and `@effect/platform-node` in the demo.\n\nThe client must import only the server function's type, while the server owns\nthe implementation and server-only Layers. Authentication and authorization\nmust be checked again on the server; a client Layer is never a security boundary.\n\nSee the [server functions POC chapter](/learn-effect/09-server-functions) and\nthe [running demo](https://github.com/craft-ts/craft-ts/tree/main/apps/demo-with-server-function).\nExpect the transport, file conventions, middleware API and production\nintegration to change before this becomes a stable feature.\n\n## Common mistakes\n\n- **Putting every value in Effect:** keep local UI state in `state` and URL state\n in `queryParams`.\n- **Subscribing in a component:** return an Effect from a resource adapter and\n let Craft own loading and cancellation.\n- **Using `Effect.die` for a business case:** use a tagged error in `E` so the UI\n can handle it exhaustively.\n- **Providing a Layer inside a loader:** provide it at app or route scope so its\n lifetime and requirements are visible.\n- **Trusting client context in a server function:** treat it as a claim and\n verify it on the server.\n- **Using a bare `yield* effect` in a route program:** use `runEffect` so Craft\n sees the typed exception union.\n- **Declaring a member `SyncOp` to get it into a computed:** the marker states a\n fact, it does not create one. If the member can suspend, move the work to a\n loader — the runtime will refuse it anyway.\n\n## See also\n\n- [Learn CraftTS with Effect](/learn-effect/)\n- [Which primitive should I use?](/guide/concepts/choose-primitive)\n- [Exceptions as values](/guide/concepts/exceptions)\n- [Program operators](/guide/advanced/program-operators)\n- [Effect Schema](/guide/state/schema-validation#effect-schema)\n- [Effect integration tests](/learn-effect/08-testing)\n"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "/guide/advanced/observability",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
{
|
|
48
48
|
"path": "/guide/app/abstract-services",
|
|
49
49
|
"title": "Abstract services",
|
|
50
|
-
"body": "# Abstract services\n\nAn `abstract` service declares a **contract** with no implementation, and forces\na concrete one to be supplied downstream. This is what makes a service's\nimplementation a decision of the mounting site — a route, a feature config, a\ntest — instead of a hard import.\n\n## Abstract Requirements\n\nUse `
|
|
50
|
+
"body": "# Abstract services\n\nAn `abstract` service declares a **contract** with no implementation, and forces\na concrete one to be supplied downstream. This is what makes a service's\nimplementation a decision of the mounting site — a route, a feature config, a\ntest — instead of a hard import.\n\n## Abstract Requirements\n\nUse `providedIn: 'abstract'` to declare a contract that must be implemented elsewhere.\n\n\n\nConcrete services can then depend on `CounterRequirement`.\n\n## Abstract Providers\n\nAn `abstract` service also exposes a `provideX(factory)` helper. It takes a **factory** — a plain\nfunction or a generator — produces a value matching the contract, and binds it to the requirement\ntoken. This lets you implement the contract **inline at the providing site** (a route, a component,\na feature config) instead of declaring a separate concrete `craftService`.\n\n```typescript\nimport { abstract, craftService } from '@craft-ts/core';\n\ntype User = { name: string };\n\nconst { User, provideUser } = craftService(\n { name: 'User', providedIn: 'abstract' },\n abstract<User>(),\n);\n\n// Implement the contract inline:\nconst providers = [provideUser(() => ({ name: 'Ada' }))];\n\n// Anywhere downstream, inside a craft generator:\nconst user = yield * User();\n```\n\nThe factory can be a **generator** that yields other services. Everything it yields is tracked, so\nthe resulting provider participates in the cascade DI check just like a regular service:\n\n```typescript\nconst { Greeting } = craftService(\n { name: 'Greeting', providedIn: 'global' },\n () => ({ prefix: 'Hello' }),\n);\n\nconst providers = [\n provideUser(function* () {\n const greeting = yield* Greeting();\n return { name: `${greeting.prefix} Ada` };\n }),\n];\n```\n\nThis is the foundation of route-scoped providers: a route can implement an abstract contract from\nits own guarded data / params. See\n[Type-safe DI/Routes → Route Providers](/guide/routing/route-providers).\n\n## See Also\n\n- [Service scopes](/guide/app/service-scopes)\n- [Route providers](/guide/routing/route-providers)\n"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "/guide/app/app-start",
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
{
|
|
58
58
|
"path": "/guide/app/craft-service",
|
|
59
59
|
"title": "craftService",
|
|
60
|
-
"body": "# craftService\n\nA service is a factory with a **name** and a **scope** — not a class. It packages\nprimitives and dependencies behind an explicit API, and keeps the whole\ndependency graph visible to the compiler.\n\n**Use it when** logic outgrows a single component field, or when two places need\nthe same behaviour.\nUse a small adapter when a dependency is owned by the runtime environment\nrather than by your application.\n\nThe contrast with `inject(...)` scattered across classes is the point:\ndependencies here are explicit and **type-visible**, which is what the route DI\ncheck and the test registers read.\n\n```typescript\nimport { craftService } from '@craft-ts/core';\n```\n\nService inputs that can change should be consumed as yieldable readers\n(`CraftServiceInput<T>`), the service counterpart of a component `Input<T>`.\nYield them so the input-to-service edge stays in the dependency graph:\n\n```typescript\nimport { craftService, query, type CraftServiceInput } from '@craft-ts/core';\n\nconst { UserQuery } = craftService(\n { name: 'UserQuery', providedIn: 'global' },\n (inputs: { userId: CraftServiceInput<string | undefined> }) =>\n query('userQuery', {\n params: function* () {\n return yield* inputs.userId();\n },\n loader: ({ params }) => ApiService.getItemById(params),\n }),\n);\n```\n\nThe call site still accepts a resolved value, a signal, or a Craft\nreader — the service boundary adapts it into that reader. Inside the factory,\nalways `yield* inputs.x()`.\n\n## What you get\n\nDeclaring a service gives you a set of generated helpers. For one named\n`Counter`:\n\n- `Counter(...)` — consume or compose it inside a craft generator\n- `Counter.someProperty(...)` — derive one public property directly\n- `provideCounter(...)` — for provider-capable scopes\n- `COUNTER_META_DATA` — for metadata-driven tooling\n- `CounterRequirement` — for `abstract` services\n- `provideCounter(factory)` — on `abstract` services, to implement the contract\n inline\n\nWhich of those exist depends on the scope.\n\n::: warning Breaking change — no more `injectX`\nThe generated helper is the service name itself: `X`. `craftService` no longer\nexports `injectX`, and the former `XToYield` helper is gone. Use `X()` in a craft\ngenerator and compose with `yield* X()`.\n:::\n\n## Supported scopes\n\nA service declares how many instances of it exist through `scope`:\n`function`, `toProvide`, `global`, `manuallyProvidedAtRoot` or `abstract`.\nDefault to `function`.\n\nEach scope and when to pick it: **[Service scopes](/guide/app/service-scopes)**.\n\n## The common case\n\n\n\n
|
|
60
|
+
"body": "# craftService\n\nA service is a factory with a **name** and a **scope** — not a class. It packages\nprimitives and dependencies behind an explicit API, and keeps the whole\ndependency graph visible to the compiler.\n\n**Use it when** logic outgrows a single component field, or when two places need\nthe same behaviour.\nUse a small adapter when a dependency is owned by the runtime environment\nrather than by your application.\n\nThe contrast with `inject(...)` scattered across classes is the point:\ndependencies here are explicit and **type-visible**, which is what the route DI\ncheck and the test registers read.\n\n```typescript\nimport { craftService } from '@craft-ts/core';\n```\n\nService inputs that can change should be consumed as yieldable readers\n(`CraftServiceInput<T>`), the service counterpart of a component `Input<T>`.\nYield them so the input-to-service edge stays in the dependency graph:\n\n```typescript\nimport { craftService, query, type CraftServiceInput } from '@craft-ts/core';\n\nconst { UserQuery } = craftService(\n { name: 'UserQuery', providedIn: 'global' },\n (inputs: { userId: CraftServiceInput<string | undefined> }) =>\n query('userQuery', {\n params: function* () {\n return yield* inputs.userId();\n },\n loader: ({ params }) => ApiService.getItemById(params),\n }),\n);\n```\n\nThe call site still accepts a resolved value, a signal, or a Craft\nreader — the service boundary adapts it into that reader. Inside the factory,\nalways `yield* inputs.x()`.\n\n## What you get\n\nDeclaring a service gives you a set of generated helpers. For one named\n`Counter`:\n\n- `Counter(...)` — consume or compose it inside a craft generator\n- `Counter.someProperty(...)` — derive one public property directly\n- `provideCounter(...)` — for provider-capable scopes\n- `COUNTER_META_DATA` — for metadata-driven tooling\n- `CounterRequirement` — for `abstract` services\n- `provideCounter(factory)` — on `abstract` services, to implement the contract\n inline\n\nWhich of those exist depends on the scope.\n\n::: warning Breaking change — no more `injectX`\nThe generated helper is the service name itself: `X`. `craftService` no longer\nexports `injectX`, and the former `XToYield` helper is gone. Use `X()` in a craft\ngenerator and compose with `yield* X()`.\n:::\n\n## Supported scopes\n\nA service declares how many instances of it exist through `scope`:\n`function`, `toProvide`, `global`, `manuallyProvidedAtRoot` or `abstract`.\nDefault to `function`.\n\nEach scope and when to pick it: **[Service scopes](/guide/app/service-scopes)**.\n\n## The common case\n\n\n\n## Returning one primitive directly\n\nWhen a service exposes only one primitive, the factory can return its generator\ndirectly. `craftService` drives it and the generated service helper returns the\nprimitive reference:\n\n```typescript\nimport { craftService, query, type CraftServiceInput } from '@craft-ts/core';\n\nconst { UserQuery } = craftService(\n { name: 'UserQuery', providedIn: 'global' },\n (inputs: { userId: CraftServiceInput<string | undefined> }) =>\n query('userQuery', {\n params: function* () {\n return yield* inputs.userId();\n },\n loader: ({ params }) => ApiService.getItemById(params),\n }),\n);\n```\n\nFor several primitives, use `craftYieldRecord`. It resolves every generator in\nthe record and preserves the record keys:\n\n```typescript\nimport {\n craftService,\n craftYieldRecord,\n query,\n state,\n type CraftServiceInput,\n} from '@craft-ts/core';\n\nconst { UserQuery } = craftService(\n { name: 'UserQueryWithState', providedIn: 'global' },\n (inputs: { userId: CraftServiceInput<string | undefined> }) =>\n craftYieldRecord({\n userQuery: query('userQuery', {\n params: function* () {\n return yield* inputs.userId();\n },\n loader: ({ params }) => ApiService.getItemById(params),\n }),\n refresh: state('refresh', 0, ({ update }) => ({\n increment: () => update((value) => value + 1),\n })),\n }),\n);\n```\n\nInside a generator factory, the equivalent explicit form remains available:\n`const userQuery = yield* query(...)`.\n\n## Scoping providers to the service\n\nUse `providers` in the service config when the service factory itself needs locally-scoped dependencies:\n\n```typescript\nconst { UserFacade } = craftService(\n {\n name: 'UserFacade',\n providedIn: 'global',\n providers: [provideUserApi(), provideUserLogger()],\n },\n function* () {\n const api = yield* UserApi();\n const logger = yield* UserLogger();\n\n return {\n rename: (user: { id: string; name: string }, name: string) => {\n logger.log(`rename:${user.id}`);\n return api.updateUser({ ...user, name });\n },\n };\n },\n);\n```\n\nThis is separate from `provideUserFacade()`, which is only generated for provider-capable scopes like `toProvide`.\n\n## Composing services\n\n\n\n## Shaping the public API\n\n`yield* X()` can expose only part of a dependency, and `X.property()` derives a\nsingle one. See **[Shaping a service's public API](/guide/app/expose-api)**.\n\n## Contracts without an implementation\n\n`scope: 'abstract'` declares a contract that a provider must satisfy later. See\n**[Abstract services](/guide/app/abstract-services)**.\n\n## Startup work\n\n`craftService` also supports startup hooks through `appStart: true` and `yield* onAppStart(...)`.\n\nThe callback can be a plain function or a generator function. Use the generator form when startup logic needs to `yield*` crafted dependencies:\n\n\n\nDependencies used only inside that callback are still tracked on the parent service.\n\n## Pitfalls\n\n**Reaching for `global` by default.** A global service is a singleton for the\nwhole app, whether or not that was intended. Start at `function` — see\n[Service scopes](/guide/app/service-scopes).\n\n**`toProvide` without the provider.** A missing provider is reported by the route\nat compile time; the failure appears at runtime. The\n[route DI check](/guide/routing/setup) is what closes that hole.\n[Architecture tests](/guide/testing/architecture#assertroutediproofs) keep that\ncheck from quietly disappearing — a `CanRun` alias that nobody references still\ncompiles.\n\n**Returning the whole world.** What a service returns is its API. Return the\nnarrow thing; consumers that need more can yield more.\n\n## See Also\n\n- [Service scopes](/guide/app/service-scopes) — the one decision to make\n- [Shaping the public API](/guide/app/expose-api)\n- [Testing services](/guide/testing/services)\n"
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
63
|
"path": "/guide/app/expose-api",
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
{
|
|
183
183
|
"path": "/guide/create-project",
|
|
184
184
|
"title": "Create a CraftTS project",
|
|
185
|
-
"body": "# Create a CraftTS project\n\nUse `craft create` to generate a framework-independent CraftTS application\nwith routing, a typed API example, linting, tests, and the architecture\ncontract already wired up.\n\n## Prerequisites\n\nThe beta toolchain requires Node.js 20.19 or newer. The `craft` executable is\npublished by `@craft-ts/dev-tools`; it is not provided by the unrelated npm\npackage named `craft`.\n\nFor a new project, invoke the executable explicitly through `npx`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe first `--yes` belongs to `npx`: it accepts the temporary package\ninstallation. The command remains interactive because `craft create` itself\nwas not given `--yes`.\n\nThe command uses the published `beta` package. A checkout of CraftTS can\ncontain a newer creation flow than the version currently published on npm;\ncheck the resolved version with `npm view @craft-ts/dev-tools@beta version` if\nthe prompts shown by your terminal do not match this page.\n\n## Interactive creation\n\nRun the command in a real terminal without `craft create --yes`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe generator presents menus for:\n\n- the frontend runtime: `plain` or `effect`;\n- the backend runtime: `none`, `promise`, or `effect`;\n- type-safe i18n, its locales, and its default locale;\n- the design system;\n- typed CSS;\n- a standalone or Nx workspace;\n- CraftTS and, when Effect is selected, EffectTS source references for agent\n context;\n- integrations for Codex, Cursor, Claude Code, or Gemini CLI.\n\nThe frontend and backend choices are independent. To create a plain browser\napplication whose server functions use Effect v4, choose `plain` for the\nfrontend and `effect` for the backend.\n\nUse `↑`/`↓` to move and `Enter` to confirm a single choice. For locales and\nagent integrations, use `Space` to select or deselect several items, then\n`Enter` to confirm. The project directory remains a text field because it is\na free-form path. If the directory is omitted, the generator asks for it too:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create\n```\n\nThe agent question is a multi-selection list. Use `↑`/`↓` to move, `Space` to\nselect or deselect an integration, and `Enter` to confirm. Codex starts\nselected, preserving the default used by scripted creation. Every starter\nreceives an `AGENTS.md` project guide describing its selected runtimes and\nfeatures; selected integrations additionally receive their editor-specific\nproject instructions and skills.\n\n### Creating inside an existing Git repository\n\nAn existing `.git` directory makes the destination non-empty. Generate into\nthe current repository with `--force`:\n\n```bash\ncd pet-foster-family\nnpx --yes --package @craft-ts/dev-tools@beta craft create . --force\n```\n\n`--force` only permits writing into a non-empty destination; it does not turn\noff the configuration prompts. Review generated file changes before\ncommitting when the repository already contains application code.\n\nDuring the interactive flow, reference sources are cloned by default:\n\n- CraftTS sources go into `.references/craft-ts`;\n- EffectTS sources are also cloned when an Effect frontend or backend is\n selected;\n- the sources are available to agents without replacing the installed npm\n packages.\n\nAnswer `n` to opt out. In non-interactive mode, references remain opt-in so\nthat `--yes` does not silently perform network clones; use\n`--references=craft-ts` or `--references=all` explicitly.\n\nThe cloned repositories are reference material for coding agents only. The\ngenerated application always imports the published CraftTS and EffectTS npm\npackages from `package.json`; it does not use `file:` dependencies or\nTypeScript/Vite aliases to the clones. Use `npm run update:references` to fetch\nthe requested refs and refresh the recorded SHAs.\n\n## Non-interactive creation\n\nPass `--yes` after `create` to use defaults and disable all prompts. Combine it\nwith explicit options when the generated configuration must be reproducible:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --agents=codex\n```\n\nFor a minimal plain starter:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --i18n=none --design-system=none --no-typed-css \\\n --agents=none\n```\n\nTo create a backend-only Effect project and clone both reference sources:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --frontend-runtime=plain --backend-runtime=effect \\\n --references=all\n```\n\nThe main configuration options are:\n\n| Option | Values | Purpose |\n| -------------------- | ------------------------------------- | ----------------------------------------------------------------------- |\n| `--effect` | `v4`, `none` | Select the Effect v4 or plain starter |\n| `--frontend-runtime` | `plain`, `effect` | Choose the frontend runtime |\n| `--backend-runtime` | `none`, `promise`, `effect` | Choose server functions |\n| `--effect-scope` | `none`, `frontend`, `backend`, `both` | Set Effect placement |\n| `--agents` | comma-separated names or `none` | Add editor-specific agent integrations; `AGENTS.md` is always generated |\n| `--i18n` | `strict`, `loose`, `none` | Configure type-safe i18n |\n| `--design-system` | `basic`, `none` | Include the design-system starter |\n| `--typed-css` | flag / `--no-typed-css` | Enable or disable typed CSS |\n| `--workspace` | `standalone`, `nx` | Choose the workspace layout |\n| `--references` | `none`, `craft-ts`, `all` | Include source references |\n| `--no-demos` | flag | Generate a domain feature without explanatory demo pages |\n| `--domain` | slug | Name the first domain feature when using `--no-demos` |\n| `--force` | flag | Allow an existing non-empty destination |\n| `--json` | flag | Print the effective configuration as JSON |\n\nUse `craft create --help` to see the complete list:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create --help\n```\n\nFor a domain-first starting point, omit the explanatory home/services/about\npages and name the feature explicitly:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create pet-foster \\\n --yes --no-demos --domain animal --frontend-runtime=effect \\\n --backend-runtime=effect\n```\n\nThe generated feature lives under `src/app/features/animal/`. Add a form to\nthat feature with the existing primitives and its unit/submission test:\n\n```bash\ncraft add form animal\n# advanced nested/schema variant:\ncraft add form animal --advanced\n```\n\n## After generation\n\nThe generator creates a Git repository when the destination is not already\ninside another repository. It does not create a commit. The generated\n`.gitignore` excludes `node_modules/`, build outputs, test reports, and local\nreference clones.\n\nInstall dependencies and start the generated application:\n\n```bash\ncd my-app\nnpm install\nnpm run dev\n```\n\nThe generated project also includes the following checks:\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm test\nnpm run architecture\nnpm run build\n```\n\nWith a backend, `src/server/application.ts` owns the registry and runtime\nLayer, while `src/server/node-http.ts` is only the Node stream adapter.\n`server.ts` re-exports both for compatibility. In the backend-only Effect\nprofile, the browser remains plain CraftTS; Effect services, middleware and\nerror projections stay under the server boundary.\n\n## Troubleshooting\n\n### `could not determine executable to run`\n\nIf the error mentions `craft@0.1.0`, `npx` resolved the unrelated public npm\npackage named `craft`. Use the explicit `--package @craft-ts/dev-tools@beta`\nform shown above.\n\nIf `@craft-ts/dev-tools` is already installed in the project, its local binary\ncan also be called with:\n\n```bash\nnpx craft create my-app\n```\n\nThe explicit form is still the safest command when bootstrapping a project\nthat has no `package.json` yet.\n"
|
|
185
|
+
"body": "# Create a CraftTS project\n\nUse `craft create` to generate a framework-independent CraftTS application\nwith routing, a typed API example, linting, tests, and the architecture\ncontract already wired up.\n\n## Prerequisites\n\nThe beta toolchain requires Node.js 20.19 or newer. The `craft` executable is\npublished by `@craft-ts/dev-tools`; it is not provided by the unrelated npm\npackage named `craft`.\n\nFor a new project, invoke the executable explicitly through `npx`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe first `--yes` belongs to `npx`: it accepts the temporary package\ninstallation. The command remains interactive because `craft create` itself\nwas not given `--yes`.\n\nThe command uses the published `beta` package. A checkout of CraftTS can\ncontain a newer creation flow than the version currently published on npm;\ncheck the resolved version with `npm view @craft-ts/dev-tools@beta version` if\nthe prompts shown by your terminal do not match this page.\n\n## Interactive creation\n\nRun the command in a real terminal without `craft create --yes`:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app\n```\n\nThe generator presents menus in this order:\n\n- the application type: frontend-only or full-stack;\n- for a full-stack app, the backend runtime: `promise` or `effect` (EffectTS\n v4 is recommended);\n- the frontend runtime: `plain` or `effect`;\n- type-safe i18n, its locales, and its default locale;\n- the design system;\n- typed CSS;\n- a standalone or Nx workspace;\n- integrations for Codex, Cursor, or Cloud Code.\n\nThe frontend and backend choices are independent. To create a plain browser\napplication whose server functions use Effect v4, choose `plain` for the\nfrontend and `effect` for the backend.\n\nUse `↑`/`↓` to move and `Enter` to confirm a single choice. For locales and\nagent integrations, use `Space` to select or deselect several items, then\n`Enter` to confirm. The project directory remains a text field because it is\na free-form path. If the directory is omitted, the generator asks for it too:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create\n```\n\nThe agent question is a multi-selection list. Use `↑`/`↓` to move, `Space` to\nselect or deselect an integration, and `Enter` to confirm. Codex starts\nselected, preserving the default used by scripted creation. Every starter\nreceives an `AGENTS.md` project guide describing its selected runtimes and\nfeatures; selected integrations additionally receive their editor-specific\nproject instructions and skills.\n\n## Agent-assisted creation\n\nWhen an agent starts a new project, it should first ask what kind of\napplication is being built and what its main features are, without collecting\ndetailed requirements yet. If those features imply a backend, it should\npropose EffectTS v4 for the backend and explain that its typed services, Layers\nand errors fit CraftTS's typed server boundary. The user can confirm that\nstack, reject it, or name another backend; the agent must not add an EffectTS\nbackend after an explicit rejection.\n\nThe agent should create a domain-ready but empty starter with the design\nsystem, typed CSS and strict i18n enabled, and without the explanatory demo\npages:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --no-demos --domain app \\\n --frontend-runtime=plain --backend-runtime=effect \\\n --i18n=strict --design-system=basic --typed-css \\\n --references=all --agents=codex\n```\n\nUse `--backend-runtime=none` when the user declines a backend, or the explicit\nrequested backend when it is supported. When no Effect runtime is selected,\nuse `--references=craft-ts` instead of `--references=all`. The `--no-demos`\nstarter still\ncontains the architecture/tooling baseline and a domain boundary, but no\nprefilled product pages or demo content.\n\n### Creating inside an existing Git repository\n\nAn existing `.git` directory makes the destination non-empty. Generate into\nthe current repository with `--force`:\n\n```bash\ncd pet-foster-family\nnpx --yes --package @craft-ts/dev-tools@beta craft create . --force\n```\n\n`--force` only permits writing into a non-empty destination; it does not turn\noff the configuration prompts. Review generated file changes before\ncommitting when the repository already contains application code.\n\nDuring the interactive flow, reference sources are cloned automatically:\n\n- CraftTS sources go into `.references/craft-ts`;\n- EffectTS sources are also cloned when an Effect frontend or backend is\n selected;\n- the sources are available to agents without replacing the installed npm\n packages.\n\nThere is no reference confirmation prompt. The same defaults apply in\nnon-interactive mode: CraftTS is cloned, and EffectTS is cloned whenever an\nEffect frontend or backend is selected. Use `--references=none` to opt out, or\n`--references=craft-ts` / `--references=all` to choose explicitly.\n\nThe cloned repositories are reference material for coding agents only. The\ngenerated application always imports the published CraftTS and EffectTS npm\npackages from `package.json`; it does not use `file:` dependencies or\nTypeScript/Vite aliases to the clones. Use `npm run update:references` to fetch\nthe requested refs and refresh the recorded SHAs.\n\n## Non-interactive creation\n\nPass `--yes` after `create` to use defaults and disable all prompts. Combine it\nwith explicit options when the generated configuration must be reproducible:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --agents=codex\n```\n\nFor a minimal plain starter:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --effect=none --i18n=none --design-system=none --no-typed-css \\\n --agents=none\n```\n\nTo create a backend-only Effect project and clone both reference sources:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create my-app \\\n --yes --frontend-runtime=plain --backend-runtime=effect \\\n --references=all\n```\n\nThe main configuration options are:\n\n| Option | Values | Purpose |\n| -------------------- | ------------------------------------- | ------------------------------------------------------------------------- |\n| `--effect` | `v4`, `none` | Select the Effect v4 or plain starter |\n| `--frontend-runtime` | `plain`, `effect` | Choose the frontend runtime |\n| `--backend-runtime` | `none`, `promise`, `effect` | Choose server functions |\n| `--effect-scope` | `none`, `frontend`, `backend`, `both` | Set Effect placement |\n| `--agents` | comma-separated names or `none` | Add editor-specific agent integrations; `AGENTS.md` is always generated |\n| `--i18n` | `strict`, `loose`, `none` | Configure type-safe i18n |\n| `--design-system` | `basic`, `none` | Include the design-system starter |\n| `--typed-css` | flag / `--no-typed-css` | Enable or disable typed CSS |\n| `--workspace` | `standalone`, `nx` | Choose the workspace layout |\n| `--references` | `none`, `craft-ts`, `all` | Include source references (default: CraftTS, plus EffectTS when selected) |\n| `--no-demos` | flag | Generate a domain feature without explanatory demo pages |\n| `--domain` | slug | Name the first domain feature when using `--no-demos` |\n| `--force` | flag | Allow an existing non-empty destination |\n| `--json` | flag | Print the effective configuration as JSON |\n\nUse `craft create --help` to see the complete list:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create --help\n```\n\nFor a domain-first starting point, omit the explanatory home/services/about\npages and name the feature explicitly:\n\n```bash\nnpx --yes --package @craft-ts/dev-tools@beta craft create pet-foster \\\n --yes --no-demos --domain animal --frontend-runtime=effect \\\n --backend-runtime=effect\n```\n\nThe generated feature lives under `src/app/features/animal/`. Add a form to\nthat feature with the existing primitives and its unit/submission test:\n\n```bash\ncraft add form animal\n# advanced nested/schema variant:\ncraft add form animal --advanced\n```\n\n## After generation\n\nThe generator creates a Git repository when the destination is not already\ninside another repository. It does not create a commit. The generated\n`.gitignore` excludes `node_modules/`, build outputs, test reports, and local\nreference clones.\n\nInstall dependencies and start the generated application:\n\n```bash\ncd my-app\nnpm install\nnpm run dev\n```\n\nThe generated project also includes the following checks:\n\n```bash\nnpm run lint\nnpm run typecheck\nnpm test\nnpm run architecture\nnpm run build\n```\n\nWith a backend, `src/server/application.ts` owns the registry and runtime\nLayer, while `src/server/node-http.ts` is only the Node stream adapter.\n`server.ts` re-exports both for compatibility. In the backend-only Effect\nprofile, the browser remains plain CraftTS; Effect services, middleware and\nerror projections stay under the server boundary.\n\n## Troubleshooting\n\n### `could not determine executable to run`\n\nIf the error mentions `craft@0.1.0`, `npx` resolved the unrelated public npm\npackage named `craft`. Use the explicit `--package @craft-ts/dev-tools@beta`\nform shown above.\n\nIf `@craft-ts/dev-tools` is already installed in the project, its local binary\ncan also be called with:\n\n```bash\nnpx craft create my-app\n```\n\nThe explicit form is still the safest command when bootstrapping a project\nthat has no `package.json` yet.\n"
|
|
186
186
|
},
|
|
187
187
|
{
|
|
188
188
|
"path": "/guide/deployment",
|
|
@@ -242,27 +242,27 @@
|
|
|
242
242
|
{
|
|
243
243
|
"path": "/guide/i18n",
|
|
244
244
|
"title": "Type-safe i18n",
|
|
245
|
-
"body": "# Type-safe i18n\n\n`@craft-ts/i18n`
|
|
245
|
+
"body": "# Type-safe i18n\n\n`@craft-ts/i18n` is the CraftTS i18n integration. The catalogue remains a plain\ndeclarative TypeScript value, while DI-aware tokens use the existing CraftTS\nservice contracts. A catalogue that does not use DI can still be formatted by\n`runtime.t`; a catalogue with DI is rendered through the reactive CraftTS\ntranslator so its dependencies are checked like component dependencies.\n\n## The contract\n\nSix things are guaranteed, and all six are checked before the app runs.\n\n| guarantee | what it costs you to break |\n| ----------------------------------------------------------- | ------------------------------------------------------------------- |\n| the key set is a **closed union** | an unknown key does not compile — no silent `order.totl` |\n| every locale has the **same keys with the same parameters** | a translation you forgot is a compile error, not a fallback |\n| parameters are **typed by their token** | a date cannot be passed where a currency amount belongs |\n| a plural carries **every category the locale requires** | Polish needs `one`/`few`/`many`/`other`; French needs `one`/`other` |\n| a DI-aware token declares its **CraftTS services** | a missing provider is a compile error at the component/route boundary |\n| a token declared with a **schema** types its own input | the call site passes what the schema parses, not what the formatter wants |\n\nThe usual failure mode of a translation layer is that all of these are\nruntime concerns: a missing key renders its own name, a wrong parameter renders\n`[object Object]`, and a missing plural category renders the wrong branch to the\nusers of one locale only. None of that is observable from the code that calls\n`t`.\n\n## The shape of it\n\n```\nsrc/i18n/\n catalog.ts the reference locale — defineCatalog + msg + plural\n locales/fr-FR.ts every other locale — defineLocaleLike\n project-tokens.ts business tokens: defineToken / defineTokenFactory\n runtime.ts createI18nRuntime, and the reactive binding\n```\n\nA key is its dotted path: `order.total` reaches\n`{ order: { total: msg`…` } }`.\n\n## Where to go next\n\n- [The catalogue](./catalog.md) — `defineCatalog`, `msg`, `plural`,\n `defineLocale`, `defineLocaleLike`.\n- [Tokens](./tokens.md) — the shipped semantic tokens, and how to add your own.\n- [The runtime](./runtime.md) — `createI18nRuntime`, `t`, `bind`, lazy locales.\n- [With Effect](./effect.md) — `@craft-ts/i18n-effect`.\n\nTwo checks belong in CI, and `craft create` wires both:\n\n```bash\nnpm run i18n:check\nnpm run i18n:test\n```\n\nA working example lives in the demo, at `apps/demo/src/app/examples/i18n/`.\n\n## Guard visible text in Craft templates\n\n`craft create` enables this preset for every project generated **with** i18n —\nits own pages already take their copy from the catalogue. A project generated\nwithout i18n never sees the rule. To add it by hand to an existing application:\n\n```js\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n plugins: { 'craft-ts': craftRules },\n rules: { ...craftRules.configs.i18n.rules },\n },\n];\n```\n\n`craft-ts/require-i18n-text` reports static text in visible headings,\nparagraphs, labels, buttons, links and options, plus visible `placeholder`,\n`aria-label` and `title` attributes — and it looks *inside* the visible\nposition, so `p('Total: ' + t('cart.total'))`, `` span(`Total: ${amount}`) ``,\n`label(isNew ? 'New' : 'Returning')`, `p(name || 'Anonymous')` and a literal in\na children array are reported too. Only what carries letters counts: `first + ' ' + last`\nis glue between values, not copy. Dynamic business values, `i18n.t(...)`, its\nkey and parameters, a generator child and catalogue files are accepted. Server files and tests are excluded so\ntechnical messages and assertions can remain literal. The rule stays separate\nfrom the recommended preset, because it only makes sense once the catalogue is\nthe application's source of truth — which is exactly the condition `craft\ncreate` checks when it decides to enable it.\n"
|
|
246
246
|
},
|
|
247
247
|
{
|
|
248
248
|
"path": "/guide/i18n/catalog",
|
|
249
249
|
"title": "The catalogue",
|
|
250
|
-
"body": "# The catalogue\n\nA catalogue is a plain nested object. Nothing is parsed, nothing is loaded from\nJSON at build time, and every guarantee on this page comes from the type of the\nvalue itself.\n\n\n\n## Tokens name the parameters\n\n\n\nA token carries a **name** and a **formatter**. The name becomes the parameter\nkey; the formatter decides how the value is rendered in the active locale. That\nis why `msg` can derive the params type of a message from the tokens it\ninterpolates — see [Tokens](./tokens.md) for the full list.\n\n## `defineCatalog`, `msg`, `plural`\n\n\n\n`msg` is a **tagged template**: the literal parts are text, the interpolations\nare tokens. `` msg`Order total ${amount}.` `` has params `{ amount: number }`,\nand nothing else.\n\n`plural(count, branches)` takes the counting token and one message per category.\nWhich categories are _required_ is decided by the locale id, not by you:\n`defineLocale('pl-PL', …)` will not accept a plural missing `few` or `many`.\nThat check is a type error, before any Polish speaker sees the wrong branch.\n\nKeys nest as deeply as you like; the key used at the call site is the dotted\npath.\n\n## Every other locale is `defineLocaleLike`\n\n\n\n`defineLocale` is for the **reference** locale — the one that decides what the\nkey set is. Every other locale goes through `defineLocaleLike(reference, id,\ncatalog)`, which checks three things against the reference at compile time:\n\n- the same keys, no more and no fewer;\n- the same parameters on every message;\n- the plural categories that _this_ locale requires, which may differ from the\n reference's.\n\nA renamed key in the reference therefore breaks every translation file that\nstill has the old name, which is the entire point. It also runs\n`assertLocaleParity` at construction, so a mismatch that slips past the types —\na catalogue built dynamically, say — still fails loudly rather than rendering a\nkey name.\n\n## Checking outside the typechecker\n\n```bash\nnpm run i18n:check\n```\n\nRuns catalogue validation and locale parity as an ordinary command, so CI and a\npre-commit hook can see what `tsc` sees. Under the hood it is\n`validateCatalog` / `assertValidCatalog` (also exported from\n`@craft-ts/i18n/testing`) and `validateLocaleParity` / `assertLocaleParity`.\n\nWhen it fails, it names the key and the locale. Add the key; do not loosen the\ncatalogue's type to make the message go away.\n\n## Next\n\n- [Tokens](./tokens.md) — what `amount` and `count` above actually are.\n- [The runtime](./runtime.md) — turning these locales into a `t`.\n"
|
|
250
|
+
"body": "# The catalogue\n\nA catalogue is a plain nested object. Nothing is parsed, nothing is loaded from\nJSON at build time, and every guarantee on this page comes from the type of the\nvalue itself.\n\n\n\n## Tokens name the parameters\n\n\n\nA token carries a **name** and a **formatter**. The name becomes the parameter\nkey; the formatter decides how the value is rendered in the active locale. That\nis why `msg` can derive the params type of a message from the tokens it\ninterpolates — see [Tokens](./tokens.md) for the full list.\n\n## `defineCatalog`, `msg`, `plural`\n\n\n\n`msg` is a **tagged template**: the literal parts are text, the interpolations\nare tokens. `` msg`Order total ${amount}.` `` has params `{ amount: number }`,\nand nothing else.\n\n`plural(count, branches)` takes the counting token and one message per category.\nWhich categories are _required_ is decided by the locale id, not by you:\n`defineLocale('pl-PL', …)` will not accept a plural missing `few` or `many`.\nThat check is a type error, before any Polish speaker sees the wrong branch.\n\nKeys nest as deeply as you like; the key used at the call site is the dotted\npath.\n\n## Every other locale is `defineLocaleLike`\n\n\n\n`defineLocale` is for the **reference** locale — the one that decides what the\nkey set is. Every other locale goes through `defineLocaleLike(reference, id,\ncatalog)`, which checks three things against the reference at compile time:\n\n- the same keys, no more and no fewer;\n- the same parameters on every message;\n- the plural categories that _this_ locale requires, which may differ from the\n reference's.\n\n`assertLocaleParity` adds one check the types cannot express: two locales must\nalso agree on **how** each token is resolved. A locale that swapped a\nservice-resolved money token for a static one, or dropped a parameter's schema,\nrenders through a different path — it is reported as a `LOCALE_MISMATCH` rather\nthan silently formatting in the wrong currency.\n\nA renamed key in the reference therefore breaks every translation file that\nstill has the old name, which is the entire point. It also runs\n`assertLocaleParity` at construction, so a mismatch that slips past the types —\na catalogue built dynamically, say — still fails loudly rather than rendering a\nkey name.\n\n## Checking outside the typechecker\n\n```bash\nnpm run i18n:check\n```\n\nRuns catalogue validation and locale parity as an ordinary command, so CI and a\npre-commit hook can see what `tsc` sees. Under the hood it is\n`validateCatalog` / `assertValidCatalog` (also exported from\n`@craft-ts/i18n/testing`) and `validateLocaleParity` / `assertLocaleParity`.\n\nWhen it fails, it names the key and the locale. Add the key; do not loosen the\ncatalogue's type to make the message go away.\n\n## Delivering a catalogue as data\n\n`serializeCatalog(catalog)` produces a JSON-safe shape where each token is\nreduced to its stable `tokenId` and parameter name — the receiving application\nregisters the executable formatters. A token that parses its input is marked, and\na token whose formatter is resolved from the injector is **refused**: its\nformatter only exists at render time, so a serialised copy would silently format\nwith the default options. Deliver such a message from the application instead.\n\n## Next\n\n- [Tokens](./tokens.md) — what `amount` and `count` above actually are.\n- [The runtime](./runtime.md) — turning these locales into a `t`.\n"
|
|
251
251
|
},
|
|
252
252
|
{
|
|
253
253
|
"path": "/guide/i18n/effect",
|
|
254
254
|
"title": "i18n with Effect",
|
|
255
|
-
"body": "# i18n with Effect\n\n`@craft-ts/i18n-effect` is an **adapter, and only an adapter**. It exposes three\nthings — a service tag, a `Layer`, and one function — over a runtime you built\nthe ordinary way. `@craft-ts/i18n` itself never imports Effect, and plain\ncomponent code should keep calling `t` directly.\n\n## The Layer\n\n\n\n`provideI18nRuntime(runtime)` returns `Layer.Layer<I18nEffectService>`. It wraps\nthe runtime you already have, so there is exactly one active locale in the\nprocess — the Effect side does not get its own.\n\n## Bind the locales once\n\n`translateEffect` has no value parameter carrying the locales, so TypeScript has\nnothing to infer them from. Called bare, its key parameter resolves to `never`\nand **even a valid key is rejected**. Bind them once, in the same file as the\nLayer:\n\n\n\nFrom there, `t` has the closed key union and the typed params back. Passing the\ntype arguments at every call site —\n`translateEffect<typeof locales, 'order.total'>(…)` — works too, and is what\nthis wrapper spares you.\n\n## `translateEffect`\n\n\n\nThe signature is `translateEffect(key, params) =>\nEffect.Effect<string, never, I18nEffectService>`. Same closed key union, same\ntyped params, same string as `runtime.t` — the snippet above is checked against\n`runtime.t` in the docs test suite rather than trusted.\n\nThe error channel is `never` on purpose: a translation that reaches this point\ncannot fail on a bad key or a bad parameter, because neither compiles. What\n_can_ fail is the locale not being loaded, and that is a defect in the app's\nstartup, which is why it throws `I18nRuntimeError` rather than becoming a typed\nfailure every call site would have to handle.\n\n## When to reach for it\n\nUse `translateEffect` **inside an Effect program** — a domain service building a\nmessage, a server handler rendering an email. In a component,
|
|
255
|
+
"body": "# i18n with Effect\n\n`@craft-ts/i18n-effect` is an **adapter, and only an adapter**. It exposes three\nthings — a service tag, a `Layer`, and one function — over a runtime you built\nthe ordinary way. `@craft-ts/i18n` itself never imports Effect, and plain\ncomponent code should keep calling `t` directly.\n\n## The Layer\n\n\n\n`provideI18nRuntime(runtime)` returns `Layer.Layer<I18nEffectService>`. It wraps\nthe runtime you already have, so there is exactly one active locale in the\nprocess — the Effect side does not get its own.\n\n## Bind the locales once\n\n`translateEffect` has no value parameter carrying the locales, so TypeScript has\nnothing to infer them from. Called bare, its key parameter resolves to `never`\nand **even a valid key is rejected**. Bind them once, in the same file as the\nLayer:\n\n\n\nFrom there, `t` has the closed key union and the typed params back. Passing the\ntype arguments at every call site —\n`translateEffect<typeof locales, 'order.total'>(…)` — works too, and is what\nthis wrapper spares you.\n\n## `translateEffect`\n\n\n\nThe signature is `translateEffect(key, params) =>\nEffect.Effect<string, never, I18nEffectService>`. Same closed key union, same\ntyped params, same string as `runtime.t` — the snippet above is checked against\n`runtime.t` in the docs test suite rather than trusted.\n\nThe error channel is `never` on purpose: a translation that reaches this point\ncannot fail on a bad key or a bad parameter, because neither compiles. What\n_can_ fail is the locale not being loaded, and that is a defect in the app's\nstartup, which is why it throws `I18nRuntimeError` rather than becoming a typed\nfailure every call site would have to handle.\n\n## When to reach for it\n\nUse `translateEffect` **inside an Effect program** — a domain service building a\nmessage, a server handler rendering an email. In a component, the bound\ntranslator is the shorter path, and reaching for Effect just to format a string\nadds a requirement to the program for nothing.\n\nAn Effect program is not a Craft injection context, so `translateEffect` accepts\n`StaticTranslationKey` — the keys whose formatting resolves no service. A\nmessage whose token yields a Craft service is rendered by the component-side\ntranslator; that is what keeps the `never` error channel above honest.\n\nSee also the [Effect adapters](../advanced/effect.md) page for the rest of the\n`@craft-ts/*-effect` family.\n"
|
|
256
256
|
},
|
|
257
257
|
{
|
|
258
258
|
"path": "/guide/i18n/runtime",
|
|
259
259
|
"title": "The runtime",
|
|
260
|
-
"body": "# The runtime\n\n`createI18nRuntime` turns a set of locales into the object the application\ntranslates through. It holds one active locale, and it is deliberately small:\n`locale`, `setLocale`, `translate` (aliased `t`), `bind`, `loadLocale`.\n\n\n\n`strict` defaults to **on**. At construction, every catalogue is validated and\nevery locale is checked for parity against the first one — so a catalogue built\nin a way the types could not see still fails at startup rather than at the\nmoment a user opens the page that needs it. Pass `strict: false` only when you\nhave a reason you can write down.\n\n`timeZone` belongs on the runtime, once. Putting it on each call site is how two\ndates in the same view end up in two zones.\n\n## Translating\n\n\n\n`t` **is** `translate` — the same function under two names, so a call site can\nread as `t('order.total', …)` without a local alias. The params argument is\noptional exactly when the message has no parameters, and required, with its\nexact shape, when it does.\n\n`setLocale(id)` throws `I18nRuntimeError` with the code `LOCALE_NOT_LOADED` for\na locale the runtime does not hold. So does `t`, if the active locale was\nsomehow never loaded. The error is not a formatting failure to be swallowed: it\nmeans the app is about to render the wrong language.\n\n## Reactive translation\n\nA string that does not change when the locale changes is not a translation.\n`runtime.bind(dependency)` returns a translator whose result re-reads whenever\nthe dependency does — the dependency being an ordinary Craft reader, typically\nthe `state` that holds the active locale:\n\n\n\n`translate('order.items', { count })` then returns a generator the template\nyields like any other Craft reader. One service owns the locale for the whole\napp; components consume it rather than each building a local binding, which is\nwhat keeps two components from disagreeing about which language is on screen.\n\n## Loading catalogues\n\n\n\n`createI18nLoader` caches by id and — the part that matters — **evicts a failed\nload**, so a catalogue whose chunk died on a flaky network can be retried\ninstead of staying permanently poisoned. `loadLocale(id)` resolves once the\ncatalogue is in; only then does `setLocale` accept it.\n\n::: warning A locale must be listed to be named\n`setLocale` and `loadLocale` are keyed on the ids in `locales`, so today a\nlocale that is **not** in that array cannot be named without a cast — while a\nlocale that _is_ in it counts as already loaded and never reaches the loader.\nIn practice that means the fully lazy catalogue is not expressible in the types\nyet. List every locale, and treat `loader` as the retry-safe cache in front of\nwhatever your own loading code does.\n:::\n\nA lazily obtained locale is not present at construction, so it is **not**\ncovered by the startup parity check. Keep it covered by `npm run i18n:check`,\nwhich reads the files rather than the runtime.\n\n## Next\n\n- [With Effect](./effect.md) — the same keys, as an `Effect`.\n"
|
|
260
|
+
"body": "# The runtime\n\n`createI18nRuntime` turns a set of locales into the object the application\ntranslates through. It holds one active locale, and it is deliberately small:\n`locale`, `setLocale`, `translate` (aliased `t`), `bind`, `loadLocale`.\n\n\n\n`strict` defaults to **on**. At construction, every catalogue is validated and\nevery locale is checked for parity against the first one — so a catalogue built\nin a way the types could not see still fails at startup rather than at the\nmoment a user opens the page that needs it. Pass `strict: false` only when you\nhave a reason you can write down.\n\n`timeZone` belongs on the runtime, once. Putting it on each call site is how two\ndates in the same view end up in two zones.\n\n## Translating\n\n\n\n`t` **is** `translate` — the same function under two names, so a call site can\nread as `t('order.total', …)` without a local alias. The params argument is\noptional exactly when the message has no parameters, and required, with its\nexact shape, when it does.\n\n`setLocale(id)` throws `I18nRuntimeError` with the code `LOCALE_NOT_LOADED` for\na locale the runtime does not hold. So does `t`, if the active locale was\nsomehow never loaded. The error is not a formatting failure to be swallowed: it\nmeans the app is about to render the wrong language.\n\nThe other codes it raises, all for the same reason — rendering something wrong\nis worse than not rendering:\n\n| code | when |\n| -------------------------- | ------------------------------------------------------------------ |\n| `MISSING_PARAM` | a token's parameter is absent from the params object |\n| `INVALID_PARAM` | a guard rejected the value, or a schema's issues, quoted verbatim |\n| `ASYNC_SCHEMA` | a parameter's schema returned a promise; a message renders in sync |\n| `CRAFT_INJECTION_REQUIRED` | `t` met a token that resolves a service (see below) |\n| `INVALID_PLURAL_COUNT` | a plural selector that is not a finite number |\n| `UNKNOWN_KEY` | a key that no longer exists in the loaded catalogue |\n\n## Reactive translation\n\nA string that does not change when the locale changes is not a translation.\n`runtime.bind(dependency)` returns a translator whose result re-reads whenever\nthe dependency does — the dependency being an ordinary Craft reader, typically\nthe `state` that holds the active locale:\n\n\n\n`translate('order.items', { count })` then returns a generator the template\nyields like any other Craft reader. One service owns the locale for the whole\napp; components consume it rather than each building a local binding, which is\nwhat keeps two components from disagreeing about which language is on screen.\n\n### DI inside a translation\n\nDependencies belong to the token that needs them, not to the whole i18n\nruntime:\n\n```ts\nconst orderAmount = money('amount', function* () {\n const currency = yield* ClientCurrency();\n return { currency: currency.code, minimumFractionDigits: 2 };\n});\n\nconst catalog = defineCatalog({\n order: msg`Order total ${orderAmount}.`,\n});\n```\n\nIn a template, the translator's result is used exactly like any other child or\nattribute value — pass it, do not drive it:\n\n```ts\np(translate('order', { amount: 1234.5 }));\np({ title: translate('order', { amount: 1234.5 }) }, 'Order');\n```\n\nBoth forms carry `ClientCurrency` into the component dependency contract, so\nthe route check reports a missing provider at compile time, just as it does for\na service yielded by the component factory.\n\nThe reader is a function, so `yield* translate(...)` does not type-check; and\ndriving it yourself inside a template generator (`yield* translate(...)()`)\nhides the dependency from that check, exactly as a service yielded straight\nfrom a template does. Pass the reader.\n\n`t` refuses such a message at compile time: its key type is\n`StaticTranslationKey`, the keys whose formatting resolves nothing. A resolver\nthat yields no request is still a `t` key — the type and the runtime draw the\nsame line.\n\n## Loading catalogues\n\n\n\n`createI18nLoader` caches by id and — the part that matters — **evicts a failed\nload**, so a catalogue whose chunk died on a flaky network can be retried\ninstead of staying permanently poisoned. `loadLocale(id)` resolves once the\ncatalogue is in; only then does `setLocale` accept it.\n\n::: warning A locale must be listed to be named\n`setLocale` and `loadLocale` are keyed on the ids in `locales`, so today a\nlocale that is **not** in that array cannot be named without a cast — while a\nlocale that _is_ in it counts as already loaded and never reaches the loader.\nIn practice that means the fully lazy catalogue is not expressible in the types\nyet. List every locale, and treat `loader` as the retry-safe cache in front of\nwhatever your own loading code does.\n:::\n\nA lazily obtained locale is not present at construction, so it is **not**\ncovered by the startup parity check. Keep it covered by `npm run i18n:check`,\nwhich reads the files rather than the runtime.\n\n## Next\n\n- [With Effect](./effect.md) — the same keys, as an `Effect`.\n"
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
"path": "/guide/i18n/tokens",
|
|
264
264
|
"title": "Tokens",
|
|
265
|
-
"body": "# Tokens\n\nA token is the unit that makes a message parameter typed. It carries a **name**\n(the parameter key), a **kind**,
|
|
265
|
+
"body": "# Tokens\n\nA token is the unit that makes a message parameter typed. It carries a **name**\n(the parameter key), a **kind**, a way to check the value — a guard or a\n**schema** — and a way to render it: a **formatter**, or a **resolver** that\nbuilds one from the injector. Both see the active locale.\n\n## The shipped tokens\n\nThey are semantic, not stylistic, and every one of them formats through `Intl`,\nso the output follows the locale rather than a hand-written rule:\n\n\n\n| factory | parameter type | formats as |\n| ------------------------------------ | ---------------- | ----------------------------- |\n| `number`, `integer`, `compactNumber` | `number` | decimal, no fraction, compact |\n| `percent` | `number` | `0.125` → `12.5 %` |\n| `money` | `number` | currency, `EUR` by default |\n| `dateShort`, `dateLong`, `dateTime` | `Date \\| number` | date and date-time styles |\n| `relativeTime` | `number` | `-2` → `2 days ago` |\n\nEach is a factory: `factory(name, adapter?, options?)`. The **name** is what the\nparams object will be keyed by, so the same factory serves any number of\nparameters — `money('amount')` and `money('refund')` are two different tokens.\nThe **adapter** position takes any of three things: a type guard, a\n[Standard Schema](#validating-and-parsing-with-a-schema), or a\n[generator](#options-that-come-from-a-service) that resolves the options from a\nservice.\n\n### Options that come from a service\n\nEvery factory — not only `money` — accepts a CraftTS generator in place of the\nadapter when its options depend on a service:\n\n```ts\nconst orderAmount = money('amount', function* () {\n const currency = yield* ClientCurrency();\n return { currency: currency.code, minimumFractionDigits: 2 };\n});\n```\n\nThe yielded service is part of the token's type. It is therefore propagated to\nthe translation reader and then to the component/route DI check. A provider\nmissing from the reachable `craftComponent`/route scope fails compilation.\nThe generator runs when the message is rendered, not when the catalogue module\nis imported.\n\nIt must be a **generator function**. An arrow that returns a generator satisfies\nthe signature but is not one, so it is rejected rather than silently installed\nas the value guard.\n\n## Validating and parsing with a schema\n\nThe second argument also accepts a **Standard Schema**: the same contract\n`state`, `query` and forms already take, so a Zod, Valibot or ArkType schema\nwritten for the rest of the application drops in unchanged.\n\n```ts\nconst placedAt = dateLong('placedAt', z.coerce.date());\n\nmsg`Placed on ${placedAt}.`;\n// the call site passes a string, the formatter receives a Date\ntranslate('order', { placedAt: '2026-08-25T14:30:00Z' });\n```\n\nA schema is not only a guard: the parameter type is the schema's **input** and\nthe formatter receives its **output**. Parsing therefore happens once, in the\ncatalogue, instead of at every call site. An invalid value raises\n`I18nRuntimeError` with the schema's own issue messages, and an asynchronous\nschema is refused — a translation renders synchronously.\n\nA project token declared with `defineToken` takes the same `schema` field, and\nmay combine it with `resolveFormatter`: the parameter is parsed, the formatter\nis resolved from the injector.\n\n## Your own token\n\nBusiness vocabulary does not belong in a shared library. `defineToken` builds\none, and it looks exactly like a shipped token at the call site:\n\n\n\nThe `validate` guard is what keeps an arbitrary string out of the params type: a\nmessage that interpolates this token accepts `'paid' | 'pending' | 'refunded'`\nand nothing else. Without it, the parameter widens and the token stops earning\nits place. A `schema` field does the same job and can parse on the way in;\n`defineToken` accepts either, and may combine a schema with a `resolveFormatter`\nso the parameter is parsed and the formatter comes from the injector.\n\nA token can also resolve its formatter from the injector rather than carry one.\nHere the unit system is a Craft service, so the same catalogue renders\nkilogrammes for one user and pounds for another:\n\n\n\nThe yielded service travels with the message: pass the reader to the template\nand a missing provider is a compile error, exactly as for a service yielded by\nthe component factory. See\n[DI inside a translation](./runtime.md#di-inside-a-translation).\n\n`format` receives the value and a context carrying `locale` and, when the\nruntime was given one, `timeZone`. Keep the branching on `context.locale`\ncoarse — a language prefix, not a full locale match — unless you genuinely have\nper-region wording.\n\n## `format` or `resolveFormatter`, never both\n\nA token formats through exactly one of the two, and both renderers take\n`resolveFormatter` first whenever it is there:\n\n| declared | when the formatter is known |\n| ------------------ | -------------------------------------------------------------- |\n| `format` | when the catalogue is written — `formatters.money('EUR')` |\n| `resolveFormatter` | at render time, from the injector — the client's currency |\n\nSo a token with a resolver declares no `format`. `t` still renders it if the\nresolver yields nothing; the moment it yields a service request, the message\nbelongs to a bound translator and its key leaves `StaticTranslationKey`.\n\n`percent` takes a ratio, not a percentage: `0.125`, not `12.5`. That is `Intl`'s\nconvention and the token does not second-guess it.\n\n## A family of tokens\n\nWhen the same formatting rule serves several parameter names and options,\n`defineTokenFactory` builds the factory instead of the token:\n\n\n\nThat is exactly how `number`, `money` and the rest are built; there is no\nprivileged path for the shipped ones.\n\nConventionally these live in `src/i18n/project-tokens.ts`, which is where\n`craft create` puts them and what the generated agent skill points at.\n\n## Next\n\n- [The runtime](./runtime.md) — spending a catalogue built from these.\n"
|
|
266
266
|
},
|
|
267
267
|
{
|
|
268
268
|
"path": "/guide/migration/wave-1-tag-and-provided-in",
|
|
@@ -322,7 +322,7 @@
|
|
|
322
322
|
{
|
|
323
323
|
"path": "/guide/routing/eslint-rules",
|
|
324
324
|
"title": "ESLint rules",
|
|
325
|
-
"body": "# ESLint rules\n\nThe rule set is not decoration: several checks in this documentation only work\nbecause a rule generated or maintained the code they read. Others enforce the\narchitecture — no hidden runtime dependencies or direct transport calls — and most of them\n**autofix**.\n\n**Install them once** when you set up routing and type-safe DI.\n**Then lean on the quick fixes** rather than writing the boilerplate by hand.\n\n::: warning An ESLint error is not a compile error\nA missing autofix does not break the build. If you skip the quick fix after\nchanging a component's DI shape, `main.ts` keeps reading a stale `GenDeps_*` and\ncan miss a real DI error. Run `eslint --fix` in CI.\n:::\n\nThe plugin is exposed from `@craft-ts/dev-tools/eslint-rules`.\n\nFor a project using `@craft-ts/effect`, the published preset enables the Craft\nrules and the Effect adapter rule in one entry:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n files: ['**/*.ts'],\n ...craftRules.configs.effect,\n },\n];\n```\n\nUse `craftRules.configs.recommended` for projects that do not use Effect.\n\nAdd it to your ESLint flat config:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n // keep your existing ESLint config entries\n {\n files: ['**/*.ts'],\n plugins: {\n 'craft-ts': craftRules,\n },\n rules: {\n 'craft-ts/prefer-craft-template-blocks': 'error',\n 'craft-ts/no-render-writes': 'error',\n 'craft-ts/require-reactive-template-bindings': 'error',\n 'craft-ts/no-craft-use': 'error',\n 'craft-ts/no-type-assertions-in-template': 'error',\n 'craft-ts/no-ephemeral-template-form-state': 'error',\n 'craft-ts/template-element-name-unique': 'error',\n 'craft-ts/no-craft-computed-side-effects': 'error',\n 'craft-ts/require-craft-method-for-yieldable-callback': 'error',\n 'craft-ts/prefer-direct-yieldable-callback': 'error',\n 'craft-ts/require-yieldable-reactive-read': 'error',\n 'craft-ts/require-yieldable-template-method': 'error',\n 'craft-ts/require-yieldable-insertion-write': 'error',\n 'craft-ts/no-craft-service-component-same-file': 'error',\n 'craft-ts/prefer-craft-http-transport': 'error',\n 'craft-ts/no-injection-token': 'error',\n 'craft-ts/require-primitive-derived-property': 'error',\n 'craft-ts/no-async-await': 'error',\n 'craft-ts/no-throw': 'error',\n 'craft-ts/no-imperative-craft-resource-trigger': 'error',\n 'craft-ts/no-imperative-craft-method-actions': 'error',\n 'craft-ts/no-imperative-storage-in-craft-method': 'error',\n 'craft-ts/no-transition-actions': 'error',\n 'craft-ts/require-craft-resource-trigger-yield': 'error',\n 'craft-ts/require-assert-exhaustive-route-exceptions': 'error',\n 'craft-ts/require-craft-exception-handler': 'error',\n 'craft-ts/require-exception-component-di-check': 'error',\n 'craft-ts/require-pending-component-di-check': 'error',\n 'craft-ts/require-child-route-mount-check': 'error',\n 'craft-ts/require-lazy-load-with-retry': 'error',\n 'craft-ts/require-cascade-route-di-check': 'error',\n 'craft-ts/global-exception-registry-match': 'error',\n },\n },\n];\n```\n\nWhat each rule does:\n\n- `craft-ts/prefer-craft-template-blocks`: keeps `craftComponent(...)` templates declarative by rejecting ternaries, logical expressions, negations, and imperative control flow; use `ifNode(...)`, `matchNode.exhaustive(...)`, `forNode(...)`, or `deferNode(...)`\n- `craft-ts/no-render-writes`: rejects detectable `set()`, `update()`, and `mutate()` calls in component templates and render bindings while allowing DOM event and `onXxx` output callbacks\n- `craft-ts/require-reactive-template-bindings`: requires signals, named Craft values, and component inputs to be read inside granular binding callbacks instead of during VNode construction; static values remain valid\n- `craft-ts/no-craft-use`: forbids the synchronous `craftUse(...)` escape hatch in Craft TypeScript files; use a generator and delegate the reader with `yield*` instead\n- `craft-ts/no-type-assertions-in-template`: forbids `as ...` and angle-bracket type assertions in Craft templates; fix the type in the logic factory or expose a correctly typed derived value\n- `craft-ts/no-ephemeral-template-form-state`: forbids `let` / `const` / `var` in the fourth argument of `craftComponent(...)` and `craftDirective(...)` (inline or a same-file identifier). Declare that state in the logic factory with `state()` or `craftComputed()` instead\n- `craft-ts/template-element-name-unique`: requires named HTML helpers to use a static, unique local name within a component; use the object-first helper form for unnamed elements such as `p({ id: 'hint' }, ...)`\n- `craft-ts/no-craft-computed-side-effects`: forbids writes and asynchronous work inside `craftComputed`; only reactive reads and `settled(...)` are allowed. The graph-wide counterpart is [`assertCraftComputedPure`](/guide/testing/architecture#assertcraftcomputedpure).\n- `craft-ts/no-effect-outside-loaders`: keeps `params`, methods, `craftComputed(...)`, and `craftEffect(...)` synchronous by allowing Effect values and Effect service reads only in Effect loaders; `no-effect-in-params` remains as a compatibility alias\n- `craft-ts/sync-effect-body`: keeps a body declared synchronous (`SyncOp` in its requirements) free of anything that may suspend — async constructors such as `Effect.sleep`/`Effect.promise`, and members nothing declares synchronous. Type-aware: the ESLint parser must use `projectService: true` or a TypeScript `project`\n- `craft-ts/no-explicit-effect-type`: lets `Effect.gen` infer its complete type instead of repeating an explicit Effect annotation; contracts declared in interfaces and type aliases remain allowed\n- `craft-ts/prefer-inline-effect-insertion`: keeps the `queryEffect` insertion factory inline so its resource and exception types are inferred without a separate `InsertionParams` context alias\n- `craft-ts/prefer-inline-route-providers`: inlines a route provider tuple used only once by `loadCraftComponent(...)`, preserving the route-level type proof\n- `craft-ts/prefer-craft-reactivity`: rejects authored signal/computed/effect/resource APIs, explicit `.subscribe()` calls, and RxJS `Subject`/`BehaviorSubject`/`ReplaySubject`; use `state`, `craftComputed`, `craftEffect`, `query`, and named `source$`/`on$` flows\n- `craft-ts/prefer-craft-service`: keeps services in the `craftService(...)` model\n- `craft-ts/no-craft-service-component-same-file`: forbids declaring `craftService(...)` and `craftComponent(...)` in the same file; a route-level service provider combined with a lazy-loaded component can break lazy loading, so keep them in separate files\n- `craft-ts/no-injection-token`: forbids authored `InjectionToken` contracts; declare them with `craftService({ name, providedIn: 'abstract' }, abstract<Contract>())`\n- `craft-ts/prefer-craft-http-client`: forbids direct transport usage in favor of `CraftHttpClient`\n- `craft-ts/prefer-craft-http-transport`: forbids direct `fetch()` and `XMLHttpRequest`; use `query()` for reads or `mutation()` for writes with `CraftHttpClient`\n- `craft-ts/prefer-craft-input-output`: keeps component inputs and outputs in the `Input`/`Output` model used by `craftComponent(...)`\n- `craft-ts/require-primitive-derived-property`: requires a `computed` or `craftComputed` that only depends on one primitive in the same component/service to be exposed by that primitive's insertion; simple cases are autofixed\n- `craft-ts/no-async-await`: forbids `async` functions, `await`, and `for await...of`; use generator-based Craft primitives, `craftSleep`, and `CraftHttpClient` instead\n- `craft-ts/no-throw`: forbids `throw` in Craft code and offers a Quick Fix that returns `craftException({ _tag: 'UNEXPECTED_ERROR' }, { error: ... })`; keep technical boundaries and tests outside this rule when their contracts require thrown errors\n- `craft-ts/no-imperative-craft-resource-trigger`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` in a `craftEffect` dependency graph, including through `craftGen(...)`. The graph-wide counterpart, including `state` / `source$` writes, is [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync).\n- `craft-ts/no-imperative-craft-method-actions`: forbids composing multiple imperative actions in a `craftMethod`; emit a `source$` event and let the affected query react with `insertReactOnMutation(...)` instead. A handler such as `event.preventDefault()` followed by one `mutation.mutate(...)` remains valid.\n- `craft-ts/no-imperative-storage-in-craft-method`: forbids direct storage access and imperative location changes in a `craftMethod`; use `insertReactOnMutation(...)` with `optimisticUpdate: () => undefined` to clear the affected query and let its persistence follow the query state.\n- `craft-ts/no-transition-actions`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` inside `transitionStep(...)`; validate the event and emit a source, then let the resource react to that source.\n- `craft-ts/require-craft-resource-trigger-yield`: requires those triggers to use `yield*` inside generator functions, while ordinary UI callbacks may keep imperative calls\n- `craft-ts/require-craft-method-for-yieldable-callback`: requires callbacks returned by a `craftComponent` factory to wrap yieldable Craft method calls in `craftMethod(...)`\n- `craft-ts/prefer-direct-yieldable-callback`: replaces a template generator or generator method that only delegates `yield* callback()` with the callback reference itself (`callback` or `object.method`)\n- `craft-ts/require-yieldable-reactive-read`: requires Craft reactive readers to be delegated with `yield*` inside generator functions; a function that reads a Craft reader must itself be a generator\n- `craft-ts/require-yieldable-template-method`: requires yieldable Craft method calls in a `craftComponent` template to be delegated with `yield*`, or passed as a reference (`click: counter.increment`)\n- `craft-ts/require-yieldable-insertion-write`: requires `set(...)`, `patch(...)`, and `update(...)` to be delegated with `yield*` when they are used inside a generator method\n- `craft-ts/require-assert-exhaustive-route-exceptions`: adds the collection-level `assertExhaustiveRouteExceptions(...)` safety net\n- `craft-ts/require-craft-exception-handler`: enforces `craftExceptionHandler(function* (...) {})`; simple handlers are autofixed and ambiguous raw redirects are reported for manual migration\n- `craft-ts/require-exception-component-di-check`: generates O(1) `RouteExceptionComponentCheckedDI` checks for `renderComponent`, route-level `errorComponent`, `withErrorComponent`, `withRouteLoadError`, and route-local `provideRouteLoadErrorComponent`\n- `craft-ts/require-pending-component-di-check`: generates the independent `RouteCheckedDI` check for each `pendingComponent`\n- `craft-ts/no-raw-class`: forbids a `class:` binding that is a string, a template literal or a function, in any file that imports `@craft-ts/style`. A class assembled at render time is a visual state nothing recorded, so the [visual matrix](/guide/style/testing) would enumerate what the sheets declare while the DOM shows something else. Move the rule into the sheet and bind the class it returns; make the variation an axis and set a `data-*` attribute\n- `craft-ts/no-raw-css-value`: forbids a string or number literal as an argument to a `@craft-ts/style` helper — `p('12px')`, `bg('red')`. If the scale is missing the step, add it to the scale; if the value genuinely cannot be proven, `unsafeLength('13px', reason)` compiles and makes the debt countable in the [graph](/guide/style/testing#what-the-graph-adds)\n- `craft-ts/no-free-has`: forbids a hand-written `:has()` in styles. It reaches across the component boundary, so what a component looks like depends on markup it does not own — a state the matrix cannot enumerate. Use the `descendant` axis, which is a closed set and carries its own test driver\n- `craft-ts/style-file-boundary`: restricts a `*.style.ts` to style-vocabulary imports. The [build plugin](/guide/style/setup) imports the file in Node to read what it registered, so an application import would run application code at build time\n- `craft-ts/craft-css-token-registry`: reports a custom property registered with `@property` by two different components. A custom property may have only one owner; two silently fight over its syntax and initial value\n- `craft-ts/require-effect-adapters`: requires the Effect-aware adapters — `queryEffect`, `mutationEffect`, `asyncProcessEffect` — instead of the plain primitives in an Effect application. See [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter)\n- `craft-ts/craft-signal-source-name-match`: requires `signalSource(name, ...)` to take a string literal matching the variable, class property or object property it is assigned to, so the name in a trace is the name in the source. A computed name defeats the [architecture graph](/guide/testing/architecture), which reads these names statically\n- `craft-ts/require-child-route-mount-check`: adds the missing `assertChildRouteMounts(...)` call + import (Quick Fix) for any `craftRoutes(...)` collection that mounts lazy `loadChildren`, so a `.withParent`-pinned child mounted under the wrong path is a compile error\n- `craft-ts/require-lazy-load-with-retry`: wraps route `loadComponent` and `loadChildren` imports with the generated `withRetry(...)` loader helper while preserving a statically analyzable import specifier\n- `craft-ts/require-cascade-route-di-check`: rejects any `craftRoutes(...)` collection without a same-file `ValidateCascadeRoutesFile + CanRun` proof; its autofix adds the conservative `<never, Router>` context, which should be adjusted when the mount inherits providers\n- `craft-ts/global-exception-registry-match`: keeps `CraftGlobalExceptionRegistry` synchronized with handlers delegating to `globalError()`\n- `craft-ts/prefer-craft-router-link`: requires `CraftRouterLink` for internal `a(..., { href: ... })` navigation; external URLs, fragment links, downloads, `_blank`, and links marked with `data-navigation: 'external'` remain native\n\n### Accessibility (`craft-ts/a11y`)\n\nSpread `craftRules.configs.a11y.rules` to enable the WCAG 2.2 AA preset as\n`error`. The rules walk **all** hyperscript in the file (`craftTemplate`,\nextracted factories, `h('tag')`), not only `craftComponent` argument 3.\n\n- `prefer-named-html-helpers`: forbids `h('img')` / `h('button')` when a named helper exists\n- `require-interactive-local-name`: requires a string-literal first argument on interactive helpers; the local name is the third segment of `data-craft-name=\"${component}:${tag}:${localName}\"`\n- `img-has-alt`, `iframe-has-title`, `button-has-type`, `anchor-has-href`\n- `control-has-accessible-name`, `label-has-associated-control`, `heading-has-content`\n- `no-noninteractive-element-interactions`, `no-positive-tabindex`\n- `valid-aria`, `role-has-required-aria`, `target-blank-noopener`\n- `prefer-relative-heading`, `require-route-heading-outline`,\n `require-outlet-heading-section`, `no-heading-level-skip`\n- `require-focus-visible`, `require-reduced-motion` (CSS of `craftComponent`)\n\nSee [Accessibility](/guide/components/accessibility).\n\nThe two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor.\n\nThe template and reactivity rules are intentionally diagnostic-only: replacing a\nresource or subscription can change lifecycle and error semantics, so the rule\npoints at the Craft primitive without applying a potentially unsafe rewrite.\n\n### Why templates use blocks\n\nCraft template blocks preserve the branch structure in the type-level render\ncontract. A ternary or `condition && node` produces only a computed value, so\nthe type checker cannot assert which branch renders which content. Keep derived\nvalues and business decisions in the component's state/query layer, then make\nthe template express visibility explicitly:\n\n```ts\nifNode(\n isReady,\n () => p('Ready'),\n () => p('Loading…'),\n);\n\nmatchNode.exhaustive(query.exceptions, '_tag', {\n NOT_FOUND: () => p('Not found'),\n FORBIDDEN: () => p('Forbidden'),\n});\n```\n\nThis rule is for Craft's TypeScript templates. It does not rewrite external\ntemplate languages.\n\nThe same restriction applies to boolean expressions. A negation is still\napplication logic, even when it is used only for a DOM property:\n\n```ts\n// Incorrect: the template derives the disabled state.\nbutton(\n {\n disabled: function* () {\n return !(yield* machine.canGoBack());\n },\n },\n 'Back',\n);\n\n// Correct: derive it in the logic factory and bind the result.\nconst backDisabled = craftComputed('backDisabled', function* () {\n return !(yield* history.canGoBack());\n});\nreturn { backDisabled };\n```\n\nKeep the template to layout and binding. Move labels, formatted values,\nvalidation state, and other decisions into `state()` or `craftComputed()`.\n\n### Derived values belong to their primitive\n\nWhen a computed reads only one local primitive, declare it in that primitive's\ninsertion. This keeps the dependency visible and lets pending/exception\nboundaries name the actual source:\n\n```ts\nconst users =\n yield *\n query('users', config, ({ resource }) => ({\n total: craftComputed('total', function* () {\n return (yield* settled(resource)).length;\n }),\n }));\n```\n\nDo not create `craftComputed('total', ...)` beside the query when the\ncomputation depends only on `users`.\n\n### Keep casts and synchronous reads out of templates\n\nCraft templates reject both `as ...` / angle-bracket assertions and\n`craftUse(...)`. Fix the type or perform the synchronous-to-reactive\nconversion in the component logic, then expose a typed reader or generator to\nthe template:\n\n```ts\nconst typedStep = machine.stepState as unknown as () => { step: Step };\nreturn { typedStep };\n\n// Template: no cast and no craftUse.\nmatchNode.exhaustive(typedStep, 'step', steps);\n```\n\n`no-craft-use` applies to Craft TypeScript files, not only the fourth\n`craftComponent(...)` argument. A synchronous integration boundary may opt out\nlocally when its external API cannot consume a generator, but application\nstate and templates should use `yield*`.\n\n### Form and accessibility diagnostics\n\nThe accessibility preset also checks the static structure of hyperscript:\n\n- give every `label` an `htmlFor` matching the control `id`, or wrap the control;\n- give named controls and helpers a unique string local name;\n- use `button` or `a` for interactions instead of adding `click` to a `div`;\n- add a `prefers-reduced-motion` branch whenever component CSS defines an\n animation or transition.\n\nThese checks run on Craft TypeScript templates and extracted helper factories,\nso moving markup into a local function does not bypass them.\n\n### Reactive values belong in binding callbacks\n\n`require-reactive-template-bindings` uses TypeScript type information to find\nreactive reads. Reading a signal while constructing a VNode would make it a\ndependency of the structural component render, so the rule rejects this form:\n\n```ts\n// Incorrect: count is read by the component template.\np(`Count: ${count()}`);\nbutton({ disabled: isDisabled() }, 'Save');\ndiv({ class: { active: isActive() } });\n```\n\nKeep each read inside the callback owned by its DOM binding. Pass a yieldable\nreader, or use a generator when the binding must format:\n\n```ts\np(count);\np(function* () {\n return `Count: ${yield* count()}`;\n});\nbutton({ disabled: isDisabled }, 'Save');\ndiv({ class: isActiveClass });\n```\n\nLiteral and otherwise static values are still allowed, as are reads performed\nfrom DOM events and `onXxx` output callbacks. Because the rule is type-aware,\nthe ESLint parser must use `projectService: true` or a TypeScript `project`.\n\n### Pass simple yieldable callbacks directly\n\n`prefer-direct-yieldable-callback` removes a generator wrapper when the\ntemplate only delegates one zero-argument callback. It handles both a value\nbinding and a generator method:\n\n```ts\n// Before: redundant wrappers around the callbacks.\nbutton(\n {\n *click() {\n yield* press();\n },\n },\n function* () {\n return yield* label();\n },\n);\n\n// After `eslint --fix`.\nbutton({ click: press }, label);\n```\n\nMember callbacks are supported as well when the access is static and has no\narguments:\n\n```ts\n// Before.\nspan(function* () {\n return yield* counter.increment();\n});\n\n// After.\nspan(counter.increment);\n```\n\nThe rule leaves callbacks with parameters, extra statements, or additional\ncomputation unchanged. In those cases the generator contains behavior that\ncannot be represented by passing the callback reference alone.\n\n### Yield insertion writes from generator methods\n\n`require-yieldable-insertion-write` requires `set(...)`, `patch(...)`, and\n`update(...)` calls to be delegated with `yield*` when they are used inside a\ngenerator method:\n\n```ts\nnextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n},\n```\n\nInsertion callbacks that are not generators may return a write directly; the\ninsertion wrapper consumes that result for them.\n\n## What generates what\n\nThree rules do more than complain — they write code you would otherwise\nmaintain by hand:\n\n| Rule | Generates |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `require-cascade-route-di-check` | the same-file DI proof for a `craftRoutes(...)` collection |\n| `require-assert-exhaustive-route-exceptions` | the collection-level exhaustiveness assert |\n| `require-child-route-mount-check` | the `assertChildRouteMounts(...)` call and its import |\n| `require-lazy-load-with-retry` | the `withRetry(...)` wrapper on lazy route imports |\n| `prefer-direct-yieldable-callback` | replaces redundant generators with direct callback references |\n\n## Adopting them progressively\n\nOn an existing codebase, enable them in waves rather than all at once:\n\n1. **The route safety nets** — the `require-*` rules. Mostly autofixable. They\n generate the proofs; [architecture tests](/guide/testing/architecture#assertroutediproofs)\n (`assertRouteDiProofs`) fail CI if a proof is later removed or left unarmed.\n2. **The architecture rules last** — `prefer-craft-service`,\n `no-craft-service-component-same-file`, `prefer-craft-http-client`,\n `require-yieldable-reactive-read`,\n `require-yieldable-template-method`, `require-yieldable-insertion-write`.\n These ask for real refactors.\n\nThe four style rules — `no-raw-class`, `no-raw-css-value`, `no-free-has`,\n`style-file-boundary` — are in `craftRules.configs.recommended` at `'error'`,\nand they are **gated on the import**: they fire only in files that import\n`@craft-ts/style`. A component you have not migrated is not claiming the\nguarantee, so nothing reports it. The day a file starts using the design system\nis the day it starts being held to it — which is why enabling them on an\nunmigrated codebase costs nothing.\n\nThe two migration rules also expose a VS Code quick fix that inserts a temporary\nlocal disable comment with the intended migration note, so you can unblock a\nfile before doing the full refactor.\n\n## See Also\n\n- [Routing setup](/guide/routing/setup) — where these rules are installed\n- [CLI automation](/guide/routing/automation) — the codemods they complement\n- [Architecture rules](/guide/testing/architecture) — graph-wide constraints ESLint cannot see\n- [Activating the style system](/guide/style/setup) — what the four style rules are guarding\n"
|
|
325
|
+
"body": "# ESLint rules\n\nThe rule set is not decoration: several checks in this documentation only work\nbecause a rule generated or maintained the code they read. Others enforce the\narchitecture — no hidden runtime dependencies or direct transport calls — and most of them\n**autofix**.\n\n**Install them once** when you set up routing and type-safe DI.\n**Then lean on the quick fixes** rather than writing the boilerplate by hand.\n\n::: warning An ESLint error is not a compile error\nA missing autofix does not break the build. If you skip the quick fix after\nchanging a component's DI shape, `main.ts` keeps reading a stale `GenDeps_*` and\ncan miss a real DI error. Run `eslint --fix` in CI.\n:::\n\nThe plugin is exposed from `@craft-ts/dev-tools/eslint-rules`.\n\nFor a project using `@craft-ts/effect`, the published preset enables the Craft\nrules and the Effect adapter rule in one entry:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n {\n files: ['**/*.ts'],\n ...craftRules.configs.effect,\n },\n];\n```\n\nUse `craftRules.configs.recommended` for projects that do not use Effect.\n\nAdd it to your ESLint flat config:\n\n```ts\nimport craftRules from '@craft-ts/dev-tools/eslint-rules';\n\nexport default [\n // keep your existing ESLint config entries\n {\n files: ['**/*.ts'],\n plugins: {\n 'craft-ts': craftRules,\n },\n rules: {\n 'craft-ts/prefer-craft-template-blocks': 'error',\n 'craft-ts/no-render-writes': 'error',\n 'craft-ts/require-reactive-template-bindings': 'error',\n 'craft-ts/no-craft-use': 'error',\n 'craft-ts/no-type-assertions-in-template': 'error',\n 'craft-ts/no-ephemeral-template-form-state': 'error',\n 'craft-ts/template-element-name-unique': 'error',\n 'craft-ts/no-craft-computed-side-effects': 'error',\n 'craft-ts/require-craft-method-for-yieldable-callback': 'error',\n 'craft-ts/prefer-direct-yieldable-callback': 'error',\n 'craft-ts/require-yieldable-reactive-read': 'error',\n 'craft-ts/require-yieldable-template-method': 'error',\n 'craft-ts/require-yieldable-insertion-write': 'error',\n 'craft-ts/no-craft-service-component-same-file': 'error',\n 'craft-ts/max-craft-declarations-per-file': 'error',\n 'craft-ts/prefer-craft-http-transport': 'error',\n 'craft-ts/no-injection-token': 'error',\n 'craft-ts/require-primitive-derived-property': 'error',\n 'craft-ts/no-async-await': 'error',\n 'craft-ts/no-throw': 'error',\n 'craft-ts/no-imperative-craft-resource-trigger': 'error',\n 'craft-ts/no-imperative-craft-method-actions': 'error',\n 'craft-ts/no-imperative-storage-in-craft-method': 'error',\n 'craft-ts/no-transition-actions': 'error',\n 'craft-ts/require-craft-resource-trigger-yield': 'error',\n 'craft-ts/require-assert-exhaustive-route-exceptions': 'error',\n 'craft-ts/require-craft-exception-handler': 'error',\n 'craft-ts/require-exception-component-di-check': 'error',\n 'craft-ts/require-pending-component-di-check': 'error',\n 'craft-ts/require-child-route-mount-check': 'error',\n 'craft-ts/require-lazy-load-with-retry': 'error',\n 'craft-ts/require-cascade-route-di-check': 'error',\n 'craft-ts/global-exception-registry-match': 'error',\n },\n },\n];\n```\n\nWhat each rule does:\n\n- `craft-ts/prefer-craft-template-blocks`: keeps `craftComponent(...)` templates declarative by rejecting ternaries, logical expressions, negations, and imperative control flow; use `ifNode(...)`, `matchNode.exhaustive(...)`, `forNode(...)`, or `deferNode(...)`\n- `craft-ts/no-render-writes`: rejects detectable `set()`, `update()`, and `mutate()` calls in component templates and render bindings while allowing DOM event and `onXxx` output callbacks\n- `craft-ts/require-reactive-template-bindings`: requires signals, named Craft values, and component inputs to be read inside granular binding callbacks instead of during VNode construction; static values remain valid\n- `craft-ts/no-craft-use`: forbids the synchronous `craftUse(...)` escape hatch in Craft TypeScript files; use a generator and delegate the reader with `yield*` instead\n- `craft-ts/no-type-assertions-in-template`: forbids `as ...` and angle-bracket type assertions in Craft templates; fix the type in the logic factory or expose a correctly typed derived value\n- `craft-ts/no-ephemeral-template-form-state`: forbids `let` / `const` / `var` in the fourth argument of `craftComponent(...)` and `craftDirective(...)` (inline or a same-file identifier). Declare that state in the logic factory with `state()` or `craftComputed()` instead\n- `craft-ts/template-element-name-unique`: requires named HTML helpers to use a static, unique local name within a component; use the object-first helper form for unnamed elements such as `p({ id: 'hint' }, ...)`\n- `craft-ts/no-craft-computed-side-effects`: forbids writes and asynchronous work inside `craftComputed`; only reactive reads and `settled(...)` are allowed. The graph-wide counterpart is [`assertCraftComputedPure`](/guide/testing/architecture#assertcraftcomputedpure).\n- `craft-ts/no-effect-outside-loaders`: keeps `params`, methods, `craftComputed(...)`, and `craftEffect(...)` synchronous by allowing Effect values and Effect service reads only in Effect loaders; `no-effect-in-params` remains as a compatibility alias\n- `craft-ts/sync-effect-body`: keeps a body declared synchronous (`SyncOp` in its requirements) free of anything that may suspend — async constructors such as `Effect.sleep`/`Effect.promise`, and members nothing declares synchronous. Type-aware: the ESLint parser must use `projectService: true` or a TypeScript `project`\n- `craft-ts/no-explicit-effect-type`: lets `Effect.gen` infer its complete type instead of repeating an explicit Effect annotation; contracts declared in interfaces and type aliases remain allowed\n- `craft-ts/prefer-inline-effect-insertion`: keeps the `queryEffect` insertion factory inline so its resource and exception types are inferred without a separate `InsertionParams` context alias\n- `craft-ts/prefer-inline-route-providers`: inlines a route provider tuple used only once by `loadCraftComponent(...)`, preserving the route-level type proof\n- `craft-ts/prefer-craft-reactivity`: rejects authored signal/computed/effect/resource APIs, explicit `.subscribe()` calls, and RxJS `Subject`/`BehaviorSubject`/`ReplaySubject`; use `state`, `craftComputed`, `craftEffect`, `query`, and named `source$`/`on$` flows\n- `craft-ts/prefer-craft-service`: keeps services in the `craftService(...)` model\n- `craft-ts/no-craft-service-component-same-file`: forbids declaring `craftService(...)` and `craftComponent(...)` in the same file; a route-level service provider combined with a lazy-loaded component can break lazy loading, so keep them in separate files\n- `craft-ts/max-craft-declarations-per-file`: reports the third and subsequent `craftComponent(...)`, `craftService(...)`, or `craftDirective(...)` declaration of the same kind in a file; keep Craft entities split across focused files\n- `craft-ts/no-injection-token`: forbids authored `InjectionToken` contracts; declare them with `craftService({ name, providedIn: 'abstract' }, abstract<Contract>())`\n- `craft-ts/prefer-craft-http-client`: forbids direct transport usage in favor of `CraftHttpClient`\n- `craft-ts/prefer-craft-http-transport`: forbids direct `fetch()` and `XMLHttpRequest`; use `query()` for reads or `mutation()` for writes with `CraftHttpClient`\n- `craft-ts/prefer-craft-input-output`: keeps component inputs and outputs in the `Input`/`Output` model used by `craftComponent(...)`\n- `craft-ts/require-primitive-derived-property`: requires a `computed` or `craftComputed` that only depends on one primitive in the same component/service to be exposed by that primitive's insertion; simple cases are autofixed\n- `craft-ts/no-async-await`: forbids `async` functions, `await`, and `for await...of`; use generator-based Craft primitives, `craftSleep`, and `CraftHttpClient` instead\n- `craft-ts/no-throw`: forbids `throw` in Craft code and offers a Quick Fix that returns `craftException({ _tag: 'UNEXPECTED_ERROR' }, { error: ... })`; keep technical boundaries and tests outside this rule when their contracts require thrown errors\n- `craft-ts/no-imperative-craft-resource-trigger`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` in a `craftEffect` dependency graph, including through `craftGen(...)`. The graph-wide counterpart, including `state` / `source$` writes, is [`assertCraftEffectNoImperativeSync`](/guide/testing/architecture#assertcrafteffectnoimperativesync).\n- `craft-ts/no-imperative-craft-method-actions`: forbids composing multiple imperative actions in a `craftMethod`; emit a `source$` event and let the affected query react with `insertReactOnMutation(...)` instead. A handler such as `event.preventDefault()` followed by one `mutation.mutate(...)` remains valid.\n- `craft-ts/no-imperative-storage-in-craft-method`: forbids direct storage access and imperative location changes in a `craftMethod`; use `insertReactOnMutation(...)` with `optimisticUpdate: () => undefined` to clear the affected query and let its persistence follow the query state.\n- `craft-ts/no-transition-actions`: forbids `query.call(...)`, `mutation.mutate(...)`, and `asyncProcess.method(...)` inside `transitionStep(...)`; validate the event and emit a source, then let the resource react to that source.\n- `craft-ts/require-craft-resource-trigger-yield`: requires those triggers to use `yield*` inside generator functions, while ordinary UI callbacks may keep imperative calls\n- `craft-ts/require-craft-method-for-yieldable-callback`: requires callbacks returned by a `craftComponent` factory to wrap yieldable Craft method calls in `craftMethod(...)`\n- `craft-ts/prefer-direct-yieldable-callback`: replaces a template generator or generator method that only delegates `yield* callback()` with the callback reference itself (`callback` or `object.method`)\n- `craft-ts/require-yieldable-reactive-read`: requires Craft reactive readers to be delegated with `yield*` inside generator functions; a function that reads a Craft reader must itself be a generator\n- `craft-ts/require-yieldable-template-method`: requires yieldable Craft method calls in a `craftComponent` template to be delegated with `yield*`, or passed as a reference (`click: counter.increment`)\n- `craft-ts/require-yieldable-insertion-write`: requires `set(...)`, `patch(...)`, and `update(...)` to be delegated with `yield*` when they are used inside a generator method\n- `craft-ts/require-assert-exhaustive-route-exceptions`: adds the collection-level `assertExhaustiveRouteExceptions(...)` safety net\n- `craft-ts/require-craft-exception-handler`: enforces `craftExceptionHandler(function* (...) {})`; simple handlers are autofixed and ambiguous raw redirects are reported for manual migration\n- `craft-ts/require-exception-component-di-check`: generates O(1) `RouteExceptionComponentCheckedDI` checks for `renderComponent`, route-level `errorComponent`, `withErrorComponent`, `withRouteLoadError`, and route-local `provideRouteLoadErrorComponent`\n- `craft-ts/require-pending-component-di-check`: generates the independent `RouteCheckedDI` check for each `pendingComponent`\n- `craft-ts/no-raw-class`: forbids a `class:` binding that is a string, a template literal or a function, in any file that imports `@craft-ts/style`. A class assembled at render time is a visual state nothing recorded, so the [visual matrix](/guide/style/testing) would enumerate what the sheets declare while the DOM shows something else. Move the rule into the sheet and bind the class it returns; make the variation an axis and set a `data-*` attribute\n- `craft-ts/no-raw-css-value`: forbids a string or number literal as an argument to a `@craft-ts/style` helper — `p('12px')`, `bg('red')`. If the scale is missing the step, add it to the scale; if the value genuinely cannot be proven, `unsafeLength('13px', reason)` compiles and makes the debt countable in the [graph](/guide/style/testing#what-the-graph-adds)\n- `craft-ts/no-free-has`: forbids a hand-written `:has()` in styles. It reaches across the component boundary, so what a component looks like depends on markup it does not own — a state the matrix cannot enumerate. Use the `descendant` axis, which is a closed set and carries its own test driver\n- `craft-ts/style-file-boundary`: restricts a `*.style.ts` to style-vocabulary imports. The [build plugin](/guide/style/setup) imports the file in Node to read what it registered, so an application import would run application code at build time\n- `craft-ts/craft-css-token-registry`: reports a custom property registered with `@property` by two different components. A custom property may have only one owner; two silently fight over its syntax and initial value\n- `craft-ts/require-effect-adapters`: requires the Effect-aware adapters — `queryEffect`, `mutationEffect`, `asyncProcessEffect` — instead of the plain primitives in an Effect application. See [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter)\n- `craft-ts/craft-signal-source-name-match`: requires `signalSource(name, ...)` to take a string literal matching the variable, class property or object property it is assigned to, so the name in a trace is the name in the source. A computed name defeats the [architecture graph](/guide/testing/architecture), which reads these names statically\n- `craft-ts/require-child-route-mount-check`: adds the missing `assertChildRouteMounts(...)` call + import (Quick Fix) for any `craftRoutes(...)` collection that mounts lazy `loadChildren`, so a `.withParent`-pinned child mounted under the wrong path is a compile error\n- `craft-ts/require-lazy-load-with-retry`: wraps route `loadComponent` and `loadChildren` imports with the generated `withRetry(...)` loader helper while preserving a statically analyzable import specifier\n- `craft-ts/require-cascade-route-di-check`: rejects any `craftRoutes(...)` collection without a same-file `ValidateCascadeRoutesFile + CanRun` proof; its autofix adds the conservative `<never, Router>` context, which should be adjusted when the mount inherits providers\n- `craft-ts/global-exception-registry-match`: keeps `CraftGlobalExceptionRegistry` synchronized with handlers delegating to `globalError()`\n- `craft-ts/prefer-craft-router-link`: requires `CraftRouterLink` for internal `a(..., { href: ... })` navigation; external URLs, fragment links, downloads, `_blank`, and links marked with `data-navigation: 'external'` remain native\n\n### Accessibility (`craft-ts/a11y`)\n\nSpread `craftRules.configs.a11y.rules` to enable the WCAG 2.2 AA preset as\n`error`. The rules walk **all** hyperscript in the file (`craftTemplate`,\nextracted factories, `h('tag')`), not only `craftComponent` argument 3.\n\n- `prefer-named-html-helpers`: forbids `h('img')` / `h('button')` when a named helper exists\n- `require-interactive-local-name`: requires a string-literal first argument on interactive helpers; the local name is the third segment of `data-craft-name=\"${component}:${tag}:${localName}\"`\n- `img-has-alt`, `iframe-has-title`, `button-has-type`, `anchor-has-href`\n- `control-has-accessible-name`, `label-has-associated-control`, `heading-has-content`\n- `no-noninteractive-element-interactions`, `no-positive-tabindex`\n- `valid-aria`, `role-has-required-aria`, `target-blank-noopener`\n- `prefer-relative-heading`, `require-route-heading-outline`,\n `require-outlet-heading-section`, `no-heading-level-skip`\n- `require-focus-visible`, `require-reduced-motion` (CSS of `craftComponent`)\n\nSee [Accessibility](/guide/components/accessibility).\n\nThe two migration rules also expose a VS Code ESLint Quick Fix suggestion that inserts a temporary local disable comment with the intended migration note when you need to unblock a file before doing the full refactor.\n\nThe template and reactivity rules are intentionally diagnostic-only: replacing a\nresource or subscription can change lifecycle and error semantics, so the rule\npoints at the Craft primitive without applying a potentially unsafe rewrite.\n\n### Why templates use blocks\n\nCraft template blocks preserve the branch structure in the type-level render\ncontract. A ternary or `condition && node` produces only a computed value, so\nthe type checker cannot assert which branch renders which content. Keep derived\nvalues and business decisions in the component's state/query layer, then make\nthe template express visibility explicitly:\n\n```ts\nifNode(\n isReady,\n () => p('Ready'),\n () => p('Loading…'),\n);\n\nmatchNode.exhaustive(query.exceptions, '_tag', {\n NOT_FOUND: () => p('Not found'),\n FORBIDDEN: () => p('Forbidden'),\n});\n```\n\nThis rule is for Craft's TypeScript templates. It does not rewrite external\ntemplate languages.\n\nThe same restriction applies to boolean expressions. A negation is still\napplication logic, even when it is used only for a DOM property:\n\n```ts\n// Incorrect: the template derives the disabled state.\nbutton(\n {\n disabled: function* () {\n return !(yield* machine.canGoBack());\n },\n },\n 'Back',\n);\n\n// Correct: derive it in the logic factory and bind the result.\nconst backDisabled = craftComputed('backDisabled', function* () {\n return !(yield* history.canGoBack());\n});\nreturn { backDisabled };\n```\n\nKeep the template to layout and binding. Move labels, formatted values,\nvalidation state, and other decisions into `state()` or `craftComputed()`.\n\n### Derived values belong to their primitive\n\nWhen a computed reads only one local primitive, declare it in that primitive's\ninsertion. This keeps the dependency visible and lets pending/exception\nboundaries name the actual source:\n\n```ts\nconst users =\n yield *\n query('users', config, ({ resource }) => ({\n total: craftComputed('total', function* () {\n return (yield* settled(resource)).length;\n }),\n }));\n```\n\nDo not create `craftComputed('total', ...)` beside the query when the\ncomputation depends only on `users`.\n\n### Keep casts and synchronous reads out of templates\n\nCraft templates reject both `as ...` / angle-bracket assertions and\n`craftUse(...)`. Fix the type or perform the synchronous-to-reactive\nconversion in the component logic, then expose a typed reader or generator to\nthe template:\n\n```ts\nconst typedStep = machine.stepState as unknown as () => { step: Step };\nreturn { typedStep };\n\n// Template: no cast and no craftUse.\nmatchNode.exhaustive(typedStep, 'step', steps);\n```\n\n`no-craft-use` applies to Craft TypeScript files, not only the fourth\n`craftComponent(...)` argument. A synchronous integration boundary may opt out\nlocally when its external API cannot consume a generator, but application\nstate and templates should use `yield*`.\n\n### Form and accessibility diagnostics\n\nThe accessibility preset also checks the static structure of hyperscript:\n\n- give every `label` an `htmlFor` matching the control `id`, or wrap the control;\n- give named controls and helpers a unique string local name;\n- use `button` or `a` for interactions instead of adding `click` to a `div`;\n- add a `prefers-reduced-motion` branch whenever component CSS defines an\n animation or transition.\n\nThese checks run on Craft TypeScript templates and extracted helper factories,\nso moving markup into a local function does not bypass them.\n\n### Reactive values belong in binding callbacks\n\n`require-reactive-template-bindings` uses TypeScript type information to find\nreactive reads. Reading a signal while constructing a VNode would make it a\ndependency of the structural component render, so the rule rejects this form:\n\n```ts\n// Incorrect: count is read by the component template.\np(`Count: ${count()}`);\nbutton({ disabled: isDisabled() }, 'Save');\ndiv({ class: { active: isActive() } });\n```\n\nKeep each read inside the callback owned by its DOM binding. Pass a yieldable\nreader, or use a generator when the binding must format:\n\n```ts\np(count);\np(function* () {\n return `Count: ${yield* count()}`;\n});\nbutton({ disabled: isDisabled }, 'Save');\ndiv({ class: isActiveClass });\n```\n\nLiteral and otherwise static values are still allowed, as are reads performed\nfrom DOM events and `onXxx` output callbacks. Because the rule is type-aware,\nthe ESLint parser must use `projectService: true` or a TypeScript `project`.\n\n### Pass simple yieldable callbacks directly\n\n`prefer-direct-yieldable-callback` removes a generator wrapper when the\ntemplate only delegates one zero-argument callback. It handles both a value\nbinding and a generator method:\n\n```ts\n// Before: redundant wrappers around the callbacks.\nbutton(\n {\n *click() {\n yield* press();\n },\n },\n function* () {\n return yield* label();\n },\n);\n\n// After `eslint --fix`.\nbutton({ click: press }, label);\n```\n\nMember callbacks are supported as well when the access is static and has no\narguments:\n\n```ts\n// Before.\nspan(function* () {\n return yield* counter.increment();\n});\n\n// After.\nspan(counter.increment);\n```\n\nThe rule leaves callbacks with parameters, extra statements, or additional\ncomputation unchanged. In those cases the generator contains behavior that\ncannot be represented by passing the callback reference alone.\n\n### Yield insertion writes from generator methods\n\n`require-yieldable-insertion-write` requires `set(...)`, `patch(...)`, and\n`update(...)` calls to be delegated with `yield*` when they are used inside a\ngenerator method:\n\n```ts\nnextPage: function* () {\n const current = yield* state();\n return yield* patch({ page: current.page + 1 });\n},\n```\n\nInsertion callbacks that are not generators may return a write directly; the\ninsertion wrapper consumes that result for them.\n\n## What generates what\n\nThree rules do more than complain — they write code you would otherwise\nmaintain by hand:\n\n| Rule | Generates |\n| -------------------------------------------- | ------------------------------------------------------------- |\n| `require-cascade-route-di-check` | the same-file DI proof for a `craftRoutes(...)` collection |\n| `require-assert-exhaustive-route-exceptions` | the collection-level exhaustiveness assert |\n| `require-child-route-mount-check` | the `assertChildRouteMounts(...)` call and its import |\n| `require-lazy-load-with-retry` | the `withRetry(...)` wrapper on lazy route imports |\n| `prefer-direct-yieldable-callback` | replaces redundant generators with direct callback references |\n\n## Adopting them progressively\n\nOn an existing codebase, enable them in waves rather than all at once:\n\n1. **The route safety nets** — the `require-*` rules. Mostly autofixable. They\n generate the proofs; [architecture tests](/guide/testing/architecture#assertroutediproofs)\n (`assertRouteDiProofs`) fail CI if a proof is later removed or left unarmed.\n2. **The architecture rules last** — `prefer-craft-service`,\n `no-craft-service-component-same-file`, `prefer-craft-http-client`,\n `require-yieldable-reactive-read`,\n `require-yieldable-template-method`, `require-yieldable-insertion-write`.\n These ask for real refactors.\n\nThe four style rules — `no-raw-class`, `no-raw-css-value`, `no-free-has`,\n`style-file-boundary` — are in `craftRules.configs.recommended` at `'error'`,\nand they are **gated on the import**: they fire only in files that import\n`@craft-ts/style`. A component you have not migrated is not claiming the\nguarantee, so nothing reports it. The day a file starts using the design system\nis the day it starts being held to it — which is why enabling them on an\nunmigrated codebase costs nothing.\n\nThe two migration rules also expose a VS Code quick fix that inserts a temporary\nlocal disable comment with the intended migration note, so you can unblock a\nfile before doing the full refactor.\n\n## See Also\n\n- [Routing setup](/guide/routing/setup) — where these rules are installed\n- [CLI automation](/guide/routing/automation) — the codemods they complement\n- [Architecture rules](/guide/testing/architecture) — graph-wide constraints ESLint cannot see\n- [Activating the style system](/guide/style/setup) — what the four style rules are guarding\n"
|
|
326
326
|
},
|
|
327
327
|
{
|
|
328
328
|
"path": "/guide/routing/exception-handling",
|
|
@@ -617,12 +617,12 @@
|
|
|
617
617
|
{
|
|
618
618
|
"path": "/learn-effect/03-effect-domain",
|
|
619
619
|
"title": "3. Put the domain in Effect",
|
|
620
|
-
"body": "# 3. Put the domain in Effect\n\n**Goal:** define typed business failures and services without making the Craft\ncomponent know how they are provided.\n\n## Typed failures are values\n\nEffect's tagged errors map naturally to Craft's exception channel:\n\n```typescript\nimport { Context, Data, Effect } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport class Unauthorized extends Data.TaggedError('Unauthorized')<{\n readonly reason: string;\n}> {}\n\ntype User = {\n readonly id: string;\n};\n\ntype UserRepository = {\n readonly find: (userId: string) => Effect.Effect<User | undefined>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport
|
|
620
|
+
"body": "# 3. Put the domain in Effect\n\n**Goal:** define typed business failures and services without making the Craft\ncomponent know how they are provided.\n\n## Typed failures are values\n\nEffect's tagged errors map naturally to Craft's exception channel:\n\n```typescript\nimport { Context, Data, Effect } from 'effect';\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport class Unauthorized extends Data.TaggedError('Unauthorized')<{\n readonly reason: string;\n}> {}\n\ntype User = {\n readonly id: string;\n};\n\ntype UserRepository = {\n readonly find: (userId: string) => Effect.Effect<User | undefined>;\n};\n\nexport class UserRepositoryService extends Context.Service<\n UserRepositoryService,\n UserRepository\n>()('app/UserRepository') {}\n\nexport const loadUser = Effect.fnUntraced(function* (userId: string) {\n const repository = yield* UserRepositoryService;\n const user = yield* repository.find(userId);\n if (!user) return yield* new UserNotFound({ userId });\n return user;\n});\n```\n\nThe program has the shape `Effect<User, UserNotFound, UserRepositoryService>`.\n`yield* UserRepositoryService` gets the repository from the Effect context;\n`yield* repository.find(userId)` then runs the `Effect` returned by its method.\n`UserNotFound` is a business outcome that the UI can handle. An unexpected\ndefect raised by `Effect.die` remains a technical error; it is not turned into a\nbusiness exception.\n\n`Data.TaggedError` creates a **yieldable error** in Effect v4, so this is the\nidiomatic form inside `Effect.gen`:\n\n```typescript\nif (!user) return yield * new UserNotFound({ userId });\n```\n\nThe explicit equivalent is `yield* Effect.fail(new UserNotFound({ userId }))`;\nthere is no `Effect.failed` constructor. At the Craft boundary, yield the\neffect through `runEffect(...)` instead of yielding the error instance directly.\n\n## Define an Effect service\n\nUse `Context.Service` for the contract and a `Layer` for the implementation:\n\n```typescript\nimport { Context, Effect, Layer } from 'effect';\n\ntype AccessPolicy = {\n readonly decide: (\n userId: string,\n ) => Effect.Effect<AccessDecision, UserNotFound>;\n};\n\nexport class AccessPolicyService extends Context.Service<\n AccessPolicyService,\n AccessPolicy\n>()('app/AccessPolicyService') {}\n\nexport const AccessPolicyLive = Layer.sync(AccessPolicyService)(() => ({\n decide: (userId) => findAccessDecision(userId),\n}));\n\nexport const checkUserAccess = Effect.fnUntraced(function* (userId: string) {\n const policy = yield* AccessPolicyService;\n return yield* policy.decide(userId);\n});\n```\n\nThe component calls `checkUserAccess`; it does not call `AccessPolicyService`\nand does not know which Layer implements it.\n\nWhen a Craft factory genuinely needs a service member, narrow it explicitly with\n`effectService` rather than resolving an untracked value:\n\n```typescript\nimport { effectService } from '@craft-ts/effect';\n\nconst { decide } =\n yield * effectService(AccessPolicyService, ({ decide }) => ({ decide }));\n```\n\nPrefer exposing a domain operation such as `checkUserAccess` to a component. The\nselector form is useful for a Craft service or adapter that deliberately owns\nthe boundary and wants the graph to record only the members it uses.\n\n## Derive Craft state from the Effect service\n\n`craftComputed` stays synchronous: it derives a Craft reader. Let\n`queryEffect` execute the Effect operation, then derive a display value from the\nquery resource:\n\n```typescript\nimport { craftComputed } from '@craft-ts/core';\nimport { queryEffect } from '@craft-ts/effect';\n\nconst accessQuery =\n yield *\n queryEffect('accessQuery', {\n params: () => 'user-ada',\n loader: ({ params }) => checkUserAccess(params),\n });\n\nconst accessLabel = craftComputed('accessLabel', function* () {\n return (yield* accessQuery.value())?.label ?? 'Loading…';\n});\n```\n\nThe chain is: `queryEffect` runs `checkUserAccess`, the active `Layer` provides\n`AccessPolicyService`, and `accessLabel` reacts to the query's Craft value. The\ncomputed does not call the Effect service or start an Effect itself.\n\n## Declare a synchronous member\n\nThat last sentence used to be a hard rule: no Effect at all inside `params`,\n`craftComputed(...)` or `craftMethod(...)`. Those run on Craft's **synchronous**\ndriver, which completes on one tick and cannot wait — and `Effect<A, E, R>` does\nnot say whether running it will suspend.\n\nIt is worse than it looks for a service member. A `Layer` closes over the\nmember's dependencies when it builds the service, so a member that calls the\nnetwork and a member that adds two numbers _both_ surface as `R = never`:\n\n\n\nThe information does not exist in the type, so you write it there. `SyncOp` is a\nphantom requirement — never provided, no runtime cost — and `R` is the one\nchannel Effect accumulates across composition. An Effect that requires `SyncOp`\nis one its author declares never suspends.\n\nRequirements union through `Effect.gen`, so the declaration propagates on its\nown. A standalone program that only calls declared-synchronous members inherits\nthe marker; one that calls nothing marked spells it out with `yield* SyncOp`:\n\n\n\n`CartPricing` in `R` is not a problem: the level in force satisfies it, exactly\nas it does for a loader. The only thing checked is that `SyncOp` is among the\nrequirements.\n\n## Use it: computedEffect, then syncEffect\n\nFor a derived value, reach for `computedEffect` — the Effect counterpart of\n`craftComputed`. The factory reads Craft dependencies and **returns** the\nEffect; the adapter runs it in place, so you get a plain reactive value:\n\n\n\nAnything nobody declared synchronous is refused at the call, before anything\nruns:\n\n\n\nFor a synchronous Effect exposed as a callable method, use `methodEffect`, the\nEffect counterpart of `craftMethod`. For lower-level positions such as a\n`params` factory or a `state` updater, `syncEffect(...)` is the same door,\nopened by hand:\n\n```typescript\nqueryEffect('shippingQuote', {\n params: function* () {\n return yield* syncEffect(cartWeightGrams(yield* lines()));\n },\n loader: ({ params }) => quoteShipping(params),\n});\n```\n\nThe relationship mirrors the asynchronous side: `computedEffect` is to\n`methodEffect` what `queryEffect` is to `asyncProcessEffect` — a value or method\nwith no resource lifecycle. `syncEffect` remains the lower-level escape hatch.\n\n::: tip Three lines of defence\n\n`SyncOp` is a claim, not a proof — nothing stops a body from declaring itself\nsynchronous and awaiting anyway. Three mechanisms check it, and none is\nredundant:\n\n1. **the type** — `computedEffect` and `syncEffect(...)` refuse an Effect nobody\n declared, at the call site;\n2. **`craft-ts/sync-effect-body`** — reads the body, every branch at once, and\n rejects a declared-synchronous body that yields something async. A unit test\n cannot do this: it only proves the inputs it was given;\n3. **the runtime** — both run through `Effect.runSyncExitWith`, which\n cannot suspend. A broken declaration throws `CraftEffectNotSynchronous`\n immediately, at the first call, instead of freezing the UI.\n\n:::\n\nKeep asynchronous work where it belongs: a loader. `SyncOp` opens one narrow,\nexplicit door for business calculations, not a way around the adapters.\n\n## Run a standalone Effect\n\nFor a low-level bridge, `runEffect` lets a Craft generator yield an Effect while\npreserving its typed error channel:\n\n```typescript\nimport { Effect } from 'effect';\nimport { runEffect } from '@craft-ts/effect';\n\nconst name = yield * runEffect(Effect.succeed('Ada'));\n```\n\nUse the adapters in the next chapters for application data. They resolve the\nEffect requirement `R` through the nearest `provideLayer(...)` and keep loading,\nvalue and exception state in the Craft resource.\n\n## Install the bridge once\n\nThe bridge teaches Craft how to execute a yielded Effect. Install it during app\nbootstrap, not in every loader:\n\n```typescript\nimport { provideAppInitializer } from '@craft-ts/core';\nimport { installCraftEffectBridge } from '@craft-ts/effect';\n\nexport const appConfig = craftAppConfig({\n providers: [\n provideAppInitializer(() => {\n installCraftEffectBridge();\n }),\n ],\n});\n```\n\nIn tests, call `installCraftEffectBridge()` in `beforeEach` and dispose the\nreturned function in `afterEach`.\n\n## What you gained\n\nAn Effect domain with typed failures, explicit service requirements and swappable\nLayers. The next step puts that program behind a reactive `queryEffect`.\n\n<div style=\"display: flex; justify-content: space-between; margin-top: 2rem\">\n\n[← 2. Derive UI state](/learn-effect/02-derive)\n\n[4. Load data with Effect →](/learn-effect/04-load-data)\n\n</div>\n"
|
|
621
621
|
},
|
|
622
622
|
{
|
|
623
623
|
"path": "/learn-effect/04-load-data",
|
|
624
624
|
"title": "4. Load data with Effect",
|
|
625
|
-
"body": "# 4. Load data with Effect\n\n**Goal:** expose an `Effect<A, E, R>` as a Craft query.\n\n## The operation being loaded\n\n`queryEffect` receives a domain function that returns an Effect. Here is the\n`loadUserProfile` used by the query below; the data is mocked so the example\ncan show each result channel:\n\n```typescript\n// profile-domain.ts\nimport { Data, Effect } from 'effect';\n\nexport type ProfileScenario =\n | 'success'\n | 'not-found'\n | 'session-expired'\n | 'database-down';\n\ntype Profile = { readonly name: string };\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport class Unauthorized extends Data.TaggedError('Unauthorized')<{\n readonly reason: string;\n}> {}\n\nexport
|
|
625
|
+
"body": "# 4. Load data with Effect\n\n**Goal:** expose an `Effect<A, E, R>` as a Craft query.\n\n## The operation being loaded\n\n`queryEffect` receives a domain function that returns an Effect. Here is the\n`loadUserProfile` used by the query below; the data is mocked so the example\ncan show each result channel:\n\n```typescript\n// profile-domain.ts\nimport { Data, Effect } from 'effect';\n\nexport type ProfileScenario =\n | 'success'\n | 'not-found'\n | 'session-expired'\n | 'database-down';\n\ntype Profile = { readonly name: string };\n\nexport class UserNotFound extends Data.TaggedError('UserNotFound')<{\n readonly userId: string;\n}> {}\n\nexport class Unauthorized extends Data.TaggedError('Unauthorized')<{\n readonly reason: string;\n}> {}\n\nexport const loadUserProfile = Effect.fnUntraced(function* (\n scenario: ProfileScenario,\n) {\n // Simulate the latency of a backend request.\n yield* Effect.sleep('400 millis');\n\n switch (scenario) {\n case 'not-found':\n return yield* new UserNotFound({ userId: 'user-404' });\n case 'session-expired':\n return yield* new Unauthorized({ reason: 'session expired' });\n case 'database-down':\n return yield* Effect.die(new Error('database unavailable'));\n case 'success':\n return { name: 'Ada Lovelace' } satisfies Profile;\n }\n});\n```\n\n`loadUserProfile` does not run when it is declared. It returns an\n`Effect<Profile, UserNotFound | Unauthorized>`, which represents a backend\nrequest and which the query runs whenever its parameters trigger the loader.\n\n## `queryEffect`\n\nThe adapter has the same lifecycle as `query`, but its loader returns an Effect:\n\n```typescript\nimport {\n type Input,\n craftComponent,\n ifNode,\n matchNode,\n p,\n} from '@craft-ts/component';\nimport { craftComputed } from '@craft-ts/core';\nimport { queryEffect } from '@craft-ts/effect';\nimport { loadUserProfile, type ProfileScenario } from './profile-domain';\n\nconst Profile = craftComponent(\n 'Profile',\n {},\n function* (profileScenarioInput: Input<ProfileScenario>) {\n const profile = yield* queryEffect(\n 'profile',\n {\n params: profileScenarioInput,\n loader: ({ params }) => loadUserProfile(params),\n },\n ({ resource, exceptions }) => ({\n hasProfile: craftComputed('hasProfile', () => resource.hasValue()),\n currentError: craftComputed('currentError', function* () {\n return (yield* exceptions()).loader;\n }),\n }),\n );\n\n return { profile };\n },\n ({ profile }) => [\n ifNode(profile.isLoading, () => p('Loading…')),\n /* bind profile.value() or match profile.exceptions().loader here */\n ],\n);\n```\n\n`queryEffect` is a Craft query with an Effect loader. It owns cancellation,\nloading state, the last value and typed exceptions. Its `Effect` requirements are\nresolved by the active Layer. Here, `profileScenarioInput` is the reactive input\nsource: changing it reruns `loadUserProfile`; there is no `method` or manual\n`profile.call(...)` because the input drives the query.\n\n## The three result channels\n\n| Effect outcome | Craft outcome |\n| -------------------------- | -------------------------------------------------- |\n| `Effect.succeed(value)` | query value; the generator resumes with `value` |\n| typed `Effect.fail(error)` | Craft exception keyed by `error._tag` |\n| `Effect.die(defect)` | technical resource error, not a business exception |\n\nInterruption is cancellation. It does not become a user-facing exception.\n\nHandle typed errors exhaustively with `matchNode.exhaustive` or with a route\nexception handler:\n\n```typescript\nmatchNode.exhaustive(profile.exception, '_tag', {\n UserNotFound: () => p('No profile matches that user.'),\n Unauthorized: () => p('Your session has expired.'),\n});\n```\n\nWhen the Effect is used in a route guard or resolver directly, prefer\n`yield* runEffect(program)`. A bare `yield* program` executes at runtime but\ndoes not advertise `E` to Craft's compile-time route exception analysis.\n\n## Reactive Effect computations\n\nWhen the derived value comes from an Effect that **cannot suspend**, use\n`computedEffect`. It is the Effect counterpart of `craftComputed`, and the\nsymmetry is the contract:\n\n```\ncraftComputed : computedEffect :: query : queryEffect\n```\n\n```typescript\nimport { computedEffect } from '@craft-ts/effect';\n\nconst totalLabel = computedEffect('totalLabel', function* () {\n const lines = yield* cartLines();\n return cartTotalLabel(lines); // returns the Effect, never runs it\n});\n```\n\nThe factory reads Craft dependencies with `yield*` and **returns** an Effect;\n`computedEffect` runs it in place against the nearest `provideLayer(...)`. The\nresult is a plain reactive value — no `value`, no `isLoading`, no `settled(...)`,\nno `pendingNode`. Read it like any `craftComputed`.\n\nWhich is why the Effect must be declared synchronous with\n[`SyncOp`](/learn-effect/03-effect-domain#declare-a-synchronous-member). A\ncomputation is asked for its value now and cannot suspend to produce it, so an\nEffect whose `R` does not carry `SyncOp` is refused at the call site:\n\n```typescript\ncomputedEffect('profile', function* () {\n const userId = yield* currentUserId();\n return loadUserProfile(userId); // ✗ hits the network\n // ^ Argument of type 'Effect<Profile, …, UserRepository>' is not\n // assignable to '… & NotDeclaredSynchronous<UserRepository>'\n});\n```\n\nThat is not a gap: the suspending case is what `queryEffect` is for. A typed\nfailure remains fine — failing is not suspending, and it travels on Craft's\nexception channel.\n\n## Synchronous params and methods\n\nThe `params` factory remains synchronous: it may read Craft dependencies, and it\nmay run a declared-synchronous Effect through `syncEffect(...)`, but it must\nnever construct a suspending Effect — move that to the loader. A `method` only\nmaps its arguments to params; the loader is the only callback allowed to\nsuspend:\n\n```typescript\nconst profile =\n yield *\n queryEffect('profile', {\n params: function* () {\n const input = yield* currentUserInput();\n return resolveProfileParams(input);\n },\n loader: ({ params }) => loadUserProfile(params),\n });\n\nconst profileByMethod =\n yield *\n queryEffect('profileByMethod', {\n method: (input: UserInput) => resolveProfileParams(input),\n loader: ({ params }) => loadUserProfile(params),\n });\n```\n\nThe Effect ESLint rule rejects Effect values and Effect service reads inside\n`params`, methods, `craftComputed(...)`, and `craftEffect(...)`, keeping the\nquery boundary synchronous and deterministic. Only the loader may return an\nEffect.\n\nFor purely synchronous local state, use native Craft values and `state`; there\nis intentionally no `stateEffect`:\n\n```typescript\nconst request = yield * state('request', 'support');\n```\n\nUse Effect for computations, I/O and service dependencies.\n\n## What you gained\n\nEffect's typed result becomes a reactive Craft resource without a manual\nsubscription or signal conversion.\n\n<div style=\"display: flex; justify-content: space-between; margin-top: 2rem\">\n\n[← 3. Put the domain in Effect](/learn-effect/03-effect-domain)\n\n[5. Write data with Effect →](/learn-effect/05-write-data)\n\n</div>\n"
|
|
626
626
|
},
|
|
627
627
|
{
|
|
628
628
|
"path": "/learn-effect/05-write-data",
|
|
@@ -712,7 +712,7 @@
|
|
|
712
712
|
{
|
|
713
713
|
"path": "/reference",
|
|
714
714
|
"title": "API index",
|
|
715
|
-
"body": "# API index\n\nEvery documented export, with the page that covers it. Use <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>F</kbd>.\n\nFor an explanation rather than a lookup, start from the [Guide](/guide/).\nCoding agents: [llms.txt](https://craft-ts.github.io/craft/llms.txt) and\n[coding agents](/resources/ai-agents).\n\n## Primitives\n\n| Symbol | What it does | Page |\n| ------------------- | -------------------------------------------------------- | --------------------------------------------- |\n| `state` | Signal-based state you own | [Local state](/guide/state/local-state) |\n| `craftStateMachine` | Declarative finite-state workflow | [State machines](/guide/state/state-machines) |\n| `query` | Server data, re-fetched from reactive `params` | [query](/guide/state/server-state) |\n| `mutation` | Server write, triggered explicitly | [Mutations](/guide/state/mutations) |\n| `queryParams` | State that lives in the URL query string | [queryParams](/guide/state/url-state) |\n| `asyncProcess` | One-off async operation with lifecycle state | [asyncProcess](/guide/state/async-process) |\n| `craftUse` | Drives a primitive outside a generator (component field) | [Learn 1](/learn/01-first-state) |\n\nNot sure which one: [Which primitive should I use?](/guide/concepts/choose-primitive)\n\n## Runtime context\n\nTyped helpers that recover `get` / `set` / `update` / `patch` from DI, for\nwrappers, WebMCP tools, and other advanced patterns. Everyday insertions\nalready receive those methods as arguments — see\n[Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context).\n\n| Symbol | What it does | Page |\n| ----------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |\n| `injectStateMethodRuntimeContext` | `state` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectQueryMethodRuntimeContext` | `query` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectMutationMethodRuntimeContext` | `mutation` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectQueryParamsMethodRuntimeContext` | `queryParams` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectAsyncProcessMethodRuntimeContext` | `asyncProcess` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectPrimitiveMethodRuntimeContext` | Same context, untyped `kind` | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `providePrimitiveResourceRuntimeObserver` | Observes `query` / `mutation` / `asyncProcess` / `queryParams` values | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n\n## Composition\n\n| Symbol | What it does | Page |\n| ------------------------ | ----------------------------------------------- | -------------------------------------------------------- |\n| `craftPipe` | Composes several insertions into one | [Insertions](/guide/concepts/insertions) |\n| `craftYieldRecord` | Resolves a record of primitive generators | [craftService](/guide/app/craft-service) |\n| `insertStatePipe` | Composes several `state` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertQueryPipe` | Composes several `query` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertMutationPipe` | Composes several `mutation` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertQueryParamsPipe` | Composes several `queryParams` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertAsyncProcessPipe` | Composes several `asyncProcess` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertStateMachinePipe` | Composes several `craftStateMachine` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `craftGen` | A standalone tracked generator | [Generators](/guide/concepts/generators) |\n| `craftMatch` | Exhaustive pattern matching | [Pattern matching](/guide/advanced/pattern-matching) |\n| `.pipe(...)` | Program operators on a craft generator | [Program operators](/guide/advanced/program-operators) |\n| `catchTag`, `retry` | Operators for `.pipe(...)` | [Program operators](/guide/advanced/program-operators) |\n\n## Insertions\n\n| Symbol | What it does | Page |\n| --------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |\n| `insertSelect` | Derives a slice of a primitive | [Selecting](/guide/state/select) |\n| `insertEntities` | Entity collection storage and updates | [Collections](/guide/state/collections) |\n| `insertStoragePersister` | Persists through the configured storage backend | [Persistence](/guide/state/persistence) |\n| `insertReactOnMutation` | Reloads / optimistically patches on a mutation | [React on mutation](/guide/state/react-on-mutation) |\n| `insertPaginationPlaceholderData` | Placeholder rows while a page loads | [Pagination placeholder](/guide/state/pagination-placeholder) |\n\n## Forms\n\n| Symbol | What it does | Page |\n| --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------- |\n| `insertForm` | Derives a form from a `state` | [Forms](/guide/forms/) |\n| `insertFormAttributes` | Validators, `disable`, `hidden` | [Forms](/guide/forms/) |\n| `insertSelectFormTree` | Targets a field sub-tree | [Nested forms](/guide/forms/nested) |\n| `insertSubFormField` | A nested sub-form | [Nested forms](/guide/forms/nested) |\n| `insertFormSubmit` | Wires submission to a mutation | [Submitting](/guide/forms/submit) |\n| `insertNoopTypingAnchor` | Type anchor required per field tree | [Forms](/guide/forms/) |\n| `CraftFieldDirective` | Binds a typed field to a Craft DOM node | [Forms](/guide/forms/) |\n| `fieldErrorNode.exhaustive` / `.partial` | Exhaustive or partial validation rendering | [Forms](/guide/forms/) |\n| `cRequired`, `cEmail`, `cMin`/`cMax`, `cMinLength`/`cMaxLength`, `cPattern` | Built-in validators | [Validators](/guide/forms/validation) |\n| `cValidate`, `cAsyncValidate` | Custom and async validators | [Validators](/guide/forms/validation) |\n\n## Services and DI\n\n| Symbol | What it does | Page |\n| --------------------------- | ------------------------------------------ | ------------------------------------------------- |\n| `craftService` | Declares a named, scoped service | [craftService](/guide/app/craft-service) |\n| `abstract` | Declares a contract with no implementation | [Abstract services](/guide/app/abstract-services) |\n| `X.OmitInputs` | Opts out of a service's input bindings | [Public API](/guide/app/expose-api) |\n| `onAppStart` | Startup callback owned by a service | [App start](/guide/app/app-start) |\n| `craftLazy` | Defers a service's instantiation | [Lazy services](/guide/app/lazy-services) |\n| `craftRegisterFor` | Registry-driven service resolution | [Register](/guide/app/register) |\n| `provideCraftTargetWrapper` | Wraps craft targets at a provider boundary | [Target wrapper](/guide/app/target-wrapper) |\n| `provideTemplateTrace` | Wraps effective template renders | [Observability](/guide/advanced/observability) |\n| `provideCraftRouterTrace` | Wraps Router events and Craft route stages | [Observability](/guide/advanced/observability) |\n| `provideCraftHttpTrace` | Wraps CraftHttpClient requests | [Observability](/guide/advanced/observability) |\n| `craftAppConfig` | Application config with the routing graph | [Routing setup](/guide/routing/setup) |\n\n## Routing\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |\n| `craftRoute`, `craftRoutes` | Declares typed routes and collections | [Setup](/guide/routing/setup) |\n| `ValidateCascadeRoutesFile`, `CanRun` | Compile-time DI check for a routes file | [Setup](/guide/routing/setup) |\n| `RouteCheckedDI` | Per-route `O(1)` variant of the check | [Scaling routes](/guide/routing/scaling) |\n| `.withParent`, `ParentRoutes`, `assertChildRouteMounts` | Pins a child collection to its mount | [Scaling routes](/guide/routing/scaling) |\n| `withRetry` | Retryable lazy `loadComponent` / `loadChildren` | [Setup](/guide/routing/setup) |\n| `provideCraftRouter`, `provideCraftLoading` | Router with craft loading features | [Pending UI](/guide/routing/pending-ui) |\n| `withA11yNavigationFocus`, `CraftTitleStrategy` | Focus after nav; route `title` → document | [Accessibility](/guide/components/accessibility) |\n| `heading`, `headingSection`, `headingRoot`, `skipLink`, `liveRegion`, `fieldControl`, `disclosureControl`, `buttonControl`, `clickFocus` | Relative outline, skip link, live regions, accessible control props, focus | [Accessibility](/guide/components/accessibility) |\n| `withErrorComponent`, `withRouteLoadError`, `withTransitionTimings` | Router features | [Route load errors](/guide/routing/route-load-errors) |\n| `CraftRouterOutlet` | Non-blocking outlet | [Pending UI](/guide/routing/pending-ui) |\n| `craftRouterLink` | Type-safe navigation target | [Setup](/guide/routing/setup) |\n| `assertExhaustiveRouteExceptions` | Exhaustiveness proof for route exceptions | [Exceptions](/guide/concepts/exceptions) |\n\n## Server rendering\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |\n| `renderCraft`, `renderToString` | Renders an isolated request to HTML, CSS, and a transfer snapshot | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `startCraft` | Hydrates an SSR host or mounts a fresh client application automatically | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `hydrateCraft` | Restores transferred state and claims the existing browser DOM | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `pendingNode({ ssr })` | Declares `block`, `fallback`, or `client` behavior for suspended data | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `CRAFT_SSR_POLICY` | Route-level default SSR policy | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `CraftUnhandledSsrResolutionError`, `CraftSsrTimeoutError` | Reports missing policies and timed-out blocking sources | [SSR and hydration](/guide/advanced/ssr-hydration) |\n\n## Exceptions\n\n| Symbol | What it does | Page |\n| ---------------------------------- | ---------------------------------------- | --------------------------------------------------------------- |\n| `craftException` | Creates a declared, typed exception | [Exceptions](/guide/concepts/exceptions) |\n| `craftExceptionHandler` | Handles route exceptions | [Exceptions](/guide/concepts/exceptions) |\n| `.exceptions()`, `.hasException()` | Reads a primitive's exceptions by origin | [query](/guide/state/server-state) |\n| `globalError()` | Delegates to the global error component | [Global error component](/guide/routing/global-error-component) |\n\n## Reactivity\n\n| Symbol | What it does | Page |\n| -------------------- | ---------------------------------- | ------------------------------------------------------------ |\n| `craftComputed` | Tracked `computed` | [craftComputed](/guide/reactivity/craft-computed) |\n| `craftEffect` | Tracked `effect` | [craftEffect](/guide/reactivity/craft-effect) |\n| `craftMethod` | A tracked method on a primitive | [craftMethod](/guide/reactivity/craft-method) |\n| `source$` | An imperative event source | [source$](/guide/reactivity/source) |\n| `on$` | Binds a method to a source | [on$](/guide/reactivity/on) |\n| `fromEventToSource$` | DOM event → source | [fromEventToSource$](/guide/reactivity/from-event-to-source) |\n| `sourceFromEvent` | Event-driven source helper | [sourceFromEvent](/guide/reactivity/source-from-event) |\n| `afterRecomputation` | Runs after a recomputation settles | [afterRecomputation](/guide/reactivity/after-recomputation) |\n\n## HTTP and boundaries\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------- |\n| `CraftHttpClient` | Tracked HTTP client with typed exceptions | [query](/guide/state/server-state) |\n| `browserBoundary` | Marks a service as a browser boundary | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `BrowserDocument`, `BrowserDocument.setLang`, `BrowserDocument.setDir` | Reads and updates document title, language, and direction | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `Console` | Yieldable console, overridable for tracing | [Observability](/guide/advanced/observability) |\n\n## Testing\n\n| Symbol | What it does | Page |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------- |\n| `setupCraftServiceTestingByRegister` | Sets up a service from a full register | [Testing services](/guide/testing/services) |\n| `boundaryOnly` | Keeps the graph real, mocks boundaries | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `mockHttpRequestForRoute` | Mocks endpoints for a route | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `ComponentTemplateOf`, `ComponentLogicOutputOf`, `SetupTestComponentTemplate` | Resolves component logic and validates a template at compile time | [Type-level tests](/guide/testing/type-level) |\n| `TemplateHasElement`, `TemplateRendersNamedElementWhen`, `TemplateNamedElementRendersStateWhen`, `TemplateNamedElementDelegatesToContext`, `TemplateRenderAvailableActionWhen` | Proves what a template renders and uses | [Type-level tests](/guide/testing/type-level) |\n| `Expect`, `Equal` | Turns a type-level result into a compile-time assertion | [Type-level tests](/guide/testing/type-level) |\n| `createArchitectureGraph`, `noExclusiveLink`, `assertCraftUnique`, `assertHttpEndpointUnique`, `assertCraftComputedPure`, `assertNoDependencyCycles`, `assertDeclarativeArchitecture`, `assertRouteDiProofs`, `assertPathBoundaries`, `assertMutationHasReactOn`, `assertPrimitiveLoaderRequirements`, `assertQueryMutationHasServerState`, `assertPersistedPrimitiveHasUnique`, `assertInsertSelectUnique`, `assertCraftEffectNoNetwork`, `assertCraftEffectNoImperativeSync`, `assertInteractiveElementNamed` | Typed lookups and declarative architecture helpers | [Architecture rules](/guide/testing/architecture) |\n\n## Effect integration\n\n`@craft-ts/effect`, in full. The guide is [Effect\nintegration](/guide/advanced/effect).\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| `installCraftEffectBridge` | Installs both bridges once, at bootstrap | [Install the bridge](/guide/advanced/effect#install-the-bridge-once) |\n| `queryEffect`, `mutationEffect`, `asyncProcessEffect`, `computedEffect`, `methodEffect` | The Effect-backed adapters of the Craft primitives | [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter) |\n| `runEffect`, `CraftEffectInterrupted` | Yields one Effect and maps its exit onto Craft's channels | [runEffect](/guide/advanced/effect#runeffect-the-low-level-form) |\n| `syncEffect`, `SyncOp`, `CraftEffectNotSynchronous`, `NotDeclaredSynchronous` | Declares and runs an Effect that never suspends | [Synchronous members](/guide/advanced/effect#run-a-synchronous-member-from-a-computed) |\n| `provideLayer` | Attaches a built Effect context to a Craft injector | [Provide services with Layer](/guide/advanced/effect#provide-services-with-layer) |\n| `effectService`, `SelectedMembers` | Selects a service from a Craft factory, recording the dependency | [Select a service](/guide/advanced/effect#select-an-effect-service-from-craft) |\n| `mockEffectService`, `UnstubbedEffectMember` | A focused Layer for tests; an unstubbed member fails loudly | [Testing](/guide/advanced/effect#testing) |\n| `EffectRequirementsCheckedDI`, `ProvidedEffectServicesOf`, `ProvidedEffectServicesOfRoute` | The route-level proof that every requirement is provided | [Provide services with Layer](/guide/advanced/effect#provide-services-with-layer) |\n| `effectServerMiddleware`, `executeEffect`, `EffectServerMiddleware`, `EffectServerMiddlewareContext` | Effect middleware and execution for server functions | [Server functions POC](/guide/advanced/effect#server-functions-current-poc) |\n\n### Lower-level exports\n\nPublic, but rarely needed directly. They exist for wrappers, generated code and\ntooling rather than for application code.\n\n| Symbol | What it is |\n| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `composeEffect` | Composes yieldable Effect middleware in declaration order, without continuations. `effectServerMiddleware` is the everyday door. |\n| `runYieldedEffect` | The single-Effect runner the bridge itself calls. Use `runEffect`, which keeps the call site blamable. |\n| `assertNoRequirements`, `AssertNoRequirements`, `MissingRequirements`, `RealRequirements`, `CraftPhantomRequirement` | Moves the `R = never` check to the **yield site**, so an unmet requirement points at the offending line instead of surfacing at runtime. `CraftPhantomRequirement` is what excludes `SyncOp` from that check. |\n| `CRAFT_EFFECT_LEVEL`, `resolveEffectLevel`, `CraftEffectLevel` | The per-injector Effect level: the built context, a `MemoMap` forked from the parent's, and a scope closed with the injector. Read it when writing your own provider; `provideLayer` is the normal way in. |\n| `AsEffect`, `CraftProgramSuccess`, `CraftProgramExceptions` | A **type-only projection** of a Craft program onto `Effect<A, E>`. It changes no runtime behaviour; it exists so a hover tooltip reads `Effect<User, UserNotFound>` instead of a raw generator type. |\n| `installCraftSyncEffectBridge` | Already installed by `installCraftEffectBridge`. Call it directly only in a host that installs the synchronous bridge alone. |\n\n## Typed styles\n\n`@craft-ts/style` is a **build step**: none of these symbols emit anything\nwithout `craftStyle` from `@craft-ts/style/vite` in the Vite config. See\n[Activating the style system](/guide/style/setup).\n\n| Symbol | What it does | Page |\n| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| `craftStyle`, `emitStyles`, `renderCss`, `styleDump`, `findStyleModules` | The build-time emitter and its artefacts (`@craft-ts/style/vite`) | [Activating the style system](/guide/style/setup) |\n| `definePalette`, `darkOf`, `palette` | Colour tokens carrying both of their values, plus the default set | [Defining a design system](/guide/style/define) |\n| `defineBreakpoints`, `at`, `above`, `below` | The viewport axis, as an ordered one | [Defining a design system](/guide/style/define) |\n| `defineStateAxis`, `defineAxis`, `onlyVarsOfKind`, `axisPoint` | Attribute-driven axes, with an optional write constraint | [Defining a design system](/guide/style/define) |\n| `defineContainer` | A container axis, closed at the element that declares the container | [Defining a design system](/guide/style/define) |\n| `scheme`, `motion`, `forcedColors`, `contrast`, `scrollState`, `descendant` | The standard axes, driven by the user agent or by element state | [Axes and the matrix](/guide/style/variants) |\n| `cssVars`, `kind`, `assign`, `set` | Typed custom properties, registered through `@property` | [Tokens and variables](/guide/style/tokens) |\n| `space`, `unit`, `radii`, `radius`, `lineWidth`, `num`, `text`, `font` | The closed value scales — no value is a string | [Tokens and variables](/guide/style/tokens) |\n| `unsafeLength`, `unsafeAssume` | The marked escape hatches; both propagate `unproven` | [Tokens and variables](/guide/style/tokens) |\n| `craftStyles`, `when` | A sheet, and conjunction by nesting | [Axes and the matrix](/guide/style/variants) |\n| `requires`, `provides`, `declares`, `seal`, `scrollPort`, `noClipping`, `containerType`, `clipOverflow` | Context obligations, and where they become an error | [Context obligations](/guide/style/obligations) |\n| `visualMatrix`, `applyScenario`, `branch`, `contentCases`, `assertExhaustiveVisualMatrix`, `baselinesIn` | The scenario matrix (`@craft-ts/style-testing`) | [Testing visual states](/guide/style/testing) |\n| `matrixSizeByComponent`, `impactedClasses`, `varsWrittenBy`, `danglingVars`, `unproven`, `extractionGaps`, `undischargedObligations` | Graph queries over the style dump (`@craft-ts/dev-tools`) | [Testing visual states](/guide/style/testing) |\n| `style_impact`, `style_matrix`, `style_debt` | The same questions as MCP tools | [Testing visual states](/guide/style/testing#the-same-questions-from-an-agent) |\n\n## Internationalisation\n\n`@craft-ts/i18n` has no CraftTS, Angular or Effect import; the catalogue is a\nplain TypeScript value.\n\n| Symbol | What it does | Page |\n| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| `defineCatalog`, `msg`, `plural` | The catalogue, its messages, and per-locale plural categories | [The catalogue](/guide/i18n/catalog) |\n| `defineLocale`, `defineLocaleLike` | The reference locale, and every other one checked against it | [The catalogue](/guide/i18n/catalog) |\n| `number`, `integer`, `percent`, `compactNumber`, `money`, `dateShort`, `dateLong`, `dateTime`, `relativeTime` | The shipped semantic tokens, formatted through `Intl` | [Tokens](/guide/i18n/tokens) |\n| `defineToken`, `defineTokenFactory`, `formatters` | Project tokens, and the factory the shipped ones are built from | [Tokens](/guide/i18n/tokens) |\n| `createI18nRuntime`, `translate` / `t`, `setLocale`, `locale` | The runtime and its one active locale | [The runtime](/guide/i18n/runtime) |\n| `bind`, `createReactiveTranslator` | A translator that re-reads when the locale state changes | [The runtime](/guide/i18n/runtime#reactive-translation) |\n| `createI18nLoader`, `loadLocale` | Lazy locales, cached by id, evicted on failure | [The runtime](/guide/i18n/runtime#lazy-locales) |\n| `validateCatalog`, `assertValidCatalog`, `validateLocaleParity`, `assertLocaleParity` | The checks behind `npm run i18n:check` (also `@craft-ts/i18n/testing`) | [The catalogue](/guide/i18n/catalog#checking-outside-the-typechecker) |\n| `I18nRuntimeError` | `NO_LOCALES`, `LOCALE_NOT_LOADED`, `INVALID_NUMBER`, `INVALID_DATE` | [The runtime](/guide/i18n/runtime) |\n| `provideI18nRuntime`, `translateEffect`, `I18nEffectService` | The Effect adapter (`@craft-ts/i18n-effect`) | [With Effect](/guide/i18n/effect) |\n\n## Tooling\n\n| Command / rule | What it does | Page |\n| ---------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `npx craft route add` | Scaffolds a typed route | [Automation](/guide/routing/automation) |\n| `npx craft route split` | Splits a flat collection | [Scaling routes](/guide/routing/scaling) |\n| `npx craft route verify` | Optional compiler-fixture suite for the type machinery | [Automation](/guide/routing/automation#compiler-fixture-suite-optional) |\n| `craft-brand --root src` | Generates and refreshes `GenDeps_*` | [Brand config](/guide/routing/setup#generated-dependencies) |\n| `@craft-ts/dev-tools/eslint-rules` | The ESLint rule set | [ESLint rules](/guide/routing/eslint-rules) · [Accessibility](/guide/components/accessibility) |\n| `npx craft-graph` | Writes the static Craft graph | [Architecture rules](/guide/testing/architecture) · [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) |\n| `npx nx architecture <app>` | Runs the app's architecture Vitest suite | [Architecture rules](/guide/testing/architecture) · [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) |\n| Live page MCP `page` | Drive the open `ng serve` tab (dev only) | [Live page MCP](/guide/ai/dev-page) |\n| Template migrator | Migrates templates to craft components | [Template migrator](/guide/components/template-migrator) |\n\n## Deployment\n\n::: warning Experimental\nThe deployment tooling is not settled: these symbols and commands can still\nchange between minor versions. See the\n[deployment guide](/guide/deployment/) for what exists today.\n:::\n\n| Symbol / command | What it does | Page |\n| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------ |\n| `defineCraftDeployment` | Declares the deployment of an application in `craft.deploy.ts` | [Manifest reference](/guide/deployment/manifest) |\n| `checkCraftDeployment`, `checkCraftDeploymentArtifact` | Runs the manifest, module graph and artefact checks | [Diagnostics](/guide/deployment/diagnostics) |\n| `resolveCraftDeploymentManifest`, `serializeCraftDeploymentManifest`, `parseCraftDeploymentManifest` | Resolves, writes and reads the provider-neutral artefact form | [Manifest reference](/guide/deployment/manifest) |\n| `CraftDeploymentProvider`, `CRAFT_DEPLOYMENT_PROVIDERS` | The provider contract and the capability matrix | [Providers](/guide/deployment/providers) |\n| `npx craft-ts check` | Validates a deployment before building | [Deployment overview](/guide/deployment/) |\n| `npx craft-ts manifest` | Writes `dist/<app>/craft-deployment-manifest.json` | [Deployment overview](/guide/deployment/) |\n| `npx craft-ts deploy preview` | Shows what a provider would change, without changing it | [Alchemy provider](/guide/deployment/alchemy) |\n| `npx craft-ts deploy` | Applies that plan once `--yes` approves it | [Alchemy provider](/guide/deployment/alchemy) |\n| `createCraftDeploymentProvider` | The single factory a provider package exports | [Providers](/guide/deployment/providers) |\n| `createAlchemyDeploymentProvider`, `planAlchemyDeployment` | The Alchemy provider and its Cloudflare/AWS planning | [Alchemy provider](/guide/deployment/alchemy) |\n| `npx craft-ts providers` | Prints the provider capability matrix | [Providers](/guide/deployment/providers) |\n"
|
|
715
|
+
"body": "# API index\n\nEvery documented export, with the page that covers it. Use <kbd>Ctrl</kbd>/<kbd>⌘</kbd>+<kbd>F</kbd>.\n\nFor an explanation rather than a lookup, start from the [Guide](/guide/).\nCoding agents: [llms.txt](https://craft-ts.github.io/craft/llms.txt) and\n[coding agents](/resources/ai-agents).\n\n## Primitives\n\n| Symbol | What it does | Page |\n| ------------------- | -------------------------------------------------------- | --------------------------------------------- |\n| `state` | Signal-based state you own | [Local state](/guide/state/local-state) |\n| `craftStateMachine` | Declarative finite-state workflow | [State machines](/guide/state/state-machines) |\n| `query` | Server data, re-fetched from reactive `params` | [query](/guide/state/server-state) |\n| `mutation` | Server write, triggered explicitly | [Mutations](/guide/state/mutations) |\n| `queryParams` | State that lives in the URL query string | [queryParams](/guide/state/url-state) |\n| `asyncProcess` | One-off async operation with lifecycle state | [asyncProcess](/guide/state/async-process) |\n| `craftUse` | Drives a primitive outside a generator (component field) | [Learn 1](/learn/01-first-state) |\n\nNot sure which one: [Which primitive should I use?](/guide/concepts/choose-primitive)\n\n## Runtime context\n\nTyped helpers that recover `get` / `set` / `update` / `patch` from DI, for\nwrappers, WebMCP tools, and other advanced patterns. Everyday insertions\nalready receive those methods as arguments — see\n[Anatomy of a primitive](/guide/concepts/primitive-anatomy#injectable-runtime-context).\n\n| Symbol | What it does | Page |\n| ----------------------------------------- | --------------------------------------------------------------------- | ----------------------------------------------------------------------- |\n| `injectStateMethodRuntimeContext` | `state` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectQueryMethodRuntimeContext` | `query` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectMutationMethodRuntimeContext` | `mutation` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectQueryParamsMethodRuntimeContext` | `queryParams` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectAsyncProcessMethodRuntimeContext` | `asyncProcess` writes inside an insertion method | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `injectPrimitiveMethodRuntimeContext` | Same context, untyped `kind` | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n| `providePrimitiveResourceRuntimeObserver` | Observes `query` / `mutation` / `asyncProcess` / `queryParams` values | [Anatomy](/guide/concepts/primitive-anatomy#injectable-runtime-context) |\n\n## Composition\n\n| Symbol | What it does | Page |\n| ------------------------ | ----------------------------------------------- | -------------------------------------------------------- |\n| `craftPipe` | Composes several insertions into one | [Insertions](/guide/concepts/insertions) |\n| `craftYieldRecord` | Resolves a record of primitive generators | [craftService](/guide/app/craft-service) |\n| `insertStatePipe` | Composes several `state` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertQueryPipe` | Composes several `query` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertMutationPipe` | Composes several `mutation` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertQueryParamsPipe` | Composes several `queryParams` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertAsyncProcessPipe` | Composes several `asyncProcess` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `insertStateMachinePipe` | Composes several `craftStateMachine` insertions | [Typed insertion pipes](/guide/concepts/insertion-pipes) |\n| `craftGen` | A standalone tracked generator | [Generators](/guide/concepts/generators) |\n| `craftMatch` | Exhaustive pattern matching | [Pattern matching](/guide/advanced/pattern-matching) |\n| `.pipe(...)` | Program operators on a craft generator | [Program operators](/guide/advanced/program-operators) |\n| `catchTag`, `retry` | Operators for `.pipe(...)` | [Program operators](/guide/advanced/program-operators) |\n\n## Insertions\n\n| Symbol | What it does | Page |\n| --------------------------------- | ----------------------------------------------- | ------------------------------------------------------------- |\n| `insertSelect` | Derives a slice of a primitive | [Selecting](/guide/state/select) |\n| `insertEntities` | Entity collection storage and updates | [Collections](/guide/state/collections) |\n| `insertStoragePersister` | Persists through the configured storage backend | [Persistence](/guide/state/persistence) |\n| `insertReactOnMutation` | Reloads / optimistically patches on a mutation | [React on mutation](/guide/state/react-on-mutation) |\n| `insertPaginationPlaceholderData` | Placeholder rows while a page loads | [Pagination placeholder](/guide/state/pagination-placeholder) |\n\n## Forms\n\n| Symbol | What it does | Page |\n| --------------------------------------------------------------------------- | ------------------------------------------ | ------------------------------------- |\n| `insertForm` | Derives a form from a `state` | [Forms](/guide/forms/) |\n| `insertFormAttributes` | Validators, `disable`, `hidden` | [Forms](/guide/forms/) |\n| `insertSelectFormTree` | Targets a field sub-tree | [Nested forms](/guide/forms/nested) |\n| `insertSubFormField` | A nested sub-form | [Nested forms](/guide/forms/nested) |\n| `insertFormSubmit` | Wires submission to a mutation | [Submitting](/guide/forms/submit) |\n| `insertNoopTypingAnchor` | Type anchor required per field tree | [Forms](/guide/forms/) |\n| `CraftFieldDirective` | Binds a typed field to a Craft DOM node | [Forms](/guide/forms/) |\n| `fieldErrorNode.exhaustive` / `.partial` | Exhaustive or partial validation rendering | [Forms](/guide/forms/) |\n| `cRequired`, `cEmail`, `cMin`/`cMax`, `cMinLength`/`cMaxLength`, `cPattern` | Built-in validators | [Validators](/guide/forms/validation) |\n| `cValidate`, `cAsyncValidate` | Custom and async validators | [Validators](/guide/forms/validation) |\n\n## Services and DI\n\n| Symbol | What it does | Page |\n| --------------------------- | ------------------------------------------ | ------------------------------------------------- |\n| `craftService` | Declares a named, scoped service | [craftService](/guide/app/craft-service) |\n| `abstract` | Declares a contract with no implementation | [Abstract services](/guide/app/abstract-services) |\n| `X.OmitInputs` | Opts out of a service's input bindings | [Public API](/guide/app/expose-api) |\n| `onAppStart` | Startup callback owned by a service | [App start](/guide/app/app-start) |\n| `craftLazy` | Defers a service's instantiation | [Lazy services](/guide/app/lazy-services) |\n| `craftRegisterFor` | Registry-driven service resolution | [Register](/guide/app/register) |\n| `provideCraftTargetWrapper` | Wraps craft targets at a provider boundary | [Target wrapper](/guide/app/target-wrapper) |\n| `provideTemplateTrace` | Wraps effective template renders | [Observability](/guide/advanced/observability) |\n| `provideCraftRouterTrace` | Wraps Router events and Craft route stages | [Observability](/guide/advanced/observability) |\n| `provideCraftHttpTrace` | Wraps CraftHttpClient requests | [Observability](/guide/advanced/observability) |\n| `craftAppConfig` | Application config with the routing graph | [Routing setup](/guide/routing/setup) |\n\n## Routing\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ----------------------------------------------------- |\n| `craftRoute`, `craftRoutes` | Declares typed routes and collections | [Setup](/guide/routing/setup) |\n| `ValidateCascadeRoutesFile`, `CanRun` | Compile-time DI check for a routes file | [Setup](/guide/routing/setup) |\n| `RouteCheckedDI` | Per-route `O(1)` variant of the check | [Scaling routes](/guide/routing/scaling) |\n| `.withParent`, `ParentRoutes`, `assertChildRouteMounts` | Pins a child collection to its mount | [Scaling routes](/guide/routing/scaling) |\n| `withRetry` | Retryable lazy `loadComponent` / `loadChildren` | [Setup](/guide/routing/setup) |\n| `provideCraftRouter`, `provideCraftLoading` | Router with craft loading features | [Pending UI](/guide/routing/pending-ui) |\n| `withA11yNavigationFocus`, `CraftTitleStrategy` | Focus after nav; route `title` → document | [Accessibility](/guide/components/accessibility) |\n| `heading`, `headingSection`, `headingRoot`, `skipLink`, `liveRegion`, `fieldControl`, `disclosureControl`, `buttonControl`, `clickFocus` | Relative outline, skip link, live regions, accessible control props, focus | [Accessibility](/guide/components/accessibility) |\n| `withErrorComponent`, `withRouteLoadError`, `withTransitionTimings` | Router features | [Route load errors](/guide/routing/route-load-errors) |\n| `CraftRouterOutlet` | Non-blocking outlet | [Pending UI](/guide/routing/pending-ui) |\n| `craftRouterLink` | Type-safe navigation target | [Setup](/guide/routing/setup) |\n| `assertExhaustiveRouteExceptions` | Exhaustiveness proof for route exceptions | [Exceptions](/guide/concepts/exceptions) |\n\n## Server rendering\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------- |\n| `renderCraft`, `renderToString` | Renders an isolated request to HTML, CSS, and a transfer snapshot | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `startCraft` | Hydrates an SSR host or mounts a fresh client application automatically | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `hydrateCraft` | Restores transferred state and claims the existing browser DOM | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `pendingNode({ ssr })` | Declares `block`, `fallback`, or `client` behavior for suspended data | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `CRAFT_SSR_POLICY` | Route-level default SSR policy | [SSR and hydration](/guide/advanced/ssr-hydration) |\n| `CraftUnhandledSsrResolutionError`, `CraftSsrTimeoutError` | Reports missing policies and timed-out blocking sources | [SSR and hydration](/guide/advanced/ssr-hydration) |\n\n## Exceptions\n\n| Symbol | What it does | Page |\n| ---------------------------------- | ---------------------------------------- | --------------------------------------------------------------- |\n| `craftException` | Creates a declared, typed exception | [Exceptions](/guide/concepts/exceptions) |\n| `craftExceptionHandler` | Handles route exceptions | [Exceptions](/guide/concepts/exceptions) |\n| `.exceptions()`, `.hasException()` | Reads a primitive's exceptions by origin | [query](/guide/state/server-state) |\n| `globalError()` | Delegates to the global error component | [Global error component](/guide/routing/global-error-component) |\n\n## Reactivity\n\n| Symbol | What it does | Page |\n| -------------------- | ---------------------------------- | ------------------------------------------------------------ |\n| `craftComputed` | Tracked `computed` | [craftComputed](/guide/reactivity/craft-computed) |\n| `craftEffect` | Tracked `effect` | [craftEffect](/guide/reactivity/craft-effect) |\n| `craftMethod` | A tracked method on a primitive | [craftMethod](/guide/reactivity/craft-method) |\n| `source$` | An imperative event source | [source$](/guide/reactivity/source) |\n| `on$` | Binds a method to a source | [on$](/guide/reactivity/on) |\n| `fromEventToSource$` | DOM event → source | [fromEventToSource$](/guide/reactivity/from-event-to-source) |\n| `sourceFromEvent` | Event-driven source helper | [sourceFromEvent](/guide/reactivity/source-from-event) |\n| `afterRecomputation` | Runs after a recomputation settles | [afterRecomputation](/guide/reactivity/after-recomputation) |\n\n## HTTP and boundaries\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------- |\n| `CraftHttpClient` | Tracked HTTP client with typed exceptions | [query](/guide/state/server-state) |\n| `browserBoundary` | Marks a service as a browser boundary | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `BrowserDocument`, `BrowserDocument.setLang`, `BrowserDocument.setDir` | Reads and updates document title, language, and direction | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `Console` | Yieldable console, overridable for tracing | [Observability](/guide/advanced/observability) |\n\n## Testing\n\n| Symbol | What it does | Page |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | ------------------------------------------------------- |\n| `setupCraftServiceTestingByRegister` | Sets up a service from a full register | [Testing services](/guide/testing/services) |\n| `boundaryOnly` | Keeps the graph real, mocks boundaries | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `mockHttpRequestForRoute` | Mocks endpoints for a route | [Browser boundaries](/guide/testing/browser-boundaries) |\n| `ComponentTemplateOf`, `ComponentLogicOutputOf`, `SetupTestComponentTemplate` | Resolves component logic and validates a template at compile time | [Type-level tests](/guide/testing/type-level) |\n| `TemplateHasElement`, `TemplateRendersNamedElementWhen`, `TemplateNamedElementRendersStateWhen`, `TemplateNamedElementDelegatesToContext`, `TemplateRenderAvailableActionWhen` | Proves what a template renders and uses | [Type-level tests](/guide/testing/type-level) |\n| `Expect`, `Equal` | Turns a type-level result into a compile-time assertion | [Type-level tests](/guide/testing/type-level) |\n| `createArchitectureGraph`, `noExclusiveLink`, `assertCraftUnique`, `assertHttpEndpointUnique`, `assertCraftComputedPure`, `assertNoDependencyCycles`, `assertDeclarativeArchitecture`, `assertRouteDiProofs`, `assertPathBoundaries`, `assertMutationHasReactOn`, `assertPrimitiveLoaderRequirements`, `assertQueryMutationHasServerState`, `assertPersistedPrimitiveHasUnique`, `assertInsertSelectUnique`, `assertCraftEffectNoNetwork`, `assertCraftEffectNoImperativeSync`, `assertInteractiveElementNamed` | Typed lookups and declarative architecture helpers | [Architecture rules](/guide/testing/architecture) |\n\n## Effect integration\n\n`@craft-ts/effect`, in full. The guide is [Effect\nintegration](/guide/advanced/effect).\n\n| Symbol | What it does | Page |\n| ---------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| `installCraftEffectBridge` | Installs both bridges once, at bootstrap | [Install the bridge](/guide/advanced/effect#install-the-bridge-once) |\n| `queryEffect`, `mutationEffect`, `asyncProcessEffect`, `computedEffect`, `methodEffect` | The Effect-backed adapters of the Craft primitives | [Choose the right adapter](/guide/advanced/effect#choose-the-right-adapter) |\n| `runEffect`, `CraftEffectInterrupted` | Yields one Effect and maps its exit onto Craft's channels | [runEffect](/guide/advanced/effect#runeffect-the-low-level-form) |\n| `syncEffect`, `SyncOp`, `CraftEffectNotSynchronous`, `NotDeclaredSynchronous` | Declares and runs an Effect that never suspends | [Synchronous members](/guide/advanced/effect#run-a-synchronous-member-from-a-computed) |\n| `provideLayer` | Attaches a built Effect context to a Craft injector | [Provide services with Layer](/guide/advanced/effect#provide-services-with-layer) |\n| `effectService`, `SelectedMembers` | Selects a service from a Craft factory, recording the dependency | [Select a service](/guide/advanced/effect#select-an-effect-service-from-craft) |\n| `mockEffectService`, `UnstubbedEffectMember` | A focused Layer for tests; an unstubbed member fails loudly | [Testing](/guide/advanced/effect#testing) |\n| `EffectRequirementsCheckedDI`, `ProvidedEffectServicesOf`, `ProvidedEffectServicesOfRoute` | The route-level proof that every requirement is provided | [Provide services with Layer](/guide/advanced/effect#provide-services-with-layer) |\n| `effectServerMiddleware`, `executeEffect`, `EffectServerMiddleware`, `EffectServerMiddlewareContext` | Effect middleware and execution for server functions | [Server functions POC](/guide/advanced/effect#server-functions-current-poc) |\n\n### Lower-level exports\n\nPublic, but rarely needed directly. They exist for wrappers, generated code and\ntooling rather than for application code.\n\n| Symbol | What it is |\n| -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `composeEffect` | Composes yieldable Effect middleware in declaration order, without continuations. `effectServerMiddleware` is the everyday door. |\n| `runYieldedEffect` | The single-Effect runner the bridge itself calls. Use `runEffect`, which keeps the call site blamable. |\n| `assertNoRequirements`, `AssertNoRequirements`, `MissingRequirements`, `RealRequirements`, `CraftPhantomRequirement` | Moves the `R = never` check to the **yield site**, so an unmet requirement points at the offending line instead of surfacing at runtime. `CraftPhantomRequirement` is what excludes `SyncOp` from that check. |\n| `CRAFT_EFFECT_LEVEL`, `resolveEffectLevel`, `CraftEffectLevel` | The per-injector Effect level: the built context, a `MemoMap` forked from the parent's, and a scope closed with the injector. Read it when writing your own provider; `provideLayer` is the normal way in. |\n| `AsEffect`, `CraftProgramSuccess`, `CraftProgramExceptions` | A **type-only projection** of a Craft program onto `Effect<A, E>`. It changes no runtime behaviour; it exists so a hover tooltip reads `Effect<User, UserNotFound>` instead of a raw generator type. |\n| `installCraftSyncEffectBridge` | Already installed by `installCraftEffectBridge`. Call it directly only in a host that installs the synchronous bridge alone. |\n\n## Typed styles\n\n`@craft-ts/style` is a **build step**: none of these symbols emit anything\nwithout `craftStyle` from `@craft-ts/style/vite` in the Vite config. See\n[Activating the style system](/guide/style/setup).\n\n| Symbol | What it does | Page |\n| ------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |\n| `craftStyle`, `emitStyles`, `renderCss`, `styleDump`, `findStyleModules` | The build-time emitter and its artefacts (`@craft-ts/style/vite`) | [Activating the style system](/guide/style/setup) |\n| `definePalette`, `darkOf`, `palette` | Colour tokens carrying both of their values, plus the default set | [Defining a design system](/guide/style/define) |\n| `defineBreakpoints`, `at`, `above`, `below` | The viewport axis, as an ordered one | [Defining a design system](/guide/style/define) |\n| `defineStateAxis`, `defineAxis`, `onlyVarsOfKind`, `axisPoint` | Attribute-driven axes, with an optional write constraint | [Defining a design system](/guide/style/define) |\n| `defineContainer` | A container axis, closed at the element that declares the container | [Defining a design system](/guide/style/define) |\n| `scheme`, `motion`, `forcedColors`, `contrast`, `scrollState`, `descendant` | The standard axes, driven by the user agent or by element state | [Axes and the matrix](/guide/style/variants) |\n| `cssVars`, `kind`, `assign`, `set` | Typed custom properties, registered through `@property` | [Tokens and variables](/guide/style/tokens) |\n| `space`, `unit`, `radii`, `radius`, `lineWidth`, `num`, `text`, `font` | The closed value scales — no value is a string | [Tokens and variables](/guide/style/tokens) |\n| `unsafeLength`, `unsafeAssume` | The marked escape hatches; both propagate `unproven` | [Tokens and variables](/guide/style/tokens) |\n| `craftStyles`, `when` | A sheet, and conjunction by nesting | [Axes and the matrix](/guide/style/variants) |\n| `requires`, `provides`, `declares`, `seal`, `scrollPort`, `noClipping`, `containerType`, `clipOverflow` | Context obligations, and where they become an error | [Context obligations](/guide/style/obligations) |\n| `visualMatrix`, `applyScenario`, `branch`, `contentCases`, `assertExhaustiveVisualMatrix`, `baselinesIn` | The scenario matrix (`@craft-ts/style-testing`) | [Testing visual states](/guide/style/testing) |\n| `matrixSizeByComponent`, `impactedClasses`, `varsWrittenBy`, `danglingVars`, `unproven`, `extractionGaps`, `undischargedObligations` | Graph queries over the style dump (`@craft-ts/dev-tools`) | [Testing visual states](/guide/style/testing) |\n| `style_impact`, `style_matrix`, `style_debt` | The same questions as MCP tools | [Testing visual states](/guide/style/testing#the-same-questions-from-an-agent) |\n\n## Internationalisation\n\n`@craft-ts/i18n` is the CraftTS i18n integration: the catalogue stays a plain\nTypeScript value, and a token may resolve a Craft service or parse its\nparameter with a Standard Schema. The package imports core for types only.\n\n| Symbol | What it does | Page |\n| ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------- |\n| `defineCatalog`, `msg`, `plural` | The catalogue, its messages, and per-locale plural categories | [The catalogue](/guide/i18n/catalog) |\n| `defineLocale`, `defineLocaleLike` | The reference locale, and every other one checked against it | [The catalogue](/guide/i18n/catalog) |\n| `number`, `integer`, `percent`, `compactNumber`, `money`, `dateShort`, `dateLong`, `dateTime`, `relativeTime` | The shipped semantic tokens, formatted through `Intl` | [Tokens](/guide/i18n/tokens) |\n| `defineToken`, `defineTokenFactory`, `formatters`, `TokenFormatter`, `FormatterContext` | Project tokens, and the factory the shipped ones are built from | [Tokens](/guide/i18n/tokens) |\n| `createI18nRuntime`, `translate` / `t`, `setLocale`, `locale` | The runtime and its one active locale | [The runtime](/guide/i18n/runtime) |\n| `TranslationDependencies`, `StaticTranslationKey` | The services a message resolves, and the keys `t` can render alone | [The runtime](/guide/i18n/runtime#di-inside-a-translation) |\n| `TokenSchema`, `TokenSchemaInput`, `TokenSchemaOutput`, `TokenFactory` | Declaring a parameter with a Standard Schema | [Tokens](/guide/i18n/tokens) |\n| `bind`, `createReactiveTranslator` | A translator that re-reads when the locale state changes | [The runtime](/guide/i18n/runtime#reactive-translation) |\n| `createI18nLoader`, `loadLocale` | Lazy locales, cached by id, evicted on failure | [The runtime](/guide/i18n/runtime#lazy-locales) |\n| `validateCatalog`, `assertValidCatalog`, `validateLocaleParity`, `assertLocaleParity` | The checks behind `npm run i18n:check` (also `@craft-ts/i18n/testing`) | [The catalogue](/guide/i18n/catalog#checking-outside-the-typechecker) |\n| `serializeCatalog`, `serializeToken` | JSON-safe delivery shape; refuses a token that resolves a service | [The catalogue](/guide/i18n/catalog) |\n| `I18nRuntimeError` | `LOCALE_NOT_LOADED`, `MISSING_PARAM`, `INVALID_PARAM`, `CRAFT_INJECTION_REQUIRED`, … | [The runtime](/guide/i18n/runtime) |\n| `provideI18nRuntime`, `translateEffect`, `I18nEffectService` | The Effect adapter (`@craft-ts/i18n-effect`) | [With Effect](/guide/i18n/effect) |\n\n## Tooling\n\n| Command / rule | What it does | Page |\n| ---------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- |\n| `npx craft route add` | Scaffolds a typed route | [Automation](/guide/routing/automation) |\n| `npx craft route split` | Splits a flat collection | [Scaling routes](/guide/routing/scaling) |\n| `npx craft route verify` | Optional compiler-fixture suite for the type machinery | [Automation](/guide/routing/automation#compiler-fixture-suite-optional) |\n| `craft-brand --root src` | Generates and refreshes `GenDeps_*` | [Brand config](/guide/routing/setup#generated-dependencies) |\n| `@craft-ts/dev-tools/eslint-rules` | The ESLint rule set | [ESLint rules](/guide/routing/eslint-rules) · [Accessibility](/guide/components/accessibility) |\n| `npx craft-graph` | Writes the static Craft graph | [Architecture rules](/guide/testing/architecture) · [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) |\n| `npx nx architecture <app>` | Runs the app's architecture Vitest suite | [Architecture rules](/guide/testing/architecture) · [Craft graph vs Nx](/guide/testing/craft-graph-vs-nx) |\n| Live page MCP `page` | Drive the open `ng serve` tab (dev only) | [Live page MCP](/guide/ai/dev-page) |\n| Template migrator | Migrates templates to craft components | [Template migrator](/guide/components/template-migrator) |\n\n## Deployment\n\n::: warning Experimental\nThe deployment tooling is not settled: these symbols and commands can still\nchange between minor versions. See the\n[deployment guide](/guide/deployment/) for what exists today.\n:::\n\n| Symbol / command | What it does | Page |\n| ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------ |\n| `defineCraftDeployment` | Declares the deployment of an application in `craft.deploy.ts` | [Manifest reference](/guide/deployment/manifest) |\n| `checkCraftDeployment`, `checkCraftDeploymentArtifact` | Runs the manifest, module graph and artefact checks | [Diagnostics](/guide/deployment/diagnostics) |\n| `resolveCraftDeploymentManifest`, `serializeCraftDeploymentManifest`, `parseCraftDeploymentManifest` | Resolves, writes and reads the provider-neutral artefact form | [Manifest reference](/guide/deployment/manifest) |\n| `CraftDeploymentProvider`, `CRAFT_DEPLOYMENT_PROVIDERS` | The provider contract and the capability matrix | [Providers](/guide/deployment/providers) |\n| `npx craft-ts check` | Validates a deployment before building | [Deployment overview](/guide/deployment/) |\n| `npx craft-ts manifest` | Writes `dist/<app>/craft-deployment-manifest.json` | [Deployment overview](/guide/deployment/) |\n| `npx craft-ts deploy preview` | Shows what a provider would change, without changing it | [Alchemy provider](/guide/deployment/alchemy) |\n| `npx craft-ts deploy` | Applies that plan once `--yes` approves it | [Alchemy provider](/guide/deployment/alchemy) |\n| `createCraftDeploymentProvider` | The single factory a provider package exports | [Providers](/guide/deployment/providers) |\n| `createAlchemyDeploymentProvider`, `planAlchemyDeployment` | The Alchemy provider and its Cloudflare/AWS planning | [Alchemy provider](/guide/deployment/alchemy) |\n| `npx craft-ts providers` | Prints the provider capability matrix | [Providers](/guide/deployment/providers) |\n"
|
|
716
716
|
},
|
|
717
717
|
{
|
|
718
718
|
"path": "/resources/ai-agents",
|
|
@@ -738,7 +738,7 @@
|
|
|
738
738
|
{
|
|
739
739
|
"path": "/resources/examples",
|
|
740
740
|
"title": "Examples",
|
|
741
|
-
"body": "# Examples\n\nEvery example below is a real route of one of the demo applications. Each\nopens in StackBlitz on the relevant file, already navigated to the page.\n\nThe demo groups them the way you would meet them: **components** first, then the\n**primitives** on their own, then the same features **behind services**, then\n**routing** and the rest.\n\n::: tip Just want to poke at something?\nThe [Playground](https://stackblitz.com/fork/github/craft-ts/craft-ts-demo/tree/main?file=src%2Fapp%2Fexamples%2Fplayground%2Fplayground.ts&initialpath=%2Fplayground)\nis a shareable sandbox with a small todo flow — the fastest way to try an idea.\n:::\n\n## Components\n\nFunctional, selectorless components rendered from typed hyperscript.\n\n| Example | What it shows |\n| --- | --- |\n| [Functional Components](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/component-demo.ts&initialpath=/) | `craftComponent`, inputs and outputs as factory parameters, hyperscript templates |\n| [Reactive Composition](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/component-composition-demo.ts&initialpath=/component-composition) | Composing components and directives with `.pipe(...)` |\n| [Content Projection](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/content-projection-demo.ts&initialpath=/content-projection) | Free DOM content, typed DOM contracts, and logical projection by contract |\n| [Pending Block](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/pending-node-demo.ts&initialpath=/pending-node) | Type-safe async suspension with `settledValue`, `settled(...)` and `pendingNode` |\n| [Pending Block — Exception](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/pending-node-exception-demo.ts&initialpath=/pending-node/exception) | Coordinating pending, reloading and business-exception fallbacks with `pendingNode` and `catchNode` |\n\n## Primitives\n\nUsing `state`, `query`, `mutation`, `queryParams` and `asyncProcess` directly,\nwith no service layer.\n\n| Example | What it shows |\n| --- | --- |\n| [Query](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/query/query.ts&initialpath=/query/1) | `query()` with reactive params, status and caching |\n| [Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/mutation/mutation.ts&initialpath=/mutation/1) | `mutation()` with manual control of modification operations |\n| [List with Pagination](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/list-with-pagination/list-with-pagination.ts&initialpath=/list-with-pagination) | Pagination with hand-managed query params and page state |\n| [Granular Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/granular-mutation/granular-mutation.ts&initialpath=/granular-mutation) | Optimistic updates and cache invalidation, done by hand |\n| [Full Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/full-demo/full-demo.ts&initialpath=/full-demo) | Everything at once, without store or service abstractions |\n| [Login Form](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/forms/login-form.ts&initialpath=/login-form) | `insertForm`, validators, and a typed submit wired to a mutation |\n| [Pixel Art](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/pixel-art/pixel-art.ts&initialpath=/pixel-art) | `state` + `insertSelect` over a flat array |\n| [Pixel Art Matrix](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/pixel-art-matrix/pixel-art-matrix.ts&initialpath=/pixel-art-matrix) | Nested `insertSelect` and internal `source$` between rows and cells |\n| [Exceptions](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/exceptions/exceptions.ts&initialpath=/exceptions) | Business exceptions on `query()`, rendered per code with `matchNode.exhaustive` |\n| [Exception QueryParams](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/exceptions/exception-query-params.ts&initialpath=/exception-query-params) | `queryParams` decode failures through `hasException()` and `exceptions().parse` |\n\n## State machines\n\nState machines for explicit transitions, history and collection-oriented UI.\n\n| Example | What it shows |\n| --- | --- |\n| [Profile editor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine/profile-editor.ts&initialpath=/state-machine) | `craftStateMachine`, typed transitions and persisted history |\n| [Text editor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine/text-editor.ts&initialpath=/state-machine-text) | A compact state machine for editing, validation and transitions |\n| [Task board](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine-list/task-board.ts&initialpath=/state-machine-list) | A state machine per list item with history and reactive collection updates |\n\n## Services\n\nThe same features, packaged behind `craftService`.\n\n| Example | What it shows |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| [Craft Query](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/query/query.ts&initialpath=/craft/query/1) | A reusable query service with configured storage persistence (localStorage by default) |\n| [Craft Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/mutation/mutation.ts&initialpath=/craft/mutation/1) | Create / update / delete with reactive cache synchronisation |\n| [Craft List Pagination](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/list-with-pagination/list-with-pagination.ts&initialpath=/craft/list-with-pagination) | `queryParams` + `insertPaginationPlaceholderData` in a service |\n| [Craft Granular Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/granular-mutation/granular-mutation.ts&initialpath=/craft/granular-mutation) | `insertReactOnMutation` updating cached data without a reload |\n| [Craft Full Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/full-demo/full-demo.ts&initialpath=/craft/full-demo) | Queries, mutations, async work, URL state and persistence together |\n| [craftService Counter](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/craft-service-counter.ts&initialpath=/craft-service/counter) | The smallest possible service — scopes and composition |\n| [craftService User Detail](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/craft-service-user-detail.ts&initialpath=/craft-service/user-detail) | Service inputs, and exposing only part of a dependency |\n| [craftRegisterFor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/register-for.ts&initialpath=/craft-service/register-for) | A parent driving live children through a typed registry |\n\n## Effect\n\nConcrete EffectTS integration examples, using the dedicated Effect demo.\n\n| Example | What it shows |\n| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Profile Lookup](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-profile-lookup.ts&initialpath=/) | `queryEffect`, typed business errors, and pending / exception rendering |\n| [Access Check](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-access-check-shared-service.ts&initialpath=/access) | An Effect service provided by the application Layer |\n| [Team Overview](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-team-overview-layer-scope.ts&initialpath=/team) | Combining application-wide and route-scoped Effect Layers |\n| [Effect Playground](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-playground.ts&initialpath=/playground) | A shareable todo sandbox with `queryEffect`, `mutationEffect`, and a route-provided Effect service |\n| [Translate in an Effect](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/shared/i18n-domain.ts&initialpath=/i18n) | `provideI18nRuntime` as a route Layer, `translateEffect` inside a plain Effect program, and the locale as Craft state driving the query params |\n\n## Design system\n\nThe typed style system, at all three of its levels. Both routes read from the\nsame sheets under `src/app/examples/design-system/`, which has a README walking\nthrough the same progression in code.\n\n| Example | What it shows |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Mini Design System](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/design-system/foundation.style.ts&initialpath=/design-system) | `definePalette`, `defineStateAxis`, `cssVars` and the theme: one dark-mode rule for the whole system, and variants as `data-*` attributes rather than class strings |\n| [Scroll context](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/design-system/scroll.style.ts&initialpath=/design-system/scroll) | Level 3: `requires(scrollPort.block)` travelling up the tree, `provides(...)` on the layout that owns the area, and the `scrollState` axis |\n\nStart from [Activating the style system](/guide/style/setup) — the sheets emit\nnothing without the Vite plugin.\n\n## Internationalisation\n\n| Example | What it shows |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Type-safe i18n](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/i18n/i18n.service.ts&initialpath=/i18n) | `defineCatalog` + `msg` + `plural`, a second locale through `defineLocaleLike`, every shipped semantic token, a custom `defineToken`, and `runtime.bind` switching the whole page reactively |\n\nThe guide is [Type-safe i18n](/guide/i18n/).\n\n## Routing\n\n| Example | What it shows |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| [Query Params in the route](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/list-with-pagination/qp-list-with-pagination.ts&initialpath=/query-params) | `queryParams` declared on the route rather than in a component |\n| [Guard Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/guard-demo/GuardDemo.ts&initialpath=/guard-demo) | Guards as bare generators, and `handleExceptions` per code |\n| [Slow Page](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/slow-page/slow-page.routes.ts&initialpath=/slow-page) | Non-blocking navigation: the stay → blank → loader phases, and a `craftGen` resolver recovered locally with `catchTag` |\n| [View Transitions](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/view-transitions/view-transitions.routes.ts&initialpath=/view-transitions) | Outlet-driven view transitions surviving the guard/resolve chain, with a per-route skeleton |\n| [Lazy Layout](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/lazy-layout/lazy-layout.routes.ts&initialpath=/craft/lazy-layout/1) | A lazy child collection with its own DI check and a route-provided service |\n\n## Tooling\n\n| Example | What it shows |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| [Playground](https://stackblitz.com/fork/github/craft-ts/craft-ts-demo/tree/main?file=src%2Fapp%2Fexamples%2Fplayground%2Fplayground.ts&initialpath=%2Fplayground) | A shareable sandbox: a small todo flow with `craftService`, `query()` and `mutation()` |\n| [Send Context to AI](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/ia/demo-send-context/demo-send-context.ts&initialpath=/demo-send-context) | Exporting the live dependency graph and app context to an assistant |\n\n## Notes\n\nEach example ships its own `api.service.ts` simulating the network, so every\nroute works standalone.\n\nSource repository:\n[craft-ts-demo](https://github.com/craft-ts/craft-ts-demo).\n\nEffect demo source repository:\n[craft-demo-effect](https://github.com/craft-ts/craft-demo-effect).\n"
|
|
741
|
+
"body": "# Examples\n\nEvery example below is a real route of one of the demo applications. Each\nopens in StackBlitz on the relevant file, already navigated to the page.\n\nThe demo groups them the way you would meet them: **components** first, then the\n**primitives** on their own, then the same features **behind services**, then\n**routing** and the rest.\n\n::: tip Just want to poke at something?\nThe [Playground](https://stackblitz.com/fork/github/craft-ts/craft-ts-demo/tree/main?file=src%2Fapp%2Fexamples%2Fplayground%2Fplayground.ts&initialpath=%2Fplayground)\nis a shareable sandbox with a small todo flow — the fastest way to try an idea.\n:::\n\n## Components\n\nFunctional, selectorless components rendered from typed hyperscript.\n\n| Example | What it shows |\n| --- | --- |\n| [Functional Components](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/component-demo.ts&initialpath=/) | `craftComponent`, inputs and outputs as factory parameters, hyperscript templates |\n| [Reactive Composition](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/component-composition-demo.ts&initialpath=/component-composition) | Composing components and directives with `.pipe(...)` |\n| [Content Projection](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/content-projection-demo.ts&initialpath=/content-projection) | Free DOM content, typed DOM contracts, and logical projection by contract |\n| [Pending Block](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/pending-node-demo.ts&initialpath=/pending-node) | Type-safe async suspension with `settledValue`, `settled(...)` and `pendingNode` |\n| [Pending Block — Exception](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/component/pending-node-exception-demo.ts&initialpath=/pending-node/exception) | Coordinating pending, reloading and business-exception fallbacks with `pendingNode` and `catchNode` |\n\n## Primitives\n\nUsing `state`, `query`, `mutation`, `queryParams` and `asyncProcess` directly,\nwith no service layer.\n\n| Example | What it shows |\n| --- | --- |\n| [Query](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/query/query.ts&initialpath=/query/1) | `query()` with reactive params, status and caching |\n| [Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/mutation/mutation.ts&initialpath=/mutation/1) | `mutation()` with manual control of modification operations |\n| [List with Pagination](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/list-with-pagination/list-with-pagination.ts&initialpath=/list-with-pagination) | Pagination with hand-managed query params and page state |\n| [Granular Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/granular-mutation/granular-mutation.ts&initialpath=/granular-mutation) | Optimistic updates and cache invalidation, done by hand |\n| [Full Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/full-demo/full-demo.ts&initialpath=/full-demo) | Everything at once, without store or service abstractions |\n| [Login Form](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/forms/login-form.ts&initialpath=/login-form) | `insertForm`, validators, and a typed submit wired to a mutation |\n| [Pixel Art](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/pixel-art/pixel-art.ts&initialpath=/pixel-art) | `state` + `insertSelect` over a flat array |\n| [Pixel Art Matrix](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/pixel-art-matrix/pixel-art-matrix.ts&initialpath=/pixel-art-matrix) | Nested `insertSelect` and internal `source$` between rows and cells |\n| [Exceptions](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/exceptions/exceptions.ts&initialpath=/exceptions) | Business exceptions on `query()`, rendered per code with `matchNode.exhaustive` |\n| [Exception QueryParams](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/exceptions/exception-query-params.ts&initialpath=/exception-query-params) | `queryParams` decode failures through `hasException()` and `exceptions().parse` |\n\n## State machines\n\nState machines for explicit transitions, history and collection-oriented UI.\n\n| Example | What it shows |\n| --- | --- |\n| [Profile editor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine/profile-editor.ts&initialpath=/state-machine) | `craftStateMachine`, typed transitions and persisted history |\n| [Text editor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine/text-editor.ts&initialpath=/state-machine-text) | A compact state machine for editing, validation and transitions |\n| [Task board](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/primitives/state-machine-list/task-board.ts&initialpath=/state-machine-list) | A state machine per list item with history and reactive collection updates |\n\n## Services\n\nThe same features, packaged behind `craftService`.\n\n| Example | What it shows |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |\n| [Craft Query](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/query/query.ts&initialpath=/craft/query/1) | A reusable query service with configured storage persistence (localStorage by default) |\n| [Craft Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/mutation/mutation.ts&initialpath=/craft/mutation/1) | Create / update / delete with reactive cache synchronisation |\n| [Craft List Pagination](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/list-with-pagination/list-with-pagination.ts&initialpath=/craft/list-with-pagination) | `queryParams` + `insertPaginationPlaceholderData` in a service |\n| [Craft Granular Mutation](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/granular-mutation/granular-mutation.ts&initialpath=/craft/granular-mutation) | `insertReactOnMutation` updating cached data without a reload |\n| [Craft Full Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/full-demo/full-demo.ts&initialpath=/craft/full-demo) | Queries, mutations, async work, URL state and persistence together |\n| [craftService Counter](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/craft-service-counter.ts&initialpath=/craft-service/counter) | The smallest possible service — scopes and composition |\n| [craftService User Detail](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/craft-service-user-detail.ts&initialpath=/craft-service/user-detail) | Service inputs, and exposing only part of a dependency |\n| [craftRegisterFor](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft-service/register-for.ts&initialpath=/craft-service/register-for) | A parent driving live children through a typed registry |\n\n## Effect\n\nConcrete EffectTS integration examples, using the dedicated Effect demo.\n\n| Example | What it shows |\n| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Profile Lookup](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-profile-lookup.ts&initialpath=/) | `queryEffect`, typed business errors, and pending / exception rendering |\n| [Access Check](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-access-check-shared-service.ts&initialpath=/access) | An Effect service provided by the application Layer |\n| [Team Overview](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-team-overview-layer-scope.ts&initialpath=/team) | Combining application-wide and route-scoped Effect Layers |\n| [Effect Playground](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/examples/effect/effect-playground.ts&initialpath=/playground) | A shareable todo sandbox with `queryEffect`, `mutationEffect`, and a route-provided Effect service |\n| [Translate in an Effect](https://stackblitz.com/github/craft-ts/craft-demo-effect/tree/main/?file=src/app/shared/i18n-domain.ts&initialpath=/i18n) | `provideI18nRuntime` as a route Layer, `translateEffect` inside a plain Effect program, and the locale as Craft state driving the query params |\n\n## Design system\n\nThe typed style system, at all three of its levels. Both routes read from the\nsame sheets under `src/app/examples/design-system/`, which has a README walking\nthrough the same progression in code.\n\n| Example | What it shows |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Mini Design System](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/design-system/foundation.style.ts&initialpath=/design-system) | `definePalette`, `defineStateAxis`, `cssVars` and the theme: one dark-mode rule for the whole system, and variants as `data-*` attributes rather than class strings |\n| [Scroll context](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/design-system/scroll.style.ts&initialpath=/design-system/scroll) | Level 3: `requires(scrollPort.block)` travelling up the tree, `provides(...)` on the layout that owns the area, and the `scrollState` axis |\n\nStart from [Activating the style system](/guide/style/setup) — the sheets emit\nnothing without the Vite plugin.\n\n## Internationalisation\n\n| Example | What it shows |\n| ---------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| [Type-safe i18n](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/i18n/i18n.service.ts&initialpath=/i18n) | `defineCatalog` + `msg` + `plural`, a second locale through `defineLocaleLike`, every shipped semantic token, a custom `defineToken` that resolves a service and parses its parameter with a schema, and `runtime.bind` switching the whole page reactively |\n\nThe guide is [Type-safe i18n](/guide/i18n/).\n\n## Routing\n\n| Example | What it shows |\n| --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |\n| [Query Params in the route](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/list-with-pagination/qp-list-with-pagination.ts&initialpath=/query-params) | `queryParams` declared on the route rather than in a component |\n| [Guard Demo](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/guard-demo/GuardDemo.ts&initialpath=/guard-demo) | Guards as bare generators, and `handleExceptions` per code |\n| [Slow Page](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/slow-page/slow-page.routes.ts&initialpath=/slow-page) | Non-blocking navigation: the stay → blank → loader phases, and a `craftGen` resolver recovered locally with `catchTag` |\n| [View Transitions](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/routes/view-transitions/view-transitions.routes.ts&initialpath=/view-transitions) | Outlet-driven view transitions surviving the guard/resolve chain, with a per-route skeleton |\n| [Lazy Layout](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/craft/lazy-layout/lazy-layout.routes.ts&initialpath=/craft/lazy-layout/1) | A lazy child collection with its own DI check and a route-provided service |\n\n## Tooling\n\n| Example | What it shows |\n| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |\n| [Playground](https://stackblitz.com/fork/github/craft-ts/craft-ts-demo/tree/main?file=src%2Fapp%2Fexamples%2Fplayground%2Fplayground.ts&initialpath=%2Fplayground) | A shareable sandbox: a small todo flow with `craftService`, `query()` and `mutation()` |\n| [Send Context to AI](https://stackblitz.com/github/craft-ts/craft-ts-demo/tree/main/?file=src/app/examples/ia/demo-send-context/demo-send-context.ts&initialpath=/demo-send-context) | Exporting the live dependency graph and app context to an assistant |\n\n## Notes\n\nEach example ships its own `api.service.ts` simulating the network, so every\nroute works standalone.\n\nSource repository:\n[craft-ts-demo](https://github.com/craft-ts/craft-ts-demo).\n\nEffect demo source repository:\n[craft-demo-effect](https://github.com/craft-ts/craft-demo-effect).\n"
|
|
742
742
|
},
|
|
743
743
|
{
|
|
744
744
|
"path": "/resources/migration",
|
package/package.json
CHANGED
package/skills/craft-ts/SKILL.md
CHANGED
|
@@ -60,8 +60,10 @@ If MCP is not configured, read https://ng-angular-stack.github.io/craft/llms.txt
|
|
|
60
60
|
typecheck and emit nothing. Load `craft-ts-style` before touching one.
|
|
61
61
|
- Translations live in a `@craft-ts/i18n` catalogue: `defineCatalog` + `msg` for
|
|
62
62
|
the reference locale, `defineLocaleLike` for every other one, so a missing key
|
|
63
|
-
is a compile error.
|
|
64
|
-
|
|
63
|
+
is a compile error. A token may validate or parse its parameter with a
|
|
64
|
+
Standard Schema, and resolve its formatter from a Craft service; a project
|
|
65
|
+
generated with i18n also forbids visible literals in templates. No Effect
|
|
66
|
+
import — use `@craft-ts/i18n-effect` only inside an Effect program. Load
|
|
65
67
|
`craft-ts-i18n` before adding a key, a locale or a token.
|
|
66
68
|
- Forms start with `state` + `insertForm`. Choose `insertSelectFormTree` for a
|
|
67
69
|
nested field, `insertFormAttributes` for validators/visibility,
|
|
@@ -22,7 +22,8 @@ suite that already exists.
|
|
|
22
22
|
|
|
23
23
|
## 1. Bootstrap a new project
|
|
24
24
|
|
|
25
|
-
Prefer `craft create`, which asks for
|
|
25
|
+
Prefer `craft create`, which asks for the application type first, recommends
|
|
26
|
+
EffectTS v4 for a full-stack backend, and then asks for agent integrations. It
|
|
26
27
|
generates the application, API/page example, routes, ESLint, unit tests,
|
|
27
28
|
architecture suite and Playwright commands together:
|
|
28
29
|
|
|
@@ -30,6 +30,14 @@ examples copied from older documentation.
|
|
|
30
30
|
- Never declare `SyncOp` on a member that can suspend. The claim is checked by
|
|
31
31
|
`craft-ts/sync-effect-body` on the body and by `Effect.runSyncExitWith` at
|
|
32
32
|
runtime, which throws `CraftEffectNotSynchronous` on the first call.
|
|
33
|
+
- Never use JavaScript `try/catch` inside `Effect.gen`; model failures with
|
|
34
|
+
Effect's error channel and combinators such as `Effect.result`.
|
|
35
|
+
- Use `return yield*` for terminal effects such as `Effect.fail`, `Effect.die`
|
|
36
|
+
and `Effect.interrupt`.
|
|
37
|
+
- Prefer `Effect.fnUntraced` for reusable functions whose implementation only
|
|
38
|
+
wraps `Effect.gen`; reserve `Effect.gen` for inline composition and one-off
|
|
39
|
+
programs.
|
|
40
|
+
- Prefer the class syntax when defining `Context.Service` contracts.
|
|
33
41
|
- Install `installCraftEffectBridge()` once during bootstrap.
|
|
34
42
|
- Run the Effect diagnostics command from `package.json` after changing an
|
|
35
43
|
Effect generator, service, schema or Layer.
|
|
@@ -5,9 +5,11 @@ description: Build and review type-safe internationalisation in a CraftTS projec
|
|
|
5
5
|
|
|
6
6
|
# CraftTS type-safe i18n
|
|
7
7
|
|
|
8
|
-
`@craft-ts/i18n`
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
`@craft-ts/i18n` integrates with CraftTS for DI-aware translation tokens. The
|
|
9
|
+
catalogue remains declarative, and non-DI messages still work with `runtime.t`.
|
|
10
|
+
Use the CraftTS-bound translator whenever a token yields a service. Do not reach
|
|
11
|
+
for Effect to translate a string; use `@craft-ts/i18n-effect` only inside an
|
|
12
|
+
Effect program.
|
|
11
13
|
|
|
12
14
|
The contract it enforces, all at typecheck time:
|
|
13
15
|
|
|
@@ -89,6 +91,93 @@ return { language, setLocale: language.setLocale, translate: runtime.bind(langua
|
|
|
89
91
|
`bind(...)('key', params)` returns a generator the template yields, like any
|
|
90
92
|
other Craft reader.
|
|
91
93
|
|
|
94
|
+
### DI-aware tokens
|
|
95
|
+
|
|
96
|
+
Any token factory accepts a **generator function** in place of the adapter. The
|
|
97
|
+
yielded services are carried into the translation reader's component dependency
|
|
98
|
+
contract:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const amount = money('amount', function* () {
|
|
102
|
+
const currency = yield* ClientCurrency();
|
|
103
|
+
return { currency: currency.code, minimumFractionDigits: 2 };
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const catalog = defineCatalog({
|
|
107
|
+
order: msg`Order total ${amount}.`,
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Render it with the translator returned by `runtime.bind(...)`, and **pass the
|
|
112
|
+
reader** — as a child or as an attribute value:
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
p(translate('order', { amount: 1234.5 }));
|
|
116
|
+
p({ title: translate('order', { amount: 1234.5 }) }, 'Order');
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Both carry the dependency, so the component and route DI checks fail
|
|
120
|
+
compilation if `ClientCurrency` is not provided. Do not drive the reader
|
|
121
|
+
yourself (`yield* translate(...)()`): it renders the same string but the
|
|
122
|
+
dependency disappears from the check.
|
|
123
|
+
|
|
124
|
+
`runtime.t` accepts `StaticTranslationKey` only — the keys that resolve no
|
|
125
|
+
service — so a DI message on that path is a compile error, not a runtime one.
|
|
126
|
+
An arrow function that returns a generator is refused: the options factory must
|
|
127
|
+
be a `function*`.
|
|
128
|
+
|
|
129
|
+
### No visible literal in a template
|
|
130
|
+
|
|
131
|
+
A project generated with i18n ships `craft-ts/require-i18n-text` in its ESLint
|
|
132
|
+
configuration: a static string in `heading`/`p`/`label`/`button`/`a`/`option`/…
|
|
133
|
+
or in a `placeholder`, `aria-label` or `title` attribute is an error. Put the
|
|
134
|
+
copy in `src/i18n/catalog.ts` (and in every locale) and read it with
|
|
135
|
+
`i18n.t(...)`.
|
|
136
|
+
|
|
137
|
+
Wrapping the literal does not hide it — concatenation, template text, ternary
|
|
138
|
+
branches, `||` fallbacks and children arrays are all inspected:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
p('Total: ' + i18n.t('cart.total', { amount })); // reported: 'Total: '
|
|
142
|
+
p(i18n.t('cart.totalLine', { amount })); // the whole sentence is a key
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Only text carrying letters counts, so `first + ' ' + last` is fine. The key and
|
|
146
|
+
parameters of `i18n.t(...)`, generator children, catalogue files, server files
|
|
147
|
+
and tests are all exempt. A project generated without i18n does not get the
|
|
148
|
+
rule.
|
|
149
|
+
|
|
150
|
+
A project token does the same through `defineToken`, and declares no `format`
|
|
151
|
+
when the formatter only exists at render time:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const weight = defineToken({
|
|
155
|
+
name: 'weight',
|
|
156
|
+
kind: 'weight',
|
|
157
|
+
resolveFormatter: function* () {
|
|
158
|
+
const units = yield* Units();
|
|
159
|
+
const unit = (yield* units.system()) === 'imperial' ? 'pound' : 'kilogram';
|
|
160
|
+
return (value: number, context) =>
|
|
161
|
+
new Intl.NumberFormat(context.locale, { style: 'unit', unit }).format(value);
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Schema-declared parameters
|
|
167
|
+
|
|
168
|
+
The adapter position also takes a Standard Schema (Zod, Valibot, ArkType — the
|
|
169
|
+
same contract as `state`, `query` and forms). The parameter type becomes the
|
|
170
|
+
schema's input and the formatter receives its output, so the schema parses
|
|
171
|
+
once, in the catalogue:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const placedAt = dateLong('placedAt', z.coerce.date());
|
|
175
|
+
translate('order', { placedAt: '2026-08-25T14:30:00Z' });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`defineToken` takes the same `schema` field and may combine it with
|
|
179
|
+
`resolveFormatter`.
|
|
180
|
+
|
|
92
181
|
## With Effect
|
|
93
182
|
|
|
94
183
|
`@craft-ts/i18n-effect` is the adapter, and only the adapter:
|