Svelte
Add VibeLang to a Svelte 5 app that builds with Vite. A component runs a VibeLang program with a ManagedRuntime and renders three states: loading, the program's result, and an error the program recovers from. Install the packages with Add VibeLang to an existing project first. Follow this page instead of Vite and Vitest: it covers the same plugin, tsconfig.json, Vitest, and build steps.
1. Install the test packages
With Bun, run:
bun add -d happy-dom@20.14.6 @testing-library/svelte@5.4.2With npm, run:
npm install --save-dev --save-exact happy-dom@20.14.6 @testing-library/svelte@5.4.22. Configure tsconfig.json
Find the configuration that includes src. In an app from Vite's svelte-ts template, tsconfig.json has "files": [] and lists tsconfig.app.json and tsconfig.node.json as references; the one that includes src is tsconfig.app.json. Make two changes there:
- In
compilerOptions, set"rewriteRelativeImportExtensions": true. Without it,vite buildfails with TS5097 for each.tsfile that imports a.vibemodule. A configuration that setsallowImportingTsExtensionsfails with TS5096 instead. - Add
"src/**/*.vibe"toinclude. The template lists.ts,.js, and.svelteglobs only. Without the.vibeglob, the compiler skips.vibefiles that no other file imports, such as tests, and Vitest fails withENOENT.
{
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte", "src/**/*.vibe"]
}3. Configure Vite and Vitest
Replace vite.config.ts with this file. It adds vibelang() before svelte(), passes the configuration that includes src as project, and imports defineConfig from vitest/config, so the same file configures Vitest. svelteTesting() makes Vitest load Svelte's browser build and clean up after each test. Keep any other plugins the app has. If tsconfig.json itself includes src, call vibelang() without the project option.
// TypeScript (.ts)
import { svelte } from "@sveltejs/vite-plugin-svelte"
import { svelteTesting } from "@testing-library/svelte/vite"
import vibelang from "vibelang-vite"
import { defineConfig } from "vitest/config"
export default defineConfig({
plugins: [vibelang({ project: "tsconfig.app.json" }), svelte(), svelteTesting()],
test: {
environment: "happy-dom",
include: ["src/**/*.test.vibe", "src/**/*.test.ts"]
}
})4. Add a VibeLang module
Create src/cart.vibe. quote reserves a product and recovers from OutOfStock, and quoteCart runs two quotes concurrently with await.all:
// src/cart.vibe
// run: provide InventoryLive { Console.log(quoteCart().join("\n")); Console.log(quote("missing", 1)); try { Inventory.reserve("coffee", 3) } catch (error) { Console.log(error._tag, error.sku, error.available) } }
export schema Product { sku: string; name: string; cents: number; stock: number }
export error OutOfStock { sku: string; available: number }
export service Inventory {
reserve(sku: string, quantity: number): Product!OutOfStock
}
const catalog: ReadonlyArray<Product> = [
{ sku: "tea", name: "Green tea", cents: 400, stock: 5 },
{ sku: "coffee", name: "Coffee beans", cents: 1200, stock: 1 }
]
export const InventoryLive = Layer.succeed(Inventory, {
reserve(sku, quantity) {
const product = catalog.find((entry) => entry.sku === sku)
const available = product?.stock ?? 0
if (product === undefined || available < quantity) throw new OutOfStock({ sku, available })
return product
}
})
export function quote(sku: string, quantity: number) {
try {
const product = Inventory.reserve(sku, quantity)
const total = (product.cents * quantity / 100).toFixed(2)
return `${quantity} x ${product.name}: $${total}`
} catch (error) {
return `Only ${error.available} ${error.sku} left`
}
}
export function quoteCart() {
return await.all [quote("tea", 2), quote("coffee", 3)]
}2 x Green tea: $8.00
Only 1 coffee left
Only 0 missing left
OutOfStock coffee 15. Run a program from a component
Create src/runtime.ts. One ManagedRuntime builds InventoryLive once and runs every program in the app:
// TypeScript (.ts)
// src/runtime.ts
// run: return import("./cart.vibe").then(({ quoteCart }) => runtime.runPromise(quoteCart())).then((lines) => console.log(lines.join("\n"))).finally(() => runtime.dispose())
import { ManagedRuntime } from "effect"
import { InventoryLive } from "./cart.vibe"
export const runtime = ManagedRuntime.make(InventoryLive)2 x Green tea: $8.00
Only 1 coffee leftCreate src/CartQuote.svelte. Its <script lang="ts"> block imports ./cart.vibe by its source name. runtime.runPromise(quoteCart()) provides Inventory and returns a Promise, which the {#await} block renders:
<script lang="ts">
import { runtime } from "./runtime.ts"
import { quoteCart } from "./cart.vibe"
const quotes = runtime.runPromise(quoteCart())
</script>
<main>
<h1>Cart</h1>
{#await quotes}
<p>Loading…</p>
{:then lines}
{#each lines as line (line)}
<p>{line}</p>
{/each}
{/await}
</main>Render it from a component the app already has, such as src/App.svelte: add import CartQuote from "./CartQuote.svelte" to its script block and <CartQuote /> to its markup. The page shows Loading…, then 2 x Green tea: $8.00 and Only 1 coffee left.
6. Test the module and the component
Create src/cart.test.vibe:
// src/cart.test.vibe
import { assert, it } from "@effect/vitest"
import { InventoryLive, Inventory, OutOfStock, quote, quoteCart } from "./cart.vibe"
it.effect("quotes a product in stock") {
const line = Effect.provide(() => quote("tea", 2), InventoryLive)
assert.strictEqual(line, "2 x Green tea: $8.00")
}
it.effect("fails a reservation with OutOfStock") {
provide InventoryLive {
try {
Inventory.reserve("coffee", 3)
assert.fail("expected OutOfStock")
} catch (error) {
assert.instanceOf(error, OutOfStock)
assert.strictEqual(error.available, 1)
}
}
}
it.effect("recovers OutOfStock in quote") {
const line = Effect.provide(() => quote("coffee", 3), InventoryLive)
assert.strictEqual(line, "Only 1 coffee left")
}
it.effect("quotes the cart concurrently") {
const lines = Effect.provide(() => quoteCart(), InventoryLive)
assert.deepStrictEqual(lines, ["2 x Green tea: $8.00", "Only 1 coffee left"])
}Create src/CartQuote.test.ts:
// TypeScript (.ts)
import { render, screen } from "@testing-library/svelte"
import { expect, it } from "vitest"
import CartQuote from "./CartQuote.svelte"
it("shows loading, then the quote and the recovered error", async () => {
render(CartQuote)
expect(screen.getByText("Loading…")).toBeDefined()
expect(await screen.findByText("2 x Green tea: $8.00")).toBeDefined()
expect(await screen.findByText("Only 1 coffee left")).toBeDefined()
})If package.json has no test script, add "test": "vitest run". Run npx vitest run. The output includes:
Test Files 2 passed (2)
Tests 5 passed (5)7. Type-check and build
svelte-check does not understand .vibe files, so the template's check script fails. svelte-check 4.7.7 reports TS2307 for each import of ./cart.vibe, and then TS2345 for the {#each} block, whose lines it types as unknown. With --output human, it prints each file's absolute path; here they are shortened to src/:
src/runtime.ts:4:31
Error: Cannot find module './cart.vibe' or its corresponding type declarations.
src/CartQuote.svelte:3:29
Error: Cannot find module './cart.vibe' or its corresponding type declarations. (ts)Use vibelang -b for the mixed .vibe and .ts program instead. It builds the same project references and checks .vibe and .ts files as one program; it does not check the script blocks of .svelte files. Keep those blocks thin: import a program, run it, and render its result, as CartQuote.svelte does. The .vibe tests and the component test cover the rest. In package.json, change the check script:
{
"scripts": {
"check": "vibelang -b"
}
}Run npm run check, or bun run check. It exits with status 0. Then run npm run build, or bun run build. It exits with status 0, and its last line starts with ✓ built in. Tell the user which files you added and changed, and that svelte-check no longer runs in the check script.
Next
Set up the editor: VS Code and Cursor, Neovim, Zed, or other editors. Then verify the setup with step 4 of Set up VibeLang.