| 1 | # @effect/doctest |
| 2 | |
| 3 | `@effect/doctest` extracts marked TypeScript examples from JSDoc comments, Markdown, and MDX files, then runs each example as an isolated Vitest module. |
| 4 | |
| 5 | ## Installation |
| 6 | |
| 7 | Install Vitest 5 (`>=5.0.0 <6.0.0`) and Vite `>=8.1.5 <9.0.0` alongside the package: |
| 8 | |
| 9 | ```sh |
| 10 | npm install -D @effect/doctest vitest@^5 vite@^8.1.5 |
| 11 | ``` |
| 12 | |
| 13 | Vitest 5 supports Node.js `^22.12.0 || ^24.0.0 || >=26.0.0`. Existing snippet markers and doctest configuration remain supported. When upgrading, import reporter types from `vitest/node`; JSON reports now default to a file, so configure `outputFile` explicitly when consuming the report. See the [Vitest migration guide](https://vitest.dev/guide/migration/) for upstream changes. |
| 14 | |
| 15 | ## Links |
| 16 | |
| 17 | - [Website](https://effect.website): documentation, guides, and news. |
| 18 | - [Reference](https://effect.website/docs/v4/api/doctest): API documentation for this package. |
| 19 | - [Discord](https://discord.gg/effect-ts): ask questions, share what you're building, and talk to the core team. |
| 20 | - [Community](https://effect.website/community-hub): meetups and events, or bring Effect to your own. |
| 21 | - [Issues](https://github.com/Effect-TS/effect/issues): bug reports and feature requests. |
| 22 | |
| 23 | ## Let's talk |
| 24 | |
| 25 | Whether your team is considering Effect, rolling it out, or already running it in production, we'd love to hear from you: what you're building, what works, and what you need from Effect next. |
| 26 | |
| 27 | - **Talk to the maintainers.** Introduce your team on [Discord](https://discord.gg/effect-ts) or email [contact@effectful.co](mailto:contact@effectful.co). We're happy to connect privately on Slack or Discord for feedback and help with adoption. |
| 28 | - **Production support.** We're exploring how to better support teams running Effect in production. If your organization has specific support needs, let's discuss them. |
| 29 | - **Adoption help.** Our [adoption partners](https://effect.website/adoption-partners) offer implementation, consulting, team extension, training, and commercial support. |
| 30 | |
| 31 | ## Usage |
| 32 | |
| 33 | Mark runnable fences with `import.meta.vitest`: |
| 34 | |
| 35 | ````ts |
| 36 | /** |
| 37 | * ```ts import.meta.vitest name="adds two numbers" |
| 38 | * 1 + 1 // => 2 |
| 39 | * ``` |
| 40 | */ |
| 41 | export const value = 1 |
| 42 | ```` |
| 43 | |
| 44 | The optional `name="..."` metadata labels the test without appearing in the example body. Unnamed examples use the opening fence line, such as `line 12`; Vitest displays the containing file alongside it. |
| 45 | |
| 46 | ## Inline assertions |
| 47 | |
| 48 | Add a trailing `// =>` comment to assert the value of an expression: |
| 49 | |
| 50 | ````ts |
| 51 | /** |
| 52 | * ```ts import.meta.vitest |
| 53 | * import { Array, Option } from "effect" |
| 54 | * |
| 55 | * Array.get([1, 2, 3], 1) // => Option.some(2) |
| 56 | * Array.get([1, 2, 3], 10) // => Option.none() |
| 57 | * ``` |
| 58 | */ |
| 59 | export const value = 1 |
| 60 | ```` |
| 61 | |
| 62 | The expected value is a TypeScript expression evaluated in the same lexical scope. Values are compared with Effect's `Equal.equals` semantics, so the convention supports primitives, arrays, plain objects, and Effect data types such as `Option`, `Result`, `Exit`, and `HashMap` without converting them to console output. |
| 63 | |
| 64 | Prefer asserting the API call directly instead of introducing a binding used only by the assertion. Keep bindings for reuse or meaningful multi-step setup, with a blank line before a later assertion block. Keep the call and assertion on one line when it fits within 120 characters, and format expected arrays densely, for example `[1, 2]`, `[[1], [2]]`, and `Option.some([1, 2])`. Preserve runnable markers on type-level examples without adding tautological runtime assertions. |
| 65 | |
| 66 | An assertion may also trail a single initialized `const` declaration with an identifier binding. The initializer is evaluated once and the binding remains available to subsequent code: |
| 67 | |
| 68 | ````ts |
| 69 | /** |
| 70 | * ```ts import.meta.vitest |
| 71 | * import { Effect, Option } from "effect" |
| 72 | * |
| 73 | * const result = await Effect.runPromise(Effect.succeed(Option.some(1))) // => Option.some(1) |
| 74 | * Option.isSome(result) // => true |
| 75 | * ``` |
| 76 | */ |
| 77 | export const value = 1 |
| 78 | ```` |
| 79 | |
| 80 | Markers must trail a complete expression statement or supported `const` declaration on the same line. Standalone markers, destructuring declarations, multiple declarations, and `let` or `var` declarations are not supported. The transform does not implicitly await promises, run Effects, or consume iterators; write those operations explicitly. Ordinary comments are ignored. Await asynchronous work so all assertions and cleanup occur before the snippet module finishes evaluating. |
| 81 | |
| 82 | Regular tests can use `include` in the same project. Documentation sources use `includeSource`, which lets Vitest discard files without the marker before collection. The plugin resolves imports relative to each example's original TypeScript, Markdown, or MDX file: |
| 83 | |
| 84 | ```ts |
| 85 | import * as Doctest from "@effect/doctest/Plugin" |
| 86 | import { defineConfig } from "vitest/config" |
| 87 | |
| 88 | export default defineConfig({ |
| 89 | plugins: [Doctest.plugin()], |
| 90 | test: { |
| 91 | include: ["test/**/*.test.ts"], |
| 92 | includeSource: ["src/**/*.ts", "docs/**/*.{md,mdx}"] |
| 93 | } |
| 94 | }) |
| 95 | ``` |
| 96 | |
| 97 | Source files selected by `includeSource` are collected through generated doctest collectors and are not executed. Native in-source tests using `import.meta.vitest` are therefore not supported by this plugin. Regular test files included through `test.include` continue to run normally. |
| 98 | |
| 99 | The plugin configures `@effect/doctest/Runner` when no test runner is specified. If `test.runner` is already configured, the plugin leaves it unchanged; that runner is then responsible for integrating doctest collection when required. |