diff --git a/.gitignore b/.gitignore index 98f8fd5..700e015 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist/ .turbo/ *.tsbuildinfo .next/ +.svelte-kit/ next-env.d.ts packages/@wterm/core/src/wasm-inline.ts coverage/ diff --git a/README.md b/README.md index 85e51e0..e464de6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ wterm ("dub-term") renders to the DOM — native text selection, copy/paste, fin | [`@wterm/core`](packages/@wterm/core) | Headless WASM bridge, `TerminalCore` interface, WebSocket transport | | [`@wterm/dom`](packages/@wterm/dom) | DOM renderer, input handler — vanilla JS terminal | | [`@wterm/react`](packages/@wterm/react) | React component + `useTerminal` hook (TypeScript) | +| [`@wterm/svelte`](packages/@wterm/svelte) | Svelte 5 component + `bind:this` API | | [`@wterm/vue`](packages/@wterm/vue) | Vue 3 component + template ref API | | [`@wterm/ghostty`](packages/@wterm/ghostty) | Full-featured VT emulation core powered by libghostty | | [`@wterm/just-bash`](packages/@wterm/just-bash) | In-browser Bash shell powered by just-bash | @@ -78,6 +79,12 @@ cp web/wterm.wasm examples/nextjs/public/ pnpm --filter nextjs dev ``` +### Run the Svelte example + +```bash +pnpm --filter svelte-example dev +``` + ### Run Zig tests ```bash diff --git a/apps/docs/src/app/api-reference/page.mdx b/apps/docs/src/app/api-reference/page.mdx index 9c03113..c3c2bae 100644 --- a/apps/docs/src/app/api-reference/page.mdx +++ b/apps/docs/src/app/api-reference/page.mdx @@ -4,7 +4,7 @@ Complete reference for all wterm options, methods, types, and transport APIs. ## Terminal Options -The React and Vue `` components and the vanilla `WTerm` constructor all accept these options: +The React, Svelte, and Vue `` components and the vanilla `WTerm` constructor all accept these options: @@ -17,62 +17,132 @@ The React and Vue `` components and the vanilla `WTerm` constructor al - - - + + + - - - + + + - - + + - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - + + + + + - - + + @@ -93,26 +163,74 @@ The React `` component adds these props on top of the shared options a - - - + + + + + + + + + + + + + + +
colsnumber80 + cols + + number + + 80 + Initial column count
rowsnumber24 + rows + + number + + 24 + Initial row count
coreTerminalCore + core + + TerminalCore + A pre-constructed terminal core instance. When provided, wasmUrl is ignored and this core is used instead of loading the built-in Zig WASM binary. See Ghostty Core for an example.
wasmUrlstring + A pre-constructed terminal core instance. When provided,{" "} + wasmUrl is ignored and this core is used instead of loading + the built-in Zig WASM binary. See{" "} + Ghostty Core for an example. +
+ wasmUrl + + string + URL to serve the WASM binary separately. When omitted, the ~12 KB binary is decoded from an inlined base64 string. Ignored when core is provided.
autoResizebooleantrue (vanilla) / false (React, Vue)Automatically resize the terminal to fit its container using a ResizeObserver
cursorBlinkbooleanfalse + URL to serve the WASM binary separately. When omitted, the ~12 KB binary + is decoded from an inlined base64 string. Ignored when{" "} + core is provided. +
+ autoResize + + boolean + + true (vanilla) / false (React, Svelte, Vue) + + Automatically resize the terminal to fit its container using a{" "} + ResizeObserver +
+ cursorBlink + + boolean + + false + Enable cursor blinking animation
debugbooleanfalseEnable debug mode. Exposes a DebugAdapter on the WTerm instance (wt.debug) for inspecting escape sequences, cell data, render performance, and unhandled CSI sequences.
onData(data: string) => void + debug + + boolean + + false + + Enable debug mode. Exposes a DebugAdapter on the{" "} + WTerm instance (wt.debug) for inspecting + escape sequences, cell data, render performance, and unhandled CSI + sequences. +
+ onData + + (data: string) => void + Called when the terminal produces data (user input or host response). When omitted, input is echoed back automatically.
onTitle(title: string) => void + Called when the terminal produces data (user input or host response). + When omitted, input is echoed back automatically. +
+ onTitle + + (title: string) => void + Called when the terminal title changes via an escape sequence
onResize(cols: number, rows: number) => void + onResize + + (cols: number, rows: number) => void + Called after the terminal is resized
themestringName of a built-in or custom theme (see Themes) + theme + + string + + Name of a built-in or custom theme (see Themes) +
+ onReady + + (wt: WTerm) => void + + Called with the underlying WTerm instance after WASM loads + and initialization completes +
+ onError + + (error: unknown) => void + + Called if WASM loading or initialization fails. When omitted, errors are + logged to the console. +
+ +## Vue-Only Props + +The Vue `` component adds these props on top of the shared options above. Vue exposes input callbacks as events (see [Vue Events](#vue-events) below) instead of `onXxx` props. Standard HTML attributes (`class`, `style`, `id`, etc.) are forwarded to the root `div` via the default `inheritAttrs` behavior. + + + - - - + + + + + - - - + + +
onReady(wt: WTerm) => voidCalled with the underlying WTerm instance after WASM loads and initialization completesPropTypeDescription
onError(error: unknown) => voidCalled if WASM loading or initialization fails. When omitted, errors are logged to the console. + theme + + string + + Name of a built-in or custom theme (see Themes). + Applied as a theme-<name> class on the root element. +
-## Vue-Only Props +## Svelte-Only Props -The Vue `` component adds these props on top of the shared options above. Vue exposes input callbacks as events (see [Vue Events](#vue-events) below) instead of `onXxx` props. Standard HTML attributes (`class`, `style`, `id`, etc.) are forwarded to the root `div` via the default `inheritAttrs` behavior. +The Svelte `` component adds these props on top of the shared options above. Svelte exposes callbacks as component props (see [Svelte Callback Props](#svelte-callback-props) below). Standard HTML attributes (`class`, `style`, `id`, etc.) are forwarded to the root `div`. @@ -124,47 +242,148 @@ The Vue `` component adds these props on top of the shared options abo - - - + + +
themestringName of a built-in or custom theme (see Themes). Applied as a theme-<name> class on the root element. + theme + + string + + Name of a built-in or custom theme (see Themes). + Applied as a theme-<name> class on the root element. +
-## Vue Events +## Svelte Callback Props - + - - - + + + + + + + + + + + + + + + + + + + + + + + + +
EventProp Payload Description
data(data: string)Emitted when the terminal produces data (user input or host response). When no listener is attached, input is echoed back automatically. + onData + + (data: string) + + Called when the terminal produces data (user input or host response). + When no callback is attached, input is echoed back automatically. +
+ onTitle + + (title: string) + Called when the terminal title changes via an escape sequence.
+ onResize + + (cols: number, rows: number) + Called after the terminal is resized.
+ onReady + + (wt: WTerm) + + Called once after WTerm.init() resolves, carrying the + underlying WTerm instance. +
+ onError + + (err: unknown) + + Called if WASM loading or initialization fails. When omitted, errors are + logged to the console. +
+ +## Vue Events + + + - - - + + + + + - - - + + + + + + + + - - - + + + - - + + + + + + + @@ -183,23 +402,33 @@ Instance methods on the vanilla `WTerm` class: - + - + - + - + - + @@ -223,23 +452,41 @@ const { ref, write, resize, focus } = useTerminal(); - - - - - - - + + + + + + + - - + + - - + + @@ -256,6 +503,75 @@ interface TerminalHandle { } ``` +## Component Ref (Svelte) + +The Svelte `` component exposes imperative methods through `bind:this`: + +```svelte + + + +``` + +
title(title: string)Emitted when the terminal title changes via an escape sequence.EventPayloadDescription
resize(cols: number, rows: number)Emitted after the terminal is resized. + data + + (data: string) + + Emitted when the terminal produces data (user input or host response). + When no listener is attached, input is echoed back automatically. +
+ title + + (title: string) + Emitted when the terminal title changes via an escape sequence.
ready(wt: WTerm)Emitted once after WTerm.init() resolves, carrying the underlying WTerm instance. + resize + + (cols: number, rows: number) + Emitted after the terminal is resized.
error(err: unknown) + ready + + (wt: WTerm) + + Emitted once after WTerm.init() resolves, carrying the + underlying WTerm instance. +
+ error + + (err: unknown) + Emitted if WASM loading or initialization fails.
init(): Promise<WTerm> + init(): Promise<WTerm> + Load WASM and start rendering
write(data: string | Uint8Array) + write(data: string | Uint8Array) + Write data to the terminal
resize(cols, rows) + resize(cols, rows) + Resize the terminal grid
focus() + focus() + Focus the terminal input
destroy() + destroy() + Clean up event listeners, observers, and DOM
refRefObject<TerminalHandle>Pass to <Terminal ref={ref}>
write(data: string | Uint8Array) => void + ref + + RefObject<TerminalHandle> + + Pass to <Terminal ref={ref}> +
+ write + + (data: string | Uint8Array) => void + Write data to the terminal
resize(cols: number, rows: number) => void + resize + + (cols: number, rows: number) => void + Resize the terminal grid
focus() => void + focus + + () => void + Focus the terminal input
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MemberTypeDescription
+ write + + (data: string | Uint8Array) => void + + Write data to the terminal. Safe to call after the onReady{" "} + callback; calls before mount are ignored. +
+ resize + + (cols: number, rows: number) => void + Resize the terminal grid. Calls before mount are ignored.
+ focus + + () => void + Focus the terminal input.
+ instance + + () => WTerm | null + + Returns the underlying WTerm instance, or null{" "} + before mount. The WASM bridge is only available after the{" "} + onReady callback. +
+ ## Template Ref (Vue) The Vue `` component exposes the same methods directly on its instance — no separate composable. Access them via `useTemplateRef`: @@ -283,24 +599,47 @@ const term = useTemplateRef("term"); - write - (data: string | Uint8Array) => void - Write data to the terminal. Safe to call after the ready event; calls before mount are ignored. - - - resize - (cols: number, rows: number) => void + + write + + + (data: string | Uint8Array) => void + + + Write data to the terminal. Safe to call after the ready{" "} + event; calls before mount are ignored. + + + + + resize + + + (cols: number, rows: number) => void + Resize the terminal grid. Calls before mount are ignored. - focus - () => void + + focus + + + () => void + Focus the terminal input. - instance - WTerm | null - Underlying WTerm instance. null until the component has mounted; the WASM bridge is only available after the ready event. + + instance + + + WTerm | null + + + Underlying WTerm instance. null until the + component has mounted; the WASM bridge is only available after the{" "} + ready event. + @@ -322,44 +661,76 @@ Connect to a PTY backend over WebSocket with automatic reconnection and send buf - url - string + + url + + + string + — WebSocket server URL - reconnect - boolean - true + + reconnect + + + boolean + + + true + Automatically reconnect on disconnect with exponential backoff - maxReconnectDelay - number - 30000 + + maxReconnectDelay + + + number + + + 30000 + Maximum delay between reconnection attempts (ms) - onData - (data: Uint8Array | string) => void + + onData + + + (data: Uint8Array | string) => void + — Called when data is received from the server - onOpen - () => void + + onOpen + + + () => void + — Called when the connection opens - onClose - () => void + + onClose + + + () => void + — Called when the connection closes - onError - (event: Event) => void + + onError + + + (event: Event) => void + — Called when a WebSocket error occurs @@ -377,15 +748,24 @@ Connect to a PTY backend over WebSocket with automatic reconnection and send buf - connect(url?) + + connect(url?) + Open the WebSocket connection. Optionally override the URL. - send(data: string | Uint8Array) - Send data to the server. If the socket is not yet open, data is buffered and flushed on connect. + + send(data: string | Uint8Array) + + + Send data to the server. If the socket is not yet open, data is buffered + and flushed on connect. + - close() + + close() + Close the connection and stop reconnection attempts. @@ -403,8 +783,12 @@ Connect to a PTY backend over WebSocket with automatic reconnection and send buf - connected - boolean + + connected + + + boolean + Whether the WebSocket is currently open @@ -436,75 +820,121 @@ When no URL is provided, the ~12 KB WASM binary is decoded from a base64 string - WasmBridge.load(url?): Promise<WasmBridge> + + WasmBridge.load(url?): Promise<WasmBridge> + Load the WASM binary and return a new bridge instance - init(cols, rows) + + init(cols, rows) + Initialize the terminal grid - writeString(str) + + writeString(str) + Write a UTF-8 string (including escape sequences) to the terminal - writeRaw(data: Uint8Array) - Write raw bytes to the terminal (chunked to 8192 bytes internally) + + writeRaw(data: Uint8Array) + + + Write raw bytes to the terminal (chunked to 8192 bytes internally) + - resize(cols, rows) + + resize(cols, rows) + Resize the terminal grid - getCell(row, col): CellData + + getCell(row, col): CellData + Get cell data at a grid position - getCursor(): CursorState + + getCursor(): CursorState + Get current cursor position and visibility - getCols() / getRows() + + getCols() / getRows() + Get current grid dimensions - isDirtyRow(row): boolean - Check if a row has changed since last clearDirty() + + isDirtyRow(row): boolean + + + Check if a row has changed since last clearDirty() + - clearDirty() + + clearDirty() + Reset all dirty-row flags - getTitle(): string | null - Get pending title change (via OSC escape), or null if unchanged + + getTitle(): string | null + + + Get pending title change (via OSC escape), or null if + unchanged + - getResponse(): string | null - Get pending host response (e.g. DSR), or null. Reading clears the buffer. + + getResponse(): string | null + + + Get pending host response (e.g. DSR), or null. Reading + clears the buffer. + - getScrollbackCount(): number + + getScrollbackCount(): number + Number of lines in the scrollback buffer - getScrollbackCell(offset, col): CellData + + getScrollbackCell(offset, col): CellData + Get cell data from a scrollback line - getScrollbackLineLen(offset): number + + getScrollbackLineLen(offset): number + Get the length of a scrollback line - cursorKeysApp(): boolean + + cursorKeysApp(): boolean + Whether cursor keys are in application mode - bracketedPaste(): boolean + + bracketedPaste(): boolean + Whether bracketed paste mode is active - usingAltScreen(): boolean + + usingAltScreen(): boolean + Whether the alternate screen buffer is active @@ -514,10 +944,10 @@ When no URL is provided, the ~12 KB WASM binary is decoded from a base64 string ```ts interface CellData { - char: number; // Unicode code point - fg: number; // Foreground color index (256 = default) - bg: number; // Background color index (256 = default) - flags: number; // Style flags (bold, italic, underline, etc.) + char: number; // Unicode code point + fg: number; // Foreground color index (256 = default) + bg: number; // Background color index (256 = default) + flags: number; // Style flags (bold, italic, underline, etc.) } interface CursorState { diff --git a/apps/docs/src/app/api/docs-chat/route.ts b/apps/docs/src/app/api/docs-chat/route.ts index be85ff6..c8e521c 100644 --- a/apps/docs/src/app/api/docs-chat/route.ts +++ b/apps/docs/src/app/api/docs-chat/route.ts @@ -16,7 +16,7 @@ const SYSTEM_PROMPT = `You are a helpful documentation assistant for wterm ("dub GitHub repository: https://github.com/vercel-labs/wterm Documentation: https://wterm.dev -npm packages: @wterm/core, @wterm/dom, @wterm/react, @wterm/vue, @wterm/markdown, @wterm/just-bash +npm packages: @wterm/core, @wterm/dom, @wterm/react, @wterm/svelte, @wterm/vue, @wterm/markdown, @wterm/just-bash You have access to the full wterm documentation via the bash and readFile tools. The docs are available as markdown files in the /workspace/ directory. diff --git a/apps/docs/src/app/configuration/page.mdx b/apps/docs/src/app/configuration/page.mdx index ded05f5..452239d 100644 --- a/apps/docs/src/app/configuration/page.mdx +++ b/apps/docs/src/app/configuration/page.mdx @@ -2,7 +2,7 @@ ## Options -The React and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as `@data`, `@title`, `@resize` events in Vue). +The React, Svelte, and Vue `Terminal` components and the vanilla `WTerm` constructor accept the same core options — `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug` — plus event callbacks `onData`, `onTitle`, and `onResize` (exposed as callback props in Svelte and as `@data`, `@title`, `@resize` events in Vue). See the full [API Reference](/api-reference#terminal-options) for types, defaults, and descriptions. @@ -16,6 +16,12 @@ The React `Terminal` component adds `theme`, `onReady`, and `onError` on top of See [React-Only Props](/api-reference#react-only-props) for details. +### Svelte-only + +The Svelte `Terminal` component adds a `theme` prop. `onReady` and `onError` are callback props. Standard DOM attributes (`class`, `style`, `id`, ARIA props, etc.) are forwarded to the root `
`. + +See [Svelte-Only Props](/api-reference#svelte-only-props) and [Svelte Callback Props](/api-reference#svelte-callback-props) for details. + ### Vue-only The Vue `Terminal` component adds a `theme` prop. `onReady` and `onError` are exposed as `@ready` and `@error` events instead of props. Standard DOM attributes (`class`, `style`, `id`, ARIA props, etc.) are forwarded to the root `
` via the default `inheritAttrs` behavior. @@ -45,6 +51,26 @@ function App() { See the full [Imperative Handle](/api-reference#imperative-handle-react) reference for all returned methods. +## Component Ref (Svelte) + +Access imperative methods on the Svelte component via `bind:this`: + +```svelte + + + +``` + +See the full [Component Ref (Svelte)](/api-reference#component-ref-svelte) reference for all exposed methods. + ## Template Ref (Vue) Access imperative methods on the Vue component via a template ref: diff --git a/apps/docs/src/app/get-started/page.mdx b/apps/docs/src/app/get-started/page.mdx index ebef1a1..29682c9 100644 --- a/apps/docs/src/app/get-started/page.mdx +++ b/apps/docs/src/app/get-started/page.mdx @@ -14,6 +14,12 @@ npm install @wterm/dom @wterm/react npm install @wterm/dom @wterm/vue ``` +### Svelte + +```bash +npm install @wterm/dom @wterm/svelte +``` + ### Vanilla JS ```bash @@ -48,6 +54,17 @@ import "@wterm/vue/css"; ``` +### Svelte + +```svelte + + + +``` + ### Vanilla JS ```js diff --git a/apps/docs/src/app/ghostty/page.mdx b/apps/docs/src/app/ghostty/page.mdx index a84823d..49ed31a 100644 --- a/apps/docs/src/app/ghostty/page.mdx +++ b/apps/docs/src/app/ghostty/page.mdx @@ -57,6 +57,20 @@ const core = await GhosttyCore.load(); ``` +### Svelte + +```svelte + + + +``` + ## Options `GhosttyCore.load()` accepts an optional options object: diff --git a/apps/docs/src/app/just-bash/page.mdx b/apps/docs/src/app/just-bash/page.mdx index 03fe3bd..ba06caa 100644 --- a/apps/docs/src/app/just-bash/page.mdx +++ b/apps/docs/src/app/just-bash/page.mdx @@ -48,6 +48,34 @@ function App() { Use a ref to hold the `BashShell` instance so it's accessible from both the `onReady` and `onData` callbacks. +### Svelte + +```svelte + + + +``` + ## Options diff --git a/apps/docs/src/app/markdown/page.mdx b/apps/docs/src/app/markdown/page.mdx index 7a1b225..63f0612 100644 --- a/apps/docs/src/app/markdown/page.mdx +++ b/apps/docs/src/app/markdown/page.mdx @@ -66,6 +66,36 @@ function App() { } ``` +### Svelte + +```svelte + + + +``` + ## Options
diff --git a/apps/docs/src/app/svelte/layout.tsx b/apps/docs/src/app/svelte/layout.tsx new file mode 100644 index 0000000..a6f88d5 --- /dev/null +++ b/apps/docs/src/app/svelte/layout.tsx @@ -0,0 +1,7 @@ +import { pageMetadata } from "@/lib/page-metadata"; + +export const metadata = pageMetadata("svelte"); + +export default function Layout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/apps/docs/src/app/svelte/page.mdx b/apps/docs/src/app/svelte/page.mdx new file mode 100644 index 0000000..5e1b0ee --- /dev/null +++ b/apps/docs/src/app/svelte/page.mdx @@ -0,0 +1,99 @@ +# Svelte + +The `@wterm/svelte` package provides a Svelte 5 `` component for integrating wterm into Svelte applications. It re-exports everything from `@wterm/dom`, so a single import covers both the component and its types. + +## Install + +```bash +npm install @wterm/dom @wterm/svelte +``` + +## Basic Usage + +```svelte + + + +``` + +## Custom Input Handling + +By default, typed input is echoed back to the terminal. Pass `onData` when you need control over input — for example, sending it to a server: + +```svelte + + + +``` + +## Props + +The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, `debug`) plus [Svelte-only props](/api-reference#svelte-only-props) (`theme`). + +Standard DOM attributes (`class`, `style`, `id`, ARIA props, etc.) are forwarded to the root `
`. + +## Callback Props + +Svelte 5 uses callback props for component events: + +```svelte + +``` + +See the full [Svelte Callback Props](/api-reference#svelte-callback-props) reference for payload types. + +## Component Ref + +Access imperative methods via `bind:this`: + +```svelte + + + +``` + +The exposed methods are `write`, `resize`, `focus`, and `instance()` (the underlying `WTerm`, or `null` before mount). See the full [Component Ref (Svelte)](/api-reference#component-ref-svelte) reference. + +## Themes + +Import the stylesheet and switch themes via the `theme` prop: + +```svelte + + + +``` + +Built-in themes: `solarized-dark`, `monokai`, `light`. Define custom themes with CSS custom properties (`--term-fg`, `--term-bg`, `--term-color-0` through `--term-color-15`). See [Themes](/themes) for details. + +## SvelteKit + +The component creates `WTerm` in `onMount`, so it is SSR-safe out of the box. diff --git a/apps/docs/src/app/themes/page.mdx b/apps/docs/src/app/themes/page.mdx index 40693f5..2dc1887 100644 --- a/apps/docs/src/app/themes/page.mdx +++ b/apps/docs/src/app/themes/page.mdx @@ -20,6 +20,14 @@ Same `theme` prop: ``` +### Svelte + +Same `theme` prop: + +```svelte + +``` + ### Vanilla JS Add the theme class to the terminal element: @@ -44,9 +52,24 @@ The default dark theme, inspired by VS Code's Dark+ palette.
- - - + + + + + + + + + + + +
Background#1e1e1e
Foreground#d4d4d4
Cursor#aeafad
Background + #1e1e1e +
Foreground + #d4d4d4 +
Cursor + #aeafad +
@@ -66,9 +89,24 @@ The classic Solarized Dark color scheme by Ethan Schoonover. - Background#002b36 - Foreground#839496 - Cursor#93a1a1 + + Background + + #002b36 + + + + Foreground + + #839496 + + + + Cursor + + #93a1a1 + + @@ -88,9 +126,24 @@ Based on the Monokai color scheme. - Background#272822 - Foreground#f8f8f2 - Cursor#f8f8f0 + + Background + + #272822 + + + + Foreground + + #f8f8f2 + + + + Cursor + + #f8f8f0 + + @@ -110,9 +163,24 @@ A clean light theme inspired by Atom's One Light. - Background#fafafa - Foreground#383a42 - Cursor#526eff + + Background + + #fafafa + + + + Foreground + + #383a42 + + + + Cursor + + #526eff + + @@ -161,14 +229,64 @@ Then apply it the same way as a built-in theme: - --term-bgTerminal background color - --term-fgDefault text color - --term-cursorCursor color - --term-font-familyFont stack (default: Menlo, Consolas, DejaVu Sans Mono, Courier New, monospace) - --term-font-sizeFont size (default: 14px) - --term-line-heightLine height multiplier (default: 1.2) - --term-color-0--term-color-7Standard ANSI colors (black, red, green, yellow, blue, magenta, cyan, white) - --term-color-8--term-color-15Bright ANSI colors + + + --term-bg + + Terminal background color + + + + --term-fg + + Default text color + + + + --term-cursor + + Cursor color + + + + --term-font-family + + + Font stack (default: Menlo, Consolas, DejaVu Sans Mono, Courier New, + monospace) + + + + + --term-font-size + + + Font size (default: 14px) + + + + + --term-line-height + + + Line height multiplier (default: 1.2) + + + + + --term-color-0--term-color-7 + + + Standard ANSI colors (black, red, green, yellow, blue, magenta, cyan, + white) + + + + + --term-color-8--term-color-15 + + Bright ANSI colors + diff --git a/apps/docs/src/lib/docs-navigation.ts b/apps/docs/src/lib/docs-navigation.ts index 39ea8f7..da333d6 100644 --- a/apps/docs/src/lib/docs-navigation.ts +++ b/apps/docs/src/lib/docs-navigation.ts @@ -26,6 +26,7 @@ export const navGroups: NavGroup[] = [ label: "Frameworks", items: [ { name: "React", href: "/react" }, + { name: "Svelte", href: "/svelte" }, { name: "Vue", href: "/vue" }, { name: "Vanilla JS", href: "/vanilla" }, ], @@ -62,6 +63,11 @@ export const navGroups: NavGroup[] = [ href: `${GITHUB}/tree/main/examples/vite`, external: true, }, + { + name: "Svelte", + href: `${GITHUB}/tree/main/examples/svelte`, + external: true, + }, { name: "Markdown Streaming", href: `${GITHUB}/tree/main/examples/markdown-streaming`, @@ -92,6 +98,11 @@ export const navGroups: NavGroup[] = [ href: `${GITHUB}/tree/main/packages/@wterm/react`, external: true, }, + { + name: "@wterm/svelte", + href: `${GITHUB}/tree/main/packages/@wterm/svelte`, + external: true, + }, { name: "@wterm/vue", href: `${GITHUB}/tree/main/packages/@wterm/vue`, diff --git a/apps/docs/src/lib/page-titles.ts b/apps/docs/src/lib/page-titles.ts index f0ed772..5c57e31 100644 --- a/apps/docs/src/lib/page-titles.ts +++ b/apps/docs/src/lib/page-titles.ts @@ -5,6 +5,7 @@ export const PAGE_TITLES: Record = { configuration: "Configuration", themes: "Themes", react: "React", + svelte: "Svelte", vue: "Vue", vanilla: "Vanilla JS", ghostty: "Ghostty Core", diff --git a/examples/svelte/.gitignore b/examples/svelte/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/examples/svelte/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/examples/svelte/README.md b/examples/svelte/README.md new file mode 100644 index 0000000..4c220ab --- /dev/null +++ b/examples/svelte/README.md @@ -0,0 +1,32 @@ +# Svelte Example + +In-browser terminal running [just-bash](https://github.com/vercel-labs/just-bash) — no backend required. Includes theme switching, a virtual filesystem, and a local terminal over WebSocket. Svelte 5 + Vite port of the Vue example. + +## Setup + +From the monorepo root: + +```bash +pnpm install +zig build +pnpm --filter svelte-example dev +``` + +Opens at `svelte-example.wterm.localhost` via [portless](https://github.com/vercel-labs/portless). + +## How It Works + +- `@wterm/svelte` renders the terminal with `` and `bind:this` +- `@wterm/just-bash` provides a Bash shell that runs entirely in the browser +- Theme selector switches between Default, Solarized Dark, Monokai, and Light +- Virtual files (`README.md`, `package.json`, `main.zig`, `hello.sh`) are preloaded into the shell +- The local shell uses `node-pty` over a Vite WebSocket middleware + +## Key Files + +| File | Description | +| ---------------------------- | -------------------------------------------- | +| `src/App.svelte` | Terminal page with theme picker + shell glue | +| `src/main.ts` | App entry, imports `@wterm/svelte/css` | +| `vite-plugins/pty-server.ts` | Local terminal WebSocket server | +| `index.html` | HTML shell; `dark` class on `` | diff --git a/examples/svelte/index.html b/examples/svelte/index.html new file mode 100644 index 0000000..a0d52a4 --- /dev/null +++ b/examples/svelte/index.html @@ -0,0 +1,12 @@ + + + + + + wterm — Svelte Example + + +
+ + + diff --git a/examples/svelte/package.json b/examples/svelte/package.json new file mode 100644 index 0000000..7f6963e --- /dev/null +++ b/examples/svelte/package.json @@ -0,0 +1,35 @@ +{ + "name": "svelte-example", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "predev": "mkdir -p public && cp ../../packages/@wterm/core/wasm/wterm.wasm public/wterm.wasm && chmod +x node_modules/node-pty/prebuilds/darwin-*/spawn-helper 2>/dev/null; (command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1))", + "dev": "portless svelte-example.wterm vite", + "prebuild": "mkdir -p public && cp ../../packages/@wterm/core/wasm/wterm.wasm public/wterm.wasm", + "build": "vite build", + "preview": "vite preview", + "type-check": "svelte-check --tsconfig ./tsconfig.json" + }, + "dependencies": { + "@wterm/core": "workspace:*", + "@wterm/dom": "workspace:*", + "@wterm/just-bash": "workspace:*", + "@wterm/svelte": "workspace:*", + "just-bash": "^2.14.2", + "node-pty": "^1.0.0", + "svelte": "5.55.5", + "ws": "^8.18.2" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "7.0.0", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^24.12.2", + "@types/ws": "^8.18.1", + "svelte-check": "4.4.6", + "tailwindcss": "^4.2.2", + "tw-animate-css": "^1.4.0", + "typescript": "~6.0.2", + "vite": "^8.0.4" + } +} diff --git a/examples/svelte/src/App.svelte b/examples/svelte/src/App.svelte new file mode 100644 index 0000000..0cb0742 --- /dev/null +++ b/examples/svelte/src/App.svelte @@ -0,0 +1,143 @@ + + +
+
+

+ {title} +

+
+ + +
+
+
+
+

+ In-browser bash (just-bash) +

+ +
+
+

+ Local shell (node-pty over WebSocket) +

+ +
+
+
diff --git a/examples/svelte/src/main.ts b/examples/svelte/src/main.ts new file mode 100644 index 0000000..0a9c705 --- /dev/null +++ b/examples/svelte/src/main.ts @@ -0,0 +1,8 @@ +import { mount } from "svelte"; +import "./style.css"; +import "@wterm/svelte/css"; +import App from "./App.svelte"; + +mount(App, { + target: document.getElementById("app")!, +}); diff --git a/examples/svelte/src/style.css b/examples/svelte/src/style.css new file mode 100644 index 0000000..a15cbf4 --- /dev/null +++ b/examples/svelte/src/style.css @@ -0,0 +1,88 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --font-heading: var(--font-sans); + --font-sans: var(--font-sans); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --color-foreground: var(--foreground); + --color-background: var(--background); + --radius-sm: calc(var(--radius) * 0.6); + --radius-md: calc(var(--radius) * 0.8); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) * 1.4); +} + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --radius: 0.625rem; +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + + body { + @apply bg-background text-foreground; + } + + html { + @apply font-sans; + } +} diff --git a/examples/svelte/svelte.config.js b/examples/svelte/svelte.config.js new file mode 100644 index 0000000..d6f6262 --- /dev/null +++ b/examples/svelte/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +const config = { + preprocess: vitePreprocess({ script: true }), +}; + +export default config; diff --git a/examples/svelte/tsconfig.json b/examples/svelte/tsconfig.json new file mode 100644 index 0000000..1a70628 --- /dev/null +++ b/examples/svelte/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "types": ["svelte", "node"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.svelte", + "*.d.ts", + "svelte.config.js", + "vite.config.ts", + "vite-plugins/**/*.ts" + ] +} diff --git a/examples/svelte/vite-plugins/pty-server.ts b/examples/svelte/vite-plugins/pty-server.ts new file mode 100644 index 0000000..6944020 --- /dev/null +++ b/examples/svelte/vite-plugins/pty-server.ts @@ -0,0 +1,79 @@ +import type { Plugin } from "vite"; +import { WebSocketServer, type WebSocket } from "ws"; +import * as pty from "node-pty"; +import { parse as parseUrl } from "url"; + +function cleanEnv(): Record { + const env: Record = {}; + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) env[key] = value; + } + return env; +} + +function handlePTYConnection(ws: WebSocket) { + const shell = process.env.SHELL || "/bin/zsh"; + + let ptyProcess: pty.IPty; + try { + ptyProcess = pty.spawn(shell, ["-l"], { + name: "xterm-256color", + cols: 80, + rows: 24, + cwd: process.env.HOME || "/", + env: cleanEnv(), + }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + console.error(`Failed to spawn PTY: ${msg}`); + if (ws.readyState === ws.OPEN) { + ws.send(`\r\n\x1b[31mFailed to spawn shell: ${msg}\x1b[0m\r\n`); + ws.close(); + } + return; + } + + ptyProcess.onData((data) => { + if (ws.readyState === ws.OPEN) ws.send(data); + }); + + ptyProcess.onExit(() => { + if (ws.readyState === ws.OPEN) ws.close(); + }); + + ws.on("message", (msg: Buffer | string) => { + const input = typeof msg === "string" ? msg : msg.toString("utf-8"); + + if (input.startsWith("\x1b[RESIZE:")) { + const match = input.match(/\x1b\[RESIZE:(\d+);(\d+)\]/); + if (match) { + ptyProcess.resize(parseInt(match[1], 10), parseInt(match[2], 10)); + return; + } + } + + ptyProcess.write(input); + }); + + ws.on("close", () => { + ptyProcess.kill(); + }); +} + +export function ptyServer(): Plugin { + return { + name: "pty-server", + configureServer(server) { + const wss = new WebSocketServer({ noServer: true }); + + server.httpServer?.on("upgrade", (req, socket, head) => { + const { pathname } = parseUrl(req.url || "/", true); + if (pathname !== "/api/terminal") return; + + wss.handleUpgrade(req, socket, head, (ws) => { + handlePTYConnection(ws); + }); + }); + }, + }; +} diff --git a/examples/svelte/vite.config.ts b/examples/svelte/vite.config.ts new file mode 100644 index 0000000..6b7d8a9 --- /dev/null +++ b/examples/svelte/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; +import tailwindcss from "@tailwindcss/vite"; +import { ptyServer } from "./vite-plugins/pty-server.js"; + +export default defineConfig({ + plugins: [svelte(), tailwindcss(), ptyServer()], + server: { + allowedHosts: ["svelte-example.wterm.localhost"], + }, +}); diff --git a/examples/svelte/wterm-svelte.d.ts b/examples/svelte/wterm-svelte.d.ts new file mode 100644 index 0000000..429a901 --- /dev/null +++ b/examples/svelte/wterm-svelte.d.ts @@ -0,0 +1,2 @@ +declare module "@wterm/svelte/css"; +declare module "*.css"; diff --git a/packages/@wterm/core/README.md b/packages/@wterm/core/README.md index b29c42d..239c3da 100644 --- a/packages/@wterm/core/README.md +++ b/packages/@wterm/core/README.md @@ -8,6 +8,7 @@ Headless terminal emulator core for [wterm](https://github.com/vercel-labs/wterm |---|---| | [`@wterm/dom`](https://www.npmjs.com/package/@wterm/dom) | DOM renderer, input handler — vanilla JS terminal | | [`@wterm/react`](https://www.npmjs.com/package/@wterm/react) | React component + `useTerminal` hook | +| [`@wterm/svelte`](https://www.npmjs.com/package/@wterm/svelte) | Svelte 5 component + `bind:this` API | | [`@wterm/vue`](https://www.npmjs.com/package/@wterm/vue) | Vue 3 component + template ref API | | [`@wterm/ghostty`](https://www.npmjs.com/package/@wterm/ghostty) | Full-featured VT emulation core powered by libghostty | | [`@wterm/just-bash`](https://www.npmjs.com/package/@wterm/just-bash) | In-browser Bash shell powered by just-bash | @@ -49,29 +50,29 @@ bridge.init(80, 24); bridge.writeString("Hello, world!\r\n"); const cell = bridge.getCell(0, 0); // { char, fg, bg, flags } -const cursor = bridge.getCursor(); // { row, col, visible } +const cursor = bridge.getCursor(); // { row, col, visible } ``` -| Method | Description | -|---|---| -| `WasmBridge.load(url?)` | Load WASM binary and return a new bridge instance. Uses the embedded binary when no URL is given. | -| `init(cols, rows)` | Initialize the terminal grid | -| `writeString(str)` | Write a UTF-8 string to the terminal | -| `writeRaw(data: Uint8Array)` | Write raw bytes to the terminal | -| `resize(cols, rows)` | Resize the terminal grid | -| `getCell(row, col)` | Get cell data (`{ char, fg, bg, flags }`) | -| `getCursor()` | Get cursor state (`{ row, col, visible }`) | -| `getCols()` / `getRows()` | Get current grid dimensions | -| `isDirtyRow(row)` | Check if a row needs re-rendering | -| `clearDirty()` | Reset all dirty-row flags | -| `getTitle()` | Get pending title change (or `null`) | -| `getResponse()` | Get pending host response (or `null`) | -| `getScrollbackCount()` | Number of lines in the scrollback buffer | -| `getScrollbackCell(offset, col)` | Get cell data from scrollback | -| `getScrollbackLineLen(offset)` | Get length of a scrollback line | -| `cursorKeysApp()` | Whether cursor keys are in application mode | -| `bracketedPaste()` | Whether bracketed paste mode is active | -| `usingAltScreen()` | Whether the alternate screen buffer is active | +| Method | Description | +| -------------------------------- | ------------------------------------------------------------------------------------------------- | +| `WasmBridge.load(url?)` | Load WASM binary and return a new bridge instance. Uses the embedded binary when no URL is given. | +| `init(cols, rows)` | Initialize the terminal grid | +| `writeString(str)` | Write a UTF-8 string to the terminal | +| `writeRaw(data: Uint8Array)` | Write raw bytes to the terminal | +| `resize(cols, rows)` | Resize the terminal grid | +| `getCell(row, col)` | Get cell data (`{ char, fg, bg, flags }`) | +| `getCursor()` | Get cursor state (`{ row, col, visible }`) | +| `getCols()` / `getRows()` | Get current grid dimensions | +| `isDirtyRow(row)` | Check if a row needs re-rendering | +| `clearDirty()` | Reset all dirty-row flags | +| `getTitle()` | Get pending title change (or `null`) | +| `getResponse()` | Get pending host response (or `null`) | +| `getScrollbackCount()` | Number of lines in the scrollback buffer | +| `getScrollbackCell(offset, col)` | Get cell data from scrollback | +| `getScrollbackLineLen(offset)` | Get length of a scrollback line | +| `cursorKeysApp()` | Whether cursor keys are in application mode | +| `bracketedPaste()` | Whether bracketed paste mode is active | +| `usingAltScreen()` | Whether the alternate screen buffer is active | ### `WebSocketTransport` @@ -82,7 +83,9 @@ import { WebSocketTransport } from "@wterm/core"; const ws = new WebSocketTransport({ url: "ws://localhost:8080/pty", - onData: (data) => { /* handle received data */ }, + onData: (data) => { + /* handle received data */ + }, }); ws.connect(); diff --git a/packages/@wterm/svelte/README.md b/packages/@wterm/svelte/README.md new file mode 100644 index 0000000..68464d2 --- /dev/null +++ b/packages/@wterm/svelte/README.md @@ -0,0 +1,76 @@ +# @wterm/svelte + +Svelte component for [wterm](https://github.com/vercel-labs/wterm), a terminal emulator for the web. Re-exports everything from `@wterm/dom`, so a single package import covers both the component and terminal types. + +## Install + +```bash +npm install @wterm/dom @wterm/svelte +``` + +## Usage + +```svelte + + + +``` + +## Props + +`Terminal` accepts all shared terminal options: `cols`, `rows`, `core`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug`. + +It also accepts a `theme` prop, which applies a `theme-` class to the root element. Standard `div` attributes like `class`, `style`, `id`, and ARIA props are forwarded to the root element. + +## Callback Props + +Svelte 5 uses callback props for component events: + +```svelte + socket.send(data)} + onTitle={(title) => (document.title = title)} + onResize={(cols, rows) => socket.send(JSON.stringify({ cols, rows }))} + onReady={(wt) => wt.write("ready\r\n")} + onError={(err) => console.error(err)} +/> +``` + +When no `onData` callback is provided, input is echoed back automatically by `@wterm/dom`. + +## Imperative API + +Use `bind:this` to access methods: + +```svelte + + + +``` + +The instance exposes `write(data)`, `resize(cols, rows)`, `focus()`, and `instance()` to access the underlying `WTerm | null`. + +## Themes + +```svelte + + + +``` + +Built-in themes are `solarized-dark`, `monokai`, and `light`. Define custom themes with CSS custom properties (`--term-fg`, `--term-bg`, `--term-color-0` through `--term-color-15`). diff --git a/packages/@wterm/svelte/package.json b/packages/@wterm/svelte/package.json new file mode 100644 index 0000000..370b64a --- /dev/null +++ b/packages/@wterm/svelte/package.json @@ -0,0 +1,64 @@ +{ + "name": "@wterm/svelte", + "version": "0.2.0", + "description": "Svelte component for wterm — a terminal emulator for the web", + "type": "module", + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "svelte": "./dist/index.js", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./css": { + "style": "./src/lib/terminal.css", + "import": "./src/lib/terminal.css", + "default": "./src/lib/terminal.css" + } + }, + "files": [ + "dist", + "src/lib/terminal.css" + ], + "sideEffects": [ + "**/*.css" + ], + "scripts": { + "build": "svelte-package", + "prepublishOnly": "pnpm build", + "test": "vitest run", + "type-check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@internal/ts": "workspace:*", + "@sveltejs/package": "2.5.7", + "@sveltejs/vite-plugin-svelte": "7.0.0", + "@wterm/dom": "workspace:*", + "jsdom": "^29.0.2", + "svelte": "5.55.5", + "svelte-check": "4.4.6", + "typescript": "^6.0.2", + "vitest": "^4.1.4" + }, + "peerDependencies": { + "@wterm/dom": "workspace:*", + "svelte": "^5.0.0" + }, + "keywords": [ + "terminal", + "emulator", + "wasm", + "zig", + "svelte", + "xterm" + ], + "license": "Apache-2.0", + "homepage": "https://wterm.dev", + "repository": { + "type": "git", + "url": "https://github.com/vercel-labs/wterm", + "directory": "packages/@wterm/svelte" + } +} diff --git a/packages/@wterm/svelte/src/lib/Terminal.svelte b/packages/@wterm/svelte/src/lib/Terminal.svelte new file mode 100644 index 0000000..5c6d821 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/Terminal.svelte @@ -0,0 +1,154 @@ + + +
diff --git a/packages/@wterm/svelte/src/lib/__tests__/Terminal.test.ts b/packages/@wterm/svelte/src/lib/__tests__/Terminal.test.ts new file mode 100644 index 0000000..24545e2 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/Terminal.test.ts @@ -0,0 +1,220 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mount, unmount, tick } from "svelte"; +import Terminal from "../Terminal.svelte"; +import TerminalHarness from "./TerminalHarness.svelte"; + +let lastWTermInstance: any = null; + +vi.mock("@wterm/dom", () => { + const mockWTerm = vi.fn().mockImplementation(function ( + this: any, + el: HTMLElement, + options: any, + ) { + this.element = el; + this.bridge = null; + this.cols = options?.cols ?? 80; + this.rows = options?.rows ?? 24; + this.onData = options?.onData ?? null; + this.onTitle = options?.onTitle ?? null; + this.onResize = options?.onResize ?? null; + this.autoResize = options?.autoResize !== false; + this.write = vi.fn(); + this.resize = vi.fn(); + this.focus = vi.fn(); + this.destroy = vi.fn(); + this.init = vi.fn().mockImplementation(async () => { + this.bridge = {}; + return this; + }); + lastWTermInstance = this; + }); + + return { + WTerm: mockWTerm, + Renderer: vi.fn(), + InputHandler: vi.fn(), + }; +}); + +function mountTerminal(props: Record = {}) { + const target = document.createElement("div"); + document.body.append(target); + const component = mount(Terminal, { target, props }); + return { component, target }; +} + +async function flushPromises() { + await Promise.resolve(); + await tick(); +} + +describe("Terminal component", () => { + beforeEach(() => { + document.body.innerHTML = ""; + lastWTermInstance = null; + vi.clearAllMocks(); + }); + + it("renders a div with terminal role and a11y attrs", () => { + const { target } = mountTerminal(); + const el = target.querySelector("[role='textbox']"); + expect(el?.getAttribute("aria-label")).toBe("Terminal"); + expect(el?.getAttribute("aria-roledescription")).toBe("terminal"); + expect(el?.getAttribute("aria-multiline")).toBe("true"); + }); + + it("applies class props and theme class", () => { + const { target } = mountTerminal({ class: "custom", theme: "dark" }); + const el = target.querySelector(".wterm"); + expect(el?.classList.contains("custom")).toBe(true); + expect(el?.classList.contains("theme-dark")).toBe(true); + }); + + it("creates WTerm instance on mount", async () => { + const { WTerm } = await import("@wterm/dom"); + mountTerminal(); + await tick(); + expect(WTerm).toHaveBeenCalled(); + }); + + it("calls init and onReady on mount", async () => { + const onReady = vi.fn(); + mountTerminal({ onReady }); + await flushPromises(); + expect(lastWTermInstance.init).toHaveBeenCalled(); + expect(onReady).toHaveBeenCalledWith(lastWTermInstance); + }); + + it("calls onError on init failure", async () => { + const { WTerm } = await import("@wterm/dom"); + (WTerm as any).mockImplementationOnce(function ( + this: any, + el: HTMLElement, + ) { + this.element = el; + this.bridge = null; + this.cols = 80; + this.rows = 24; + this.onData = null; + this.onTitle = null; + this.onResize = null; + this.write = vi.fn(); + this.resize = vi.fn(); + this.focus = vi.fn(); + this.destroy = vi.fn(); + this.init = vi.fn().mockRejectedValue(new Error("WASM failed")); + lastWTermInstance = this; + }); + + const onError = vi.fn(); + mountTerminal({ onError }); + await flushPromises(); + expect(onError).toHaveBeenCalledWith(expect.any(Error)); + }); + + it("calls destroy on unmount", async () => { + const { component } = mountTerminal(); + await flushPromises(); + const instance = lastWTermInstance; + unmount(component); + expect(instance.destroy).toHaveBeenCalled(); + }); + + it("exposes imperative API through bind:this", async () => { + const { component } = mountTerminal(); + await flushPromises(); + + expect(typeof component.write).toBe("function"); + expect(typeof component.resize).toBe("function"); + expect(typeof component.focus).toBe("function"); + expect(component.instance()).toBe(lastWTermInstance); + }); + + it("delegates imperative methods", async () => { + const { component } = mountTerminal(); + await flushPromises(); + + component.write("test data"); + component.resize(120, 40); + component.focus(); + + expect(lastWTermInstance.write).toHaveBeenCalledWith("test data"); + expect(lastWTermInstance.resize).toHaveBeenCalledWith(120, 40); + expect(lastWTermInstance.focus).toHaveBeenCalled(); + }); + + it("syncs cols/rows on prop change", async () => { + const target = document.createElement("div"); + document.body.append(target); + const component = mount(TerminalHarness, { target }); + await flushPromises(); + + component.setSize(120, 40); + await tick(); + + expect(lastWTermInstance.resize).toHaveBeenCalledWith(120, 40); + }); + + it("toggles cursor-blink class on prop change", async () => { + const target = document.createElement("div"); + document.body.append(target); + const component = mount(TerminalHarness, { target }); + await flushPromises(); + + component.setCursorBlink(true); + await tick(); + expect(lastWTermInstance.element.classList.contains("cursor-blink")).toBe( + true, + ); + + component.setCursorBlink(false); + await tick(); + expect(lastWTermInstance.element.classList.contains("cursor-blink")).toBe( + false, + ); + }); + + it("wires callback props to WTerm callbacks", async () => { + const onData = vi.fn(); + const onTitle = vi.fn(); + const onResize = vi.fn(); + mountTerminal({ onData, onTitle, onResize }); + await flushPromises(); + + lastWTermInstance.onData("hello"); + lastWTermInstance.onTitle("my title"); + lastWTermInstance.onResize(100, 30); + + expect(onData).toHaveBeenCalledWith("hello"); + expect(onTitle).toHaveBeenCalledWith("my title"); + expect(onResize).toHaveBeenCalledWith(100, 30); + }); + + it("does not set onData on WTerm when no onData prop is provided", async () => { + mountTerminal(); + await flushPromises(); + expect(lastWTermInstance.onData).toBeNull(); + }); + + it("passes debug option to WTerm", async () => { + const { WTerm } = await import("@wterm/dom"); + mountTerminal({ debug: true }); + await flushPromises(); + expect(WTerm).toHaveBeenCalledWith( + expect.any(HTMLElement), + expect.objectContaining({ debug: true }), + ); + }); + + it("passes core option to WTerm", async () => { + const { WTerm } = await import("@wterm/dom"); + const core = { init: vi.fn() }; + mountTerminal({ core }); + await flushPromises(); + expect(WTerm).toHaveBeenCalledWith( + expect.any(HTMLElement), + expect.objectContaining({ core }), + ); + }); +}); diff --git a/packages/@wterm/svelte/src/lib/__tests__/Terminal.types.test.ts b/packages/@wterm/svelte/src/lib/__tests__/Terminal.types.test.ts new file mode 100644 index 0000000..dc99fd6 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/Terminal.types.test.ts @@ -0,0 +1,51 @@ +import { describe, it, expectTypeOf } from "vitest"; +import type Terminal from "../Terminal.svelte"; +import type { ComponentProps } from "svelte"; +import type { TerminalCore, WTerm } from "@wterm/dom"; + +describe("Terminal types", () => { + it("bind:this carries the imperative handle", () => { + let terminal: Terminal | undefined; + + expectTypeOf(terminal?.write).toEqualTypeOf< + ((data: string | Uint8Array) => void) | undefined + >(); + expectTypeOf(terminal?.resize).toEqualTypeOf< + ((cols: number, rows: number) => void) | undefined + >(); + expectTypeOf(terminal?.focus).toEqualTypeOf<(() => void) | undefined>(); + expectTypeOf(terminal?.instance).toEqualTypeOf< + (() => WTerm | null) | undefined + >(); + + if (0) { + terminal?.write("x"); + terminal?.write(new Uint8Array()); + terminal?.resize(80, 24); + terminal?.focus(); + + // @ts-expect-error — wrong argument type + terminal?.write(42); + // @ts-expect-error — wrong arity + terminal?.resize(80); + } + }); + + it("typed props", () => { + type Props = ComponentProps; + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + ((data: string) => void) | undefined + >(); + expectTypeOf().toEqualTypeOf< + ((wt: WTerm) => void) | undefined + >(); + }); +}); diff --git a/packages/@wterm/svelte/src/lib/__tests__/TerminalHarness.svelte b/packages/@wterm/svelte/src/lib/__tests__/TerminalHarness.svelte new file mode 100644 index 0000000..73e63a9 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/TerminalHarness.svelte @@ -0,0 +1,23 @@ + + + diff --git a/packages/@wterm/svelte/src/lib/__tests__/setup.ts b/packages/@wterm/svelte/src/lib/__tests__/setup.ts new file mode 100644 index 0000000..6c8aca9 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/setup.ts @@ -0,0 +1,10 @@ +import { vi } from "vitest"; + +Object.defineProperty(globalThis, "ResizeObserver", { + writable: true, + value: vi.fn().mockImplementation(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })), +}); diff --git a/packages/@wterm/svelte/src/lib/index.ts b/packages/@wterm/svelte/src/lib/index.ts new file mode 100644 index 0000000..bc3d574 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/index.ts @@ -0,0 +1,2 @@ +export { default as Terminal } from "./Terminal.svelte"; +export * from "@wterm/dom"; diff --git a/packages/@wterm/svelte/src/lib/terminal.css b/packages/@wterm/svelte/src/lib/terminal.css new file mode 100644 index 0000000..8201c53 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/terminal.css @@ -0,0 +1 @@ +@import "../../../dom/src/terminal.css"; diff --git a/packages/@wterm/svelte/svelte.config.js b/packages/@wterm/svelte/svelte.config.js new file mode 100644 index 0000000..d6f6262 --- /dev/null +++ b/packages/@wterm/svelte/svelte.config.js @@ -0,0 +1,7 @@ +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +const config = { + preprocess: vitePreprocess({ script: true }), +}; + +export default config; diff --git a/packages/@wterm/svelte/tsconfig.json b/packages/@wterm/svelte/tsconfig.json new file mode 100644 index 0000000..15fa7e8 --- /dev/null +++ b/packages/@wterm/svelte/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@internal/ts/tsconfig.base.json", + "compilerOptions": { + "moduleResolution": "bundler", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "types": ["svelte", "vitest/globals"] + }, + "include": ["src/**/*.ts", "src/**/*.svelte", "svelte.config.js"], + "exclude": ["dist", "src/lib/__tests__"] +} diff --git a/packages/@wterm/svelte/vitest.config.ts b/packages/@wterm/svelte/vitest.config.ts new file mode 100644 index 0000000..8f9262f --- /dev/null +++ b/packages/@wterm/svelte/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +export default defineConfig({ + plugins: [svelte()], + resolve: { + conditions: ["browser"], + }, + test: { + environment: "jsdom", + setupFiles: ["./src/lib/__tests__/setup.ts"], + exclude: ["dist/**", ".svelte-kit/**", "node_modules/**"], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 432b5e2..9497bb5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -127,7 +127,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.3 - version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) tailwindcss: specifier: ^4 version: 4.2.2 @@ -135,22 +135,6 @@ importers: specifier: ^6.0.2 version: 6.0.2 - examples/ghostty: - dependencies: - '@wterm/dom': - specifier: workspace:* - version: link:../../packages/@wterm/dom - '@wterm/ghostty': - specifier: workspace:* - version: link:../../packages/@wterm/ghostty - devDependencies: - typescript: - specifier: ^6.0.2 - version: 6.0.2 - vite: - specifier: ^6.3.5 - version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.3) - examples/local: dependencies: '@tailwindcss/postcss': @@ -162,9 +146,6 @@ importers: '@wterm/dom': specifier: workspace:* version: link:../../packages/@wterm/dom - '@wterm/ghostty': - specifier: workspace:* - version: link:../../packages/@wterm/ghostty '@wterm/react': specifier: workspace:* version: link:../../packages/@wterm/react @@ -210,7 +191,7 @@ importers: version: 9.39.4(jiti@2.6.1) eslint-config-next: specifier: 16.2.3 - version: 16.2.3(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + version: 16.2.3(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) tsx: specifier: ^4.19.4 version: 4.21.0 @@ -419,6 +400,61 @@ importers: specifier: ^6.0.2 version: 6.0.2 + examples/svelte: + dependencies: + '@wterm/core': + specifier: workspace:* + version: link:../../packages/@wterm/core + '@wterm/dom': + specifier: workspace:* + version: link:../../packages/@wterm/dom + '@wterm/just-bash': + specifier: workspace:* + version: link:../../packages/@wterm/just-bash + '@wterm/svelte': + specifier: workspace:* + version: link:../../packages/@wterm/svelte + just-bash: + specifier: ^2.14.2 + version: 2.14.2 + node-pty: + specifier: ^1.0.0 + version: 1.1.0 + svelte: + specifier: 5.55.5 + version: 5.55.5(@typescript-eslint/types@8.58.2) + ws: + specifier: ^8.18.2 + version: 8.20.0 + devDependencies: + '@sveltejs/vite-plugin-svelte': + specifier: 7.0.0 + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@tailwindcss/vite': + specifier: ^4.2.2 + version: 4.2.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@types/node': + specifier: ^24.12.2 + version: 24.12.2 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + svelte-check: + specifier: 4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2) + tailwindcss: + specifier: ^4.2.2 + version: 4.2.2 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + typescript: + specifier: ~6.0.2 + version: 6.0.2 + vite: + specifier: ^8.0.4 + version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + examples/vite: dependencies: '@wterm/dom': @@ -526,19 +562,6 @@ importers: specifier: ^6.0.2 version: 6.0.2 - packages/@wterm/ghostty: - dependencies: - '@wterm/core': - specifier: workspace:* - version: link:../core - devDependencies: - '@internal/ts': - specifier: workspace:* - version: link:../../@internal/ts - typescript: - specifier: ^6.0.2 - version: 6.0.2 - packages/@wterm/just-bash: devDependencies: '@internal/ts': @@ -593,6 +616,36 @@ importers: specifier: ^6.0.2 version: 6.0.2 + packages/@wterm/svelte: + devDependencies: + '@internal/ts': + specifier: workspace:* + version: link:../../@internal/ts + '@sveltejs/package': + specifier: 2.5.7 + version: 2.5.7(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2) + '@sveltejs/vite-plugin-svelte': + specifier: 7.0.0 + version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@wterm/dom': + specifier: workspace:* + version: link:../dom + jsdom: + specifier: ^29.0.2 + version: 29.0.2(@noble/hashes@1.8.0) + svelte: + specifier: 5.55.5 + version: 5.55.5(@typescript-eslint/types@8.58.2) + svelte-check: + specifier: 4.4.6 + version: 4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2) + typescript: + specifier: ^6.0.2 + version: 6.0.2 + vitest: + specifier: ^4.1.4 + version: 4.1.4(@opentelemetry/api@1.9.0)(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@1.8.0))(msw@2.13.3(@types/node@25.6.0)(typescript@6.0.2))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + packages/@wterm/vue: devDependencies: '@internal/ts': @@ -2815,6 +2868,25 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.9': + resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/package@2.5.7': + resolution: {integrity: sha512-qqD9xa9H7TDiGFrF6rz7AirOR8k15qDK/9i4MIE8te4vWsv5GEogPks61rrZcLy+yWph+aI6pIj2MdoK3YI8AQ==} + engines: {node: ^16.14 || >=18} + hasBin: true + peerDependencies: + svelte: ^3.44.0 || ^4.0.0 || ^5.0.0-next.1 + + '@sveltejs/vite-plugin-svelte@7.0.0': + resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.46.4 + vite: ^8.0.0-beta.7 || ^8.0.0 + '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} @@ -3533,6 +3605,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.1: + resolution: {integrity: sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==} + engines: {node: '>= 0.4'} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -3795,6 +3871,14 @@ packages: resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==} engines: {node: '>=22.0.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -4179,6 +4263,9 @@ packages: resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} engines: {node: '>=10'} + dedent-js@1.0.1: + resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + dedent@1.7.2: resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} peerDependencies: @@ -4243,6 +4330,9 @@ packages: detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + devalue@5.7.1: + resolution: {integrity: sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==} + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -4508,6 +4598,9 @@ packages: jiti: optional: true + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + espree@10.4.0: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -4521,6 +4614,14 @@ packages: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} + esrap@2.2.5: + resolution: {integrity: sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==} + peerDependencies: + '@typescript-eslint/types': ^8.2.0 + peerDependenciesMeta: + '@typescript-eslint/types': + optional: true + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5131,6 +5232,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -5420,6 +5524,9 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -5737,6 +5844,10 @@ packages: resolution: {integrity: sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==} engines: {node: '>=18.0.0'} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -6269,6 +6380,14 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + recast@0.23.11: resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==} engines: {node: '>= 4'} @@ -6411,6 +6530,10 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -6439,6 +6562,9 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + seek-bzip@2.0.0: resolution: {integrity: sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg==} hasBin: true @@ -6723,6 +6849,24 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svelte-check@4.4.6: + resolution: {integrity: sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte2tsx@0.7.53: + resolution: {integrity: sha512-ljVSwmnYRDHRm8+7ICP6QoAN7U7vgOFfPBLN6T745YWNYqRRSzHxlrzUVqMjYls2Un8MzJissfziy/38e6Deeg==} + peerDependencies: + svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 + typescript: ^4.9.4 || ^5.0.0 + + svelte@5.55.5: + resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} + engines: {node: '>=18'} + swr@2.4.1: resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} peerDependencies: @@ -7178,6 +7322,14 @@ packages: yaml: optional: true + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + vite: + optional: true + vitest@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -7420,6 +7572,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + zod-to-json-schema@3.25.2: resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} peerDependencies: @@ -9420,6 +9575,39 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': + dependencies: + acorn: 8.16.0 + + '@sveltejs/package@2.5.7(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2)': + dependencies: + chokidar: 5.0.0 + kleur: 4.1.5 + sade: 1.8.1 + semver: 7.7.4 + svelte: 5.55.5(@typescript-eslint/types@8.58.2) + svelte2tsx: 0.7.53(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2) + transitivePeerDependencies: + - typescript + + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.55.5(@typescript-eslint/types@8.58.2) + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.58.2))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + deepmerge: 4.3.1 + magic-string: 0.30.21 + obug: 2.1.1 + svelte: 5.55.5(@typescript-eslint/types@8.58.2) + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)) + '@swc/helpers@0.5.15': dependencies: tslib: 2.8.1 @@ -9748,8 +9936,7 @@ snapshots: '@types/statuses@2.0.6': {} - '@types/trusted-types@2.0.7': - optional: true + '@types/trusted-types@2.0.7': {} '@types/unist@2.0.11': {} @@ -10195,6 +10382,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.1: {} + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -10518,6 +10707,14 @@ snapshots: '@chevrotain/types': 12.0.0 '@chevrotain/utils': 12.0.0 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + chownr@1.1.4: optional: true @@ -10920,6 +11117,8 @@ snapshots: mimic-response: 3.1.0 optional: true + dedent-js@1.0.1: {} + dedent@1.7.2: {} deep-eql@5.0.2: {} @@ -10969,6 +11168,8 @@ snapshots: detect-node-es@1.1.0: {} + devalue@5.7.1: {} + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -11277,7 +11478,7 @@ snapshots: '@next/eslint-plugin-next': 16.2.3 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.4(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.4(jiti@2.6.1)) @@ -11320,7 +11521,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -11335,11 +11536,36 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + get-tsconfig: 4.13.7 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.16 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2) + eslint: 9.39.4(jiti@2.6.1) + eslint-import-resolver-node: 0.3.10 + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)): + dependencies: + debug: 3.2.7 + optionalDependencies: eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.4(jiti@2.6.1)) @@ -11357,7 +11583,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)))(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11386,7 +11612,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.4(jiti@2.6.1) eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.58.2(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.2))(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) + eslint-module-utils: 2.12.1(eslint-import-resolver-node@0.3.10)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.4(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -11506,6 +11732,8 @@ snapshots: transitivePeerDependencies: - supports-color + esm-env@1.2.2: {} + espree@10.4.0: dependencies: acorn: 8.16.0 @@ -11518,6 +11746,12 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@2.2.5(@typescript-eslint/types@8.58.2): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.58.2 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -12239,6 +12473,10 @@ snapshots: is-promise@4.0.0: {} + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -12525,6 +12763,8 @@ snapshots: lines-and-columns@1.2.4: {} + locate-character@3.0.0: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -13108,6 +13348,8 @@ snapshots: modern-tar@0.7.6: {} + mri@1.2.0: {} + ms@2.1.3: {} msw@2.13.3(@types/node@25.6.0)(typescript@6.0.2): @@ -13769,6 +14011,10 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@4.1.2: {} + + readdirp@5.0.0: {} + recast@0.23.11: dependencies: ast-types: 0.16.1 @@ -14026,6 +14272,10 @@ snapshots: rw@1.3.3: {} + sade@1.8.1: + dependencies: + mri: 1.2.0 + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.9 @@ -14057,6 +14307,8 @@ snapshots: scheduler@0.27.0: {} + scule@1.3.0: {} + seek-bzip@2.0.0: dependencies: commander: 6.2.1 @@ -14475,6 +14727,46 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.4.6(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.55.5(@typescript-eslint/types@8.58.2) + typescript: 6.0.2 + transitivePeerDependencies: + - picomatch + + svelte2tsx@0.7.53(svelte@5.55.5(@typescript-eslint/types@8.58.2))(typescript@6.0.2): + dependencies: + dedent-js: 1.0.1 + scule: 1.3.0 + svelte: 5.55.5(@typescript-eslint/types@8.58.2) + typescript: 6.0.2 + + svelte@5.55.5(@typescript-eslint/types@8.58.2): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@types/estree': 1.0.8 + '@types/trusted-types': 2.0.7 + acorn: 8.16.0 + aria-query: 5.3.1 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.7.1 + esm-env: 1.2.2 + esrap: 2.2.5(@typescript-eslint/types@8.58.2) + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + transitivePeerDependencies: + - '@typescript-eslint/types' + swr@2.4.1(react@19.2.5): dependencies: dequal: 2.0.3 @@ -14929,6 +15221,14 @@ snapshots: tsx: 4.21.0 yaml: 2.8.3 + vitefu@1.1.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + optionalDependencies: + vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + + vitefu@1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3)): + optionalDependencies: + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3) + vitest@2.1.9(@types/node@25.6.0)(jsdom@29.0.2(@noble/hashes@1.8.0))(lightningcss@1.32.0)(msw@2.13.3(@types/node@25.6.0)(typescript@6.0.2)): dependencies: '@vitest/expect': 2.1.9 @@ -15169,6 +15469,8 @@ snapshots: yoctocolors@2.1.2: {} + zimmerframe@1.1.4: {} + zod-to-json-schema@3.25.2(zod@3.25.76): dependencies: zod: 3.25.76 diff --git a/vitest.workspace.ts b/vitest.workspace.ts index 4974e8a..9321ad7 100644 --- a/vitest.workspace.ts +++ b/vitest.workspace.ts @@ -5,5 +5,7 @@ export default defineWorkspace([ "packages/@wterm/core", "packages/@wterm/dom", "packages/@wterm/react", + "packages/@wterm/svelte", + "packages/@wterm/vue", "packages/@wterm/just-bash", ]);