Compare commits

...

14 Commits

Author SHA1 Message Date
renovate[bot]
a6a7dda678 chore(deps): update actions/setup-node action to v7 2026-07-30 21:08:42 +00:00
github-actions[bot]
6468954022 Version Packages (#246) 2026-07-27 17:51:10 +02:00
Paolo Ricciuti
1eb06fd05a fix: remove special characters from references (#247) 2026-07-27 17:22:19 +02:00
Roman
e0aa00a3a5 fix: use "directory" for "repository" field (#244) 2026-07-27 16:33:48 +02:00
Paolo Ricciuti
cbac8c2bb7 fix: links in skill references (for real this time) (#245) 2026-07-27 16:33:25 +02:00
github-actions[bot]
c240d44eda Version Packages (#241) 2026-07-26 16:33:25 +02:00
Paolo Ricciuti
c2bbc2a4e7 fix: replace links in references with respective llms.txt (#243) 2026-07-26 16:24:40 +02:00
github-actions[bot]
022324b0ed chore: sync plugins from tools/ (#242)
Co-authored-by: paoloricciuti <26281609+paoloricciuti@users.noreply.github.com>
2026-07-26 14:58:03 +02:00
jyc.dev
d2571d81ad chore: fix typos and heading casing in agent + skill docs (#240) 2026-07-26 14:56:11 +02:00
Paolo Ricciuti
ed4272d55f chore: show warning for old versions (#238)
Co-authored-by: Rich Harris <hello@rich-harris.dev>
2026-07-26 14:55:12 +02:00
github-actions[bot]
ab14e46ec3 Version Packages (#237) 2026-07-22 13:43:04 +02:00
jyc.dev
d6c694c778 chore: switch to @changesets/changelog-github (#235) 2026-07-22 13:35:11 +02:00
Paolo Ricciuti
a52c8b63bb fix: ship plugin as js files instead of ts (#236) 2026-07-22 13:34:27 +02:00
Rich Harris
8152ed9fd4 chore: use quotemarks instead of bold text (#234) 2026-07-18 20:02:05 -04:00
77 changed files with 1454 additions and 558 deletions

View File

@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
"changelog": ["@changesets/changelog-github", { "repo": "sveltejs/ai-tools", "template": "\n- {summary} {ref}" }],
"commit": false,
"fixed": [],
"linked": [],

View File

@@ -19,7 +19,7 @@ jobs:
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -19,7 +19,7 @@ jobs:
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -29,7 +29,7 @@ jobs:
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: false # pnpm is not installed yet
@@ -39,7 +39,7 @@ jobs:
PNPM_VER=$(jq -r '.packageManager | if .[0:5] == "pnpm@" then .[5:] else "packageManager in package.json does not start with pnpm@\n" | halt_error(1) end' package.json)
echo installing pnpm version $PNPM_VER
npm i -g pnpm@$PNPM_VER
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node }}
package-manager-cache: true # caches pnpm via packageManager field in package.json

View File

@@ -20,7 +20,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
@@ -33,7 +33,7 @@ jobs:
npm i -g "pnpm@$PNPM_VER"
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json

View File

@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
@@ -37,7 +37,7 @@ jobs:
npm i -g pnpm@$PNPM_VER
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json

View File

@@ -19,7 +19,7 @@ jobs:
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
cache: 'pnpm'

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
paths:
- 'packages/opencode/config.ts'
- 'packages/opencode/config.js'
permissions:
contents: write
@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
@@ -37,7 +37,7 @@ jobs:
npm i -g pnpm@$PNPM_VER
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json
@@ -67,7 +67,7 @@ jobs:
## Summary
Automatically generated update for OpenCode JSON schema.
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.js`.
## Changes
- Updated `packages/opencode/schema.json` with latest JSON schema

View File

@@ -24,7 +24,7 @@ jobs:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: false # pnpm is not installed yet
@@ -37,7 +37,7 @@ jobs:
npm i -g pnpm@$PNPM_VER
- name: Setup Node.js with pnpm cache
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
package-manager-cache: true # caches pnpm via packageManager field in package.json

View File

@@ -9,13 +9,11 @@ CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used when
<!-- prettier-ignore-start -->
````markdown
# Svelte 5 Code Writer
## CLI Tools
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
@@ -23,7 +21,7 @@ npx @sveltejs/mcp list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
@@ -37,7 +35,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
@@ -118,9 +116,9 @@ If the derived expression is an object or array, it will be returned as-is — i
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
@@ -169,7 +167,7 @@ Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}

View File

@@ -3,13 +3,13 @@ name: svelte-file-editor
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## Available MCP Tools
## Available MCP tools
### 1. list-sections
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
### 1. Gather context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
### 2. Read the target file
Read the file to understand the current implementation.
### 3. Make Changes
### 3. Make changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
### 4. Validate changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
### 5. Fix any issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
## Output format
After completing your work, provide:

View File

@@ -34,11 +34,11 @@ The package also includes a TUI plugin for configuring these features interactiv
}
```
Restart OpenCode, then run `/svelte-plugin` or select **Configure Svelte plugin** from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and **Revert changes** restores the values from when the dialog was opened.
Restart OpenCode, then run `/svelte-plugin` or select 'Configure Svelte plugin' from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and 'Revert changes' restores the values from when the dialog was opened.
## Configuration
By default, everything is enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
By default, the MCP server, subagent, skills, instructions, and automatic updates are enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
- locally, in `.opencode/svelte.json`
- globally, in `~/.config/opencode/svelte.json` (or, if you have specified the environment variable, in `$OPENCODE_CONFIG_DIR/svelte.json`)
@@ -68,6 +68,13 @@ By default, everything is enabled. The TUI plugin writes the same configuration
},
"instructions": {
"enabled": true
}
},
"autoupdate": true
}
```
### Automatic updates
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so it continues using the cached version until that cache is removed.
Automatic updates are enabled by default. After detecting a newer version, the plugin removes itself from the cache when OpenCode shuts down. OpenCode installs the latest version the next time it starts. Automatic updates only apply when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.

View File

@@ -10,7 +10,7 @@ If possible, we recommend that you instruct the LLM to execute MCP calls with th
## Installation
In VS Code, run the **Install plugin from source** command and use the repository URL:
In VS Code, run the 'Install plugin from source' command and use the repository URL:
```text
https://github.com/sveltejs/ai-tools

View File

@@ -43,12 +43,12 @@
],
"private": true,
"devDependencies": {
"@changesets/changelog-github": "catalog:tooling",
"@changesets/cli": "catalog:tooling",
"@eslint/compat": "catalog:lint",
"@eslint/js": "catalog:lint",
"@modelcontextprotocol/inspector": "catalog:ai",
"@sveltejs/adapter-vercel": "catalog:svelte",
"@svitejs/changesets-changelog-github-compact": "catalog:tooling",
"eslint": "catalog:lint",
"eslint-config-prettier": "catalog:lint",
"eslint-plugin-import": "catalog:lint",

View File

@@ -14,7 +14,7 @@
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/ai-tools.git",
"path": "packages/mcp-stdio"
"directory": "packages/mcp-stdio"
},
"files": [
"dist"

View File

@@ -1,5 +1,29 @@
# @sveltejs/opencode
## 0.1.13
### Patch Changes
- fix: links in skill references (for real this time) ([#245](https://github.com/sveltejs/ai-tools/pull/245))
- fix: remove special characters from references ([#247](https://github.com/sveltejs/ai-tools/pull/247))
## 0.1.12
### Patch Changes
- chore: show warning for old versions ([#238](https://github.com/sveltejs/ai-tools/pull/238))
- feat: add `autoupdate` option to reinstall the plugin when a new version is available ([#238](https://github.com/sveltejs/ai-tools/pull/238))
- fix: links within references in SKILLS ([#243](https://github.com/sveltejs/ai-tools/pull/243))
## 0.1.11
### Patch Changes
- fix: ship plugin as js files instead of ts ([#236](https://github.com/sveltejs/ai-tools/pull/236))
## 0.1.10
### Patch Changes

View File

@@ -72,10 +72,17 @@ Create `svelte.json` to customize how the plugin configures MCP, the Svelte suba
},
"skills": {
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
}
},
"autoupdate": true
}
```
### Auto update
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so a new version is only picked up once that cache is wiped.
Automatic updates are enabled by default. When a newer version is detected, the plugin removes itself from the OpenCode cache as OpenCode shuts down, so the latest version is installed on the next start. This only applies when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.
### Defaults
If omitted, the plugin uses these defaults:
@@ -86,6 +93,7 @@ If omitted, the plugin uses these defaults:
- `subagent.agents`: `{}`
- `instructions.enabled`: `true`
- `skills.enabled`: `true`
- `autoupdate`: `true`
### Configuration Options
@@ -100,6 +108,7 @@ If omitted, the plugin uses these defaults:
| `subagent.agents.svelte-file-editor.maxSteps` | `number` | unlimited | Limit the number of steps the subagent can execute. |
| `instructions.enabled` | `boolean` | `true` | Enable or disable automatic instruction-file injection. |
| `skills.enabled` | `boolean \| string[]` | `true` | Enable all skills (`true`), disable all skills (`false`), or enable only specific skill names. |
| `autoupdate` | `boolean` | `true` | Remove an unpinned/latest plugin from the cache on exit when a newer version is available. |
### Supported Skill Names

View File

@@ -0,0 +1,11 @@
// This file is auto-generated by scripts/sync-opencode-plugin.ts
// Do not edit manually — edit the markdown files in tools/agents/ instead.
export const agents = {
'svelte-file-editor': {
description:
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
prompt:
"You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the target file\n\nRead the file to understand the current implementation.\n\n### 3. Make changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix any issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
},
};

View File

@@ -1,11 +0,0 @@
// This file is auto-generated by scripts/sync-opencode-plugin.ts
// Do not edit manually — edit the markdown files in tools/agents/ instead.
export const agents = {
'svelte-file-editor': {
description:
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
prompt:
"You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP Tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather Context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the Target File\n\nRead the file to understand the current implementation.\n\n### 3. Make Changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate Changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix Any Issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output Format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
},
} as const;

View File

@@ -1,9 +1,10 @@
import type { PluginInput } from '@opencode-ai/plugin';
import { existsSync, readFileSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import * as v from 'valibot';
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
// Schema for individual agent configuration
const agent_config_schema = v.object({
model: v.pipe(
@@ -28,19 +29,20 @@ const agent_config_schema = v.object({
const default_config = {
mcp: {
type: 'remote' as 'remote' | 'local',
type: /** @type {'remote' | 'local'} */ ('remote'),
enabled: true,
},
subagent: {
enabled: true,
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
agents: /** @type {Record<string, v.InferInput<typeof agent_config_schema>>} */ ({}),
},
instructions: {
enabled: true,
},
skills: {
enabled: true as boolean | string[],
enabled: /** @type {boolean | string[]} */ (true),
},
autoupdate: true,
};
export const config_schema = v.object({
@@ -89,27 +91,32 @@ export const config_schema = v.object({
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
),
),
autoupdate: v.pipe(
v.optional(v.boolean()),
v.description(
'When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning.',
),
),
});
export type McpConfig = v.InferInput<typeof config_schema>;
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
interface ConfigLoadResult {
data: Record<string, unknown> | null;
parse_error?: string;
}
/** @typedef {{ data: Record<string, unknown> | null, parse_error?: string }} ConfigLoadResult */
function get_config_paths() {
// Global: ~/.config/opencode/svelte.json
let global_path: string | null = null;
/** @type {string | null} */
let global_path = null;
if (existsSync(GLOBAL_CONFIG_PATH)) {
global_path = GLOBAL_CONFIG_PATH;
}
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
let config_dir_path: string | null = null;
/** @type {string | null} */
let config_dir_path = null;
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
if (opencode_config_dir) {
const config_json = join(opencode_config_dir, 'svelte.json');
@@ -119,7 +126,8 @@ function get_config_paths() {
}
// Project-local: ./.opencode/svelte.json (cwd)
let project_path: string | null = null;
/** @type {string | null} */
let project_path = null;
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
if (existsSync(project_config)) {
project_path = project_config;
@@ -129,8 +137,13 @@ function get_config_paths() {
return [global_path, config_dir_path, project_path];
}
function load_config_file(config_path: string): ConfigLoadResult {
let file_content: string;
/**
* @param {string} config_path
* @returns {ConfigLoadResult}
*/
function load_config_file(config_path) {
/** @type {string} */
let file_content;
try {
file_content = readFileSync(config_path, 'utf-8');
} catch {
@@ -144,7 +157,7 @@ function load_config_file(config_path: string): ConfigLoadResult {
return { data: null, parse_error: 'Config file is empty or invalid' };
}
return { data: parsed };
} catch (error: unknown) {
} catch (error) {
return {
data: null,
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
@@ -152,7 +165,11 @@ function load_config_file(config_path: string): ConfigLoadResult {
}
}
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
/**
* @param {Partial<McpConfig>} user_config
* @returns {McpConfig}
*/
function merge_with_defaults(user_config) {
return {
mcp: {
...default_config.mcp,
@@ -174,12 +191,15 @@ function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
...default_config.skills,
...user_config.skills,
},
autoupdate: user_config.autoupdate ?? default_config.autoupdate,
};
}
export function get_mcp_config(ctx: PluginInput) {
/** @param {PluginInput} ctx */
export function get_mcp_config(ctx) {
const config_paths = get_config_paths();
let merged: Partial<McpConfig> = {};
/** @type {Partial<McpConfig>} */
let merged = {};
// Iterate from lowest to highest priority, merging as we go
for (const path of config_paths) {
@@ -209,6 +229,7 @@ export function get_mcp_config(ctx: PluginInput) {
},
instructions: { ...merged.instructions, ...parsed.output.instructions },
skills: { ...merged.skills, ...parsed.output.skills },
autoupdate: parsed.output.autoupdate ?? merged.autoupdate,
};
} else {
setTimeout(() => {

View File

@@ -1,14 +1,24 @@
import type { Plugin } from '@opencode-ai/plugin';
import { readdir } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { agents } from './agents.ts';
import { get_mcp_config } from './config.ts';
import { agents } from './agents.js';
import { get_mcp_config } from './config.js';
import { setup_updates } from './update.js';
/** @typedef {import('@opencode-ai/plugin').Plugin} Plugin */
const current_dir = dirname(fileURLToPath(import.meta.url));
export const svelte_plugin: Plugin = async (ctx) => {
/**
* @param {Parameters<Plugin>[0]} ctx
* @returns {ReturnType<Plugin>}
*/
export async function svelte_plugin(ctx) {
const mcp_config = get_mcp_config(ctx);
const dispose = setup_updates(ctx, mcp_config.autoupdate === true);
return {
dispose,
async config(input) {
input.agent ??= {};
input.mcp ??= {};
@@ -24,15 +34,13 @@ export const svelte_plugin: Plugin = async (ctx) => {
const mcp = input.mcp[name];
if (
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
(mcp?.type === 'local' &&
mcp.command.some((cmd: string) => cmd.includes('@sveltejs/mcp')))
(mcp?.type === 'local' && mcp.command.some((cmd) => cmd.includes('@sveltejs/mcp')))
) {
// if we found the svelte MCP server, we store its name and break
svelte_mcp_name = name;
break;
}
}
const mcp_config = get_mcp_config(ctx);
if (mcp_config.instructions?.enabled !== false) {
const instructions_dir = join(current_dir, 'instructions');
@@ -75,7 +83,8 @@ export const svelte_plugin: Plugin = async (ctx) => {
if (mcp_config.subagent?.enabled !== false) {
for (const [agent_name, agent_data] of Object.entries(agents)) {
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
const default_config: (typeof input.agent)[string] = {
/** @type {(typeof input.agent)[string]} */
const default_config = {
color: '#ff3e00',
mode: 'subagent',
prompt: agent_data.prompt,
@@ -109,4 +118,4 @@ export const svelte_plugin: Plugin = async (ctx) => {
}
},
};
};
}

View File

@@ -1,6 +1,6 @@
{
"name": "@sveltejs/opencode",
"version": "0.1.10",
"version": "0.1.13",
"type": "module",
"license": "MIT",
"homepage": "https://github.com/sveltejs/ai-tools#readme",
@@ -9,44 +9,46 @@
},
"scripts": {
"check": "tsc --noEmit",
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
"generate-schema": "node scripts/generate-schema.js"
},
"files": [
"index.ts",
"config.ts",
"tui.tsx",
"agents.ts",
"index.js",
"config.js",
"update.js",
"tui.jsx",
"agents.js",
"instructions",
"skills"
],
"exports": {
"./server": {
"types": "./index.ts",
"import": "./index.ts"
"types": "./index.js",
"import": "./index.js"
},
"./tui": {
"types": "./tui.tsx",
"import": "./tui.tsx"
"types": "./tui.jsx",
"import": "./tui.jsx"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/sveltejs/ai-tools.git",
"path": "packages/opencode"
"directory": "packages/opencode"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"valibot": "catalog:tooling",
"@opentui/core": "catalog:opencode",
"@opentui/keymap": "catalog:opencode",
"@opentui/solid": "catalog:opencode",
"solid-js": "catalog:opencode"
"solid-js": "catalog:opencode",
"valibot": "catalog:tooling",
"verkit": "catalog:tooling"
},
"devDependencies": {
"@opencode-ai/plugin": "catalog:opencode",
"@valibot/to-json-schema": "catalog:tooling",
"@types/node": "catalog:tooling"
"@types/node": "catalog:tooling",
"@valibot/to-json-schema": "catalog:tooling"
}
}

View File

@@ -104,6 +104,10 @@
},
"required": [],
"description": "Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills."
},
"autoupdate": {
"type": "boolean",
"description": "When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning."
}
},
"required": [],

View File

@@ -4,7 +4,8 @@ import fs from 'node:fs';
import path from 'node:path';
// Read agent names from tools/agents/*.md files
function get_agent_names(agents_dir: string) {
/** @param {string} agents_dir */
function get_agent_names(agents_dir) {
if (!fs.existsSync(agents_dir)) return [];
return fs
.readdirSync(agents_dir, { withFileTypes: true })
@@ -12,7 +13,8 @@ function get_agent_names(agents_dir: string) {
.map((entry) => entry.name.replace(/\.md$/, ''));
}
function get_skill_names(skills_dir: string) {
/** @param {string} skills_dir */
function get_skill_names(skills_dir) {
if (!fs.existsSync(skills_dir)) return [];
return fs
.readdirSync(skills_dir, { withFileTypes: true })
@@ -29,10 +31,13 @@ const json_schema = toJsonSchema(schema);
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
// editors will autocomplete the known names but any string is still valid.
if (skill_names.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const enabled = (json_schema as any).properties?.skills?.properties?.enabled;
const enabled = /** @type {any} */ (json_schema).properties?.skills?.properties?.enabled;
if (enabled?.anyOf) {
const array_branch = enabled.anyOf.find((s: Record<string, unknown>) => s.type === 'array');
const array_branch = enabled.anyOf.find(
/** @type {(schema: Record<string, unknown>) => boolean} */ (
(schema) => schema.type === 'array'
),
);
if (array_branch) {
array_branch.items = {
anyOf: [{ enum: skill_names }, { type: 'string' }],
@@ -48,8 +53,7 @@ const agents_dir = path.resolve('../../tools/agents');
const agent_names = get_agent_names(agents_dir);
if (agent_names.length > 0) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const agents = (json_schema as any).properties?.subagent?.properties?.agents;
const agents = /** @type {any} */ (json_schema).properties?.subagent?.properties?.agents;
if (agents) {
agents.propertyNames = {
anyOf: [{ enum: agent_names }, { type: 'string' }],

View File

@@ -3,13 +3,11 @@ name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]

View File

@@ -35,9 +35,9 @@ If the derived expression is an object or array, it will be returned as-is — i
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
@@ -86,7 +86,7 @@ Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}

View File

@@ -1,4 +1,4 @@
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
<input bind:value={content} />
<button {@attach tooltip(content)}> Hover me </button>
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements (demo:
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
<input bind:value={content} />
<Button {@attach tooltip(content)}>Hover me</Button>
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#fromAction), allowing you to (for example) use them with components.

View File

@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true,
},
},
async: true
}
}
};
```
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
}
</script>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p><p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings/llms.txt#Client-warnings-await_waterfall) warning
## Indicating loading states
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect/llms.txt#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte/llms.txt#settled) to get a promise that resolves when the current update is complete:
```js
import { tick, settled } from 'svelte';
@@ -103,7 +110,7 @@ async function onclick() {
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
## Server-side rendering
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte/llms.txt#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}>open menu</button
>
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => (open = false)} />
<Menu onclose={() => open = false} />
{/if}
```

View File

@@ -3,13 +3,19 @@
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
```svelte
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
```
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
```svelte
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
```
> [!NOTE]

View File

@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
```svelte
<script>
import { getUser } from 'my-database-library';
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
};
});
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce },
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
```js
const { head, body, hashes } = await render(App, {
csp: { hash: true },
csp: { hash: true }
});
```
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
```js
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
);
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

View File

@@ -1,8 +1,11 @@
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>

View File

@@ -1,4 +1,4 @@
To render a [snippet](snippet), use a `{@render ...}` tag.
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
```svelte
{#snippet sum(a, b)}
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
{@render children?.()}
```
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
```svelte
{#if children}

View File

@@ -8,7 +8,7 @@
{#snippet name(param1, param2, paramN)}...{/snippet}
```
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
```svelte
{#each images as image}
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
```svelte
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other (demo:
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
### Optional snippet props
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{@render children?.()}
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{#if children}
{@render children()}
{@render children()}
{:else}
fallback content
fallback content
{/if}
```
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
let {
data,
children,
row,
row
}: {
data: T[];
children: Snippet;
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer
## Programmatic snippets
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte/llms.txt#createRawSnippet) API. This is intended for advanced use cases.
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

View File

@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
are active, and the returned teardown function will only be called when all effects are destroyed.
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity/llms.txt#MediaQuery):
```js
// @errors: 7031

View File

@@ -1,11 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": true,
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"types": ["@types/node"]
},
"include": ["index.ts", "config.ts", "agents.ts", "tui.tsx", "scripts/*"],
"include": ["index.js", "config.js", "update.js", "agents.js", "tui.jsx", "scripts/*"],
"exclude": ["node_modules"]
}

View File

@@ -1,25 +1,33 @@
/** @jsxImportSource @opentui/solid */
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
import { existsSync } from 'node:fs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join } from 'node:path';
import * as v from 'valibot';
import { config_schema, type McpConfig } from './config.ts';
import { config_schema } from './config.js';
/** @typedef {import('@opencode-ai/plugin/tui').TuiPlugin} TuiPlugin */
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginApi} TuiPluginApi */
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginModule} TuiPluginModule */
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
/** @typedef {'project' | 'global'} Scope */
/** @typedef {Partial<McpConfig>} Config */
const plugin_id = 'svelte.configure';
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'] as const;
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'];
const agent_name = 'svelte-file-editor';
type Scope = 'project' | 'global';
type Config = Partial<McpConfig>;
function project_root(api: Parameters<TuiPlugin>[0]) {
/** @param {TuiPluginApi} api */
function project_root(api) {
const worktree = api.state.path.worktree;
return worktree && worktree !== '/' ? worktree : api.state.path.directory;
}
function config_path(api: Parameters<TuiPlugin>[0], scope: Scope) {
/**
* @param {TuiPluginApi} api
* @param {Scope} scope
*/
function config_path(api, scope) {
if (scope === 'project') return join(project_root(api), '.opencode', 'svelte.json');
return join(
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode'),
@@ -27,26 +35,40 @@ function config_path(api: Parameters<TuiPlugin>[0], scope: Scope) {
);
}
async function read_config(path: string): Promise<Config> {
/**
* @param {string} path
* @returns {Promise<Config>}
*/
async function read_config(path) {
if (!existsSync(path)) return {};
const parsed: unknown = JSON.parse(await readFile(path, 'utf8'));
/** @type {unknown} */
const parsed = JSON.parse(await readFile(path, 'utf8'));
const result = v.safeParse(config_schema, parsed);
if (!result.success)
throw new Error('The existing file does not match the Svelte plugin schema.');
// Keep schema annotations and future fields that this version does not edit.
return parsed as Config;
return /** @type {Config} */ (parsed);
}
async function save_config(path: string, config: Config) {
/**
* @param {string} path
* @param {Config} config
*/
async function save_config(path, config) {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
}
function display(value: unknown, fallback = 'default') {
/**
* @param {unknown} value
* @param {string} [fallback]
*/
function display(value, fallback = 'default') {
return value === undefined ? fallback : String(value);
}
const tui: TuiPlugin = async (api) => {
/** @type {TuiPlugin} */
const tui = async (api) => {
function open_scope() {
if (!api.state.path.directory) {
api.ui.toast({
@@ -57,7 +79,7 @@ const tui: TuiPlugin = async (api) => {
}
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<Scope>
<api.ui.DialogSelect
title="Configure Svelte plugin"
options={[
{
@@ -71,14 +93,16 @@ const tui: TuiPlugin = async (api) => {
description: 'Write svelte.json in the OpenCode config directory',
},
]}
onSelect={(option) => void open_config(option.value)}
onSelect={(option) => void open_config(/** @type {Scope} */ (option.value))}
/>
));
}
async function open_config(scope: Scope) {
/** @param {Scope} scope */
async function open_config(scope) {
const path = config_path(api, scope);
let config: Config;
/** @type {Config} */
let config;
try {
config = await read_config(path);
} catch (error) {
@@ -90,7 +114,8 @@ const tui: TuiPlugin = async (api) => {
return;
}
const original_config = structuredClone(config);
let current_option: string | undefined;
/** @type {string | undefined} */
let current_option;
async function persist(show_toast = true) {
try {
@@ -102,7 +127,11 @@ const tui: TuiPlugin = async (api) => {
}
}
function prompt_agent_number(key: 'temperature' | 'top_p' | 'maxSteps', label: string) {
/**
* @param {'temperature' | 'top_p' | 'maxSteps'} key
* @param {string} label
*/
function prompt_agent_number(key, label) {
const agent = config.subagent?.agents?.[agent_name];
api.ui.dialog.replace(() => (
<api.ui.DialogPrompt
@@ -129,7 +158,7 @@ const tui: TuiPlugin = async (api) => {
function open_agent() {
const agent = config.subagent?.agents?.[agent_name];
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<string>
<api.ui.DialogSelect
title={`Configure ${agent_name}`}
options={[
{ title: 'Model', value: 'model', description: display(agent?.model) },
@@ -144,11 +173,12 @@ const tui: TuiPlugin = async (api) => {
]}
onSelect={(option) => {
if (option.value === 'back') return open_menu();
if (option.value !== 'model')
return prompt_agent_number(
option.value as 'temperature' | 'top_p' | 'maxSteps',
option.title,
);
if (
option.value === 'temperature' ||
option.value === 'top_p' ||
option.value === 'maxSteps'
)
return prompt_agent_number(option.value, option.title);
api.ui.dialog.replace(() => (
<api.ui.DialogPrompt
title={`${agent_name}: model`}
@@ -178,14 +208,16 @@ const tui: TuiPlugin = async (api) => {
Array.isArray(skills) ? skills : skills === false ? [] : skill_names,
);
const all_skills_selected = skill_names.every((name) => selected_skills.has(name));
function checked(value: boolean | undefined) {
/** @param {boolean | undefined} value */
function checked(value) {
return value !== false ? '[x]' : '[ ]';
}
function radio(value: 'remote' | 'local') {
/** @param {'remote' | 'local'} value */
function radio(value) {
return (config.mcp?.type ?? 'remote') === value ? '(*)' : '( )';
}
api.ui.dialog.replace(() => (
<api.ui.DialogSelect<string>
<api.ui.DialogSelect
title={`Svelte plugin (${scope})`}
{...(current_option === undefined ? {} : { current: current_option })}
skipFilter
@@ -218,6 +250,12 @@ const tui: TuiPlugin = async (api) => {
value: `skill:${name}`,
category: 'Skills',
})),
{
title: `${config.autoupdate !== false ? '[x]' : '[ ]'} Auto update`,
value: 'autoupdate',
category: 'Updates',
description: 'Reinstall the plugin on the next start when a new version is out',
},
{
title: 'Revert changes',
value: 'revert',
@@ -252,6 +290,7 @@ const tui: TuiPlugin = async (api) => {
if (option.value === 'skills-all') {
config.skills = { enabled: all_skills_selected ? [] : [...skill_names] };
}
if (option.value === 'autoupdate') config.autoupdate = config.autoupdate === false;
if (option.value.startsWith('skill:')) {
const name = option.value.slice('skill:'.length);
if (selected_skills.has(name)) {
@@ -285,7 +324,7 @@ const tui: TuiPlugin = async (api) => {
});
};
export default {
export default /** @satisfies {TuiPluginModule & { id: string }} */ ({
id: plugin_id,
tui,
} satisfies TuiPluginModule & { id: string };
});

View File

@@ -0,0 +1,97 @@
import { exec } from 'node:child_process';
import { rmSync } from 'node:fs';
import { basename, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { compare } from 'verkit';
import package_json from './package.json' with { type: 'json' };
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
const current_dir = dirname(fileURLToPath(import.meta.url));
const name_segments = package_json.name.split('/');
/**
* @param {string} dir
* @param {number} levels
*/
function up(dir, levels) {
for (let i = 0; i < levels; i++) dir = dirname(dir);
return dir;
}
/**
* opencode installs every plugin in `<cache>/packages/<spec>/node_modules/<name>`, so removing
* `<spec>` is enough to make it reinstall the plugin from scratch on the next start.
*
* We return `null` whenever we don't recognize that layout (for example when the plugin is linked
* locally during development) so that we never delete a folder we don't own.
*/
export function get_install_dir(dir = current_dir) {
// from `<cache>/packages/<spec>/node_modules/<name>` up to `<cache>/packages/<spec>`
const install_dir = up(dir, name_segments.length + 1);
// ...and from there up to `<cache>/packages`
if (basename(up(install_dir, name_segments.length)) !== 'packages') return null;
// Only unconstrained installs can pick up npm's latest version. Ranges and alternate tags may
// resolve to the same installed version after every wipe.
const package_name = name_segments.at(-1);
if (!package_name || ![package_name, `${package_name}@latest`].includes(basename(install_dir))) {
return null;
}
return install_dir;
}
/**
* Checks npm for a newer version of the plugin and warns the user about it. If `autoupdate` is
* enabled we also delete the cached plugin once opencode shuts down, so the next start picks up the
* new version.
*
* @param {PluginInput} ctx
* @param {boolean} autoupdate
* @returns {() => Promise<void>} the `dispose` hook
*/
export function setup_updates(ctx, autoupdate) {
/** @type {string | null} */
let stale_dir = null;
let wiped = false;
function wipe() {
if (wiped || !stale_dir) return;
wiped = true;
try {
rmSync(stale_dir, { recursive: true, force: true });
} catch {
// if we can't delete it there's nothing useful we can do at this point, the user will
// just get the warning again on the next start
}
}
exec(`npm view ${package_json.name} version`, (_, version) => {
const latest = version?.trim();
if (!latest || compare(latest, package_json.version) !== 1) return;
stale_dir = autoupdate ? get_install_dir() : null;
// `dispose` covers a graceful shutdown, `exit` is the safety net for everything else. We only
// register it once we know we have something to delete to avoid piling up listeners.
if (stale_dir) process.once('exit', wipe);
setTimeout(() => {
ctx.client.tui.showToast({
body: {
title: 'Svelte: new plugin version available',
message: `${package_json.name}@${latest} is available (you are using ${package_json.version}).\n\n${
stale_dir
? 'It will be installed automatically the next time you start OpenCode.'
: 'Wipe the cache or update your OpenCode config to update.'
}`,
variant: 'warning',
duration: 7000,
},
});
}, 7000);
});
return async () => {
process.off('exit', wipe);
wipe();
};
}

View File

@@ -0,0 +1,35 @@
import { join } from 'node:path';
import { describe, expect, test } from 'vitest';
import { get_install_dir } from './update.js';
const cache_packages = join('/cache', 'packages');
/**
* @param {string} spec
*/
function plugin_dir(spec) {
return join(cache_packages, '@sveltejs', spec, 'node_modules', '@sveltejs', 'opencode');
}
describe('get_install_dir', () => {
test.each([
['an unpinned install', 'opencode'],
['the latest tag', 'opencode@latest'],
])('returns the cache directory for %s', (_, spec) => {
expect(get_install_dir(plugin_dir(spec))).toBe(join(cache_packages, '@sveltejs', spec));
});
test.each([
['an exact version', 'opencode@0.1.11'],
['an exact version with a v prefix', 'opencode@v0.1.11'],
['a range', 'opencode@^0.1.0'],
['an alternate dist-tag', 'opencode@beta'],
])('ignores %s', (_, spec) => {
expect(get_install_dir(plugin_dir(spec))).toBeNull();
});
test('ignores a matching layout outside the OpenCode package cache', () => {
const dir = join('/workspace', 'node_modules', '@sveltejs', 'opencode');
expect(get_install_dir(dir)).toBeNull();
});
});

View File

@@ -1,7 +1,7 @@
{
"name": "svelte",
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
"version": "1.0.4",
"version": "1.0.8",
"author": {
"name": "Svelte"
},

View File

@@ -4,13 +4,13 @@ description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when cre
permissionMode: acceptEdits
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## Available MCP Tools
## Available MCP tools
### 1. list-sections
@@ -36,30 +36,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
### 1. Gather context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
### 2. Read the target file
Read the file to understand the current implementation.
### 3. Make Changes
### 3. Make changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
### 4. Validate changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
### 5. Fix any issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
## Output format
After completing your work, provide:

View File

@@ -3,13 +3,11 @@ name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]

View File

@@ -35,9 +35,9 @@ If the derived expression is an object or array, it will be returned as-is — i
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
@@ -86,7 +86,7 @@ Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}

View File

@@ -1,4 +1,4 @@
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
<input bind:value={content} />
<button {@attach tooltip(content)}> Hover me </button>
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements (demo:
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
<input bind:value={content} />
<Button {@attach tooltip(content)}>Hover me</Button>
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#fromAction), allowing you to (for example) use them with components.

View File

@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true,
},
},
async: true
}
}
};
```
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
}
</script>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p><p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings/llms.txt#Client-warnings-await_waterfall) warning
## Indicating loading states
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect/llms.txt#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte/llms.txt#settled) to get a promise that resolves when the current update is complete:
```js
import { tick, settled } from 'svelte';
@@ -103,7 +110,7 @@ async function onclick() {
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
## Server-side rendering
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte/llms.txt#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}>open menu</button
>
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => (open = false)} />
<Menu onclose={() => open = false} />
{/if}
```

View File

@@ -3,13 +3,19 @@
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
```svelte
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
```
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
```svelte
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
```
> [!NOTE]

View File

@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
```svelte
<script>
import { getUser } from 'my-database-library';
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
};
});
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce },
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
```js
const { head, body, hashes } = await render(App, {
csp: { hash: true },
csp: { hash: true }
});
```
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
```js
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
);
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

View File

@@ -1,8 +1,11 @@
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>

View File

@@ -1,4 +1,4 @@
To render a [snippet](snippet), use a `{@render ...}` tag.
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
```svelte
{#snippet sum(a, b)}
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
{@render children?.()}
```
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
```svelte
{#if children}

View File

@@ -8,7 +8,7 @@
{#snippet name(param1, param2, paramN)}...{/snippet}
```
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
```svelte
{#each images as image}
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
```svelte
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other (demo:
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
### Optional snippet props
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{@render children?.()}
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{#if children}
{@render children()}
{@render children()}
{:else}
fallback content
fallback content
{/if}
```
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
let {
data,
children,
row,
row
}: {
data: T[];
children: Snippet;
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer
## Programmatic snippets
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte/llms.txt#createRawSnippet) API. This is intended for advanced use cases.
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

View File

@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
are active, and the returned teardown function will only be called when all effects are destroyed.
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity/llms.txt#MediaQuery):
```js
// @errors: 7031

View File

@@ -1,7 +1,7 @@
{
"name": "svelte",
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
"version": "1.0.4",
"version": "1.0.8",
"author": {
"name": "Svelte"
},

View File

@@ -3,13 +3,13 @@ name: svelte-file-editor
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## Available MCP Tools
## Available MCP tools
### 1. list-sections
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
### 1. Gather context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
### 2. Read the target file
Read the file to understand the current implementation.
### 3. Make Changes
### 3. Make changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
### 4. Validate changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
### 5. Fix any issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
## Output format
After completing your work, provide:

View File

@@ -3,13 +3,11 @@ name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]

View File

@@ -35,9 +35,9 @@ If the derived expression is an object or array, it will be returned as-is — i
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
@@ -86,7 +86,7 @@ Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}

View File

@@ -1,4 +1,4 @@
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
<input bind:value={content} />
<button {@attach tooltip(content)}> Hover me </button>
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements (demo:
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
<input bind:value={content} />
<Button {@attach tooltip(content)}>Hover me</Button>
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#fromAction), allowing you to (for example) use them with components.

View File

@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true,
},
},
async: true
}
}
};
```
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
}
</script>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p><p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings/llms.txt#Client-warnings-await_waterfall) warning
## Indicating loading states
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect/llms.txt#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte/llms.txt#settled) to get a promise that resolves when the current update is complete:
```js
import { tick, settled } from 'svelte';
@@ -103,7 +110,7 @@ async function onclick() {
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
## Server-side rendering
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte/llms.txt#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}>open menu</button
>
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => (open = false)} />
<Menu onclose={() => open = false} />
{/if}
```

View File

@@ -3,13 +3,19 @@
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
```svelte
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
```
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
```svelte
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
```
> [!NOTE]

View File

@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
```svelte
<script>
import { getUser } from 'my-database-library';
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
};
});
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce },
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
```js
const { head, body, hashes } = await render(App, {
csp: { hash: true },
csp: { hash: true }
});
```
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
```js
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
);
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

View File

@@ -1,8 +1,11 @@
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>

View File

@@ -1,4 +1,4 @@
To render a [snippet](snippet), use a `{@render ...}` tag.
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
```svelte
{#snippet sum(a, b)}
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
{@render children?.()}
```
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
```svelte
{#if children}

View File

@@ -8,7 +8,7 @@
{#snippet name(param1, param2, paramN)}...{/snippet}
```
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
```svelte
{#each images as image}
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
```svelte
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other (demo:
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
### Optional snippet props
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{@render children?.()}
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{#if children}
{@render children()}
{@render children()}
{:else}
fallback content
fallback content
{/if}
```
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
let {
data,
children,
row,
row
}: {
data: T[];
children: Snippet;
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer
## Programmatic snippets
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte/llms.txt#createRawSnippet) API. This is intended for advanced use cases.
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

View File

@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
are active, and the returned teardown function will only be called when all effects are destroyed.
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity/llms.txt#MediaQuery):
```js
// @errors: 7031

76
pnpm-lock.yaml generated
View File

@@ -42,7 +42,7 @@ catalogs:
version: 1.5.0
eslint-plugin-svelte:
specifier: ^3.19.0
version: 3.14.0
version: 3.19.0
globals:
specifier: ^17.0.0
version: 17.2.0
@@ -107,12 +107,12 @@ catalogs:
specifier: ^1.19.3
version: 1.19.3
tooling:
'@changesets/changelog-github':
specifier: 1.0.0-next.6
version: 1.0.0-next.6
'@changesets/cli':
specifier: ^2.29.7
version: 2.29.8
'@svitejs/changesets-changelog-github-compact':
specifier: ^1.2.0
version: 1.2.0
'@types/estree':
specifier: ^1.0.8
version: 1.0.8
@@ -149,6 +149,9 @@ catalogs:
valibot:
specifier: ^1.2.0
version: 1.2.0
verkit:
specifier: ^0.1.2
version: 0.1.2
vite:
specifier: ^7.0.4
version: 7.3.1
@@ -166,6 +169,9 @@ importers:
.:
devDependencies:
'@changesets/changelog-github':
specifier: catalog:tooling
version: 1.0.0-next.6
'@changesets/cli':
specifier: catalog:tooling
version: 2.29.8(@types/node@24.10.9)
@@ -181,9 +187,6 @@ importers:
'@sveltejs/adapter-vercel':
specifier: catalog:svelte
version: 6.3.1(@sveltejs/kit@2.50.1(@sveltejs/vite-plugin-svelte@6.2.4(svelte@5.56.1(@typescript-eslint/types@8.54.0))(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)))(svelte@5.56.1(@typescript-eslint/types@8.54.0))(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)))(rollup@4.57.0)
'@svitejs/changesets-changelog-github-compact':
specifier: catalog:tooling
version: 1.2.0
eslint:
specifier: catalog:lint
version: 9.39.2
@@ -412,6 +415,9 @@ importers:
valibot:
specifier: catalog:tooling
version: 1.2.0(typescript@5.9.3)
verkit:
specifier: catalog:tooling
version: 0.1.2
devDependencies:
'@opencode-ai/plugin':
specifier: catalog:opencode
@@ -605,6 +611,10 @@ packages:
'@changesets/changelog-git@0.2.1':
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
'@changesets/changelog-github@1.0.0-next.6':
resolution: {integrity: sha512-0ShCWgNt50xP0mryAYT8wtSkMUinG8uhxXgCgwHZ4UvJnR2f4ns8OsGAxYu8FIds7kHFdLOz7qX4YQ6AX4tVUA==}
engines: {node: ^22.11 || ^24 || >=26}
'@changesets/cli@2.29.8':
resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==}
hasBin: true
@@ -618,8 +628,9 @@ packages:
'@changesets/get-dependents-graph@2.1.3':
resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
'@changesets/get-github-info@0.6.0':
resolution: {integrity: sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==}
'@changesets/get-github-info@1.0.0-next.4':
resolution: {integrity: sha512-Bosh+XOoFvLMzAj301tg6phbrEggBtvEccrnceEvYsKSM7PjcIa32Fy22SU5g+501HZywkdwcAlybdWF+e/zzw==}
engines: {node: ^22.11 || ^24 || >=26}
'@changesets/get-release-plan@4.0.14':
resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==}
@@ -651,6 +662,10 @@ packages:
'@changesets/types@6.1.0':
resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
'@changesets/types@7.0.0-next.7':
resolution: {integrity: sha512-1XyshLw+lRCg2DWxi1Qt3hbezjBostHzXvGXVgSzAeZ+fL+r2ikcMbpaQ98D9ivwKhYFf1FBbKbEc+XsBZsIJQ==}
engines: {node: ^22.11 || ^24 || >=26}
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
@@ -2053,10 +2068,6 @@ packages:
svelte: ^5.0.0
vite: ^6.3.0 || ^7.0.0
'@svitejs/changesets-changelog-github-compact@1.2.0':
resolution: {integrity: sha512-08eKiDAjj4zLug1taXSIJ0kGL5cawjVCyJkBb6EWSg5fEPX6L+Wtr0CH2If4j5KYylz85iaZiFlUItvgJvll5g==}
engines: {node: ^14.13.1 || ^16.0.0 || >=18}
'@tmcp/adapter-valibot@0.1.5':
resolution: {integrity: sha512-9P2wrVYPngemNK0UvPb/opC722/jfd09QxXmme1TRp/wPsl98vpSk/MXt24BCMqBRv4Dvs0xxJH4KHDcjXW52Q==}
peerDependencies:
@@ -2611,8 +2622,8 @@ packages:
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
engines: {node: '>= 0.4'}
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
dataloader@2.2.3:
resolution: {integrity: sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==}
debug@3.2.7:
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
@@ -2698,10 +2709,6 @@ packages:
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
engines: {node: '>=0.10.0'}
dotenv@16.6.1:
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
engines: {node: '>=12'}
dotenv@17.2.3:
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
engines: {node: '>=12'}
@@ -4516,6 +4523,10 @@ packages:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
verkit@0.1.2:
resolution: {integrity: sha512-WqkT8n3hqizuCu71W3bUzf5fjBmkbXcudsehe/NbxA8PgqoKnSOY5K0Ba2ckg1qaRaSpSz7as/n9K1R9JXjQKg==}
engines: {node: '>=18.12.0'}
vite-plugin-devtools-json@1.0.0:
resolution: {integrity: sha512-MobvwqX76Vqt/O4AbnNMNWoXWGrKUqZbphCUle/J2KXH82yKQiunOeKnz/nqEPosPsoWWPP9FtNuPBSYpiiwkw==}
peerDependencies:
@@ -4989,6 +5000,11 @@ snapshots:
dependencies:
'@changesets/types': 6.1.0
'@changesets/changelog-github@1.0.0-next.6':
dependencies:
'@changesets/get-github-info': 1.0.0-next.4
'@changesets/types': 7.0.0-next.7
'@changesets/cli@2.29.8(@types/node@24.10.9)':
dependencies:
'@changesets/apply-release-plan': 7.0.14
@@ -5043,12 +5059,9 @@ snapshots:
picocolors: 1.1.1
semver: 7.7.3
'@changesets/get-github-info@0.6.0':
'@changesets/get-github-info@1.0.0-next.4':
dependencies:
dataloader: 1.4.0
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
dataloader: 2.2.3
'@changesets/get-release-plan@4.0.14':
dependencies:
@@ -5104,6 +5117,8 @@ snapshots:
'@changesets/types@6.1.0': {}
'@changesets/types@7.0.0-next.7': {}
'@changesets/write@0.4.0':
dependencies:
'@changesets/types': 6.1.0
@@ -6280,13 +6295,6 @@ snapshots:
vite: 7.3.1(@types/node@24.10.9)(yaml@2.9.0)
vitefu: 1.1.1(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0))
'@svitejs/changesets-changelog-github-compact@1.2.0':
dependencies:
'@changesets/get-github-info': 0.6.0
dotenv: 16.6.1
transitivePeerDependencies:
- encoding
'@tmcp/adapter-valibot@0.1.5(tmcp@1.19.3(typescript@5.9.3))(valibot@1.2.0(typescript@5.9.3))':
dependencies:
'@standard-schema/spec': 1.1.0
@@ -6868,7 +6876,7 @@ snapshots:
es-errors: 1.3.0
is-data-view: 1.0.2
dataloader@1.4.0: {}
dataloader@2.2.3: {}
debug@3.2.7:
dependencies:
@@ -6929,8 +6937,6 @@ snapshots:
dependencies:
esutils: 2.0.3
dotenv@16.6.1: {}
dotenv@17.2.3: {}
dts-resolver@2.1.3: {}
@@ -8918,6 +8924,8 @@ snapshots:
vary@1.1.2: {}
verkit@0.1.2: {}
vite-plugin-devtools-json@1.0.0(vite@7.3.1(@types/node@24.10.9)(yaml@2.9.0)):
dependencies:
uuid: 11.1.0

View File

@@ -8,12 +8,6 @@ catalogs:
'@anthropic-ai/sdk': ^0.71.0
'@mcp-ui/server': ^6.0.0
'@modelcontextprotocol/inspector': ^0.19.0
opencode:
'@opencode-ai/plugin': 1.17.9
'@opentui/core': ^0.4.3
'@opentui/keymap': ^0.4.3
'@opentui/solid': ^0.4.3
solid-js: 1.9.12
lint:
'@eslint/compat': ^2.0.0
'@eslint/js': ^9.36.0
@@ -29,6 +23,12 @@ catalogs:
prettier-plugin-svelte: ^3.3.3
svelte-eslint-parser: ^1.7.1
typescript-eslint: ^8.44.0
opencode:
'@opencode-ai/plugin': 1.17.9
'@opentui/core': ^0.4.3
'@opentui/keymap': ^0.4.3
'@opentui/solid': ^0.4.3
solid-js: 1.9.12
svelte:
'@sveltejs/adapter-vercel': ^6.0.0
'@sveltejs/kit': ^2.42.2
@@ -42,8 +42,8 @@ catalogs:
'@tmcp/transport-stdio': ^0.4.2
tmcp: ^1.19.3
tooling:
'@changesets/changelog-github': 1.0.0-next.6
'@changesets/cli': ^2.29.7
'@svitejs/changesets-changelog-github-compact': ^1.2.0
'@types/estree': ^1.0.8
'@types/node': ^24.3.1
'@valibot/to-json-schema': ^1.5.0
@@ -56,6 +56,7 @@ catalogs:
tsdown: ^0.20.0
typescript: ^5.0.0
valibot: ^1.2.0
verkit: ^0.1.2
vite: ^7.0.4
vite-plugin-devtools-json: ^1.0.0
vitest: ^4.0.0

View File

@@ -103,7 +103,33 @@ function remove_frontmatter_unneeded_fields(content: string) {
function derive_name(link: string) {
const without_hash = link.split('#')[0]!;
const segments = without_hash.split('/').filter(Boolean);
return segments[segments.length - 1] ?? 'reference';
const name = segments[segments.length - 1] ?? 'reference';
const safe_name = name
.normalize('NFKD')
.replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-zA-Z0-9_-]+/g, '-')
.replace(/^-+|-+$/g, '');
return safe_name || 'reference';
}
/**
* Makes links copied into reference files point back to their pages on svelte.dev.
*/
export function resolve_reference_links(content: string, repo: string) {
return content.replace(
/\[([^\]]*)\]\((?![a-z][a-z\d+.-]*:|#|\/\/)([^)]+)\)/gi,
(full_match, text: string, href: string) => {
const url = new URL(
href.startsWith('/')
? `https://svelte.dev${href}`
: `https://svelte.dev/docs/${repo}/${href}`,
);
url.pathname += '/llms.txt';
return `[${text}](${url})`;
},
);
}
const content = remove_llm_ignore_blocks(
@@ -203,8 +229,13 @@ for (const link of links) {
const ref_filename = `${name}.md`;
const ref_path = path.join(references_dir, ref_filename);
const reference_repo = link.is_absolute_docs ? link.clean_path.split('/')[2]! : repo;
const reference_content = resolve_reference_links(
remove_llm_ignore_blocks(remove_cut_preambles(fetched_content)),
reference_repo,
);
await fs.writeFile(ref_path, remove_llm_ignore_blocks(remove_cut_preambles(fetched_content)));
await fs.writeFile(ref_path, reference_content);
console.log(` Saved: references/${ref_filename}`);
// Replace the link in the markdown

View File

@@ -68,7 +68,7 @@ function parse_agent_md(content: string, file_path: string): AgentData | null {
}
/**
* Generate agents.ts module from tools/agents/*.md files
* Generate agents.js module from tools/agents/*.md files
*/
async function sync_agents() {
const agents_dir = path.join(TOOLS_DIR, 'agents');
@@ -96,14 +96,14 @@ async function sync_agents() {
),
null,
'\t',
)} as const;`,
)};`,
'',
].join('\n');
const dest = path.join(OPENCODE_PKG_DIR, 'agents.ts');
const dest = path.join(OPENCODE_PKG_DIR, 'agents.js');
await fs.writeFile(dest, output);
console.log(`Generated agents.ts with ${agents.length} agent(s)`);
console.log(`Generated agents.js with ${agents.length} agent(s)`);
}
await sync_skills();

View File

@@ -3,13 +3,13 @@ name: svelte-file-editor
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
---
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
## Available MCP Tools
## Available MCP tools
### 1. list-sections
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
When invoked to work on a Svelte file:
### 1. Gather Context (if needed)
### 1. Gather context (if needed)
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
1. Call `list-sections` to see available documentation
2. Call `get-documentation` with relevant section names
### 2. Read the Target File
### 2. Read the target file
Read the file to understand the current implementation.
### 3. Make Changes
### 3. Make changes
Apply edits following Svelte 5 best practices:
### 4. Validate Changes
### 4. Validate changes
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
### 5. Fix Any Issues
### 5. Fix any issues
If the autofixer reports problems, fix them and re-validate until no issues remain.
## Output Format
## Output format
After completing your work, provide:

View File

@@ -3,13 +3,11 @@ name: svelte-code-writer
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
---
# Svelte 5 Code Writer
## CLI Tools
## CLI tools
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
### List Documentation Sections
### List documentation sections
```bash
npx @sveltejs/mcp list-sections
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
### Get Documentation
### Get documentation
```bash
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
```
### Svelte Autofixer
### Svelte autofixer
```bash
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]

View File

@@ -35,9 +35,9 @@ If the derived expression is an object or array, it will be returned as-is — i
Effects are an escape hatch and should mostly be avoided. In particular, avoid updating state inside effects.
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/@attach.md)
- If you need to sync state to an external library such as D3, it is often neater to use [`{@attach ...}`](references/attach.md)
- If you need to run some code in response to user interaction, put the code directly in an event handler or use a [function binding](references/bind.md) as appropriate
- If you need to log values for debugging purposes, use [`$inspect`](references/$inspect.md)
- If you need to log values for debugging purposes, use [`$inspect`](references/inspect.md)
- If you need to observe something external to Svelte, use [`createSubscriber`](references/svelte-reactivity.md)
Never wrap the contents of an effect in `if (browser) {...}` or similar — effects do not run on the server.
@@ -86,7 +86,7 @@ Avoid using `onMount` or `$effect` for this.
## Snippets
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/@render.md) tag, or passed to components as props. They must be declared within the template.
[Snippets](references/snippet.md) are a way to define reusable chunks of markup that can be instantiated with the [`{@render ...}`](references/render.md) tag, or passed to components as props. They must be declared within the template.
```svelte
{#snippet greeting(name)}

View File

@@ -1,4 +1,4 @@
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
<input bind:value={content} />
<button {@attach tooltip(content)}> Hover me </button>
<button {@attach tooltip(content)}>
Hover me
</button>
```
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
## Inline attachments
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
## Passing attachments to components
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
This allows you to create _wrapper components_ that augment elements (demo:
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
<input bind:value={content} />
<Button {@attach tooltip(content)}>Hover me</Button>
<Button {@attach tooltip(content)}>
Hover me
</Button>
```
## Controlling when attachments re-run
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
```js
// @errors: 7006 2304 2552
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
## Creating attachments programmatically
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#createAttachmentKey).
## Converting actions to attachments
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments/llms.txt#fromAction), allowing you to (for example) use them with components.

View File

@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
- inside `$derived(...)` declarations
- inside your markup
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
```js
/// file: svelte.config.js
export default {
compilerOptions: {
experimental: {
async: true,
},
},
async: true
}
}
};
```
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
<!-- codeblock:start {"title":"Synchronized updates"} -->
```svelte
<!--- file: App.svelte --->
<script>
let a = $state(1);
let b = $state(2);
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
}
</script>
<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<input type="number" bind:value={a}>
<input type="number" bind:value={b}>
<p>{a} + {b} = {await add(a, b)}</p>
```
<!-- codeblock:end -->
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
```html
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
```svelte
<p>{await one()}</p><p>{await two()}</p>
<p>{await one(x)}</p>
<p>{await two(y)}</p>
```
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
```js
// these will run sequentially the first time,
// but will update independently
let a = $derived(await one());
let b = $derived(await two());
// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));
```
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings/llms.txt#Client-warnings-await_waterfall) warning
## Indicating loading states
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect/llms.txt#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte/llms.txt#settled) to get a promise that resolves when the current update is complete:
```js
import { tick, settled } from 'svelte';
@@ -103,7 +110,7 @@ async function onclick() {
## Error handling
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
## Server-side rendering
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
## Forking
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte/llms.txt#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
```svelte
<script>
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
// in case `pending` didn't exist
// (if it did, this is a no-op)
open = true;
}}>open menu</button
>
}}
>open menu</button>
{#if open}
<!-- any async work inside this component will start
as soon as the fork is created -->
<Menu onclose={() => (open = false)} />
<Menu onclose={() => open = false} />
{/if}
```

View File

@@ -3,13 +3,19 @@
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
```svelte
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
<input bind:value={
() => value,
(v) => value = v.toLowerCase()}
/>
```
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
```svelte
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
<div
bind:clientWidth={null, redraw}
bind:clientHeight={null, redraw}
>...</div>
```
> [!NOTE]

View File

@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
```svelte
<script>
import { getUser } from 'my-database-library';
import { getUser } from 'my-database-library';
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>
<h1>{user.name}</h1>
```
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
To fix the example above:
```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', () => getUser());
</script>
<h1>{user.name}</h1>
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
```svelte
<script>
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2),
};
});
import { hydratable } from 'svelte';
const promises = hydratable('random', () => {
return {
one: Promise.resolve(1),
two: Promise.resolve(2)
}
});
</script>
{await promises.one}
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
const nonce = crypto.randomUUID();
const { head, body } = await render(App, {
csp: { nonce },
csp: { nonce }
});
```
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
```js
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
response.headers.set(
'Content-Security-Policy',
`script-src 'nonce-${nonce}'`
);
```
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
```js
const { head, body, hashes } = await render(App, {
csp: { hash: true },
csp: { hash: true }
});
```
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
```js
response.headers.set(
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
);
'Content-Security-Policy',
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
);
```
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.

View File

@@ -1,8 +1,11 @@
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
<!-- codeblock:start {"title":"$inspect(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
let message = $state('hello');
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
<input bind:value={message} />
```
<!-- codeblock:end -->
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
## $inspect(...).with
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
```svelte
<!--- file: App.svelte --->
<script>
let count = $state(0);
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
<button onclick={() => count++}>Increment</button>
```
<!-- codeblock:end -->
## $inspect.trace(...)
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
```svelte
<script>

View File

@@ -1,4 +1,4 @@
To render a [snippet](snippet), use a `{@render ...}` tag.
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
```svelte
{#snippet sum(a, b)}
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
{@render children?.()}
```
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
```svelte
{#if children}

View File

@@ -8,7 +8,7 @@
{#snippet name(param1, param2, paramN)}...{/snippet}
```
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
```svelte
{#each images as image}
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
## Snippet scope
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
<!-- codeblock:start {"title":"Snippets"} -->
```svelte
<!--- file: App.svelte --->
<script>
let { message = `it's great to see you!` } = $props();
</script>
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render hello('bob')}
```
<!-- codeblock:end -->
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
```svelte
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
{@render x()}
```
Snippets can reference themselves and each other (demo:
Snippets can reference themselves and each other:
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
```svelte
<!--- file: App.svelte --->
{#snippet blastoff()}
<span>🚀</span>
{/snippet}
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
{@render countdown(10)}
```
<!-- codeblock:end -->
## Passing snippets to components
### Explicit props
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
<!-- codeblock:start {"title":"Explicit snippet props"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
<td>{d.qty * d.price}</td>
{/snippet}
<Table data={fruits} {header} {row} />
<Table data={fruits} +++{header} {row}+++ />
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
### Implicit props
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
<!-- codeblock:start {"title":"Implicit snippet props"} -->
```svelte
<!-- this is semantically the same as the above -->
<!--- file: App.svelte --->
<script>
import Table from './Table.svelte';
const fruits = [
{ name: 'apples', qty: 5, price: 2 },
{ name: 'bananas', qty: 10, price: 1 },
{ name: 'cherries', qty: 20, price: 0.5 }
];
</script>
<Table data={fruits}>
{#snippet header()}
<th>fruit</th>
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
</Table>
```
```svelte
<!--- file: Table.svelte --->
<script>
let { data, header, row } = $props();
</script>
<table>
{#if header}
<thead>
<tr>{@render header()}</tr>
</thead>
{/if}
<tbody>
{#each data as d}
<tr>{@render row(d)}</tr>
{/each}
</tbody>
</table>
<style>
table {
text-align: left;
border-spacing: 0;
}
tbody tr:nth-child(2n+1) {
background: ButtonFace;
}
table :global(th), table :global(td) {
padding: 0.5em;
}
</style>
```
<!-- codeblock:end -->
### Implicit `children` snippet
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import Button from './Button.svelte';
</script>
<Button>click me</Button>
```
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
<button>{@render children()}</button>
```
<!-- codeblock:end -->
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
### Optional snippet props
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{@render children?.()}
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
```svelte
<script>
let { children } = $props();
let { children } = $props();
</script>
{#if children}
{@render children()}
{@render children()}
{:else}
fallback content
fallback content
{/if}
```
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
let {
data,
children,
row,
row
}: {
data: T[];
children: Snippet;
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
## Exporting snippets
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
```svelte
<!--- file: App.svelte --->
<script>
import { add } from './snippets.svelte';
</script>
{@render add(1, 2)}
```
```svelte
<!--- file: snippets.svelte --->
<script module>
export { add };
</script>
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
{/snippet}
```
<!-- codeblock:end -->
> [!NOTE]
> This requires Svelte 5.5.0 or newer
## Programmatic snippets
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte/llms.txt#createRawSnippet) API. This is intended for advanced use cases.
## Snippets and slots
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.

View File

@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
are active, and the returned teardown function will only be called when all effects are destroyed.
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity/llms.txt#MediaQuery):
```js
// @errors: 7031