mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-04 03:19:38 +08:00
Compare commits
90 Commits
@sveltejs/
...
opencode-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9571736279 | ||
|
|
88cea9e539 | ||
|
|
fdb1bc7370 | ||
|
|
5b4d3aa68a | ||
|
|
04c52f2b72 | ||
|
|
0ea2a617ef | ||
|
|
484453e5f8 | ||
|
|
841af5b3a4 | ||
|
|
1ce957ac72 | ||
|
|
e429cd7839 | ||
|
|
2ded13539a | ||
|
|
96c50acae2 | ||
|
|
4920088e5c | ||
|
|
8557f0af6f | ||
|
|
972d0f0bb2 | ||
|
|
7bf364a9d5 | ||
|
|
d06d758b81 | ||
|
|
e5ce7437c4 | ||
|
|
2f422ee190 | ||
|
|
14f087cd7a | ||
|
|
1ef5ddf605 | ||
|
|
0e55ee792d | ||
|
|
84ec24b6f6 | ||
|
|
710cebe539 | ||
|
|
b2a380c4ce | ||
|
|
eef0a9b4d9 | ||
|
|
260b36e8af | ||
|
|
6df3ebe568 | ||
|
|
27a2fc5653 | ||
|
|
5cd99d8234 | ||
|
|
e9f19199cb | ||
|
|
480b46df0a | ||
|
|
5f5fb27977 | ||
|
|
29cfa77c39 | ||
|
|
249923b1e5 | ||
|
|
8c67cae90f | ||
|
|
4aad2e0cfe | ||
|
|
02935b00a6 | ||
|
|
1ea98adacd | ||
|
|
494409cc42 | ||
|
|
46d8f6cce8 | ||
|
|
8dc63dca08 | ||
|
|
19fedcd35f | ||
|
|
b4eb5cc960 | ||
|
|
6676fd8116 | ||
|
|
a755a33a5f | ||
|
|
556f96cfaf | ||
|
|
77a3340c2f | ||
|
|
9ac8fd51e7 | ||
|
|
c764308d79 | ||
|
|
01a7e6a8d3 | ||
|
|
d8ed686e3a | ||
|
|
6f0390d0a9 | ||
|
|
c2c1b3e5e7 | ||
|
|
cdfbb907b6 | ||
|
|
2eb2b18008 | ||
|
|
e8554f3d8f | ||
|
|
0213bd951f | ||
|
|
2245cb2dc9 | ||
|
|
2a5f7d6314 | ||
|
|
8518e627d3 | ||
|
|
9e824da9e2 | ||
|
|
b38f2c11da | ||
|
|
ea37c7120c | ||
|
|
099e939f79 | ||
|
|
69b36eefdc | ||
|
|
15ad554f53 | ||
|
|
74477448ce | ||
|
|
6f538265d1 | ||
|
|
71295bc11f | ||
|
|
b5040ff5cf | ||
|
|
45c961417f | ||
|
|
57e2d1def1 | ||
|
|
398a703580 | ||
|
|
75c802a115 | ||
|
|
53a634cdb0 | ||
|
|
bcdc33e7a5 | ||
|
|
825ae33427 | ||
|
|
ccf940cc45 | ||
|
|
b2f195fb7b | ||
|
|
d8e4b18bff | ||
|
|
6a2198b433 | ||
|
|
2ce60c6110 | ||
|
|
0f8987fdcf | ||
|
|
42911e2631 | ||
|
|
655eb85eba | ||
|
|
89403a7d0c | ||
|
|
3747623d55 | ||
|
|
c1f230455f | ||
|
|
9dfb4dedb4 |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/mcp" }],
|
||||
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["!@sveltejs/mcp"]
|
||||
"ignore": ["!@sveltejs/mcp", "!@sveltejs/opencode"]
|
||||
}
|
||||
|
||||
5
.changeset/fuzzy-dancers-deny.md
Normal file
5
.changeset/fuzzy-dancers-deny.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@sveltejs/opencode': patch
|
||||
---
|
||||
|
||||
fix: remove restrictive permissions from subagent
|
||||
22
.claude-plugin/marketplace.json
Normal file
22
.claude-plugin/marketplace.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "svelte",
|
||||
"owner": {
|
||||
"name": "Svelte"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "svelte",
|
||||
"source": "./plugins/claude/svelte",
|
||||
"description": "A plugin for all things Svelte development, MCP, skills, and more.",
|
||||
"lspServers": {
|
||||
"svelte": {
|
||||
"command": "svelteserver",
|
||||
"args": ["--stdio"],
|
||||
"extensionToLanguage": {
|
||||
".svelte": "svelte"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
.cursor-plugin/marketplace.json
Normal file
13
.cursor-plugin/marketplace.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "svelte",
|
||||
"owner": {
|
||||
"name": "Svelte"
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "svelte",
|
||||
"source": "./plugins/cursor/svelte",
|
||||
"description": "A plugin for all things Svelte development, MCP, skills, and more."
|
||||
}
|
||||
]
|
||||
}
|
||||
8
.github/workflows/check.yml
vendored
8
.github/workflows/check.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.26.0
|
||||
version: 10.28.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
@@ -32,6 +32,4 @@ jobs:
|
||||
- name: Run type check
|
||||
run: pnpm run check
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
DATABASE_TOKEN: dummy-key
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
|
||||
8
.github/workflows/lint.yml
vendored
8
.github/workflows/lint.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.26.0
|
||||
version: 10.28.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
@@ -32,6 +32,4 @@ jobs:
|
||||
- name: Run linting
|
||||
run: pnpm run lint
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
DATABASE_TOKEN: dummy-key
|
||||
|
||||
26
.github/workflows/publish-any-commit.yml
vendored
Normal file
26
.github/workflows/publish-any-commit.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Publish Any Commit
|
||||
on: [push, pull_request]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- run: corepack enable
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
# Opencode doesn't have a build step
|
||||
- name: Build Stdio
|
||||
run: pnpm --filter @sveltejs/mcp run build
|
||||
|
||||
- run: pnpm dlx pkg-pr-new publish --compact './packages/mcp-stdio' './packages/opencode' --pnpm
|
||||
6
.github/workflows/publish-mcp.yml
vendored
6
.github/workflows/publish-mcp.yml
vendored
@@ -21,11 +21,11 @@ jobs:
|
||||
MCP_KEY: ${{ secrets.MCP_KEY }}
|
||||
run: |
|
||||
NAME=mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz
|
||||
# Download MCP Publisher pinned to v1.3.10 using latest https for security and save it to a file named mcp-publisher.tar.gz
|
||||
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fL "https://github.com/modelcontextprotocol/registry/releases/download/v1.3.10/$NAME" -O
|
||||
# Download MCP Publisher pinned to v1.4.0 using latest https for security and save it to a file named mcp-publisher.tar.gz
|
||||
curl --proto '=https' --proto-redir '=https' --tlsv1.2 -fL "https://github.com/modelcontextprotocol/registry/releases/download/v1.4.0/$NAME" -O
|
||||
|
||||
# Verify the SHA256 checksum of the downloaded file
|
||||
sha256sum --ignore-missing -c ./checksums/registry_1.3.10_checksums.txt
|
||||
sha256sum --ignore-missing -c ./checksums/registry_1.4.0_checksums.txt
|
||||
|
||||
# Extract the tarball
|
||||
mkdir tmp
|
||||
|
||||
66
.github/workflows/release-svelte-skill.yml
vendored
Normal file
66
.github/workflows/release-svelte-skill.yml
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
name: Release Svelte Skills
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'tools/skills/**'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
detect-skills:
|
||||
permissions:
|
||||
contents: read
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Detect changed skills
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
skills: ${{ steps.find-skills.outputs.skills }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Find all skills
|
||||
id: find-skills
|
||||
run: |
|
||||
skills=$(ls -d tools/skills/*/ | xargs -I {} basename {} | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
||||
echo "skills=$skills" >> $GITHUB_OUTPUT
|
||||
|
||||
release:
|
||||
needs: detect-skills
|
||||
if: needs.detect-skills.outputs.skills != '[]'
|
||||
permissions:
|
||||
contents: write
|
||||
name: Release ${{ matrix.skill }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
skill: ${{ fromJson(needs.detect-skills.outputs.skills) }}
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Get version from date
|
||||
id: version
|
||||
run: echo "version=$(date +'%Y.%m.%d-%H%M%S')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create zip
|
||||
run: |
|
||||
cd tools/skills
|
||||
zip -r ${{ matrix.skill }}.zip ${{ matrix.skill }}/
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ matrix.skill }}-v${{ steps.version.outputs.version }}
|
||||
name: ${{ matrix.skill }} v${{ steps.version.outputs.version }}
|
||||
body: |
|
||||
Automated release of the ${{ matrix.skill }} skill.
|
||||
|
||||
This release was triggered by changes to the `tools/skills/${{ matrix.skill }}/` directory.
|
||||
files: tools/skills/${{ matrix.skill }}.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -13,11 +13,11 @@ jobs:
|
||||
id-token: write # OpenID Connect token needed for provenance
|
||||
pull-requests: write # to create pull request (changesets/action)
|
||||
# prevents this action from running on forks
|
||||
if: github.repository == 'sveltejs/mcp'
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Release
|
||||
runs-on: ${{ matrix.os }}
|
||||
outputs:
|
||||
published: ${{ steps.changesets.outputs.published }}
|
||||
publishedPackages: ${{ steps.changesets.outputs.publishedPackages }}
|
||||
strategy:
|
||||
matrix:
|
||||
# pseudo-matrix for convenience, NEVER use more than a single combination
|
||||
@@ -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@v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
@@ -39,10 +39,11 @@ 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@v6
|
||||
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
- name: install
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
- name: build
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
- name: Create Release Pull Request or Publish to npm
|
||||
id: changesets
|
||||
# pinned for security, always review third party action code before updating
|
||||
uses: changesets/action@e0145edc7d9d8679003495b11f87bd8ef63c0cba # v1.5.3
|
||||
uses: changesets/action@c48e67d110a68bc90ccf1098e9646092baacaa87 # v1.6.0
|
||||
with:
|
||||
# This expects you to have a script called changeset:version version that calls changeset version and updated what it needs to be updated
|
||||
version: pnpm changeset:version
|
||||
@@ -63,7 +64,7 @@ jobs:
|
||||
|
||||
publish-mcp:
|
||||
needs: release
|
||||
if: needs.release.outputs.published == 'true'
|
||||
if: contains(needs.release.outputs.publishedPackages, '"@sveltejs/mcp"')
|
||||
uses: ./.github/workflows/publish-mcp.yml
|
||||
secrets:
|
||||
MCP_KEY: ${{ secrets.MCP_KEY }}
|
||||
|
||||
116
.github/workflows/sync-docs-skills.yml
vendored
Normal file
116
.github/workflows/sync-docs-skills.yml
vendored
Normal file
@@ -0,0 +1,116 @@
|
||||
name: Sync Skills
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
sync-skills:
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Sync skills from svelte.dev
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
|
||||
- name: Install pnpm
|
||||
shell: bash
|
||||
run: |
|
||||
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"
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
|
||||
- name: Clone svelte.dev
|
||||
run: git clone --depth 2 https://github.com/sveltejs/svelte.dev.git "${{ runner.temp }}/svelte.dev"
|
||||
|
||||
- name: Discover changed skill files
|
||||
id: discover
|
||||
env:
|
||||
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
|
||||
run: |
|
||||
skill_files=$(git -C "$SVELTE_DEV_ROOT" diff --name-only --diff-filter=ACMR HEAD~1 HEAD | grep '^apps/svelte.dev/content/docs/.*\.md$' | xargs -I{} grep -l '^skill: *true' "$SVELTE_DEV_ROOT/{}" || true)
|
||||
echo "skill_files=$skill_files" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Sync skills
|
||||
if: steps.discover.outputs.skill_files != ''
|
||||
env:
|
||||
SVELTE_DEV_ROOT: ${{ runner.temp }}/svelte.dev
|
||||
DOCS_PREFIX: apps/svelte.dev/content/docs/
|
||||
run: |
|
||||
for full_path in ${{ steps.discover.outputs.skill_files }}; do
|
||||
file="${full_path#$SVELTE_DEV_ROOT/}"
|
||||
name=$(grep '^name: ' "$full_path" | head -1 | sed 's/^name: *//')
|
||||
repo="${file#$DOCS_PREFIX}"
|
||||
repo="${repo#/}"
|
||||
repo="${repo%%/*}"
|
||||
|
||||
output_dir="tools/skills/$name"
|
||||
rm -rf "$output_dir"
|
||||
mkdir -p "$output_dir"
|
||||
|
||||
pnpm resolve-references --file "$full_path" --repo "$repo" --output "$output_dir"
|
||||
done
|
||||
|
||||
- name: Sync plugins
|
||||
if: steps.discover.outputs.skill_files != ''
|
||||
run: |
|
||||
pnpm sync-claude-plugin
|
||||
pnpm sync-cursor-plugin
|
||||
pnpm sync-opencode-plugin
|
||||
pnpm generate-skill-docs
|
||||
pnpm bump-plugin-versions
|
||||
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git diff --exit-code -- tools/skills/ plugins/ packages/opencode/ documentation/docs/ || echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.git-check.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync skills from svelte.dev'
|
||||
branch: chore/sync-skills
|
||||
delete-branch: true
|
||||
title: 'chore: sync skills from svelte.dev'
|
||||
body: |
|
||||
## Summary
|
||||
Automatically synced skill markdown from `sveltejs/svelte.dev` into `tools/skills/`.
|
||||
|
||||
## Changes
|
||||
- Cloned `sveltejs/svelte.dev`
|
||||
- Filtered markdown files with `skill: true` frontmatter
|
||||
- Rebuilt synced skill folders with `scripts/resolve-references.ts`
|
||||
- Synced `plugins/claude/svelte/` (skills, agents)
|
||||
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
|
||||
- Synced `packages/opencode/` (skills, instructions)
|
||||
- Updated documentation
|
||||
|
||||
## Generated by
|
||||
GitHub Action: Sync Skills
|
||||
labels: |
|
||||
chore
|
||||
automated
|
||||
95
.github/workflows/sync-plugins.yml
vendored
Normal file
95
.github/workflows/sync-plugins.yml
vendored
Normal file
@@ -0,0 +1,95 @@
|
||||
name: Sync Plugins
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'tools/**'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
sync-plugins:
|
||||
# prevents this action from running on forks
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Sync Plugins from tools/
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
|
||||
- name: Install pnpm
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
|
||||
- name: Sync plugins
|
||||
run: pnpm sync-plugins
|
||||
|
||||
- name: Generate skills documentation
|
||||
run: pnpm generate-skill-docs
|
||||
|
||||
- name: Generate subagent documentation
|
||||
run: pnpm generate-subagent-docs
|
||||
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git diff --exit-code \
|
||||
plugins/claude/svelte/ \
|
||||
plugins/cursor/svelte/ \
|
||||
packages/opencode/skills/ \
|
||||
packages/opencode/instructions/ \
|
||||
packages/opencode/schema.json \
|
||||
documentation/docs/ \
|
||||
|| echo "changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.git-check.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'chore: sync plugins from tools/'
|
||||
branch: chore/sync-plugins
|
||||
delete-branch: true
|
||||
title: 'chore: sync plugins from tools/'
|
||||
body: |
|
||||
## Summary
|
||||
Automatically synced all plugins from the `tools/` source of truth.
|
||||
|
||||
This PR was triggered by changes to `tools/**`.
|
||||
|
||||
## Changes
|
||||
- Synced `plugins/claude/svelte/` (skills, agents with `permissionMode`)
|
||||
- Synced `plugins/cursor/svelte/` (skills, agents, rules)
|
||||
- Synced `packages/opencode/` (skills, instructions, schema)
|
||||
- Updated documentation
|
||||
|
||||
## Generated by
|
||||
GitHub Action: Sync Plugins
|
||||
labels: |
|
||||
chore
|
||||
automated
|
||||
10
.github/workflows/test.yml
vendored
10
.github/workflows/test.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
uses: pnpm/action-setup@v5
|
||||
with:
|
||||
version: 10.26.0
|
||||
version: 10.28.2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: 'pnpm'
|
||||
@@ -32,13 +32,9 @@ jobs:
|
||||
- name: Build project
|
||||
run: pnpm run build
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
DATABASE_TOKEN: dummy-key
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm run test
|
||||
env:
|
||||
DATABASE_URL: file:test.db
|
||||
VOYAGE_API_KEY: dummy-key
|
||||
DATABASE_TOKEN: dummy-key
|
||||
|
||||
78
.github/workflows/update-opencode-jsonschema.yml
vendored
Normal file
78
.github/workflows/update-opencode-jsonschema.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Update OpenCode JSON Schema
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/opencode/config.ts'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
update-docs:
|
||||
# prevents this action from running on forks
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Update OpenCode JSON Schema
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
|
||||
- name: Install pnpm
|
||||
shell: bash
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
|
||||
- name: Generate opencode JSON schema
|
||||
run: pnpm generate-opencode-jsonschema
|
||||
|
||||
- name: Check for changes
|
||||
id: git-check
|
||||
run: |
|
||||
git diff --exit-code packages/opencode/schema.json || echo "changed=true" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.git-check.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'docs: update opencode JSON schema'
|
||||
branch: docs/update-opencode-jsonschema
|
||||
delete-branch: true
|
||||
title: 'docs: update opencode JSON schema'
|
||||
body: |
|
||||
## Summary
|
||||
Automatically generated update for OpenCode JSON schema.
|
||||
|
||||
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
|
||||
|
||||
## Changes
|
||||
- Updated `packages/opencode/schema.json` with latest JSON schema
|
||||
|
||||
## Generated by
|
||||
GitHub Action: Update OpenCode JSON Schema
|
||||
labels: |
|
||||
automated
|
||||
9
.github/workflows/update-prompt-docs.yml
vendored
9
.github/workflows/update-prompt-docs.yml
vendored
@@ -14,7 +14,7 @@ permissions:
|
||||
jobs:
|
||||
update-docs:
|
||||
# prevents this action from running on forks
|
||||
if: github.repository == 'sveltejs/mcp'
|
||||
if: github.repository == 'sveltejs/ai-tools'
|
||||
name: Update Prompt Documentation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: false # pnpm is not installed yet
|
||||
@@ -37,10 +37,11 @@ jobs:
|
||||
npm i -g pnpm@$PNPM_VER
|
||||
|
||||
- name: Setup Node.js with pnpm cache
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||
with:
|
||||
node-version: 24
|
||||
package-manager-cache: true # caches pnpm via packageManager field in package.json
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts
|
||||
@@ -55,7 +56,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
if: steps.git-check.outputs.changed == 'true'
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: 'docs: update prompts documentation'
|
||||
|
||||
@@ -12,4 +12,6 @@ bun.lockb
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
.changeset/
|
||||
.changeset/
|
||||
|
||||
/packages/opencode/schema.json
|
||||
@@ -10,6 +10,12 @@
|
||||
"options": {
|
||||
"parser": "svelte"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": "**/references/*.md",
|
||||
"options": {
|
||||
"embeddedLanguageFormatting": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -83,7 +83,6 @@ Located in `src/lib/server/analyze/`:
|
||||
|
||||
Required environment variables:
|
||||
|
||||
- `DATABASE_URL`: SQLite database path (default: `file:test.db`)
|
||||
- `VOYAGE_API_KEY`: API key for embeddings support (optional)
|
||||
|
||||
When connected to the svelte-llm MCP server, you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 [Svelte Contributors](https://github.com/sveltejs/ai-tools/graphs/contributors)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,3 +1 @@
|
||||
DATABASE_URL=file:test.db
|
||||
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
|
||||
VOYAGE_API_KEY=your_actual_api_key_here
|
||||
@@ -1,12 +0,0 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
dialect: 'turso',
|
||||
dbCredentials: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_TOKEN },
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
@@ -23,10 +23,6 @@
|
||||
"test:unit": "vitest",
|
||||
"test": "npm run test:unit -- --run",
|
||||
"test:watch": "npm run test:unit -- --watch",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"inspect": "pnpm mcp-inspector"
|
||||
},
|
||||
"keywords": [
|
||||
@@ -39,15 +35,12 @@
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "catalog:lint",
|
||||
"@eslint/js": "catalog:lint",
|
||||
"@libsql/client": "catalog:orm",
|
||||
"@modelcontextprotocol/inspector": "catalog:ai",
|
||||
"@sveltejs/adapter-vercel": "catalog:svelte",
|
||||
"@sveltejs/kit": "catalog:svelte",
|
||||
"@sveltejs/vite-plugin-svelte": "catalog:svelte",
|
||||
"@types/node": "catalog:tooling",
|
||||
"@typescript-eslint/parser": "catalog:lint",
|
||||
"drizzle-kit": "catalog:orm",
|
||||
"drizzle-orm": "catalog:orm",
|
||||
"eslint-config-prettier": "catalog:lint",
|
||||
"eslint-plugin-svelte": "catalog:lint",
|
||||
"globals": "catalog:lint",
|
||||
@@ -62,7 +55,6 @@
|
||||
"vitest": "catalog:tooling"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sveltejs/mcp-schema": "workspace:^",
|
||||
"@sveltejs/mcp-server": "workspace:^",
|
||||
"@tmcp/transport-http": "catalog:tmcp",
|
||||
"@vercel/analytics": "catalog:tooling",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { dev } from '$app/environment';
|
||||
import { http_transport } from '$lib/mcp/index.js';
|
||||
import { db } from '$lib/server/db/index.js';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { track } from '@vercel/analytics/server';
|
||||
|
||||
@@ -17,7 +16,6 @@ export async function handle({ event, resolve }) {
|
||||
}
|
||||
}
|
||||
const mcp_response = await http_transport.respond(event.request, {
|
||||
db,
|
||||
// only add analytics in production
|
||||
track: dev
|
||||
? undefined
|
||||
@@ -25,22 +23,5 @@ export async function handle({ event, resolve }) {
|
||||
await track(event, { session_id, ...(extra ? { extra } : {}) });
|
||||
},
|
||||
});
|
||||
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for
|
||||
// the moment we are not sending back any notifications (logs, or list changed notifications)
|
||||
// so it's a waste of resources to keep a connection open that will error
|
||||
// after 5 minutes making the logs dirty. For this reason if we have a response from
|
||||
// the MCP server and it's a GET request we just return an empty response (it has to be
|
||||
// 200 or the MCP client will complain)
|
||||
if (mcp_response && event.request.method === 'GET') {
|
||||
try {
|
||||
return mcp_response;
|
||||
} finally {
|
||||
try {
|
||||
await mcp_response.body?.cancel();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
return mcp_response ?? resolve(event);
|
||||
}
|
||||
|
||||
@@ -4,4 +4,9 @@ import { HttpTransport } from '@tmcp/transport-http';
|
||||
export const http_transport = new HttpTransport(server, {
|
||||
cors: true,
|
||||
path: '/mcp',
|
||||
// we are deploying on vercel the SSE connection will timeout after 5 minutes...for
|
||||
// the moment we are not sending back any notifications (logs, or list changed notifications)
|
||||
// so it's a waste of resources to keep a connection open that will error
|
||||
// after 5 minutes making the logs dirty.
|
||||
disableSse: true,
|
||||
});
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import * as schema from './schema.js';
|
||||
import { DATABASE_TOKEN, DATABASE_URL } from '$env/static/private';
|
||||
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
|
||||
|
||||
const client = createClient({
|
||||
url: DATABASE_URL,
|
||||
authToken: DATABASE_TOKEN,
|
||||
});
|
||||
|
||||
export const db = drizzle(client, { schema, logger: true });
|
||||
@@ -1,2 +0,0 @@
|
||||
// we need to re-export from here to allow for the drizzle config to pick them up for migrations
|
||||
export * from '@sveltejs/mcp-schema/schema';
|
||||
@@ -2,45 +2,11 @@
|
||||
title: Overview
|
||||
---
|
||||
|
||||
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your LLM or agent of choice write better Svelte code. It works by providing documentation relevant to the task at hand, and statically analysing generated code so that it can suggest fixes and best practices.
|
||||
The following pages will help you set up and use the AI tools officially maintained by the Svelte team.
|
||||
|
||||
## Setup
|
||||
There are four tools, designed to help your agent write correct, robust Svelte code. They are designed to work together, but each can be used individually:
|
||||
|
||||
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
|
||||
|
||||
- [local setup](local-setup) using `@sveltejs/mcp`
|
||||
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
|
||||
|
||||
## Usage
|
||||
|
||||
To get the most out of the MCP server we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports), if using Claude Code. Or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using GEMINI). This will tell the LLM which tools are available and when it's appropriate to use them.
|
||||
|
||||
> [!NOTE] This is already setup for you when using `npx sv add mcp`
|
||||
|
||||
```md
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
```
|
||||
|
||||
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.
|
||||
- [Instructions](instructions): small prompt always injected into your session to make your agent more aware of the available tools
|
||||
- [MCP Server](mcp): with tools, prompts and resources to give your agent more context, by pulling directly from the official Svelte documentation and using static analysis to correct common generative AI pitfalls
|
||||
- [Skills](skills): lazy-loaded descriptions that teach your agent Svelte best practices, and how to use the [`@sveltejs/mcp` cli](cli)
|
||||
- [Subagents](subagent): focused agents that can be invoked in parallel to execute atomic operations in a separate context window
|
||||
|
||||
23
documentation/docs/20-instructions/.generated/agents.md
Normal file
23
documentation/docs/20-instructions/.generated/agents.md
Normal file
@@ -0,0 +1,23 @@
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available Svelte MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
13
documentation/docs/20-instructions/10-instructions.md
Normal file
13
documentation/docs/20-instructions/10-instructions.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
title: AGENTS.md
|
||||
---
|
||||
|
||||
To get the most out of the [MCP server](mcp) and [skills](skills) we recommend including the following prompt in your [`AGENTS.md`](https://agents.md) (or [`CLAUDE.md`](https://docs.claude.com/en/docs/claude-code/memory#claude-md-imports) or [`GEMINI.md`](https://geminicli.com/docs/cli/gemini-md/), if using Claude Code or Gemini). This will tell your agent which tools are available and when it is appropriate to use them.
|
||||
|
||||
> [!NOTE] This is already setup for you when using `npx sv add mcp`
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
@include .generated/agents.md
|
||||
````
|
||||
<!-- prettier-ignore-end -->
|
||||
3
documentation/docs/20-instructions/index.md
Normal file
3
documentation/docs/20-instructions/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Instructions
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Setup
|
||||
---
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
title: Capabilities
|
||||
---
|
||||
@@ -1,9 +1,3 @@
|
||||
---
|
||||
title: Prompts
|
||||
---
|
||||
|
||||
This is the list of available prompts provided by the MCP server. Prompts are selected by the user and are sent as a user message. They can be useful to write repetitive instructions for the LLM on how to properly use the MCP server.
|
||||
|
||||
## svelte-task
|
||||
|
||||
This prompt should be used whenever you are asking the model to work on a Svelte-related task. It will instruct the LLM which documentation sections are available, which tools to invoke, when to invoke them, and how to interpret the results.
|
||||
@@ -11,10 +5,22 @@ This prompt should be used whenever you are asking the model to work on a Svelte
|
||||
<details>
|
||||
<summary>Copy the prompt</summary>
|
||||
|
||||
```md
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool `get-documentation` with one of the following paths. However: before invoking the `get-documentation` tool, try to answer the users query using your own knowledge and the `svelte-autofixer` tool. Be mindful of how many section you request, since it is token-intensive!
|
||||
<available-docs>
|
||||
|
||||
- title: Overview, use_cases: use title and path to estimate use case, path: ai/overview
|
||||
- title: Local setup, use_cases: use title and path to estimate use case, path: ai/local-setup
|
||||
- title: Remote setup, use_cases: use title and path to estimate use case, path: ai/remote-setup
|
||||
- title: Tools, use_cases: use title and path to estimate use case, path: ai/tools
|
||||
- title: Resources, use_cases: use title and path to estimate use case, path: ai/resources
|
||||
- title: Prompts, use_cases: use title and path to estimate use case, path: ai/prompts
|
||||
- title: Overview, use_cases: use title and path to estimate use case, path: ai/plugin
|
||||
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/subagent
|
||||
- title: Overview, use_cases: use title and path to estimate use case, path: ai/opencode-plugin
|
||||
- title: Subagent, use_cases: use title and path to estimate use case, path: ai/opencode-subagent
|
||||
- title: Overview, use_cases: use title and path to estimate use case, path: ai/skills
|
||||
- title: Overview, use_cases: project setup, creating new svelte apps, scaffolding, cli tools, initializing projects, path: cli/overview
|
||||
- title: Frequently asked questions, use_cases: project setup, initializing new svelte projects, troubleshooting cli installation, package manager configuration, path: cli/faq
|
||||
- title: sv create, use_cases: project setup, starting new sveltekit app, initializing project, creating from playground, choosing project template, path: cli/sv-create
|
||||
@@ -24,7 +30,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
|
||||
- title: devtools-json, use_cases: development setup, chrome devtools integration, browser-based editing, local development workflow, debugging setup, path: cli/devtools-json
|
||||
- title: drizzle, use_cases: database setup, sql queries, orm integration, data modeling, postgresql, mysql, sqlite, server-side data access, database migrations, type-safe queries, path: cli/drizzle
|
||||
- title: eslint, use_cases: code quality, linting, error detection, project setup, code standards, team collaboration, typescript projects, path: cli/eslint
|
||||
- title: lucia, use_cases: authentication, login systems, user management, registration pages, session handling, auth setup, path: cli/lucia
|
||||
- title: better-auth, use_cases: use title and path to estimate use case, path: cli/better-auth
|
||||
- title: mcp, use_cases: use title and path to estimate use case, path: cli/mcp
|
||||
- title: mdsvex, use_cases: blog, content sites, markdown rendering, documentation sites, technical writing, cms integration, article pages, path: cli/mdsvex
|
||||
- title: paraglide, use_cases: internationalization, multi-language sites, i18n, translation, localization, language switching, global apps, multilingual content, path: cli/paraglide
|
||||
@@ -34,6 +40,8 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
|
||||
- title: sveltekit-adapter, use_cases: deployment, production builds, hosting setup, choosing deployment platform, configuring adapters, static site generation, node server, vercel, cloudflare, netlify, path: cli/sveltekit-adapter
|
||||
- title: tailwindcss, use_cases: project setup, styling, css framework, rapid prototyping, utility-first css, design systems, responsive design, adding tailwind to svelte, path: cli/tailwind
|
||||
- title: vitest, use_cases: testing, unit tests, component testing, test setup, quality assurance, ci/cd pipelines, test-driven development, path: cli/vitest
|
||||
- title: add-on, use_cases: use title and path to estimate use case, path: cli/add-on
|
||||
- title: sv-utils, use_cases: use title and path to estimate use case, path: cli/sv-utils
|
||||
- title: Introduction, use_cases: learning sveltekit, project setup, understanding framework basics, choosing between svelte and sveltekit, getting started with full-stack apps, path: kit/introduction
|
||||
- title: Creating a project, use_cases: project setup, starting new sveltekit app, initial development environment, first-time sveltekit users, scaffolding projects, path: kit/creating-a-project
|
||||
- title: Project types, use_cases: deployment, project setup, choosing adapters, ssg, spa, ssr, serverless, mobile apps, desktop apps, pwa, offline apps, browser extensions, separate backend, docker containers, path: kit/project-types
|
||||
@@ -101,12 +109,6 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
|
||||
- title: Configuration, use_cases: project setup, configuration, adapters, deployment, build settings, environment variables, routing customization, prerendering, csp security, csrf protection, path configuration, typescript setup, path: kit/configuration
|
||||
- title: Command Line Interface, use_cases: project setup, typescript configuration, generated types, ./$types imports, initial project configuration, path: kit/cli
|
||||
- title: Types, use_cases: typescript, type safety, route parameters, api endpoints, load functions, form actions, generated types, jsconfig setup, path: kit/types
|
||||
- title: Overview, use_cases: use title and path to estimate use case, path: mcp/overview
|
||||
- title: Local setup, use_cases: use title and path to estimate use case, path: mcp/local-setup
|
||||
- title: Remote setup, use_cases: use title and path to estimate use case, path: mcp/remote-setup
|
||||
- title: Tools, use_cases: use title and path to estimate use case, path: mcp/tools
|
||||
- title: Resources, use_cases: use title and path to estimate use case, path: mcp/resources
|
||||
- title: Prompts, use_cases: use title and path to estimate use case, path: mcp/prompts
|
||||
- title: Overview, use_cases: always, any svelte project, getting started, learning svelte, introduction, project setup, understanding framework basics, path: svelte/overview
|
||||
- title: Getting started, use_cases: project setup, starting new svelte project, initial installation, choosing between sveltekit and vite, editor configuration, path: svelte/getting-started
|
||||
- title: .svelte files, use_cases: always, any svelte project, component creation, project setup, learning svelte basics, path: svelte/svelte-files
|
||||
@@ -154,6 +156,7 @@ You are a Svelte expert tasked to build components and utilities for Svelte deve
|
||||
- title: Lifecycle hooks, use_cases: component initialization, cleanup tasks, timers, subscriptions, dom measurements, chat windows, autoscroll features, migration from svelte 4, path: svelte/lifecycle-hooks
|
||||
- title: Imperative component API, use_cases: project setup, client-side rendering, server-side rendering, ssr, hydration, testing, programmatic component creation, tooltips, dynamic mounting, path: svelte/imperative-component-api
|
||||
- title: Hydratable data, use_cases: use title and path to estimate use case, path: svelte/hydratable
|
||||
- title: Best practices, use_cases: use title and path to estimate use case, path: svelte/best-practices
|
||||
- title: Testing, use_cases: testing, quality assurance, unit tests, integration tests, component tests, e2e tests, vitest setup, playwright setup, test automation, path: svelte/testing
|
||||
- title: TypeScript, use_cases: typescript setup, type safety, component props typing, generic components, wrapper components, dom type augmentation, project configuration, path: svelte/typescript
|
||||
- title: Custom elements, use_cases: web components, custom elements, component library, design system, framework-agnostic components, embedding svelte in non-svelte apps, shadow dom, path: svelte/custom-elements
|
||||
@@ -204,6 +207,7 @@ This is the task you will work on:
|
||||
</task>
|
||||
|
||||
If you are not writing the code into a file, once you have the final version of the code ask the user if it wants to generate a playground link to quickly check the code in it and if it answer yes call the `playground-link` tool and return the url to the user nicely formatted. The playground link MUST be generated only once you have the final version of the code and you are ready to share it, it MUST include an entry point file called `App.svelte` where the main component should live. If you have multiple files to include in the playground link you can include them all at the root.
|
||||
```
|
||||
````
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
</details>
|
||||
16
documentation/docs/30-mcp/10-mcp.md
Normal file
16
documentation/docs/30-mcp/10-mcp.md
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
The Svelte MCP ([Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro)) server can help your agent write better Svelte code. It works by providing relevant documentation, and statically analysing generated code so that it can suggest fixes and best practices.
|
||||
|
||||
## Setup
|
||||
|
||||
The setup varies based on the version of the MCP you prefer — remote or local — and your chosen MCP client (e.g. Claude Code, Codex CLI or GitHub Copilot):
|
||||
|
||||
- [local setup](local-setup) using `@sveltejs/mcp`
|
||||
- [remote setup](remote-setup) using `https://mcp.svelte.dev/mcp`
|
||||
|
||||
## Usage
|
||||
|
||||
If your MCP client supports it, we also recommend using the [svelte-task](prompts#svelte-task) prompt to instruct the LLM on the best way to use the MCP server.
|
||||
@@ -45,6 +45,27 @@ command = "npx"
|
||||
args = ["-y", "@sveltejs/mcp"]
|
||||
```
|
||||
|
||||
## Copilot CLI
|
||||
|
||||
Use the Copilot CLI to interactively add the MCP server:
|
||||
|
||||
```bash
|
||||
/mcp add
|
||||
```
|
||||
|
||||
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@sveltejs/mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
To include the local MCP version in Gemini CLI, simply run the following command:
|
||||
@@ -57,7 +78,7 @@ The `[scope]` must be `user`, `project` or `local`.
|
||||
|
||||
## OpenCode
|
||||
|
||||
Run the command:
|
||||
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
@@ -91,6 +112,8 @@ opencode mcp add
|
||||
|
||||
## Cursor
|
||||
|
||||
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
|
||||
|
||||
- Open the command palette
|
||||
- Select "View: Open MCP Settings"
|
||||
- Click on "Add custom MCP"
|
||||
@@ -16,6 +16,8 @@ claude mcp add -t http -s [scope] svelte https://mcp.svelte.dev/mcp
|
||||
|
||||
You can choose your preferred `scope` (it must be `user`, `project` or `local`) and `name`.
|
||||
|
||||
If you prefer you can also install the `svelte` plugin in [the Svelte Claude Code Marketplace](claude-plugin) that will give you both the remote server and useful [skills](skills).
|
||||
|
||||
## Claude Desktop
|
||||
|
||||
- Open Settings > Connectors
|
||||
@@ -34,6 +36,26 @@ experimental_use_rmcp_client = true
|
||||
url = "https://mcp.svelte.dev/mcp"
|
||||
```
|
||||
|
||||
## Copilot CLI
|
||||
|
||||
Use the Copilot CLI to interactively add the MCP server:
|
||||
|
||||
```bash
|
||||
/mcp add
|
||||
```
|
||||
|
||||
Alternatively, create or edit `~/.copilot/mcp-config.json` and add the following configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"svelte": {
|
||||
"url": "https://mcp.svelte.dev/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Gemini CLI
|
||||
|
||||
To use the remote MCP server with Gemini CLI, simply run the following command:
|
||||
@@ -46,7 +68,7 @@ The `[scope]` must be `user` or `project`.
|
||||
|
||||
## OpenCode
|
||||
|
||||
Run the command:
|
||||
You can automatically configure the MCP server using the [OpenCode plugin](opencode-plugin) (recommended). If you prefer to configure the MCP server manually, run:
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
@@ -80,6 +102,8 @@ opencode mcp add
|
||||
|
||||
## Cursor
|
||||
|
||||
You can automatically configure the MCP server using the [Cursor plugin](cursor-plugin) (recommended). If you prefer to configure the MCP server manually you can:
|
||||
|
||||
- Open the command palette
|
||||
- Select "View: Open MCP Settings"
|
||||
- Click on "Add custom MCP"
|
||||
7
documentation/docs/30-mcp/60-prompts.md
Normal file
7
documentation/docs/30-mcp/60-prompts.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Prompts
|
||||
---
|
||||
|
||||
This is the list of available prompts provided by the MCP server. Prompts are selected by the user and are sent as a user message. They can be useful to write repetitive instructions for the LLM on how to properly use the MCP server.
|
||||
|
||||
@include .generated/prompts.md
|
||||
78
documentation/docs/30-mcp/70-cli.md
Normal file
78
documentation/docs/30-mcp/70-cli.md
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: CLI
|
||||
---
|
||||
|
||||
The `@sveltejs/mcp` npm package normally launches the local `stdio` MCP server:
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp
|
||||
```
|
||||
|
||||
If you invoke it with a subcommand, it behaves like a regular CLI and prints the result directly in your terminal instead. This is useful for agents, scripts and quick manual checks.
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp <command> [options]
|
||||
```
|
||||
|
||||
Available commands:
|
||||
|
||||
- `list-sections`
|
||||
- `get-documentation <sections>`
|
||||
- `svelte-autofixer <code_or_path>`
|
||||
|
||||
You can learn more about the commands with
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp --help
|
||||
npx -y @sveltejs/mcp <command> --help
|
||||
npx -y @sveltejs/mcp --version
|
||||
```
|
||||
|
||||
## `list-sections`
|
||||
|
||||
Lists all available Svelte and SvelteKit documentation sections.
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp list-sections
|
||||
```
|
||||
|
||||
The output is a structured text list of sections, including each section's title, `use_cases`, and documentation path. This is the same catalog the MCP tool uses before calling `get-documentation`.
|
||||
|
||||
## `get-documentation`
|
||||
|
||||
Fetches the full documentation for one or more sections.
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp get-documentation 'svelte/$state'
|
||||
# or
|
||||
npx -y @sveltejs/mcp get-documentation 'svelte/$state,svelte/await-expressions'
|
||||
```
|
||||
|
||||
Each section can be matched by title or by documentation path. If a section cannot be found, the CLI returns an error plus similar matches when available.
|
||||
|
||||
## `svelte-autofixer`
|
||||
|
||||
Runs the Svelte autofixer against either inline code or a file path:
|
||||
|
||||
```bash
|
||||
npx -y @sveltejs/mcp svelte-autofixer 'src/routes/+page.svelte'
|
||||
```
|
||||
|
||||
If the argument is an existing path, the CLI reads the file automatically. Otherwise it treats the argument as raw Svelte code.
|
||||
|
||||
Because most shells expand `$`, inline code should be quoted or escaped correctly. In practice, passing a file path is usually easier than passing source directly.
|
||||
|
||||
Available options:
|
||||
|
||||
- `--svelte-version <4|5>` - choose which Svelte version to validate against (defaults to `5`)
|
||||
- `--async` - enable async Svelte analysis for Svelte 5 projects
|
||||
|
||||
The command prints an object with:
|
||||
|
||||
- `issues`
|
||||
- `suggestions`
|
||||
- `require_another_tool_call_after_fixing`
|
||||
|
||||
This makes it easy to use in an agentic loop: run the autofixer, apply fixes, then run it again until it reports no remaining issues or suggestions.
|
||||
3
documentation/docs/30-mcp/index.md
Normal file
3
documentation/docs/30-mcp/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: MCP server
|
||||
---
|
||||
263
documentation/docs/40-skills/.generated/skills.md
Normal file
263
documentation/docs/40-skills/.generated/skills.md
Normal file
@@ -0,0 +1,263 @@
|
||||
## `svelte-code-writer`
|
||||
|
||||
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.
|
||||
|
||||
<a href="https://github.com/sveltejs/ai-tools/releases?q=svelte-code-writer" target="_blank" rel="noopener noreferrer">Open Releases page</a>
|
||||
|
||||
<details>
|
||||
<summary>View skill content</summary>
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
```
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
```
|
||||
|
||||
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
```
|
||||
|
||||
Analyzes Svelte code and suggests fixes for common issues.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--async` - Enable async Svelte mode (default: false)
|
||||
- `--svelte-version` - Target version: 4 or 5 (default: 5)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Analyze inline code (escape $ as \$)
|
||||
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
|
||||
|
||||
# Analyze a file
|
||||
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
|
||||
|
||||
# Target Svelte 4
|
||||
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
|
||||
```
|
||||
|
||||
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
|
||||
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
|
||||
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
|
||||
````
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
</details>
|
||||
|
||||
## `svelte-core-bestpractices`
|
||||
|
||||
Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
|
||||
|
||||
<a href="https://github.com/sveltejs/ai-tools/releases?q=svelte-core-bestpractices" target="_blank" rel="noopener noreferrer">Open Releases page</a>
|
||||
|
||||
<details>
|
||||
<summary>View skill content</summary>
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
## `$state`
|
||||
|
||||
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
|
||||
|
||||
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
|
||||
|
||||
## `$derived`
|
||||
|
||||
To compute something from state, use `$derived` rather than `$effect`:
|
||||
|
||||
```js
|
||||
// do this
|
||||
let square = $derived(num * num);
|
||||
|
||||
// don't do this
|
||||
let square;
|
||||
|
||||
$effect(() => {
|
||||
square = num * num;
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
|
||||
|
||||
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
|
||||
|
||||
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
|
||||
|
||||
## `$effect`
|
||||
|
||||
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 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 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.
|
||||
|
||||
## `$props`
|
||||
|
||||
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
|
||||
|
||||
```js
|
||||
// @errors: 2451
|
||||
let { type } = $props();
|
||||
|
||||
// do this
|
||||
let color = $derived(type === 'danger' ? 'red' : 'green');
|
||||
|
||||
// don't do this — `color` will not update if `type` changes
|
||||
let color = type === 'danger' ? 'red' : 'green';
|
||||
```
|
||||
|
||||
## `$inspect.trace`
|
||||
|
||||
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
|
||||
|
||||
## Events
|
||||
|
||||
Any element attribute starting with `on` is treated as an event listener:
|
||||
|
||||
```svelte
|
||||
<button onclick={() => {...}}>click me</button>
|
||||
|
||||
<!-- attribute shorthand also works -->
|
||||
<button {onclick}>...</button>
|
||||
|
||||
<!-- so do spread attributes -->
|
||||
<button {...props}>...</button>
|
||||
```
|
||||
|
||||
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
|
||||
|
||||
```svelte
|
||||
<svelte:window onkeydown={...} />
|
||||
<svelte:document onvisibilitychange={...} />
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```svelte
|
||||
{#snippet greeting(name)}
|
||||
<p>hello {name}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render greeting('world')}
|
||||
```
|
||||
|
||||
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
|
||||
|
||||
## Each blocks
|
||||
|
||||
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
|
||||
|
||||
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
|
||||
|
||||
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
|
||||
|
||||
## Using JavaScript variables in CSS
|
||||
|
||||
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
|
||||
|
||||
```svelte
|
||||
<div style:--columns={columns}>...</div>
|
||||
```
|
||||
|
||||
You can then reference `var(--columns)` inside the component's `<style>`.
|
||||
|
||||
## Styling child components
|
||||
|
||||
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
|
||||
|
||||
```svelte
|
||||
<!-- Parent.svelte -->
|
||||
<Child --color="red" />
|
||||
|
||||
<!-- Child.svelte -->
|
||||
<h1>Hello</h1>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
color: var(--color);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
<Child />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div :global {
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
|
||||
|
||||
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
|
||||
|
||||
## Async Svelte
|
||||
|
||||
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
|
||||
|
||||
## Avoid legacy features
|
||||
|
||||
Always use runes mode for new code, and avoid features that have more modern replacements:
|
||||
|
||||
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
|
||||
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
|
||||
- use `$props` instead of `export let`, `$$props` and `$$restProps`
|
||||
- use `onclick={...}` instead of `on:click={...}`
|
||||
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
|
||||
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
|
||||
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
|
||||
- use classes with `$state` fields to share reactivity between components, instead of using stores
|
||||
- use `{@attach ...}` instead of `use:action`
|
||||
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive
|
||||
````
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
</details>
|
||||
11
documentation/docs/40-skills/10-skills.md
Normal file
11
documentation/docs/40-skills/10-skills.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
This is the list of available skills provided by the Svelte MCP package. Skills are sets of instructions that AI agents can load on-demand to help with specific tasks.
|
||||
|
||||
Skills are available in both the Claude Code plugin (installed via the marketplace) and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills` or `.opencode/skills` folder.
|
||||
|
||||
You can download the latest skills from the [releases page](https://github.com/sveltejs/ai-tools/releases) of the repo, or find them in the [`tools/skills`](https://github.com/sveltejs/ai-tools/tree/main/tools/skills) folder.
|
||||
|
||||
@include .generated/skills.md
|
||||
3
documentation/docs/40-skills/index.md
Normal file
3
documentation/docs/40-skills/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Skills
|
||||
---
|
||||
67
documentation/docs/50-subagents/.generated/subagent.md
Normal file
67
documentation/docs/50-subagents/.generated/subagent.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves 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.
|
||||
|
||||
**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:
|
||||
|
||||
- Using `$effect` instead of `$derived` for computations
|
||||
- Missing cleanup in effects
|
||||
- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)
|
||||
- Missing keys in `{#each}` blocks
|
||||
- And more
|
||||
|
||||
## Workflow
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 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
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
1. Summary of changes made
|
||||
2. Any issues found and fixed by the autofixer
|
||||
3. Recommendations for further improvements (if any)
|
||||
20
documentation/docs/50-subagents/10-subagent.md
Normal file
20
documentation/docs/50-subagents/10-subagent.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
title: Overview
|
||||
---
|
||||
|
||||
Since creating, editing or analyzing a Svelte file is an atomic operation we recommend creating a subagent that your main agent can invoke whenever it needs to interact with a Svelte component. Subagents use a separate context window, allowing them to fetch documentation, iterate with [`svelte-autofixer`](tools#svelte-autofixer) and write to the filesystem without wasting context in the main agent.
|
||||
|
||||
Delegation should happen automatically when appropriate, but you can also explicitly request the subagent be used for Svelte-related tasks.
|
||||
|
||||
You can write your own or take inspiration from the one available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/tools/agents) repository: a specialized subagent called `svelte-file-editor` designed for creating, editing, and reviewing Svelte files.
|
||||
|
||||
<details>
|
||||
<summary>View subagent definition</summary>
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
@include .generated/subagent.md
|
||||
````
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
</details>
|
||||
3
documentation/docs/50-subagents/index.md
Normal file
3
documentation/docs/50-subagents/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Subagents
|
||||
---
|
||||
23
documentation/docs/60-plugins/10-claude-plugin.md
Normal file
23
documentation/docs/60-plugins/10-claude-plugin.md
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
title: Claude Code
|
||||
---
|
||||
|
||||
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Claude Code [plugin marketplace](https://code.claude.com/docs/en/discover-plugins).
|
||||
|
||||
The marketplace allows you to install the `svelte` plugin which will give you the remote MCP server, [skills](skills) to instruct the LLM on how to properly write Svelte 5 code, and a specialized agent for editing Svelte files.
|
||||
|
||||
If possible, we recommend that you instruct the LLM to execute MCP calls with the agent (you can explicitly mention an agent in your message to delegate work to it) when creating or editing `.svelte` files or `.svelte.ts`/`.svelte.js` modules — this will help save context by handling Svelte-specific tasks more efficiently.
|
||||
|
||||
## Installation
|
||||
|
||||
To add the repository as a marketplace, launch Claude Code and type the following:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add sveltejs/ai-tools
|
||||
```
|
||||
|
||||
Then, install the Svelte plugin:
|
||||
|
||||
```bash
|
||||
/plugin install svelte
|
||||
```
|
||||
54
documentation/docs/60-plugins/20-opencode-plugin.md
Normal file
54
documentation/docs/60-plugins/20-opencode-plugin.md
Normal file
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: OpenCode
|
||||
---
|
||||
|
||||
OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows developers to add MCP servers, agents and commands programmatically. Svelte has an OpenCode plugin published under `@sveltejs/opencode`.
|
||||
|
||||
## Installation
|
||||
|
||||
To install the plugin you can edit your [OpenCode config](https://opencode.ai/docs/config/) (either the global or the local one), adding `@sveltejs/opencode` to the list of plugins.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["@sveltejs/opencode"]
|
||||
}
|
||||
```
|
||||
|
||||
That's it! You now have the Svelte [MCP server](mcp), [skills](skills), and the `svelte-file-editor` [subagent](subagent) configured for you.
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, everything is enabled, but you can configure the plugin by adding a configuration file:
|
||||
|
||||
- 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`)
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://svelte.dev/opencode/schema.json",
|
||||
"mcp": {
|
||||
"type": "remote", // or "local" — defaults to remote
|
||||
"enabled": true
|
||||
},
|
||||
"subagent": {
|
||||
"enabled": true,
|
||||
"agents": {
|
||||
"svelte-file-editor": {
|
||||
"model": "<other-model>", // defaults to the same as main agent
|
||||
"temperature": 1, // defaults to unset
|
||||
"top_p": 0.7, // defaults to unset
|
||||
"maxSteps": 20 // defaults to unlimited
|
||||
}
|
||||
}
|
||||
},
|
||||
"skills": {
|
||||
// this can be `true`, or an array of skills to enable
|
||||
// e.g. ["svelte-core-bestpractices"]
|
||||
"enabled": true
|
||||
},
|
||||
"instructions": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
```
|
||||
25
documentation/docs/60-plugins/30-cursor-plugin.md
Normal file
25
documentation/docs/60-plugins/30-cursor-plugin.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
title: Cursor
|
||||
---
|
||||
|
||||
Cursor has a [plugin system](https://cursor.com/docs/plugins) that can bundle rules, skills, agents, commands, MCP servers, and hooks.
|
||||
|
||||
The Svelte plugin gives you the remote Svelte MCP server, Cursor [skills](skills), an always-on rule that tells the model how to use the Svelte MCP tools correctly, and the `svelte-file-editor` subagent for working on `.svelte` files and `.svelte.ts`/`.svelte.js` modules. The source is available in the [`sveltejs/ai-tools`](https://github.com/sveltejs/ai-tools/tree/main/plugins/cursor/svelte) repo.
|
||||
|
||||
## Installation
|
||||
|
||||
Install the plugin from the [Cursor Marketplace](https://cursor.com/marketplace/svelte) with the following command:
|
||||
|
||||
```
|
||||
/add-plugin svelte
|
||||
```
|
||||
|
||||
Plugins can be installed either for the current project or at user level.
|
||||
|
||||
Once installed, Cursor will discover the plugin components automatically:
|
||||
|
||||
- the Svelte MCP server is added from the plugin's `.mcp.json`
|
||||
- rules and skills appear in Cursor's rules UI
|
||||
- the `svelte-file-editor` agent becomes available in chat
|
||||
|
||||
> [!NOTE] The Cursor CLI does not support plugins yet. Plugin support in [Cloud Agents](https://cursor.com/docs/cloud-agent) is limited to MCP servers.
|
||||
3
documentation/docs/60-plugins/index.md
Normal file
3
documentation/docs/60-plugins/index.md
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Plugins
|
||||
---
|
||||
@@ -1,3 +1,3 @@
|
||||
---
|
||||
title: MCP
|
||||
title: AI
|
||||
---
|
||||
|
||||
15
package.json
15
package.json
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.1",
|
||||
"description": "The official Svelte MCP server implementation",
|
||||
"type": "module",
|
||||
"packageManager": "pnpm@10.26.0",
|
||||
"packageManager": "pnpm@10.28.2",
|
||||
"scripts": {
|
||||
"build": "pnpm -r run build",
|
||||
"dev": "pnpm --filter @sveltejs/mcp-remote run dev",
|
||||
@@ -18,11 +18,22 @@
|
||||
"test": "npm run test:unit -- --run",
|
||||
"test:watch": "npm run test:unit -- --watch",
|
||||
"inspect": "pnpm mcp-inspector",
|
||||
"generate-opencode-jsonschema": "pnpm --filter @sveltejs/opencode run generate-schema",
|
||||
"generate-summaries": "pnpm --filter @sveltejs/mcp-server run generate-summaries",
|
||||
"generate-prompt-docs": "node --import node-resolve-ts/register scripts/update-docs-prompts.ts",
|
||||
"generate-skill-docs": "node --import node-resolve-ts/register scripts/update-docs-skills.ts",
|
||||
"generate-subagent-docs": "cp ./tools/agents/svelte-file-editor.md ./documentation/docs/50-subagents/.generated/subagent.md",
|
||||
"debug:generate-summaries": "pnpm --filter @sveltejs/mcp-server run debug:generate-summaries",
|
||||
"release": "pnpm --filter @sveltejs/mcp run build && changeset publish",
|
||||
"changeset:version": "changeset version && pnpm --filter @sveltejs/mcp run update:version && git add --all"
|
||||
"changeset:version": "changeset version && pnpm --filter @sveltejs/mcp run update:version && git add --all",
|
||||
"sync-plugins": "pnpm sync-claude-plugin && pnpm sync-cursor-plugin && pnpm sync-opencode-plugin && pnpm bump-plugin-versions",
|
||||
"sync-claude-plugin": "node scripts/sync-claude-plugin.ts",
|
||||
"sync-cursor-plugin": "node scripts/sync-cursor-plugin.ts",
|
||||
"sync-opencode-plugin": "node scripts/sync-opencode-plugin.ts && pnpm generate-opencode-jsonschema",
|
||||
"bump-plugin-versions": "node scripts/bump-plugin-versions.ts",
|
||||
"postbump-plugin-versions": "pnpm format",
|
||||
"resolve-references": "node scripts/resolve-references.ts",
|
||||
"postresolve-references": "pnpm format"
|
||||
},
|
||||
"keywords": [
|
||||
"svelte",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp-schema",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./utils": "./src/utils.js",
|
||||
"./schema": "./src/schema.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"drizzle-orm": "catalog:orm"
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* @import * as schema from './schema.js'
|
||||
*/
|
||||
export * from './schema.js';
|
||||
|
||||
/**
|
||||
* @typedef {typeof schema} Schema
|
||||
*/
|
||||
@@ -1,82 +0,0 @@
|
||||
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core';
|
||||
import { float_32_array } from './utils.js';
|
||||
|
||||
/**
|
||||
* NOTE: if you modify a schema adding a vector column you need to manually add this
|
||||
*
|
||||
* CREATE INDEX IF NOT EXISTS name_of_the_index
|
||||
* ON `name_of_the_table` (
|
||||
* libsql_vector_idx(name_of_the_column, 'metric=cosine')
|
||||
* )
|
||||
*
|
||||
* to the generated migration file
|
||||
*/
|
||||
|
||||
export const distillations = sqliteTable('distillations', {
|
||||
id: integer('id').primaryKey(),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
version: text('version').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_kb: integer('size_kb').notNull(),
|
||||
document_count: integer('document_count').notNull(),
|
||||
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const distillation_jobs = sqliteTable('distillation_jobs', {
|
||||
id: integer('id').primaryKey(),
|
||||
preset_name: text('preset_name').notNull(),
|
||||
batch_id: text('batch_id'),
|
||||
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
|
||||
model_used: text('model_used').notNull(),
|
||||
total_files: integer('total_files').notNull(),
|
||||
processed_files: integer('processed_files').notNull().default(0),
|
||||
successful_files: integer('successful_files').notNull().default(0),
|
||||
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
|
||||
total_input_tokens: integer('total_input_tokens').notNull().default(0),
|
||||
total_output_tokens: integer('total_output_tokens').notNull().default(0),
|
||||
started_at: integer('started_at', { mode: 'timestamp' }),
|
||||
completed_at: integer('completed_at', { mode: 'timestamp' }),
|
||||
error_message: text('error_message'),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content = sqliteTable('content', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
|
||||
export const content_distilled = sqliteTable('content_distilled', {
|
||||
id: integer('id').primaryKey(),
|
||||
path: text('path').notNull(),
|
||||
filename: text('filename').notNull(),
|
||||
content: text('content').notNull(),
|
||||
size_bytes: integer('size_bytes').notNull(),
|
||||
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
|
||||
metadata: text('metadata', { mode: 'json' }).notNull().default({}),
|
||||
created_at: integer('created_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
updated_at: integer('updated_at', { mode: 'timestamp' })
|
||||
.notNull()
|
||||
.$defaultFn(() => new Date()),
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* @import { Column } from 'drizzle-orm';
|
||||
*/
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { customType } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
/**
|
||||
* Helper function to convert an array of embeddings into a format that can be inserted into a LibSQL vector column.
|
||||
* @param {number[]} arr The embeddings array.
|
||||
*/
|
||||
export function vector(arr) {
|
||||
return sql`vector32(${JSON.stringify(arr)})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to calculate the distance between a vector column and an array of embeddings and return it as a columns.
|
||||
* @param {Column} column The drizzle column representing the vector.
|
||||
* @param {number} arr The embeddings array.
|
||||
* @param {string} as The name of the returned column. Default is 'distance'.
|
||||
*
|
||||
* @example
|
||||
* await db.select({
|
||||
* id: vector_table.id,
|
||||
* text: vector_table.text,
|
||||
* distance: distance(vector_table.vector, await get_embeddings(sentence)),
|
||||
* })
|
||||
* .from(vector_table)
|
||||
* .orderBy(sql`distance`)
|
||||
* .execute();
|
||||
*/
|
||||
export function distance(column, arr, as = 'distance') {
|
||||
return /** @type {typeof sql<number>} */ (
|
||||
sql
|
||||
)`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
|
||||
as,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom drizzle type to use the LibSQL vector column type.
|
||||
*/
|
||||
export const float_32_array = /** @type {typeof customType<{
|
||||
data: number[];
|
||||
config: { dimensions: number };
|
||||
configRequired: true;
|
||||
driverData: Buffer;
|
||||
}>} */ (customType)({
|
||||
dataType(config) {
|
||||
return `F32_BLOB(${config.dimensions})`;
|
||||
},
|
||||
/**
|
||||
* @param {Buffer} value
|
||||
*/
|
||||
fromDriver(value) {
|
||||
return Array.from(new Float32Array(value.buffer));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number[]} value
|
||||
* @returns
|
||||
*/
|
||||
toDriver(value) {
|
||||
return vector(value);
|
||||
},
|
||||
});
|
||||
@@ -14,14 +14,11 @@
|
||||
"debug:generate-summaries": "DEBUG_MODE=1 node scripts/generate-summaries.ts --experimental-strip-types"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"drizzle-orm": "^0.45.0"
|
||||
".": "./src/index.ts",
|
||||
"./handlers": "./src/mcp/handlers/tools/handlers.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mcp-ui/server": "catalog:ai",
|
||||
"@sveltejs/mcp-schema": "workspace:^",
|
||||
"@tmcp/adapter-valibot": "catalog:tmcp",
|
||||
"@tmcp/transport-in-memory": "catalog:tmcp",
|
||||
"@typescript-eslint/parser": "catalog:lint",
|
||||
|
||||
@@ -344,87 +344,111 @@ describe('add_autofixers_issues', () => {
|
||||
});
|
||||
|
||||
describe('imported_runes', () => {
|
||||
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(
|
||||
'from "$source"',
|
||||
({ source }) => {
|
||||
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
|
||||
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
describe.each([
|
||||
{ source: 'svelte' },
|
||||
{ source: 'svelte/runes' },
|
||||
{ source: '@sveltejs/runes' },
|
||||
{ source: '@sveltejs/vite-plugin-svelte' },
|
||||
])('from "$source"', ({ source }) => {
|
||||
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
|
||||
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { ${rune} } from '${source}';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import ${rune} from '${source}';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import * as ${rune} from '${source}';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { onMount, state, effect } from '${source}';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
|
||||
);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
|
||||
);
|
||||
});
|
||||
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
|
||||
);
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { onMount } from '${source}';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
expect(content.suggestions).not.toContain(
|
||||
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe.each(dollarless_runes)('importing $rune from external lib', ({ rune }) => {
|
||||
it(`should not add suggestions when importing from packages that are not svelte`, () => {
|
||||
it(`should not add suggestions when importing from packages whose name doesn't contain svelte`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { ${rune} } from 'something-something';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
`You are importing "${rune}" from "something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
);
|
||||
});
|
||||
|
||||
it(`should add suggestions with a different hint when importing from packages whose name contains svelte but it's not official`, () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { ${rune} } from 'svelte-something-something';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
`You are importing "${rune}" from "svelte-something-something". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
|
||||
expect(content.suggestions).toContain(
|
||||
`You are importing "${rune}" from "svelte-something-something". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly. If you are importing the function from a separate library ignore this suggestion.`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add the imported_runes suggestion when importing derived from svelte/store', () => {
|
||||
const content = run_autofixers_on_code(`
|
||||
<script>
|
||||
import { derived } from 'svelte/store';
|
||||
</script>`);
|
||||
|
||||
expect(content.suggestions).not.toContain(
|
||||
'You are importing "derived" from "svelte/store". This is not necessary, all runes are globally available. Please remove this import and use "$derived" directly.',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('derived_with_function', () => {
|
||||
@@ -751,5 +775,16 @@ describe('add_autofixers_issues', () => {
|
||||
`You are reading the stateful variable "$x" with a "$" prefix. Stateful variables are not stores and should be read without the "$". Please read it as a normal variable "x"`,
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/sveltejs/ai-tools/issues/200
|
||||
it('should not crash when reading $-prefixed identifier for variable initialized with member expression', () => {
|
||||
expect(() =>
|
||||
run_autofixers_on_code(`<div>{$x}</div>
|
||||
|
||||
<script>
|
||||
const x = foo.bar;
|
||||
</script>`),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +35,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
|
||||
'svelte/prefer-writable-derived': 'warn',
|
||||
'svelte/require-event-dispatcher-types': 'warn',
|
||||
'svelte/require-store-reactive-access': 'warn',
|
||||
'svelte/no-inspect': 'off',
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
|
||||
@@ -3,10 +3,20 @@ import type { Autofixer } from './index.js';
|
||||
|
||||
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
|
||||
|
||||
function should_suggest_for_source(source: string, rune: string) {
|
||||
if (!source.includes('svelte')) {
|
||||
return false;
|
||||
}
|
||||
if (source === 'svelte/store' && rune === 'derived') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export const imported_runes: Autofixer = {
|
||||
ImportDeclaration(node, { state, next }) {
|
||||
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
|
||||
if (source && (source === 'svelte' || source.startsWith('svelte/'))) {
|
||||
if (source) {
|
||||
for (const specifier of node.specifiers) {
|
||||
const id =
|
||||
specifier.type === 'ImportDefaultSpecifier'
|
||||
@@ -16,10 +26,25 @@ export const imported_runes: Autofixer = {
|
||||
: specifier.type === 'ImportSpecifier'
|
||||
? specifier.imported
|
||||
: null;
|
||||
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
|
||||
state.output.suggestions.push(
|
||||
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
|
||||
);
|
||||
if (
|
||||
id &&
|
||||
id.type === 'Identifier' &&
|
||||
dollarless_runes.includes(id.name) &&
|
||||
should_suggest_for_source(source, id.name)
|
||||
) {
|
||||
if (
|
||||
source === 'svelte' ||
|
||||
source.startsWith('svelte/') ||
|
||||
source.startsWith('@sveltejs')
|
||||
) {
|
||||
state.output.suggestions.push(
|
||||
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
|
||||
);
|
||||
} else {
|
||||
state.output.suggestions.push(
|
||||
`You are importing "${id.name}" from "${source}". If you are trying to import runes to use them this is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly. If you are importing the function from a separate library ignore this suggestion.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,110 +5,177 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
|
||||
import { icons } from '../../icons/index.js';
|
||||
import { tool } from 'tmcp/utils';
|
||||
|
||||
const get_documentation_schema = v.object({
|
||||
section: v.pipe(
|
||||
v.union([v.string(), v.array(v.string())]),
|
||||
v.description(
|
||||
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "cli/overview"). Supports single string and array of strings',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
export async function get_documentation_handler({
|
||||
section,
|
||||
}: v.InferInput<typeof get_documentation_schema>) {
|
||||
let sections: string[];
|
||||
|
||||
if (Array.isArray(section)) {
|
||||
sections = section.filter((s): s is string => typeof s === 'string');
|
||||
} else if (
|
||||
typeof section === 'string' &&
|
||||
section.trim().startsWith('[') &&
|
||||
section.trim().endsWith(']')
|
||||
) {
|
||||
try {
|
||||
const parsed = JSON.parse(section);
|
||||
if (Array.isArray(parsed)) {
|
||||
sections = parsed.filter((s): s is string => typeof s === 'string');
|
||||
} else {
|
||||
sections = [section];
|
||||
}
|
||||
} catch {
|
||||
sections = [section];
|
||||
}
|
||||
} else if (typeof section === 'string') {
|
||||
sections = [section];
|
||||
} else {
|
||||
sections = [];
|
||||
}
|
||||
|
||||
const available_sections = await get_sections();
|
||||
|
||||
const settled_results = await Promise.allSettled(
|
||||
sections.map(async (requested_section) => {
|
||||
const matched_section = available_sections.find(
|
||||
(s) =>
|
||||
s.title.toLowerCase() === requested_section.toLowerCase() ||
|
||||
s.slug === requested_section ||
|
||||
s.url === requested_section,
|
||||
);
|
||||
|
||||
if (matched_section) {
|
||||
try {
|
||||
const response = await fetch_with_timeout(matched_section.url);
|
||||
if (response.ok) {
|
||||
const content = await response.text();
|
||||
return { success: true, content: `## ${matched_section.title}\n\n${content}` };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Could not fetch documentation (HTTP ${response.status})`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Failed to fetch documentation - ${error}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${requested_section}\n\nError: Section not found.`,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const results = settled_results.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `Error: Couldn't fetch - ${result.reason}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const successes = results.filter((r) => r.success);
|
||||
const failed_sections = sections.filter(
|
||||
(s) =>
|
||||
!available_sections.some(
|
||||
(a) => a.title.toLowerCase() === s.toLowerCase() || a.slug === s || a.url === s,
|
||||
),
|
||||
);
|
||||
|
||||
if (successes.length > 0 && failed_sections.length === 0) {
|
||||
return successes.map((r) => r.content).join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
const parts: string[] = [];
|
||||
|
||||
if (successes.length > 0) {
|
||||
parts.push(successes.map((r) => r.content).join('\n\n---\n\n'));
|
||||
}
|
||||
|
||||
const fuzzy_results = failed_sections.map((requested) => {
|
||||
const lower = requested.toLowerCase();
|
||||
const matches = available_sections.filter(
|
||||
(a) =>
|
||||
a.title.toLowerCase().includes(lower) ||
|
||||
a.slug.includes(lower) ||
|
||||
lower.includes(a.slug.split('/').pop() ?? '') ||
|
||||
a.use_cases.toLowerCase().includes(lower),
|
||||
);
|
||||
return { requested, matches };
|
||||
});
|
||||
|
||||
const has_fuzzy = fuzzy_results.some((r) => r.matches.length > 0);
|
||||
|
||||
// Full list only when no successes and no fuzzy matches
|
||||
if (successes.length === 0 && !has_fuzzy) {
|
||||
const formatted_sections = await format_sections_list();
|
||||
parts.push(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
|
||||
}
|
||||
|
||||
// Similar results then errors
|
||||
for (const { requested, matches } of fuzzy_results) {
|
||||
if (matches.length > 0) {
|
||||
const match_list = matches.map((m) => `- title: ${m.title}, section: ${m.slug}`).join('\n');
|
||||
parts.push(
|
||||
`${matches.length} similar result${matches.length > 1 ? 's' : ''} for "${requested}":\n${match_list}`,
|
||||
);
|
||||
}
|
||||
parts.push(`Section not found: "${requested}".`);
|
||||
}
|
||||
|
||||
return parts.join('\n\n---\n\n');
|
||||
}
|
||||
|
||||
export function get_documentation(server: SvelteMcp) {
|
||||
server.tool(
|
||||
{
|
||||
name: 'get-documentation',
|
||||
description:
|
||||
'Retrieves full documentation content for Svelte 5 or SvelteKit sections. Supports flexible search by title (e.g., "$state", "routing") or file path (e.g., "cli/overview"). Can accept a single section name or an array of sections. Before running this, make sure to analyze the users query, as well as the output from list-sections (which should be called first). Then ask for ALL relevant sections the user might require. For example, if the user asks to build anything interactive, you will need to fetch all relevant runes, and so on. Before calling this tool, try to implement Svelte components using your own knowledge and the `svelte-autofixer` tool, since calling this tool is token intensive.',
|
||||
schema: v.object({
|
||||
section: v.pipe(
|
||||
v.union([v.string(), v.array(v.string())]),
|
||||
v.description(
|
||||
'The section name(s) to retrieve. Can search by title (e.g., "$state", "load functions") or file path (e.g., "cli/overview"). Supports single string and array of strings',
|
||||
),
|
||||
),
|
||||
}),
|
||||
schema: get_documentation_schema,
|
||||
annotations: {
|
||||
title: 'Get Documentation',
|
||||
destructiveHint: false,
|
||||
readOnlyHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
icons,
|
||||
},
|
||||
async ({ section }) => {
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'get-documentation');
|
||||
}
|
||||
let sections: string[];
|
||||
|
||||
if (Array.isArray(section)) {
|
||||
sections = section.filter((s): s is string => typeof s === 'string');
|
||||
} else if (
|
||||
typeof section === 'string' &&
|
||||
section.trim().startsWith('[') &&
|
||||
section.trim().endsWith(']')
|
||||
) {
|
||||
try {
|
||||
const parsed = JSON.parse(section);
|
||||
if (Array.isArray(parsed)) {
|
||||
sections = parsed.filter((s): s is string => typeof s === 'string');
|
||||
} else {
|
||||
sections = [section];
|
||||
}
|
||||
} catch {
|
||||
sections = [section];
|
||||
}
|
||||
} else if (typeof section === 'string') {
|
||||
sections = [section];
|
||||
} else {
|
||||
sections = [];
|
||||
}
|
||||
|
||||
const available_sections = await get_sections();
|
||||
|
||||
const settled_results = await Promise.allSettled(
|
||||
sections.map(async (requested_section) => {
|
||||
const matched_section = available_sections.find(
|
||||
(s) =>
|
||||
s.title.toLowerCase() === requested_section.toLowerCase() ||
|
||||
s.slug === requested_section ||
|
||||
s.url === requested_section,
|
||||
try {
|
||||
const content = await get_documentation_handler({ section });
|
||||
return tool.text(content);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(
|
||||
server.ctx.sessionId,
|
||||
'get-documentation-error',
|
||||
error.message,
|
||||
);
|
||||
|
||||
if (matched_section) {
|
||||
try {
|
||||
const response = await fetch_with_timeout(matched_section.url);
|
||||
if (response.ok) {
|
||||
const content = await response.text();
|
||||
return { success: true, content: `## ${matched_section.title}\n\n${content}` };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Could not fetch documentation (HTTP ${response.status})`,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${matched_section.title}\n\nError: Failed to fetch documentation - ${error}`,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `## ${requested_section}\n\nError: Section not found.`,
|
||||
};
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const results = settled_results.map((result) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
return result.value;
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
content: `Error: Couldn't fetch - ${result.reason}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const has_any_success = results.some((result) => result.success);
|
||||
let final_text = results.map((r) => r.content).join('\n\n---\n\n');
|
||||
|
||||
if (!has_any_success) {
|
||||
const formatted_sections = await format_sections_list();
|
||||
|
||||
final_text += `\n\n---\n\n${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
||||
return tool.error(error.message);
|
||||
}
|
||||
|
||||
return tool.text(final_text);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
4
packages/mcp-server/src/mcp/handlers/tools/handlers.ts
Normal file
4
packages/mcp-server/src/mcp/handlers/tools/handlers.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { get_documentation_handler } from './get-documentation.js';
|
||||
export { list_sections_handler } from './list-sections.js';
|
||||
export { svelte_autofixer_handler } from './svelte-autofixer.js';
|
||||
export { playground_link_handler } from './playground-link.js';
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './get-documentation.js';
|
||||
export * from './list-sections.js';
|
||||
export * from './svelte-autofixer.js';
|
||||
export * from './playground-link.js';
|
||||
export { get_documentation } from './get-documentation.js';
|
||||
export { list_sections } from './list-sections.js';
|
||||
export { svelte_autofixer } from './svelte-autofixer.js';
|
||||
export { playground_link } from './playground-link.js';
|
||||
|
||||
@@ -4,21 +4,44 @@ import { SECTIONS_LIST_INTRO, SECTIONS_LIST_OUTRO } from './prompts.js';
|
||||
import { icons } from '../../icons/index.js';
|
||||
import { tool } from 'tmcp/utils';
|
||||
|
||||
export async function list_sections_handler() {
|
||||
const formatted_sections = await format_sections_list();
|
||||
|
||||
return `${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`;
|
||||
}
|
||||
|
||||
export function list_sections(server: SvelteMcp) {
|
||||
server.tool(
|
||||
{
|
||||
name: 'list-sections',
|
||||
description:
|
||||
'Lists all available Svelte 5 and SvelteKit documentation sections in a structured format. Each section includes a "use_cases" field that describes WHEN this documentation would be useful. You should carefully analyze the use_cases field to determine which sections are relevant for the user\'s query. The use_cases contain comma-separated keywords describing project types (e.g., "e-commerce", "blog"), features (e.g., "authentication", "forms"), components (e.g., "slider", "modal"), development stages (e.g., "deployment", "testing"), or "always" for fundamental concepts. Match these use_cases against the user\'s intent - for example, if building an e-commerce site, fetch sections with use_cases containing "e-commerce", "product listings", "shopping cart", etc. If building a slider, look for "slider", "carousel", "animation", etc. Returns sections as "* title: [section_title], use_cases: [use_cases], path: [file_path]". Always run list-sections FIRST for any Svelte query, then analyze ALL use_cases to identify relevant sections, and finally use get_documentation to fetch ALL relevant sections at once.',
|
||||
annotations: {
|
||||
title: 'List Sections',
|
||||
destructiveHint: false,
|
||||
readOnlyHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
icons,
|
||||
},
|
||||
async () => {
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'list-sections');
|
||||
}
|
||||
const formatted_sections = await format_sections_list();
|
||||
|
||||
return tool.text(`${SECTIONS_LIST_INTRO}\n\n${formatted_sections}\n\n${SECTIONS_LIST_OUTRO}`);
|
||||
try {
|
||||
const content = await list_sections_handler();
|
||||
return tool.text(content);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(
|
||||
server.ctx.sessionId,
|
||||
'list-sections-error',
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
return tool.error(error.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('playground-link tool', () => {
|
||||
type: 'resource',
|
||||
resource: expect.objectContaining({
|
||||
uri: 'ui://svelte/playground-link',
|
||||
mimeType: 'text/uri-list',
|
||||
mimeType: 'text/html;profile=mcp-app',
|
||||
_meta: { 'mcpui.dev/ui-preferred-frame-size': ['100%', '1200px'] },
|
||||
text: expect.stringMatching(/^https:\/\/svelte\.dev\/playground\/embed#H4sIA/),
|
||||
}),
|
||||
@@ -67,6 +67,24 @@ describe('playground-link tool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should have tool _meta with resource URI for MCP Apps hosts', async () => {
|
||||
const tools = await session.listTools();
|
||||
const playground_tool = tools.tools.find((t) => t.name === 'playground-link');
|
||||
expect(playground_tool).toBeDefined();
|
||||
expect(playground_tool?._meta).toStrictEqual({
|
||||
ui: { resourceUri: 'ui://svelte/playground-link' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose a resource for MCP Apps hosts', async () => {
|
||||
const resources = await session.listResources();
|
||||
const playground_resource = resources.resources.find(
|
||||
(r) => r.uri === 'ui://svelte/playground-link',
|
||||
);
|
||||
expect(playground_resource).toBeDefined();
|
||||
expect(playground_resource?.name).toBe('playground-link-ui');
|
||||
});
|
||||
|
||||
it('should not create a playground link if App.svelte is missing', async () => {
|
||||
const result = await session.callTool<{ url: string }>('playground-link', {
|
||||
name: 'My Playground',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import * as v from 'valibot';
|
||||
import { icons } from '../../icons/index.js';
|
||||
import { createUIResource } from '@mcp-ui/server';
|
||||
import { tool } from 'tmcp/utils';
|
||||
import * as v from 'valibot';
|
||||
import { icons } from '../../icons/index.js';
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
|
||||
async function compress_and_encode_text(input: string) {
|
||||
const reader = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip')).getReader();
|
||||
@@ -30,98 +30,232 @@ type File = {
|
||||
text: boolean;
|
||||
};
|
||||
|
||||
const playground_link_schema = v.object({
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.description('The name of the Playground, it should reflect the user task'),
|
||||
),
|
||||
tailwind: v.pipe(
|
||||
v.boolean(),
|
||||
v.description(
|
||||
"If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
|
||||
),
|
||||
),
|
||||
files: v.pipe(
|
||||
v.record(v.string(), v.string()),
|
||||
v.description(
|
||||
"An object where all the keys are the filenames (with extensions) and the values are the file content. For example: { 'Component.svelte': '<script>...</script>', 'utils.js': 'export function ...' }. The playground accept multiple files so if are importing from other files just include them all at the root level.",
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
const playground_link_output_schema = v.object({
|
||||
url: v.string(),
|
||||
});
|
||||
|
||||
export async function playground_link_handler({
|
||||
files,
|
||||
name,
|
||||
tailwind,
|
||||
}: v.InferInput<typeof playground_link_schema>) {
|
||||
const playground_base = new URL('https://svelte.dev/playground');
|
||||
const playground_files: File[] = [];
|
||||
|
||||
let has_app_svelte = false;
|
||||
|
||||
for (const [filename, contents] of Object.entries(files)) {
|
||||
if (filename === 'App.svelte') has_app_svelte = true;
|
||||
playground_files.push({
|
||||
type: 'file',
|
||||
name: filename,
|
||||
basename: filename.replace(/^.*[\\/]/, ''),
|
||||
contents,
|
||||
text: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_app_svelte) {
|
||||
throw new Error('The files must contain an App.svelte file as the entry point');
|
||||
}
|
||||
|
||||
const playground_config = {
|
||||
name,
|
||||
tailwind: tailwind ?? false,
|
||||
files: playground_files,
|
||||
};
|
||||
|
||||
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
|
||||
|
||||
const url = playground_base.toString();
|
||||
|
||||
// use the embed path to have a cleaner UI for mcp-ui
|
||||
playground_base.pathname = '/playground/embed';
|
||||
|
||||
return {
|
||||
url,
|
||||
iframe_url: playground_base.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Create the UI resource for MCP Apps hosts (with adapter)
|
||||
// This will be registered as a resource that MCP Apps hosts can fetch
|
||||
const playground_ui_resource = createUIResource({
|
||||
uri: 'ui://svelte/playground-link',
|
||||
encoding: 'text',
|
||||
resourceProps: {
|
||||
_meta: {
|
||||
ui: {
|
||||
csp: {
|
||||
connectDomains: ['https://svelte.dev'],
|
||||
resourceDomains: ['https://svelte.dev'],
|
||||
frameDomains: ['https://svelte.dev'],
|
||||
baseUriDomains: ['https://svelte.dev'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
content: {
|
||||
type: 'rawHtml',
|
||||
// This is a placeholder HTML - the actual iframe URL will be set per-request
|
||||
// MCP Apps hosts receive the tool input/output via postMessage
|
||||
htmlString: `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body { width: 100%; height: 100%; }
|
||||
iframe { width: 100%; height: 100%; border: none; display: none; }
|
||||
.loading { display: flex; align-items: center; justify-content: center; height: 100%; font-family: system-ui, sans-serif; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="loading" id="loading">Loading playground...</div>
|
||||
<iframe id="playground" allow="clipboard-write"></iframe>
|
||||
<script>
|
||||
function size_changed() {
|
||||
const width = document.body.scrollWidth;
|
||||
window.parent.postMessage({
|
||||
jsonrpc: '2.0',
|
||||
method: 'ui/notifications/size-changed',
|
||||
params: {
|
||||
width,
|
||||
height: 800
|
||||
}
|
||||
}, '*');
|
||||
}
|
||||
// Signal that the widget is ready
|
||||
window.parent.postMessage({ type: 'ui-lifecycle-iframe-ready' }, '*');
|
||||
|
||||
// Listen for render data from the adapter (for MCP Apps hosts)
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data.type === 'ui-lifecycle-iframe-render-data') {
|
||||
const renderData = event.data.payload.renderData || {};
|
||||
const toolOutput = renderData.toolOutput;
|
||||
|
||||
// The tool output contains the iframe URL
|
||||
if (toolOutput && toolOutput.structuredContent && toolOutput.structuredContent.url) {
|
||||
const iframe = document.getElementById('playground');
|
||||
const loading = document.getElementById('loading');
|
||||
// Convert the URL to embed URL
|
||||
const embedUrl = toolOutput.structuredContent.url.replace('/playground#', '/playground/embed#');
|
||||
iframe.src = embedUrl;
|
||||
iframe.style.display = 'block';
|
||||
iframe.addEventListener("load", () => {
|
||||
size_changed();
|
||||
});
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>`,
|
||||
},
|
||||
uiMetadata: {
|
||||
'preferred-frame-size': ['100%', '1200px'],
|
||||
},
|
||||
adapters: {
|
||||
mcpApps: { enabled: true },
|
||||
},
|
||||
});
|
||||
|
||||
export function playground_link(server: SvelteMcp) {
|
||||
// Register the UI resource so MCP Apps hosts can fetch it
|
||||
server.resource(
|
||||
{
|
||||
name: 'playground-link-ui',
|
||||
description: 'UI resource for the Svelte Playground widget',
|
||||
uri: playground_ui_resource.resource.uri,
|
||||
icons,
|
||||
},
|
||||
() => {
|
||||
return {
|
||||
contents: [playground_ui_resource.resource],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
{
|
||||
name: 'playground-link',
|
||||
description:
|
||||
'Generates a Playground link given a Svelte code snippet. Once you have the final version of the code you want to send to the user, ALWAYS ask the user if it wants a playground link to allow it to quickly check the code in the playground before calling this tool. NEVER use this tool if you have written the component to a file in the user project. The playground accept multiple files so if are importing from other files just include them all at the root level.',
|
||||
schema: v.object({
|
||||
name: v.pipe(
|
||||
v.string(),
|
||||
v.description('The name of the Playground, it should reflect the user task'),
|
||||
),
|
||||
tailwind: v.pipe(
|
||||
v.boolean(),
|
||||
v.description(
|
||||
"If the code requires Tailwind CSS to work...only send true if it it's using tailwind classes in the code",
|
||||
),
|
||||
),
|
||||
files: v.pipe(
|
||||
v.record(v.string(), v.string()),
|
||||
v.description(
|
||||
"An object where all the keys are the filenames (with extensions) and the values are the file content. For example: { 'Component.svelte': '<script>...</script>', 'utils.js': 'export function ...' }. The playground accept multiple files so if are importing from other files just include them all at the root level.",
|
||||
),
|
||||
),
|
||||
}),
|
||||
outputSchema: v.object({
|
||||
url: v.string(),
|
||||
}),
|
||||
schema: playground_link_schema,
|
||||
outputSchema: playground_link_output_schema,
|
||||
annotations: {
|
||||
title: 'Playground Link',
|
||||
destructiveHint: false,
|
||||
readOnlyHint: true,
|
||||
openWorldHint: false,
|
||||
},
|
||||
icons,
|
||||
// For MCP Apps hosts - points to the registered resource
|
||||
_meta: {
|
||||
ui: {
|
||||
resourceUri: playground_ui_resource.resource.uri,
|
||||
},
|
||||
},
|
||||
},
|
||||
async ({ files, name, tailwind }) => {
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link');
|
||||
}
|
||||
const playground_base = new URL('https://svelte.dev/playground');
|
||||
const playground_files: File[] = [];
|
||||
|
||||
let has_app_svelte = false;
|
||||
|
||||
for (const [filename, contents] of Object.entries(files)) {
|
||||
if (filename === 'App.svelte') has_app_svelte = true;
|
||||
playground_files.push({
|
||||
type: 'file',
|
||||
name: filename,
|
||||
basename: filename.replace(/^.*[\\/]/, ''),
|
||||
contents,
|
||||
text: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (!has_app_svelte) {
|
||||
try {
|
||||
const result = await playground_link_handler({ files, name, tailwind });
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({ url: result.url }),
|
||||
},
|
||||
// Embedded resource for MCP-UI hosts (no adapter, uses externalUrl)
|
||||
createUIResource({
|
||||
uri: 'ui://svelte/playground-link',
|
||||
content: {
|
||||
type: 'externalUrl',
|
||||
iframeUrl: result.iframe_url,
|
||||
},
|
||||
uiMetadata: {
|
||||
'preferred-frame-size': ['100%', '1200px'],
|
||||
},
|
||||
encoding: 'text',
|
||||
}),
|
||||
],
|
||||
structuredContent: { url: result.url },
|
||||
};
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'playground-link-no-app-svelte');
|
||||
await server.ctx.custom?.track?.(
|
||||
server.ctx.sessionId,
|
||||
'playground-link-error',
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
|
||||
return tool.error('The files must contain an App.svelte file as the entry point');
|
||||
return tool.error(error.message);
|
||||
}
|
||||
|
||||
const playground_config = {
|
||||
name,
|
||||
tailwind: tailwind ?? false,
|
||||
files: playground_files,
|
||||
};
|
||||
|
||||
playground_base.hash = await compress_and_encode_text(JSON.stringify(playground_config));
|
||||
|
||||
const content = {
|
||||
url: playground_base.toString(),
|
||||
};
|
||||
|
||||
// use the embed path to have a cleaner UI for mcp-ui
|
||||
playground_base.pathname = '/playground/embed';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(content),
|
||||
},
|
||||
createUIResource({
|
||||
uri: 'ui://svelte/playground-link',
|
||||
content: {
|
||||
type: 'externalUrl',
|
||||
iframeUrl: playground_base.toString(),
|
||||
},
|
||||
uiMetadata: {
|
||||
'preferred-frame-size': ['100%', '1200px'],
|
||||
},
|
||||
encoding: 'text',
|
||||
}),
|
||||
],
|
||||
structuredContent: content,
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { writeFileSync, unlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { InMemoryTransport } from '@tmcp/transport-in-memory';
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { server } from '../../index.js';
|
||||
@@ -12,13 +15,18 @@ async function autofixer_tool_call(
|
||||
is_error = false,
|
||||
desired_svelte_version = 5,
|
||||
async = false,
|
||||
ctx?: { stdio?: boolean },
|
||||
) {
|
||||
const result = await session.callTool('svelte-autofixer', {
|
||||
code,
|
||||
desired_svelte_version,
|
||||
filename: 'App.svelte',
|
||||
async,
|
||||
});
|
||||
const result = await session.callTool(
|
||||
'svelte-autofixer',
|
||||
{
|
||||
code,
|
||||
desired_svelte_version,
|
||||
filename: 'App.svelte',
|
||||
async,
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
if (is_error) {
|
||||
@@ -53,6 +61,18 @@ describe('svelte-autofixer tool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should add suggestions for snippets declared in script tag', async () => {
|
||||
const content = await autofixer_tool_call(`<script>
|
||||
{#snippet my_snippet()}
|
||||
some content
|
||||
{/snippet}
|
||||
</script>`);
|
||||
expect(content.issues.length).toBeGreaterThan(0);
|
||||
expect(content.suggestions).toContain(
|
||||
"The code can't be compiled because a Javascript parse error. The error suggests you have a `{#snippet ...}` block inside the `<script>` tag. Snippets are template syntax and should be declared in the markup section of the component, not in the script. Move the snippet outside of the `<script>` tag. Snippets declared in the markup can also be accessed in the script tag in case you need them.",
|
||||
);
|
||||
});
|
||||
|
||||
it('should error out if async is true with a version less than 5', async () => {
|
||||
const content = await autofixer_tool_call(
|
||||
`<script>
|
||||
@@ -134,4 +154,40 @@ describe('svelte-autofixer tool', () => {
|
||||
'The desired_svelte_version MUST be either 4 or 5 but received "3"',
|
||||
);
|
||||
});
|
||||
|
||||
it('should read file content from path when stdio context is true', async () => {
|
||||
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
|
||||
const file_content = `<script>
|
||||
$state count = 0;
|
||||
</script>`;
|
||||
|
||||
writeFileSync(tmp_file, file_content, 'utf-8');
|
||||
|
||||
try {
|
||||
// with stdio: true, the file is read from disk and parsed, producing issues
|
||||
const content = await autofixer_tool_call(tmp_file, false, 5, false, { stdio: true });
|
||||
expect(content.issues.length).toBeGreaterThan(0);
|
||||
expect(content.suggestions.length).toBeGreaterThan(0);
|
||||
} finally {
|
||||
unlinkSync(tmp_file);
|
||||
}
|
||||
});
|
||||
|
||||
it('should treat file path as code when stdio context is not set', async () => {
|
||||
const tmp_file = join(tmpdir(), `svelte-autofixer-test-${Date.now()}.svelte`);
|
||||
const file_content = `<script>
|
||||
$state count = 0;
|
||||
</script>`;
|
||||
|
||||
writeFileSync(tmp_file, file_content, 'utf-8');
|
||||
|
||||
try {
|
||||
// without stdio context, the path string is treated as raw code (plain text), no issues
|
||||
const content = await autofixer_tool_call(tmp_file, false, 5, false);
|
||||
expect(content.issues).toHaveLength(0);
|
||||
expect(content.suggestions).toHaveLength(0);
|
||||
} finally {
|
||||
unlinkSync(tmp_file);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,121 @@
|
||||
import { basename } from 'node:path';
|
||||
import type { SvelteMcp } from '../../index.js';
|
||||
import { tool } from 'tmcp/utils';
|
||||
import * as v from 'valibot';
|
||||
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
|
||||
import { add_compile_issues } from '../../autofixers/add-compile-issues.js';
|
||||
import { add_eslint_issues } from '../../autofixers/add-eslint-issues.js';
|
||||
import { add_autofixers_issues } from '../../autofixers/add-autofixers-issues.js';
|
||||
import { icons } from '../../icons/index.js';
|
||||
import { tool } from 'tmcp/utils';
|
||||
import { type SvelteMcp } from '../../index.js';
|
||||
|
||||
let cached_schema: ReturnType<typeof get_autofixer_schema> | null = null;
|
||||
|
||||
function get_autofixer_schema(stdio: boolean) {
|
||||
let code = v.string();
|
||||
if (stdio) {
|
||||
// we only add the description if we are running in stdio, this saves a few tokens for the remote server
|
||||
code = v.pipe(
|
||||
v.string(),
|
||||
v.description(
|
||||
"The code to be processed by the autofixer. It can also be a path to a file containing the code. If the file doesn't exists the string will be treated as the code",
|
||||
),
|
||||
);
|
||||
}
|
||||
return v.object({
|
||||
code,
|
||||
desired_svelte_version: v.pipe(
|
||||
v.union([v.string(), v.number()]),
|
||||
v.description(
|
||||
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
|
||||
),
|
||||
),
|
||||
async: v.pipe(
|
||||
v.optional(v.boolean()),
|
||||
v.description(
|
||||
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
|
||||
),
|
||||
),
|
||||
filename: v.pipe(
|
||||
v.optional(v.string()),
|
||||
v.description(
|
||||
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const autofixer_output_schema = v.object({
|
||||
issues: v.array(v.string()),
|
||||
suggestions: v.array(v.string()),
|
||||
require_another_tool_call_after_fixing: v.boolean(),
|
||||
});
|
||||
|
||||
export async function svelte_autofixer_handler({
|
||||
code,
|
||||
desired_svelte_version: desired_svelte_version_unchecked,
|
||||
async,
|
||||
filename: filename_or_path,
|
||||
}: v.InferInput<ReturnType<typeof get_autofixer_schema>>) {
|
||||
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
|
||||
const parsed_version = v.safeParse(
|
||||
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
|
||||
desired_svelte_version_unchecked,
|
||||
);
|
||||
if (parsed_version.success === false) {
|
||||
throw new Error(
|
||||
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const desired_svelte_version = parsed_version.output;
|
||||
|
||||
if (async && +desired_svelte_version < 5) {
|
||||
throw new Error('The async option can only be used with Svelte version 5 or higher.');
|
||||
}
|
||||
|
||||
const content: {
|
||||
issues: string[];
|
||||
suggestions: string[];
|
||||
require_another_tool_call_after_fixing: boolean;
|
||||
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
|
||||
try {
|
||||
// just in case the LLM sends a full path we extract the filename...it's not really needed
|
||||
// but it's nice to have a filename in the errors
|
||||
|
||||
const filename = filename_or_path ? basename(filename_or_path) : 'Component.svelte';
|
||||
|
||||
add_compile_issues(content, code, +desired_svelte_version, filename, async);
|
||||
|
||||
add_autofixers_issues(content, code, +desired_svelte_version, filename, async);
|
||||
|
||||
await add_eslint_issues(content, code, +desired_svelte_version, filename, async);
|
||||
} catch (e: unknown) {
|
||||
const error = e as Error & { start?: { line: number; column: number }; frame?: string };
|
||||
content.issues.push(
|
||||
`${error.message} at line ${error.start?.line}, column ${error.start?.column}`,
|
||||
);
|
||||
if (error.message.includes('js_parse_error')) {
|
||||
// Check if the error frame contains template syntax that was incorrectly placed in the script tag
|
||||
if (error.frame?.includes('{#snippet')) {
|
||||
content.suggestions.push(
|
||||
"The code can't be compiled because a Javascript parse error. The error suggests you have a `{#snippet ...}` block inside the `<script>` tag. Snippets are template syntax and should be declared in the markup section of the component, not in the script. Move the snippet outside of the `<script>` tag. Snippets declared in the markup can also be accessed in the script tag in case you need them.",
|
||||
);
|
||||
} else {
|
||||
content.suggestions.push(
|
||||
"The code can't be compiled because a Javascript parse error. In case you are using runes like this `$state variable_name = 3;` or `$derived variable_name = 3 * count` that's not how runes are used. You need to use them as function calls without importing them: `const variable_name = $state(3)` and `const variable_name = $derived(3 * count)`.",
|
||||
);
|
||||
}
|
||||
} else if (error.message.includes('css_expected_identifier')) {
|
||||
content.suggestions.push(
|
||||
"The code can't be compiled because a valid CSS identifier is expected. This sometimes means you are trying to use a variable in CSS like this: `color: {my_color}` but Svelte doesn't support that. You can use inline CSS variables for that `<div style:--color={my_color}></div>` and then use the variable as usual in CSS with `color: var(--color)`.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (content.issues.length > 0 || content.suggestions.length > 0) {
|
||||
content.require_another_tool_call_after_fixing = true;
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
export function svelte_autofixer(server: SvelteMcp) {
|
||||
server.tool(
|
||||
@@ -14,32 +124,12 @@ export function svelte_autofixer(server: SvelteMcp) {
|
||||
title: 'Svelte Autofixer',
|
||||
description:
|
||||
'Given a svelte component or module returns a list of suggestions to fix any issues it has. This tool MUST be used whenever the user is asking to write svelte code before sending the code back to the user',
|
||||
schema: v.object({
|
||||
code: v.string(),
|
||||
desired_svelte_version: v.pipe(
|
||||
v.union([v.string(), v.number()]),
|
||||
v.description(
|
||||
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
|
||||
),
|
||||
),
|
||||
async: v.pipe(
|
||||
v.optional(v.boolean()),
|
||||
v.description(
|
||||
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
|
||||
),
|
||||
),
|
||||
filename: v.pipe(
|
||||
v.optional(v.string()),
|
||||
v.description(
|
||||
'The filename of the component if available, it MUST be only the Component name with .svelte or .svelte.ts extension and not the entire path.',
|
||||
),
|
||||
),
|
||||
}),
|
||||
outputSchema: v.object({
|
||||
issues: v.array(v.string()),
|
||||
suggestions: v.array(v.string()),
|
||||
require_another_tool_call_after_fixing: v.boolean(),
|
||||
}),
|
||||
get schema() {
|
||||
return (
|
||||
cached_schema ?? (cached_schema = get_autofixer_schema(server.ctx.custom?.stdio ?? false))
|
||||
);
|
||||
},
|
||||
outputSchema: autofixer_output_schema,
|
||||
annotations: {
|
||||
title: 'Svelte Autofixer',
|
||||
destructiveHint: false,
|
||||
@@ -57,63 +147,37 @@ export function svelte_autofixer(server: SvelteMcp) {
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
|
||||
}
|
||||
// we validate manually because some clients don't support union in the input schema (looking at you cursor)
|
||||
const parsed_version = v.safeParse(
|
||||
v.union([v.literal(4), v.literal(5), v.literal('4'), v.literal('5')]),
|
||||
desired_svelte_version_unchecked,
|
||||
);
|
||||
if (parsed_version.success === false) {
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer-wrong-version');
|
||||
|
||||
// we only do this if we know we are running in stdio mode (only stdio pass the context as true)
|
||||
if (server.ctx.custom?.stdio) {
|
||||
const [exists_sync, read_file] = await Promise.all([
|
||||
import('node:fs').then((mod) => mod.existsSync),
|
||||
import('node:fs/promises').then((mod) => mod.readFile),
|
||||
]);
|
||||
if (exists_sync(code)) {
|
||||
code = await read_file(code, 'utf-8');
|
||||
}
|
||||
return tool.error(
|
||||
`The desired_svelte_version MUST be either 4 or 5 but received "${desired_svelte_version_unchecked}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const desired_svelte_version = parsed_version.output;
|
||||
|
||||
if (async && +desired_svelte_version < 5) {
|
||||
return tool.error('The async option can only be used with Svelte version 5 or higher.');
|
||||
}
|
||||
|
||||
const content: {
|
||||
issues: string[];
|
||||
suggestions: string[];
|
||||
require_another_tool_call_after_fixing: boolean;
|
||||
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
|
||||
try {
|
||||
// just in case the LLM sends a full path we extract the filename...it's not really needed
|
||||
// but it's nice to have a filename in the errors
|
||||
|
||||
const filename = filename_or_path ? basename(filename_or_path) : 'Component.svelte';
|
||||
|
||||
add_compile_issues(content, code, +desired_svelte_version, filename, async);
|
||||
|
||||
add_autofixers_issues(content, code, +desired_svelte_version, filename, async);
|
||||
|
||||
await add_eslint_issues(content, code, +desired_svelte_version, filename, async);
|
||||
} catch (e: unknown) {
|
||||
const error = e as Error & { start?: { line: number; column: number } };
|
||||
content.issues.push(
|
||||
`${error.message} at line ${error.start?.line}, column ${error.start?.column}`,
|
||||
);
|
||||
if (error.message.includes('js_parse_error')) {
|
||||
content.suggestions.push(
|
||||
"The code can't be compiled because a Javascript parse error. In case you are using runes like this `$state variable_name = 3;` or `$derived variable_name = 3 * count` that's not how runes are used. You need to use them as function calls without importing them: `const variable_name = $state(3)` and `const variable_name = $derived(3 * count)`.",
|
||||
);
|
||||
} else if (error.message.includes('css_expected_identifier')) {
|
||||
content.suggestions.push(
|
||||
"The code can't be compiled because a valid CSS identifier is expected. This sometimes means you are trying to use a variable in CSS like this: `color: {my_color}` but Svelte doesn't support that. You can use inline CSS variables for that `<div style:--color={my_color}></div>` and then use the variable as usual in CSS with `color: var(--color)`.",
|
||||
const content = await svelte_autofixer_handler({
|
||||
code,
|
||||
desired_svelte_version: desired_svelte_version_unchecked,
|
||||
async,
|
||||
filename: filename_or_path,
|
||||
});
|
||||
return tool.structured(content);
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
if (server.ctx.sessionId && server.ctx.custom?.track) {
|
||||
await server.ctx.custom?.track?.(
|
||||
server.ctx.sessionId,
|
||||
'svelte-autofixer-error',
|
||||
error.message,
|
||||
);
|
||||
}
|
||||
return tool.error(error.message);
|
||||
}
|
||||
|
||||
if (content.issues.length > 0 || content.suggestions.length > 0) {
|
||||
content.require_another_tool_call_after_fixing = true;
|
||||
}
|
||||
|
||||
return tool.structured(content);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
|
||||
import { McpServer } from 'tmcp';
|
||||
import { setup_prompts, setup_resources, setup_tools } from './handlers/index.js';
|
||||
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
|
||||
import type { Schema } from '@sveltejs/mcp-schema';
|
||||
import { icons } from './icons/index.js';
|
||||
|
||||
export const server = new McpServer(
|
||||
@@ -25,8 +23,8 @@ export const server = new McpServer(
|
||||
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
|
||||
},
|
||||
).withContext<{
|
||||
db: LibSQLDatabase<Schema>;
|
||||
track?: (sessionId: string, event: string, extra?: string) => Promise<void>;
|
||||
stdio?: boolean;
|
||||
}>();
|
||||
|
||||
export type SvelteMcp = typeof server;
|
||||
|
||||
@@ -69,6 +69,8 @@ export function parse(code: string, file_path: string) {
|
||||
return this.all_references.find((r) => r.identifier === id);
|
||||
},
|
||||
is_rune(call: CallExpression, rune?: (typeof runes)[number][]) {
|
||||
// it should always be a call expression but we check just in case because sometimes it's any and TS doesn't complain
|
||||
if (call.type !== 'CallExpression') return false;
|
||||
if (call.callee.type !== 'Identifier' && call.callee.type !== 'MemberExpression')
|
||||
return false;
|
||||
const id = call.callee.type === 'Identifier' ? call.callee : call.callee.object;
|
||||
|
||||
@@ -1,139 +1,197 @@
|
||||
# @sveltejs/mcp
|
||||
|
||||
## 0.1.23
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: allow stdio mcp to read the content of the file directly ([#198](https://github.com/sveltejs/ai-tools/pull/198))
|
||||
|
||||
- fix: handle non call expressions passed to `is_rune` ([#201](https://github.com/sveltejs/ai-tools/pull/201))
|
||||
|
||||
- chore: remove db requirement ([#196](https://github.com/sveltejs/ai-tools/pull/196))
|
||||
|
||||
## 0.1.22
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: broaden checks for imported runes because LLMs are unhinged ([#185](https://github.com/sveltejs/ai-tools/pull/185))
|
||||
|
||||
## 0.1.21
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: display similar result & error at the end ([#161](https://github.com/sveltejs/ai-tools/pull/161))
|
||||
|
||||
## 0.1.20
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: turn off no-inspect in eslint for mcp ([`2245cb2`](https://github.com/sveltejs/ai-tools/commit/2245cb2dc9e2d217869b6a800795ce59ffb40c51))
|
||||
|
||||
## 0.1.19
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: update svelte ([`7447744`](https://github.com/sveltejs/ai-tools/commit/74477448cea44ec21684ea4d39f2c5c7133b5150))
|
||||
|
||||
## 0.1.18
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: expose playground link as MCP App ([#138](https://github.com/sveltejs/ai-tools/pull/138))
|
||||
|
||||
## 0.1.17
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: add suggestion for snippets declared in script tag ([#132](https://github.com/sveltejs/ai-tools/pull/132))
|
||||
|
||||
## 0.1.16
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: expose tools as JS api + cli ([#128](https://github.com/sveltejs/ai-tools/pull/128))
|
||||
|
||||
## 0.1.15
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: server.json version + update publisher ([`9dfb4de`](https://github.com/sveltejs/ai-tools/commit/9dfb4dedb42837c40c4e660f0f816d7cf9081fc4))
|
||||
|
||||
## 0.1.14
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: improve prompt to reduce token usage ([#124](https://github.com/sveltejs/mcp/pull/124))
|
||||
- fix: improve prompt to reduce token usage ([#124](https://github.com/sveltejs/ai-tools/pull/124))
|
||||
|
||||
## 0.1.13
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: revert name change and add title ([`98efa1e`](https://github.com/sveltejs/mcp/commit/98efa1e09ebcca7827b10dc6bc8e1699fc1e5171))
|
||||
- fix: revert name change and add title ([`98efa1e`](https://github.com/sveltejs/ai-tools/commit/98efa1e09ebcca7827b10dc6bc8e1699fc1e5171))
|
||||
|
||||
## 0.1.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: update server name on mcp registry ([`60297b3`](https://github.com/sveltejs/mcp/commit/60297b3c49bf110b48908e61b5d5d902ea1bdf39))
|
||||
- fix: update server name on mcp registry ([`60297b3`](https://github.com/sveltejs/ai-tools/commit/60297b3c49bf110b48908e61b5d5d902ea1bdf39))
|
||||
|
||||
- chore: update tmcp ([#99](https://github.com/sveltejs/mcp/pull/99))
|
||||
- chore: update tmcp ([#99](https://github.com/sveltejs/ai-tools/pull/99))
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: add `async` parameter to `svelte-autofixer` ([#94](https://github.com/sveltejs/mcp/pull/94))
|
||||
- fix: add `async` parameter to `svelte-autofixer` ([#94](https://github.com/sveltejs/ai-tools/pull/94))
|
||||
|
||||
- fix: install latest eslint svelte packages to support `$state.eager` ([`f6ce89f`](https://github.com/sveltejs/mcp/commit/f6ce89ff34faabc3d746a350ea347298ecfed2ec))
|
||||
- fix: install latest eslint svelte packages to support `$state.eager` ([`f6ce89f`](https://github.com/sveltejs/ai-tools/commit/f6ce89ff34faabc3d746a350ea347298ecfed2ec))
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: add icons to `server.json` ([`02c951b`](https://github.com/sveltejs/mcp/commit/02c951baa86ac8103ffc158a202c06cfe6b15c01))
|
||||
- fix: add icons to `server.json` ([`02c951b`](https://github.com/sveltejs/ai-tools/commit/02c951baa86ac8103ffc158a202c06cfe6b15c01))
|
||||
|
||||
- fix: add `preferred-frame-size` to UI resource ([`3fabcc0`](https://github.com/sveltejs/mcp/commit/3fabcc0f9bfee916c0deb9c2ffa931ed2168af2d))
|
||||
- fix: add `preferred-frame-size` to UI resource ([`3fabcc0`](https://github.com/sveltejs/ai-tools/commit/3fabcc0f9bfee916c0deb9c2ffa931ed2168af2d))
|
||||
|
||||
- feat: support: `$state.eager` ([#90](https://github.com/sveltejs/mcp/pull/90))
|
||||
- feat: support: `$state.eager` ([#90](https://github.com/sveltejs/ai-tools/pull/90))
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: return `mcp-ui` resource from `playground-link` ([#84](https://github.com/sveltejs/mcp/pull/84))
|
||||
- feat: return `mcp-ui` resource from `playground-link` ([#84](https://github.com/sveltejs/ai-tools/pull/84))
|
||||
|
||||
- feat: suggest against js variables in css ([#78](https://github.com/sveltejs/mcp/pull/78))
|
||||
- feat: suggest against js variables in css ([#78](https://github.com/sveltejs/ai-tools/pull/78))
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: upgrade registry publisher cli ([`5fa2baa`](https://github.com/sveltejs/mcp/commit/5fa2baa27009f01e0e4e91cee7984b81a81c1c29))
|
||||
- fix: upgrade registry publisher cli ([`5fa2baa`](https://github.com/sveltejs/ai-tools/commit/5fa2baa27009f01e0e4e91cee7984b81a81c1c29))
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: use correct server schema version ([`579be87`](https://github.com/sveltejs/mcp/commit/579be877fa9f87f7f173450ca5bc918824d68282))
|
||||
- fix: use correct server schema version ([`579be87`](https://github.com/sveltejs/ai-tools/commit/579be877fa9f87f7f173450ca5bc918824d68282))
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: prevent `imported_runes` suggestion from being added for libs that are not svelte ([`87af64f`](https://github.com/sveltejs/mcp/commit/87af64f4bc6d07b75640eb987a33655654363997))
|
||||
- fix: prevent `imported_runes` suggestion from being added for libs that are not svelte ([`87af64f`](https://github.com/sveltejs/ai-tools/commit/87af64f4bc6d07b75640eb987a33655654363997))
|
||||
|
||||
- feat: add svelte icon and website url for mcp server ([#75](https://github.com/sveltejs/mcp/pull/75))
|
||||
- feat: add svelte icon and website url for mcp server ([#75](https://github.com/sveltejs/ai-tools/pull/75))
|
||||
|
||||
- fix: use `data:` uri for local icon & add icons to tools + resources + prompts ([`cf62286`](https://github.com/sveltejs/mcp/commit/cf622869129382a97ad059bb1389f115907adc8e))
|
||||
- fix: use `data:` uri for local icon & add icons to tools + resources + prompts ([`cf62286`](https://github.com/sveltejs/ai-tools/commit/cf622869129382a97ad059bb1389f115907adc8e))
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: widen `desired_svelte_version` validation to accommodate some clients ([`3b301d7`](https://github.com/sveltejs/mcp/commit/3b301d7d9c2f49758023408f505bc4ca79caaff4))
|
||||
- fix: widen `desired_svelte_version` validation to accommodate some clients ([`3b301d7`](https://github.com/sveltejs/ai-tools/commit/3b301d7d9c2f49758023408f505bc4ca79caaff4))
|
||||
|
||||
- fix: minor tweaks to the prompt to allow for automatic sync ([#63](https://github.com/sveltejs/mcp/pull/63))
|
||||
- fix: minor tweaks to the prompt to allow for automatic sync ([#63](https://github.com/sveltejs/ai-tools/pull/63))
|
||||
|
||||
- feat: `read_state_with_dollar` autofixer ([#66](https://github.com/sveltejs/mcp/pull/66))
|
||||
- feat: `read_state_with_dollar` autofixer ([#66](https://github.com/sveltejs/ai-tools/pull/66))
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: pass secrets in action and update `mcpName` ([`044f098`](https://github.com/sveltejs/mcp/commit/044f0988b935fff39911a861a648dfb276f5831a))
|
||||
- fix: pass secrets in action and update `mcpName` ([`044f098`](https://github.com/sveltejs/ai-tools/commit/044f0988b935fff39911a861a648dfb276f5831a))
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: use DNS to publish MCP ([#59](https://github.com/sveltejs/mcp/pull/59))
|
||||
- fix: use DNS to publish MCP ([#59](https://github.com/sveltejs/ai-tools/pull/59))
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: publish to MCP registry (I really hope this time for real) ([`ef5241c`](https://github.com/sveltejs/mcp/commit/ef5241cbc204ad8bb84bde27db7c9d0a08280245))
|
||||
- fix: publish to MCP registry (I really hope this time for real) ([`ef5241c`](https://github.com/sveltejs/ai-tools/commit/ef5241cbc204ad8bb84bde27db7c9d0a08280245))
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: publish mcp to registry (maybe for real this time) ([`132943d`](https://github.com/sveltejs/mcp/commit/132943db3b04dbbd322d08926c0880c990a61f5f))
|
||||
- feat: publish mcp to registry (maybe for real this time) ([`132943d`](https://github.com/sveltejs/ai-tools/commit/132943db3b04dbbd322d08926c0880c990a61f5f))
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat: publish to registry ([#45](https://github.com/sveltejs/mcp/pull/45))
|
||||
- feat: publish to registry ([#45](https://github.com/sveltejs/ai-tools/pull/45))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: add autofixer to tell the LLM to check if some function called in effect is assigning state #26 ([`73d7625`](https://github.com/sveltejs/mcp/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
|
||||
- feat: add autofixer to tell the LLM to check if some function called in effect is assigning state #26 ([`73d7625`](https://github.com/sveltejs/ai-tools/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
|
||||
|
||||
- feat: add bind:this -> attachment and action -> attachment autofixer #20 ([`73d7625`](https://github.com/sveltejs/mcp/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
|
||||
- feat: add bind:this -> attachment and action -> attachment autofixer #20 ([`73d7625`](https://github.com/sveltejs/ai-tools/commit/73d7625b3ca6a812ba91883ea668d80ff1e7c703))
|
||||
|
||||
## 0.0.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: allow TS `.svelte.ts` modules ([#49](https://github.com/sveltejs/mcp/pull/49))
|
||||
- fix: allow TS `.svelte.ts` modules ([#49](https://github.com/sveltejs/ai-tools/pull/49))
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: check effect.pre in assign-in-effect ([#41](https://github.com/sveltejs/mcp/pull/41))
|
||||
- fix: check effect.pre in assign-in-effect ([#41](https://github.com/sveltejs/ai-tools/pull/41))
|
||||
|
||||
- feat: `use_cases` documentation metadata ([#29](https://github.com/sveltejs/mcp/pull/29))
|
||||
- feat: `use_cases` documentation metadata ([#29](https://github.com/sveltejs/ai-tools/pull/29))
|
||||
|
||||
- fix: change title names to allow for claude code to use the prompt ([`725f785`](https://github.com/sveltejs/mcp/commit/725f785766d04e9ed810a7c3f6bcfdb2e2b8234c))
|
||||
- fix: change title names to allow for claude code to use the prompt ([`725f785`](https://github.com/sveltejs/ai-tools/commit/725f785766d04e9ed810a7c3f6bcfdb2e2b8234c))
|
||||
|
||||
- fix: enable doc tools ([`cb316c5`](https://github.com/sveltejs/mcp/commit/cb316c5b3ebc712946969d2d57236d159e796d58))
|
||||
- fix: enable doc tools ([`cb316c5`](https://github.com/sveltejs/ai-tools/commit/cb316c5b3ebc712946969d2d57236d159e796d58))
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: latest version ([#25](https://github.com/sveltejs/mcp/pull/25))
|
||||
- feat: latest version ([#25](https://github.com/sveltejs/ai-tools/pull/25))
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
aada17479e04faa55e380ae1ee0de3c5a5d86b4c0ad5dd4b64f68426f87c84e0 mcp-publisher_darwin_amd64.tar.gz
|
||||
d426c953218a61699bb47d1dae6f0b89807313f082d650f243b8a91bc7f448f3 mcp-publisher_darwin_amd64.tar.gz.sbom.json
|
||||
75a14ff2570bca0eba656dee1d4ad4b2955de67e97aacc5884bcab1793d8dd2d mcp-publisher_darwin_arm64.tar.gz
|
||||
988adc169f9ce4cded2b08ab0df7c1738f8ea77dccb581a234f7e036ad7b478e mcp-publisher_darwin_arm64.tar.gz.sbom.json
|
||||
dc97c003df75d40134b9bf003077ac9057187d64f8d601c4f2457980712dbb62 mcp-publisher_linux_amd64.tar.gz
|
||||
49e63cd1c1ade8988f6c6f679b3172ab28b5ae6ef92243a4f8a78c58150853c7 mcp-publisher_linux_amd64.tar.gz.sbom.json
|
||||
094e70c73ff671da69b138b448c68336f582e9245355d469c8372a48639bbc6c mcp-publisher_linux_arm64.tar.gz
|
||||
3888ef7db7a77efeb3cfb711527a00ed93188ff3fce05468a6308ae75e2bb913 mcp-publisher_linux_arm64.tar.gz.sbom.json
|
||||
43a1f3ca680350dd898697c93e4e0a1489c2b638dc946c4ef496a815c0e46d53 mcp-publisher_windows_amd64.tar.gz
|
||||
512ac8c24275845bcc9c4c4faf4f08d950de3585e13d283e1aa48c567735ad73 mcp-publisher_windows_amd64.tar.gz.sbom.json
|
||||
739347d69d4979faf06365843c49ede92e16975cd1afe3ef627e9511459fa30b mcp-publisher_windows_arm64.tar.gz
|
||||
b8ecb1cf98caef446447a1ad89f7259699632f09bd19d600c89fd800aa3c7698 mcp-publisher_windows_arm64.tar.gz.sbom.json
|
||||
5b4d676da2a574ee0d3cfd752d9d3a663e675d48d928706dda0084271ded1cb5 registry-1.3.10.tar.gz
|
||||
d5140c2730991dfc236305d3e6eddb6c05da9325d9803b1eaee50c06953d5991 registry_darwin_amd64.tar.gz
|
||||
fd1380e0d308a1702d040427603d203a9407603e1156cd23743a3074e1313b06 registry_darwin_amd64.tar.gz.sbom.json
|
||||
db22e6b0f518b1ccaf44f749babc28d70d2573ba5641b06ff085430826eac688 registry_darwin_arm64.tar.gz
|
||||
4415141c7b490b1c7d9643e799df5a2885f3292b4c1c3e4c71f2841aae52fdeb registry_darwin_arm64.tar.gz.sbom.json
|
||||
8fbb607254b412513eeec45c89fb3051fe9ce623196f6361ded1d335a5348566 registry_linux_amd64.tar.gz
|
||||
c256554e7060cbfc92494f9b22c4e857b0606ca57479a29fa61649917c63c974 registry_linux_amd64.tar.gz.sbom.json
|
||||
86ff45f0c5afc674e9b30ec47815ab54401d716b12d70f27bd73f72d5b5ad2f8 registry_linux_arm64.tar.gz
|
||||
0ccb7a3d856a7951bea5d5e63314459b365199577f573c2fbcd11e7e98b9b94c registry_linux_arm64.tar.gz.sbom.json
|
||||
f56d75a2569e6e37f44d8f7bede0bd01f8056130f8eab58b1211d55d43fa08fe registry_windows_amd64.tar.gz
|
||||
d941a82dd69937edff791b4f84b61006d78802280fb0c06d15c07a5d90d6e0a0 registry_windows_amd64.tar.gz.sbom.json
|
||||
b2e6fff3cd9bab76d4431294b2844e1e9a399d45728e5459492b21367da62453 registry_windows_arm64.tar.gz
|
||||
1954a54f47048e1ac25c92196440f0c2b1ba2357e01720d5b14e6faf43be46b2 registry_windows_arm64.tar.gz.sbom.json
|
||||
25
packages/mcp-stdio/checksums/registry_1.4.0_checksums.txt
Normal file
25
packages/mcp-stdio/checksums/registry_1.4.0_checksums.txt
Normal file
@@ -0,0 +1,25 @@
|
||||
eb5f89b76fc45a97070fa481eb03977584a78e3dff2781402d43482f114e4d6a mcp-publisher_darwin_amd64.tar.gz
|
||||
29fc1b46a6f6be2580129369a9b681204b11b425d9a44147d79c8c658c7b8474 mcp-publisher_darwin_amd64.tar.gz.sbom.json
|
||||
9eddbbb95efd54b9503f6c0668f43bab3f04c856946d3c7164f6daead232402f mcp-publisher_darwin_arm64.tar.gz
|
||||
a8349b0ea7916f34cf4ee4e1ced5b91bc1ded6d100312cbb2095da018710da04 mcp-publisher_darwin_arm64.tar.gz.sbom.json
|
||||
c4b402b43a85166c3f840641ca1c9e6de5bfa1cf533c22576d663ccbda0711bb mcp-publisher_linux_amd64.tar.gz
|
||||
6f21cf917055be885f16b93154e06379540236599cfad6af4404a8323bde74b7 mcp-publisher_linux_amd64.tar.gz.sbom.json
|
||||
ba5d486f86b2cef48ea506e8314d901a5169dcd56a5d6e9daf18d41244316235 mcp-publisher_linux_arm64.tar.gz
|
||||
8a0096a407b916ac7270732df017a26f6112c73a066252f6556b8956c492a0b4 mcp-publisher_linux_arm64.tar.gz.sbom.json
|
||||
59ee8c4a997f94794db8db13f8809666631686a70a8d89a9f0fea993f9aede0f mcp-publisher_windows_amd64.tar.gz
|
||||
57d211fd9181f698d126bd773b55c98b92454d19b1e32e77860766179a8a2e8e mcp-publisher_windows_amd64.tar.gz.sbom.json
|
||||
1410952b0a5968cbe89590e7b4ee6105147ef7267cf0cd50095c9bec2ee3b0d7 mcp-publisher_windows_arm64.tar.gz
|
||||
6cb93e118a89ed1419135bfbaa7401bd3b7a7c5680a0d8fd7c78728f9d860630 mcp-publisher_windows_arm64.tar.gz.sbom.json
|
||||
ebc17c3b7a5b86f9c036acf1d44fb904bb363bad0ac1ac37b7979eb17cf3d218 registry-1.4.0.tar.gz
|
||||
5fffe8b078513fa5fbb625a213d164bb391c7c85e216e541cab789517bc6365b registry_darwin_amd64.tar.gz
|
||||
10a61cf4173d8b5be63044af0a10e6c809eebc1006c0c1643753a252db808ddd registry_darwin_amd64.tar.gz.sbom.json
|
||||
437746a1045f093266ad7298a47be41dc44cc33ecaeec145449c4eefeddf8880 registry_darwin_arm64.tar.gz
|
||||
4c0cb24bcef0658540fb044d771294efd662edecd2f7fae7b1ca7ca2ae68f83a registry_darwin_arm64.tar.gz.sbom.json
|
||||
bd77fafcc881714a63a375a9bdb53a761a2b8e367a9d2759835126c993df2356 registry_linux_amd64.tar.gz
|
||||
d62a461b174089fbcaa4f1ac096bca491d18bc7f70e93ce0824fe89dbe42e974 registry_linux_amd64.tar.gz.sbom.json
|
||||
38cb9e6112ff11544ba8ec88c5c0f44d4c851504ec2e33d497e25616a6f7a21e registry_linux_arm64.tar.gz
|
||||
84e5a004929e7231ae35350a1fe8fb08668fc05183e13d1d78004abf08a25f3b registry_linux_arm64.tar.gz.sbom.json
|
||||
77ca9243d2f744f282b39d07625f802d77f593850a0392debabe407643a8579c registry_windows_amd64.tar.gz
|
||||
bb043e8a6a8d187ffab8987d36d0018024115d9217af6d2ddd233f94aea880ea registry_windows_amd64.tar.gz.sbom.json
|
||||
e4ee50e05a95f288b874f5e24c7716a5032548feeabc83b715193298cec06890 registry_windows_arm64.tar.gz
|
||||
49f189b615bce3d09ea24ae5d80e0816ac69225b3c8901dd7be19ef9ca06830c registry_windows_arm64.tar.gz.sbom.json
|
||||
@@ -1,24 +1,30 @@
|
||||
{
|
||||
"name": "@sveltejs/mcp",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.23",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"mcpName": "dev.svelte/mcp",
|
||||
"homepage": "https://github.com/sveltejs/mcp#readme",
|
||||
"homepage": "https://github.com/sveltejs/ai-tools#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/mcp/issues"
|
||||
"url": "https://github.com/sveltejs/ai-tools/issues"
|
||||
},
|
||||
"bin": {
|
||||
"svelte-mcp": "./dist/index.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/mcp.git",
|
||||
"url": "git+https://github.com/sveltejs/ai-tools.git",
|
||||
"path": "packages/mcp-stdio"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/handlers.d.mts",
|
||||
"import": "./dist/handlers.mjs"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
@@ -41,6 +47,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint": "catalog:lint",
|
||||
"sade": "catalog:tooling",
|
||||
"tmcp": "catalog:tmcp"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-10-17/server.schema.json",
|
||||
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
||||
"name": "dev.svelte/mcp",
|
||||
"title": "Svelte MCP",
|
||||
"description": "The official Svelte MCP server providing docs and autofixing tools for Svelte development",
|
||||
"repository": {
|
||||
"id": "1054419133",
|
||||
"url": "https://github.com/sveltejs/mcp",
|
||||
"url": "https://github.com/sveltejs/ai-tools",
|
||||
"subfolder": "packages/mcp-stdio",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.23",
|
||||
"websiteUrl": "https://svelte.dev/docs/mcp/overview",
|
||||
"icons": [
|
||||
{
|
||||
@@ -25,7 +25,7 @@
|
||||
{
|
||||
"registryType": "npm",
|
||||
"identifier": "@sveltejs/mcp",
|
||||
"version": "0.1.14",
|
||||
"version": "0.1.23",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
6
packages/mcp-stdio/src/handlers.ts
Normal file
6
packages/mcp-stdio/src/handlers.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
list_sections_handler as listSections,
|
||||
get_documentation_handler as getDocumentation,
|
||||
svelte_autofixer_handler as svelteAutofixer,
|
||||
playground_link_handler as playgroundLink,
|
||||
} from '@sveltejs/mcp-server/handlers';
|
||||
@@ -1,7 +1,67 @@
|
||||
#! /usr/bin/env node
|
||||
import { server } from '@sveltejs/mcp-server';
|
||||
import {
|
||||
list_sections_handler,
|
||||
get_documentation_handler,
|
||||
svelte_autofixer_handler,
|
||||
} from '@sveltejs/mcp-server/handlers';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { StdioTransport } from '@tmcp/transport-stdio';
|
||||
import sade from 'sade';
|
||||
|
||||
const transport = new StdioTransport(server);
|
||||
const cli = sade('svelte-mcp');
|
||||
|
||||
transport.listen();
|
||||
cli.command('__mcp', '', { default: true }).action(() => {
|
||||
const transport = new StdioTransport(server);
|
||||
transport.listen({
|
||||
stdio: true,
|
||||
});
|
||||
});
|
||||
|
||||
cli
|
||||
.command('list-sections')
|
||||
.describe('List all the available documentation sections')
|
||||
.action(async () => {
|
||||
console.log(await list_sections_handler());
|
||||
});
|
||||
|
||||
cli
|
||||
.command('get-documentation <sections>')
|
||||
.describe('Get documentation for specified sections, separated by commas')
|
||||
.action(async (sections) => {
|
||||
console.log(await get_documentation_handler({ section: sections.split(',') }));
|
||||
});
|
||||
|
||||
cli
|
||||
.command('svelte-autofixer <code_or_path>')
|
||||
.describe(
|
||||
'Detect and suggest fixes for Svelte code issues, because the terminal will substitute variables `$` should be correctly escaped',
|
||||
)
|
||||
.option('--async', 'Wether the project is using async svelte or not', false)
|
||||
.option('--svelte-version', 'Which version of svelte to use...it can be 4 or 5', 5)
|
||||
.action(async (code_or_path, { async, 'svelte-version': version }) => {
|
||||
let code = code_or_path;
|
||||
|
||||
let is_path = false;
|
||||
|
||||
if (existsSync(code_or_path)) {
|
||||
console.log('Detected file path, reading file...');
|
||||
code = await readFile(code_or_path, 'utf-8');
|
||||
is_path = true;
|
||||
} else {
|
||||
console.log('File not found, treating input as code...');
|
||||
}
|
||||
|
||||
const desired_svelte_version = +version;
|
||||
|
||||
const result = await svelte_autofixer_handler({
|
||||
code,
|
||||
async: Boolean(async),
|
||||
desired_svelte_version,
|
||||
filename: is_path ? code_or_path : undefined,
|
||||
});
|
||||
console.log(result);
|
||||
});
|
||||
|
||||
cli.parse(process.argv);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
entry: ['./src/index.ts'],
|
||||
entry: ['./src/index.ts', './src/handlers.ts'],
|
||||
platform: 'node',
|
||||
define: {
|
||||
// some eslint-plugin-svelte code expects __filename to exists but in an ESM environment it does not.
|
||||
@@ -13,9 +13,12 @@ export default defineConfig([
|
||||
// the require would fail once executed in a project without eslint installed.
|
||||
external: ['eslint'],
|
||||
publint: true,
|
||||
dts: false,
|
||||
dts: {
|
||||
eager: true,
|
||||
},
|
||||
treeshake: true,
|
||||
clean: true,
|
||||
target: 'esnext',
|
||||
inlineOnly: false,
|
||||
},
|
||||
]);
|
||||
|
||||
73
packages/opencode/CHANGELOG.md
Normal file
73
packages/opencode/CHANGELOG.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# @sveltejs/opencode
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: add `server` export to opencode plugin ([`96c50ac`](https://github.com/sveltejs/ai-tools/commit/96c50acae2b4131a6c72d3579a73c44ab9158b18))
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: import `ts` files directly ([#190](https://github.com/sveltejs/ai-tools/pull/190))
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: sync skills from svelte.dev ([#178](https://github.com/sveltejs/ai-tools/pull/178))
|
||||
|
||||
- fix: update svelte-file-editor agent to use proper name ([#183](https://github.com/sveltejs/ai-tools/pull/183))
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: merge user-configured svelte-file-editor agent settings ([#176](https://github.com/sveltejs/ai-tools/pull/176))
|
||||
|
||||
- feat(opencode): mcp enabled option is passed to opencode ([#171](https://github.com/sveltejs/ai-tools/pull/171))
|
||||
|
||||
- feat: allow enabling a specific skill in opencode plugin ([#174](https://github.com/sveltejs/ai-tools/pull/174))
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: `svelte-core-bestpractices` skill ([#162](https://github.com/sveltejs/ai-tools/pull/162))
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: better subagent instructions to use MCP or skill ([#163](https://github.com/sveltejs/ai-tools/pull/163))
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: allow for local opencode config ([#156](https://github.com/sveltejs/ai-tools/pull/156))
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: actually push skills to right config path ([`c2c1b3e`](https://github.com/sveltejs/ai-tools/commit/c2c1b3e5e788b14eea17cd37a83ca55433cc4072))
|
||||
|
||||
## 0.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- feat: distribute skills through opencode plugin ([#151](https://github.com/sveltejs/ai-tools/pull/151))
|
||||
|
||||
## 0.0.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: add README to opencode plugin ([`71295bc`](https://github.com/sveltejs/ai-tools/commit/71295bc11fb7bac6703e655f5fddead29967353c))
|
||||
|
||||
## 0.0.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: show toast after a few seconds to avoid race condition ([`57e2d1d`](https://github.com/sveltejs/ai-tools/commit/57e2d1def1f5590d0a3dd6d269ac39f6397ffecf))
|
||||
112
packages/opencode/README.md
Normal file
112
packages/opencode/README.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# @sveltejs/opencode
|
||||
|
||||
OpenCode plugin for Svelte that provides the Svelte MCP server, a specialized file editor subagent and instruction files.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `@sveltejs/opencode` to your OpenCode config (either global or local):
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["@sveltejs/opencode"]
|
||||
}
|
||||
```
|
||||
|
||||
That's it! You now have the Svelte MCP server and the file editor subagent configured automatically.
|
||||
|
||||
## Features
|
||||
|
||||
### Svelte MCP Server
|
||||
|
||||
The plugin automatically configures the [Svelte MCP server](https://mcp.svelte.dev) which provides:
|
||||
|
||||
- **list-sections** - Discover available Svelte 5 and SvelteKit documentation sections
|
||||
- **get-documentation** - Retrieve full documentation content for specific sections
|
||||
- **svelte-autofixer** - Analyze Svelte code and get issues/suggestions
|
||||
- **playground-link** - Generate Svelte Playground links with provided code
|
||||
|
||||
### Svelte File Editor Subagent
|
||||
|
||||
A specialized subagent (`svelte-file-editor`) that is automatically used when creating, editing, or reviewing `.svelte`, `.svelte.ts`, or `.svelte.js` files. It fetches relevant documentation and validates code using the Svelte MCP server tools.
|
||||
|
||||
### Agent Instructions
|
||||
|
||||
The plugin injects instructions that teach the agent how to effectively use the Svelte MCP tools.
|
||||
|
||||
## Configuration
|
||||
|
||||
Create `svelte.json` to customize how the plugin configures MCP, the Svelte subagent, instructions, and skills.
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://svelte.dev/opencode/schema.json",
|
||||
"mcp": {
|
||||
"type": "remote",
|
||||
"enabled": true
|
||||
},
|
||||
"subagent": {
|
||||
"enabled": true,
|
||||
"agents": {
|
||||
"svelte-file-editor": {
|
||||
"model": "anthropic/claude-sonnet-4-20250514",
|
||||
"temperature": 0.7,
|
||||
"top_p": 0.9,
|
||||
"maxSteps": 20
|
||||
}
|
||||
}
|
||||
},
|
||||
"instructions": {
|
||||
"enabled": true
|
||||
},
|
||||
"skills": {
|
||||
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Defaults
|
||||
|
||||
If omitted, the plugin uses these defaults:
|
||||
|
||||
- `mcp.type`: `"remote"`
|
||||
- `mcp.enabled`: `true`
|
||||
- `subagent.enabled`: `true`
|
||||
- `subagent.agents`: `{}`
|
||||
- `instructions.enabled`: `true`
|
||||
- `skills.enabled`: `true`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Option | Type | Default | Description |
|
||||
| ------------------------------------------------ | --------------------- | ---------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `mcp.type` | `"remote" \| "local"` | `"remote"` | Use `https://mcp.svelte.dev/mcp` (`remote`) or run `@sveltejs/mcp` via `npx` (`local`). |
|
||||
| `mcp.enabled` | `boolean` | `true` | Enable or disable the Svelte MCP server entry. |
|
||||
| `subagent.enabled` | `boolean` | `true` | Enable or disable registration of the `svelte-file-editor` subagent. |
|
||||
| `subagent.agents.svelte-file-editor.model` | `string` | main agent | Override the model used by the Svelte file editor subagent. |
|
||||
| `subagent.agents.svelte-file-editor.temperature` | `number` | unset | Set temperature for the subagent. |
|
||||
| `subagent.agents.svelte-file-editor.top_p` | `number` | unset | Set top-p sampling for the subagent. |
|
||||
| `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. |
|
||||
|
||||
### Supported Skill Names
|
||||
|
||||
When using `skills.enabled` as an array, these built-in names are currently available:
|
||||
|
||||
- `svelte-code-writer`
|
||||
- `svelte-core-bestpractices`
|
||||
|
||||
### Config File Locations and Precedence
|
||||
|
||||
The plugin reads from these files (lowest priority first, highest priority last):
|
||||
|
||||
- `~/.config/opencode/svelte.json`
|
||||
- `$OPENCODE_CONFIG_DIR/svelte.json` (when `OPENCODE_CONFIG_DIR` is set)
|
||||
- `.opencode/svelte.json` in the current project
|
||||
|
||||
If the same key is defined in multiple files, the later location overrides earlier ones.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
11
packages/opencode/agents.ts
Normal file
11
packages/opencode/agents.ts
Normal 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 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;
|
||||
229
packages/opencode/config.ts
Normal file
229
packages/opencode/config.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
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';
|
||||
|
||||
// Schema for individual agent configuration
|
||||
const agent_config_schema = v.object({
|
||||
model: v.pipe(
|
||||
v.optional(v.string()),
|
||||
v.description('Model identifier for the agent (e.g., "anthropic/claude-sonnet-4-20250514")'),
|
||||
),
|
||||
temperature: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description('Temperature setting for the agent (e.g., 0.7)'),
|
||||
),
|
||||
top_p: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description(
|
||||
'Control response diversity with the top_p option. Alternative to temperature for controlling randomness.',
|
||||
),
|
||||
),
|
||||
maxSteps: v.pipe(
|
||||
v.optional(v.number()),
|
||||
v.description('Maximum number of steps the agent can take (e.g., 10)'),
|
||||
),
|
||||
});
|
||||
|
||||
const default_config = {
|
||||
mcp: {
|
||||
type: 'remote' as 'remote' | 'local',
|
||||
enabled: true,
|
||||
},
|
||||
subagent: {
|
||||
enabled: true,
|
||||
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
|
||||
},
|
||||
instructions: {
|
||||
enabled: true,
|
||||
},
|
||||
skills: {
|
||||
enabled: true as boolean | string[],
|
||||
},
|
||||
};
|
||||
|
||||
export const config_schema = v.object({
|
||||
mcp: v.pipe(
|
||||
v.optional(
|
||||
v.object({
|
||||
type: v.optional(v.picklist(['remote', 'local'])),
|
||||
enabled: v.optional(v.boolean()),
|
||||
}),
|
||||
),
|
||||
v.description(
|
||||
"Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'.",
|
||||
),
|
||||
),
|
||||
subagent: v.pipe(
|
||||
v.optional(
|
||||
v.object({
|
||||
enabled: v.optional(v.boolean()),
|
||||
agents: v.optional(v.record(v.string(), agent_config_schema)),
|
||||
}),
|
||||
),
|
||||
v.description('Configuration for the subagent. You can choose if it should be enabled or not.'),
|
||||
),
|
||||
instructions: v.pipe(
|
||||
v.optional(
|
||||
v.object({
|
||||
enabled: v.optional(v.boolean()),
|
||||
}),
|
||||
),
|
||||
v.description(
|
||||
'Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not.',
|
||||
),
|
||||
),
|
||||
skills: v.pipe(
|
||||
v.optional(
|
||||
v.object({
|
||||
enabled: v.pipe(
|
||||
v.optional(v.union([v.boolean(), v.array(v.string())])),
|
||||
v.description(
|
||||
'It can be either a boolean or an array containing the skills that you want to enable',
|
||||
),
|
||||
),
|
||||
}),
|
||||
),
|
||||
v.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.',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
export type McpConfig = v.InferInput<typeof config_schema>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function get_config_paths() {
|
||||
// Global: ~/.config/opencode/svelte.json
|
||||
let global_path: string | null = 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;
|
||||
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
|
||||
if (opencode_config_dir) {
|
||||
const config_json = join(opencode_config_dir, 'svelte.json');
|
||||
if (existsSync(config_json)) {
|
||||
config_dir_path = config_json;
|
||||
}
|
||||
}
|
||||
|
||||
// Project-local: ./.opencode/svelte.json (cwd)
|
||||
let project_path: string | null = null;
|
||||
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
|
||||
if (existsSync(project_config)) {
|
||||
project_path = project_config;
|
||||
}
|
||||
|
||||
// Lowest priority first, highest priority last (project overrides global)
|
||||
return [global_path, config_dir_path, project_path];
|
||||
}
|
||||
|
||||
function load_config_file(config_path: string): ConfigLoadResult {
|
||||
let file_content: string;
|
||||
try {
|
||||
file_content = readFileSync(config_path, 'utf-8');
|
||||
} catch {
|
||||
// File doesn't exist or can't be read
|
||||
return { data: null };
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(file_content);
|
||||
if (parsed === undefined || parsed === null) {
|
||||
return { data: null, parse_error: 'Config file is empty or invalid' };
|
||||
}
|
||||
return { data: parsed };
|
||||
} catch (error: unknown) {
|
||||
return {
|
||||
data: null,
|
||||
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
return {
|
||||
mcp: {
|
||||
...default_config.mcp,
|
||||
...user_config.mcp,
|
||||
},
|
||||
subagent: {
|
||||
enabled: default_config.subagent.enabled,
|
||||
...user_config.subagent,
|
||||
agents: {
|
||||
...default_config.subagent.agents,
|
||||
...user_config.subagent?.agents,
|
||||
},
|
||||
},
|
||||
instructions: {
|
||||
...default_config.instructions,
|
||||
...user_config.instructions,
|
||||
},
|
||||
skills: {
|
||||
...default_config.skills,
|
||||
...user_config.skills,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function get_mcp_config(ctx: PluginInput) {
|
||||
const config_paths = get_config_paths();
|
||||
let merged: Partial<McpConfig> = {};
|
||||
|
||||
// Iterate from lowest to highest priority, merging as we go
|
||||
for (const path of config_paths) {
|
||||
if (path && existsSync(path)) {
|
||||
const result = load_config_file(path);
|
||||
if (result.parse_error) {
|
||||
setTimeout(() => {
|
||||
ctx.client.tui.showToast({
|
||||
body: {
|
||||
title: 'Svelte: Invalid opencode plugin config',
|
||||
message: `${result.parse_error} (${path})\nSkipping this config file`,
|
||||
variant: 'warning',
|
||||
duration: 7000,
|
||||
},
|
||||
});
|
||||
}, 7000);
|
||||
continue;
|
||||
}
|
||||
const parsed = v.safeParse(config_schema, result.data);
|
||||
if (parsed.success) {
|
||||
merged = {
|
||||
mcp: { ...merged.mcp, ...parsed.output.mcp },
|
||||
subagent: {
|
||||
...merged.subagent,
|
||||
...parsed.output.subagent,
|
||||
agents: { ...merged.subagent?.agents, ...parsed.output.subagent?.agents },
|
||||
},
|
||||
instructions: { ...merged.instructions, ...parsed.output.instructions },
|
||||
skills: { ...merged.skills, ...parsed.output.skills },
|
||||
};
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
ctx.client.tui.showToast({
|
||||
body: {
|
||||
title: 'Svelte: Invalid opencode plugin config',
|
||||
message: `Invalid config schema (${path})\nSkipping this config file`,
|
||||
variant: 'warning',
|
||||
duration: 7000,
|
||||
},
|
||||
});
|
||||
}, 7000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merge_with_defaults(merged);
|
||||
}
|
||||
112
packages/opencode/index.ts
Normal file
112
packages/opencode/index.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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';
|
||||
|
||||
const current_dir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const svelte_plugin: Plugin = async (ctx) => {
|
||||
return {
|
||||
async config(input) {
|
||||
input.agent ??= {};
|
||||
input.mcp ??= {};
|
||||
input.instructions ??= [];
|
||||
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
|
||||
input.skills ??= {};
|
||||
// @ts-expect-error -- types are wrong in the opencode package...will fix there and remove this
|
||||
input.skills.paths ??= [];
|
||||
// by default we use svelte as the name for the svelte MCP server
|
||||
let svelte_mcp_name = 'svelte';
|
||||
// we loop over every mcp server to see if any of them is already the svelte MCP server
|
||||
for (const name in input.mcp) {
|
||||
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')))
|
||||
) {
|
||||
// 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');
|
||||
const instructions_paths = await readdir(instructions_dir);
|
||||
input.instructions.push(...instructions_paths.map((file) => join(instructions_dir, file)));
|
||||
}
|
||||
|
||||
const skills_enabled = mcp_config.skills?.enabled;
|
||||
if (skills_enabled !== false) {
|
||||
const skills_dir = join(current_dir, 'skills');
|
||||
if (Array.isArray(skills_enabled)) {
|
||||
// only add specific skill directories by name
|
||||
for (const skill_name of skills_enabled) {
|
||||
const skill_path = join(skills_dir, skill_name);
|
||||
// @ts-expect-error -- skills is a new opencode feature
|
||||
input.skills.paths.push(skill_path);
|
||||
}
|
||||
} else {
|
||||
// @ts-expect-error -- skills is a new opencode feature
|
||||
input.skills.paths.push(skills_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// if the user doesn't have the MCP server already we add one based on config
|
||||
if (!input.mcp[svelte_mcp_name]) {
|
||||
if (mcp_config.mcp?.type === 'remote') {
|
||||
input.mcp[svelte_mcp_name] = {
|
||||
type: 'remote',
|
||||
url: 'https://mcp.svelte.dev/mcp',
|
||||
enabled: mcp_config.mcp?.enabled ?? true,
|
||||
};
|
||||
} else {
|
||||
input.mcp[svelte_mcp_name] = {
|
||||
type: 'local',
|
||||
command: ['npx', '-y', '@sveltejs/mcp'],
|
||||
enabled: mcp_config.mcp?.enabled ?? true,
|
||||
};
|
||||
}
|
||||
}
|
||||
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] = {
|
||||
color: '#ff3e00',
|
||||
mode: 'subagent',
|
||||
prompt: agent_data.prompt,
|
||||
description: agent_data.description,
|
||||
permission: {
|
||||
[`${svelte_mcp_name}_*`]: 'allow',
|
||||
},
|
||||
};
|
||||
|
||||
// Get per-agent config from svelte.json (if any)
|
||||
const agent_config = mcp_config.subagent?.agents?.[agent_name];
|
||||
|
||||
// Configure agent from svelte.json only
|
||||
// Priority: svelte.json agent config > defaults
|
||||
input.agent[agent_name] = {
|
||||
...default_config,
|
||||
...(agent_config?.model !== undefined && {
|
||||
model: agent_config.model,
|
||||
}),
|
||||
...(agent_config?.temperature !== undefined && {
|
||||
temperature: agent_config.temperature,
|
||||
}),
|
||||
...(agent_config?.maxSteps !== undefined && {
|
||||
maxSteps: agent_config.maxSteps,
|
||||
}),
|
||||
...(agent_config?.top_p !== undefined && {
|
||||
top_p: agent_config.top_p,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
23
packages/opencode/instructions/opencode-agents.md
Normal file
23
packages/opencode/instructions/opencode-agents.md
Normal file
@@ -0,0 +1,23 @@
|
||||
You are able to use the Svelte MCP server, where you have access to comprehensive Svelte 5 and SvelteKit documentation. Here's how to use the available tools effectively:
|
||||
|
||||
## Available Svelte MCP Tools:
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
Use this FIRST to discover all available documentation sections. Returns a structured list with titles, use_cases, and paths.
|
||||
When asked about Svelte or SvelteKit topics, ALWAYS use this tool at the start of the chat to find relevant sections.
|
||||
|
||||
### 2. get-documentation
|
||||
|
||||
Retrieves full documentation content for specific sections. Accepts single or multiple sections.
|
||||
After calling the list-sections tool, you MUST analyze the returned documentation sections (especially the use_cases field) and then use the get-documentation tool to fetch ALL documentation sections that are relevant for the user's task.
|
||||
|
||||
### 3. svelte-autofixer
|
||||
|
||||
Analyzes Svelte code and returns issues and suggestions.
|
||||
You MUST use this tool whenever writing Svelte code before sending it to the user. Keep calling it until no issues or suggestions are returned.
|
||||
|
||||
### 4. playground-link
|
||||
|
||||
Generates a Svelte Playground link with the provided code.
|
||||
After completing the code, ask the user if they want a playground link. Only call this tool after user confirmation and NEVER if code was written to files in their project.
|
||||
43
packages/opencode/package.json
Normal file
43
packages/opencode/package.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"name": "@sveltejs/opencode",
|
||||
"version": "0.1.8",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/sveltejs/ai-tools#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/sveltejs/ai-tools/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"config.ts",
|
||||
"agents.ts",
|
||||
"instructions",
|
||||
"skills"
|
||||
],
|
||||
"exports": {
|
||||
"./server": {
|
||||
"types": "./index.ts",
|
||||
"import": "./index.ts"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/sveltejs/ai-tools.git",
|
||||
"path": "packages/opencode"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"valibot": "catalog:tooling"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "catalog:ai",
|
||||
"@valibot/to-json-schema": "catalog:tooling",
|
||||
"@types/node": "catalog:tooling"
|
||||
}
|
||||
}
|
||||
111
packages/opencode/schema.json
Normal file
111
packages/opencode/schema.json
Normal file
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mcp": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote",
|
||||
"local"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "Configuration for the MCP. You can chose if it should be enabled or not and the transport to use 'remote' (default) and 'local'."
|
||||
},
|
||||
"subagent": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"agents": {
|
||||
"type": "object",
|
||||
"propertyNames": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"svelte-file-editor"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"model": {
|
||||
"type": "string",
|
||||
"description": "Model identifier for the agent (e.g., \"anthropic/claude-sonnet-4-20250514\")"
|
||||
},
|
||||
"temperature": {
|
||||
"type": "number",
|
||||
"description": "Temperature setting for the agent (e.g., 0.7)"
|
||||
},
|
||||
"top_p": {
|
||||
"type": "number",
|
||||
"description": "Control response diversity with the top_p option. Alternative to temperature for controlling randomness."
|
||||
},
|
||||
"maxSteps": {
|
||||
"type": "number",
|
||||
"description": "Maximum number of steps the agent can take (e.g., 10)"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "Configuration for the subagent. You can choose if it should be enabled or not."
|
||||
},
|
||||
"instructions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"description": "Configuration for the automatic AGENTS.md injection. You can choose if it should be enabled or not."
|
||||
},
|
||||
"skills": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"svelte-code-writer",
|
||||
"svelte-core-bestpractices"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"description": "It can be either a boolean or an array containing the skills that you want to enable"
|
||||
}
|
||||
},
|
||||
"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."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
"$schema": "http://json-schema.org/draft-07/schema#"
|
||||
}
|
||||
60
packages/opencode/scripts/generate-schema.ts
Normal file
60
packages/opencode/scripts/generate-schema.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { toJsonSchema } from '@valibot/to-json-schema';
|
||||
import { config_schema } from '../config.js';
|
||||
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) {
|
||||
if (!fs.existsSync(agents_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(agents_dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
||||
.map((entry) => entry.name.replace(/\.md$/, ''));
|
||||
}
|
||||
|
||||
function get_skill_names(skills_dir: string) {
|
||||
if (!fs.existsSync(skills_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(skills_dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory())
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
|
||||
const skills_dir = path.resolve('./skills');
|
||||
const skill_names = get_skill_names(skills_dir);
|
||||
const schema = config_schema;
|
||||
const json_schema = toJsonSchema(schema);
|
||||
|
||||
// Post-process: inject skill name suggestions into the items 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;
|
||||
if (enabled?.anyOf) {
|
||||
const array_branch = enabled.anyOf.find((s: Record<string, unknown>) => s.type === 'array');
|
||||
if (array_branch) {
|
||||
array_branch.items = {
|
||||
anyOf: [{ enum: skill_names }, { type: 'string' }],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Post-process: inject known agent names for intellisense
|
||||
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
|
||||
// editors will autocomplete the known names but any string is still valid.
|
||||
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;
|
||||
if (agents) {
|
||||
agents.propertyNames = {
|
||||
anyOf: [{ enum: agent_names }, { type: 'string' }],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.resolve('./schema.json'), JSON.stringify(json_schema, null, '\t'));
|
||||
66
packages/opencode/skills/svelte-code-writer/SKILL.md
Normal file
66
packages/opencode/skills/svelte-code-writer/SKILL.md
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
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
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
```
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
```
|
||||
|
||||
Retrieves full documentation for specified sections. Use after `list-sections` to fetch relevant docs.
|
||||
|
||||
**Example:**
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
```
|
||||
|
||||
Analyzes Svelte code and suggests fixes for common issues.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--async` - Enable async Svelte mode (default: false)
|
||||
- `--svelte-version` - Target version: 4 or 5 (default: 5)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Analyze inline code (escape $ as \$)
|
||||
npx @sveltejs/mcp svelte-autofixer '<script>let count = \$state(0);</script>'
|
||||
|
||||
# Analyze a file
|
||||
npx @sveltejs/mcp svelte-autofixer ./src/lib/Component.svelte
|
||||
|
||||
# Target Svelte 4
|
||||
npx @sveltejs/mcp svelte-autofixer ./Component.svelte --svelte-version 4
|
||||
```
|
||||
|
||||
**Important:** When passing code with runes (`$state`, `$derived`, etc.) via the terminal, escape the `$` character as `\$` to prevent shell variable substitution.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Uncertain about syntax?** Run `list-sections` then `get-documentation` for relevant topics
|
||||
2. **Reviewing/debugging?** Run `svelte-autofixer` on the code to detect issues
|
||||
3. **Always validate** - Run `svelte-autofixer` before finalizing any Svelte component
|
||||
176
packages/opencode/skills/svelte-core-bestpractices/SKILL.md
Normal file
176
packages/opencode/skills/svelte-core-bestpractices/SKILL.md
Normal file
@@ -0,0 +1,176 @@
|
||||
---
|
||||
name: svelte-core-bestpractices
|
||||
description: Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.
|
||||
---
|
||||
|
||||
## `$state`
|
||||
|
||||
Only use the `$state` rune for variables that should be _reactive_ — in other words, variables that cause an `$effect`, `$derived` or template expression to update. Everything else can be a normal variable.
|
||||
|
||||
Objects and arrays (`$state({...})` or `$state([...])`) are made deeply reactive, meaning mutation will trigger updates. This has a trade-off: in exchange for fine-grained reactivity, the objects must be proxied, which has performance overhead. In cases where you're dealing with large objects that are only ever reassigned (rather than mutated), use `$state.raw` instead. This is often the case with API responses, for example.
|
||||
|
||||
## `$derived`
|
||||
|
||||
To compute something from state, use `$derived` rather than `$effect`:
|
||||
|
||||
```js
|
||||
// do this
|
||||
let square = $derived(num * num);
|
||||
|
||||
// don't do this
|
||||
let square;
|
||||
|
||||
$effect(() => {
|
||||
square = num * num;
|
||||
});
|
||||
```
|
||||
|
||||
> [!NOTE] `$derived` is given an expression, _not_ a function. If you need to use a function (because the expression is complex, for example) use `$derived.by`.
|
||||
|
||||
Deriveds are writable — you can assign to them, just like `$state`, except that they will re-evaluate when their expression changes.
|
||||
|
||||
If the derived expression is an object or array, it will be returned as-is — it is _not_ made deeply reactive. You can, however, use `$state` inside `$derived.by` in the rare cases that you need this.
|
||||
|
||||
## `$effect`
|
||||
|
||||
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 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 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.
|
||||
|
||||
## `$props`
|
||||
|
||||
Treat props as though they will change. For example, values that depend on props should usually use `$derived`:
|
||||
|
||||
```js
|
||||
// @errors: 2451
|
||||
let { type } = $props();
|
||||
|
||||
// do this
|
||||
let color = $derived(type === 'danger' ? 'red' : 'green');
|
||||
|
||||
// don't do this — `color` will not update if `type` changes
|
||||
let color = type === 'danger' ? 'red' : 'green';
|
||||
```
|
||||
|
||||
## `$inspect.trace`
|
||||
|
||||
`$inspect.trace` is a debugging tool for reactivity. If something is not updating properly or running more than it should you can add `$inspect.trace(label)` as the first line of an `$effect` or `$derived.by` (or any function they call) to trace their dependencies and discover which one triggered an update.
|
||||
|
||||
## Events
|
||||
|
||||
Any element attribute starting with `on` is treated as an event listener:
|
||||
|
||||
```svelte
|
||||
<button onclick={() => {...}}>click me</button>
|
||||
|
||||
<!-- attribute shorthand also works -->
|
||||
<button {onclick}>...</button>
|
||||
|
||||
<!-- so do spread attributes -->
|
||||
<button {...props}>...</button>
|
||||
```
|
||||
|
||||
If you need to attach listeners to `window` or `document` you can use `<svelte:window>` and `<svelte:document>`:
|
||||
|
||||
```svelte
|
||||
<svelte:window onkeydown={...} />
|
||||
<svelte:document onvisibilitychange={...} />
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
```svelte
|
||||
{#snippet greeting(name)}
|
||||
<p>hello {name}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render greeting('world')}
|
||||
```
|
||||
|
||||
> [!NOTE] Snippets declared at the top level of a component (i.e. not inside elements or blocks) can be referenced inside `<script>`. A snippet that doesn't reference component state is also available in a `<script module>`, in which case it can be exported for use by other components.
|
||||
|
||||
## Each blocks
|
||||
|
||||
Prefer to use [keyed each blocks](references/each.md) — this improves performance by allowing Svelte to surgically insert or remove items rather than updating the DOM belonging to existing items.
|
||||
|
||||
> [!NOTE] The key _must_ uniquely identify the object. Do not use the index as a key.
|
||||
|
||||
Avoid destructuring if you need to mutate the item (with something like `bind:value={item.count}`, for example).
|
||||
|
||||
## Using JavaScript variables in CSS
|
||||
|
||||
If you have a JS variable that you want to use inside CSS you can set a CSS custom property with the `style:` directive.
|
||||
|
||||
```svelte
|
||||
<div style:--columns={columns}>...</div>
|
||||
```
|
||||
|
||||
You can then reference `var(--columns)` inside the component's `<style>`.
|
||||
|
||||
## Styling child components
|
||||
|
||||
The CSS in a component's `<style>` is scoped to that component. If a parent component needs to control the child's styles, the preferred way is to use CSS custom properties:
|
||||
|
||||
```svelte
|
||||
<!-- Parent.svelte -->
|
||||
<Child --color="red" />
|
||||
|
||||
<!-- Child.svelte -->
|
||||
<h1>Hello</h1>
|
||||
|
||||
<style>
|
||||
h1 {
|
||||
color: var(--color);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
If this is impossible (for example, the child component comes from a library) you can use `:global` to override styles:
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
<Child />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div :global {
|
||||
h1 {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
Consider using context instead of declaring state in a shared module. This will scope the state to the part of the app that needs it, and eliminate the possibility of it leaking between users when server-side rendering.
|
||||
|
||||
Use `createContext` rather than `setContext` and `getContext`, as it provides type safety.
|
||||
|
||||
## Async Svelte
|
||||
|
||||
If using version 5.36 or higher, you can use [await expressions](references/await-expressions.md) and [hydratable](references/hydratable.md) to use promises directly inside components. Note that these require the `experimental.async` option to be enabled in `svelte.config.js` as they are not yet considered fully stable.
|
||||
|
||||
## Avoid legacy features
|
||||
|
||||
Always use runes mode for new code, and avoid features that have more modern replacements:
|
||||
|
||||
- use `$state` instead of implicit reactivity (e.g. `let count = 0; count += 1`)
|
||||
- use `$derived` and `$effect` instead of `$:` assignments and statements (but only use effects when there is no better solution)
|
||||
- use `$props` instead of `export let`, `$$props` and `$$restProps`
|
||||
- use `onclick={...}` instead of `on:click={...}`
|
||||
- use `{#snippet ...}` and `{@render ...}` instead of `<slot>` and `$$slots` and `<svelte:fragment>`
|
||||
- use `<DynamicComponent>` instead of `<svelte:component this={DynamicComponent}>`
|
||||
- use `import Self from './ThisComponent.svelte'` and `<Self>` instead of `<svelte:self>`
|
||||
- use classes with `$state` fields to share reactivity between components, instead of using stores
|
||||
- use `{@attach ...}` instead of `use:action`
|
||||
- use clsx-style arrays and objects in `class` attributes, instead of the `class:` directive
|
||||
@@ -0,0 +1,53 @@
|
||||
> [!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:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
|
||||
$inspect(count, message); // will console.log when `count` or `message` change
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
$inspect(count).with((type, count) => {
|
||||
if (type === 'update') {
|
||||
debugger; // or `console.trace`, or whatever you want
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
## $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.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { doSomeWork } from './elsewhere';
|
||||
|
||||
$effect(() => {
|
||||
+++// $inspect.trace must be the first statement of a function body+++
|
||||
+++$inspect.trace();+++
|
||||
doSomeWork();
|
||||
});
|
||||
</script>
|
||||
```
|
||||
|
||||
`$inspect.trace` takes an optional first argument which will be used as the label.
|
||||
@@ -0,0 +1,166 @@
|
||||
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.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
> [!NOTE]
|
||||
> Attachments are available in Svelte 5.29 and newer.
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/attachments').Attachment} */
|
||||
function myAttachment(element) {
|
||||
console.log(element.nodeName); // 'DIV'
|
||||
|
||||
return () => {
|
||||
console.log('cleaning up');
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<div {@attach myAttachment}>...</div>
|
||||
```
|
||||
|
||||
An element can have any number of attachments.
|
||||
|
||||
## Attachment factories
|
||||
|
||||
A useful pattern is for a function, such as `tooltip` in this example, to _return_ an attachment (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<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).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
Attachments can also be created inline (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<canvas
|
||||
width={32}
|
||||
height={32}
|
||||
{@attach (canvas) => {
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
$effect(() => {
|
||||
context.fillStyle = color;
|
||||
context.fillRect(0, 0, canvas.width, canvas.height);
|
||||
});
|
||||
}}
|
||||
></canvas>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> The nested effect runs whenever `color` changes, while the outer effect (where `canvas.getContext(...)` is called) only runs once, since it doesn't read any reactive state.
|
||||
|
||||
## Conditional attachments
|
||||
|
||||
Falsy values like `false` or `undefined` are treated as no attachment, enabling conditional usage:
|
||||
|
||||
```svelte
|
||||
<div {@attach enabled && myAttachment}>...</div>
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
/** @type {import('svelte/elements').HTMLButtonAttributes} */
|
||||
let { children, ...props } = $props();
|
||||
</script>
|
||||
|
||||
<!-- `props` includes attachments -->
|
||||
<button {...props}>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import tippy from 'tippy.js';
|
||||
import Button from './Button.svelte';
|
||||
|
||||
let content = $state('Hello!');
|
||||
|
||||
/**
|
||||
* @param {string} content
|
||||
* @returns {import('svelte/attachments').Attachment}
|
||||
*/
|
||||
function tooltip(content) {
|
||||
return (element) => {
|
||||
const tooltip = tippy(element, { content });
|
||||
return tooltip.destroy;
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<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`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(bar) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
update(node, bar);
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
In the rare case that this is a problem (for example, if `foo` does expensive and unavoidable setup work) consider passing the data inside a function and reading it in a child effect:
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
function foo(+++getBar+++) {
|
||||
return (node) => {
|
||||
veryExpensiveSetupWork(node);
|
||||
|
||||
+++ $effect(() => {
|
||||
update(node, getBar());
|
||||
});+++
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#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.
|
||||
@@ -0,0 +1,35 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
<p>{a} + {b} = {a + b}</p>
|
||||
{/snippet}
|
||||
|
||||
{@render sum(1, 2)}
|
||||
{@render sum(3, 4)}
|
||||
{@render sum(5, 6)}
|
||||
```
|
||||
|
||||
The expression can be an identifier like `sum`, or an arbitrary JavaScript expression:
|
||||
|
||||
```svelte
|
||||
{@render (cool ? coolSnippet : lameSnippet)()}
|
||||
```
|
||||
|
||||
## Optional snippets
|
||||
|
||||
If the snippet is potentially undefined — for example, because it's an incoming prop — then you can use optional chaining to only render it when it _is_ defined:
|
||||
|
||||
```svelte
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<p>fallback content</p>
|
||||
{/if}
|
||||
```
|
||||
@@ -0,0 +1,180 @@
|
||||
As of Svelte 5.36, you can use the `await` keyword inside your components in three places where it was previously unavailable:
|
||||
|
||||
- at the top level of your component's `<script>`
|
||||
- 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`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
The experimental flag will be removed in Svelte 6.
|
||||
|
||||
## Synchronized updates
|
||||
|
||||
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...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
|
||||
async function add(a, b) {
|
||||
await new Promise((f) => setTimeout(f, 500)); // artificial delay
|
||||
return a + b;
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
<p>2 + 2 = 3</p>
|
||||
```
|
||||
|
||||
— instead, the text will update to `2 + 2 = 4` when `add(a, b)` resolves.
|
||||
|
||||
Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.
|
||||
|
||||
## Concurrency
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
|
||||
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());
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#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.
|
||||
|
||||
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.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
|
||||
async function onclick() {
|
||||
updating = true;
|
||||
|
||||
// without this, the change to `updating` will be
|
||||
// grouped with the other changes, meaning it
|
||||
// won't be reflected in the UI
|
||||
await tick();
|
||||
|
||||
color = 'octarine';
|
||||
answer = 42;
|
||||
|
||||
await settled();
|
||||
|
||||
// any updates affected by `color` or `answer`
|
||||
// have now been applied
|
||||
updating = false;
|
||||
}
|
||||
```
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
Svelte supports asynchronous server-side rendering (SSR) with the `render(...)` API. To use it, simply await the return value:
|
||||
|
||||
```js
|
||||
/// file: server.js
|
||||
import { render } from 'svelte/server';
|
||||
import App from './App.svelte';
|
||||
|
||||
const { head, body } = +++await+++ render(App);
|
||||
```
|
||||
|
||||
> [!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.
|
||||
|
||||
If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All `await` expressions encountered outside boundaries with `pending` snippets will resolve and render their contents prior to `await render(...)` returning.
|
||||
|
||||
> [!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.
|
||||
|
||||
## 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.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { fork } from 'svelte';
|
||||
import Menu from './Menu.svelte';
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
/** @type {import('svelte').Fork | null} */
|
||||
let pending = null;
|
||||
|
||||
function preload() {
|
||||
pending ??= fork(() => {
|
||||
open = true;
|
||||
});
|
||||
}
|
||||
|
||||
function discard() {
|
||||
pending?.discard();
|
||||
pending = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
onfocusin={preload}
|
||||
onfocusout={discard}
|
||||
onpointerenter={preload}
|
||||
onpointerleave={discard}
|
||||
onclick={() => {
|
||||
pending?.commit();
|
||||
pending = null;
|
||||
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Caveats
|
||||
|
||||
As an experimental feature, the details of how `await` is handled (and related APIs like `$effect.pending()`) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
Effects run in a slightly different order when the `experimental.async` option is `true`. Specifically, _block_ effects like `{#if ...}` and `{#each ...}` now run before an `$effect.pre` or `beforeUpdate` in the same component, which means that in very rare situations.
|
||||
@@ -0,0 +1,16 @@
|
||||
## Function bindings
|
||||
|
||||
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())} />
|
||||
```
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Function bindings are available in Svelte 5.9.0 and newer.
|
||||
@@ -0,0 +1,42 @@
|
||||
## Keyed each blocks
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#each expression as name (key)}...{/each}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#each expression as name, index (key)}...{/each}
|
||||
```
|
||||
|
||||
If a _key_ expression is provided — which must uniquely identify each list item — Svelte will use it to intelligently update the list when data changes by inserting, moving and deleting items, rather than adding or removing items at the end and updating the state in the middle.
|
||||
|
||||
The key can be any object, but strings and numbers are recommended since they allow identity to persist when the objects themselves change.
|
||||
|
||||
```svelte
|
||||
{#each items as item (item.id)}
|
||||
<li>{item.name} x {item.qty}</li>
|
||||
{/each}
|
||||
|
||||
<!-- or with additional index value -->
|
||||
{#each items as item, i (item.id)}
|
||||
<li>{i + 1}: {item.name} x {item.qty}</li>
|
||||
{/each}
|
||||
```
|
||||
|
||||
You can freely use destructuring and rest patterns in each blocks.
|
||||
|
||||
```svelte
|
||||
{#each items as { id, name, qty }, i (id)}
|
||||
<li>{i + 1}: {name} x {qty}</li>
|
||||
{/each}
|
||||
|
||||
{#each objects as { id, ...rest }}
|
||||
<li><span>{id}</span><MyComponent {...rest} /></li>
|
||||
{/each}
|
||||
|
||||
{#each items as [id, ...rest]}
|
||||
<li><span>{id}</span><MyComponent values={rest} /></li>
|
||||
{/each}
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
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();
|
||||
</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).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
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());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:
|
||||
|
||||
```ts
|
||||
import { hydratable } from 'svelte';
|
||||
const rand = hydratable('random', () => Math.random());
|
||||
```
|
||||
|
||||
If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.
|
||||
|
||||
## Serialization
|
||||
|
||||
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
{await promises.two}
|
||||
```
|
||||
|
||||
## CSP
|
||||
|
||||
`hydratable` adds an inline `<script>` block to the `head` returned from `render`. If you're using [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) (CSP), this script will likely fail to run. You can provide a `nonce` to `render`:
|
||||
|
||||
```js
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
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}'`);
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
If instead you are generating static HTML ahead of time, you must use hashes instead:
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
});
|
||||
```
|
||||
|
||||
`hashes.script` will be an array of strings like `["sha256-abcd123"]`. As with `nonce`, the hashes should be used in your CSP header:
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'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.
|
||||
@@ -0,0 +1,276 @@
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#snippet name()}...{/snippet}
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- copy: false --->
|
||||
{#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...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
</a>
|
||||
{:else}
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
...you can write this:
|
||||
|
||||
```svelte
|
||||
{#snippet figure(image)}
|
||||
<figure>
|
||||
<img src={image.src} alt={image.caption} width={image.width} height={image.height} />
|
||||
<figcaption>{image.caption}</figcaption>
|
||||
</figure>
|
||||
{/snippet}
|
||||
|
||||
{#each images as image}
|
||||
{#if image.href}
|
||||
<a href={image.href}>
|
||||
{@render figure(image)}
|
||||
</a>
|
||||
{:else}
|
||||
{@render figure(image)}
|
||||
{/if}
|
||||
{/each}
|
||||
```
|
||||
|
||||
Like function declarations, snippets can have an arbitrary number of parameters, which can have default values, and you can destructure each parameter. You cannot use rest parameters, however.
|
||||
|
||||
## 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...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
|
||||
{#snippet hello(name)}
|
||||
<p>hello {name}! {message}!</p>
|
||||
{/snippet}
|
||||
|
||||
{@render hello('alice')}
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
<div>
|
||||
{#snippet x()}
|
||||
{#snippet y()}...{/snippet}
|
||||
|
||||
<!-- this is fine -->
|
||||
{@render y()}
|
||||
{/snippet}
|
||||
|
||||
<!-- this will error, as `y` is not in scope -->
|
||||
{@render y()}
|
||||
</div>
|
||||
|
||||
<!-- this will also error, as `x` is not in scope -->
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
|
||||
```svelte
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
|
||||
{#snippet countdown(n)}
|
||||
{#if n > 0}
|
||||
<span>{n}...</span>
|
||||
{@render countdown(n - 1)}
|
||||
{:else}
|
||||
{@render blastoff()}
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```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>
|
||||
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
<th>qty</th>
|
||||
<th>price</th>
|
||||
<th>total</th>
|
||||
{/snippet}
|
||||
|
||||
{#snippet row(d)}
|
||||
<td>{d.name}</td>
|
||||
<td>{d.qty}</td>
|
||||
<td>{d.price}</td>
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
</Table>
|
||||
```
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Button.svelte --->
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<!-- result will be <button>click me</button> -->
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
> [!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
|
||||
|
||||
You can declare snippet props as being optional. You can either use optional chaining to not render anything if the snippet isn't set...
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
...or use an `#if` block to render fallback content:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
## Typing snippets
|
||||
|
||||
Snippets implement the `Snippet` interface imported from `'svelte'`:
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
data: any[];
|
||||
children: Snippet;
|
||||
row: Snippet<[any]>;
|
||||
}
|
||||
|
||||
let { data, children, row }: Props = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
With this change, red squigglies will appear if you try and use the component without providing a `data` prop and a `row` snippet. Notice that the type argument provided to `Snippet` is a tuple, since snippets can have multiple parameters.
|
||||
|
||||
We can tighten things up further by declaring a generic, so that `data` and `row` refer to the same type:
|
||||
|
||||
```svelte
|
||||
<script lang="ts" generics="T">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
row: Snippet<[T]>;
|
||||
} = $props();
|
||||
</script>
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```svelte
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
|
||||
{#snippet add(a, b)}
|
||||
{a} + {b} = {a + b}
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
> [!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 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.
|
||||
@@ -0,0 +1,61 @@
|
||||
## createSubscriber
|
||||
|
||||
<blockquote class="since note">
|
||||
|
||||
Available since 5.7.0
|
||||
|
||||
</blockquote>
|
||||
|
||||
Returns a `subscribe` function that integrates external event-based systems with Svelte's reactivity.
|
||||
It's particularly useful for integrating with web APIs like `MediaQuery`, `IntersectionObserver`, or `WebSocket`.
|
||||
|
||||
If `subscribe` is called inside an effect (including indirectly, for example inside a getter),
|
||||
the `start` callback will be called with an `update` function. Whenever `update` is called, the effect re-runs.
|
||||
|
||||
If `start` returns a cleanup function, it will be called when the effect is destroyed.
|
||||
|
||||
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):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
import { createSubscriber } from 'svelte/reactivity';
|
||||
import { on } from 'svelte/events';
|
||||
|
||||
export class MediaQuery {
|
||||
#query;
|
||||
#subscribe;
|
||||
|
||||
constructor(query) {
|
||||
this.#query = window.matchMedia(`(${query})`);
|
||||
|
||||
this.#subscribe = createSubscriber((update) => {
|
||||
// when the `change` event occurs, re-run any effects that read `this.current`
|
||||
const off = on(this.#query, 'change', update);
|
||||
|
||||
// stop listening when all the effects are destroyed
|
||||
return () => off();
|
||||
});
|
||||
}
|
||||
|
||||
get current() {
|
||||
// This makes the getter reactive, if read in an effect
|
||||
this.#subscribe();
|
||||
|
||||
// Return the current state of the query, whether or not we're in an effect
|
||||
return this.#query.matches;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<div class="ts-block">
|
||||
|
||||
```dts
|
||||
function createSubscriber(
|
||||
start: (update: () => void) => (() => void) | void
|
||||
): () => void;
|
||||
```
|
||||
|
||||
</div>
|
||||
9
packages/opencode/tsconfig.json
Normal file
9
packages/opencode/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["index.ts", "config.ts", "agents.ts", "scripts/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user