@craft-ts/mcp 0.7.0-beta.19 → 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.
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",
|
|
@@ -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",
|
package/package.json
CHANGED
|
@@ -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.
|