From 21be1881bfa8f51d24467d7a3420d2b0a90f8415 Mon Sep 17 00:00:00 2001 From: Max Farrell Date: Sun, 26 Apr 2026 11:40:23 -0500 Subject: [PATCH 1/4] Add Svelte support --- README.md | 7 + apps/docs/src/app/api-reference/page.mdx | 724 ++++++++++++++---- apps/docs/src/app/api/docs-chat/route.ts | 2 +- apps/docs/src/app/configuration/page.mdx | 28 +- apps/docs/src/app/get-started/page.mdx | 17 + apps/docs/src/app/svelte/layout.tsx | 7 + apps/docs/src/app/svelte/page.mdx | 99 +++ apps/docs/src/app/themes/page.mdx | 158 +++- apps/docs/src/lib/docs-navigation.ts | 11 + apps/docs/src/lib/page-titles.ts | 1 + examples/svelte/README.md | 32 + examples/svelte/index.html | 12 + examples/svelte/package.json | 35 + examples/svelte/src/App.svelte | 143 ++++ examples/svelte/src/main.ts | 8 + examples/svelte/src/style.css | 88 +++ examples/svelte/svelte.config.js | 7 + examples/svelte/tsconfig.json | 22 + examples/svelte/vite-plugins/pty-server.ts | 79 ++ examples/svelte/vite.config.ts | 11 + examples/svelte/wterm-svelte.d.ts | 2 + packages/@wterm/core/README.md | 47 +- .../.svelte-kit/__package__/Terminal.svelte | 75 ++ .../__package__/Terminal.svelte.d.ts | 41 + .../__package__/Terminal.svelte.d.ts.map | 1 + .../__package__/__tests__/Terminal.test.js | 144 ++++ .../__tests__/Terminal.types.test.js | 31 + .../__package__/__tests__/setup.js | 9 + .../svelte/.svelte-kit/__package__/index.d.ts | 3 + .../.svelte-kit/__package__/index.d.ts.map | 1 + .../svelte/.svelte-kit/__package__/index.js | 2 + .../.svelte-kit/__package__/terminal.css | 1 + packages/@wterm/svelte/README.md | 76 ++ packages/@wterm/svelte/package.json | 64 ++ .../@wterm/svelte/src/lib/Terminal.svelte | 147 ++++ .../svelte/src/lib/__tests__/Terminal.test.ts | 177 +++++ .../src/lib/__tests__/Terminal.types.test.ts | 50 ++ .../@wterm/svelte/src/lib/__tests__/setup.ts | 10 + packages/@wterm/svelte/src/lib/index.ts | 2 + packages/@wterm/svelte/src/lib/terminal.css | 1 + packages/@wterm/svelte/svelte.config.js | 7 + packages/@wterm/svelte/tsconfig.json | 11 + packages/@wterm/svelte/vitest.config.ts | 14 + pnpm-lock.yaml | 384 +++++++++- vitest.workspace.ts | 2 + 45 files changed, 2561 insertions(+), 232 deletions(-) create mode 100644 apps/docs/src/app/svelte/layout.tsx create mode 100644 apps/docs/src/app/svelte/page.mdx create mode 100644 examples/svelte/README.md create mode 100644 examples/svelte/index.html create mode 100644 examples/svelte/package.json create mode 100644 examples/svelte/src/App.svelte create mode 100644 examples/svelte/src/main.ts create mode 100644 examples/svelte/src/style.css create mode 100644 examples/svelte/svelte.config.js create mode 100644 examples/svelte/tsconfig.json create mode 100644 examples/svelte/vite-plugins/pty-server.ts create mode 100644 examples/svelte/vite.config.ts create mode 100644 examples/svelte/wterm-svelte.d.ts create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.js create mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/terminal.css create mode 100644 packages/@wterm/svelte/README.md create mode 100644 packages/@wterm/svelte/package.json create mode 100644 packages/@wterm/svelte/src/lib/Terminal.svelte create mode 100644 packages/@wterm/svelte/src/lib/__tests__/Terminal.test.ts create mode 100644 packages/@wterm/svelte/src/lib/__tests__/Terminal.types.test.ts create mode 100644 packages/@wterm/svelte/src/lib/__tests__/setup.ts create mode 100644 packages/@wterm/svelte/src/lib/index.ts create mode 100644 packages/@wterm/svelte/src/lib/terminal.css create mode 100644 packages/@wterm/svelte/svelte.config.js create mode 100644 packages/@wterm/svelte/tsconfig.json create mode 100644 packages/@wterm/svelte/vitest.config.ts 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/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..0ed1c43 --- /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`, `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/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/.svelte-kit/__package__/Terminal.svelte b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte new file mode 100644 index 0000000..1d39ec1 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte @@ -0,0 +1,75 @@ + + +
diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts new file mode 100644 index 0000000..5ca0951 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts @@ -0,0 +1,41 @@ +import type { SvelteHTMLElements } from "svelte/elements"; +import { WTerm } from "@wterm/dom"; +type DivAttributes = SvelteHTMLElements["div"]; +export interface TerminalProps extends Omit { + /** Column count. */ + cols?: number; + /** Row count. */ + rows?: number; + /** Optional override for the WASM binary URL used by the terminal core. */ + wasmUrl?: string; + /** Theme name appended as a `theme-` class on the root element. */ + theme?: string; + /** + * When `true`, the terminal observes its container and reflows on size + * changes. Defaults to `false` for framework wrappers. + */ + autoResize?: boolean; + /** Toggles the `cursor-blink` class on the root element. */ + cursorBlink?: boolean; + /** Enable debug mode (init-only — changing after mount has no effect). */ + debug?: boolean; + /** Called when the terminal produces input data. */ + onData?: (data: string) => void; + /** Called when the terminal title changes via an escape sequence. */ + onTitle?: (title: string) => void; + /** Called after the terminal is resized. */ + onResize?: (cols: number, rows: number) => void; + /** Called once after `WTerm.init()` resolves. */ + onReady?: (wt: WTerm) => void; + /** Called if WASM loading or initialization fails. */ + onError?: (error: unknown) => void; +} +declare const Terminal: import("svelte").Component void; + resize: (nextCols: number, nextRows: number) => void; + focus: () => void; + instance: () => WTerm | null; +}, "">; +type Terminal = ReturnType; +export default Terminal; +//# sourceMappingURL=Terminal.svelte.d.ts.map \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map new file mode 100644 index 0000000..b04ba66 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"Terminal.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Terminal.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGjC,KAAK,aAAa,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,WAAW,aACf,SAAQ,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC;IACvC,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oDAAoD;IACpD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qEAAqE;IACrE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA+GH,QAAA,MAAM,QAAQ;kBAlFU,MAAM,GAAG,UAAU;uBAId,MAAM,YAAY,MAAM;;;MA8EC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"} \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js new file mode 100644 index 0000000..49b12ed --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js @@ -0,0 +1,144 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mount, unmount, tick } from "svelte"; +import Terminal from "../Terminal.svelte"; +let lastWTermInstance = null; +vi.mock("@wterm/dom", () => { + const mockWTerm = vi.fn().mockImplementation(function (el, options) { + 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 = {}) { + 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.mockImplementationOnce(function (el) { + 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("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 })); + }); +}); diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js new file mode 100644 index 0000000..416bccc --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js @@ -0,0 +1,31 @@ +import { describe, it, expectTypeOf } from "vitest"; +describe("Terminal types", () => { + it("bind:this carries the imperative handle", () => { + let terminal; + expectTypeOf(terminal?.write).toEqualTypeOf(); + expectTypeOf(terminal?.resize).toEqualTypeOf(); + expectTypeOf(terminal?.focus).toEqualTypeOf(); + expectTypeOf(terminal?.instance).toEqualTypeOf(); + 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", () => { + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf(); + }); +}); diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js new file mode 100644 index 0000000..0e829f0 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js @@ -0,0 +1,9 @@ +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/.svelte-kit/__package__/index.d.ts b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts new file mode 100644 index 0000000..8c160cc --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts @@ -0,0 +1,3 @@ +export { default as Terminal } from "./Terminal.svelte"; +export * from "@wterm/dom"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map new file mode 100644 index 0000000..a08a037 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,cAAc,YAAY,CAAC"} \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/index.js b/packages/@wterm/svelte/.svelte-kit/__package__/index.js new file mode 100644 index 0000000..bc3d574 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/index.js @@ -0,0 +1,2 @@ +export { default as Terminal } from "./Terminal.svelte"; +export * from "@wterm/dom"; diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css b/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css new file mode 100644 index 0000000..8201c53 --- /dev/null +++ b/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css @@ -0,0 +1 @@ +@import "../../../dom/src/terminal.css"; diff --git a/packages/@wterm/svelte/README.md b/packages/@wterm/svelte/README.md new file mode 100644 index 0000000..8ac0a7e --- /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`, `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..96fc8b6 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/Terminal.svelte @@ -0,0 +1,147 @@ + + +
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..00b27e5 --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/Terminal.test.ts @@ -0,0 +1,177 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mount, unmount, tick } from "svelte"; +import Terminal from "../Terminal.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("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 }), + ); + }); +}); 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..6d4081d --- /dev/null +++ b/packages/@wterm/svelte/src/lib/__tests__/Terminal.types.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expectTypeOf } from "vitest"; +import type Terminal from "../Terminal.svelte"; +import type { ComponentProps } from "svelte"; +import type { 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< + ((data: string) => void) | undefined + >(); + expectTypeOf().toEqualTypeOf< + ((wt: WTerm) => void) | undefined + >(); + }); +}); 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", ]); From 07bce311f68155777fee4ba86b6b13e229f3bc62 Mon Sep 17 00:00:00 2001 From: Max Farrell Date: Sun, 26 Apr 2026 14:13:10 -0500 Subject: [PATCH 2/4] Remove committed Svelte packaging artifacts --- .gitignore | 1 + .../.svelte-kit/__package__/Terminal.svelte | 75 --------- .../__package__/Terminal.svelte.d.ts | 41 ----- .../__package__/Terminal.svelte.d.ts.map | 1 - .../__package__/__tests__/Terminal.test.js | 144 ------------------ .../__tests__/Terminal.types.test.js | 31 ---- .../__package__/__tests__/setup.js | 9 -- .../svelte/.svelte-kit/__package__/index.d.ts | 3 - .../.svelte-kit/__package__/index.d.ts.map | 1 - .../svelte/.svelte-kit/__package__/index.js | 2 - .../.svelte-kit/__package__/terminal.css | 1 - 11 files changed, 1 insertion(+), 308 deletions(-) delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/index.js delete mode 100644 packages/@wterm/svelte/.svelte-kit/__package__/terminal.css 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/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte deleted file mode 100644 index 1d39ec1..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte +++ /dev/null @@ -1,75 +0,0 @@ - - -
diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts deleted file mode 100644 index 5ca0951..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { SvelteHTMLElements } from "svelte/elements"; -import { WTerm } from "@wterm/dom"; -type DivAttributes = SvelteHTMLElements["div"]; -export interface TerminalProps extends Omit { - /** Column count. */ - cols?: number; - /** Row count. */ - rows?: number; - /** Optional override for the WASM binary URL used by the terminal core. */ - wasmUrl?: string; - /** Theme name appended as a `theme-` class on the root element. */ - theme?: string; - /** - * When `true`, the terminal observes its container and reflows on size - * changes. Defaults to `false` for framework wrappers. - */ - autoResize?: boolean; - /** Toggles the `cursor-blink` class on the root element. */ - cursorBlink?: boolean; - /** Enable debug mode (init-only — changing after mount has no effect). */ - debug?: boolean; - /** Called when the terminal produces input data. */ - onData?: (data: string) => void; - /** Called when the terminal title changes via an escape sequence. */ - onTitle?: (title: string) => void; - /** Called after the terminal is resized. */ - onResize?: (cols: number, rows: number) => void; - /** Called once after `WTerm.init()` resolves. */ - onReady?: (wt: WTerm) => void; - /** Called if WASM loading or initialization fails. */ - onError?: (error: unknown) => void; -} -declare const Terminal: import("svelte").Component void; - resize: (nextCols: number, nextRows: number) => void; - focus: () => void; - instance: () => WTerm | null; -}, "">; -type Terminal = ReturnType; -export default Terminal; -//# sourceMappingURL=Terminal.svelte.d.ts.map \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map b/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map deleted file mode 100644 index b04ba66..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/Terminal.svelte.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"Terminal.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Terminal.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAGjC,KAAK,aAAa,GAAG,kBAAkB,CAAC,KAAK,CAAC,CAAC;AAE/C,MAAM,WAAW,aACf,SAAQ,IAAI,CAAC,aAAa,EAAE,UAAU,CAAC;IACvC,oBAAoB;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iBAAiB;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,4DAA4D;IAC5D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,oDAAoD;IACpD,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,qEAAqE;IACrE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,4CAA4C;IAC5C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAChD,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,KAAK,KAAK,IAAI,CAAC;IAC9B,sDAAsD;IACtD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AA+GH,QAAA,MAAM,QAAQ;kBAlFU,MAAM,GAAG,UAAU;uBAId,MAAM,YAAY,MAAM;;;MA8EC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"} \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js deleted file mode 100644 index 49b12ed..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.test.js +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { mount, unmount, tick } from "svelte"; -import Terminal from "../Terminal.svelte"; -let lastWTermInstance = null; -vi.mock("@wterm/dom", () => { - const mockWTerm = vi.fn().mockImplementation(function (el, options) { - 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 = {}) { - 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.mockImplementationOnce(function (el) { - 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("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 })); - }); -}); diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js deleted file mode 100644 index 416bccc..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/Terminal.types.test.js +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, it, expectTypeOf } from "vitest"; -describe("Terminal types", () => { - it("bind:this carries the imperative handle", () => { - let terminal; - expectTypeOf(terminal?.write).toEqualTypeOf(); - expectTypeOf(terminal?.resize).toEqualTypeOf(); - expectTypeOf(terminal?.focus).toEqualTypeOf(); - expectTypeOf(terminal?.instance).toEqualTypeOf(); - 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", () => { - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - expectTypeOf().toEqualTypeOf(); - }); -}); diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js b/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js deleted file mode 100644 index 0e829f0..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/__tests__/setup.js +++ /dev/null @@ -1,9 +0,0 @@ -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/.svelte-kit/__package__/index.d.ts b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts deleted file mode 100644 index 8c160cc..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as Terminal } from "./Terminal.svelte"; -export * from "@wterm/dom"; -//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map b/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map deleted file mode 100644 index a08a037..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/index.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,cAAc,YAAY,CAAC"} \ No newline at end of file diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/index.js b/packages/@wterm/svelte/.svelte-kit/__package__/index.js deleted file mode 100644 index bc3d574..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/index.js +++ /dev/null @@ -1,2 +0,0 @@ -export { default as Terminal } from "./Terminal.svelte"; -export * from "@wterm/dom"; diff --git a/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css b/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css deleted file mode 100644 index 8201c53..0000000 --- a/packages/@wterm/svelte/.svelte-kit/__package__/terminal.css +++ /dev/null @@ -1 +0,0 @@ -@import "../../../dom/src/terminal.css"; From e9d37544b1108dc1787916778bafdf8ebcde252e Mon Sep 17 00:00:00 2001 From: Max Farrell Date: Sun, 3 May 2026 21:55:52 -0500 Subject: [PATCH 3/4] feat(svelte): add core prop and reactive prop tests Forwards the `core` option (a pre-constructed TerminalCore) to WTerm so callers can supply a custom WASM backend (e.g. @wterm/ghostty) without providing a wasmUrl. Adds TerminalHarness.svelte test helper and tests for cols/rows reactivity, cursor-blink toggling, and the new core prop. Updates README and docs to list core as a supported prop. Co-Authored-By: Claude Sonnet 4.6 --- apps/docs/src/app/svelte/page.mdx | 2 +- examples/svelte/.gitignore | 24 +++++++++++ packages/@wterm/svelte/README.md | 2 +- .../@wterm/svelte/src/lib/Terminal.svelte | 9 +++- .../svelte/src/lib/__tests__/Terminal.test.ts | 43 +++++++++++++++++++ .../src/lib/__tests__/Terminal.types.test.ts | 3 +- .../src/lib/__tests__/TerminalHarness.svelte | 23 ++++++++++ 7 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 examples/svelte/.gitignore create mode 100644 packages/@wterm/svelte/src/lib/__tests__/TerminalHarness.svelte diff --git a/apps/docs/src/app/svelte/page.mdx b/apps/docs/src/app/svelte/page.mdx index 0ed1c43..5e1b0ee 100644 --- a/apps/docs/src/app/svelte/page.mdx +++ b/apps/docs/src/app/svelte/page.mdx @@ -38,7 +38,7 @@ By default, typed input is echoed back to the terminal. Pass `onData` when you n ## Props -The `` component accepts all [shared terminal options](/api-reference#terminal-options) (`cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`, `debug`) plus [Svelte-only props](/api-reference#svelte-only-props) (`theme`). +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 `
`. 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/packages/@wterm/svelte/README.md b/packages/@wterm/svelte/README.md index 8ac0a7e..68464d2 100644 --- a/packages/@wterm/svelte/README.md +++ b/packages/@wterm/svelte/README.md @@ -21,7 +21,7 @@ npm install @wterm/dom @wterm/svelte ## Props -`Terminal` accepts all shared terminal options: `cols`, `rows`, `wasmUrl`, `autoResize`, `cursorBlink`, and `debug`. +`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. diff --git a/packages/@wterm/svelte/src/lib/Terminal.svelte b/packages/@wterm/svelte/src/lib/Terminal.svelte index 96fc8b6..5c6d821 100644 --- a/packages/@wterm/svelte/src/lib/Terminal.svelte +++ b/packages/@wterm/svelte/src/lib/Terminal.svelte @@ -1,7 +1,7 @@ + + From 195056fe2e2bfbdffbfc11d18f1584555ede85a6 Mon Sep 17 00:00:00 2001 From: Max Farrell Date: Sun, 3 May 2026 22:02:33 -0500 Subject: [PATCH 4/4] docs: add Svelte examples to ghostty, just-bash, and markdown pages Adds Svelte 5 code snippets alongside existing React/Vue/Vanilla examples for all three package integration guides, completing framework parity. Co-Authored-By: Claude Sonnet 4.6 --- apps/docs/src/app/ghostty/page.mdx | 14 +++++++++++++ apps/docs/src/app/just-bash/page.mdx | 28 ++++++++++++++++++++++++++ apps/docs/src/app/markdown/page.mdx | 30 ++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+) 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