mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-08-03 09:04:16 +08:00
Compare commits
17 Commits
@sveltejs/
...
@sveltejs/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c240d44eda | ||
|
|
c2bbc2a4e7 | ||
|
|
022324b0ed | ||
|
|
d2571d81ad | ||
|
|
ed4272d55f | ||
|
|
ab14e46ec3 | ||
|
|
d6c694c778 | ||
|
|
a52c8b63bb | ||
|
|
8152ed9fd4 | ||
|
|
cccb61197f | ||
|
|
51bf537e23 | ||
|
|
35b6a5a51c | ||
|
|
8a7763868a | ||
|
|
7239d7cb13 | ||
|
|
ff78a85ddf | ||
|
|
8984bb0303 | ||
|
|
3fa951f1b6 |
195
.agents/skills/writing-great-skills/GLOSSARY.md
Normal file
195
.agents/skills/writing-great-skills/GLOSSARY.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Glossary — Building Great Skills
|
||||
|
||||
The domain model for what makes a skill great. A skill exists to wrangle determinism out of a stochastic system; the root virtue is **Predictability**, and every term below is a lever on it. This is the disclosed reference for [`writing-great-skills`](SKILL.md).
|
||||
|
||||
The terms are grouped by axis: **Invocation** (how a skill is reached), **Information Hierarchy** (how its content is arranged), **Steering** (how the agent's runtime behaviour is shaped), and **Pruning** (how it is kept lean). Each **failure mode** lives beside the lever that cures it, tagged _failure mode_.
|
||||
|
||||
**Bold terms** in any definition are themselves defined in this glossary; find them by their heading.
|
||||
|
||||
## Predictability
|
||||
|
||||
The degree to which a skill makes the agent behave the same _way_ on every run — the same process, not the same output (a brainstorming skill should _predictably_ diverge; its tokens vary, its behaviour doesn't). The root virtue every other term serves — cost and maintainability are symptoms of it, not rivals.
|
||||
|
||||
_Avoid_: consistency, reliability, robustness, output-determinism
|
||||
|
||||
## Invocation
|
||||
|
||||
How a skill is reached — and the two loads you pay for the choice.
|
||||
|
||||
### Model-Invoked
|
||||
|
||||
A skill that keeps its **description** field, so the agent can see it and fire it autonomously — and the human can still type its name, so model-invocation always _includes_ user reach. There is no model-only state: a description only ever _adds_ agent discovery, never removes the human's. Pays a permanent **context load** on every turn in exchange for that discoverability. Reachable by other skills, because the description that makes it agent-discoverable makes it invocable. A model-invoked skill whose content is all **reference** is also one home for shared reference: another skill can invoke it, so reference needed by several skills lives in one place. Pick model-invocation only when the agent must reach the skill on its own; if it never fires except by hand, drop the description and pay no context load.
|
||||
|
||||
_Avoid_: ability, tool, capability
|
||||
|
||||
### User-Invoked
|
||||
|
||||
A skill with its **description** stripped — invisible to the agent and reachable only by the human typing its name (user-_only_, where **model-invoked** is user-_and-agent_). Trades agent-discoverability for zero **context load**. Because it has no description, nothing but the human can reach it: no other skill can fire it.
|
||||
|
||||
_Avoid_: procedure, workflow, command
|
||||
|
||||
### Description
|
||||
|
||||
The skill's machine-readable trigger, and the one **context pointer** a **model-invoked** skill is forced to keep loaded at all times. Its mere presence _is_ the invocation axis: keep it and the skill is model-invoked (and reachable by other skills); delete it and the skill is **user-invoked**, reachable only by the human. The source of a model-invoked skill's **context load**.
|
||||
|
||||
_Avoid_: frontmatter, summary
|
||||
|
||||
### Context Pointer
|
||||
|
||||
A reference held in the agent's context that names some out-of-context material and encodes the condition for reaching it. The **description** is the top-level context pointer (context window → skill); pointers to disclosed files are the same object one level down. Its wording, not the target, decides _when_ the agent reaches — and _how reliably_. A must-have target behind a weakly worded pointer is a variance bug: fix the wording first, and inline the material only if sharpening fails.
|
||||
|
||||
_Avoid_: link, reference, import
|
||||
|
||||
### Context Load
|
||||
|
||||
The cost a **model-invoked** skill imposes on the agent's context window — its **description**, always loaded, spending both tokens and attention. What **user-invoked** skills escape by having no description, and the brake on splitting into more model-invoked skills.
|
||||
|
||||
_Avoid_: token cost, context bloat
|
||||
|
||||
### Cognitive Load
|
||||
|
||||
The cost a **user-invoked** skill imposes on the human — what they must hold in their head: which skills exist and when to reach for each (the human is the index). What **model-invocation** removes by being agent-discoverable, and the brake on splitting into more user-invoked skills. Not a cost to minimise: it is the price of human agency, the reason some skills stay user-invoked. Spend it where human judgement matters; remove it where it does not.
|
||||
|
||||
_Avoid_: human index, burden, overhead
|
||||
|
||||
### Router Skill
|
||||
|
||||
A **user-invoked** skill whose job is to point at your other user-invoked skills — naming each and when to reach for it — so the human has one skill to remember instead of many. It can only hint, never fire them: user-invoked skills have no **description**, so nothing but the human can reach them. The cure for **cognitive load** when user-invoked skills multiply.
|
||||
|
||||
_Avoid_: dispatcher, menu, registry, index, router procedure
|
||||
|
||||
### Granularity
|
||||
|
||||
How finely you divide skills. Finer division spends one of the two loads: more **model-invoked** skills spend **context load** (more descriptions crowding the window and competing for attention); more **user-invoked** skills spend **cognitive load** (more for the human to remember and reach for). Two cuts guide the division. By **invocation**, split off a model-invoked skill where you have a distinct **leading word** to trigger it — a trigger word you actually use in your prompts. By **sequence**, split a run of **steps** where a step's **post-completion steps** need hiding, since isolating it in its own context clears what follows. Beware the reverse: merging sequences exposes each step's post-completion steps to what follows, inviting premature completion.
|
||||
|
||||
_Avoid_: chunking, modularity
|
||||
|
||||
## Information Hierarchy
|
||||
|
||||
How a skill's content is arranged, and how far down the ladder each piece sits.
|
||||
|
||||
### Information Hierarchy
|
||||
|
||||
A skill's content ranked by how immediately the agent needs it — a single ladder, produced by two cuts: in-file or behind a pointer, and step or reference. The rungs:
|
||||
|
||||
- **Steps** — in-file, primary
|
||||
- **Reference**, in-file — secondary
|
||||
- **Reference**, disclosed — behind a **context pointer**
|
||||
|
||||
A skill with no **steps** uses just the bottom two rungs — often a legitimately flat peer-set (e.g. every rule of a review on one rung), which is a fine arrangement, not a smell. The hierarchy is independent of invocation: a skill can be model- or user-invoked whether it is all steps, all reference, or both. When a skill has steps, in-file reference that should be disclosed buries them and turns attending to them into a coin-flip — a variance lever, not just a legibility one. Keep the top of the ladder legible; push down it whatever you can.
|
||||
|
||||
_Avoid_: structure, organization, layout
|
||||
|
||||
### Steps
|
||||
|
||||
The ordered actions the agent performs — when a skill has them, the primary tier of its content, and the part that earns its place in SKILL.md. Not every skill has steps: a skill can be all steps (`tdd`), all **reference** (a review), or both, independent of invocation. Every step ends on a **completion criterion**, clear or vague.
|
||||
|
||||
_Avoid_: workflow, instructions, choreography
|
||||
|
||||
### Reference
|
||||
|
||||
Material the agent refers to on demand — definitions, facts, parameters, examples, conditional instructions. When a skill has **steps** it is secondary to them; when a skill has none it is the entire content; or it lives outside any skill entirely — see **External Reference**. Reached via **context pointers**, and the prime candidate for **progressive disclosure**.
|
||||
|
||||
_Avoid_: supporting material, docs, background
|
||||
|
||||
### External Reference
|
||||
|
||||
**Reference** that lives outside the skill system — a plain file, no **description**, no **steps**, not invocable — that any skill can point at. The home for shared reference that needn't fire on its own, and the only shared home two **user-invoked** skills can use, since neither has a description and so neither can fire the other.
|
||||
|
||||
_Avoid_: doc, resource, knowledge base
|
||||
|
||||
### Progressive Disclosure
|
||||
|
||||
Moving **reference** down the ladder — out of SKILL.md and behind a **context pointer** — so the top stays legible. Not primarily a token optimisation; it is how the **information hierarchy** is protected. Licensed by **branching**: disclose what only some branches need, inline what every path needs, and if a pointer fires unreliably on must-have material, sharpen its wording, and pull it back inline only if that fails.
|
||||
|
||||
_Avoid_: lazy loading, chunking
|
||||
|
||||
### Co-location
|
||||
|
||||
Keeping the material an agent needs at once in one place — a concept's definition, rules, and caveats under a single heading, not scattered across the file — so reading one part brings its neighbours with it. The within-file companion to the **Information Hierarchy**: the hierarchy ranks _how far down_ a piece sits; co-location decides _what sits beside it_ once there. There is no formula for the right format of a body of **reference**; the test is that a skill should read like documentation written for the agent, and grouped material reads that way where scattered material does not. Distinct from **Duplication**: that repeats one meaning in two places, where scattering fragments a single meaning across many.
|
||||
|
||||
_Avoid_: grouping, clustering, cohesion
|
||||
|
||||
### Sprawl
|
||||
|
||||
_Failure mode._ A skill that is simply too long — too many lines in SKILL.md — independent of whether they are stale or repeated. Even an all-live, all-unique skill can sprawl. It costs readability (the agent wades through more before it can act, and attention thins across the excess), maintainability (every extra line is one more to keep **relevant**), and tokens. The cure is the **information hierarchy**: push **reference** down behind **context pointers**, and split by **branch** or sequence so each path carries only what it needs. Distinct from **sediment** (length from stale accumulation) and **duplication** (length from repeated meaning) — sprawl is length itself, whatever its cause.
|
||||
|
||||
_Avoid_: bloat, length, size, verbosity
|
||||
|
||||
## Steering
|
||||
|
||||
The levers that shape the agent's runtime behaviour toward **Predictability**.
|
||||
|
||||
### Branch
|
||||
|
||||
A distinct way a skill can be invoked — a case the skill handles — so different runs take different paths through it. A skill with many steps may carry many branches; a linear one has none.
|
||||
|
||||
_Avoid_: path, case, fork
|
||||
|
||||
### Leading Word
|
||||
|
||||
A compact concept — also called a _Leitwort_ — already living in the model's pretraining, that the agent thinks with while running the skill. It encodes a behavioural principle in the fewest possible tokens by invoking priors the model already holds (e.g. _lesson_, _proximal zone of development_, _fog of war_, _tracer bullets_). Repeated as a token, never as a sentence, it accumulates a distributed definition across the skill and anchors a whole region of behaviour. Coining your own works if you define it clearly, but a made-up word recruits no priors — you pay in definition tokens what a pretrained word gives free. Reach for an existing word first.
|
||||
|
||||
A leading word serves **predictability** twice. In the body it anchors **execution** — the agent reaches for the same behaviour every time the concept appears, and inside flat reference it focuses attention on a class of thing to look for, recruiting the right checks each run. In the **description** it anchors **invocation** — and not only within the skill: when the same word lives in your prompts, your docs, and your codebase, the agent links that shared language to the skill and fires it more reliably. Word a description with the leading words you actually use when you want the skill.
|
||||
|
||||
_Avoid_: keyword, term, motif
|
||||
|
||||
### Completion Criterion
|
||||
|
||||
The condition that tells the agent a unit of work is done — the target it judges against. Two properties make it a lever, not just a quality. Its **clarity** (can the agent tell done from not-done?) resists **premature completion** — a vague bound ("understanding reached") lets the agent declare done and slip to the next step; this axis needs _steps_ to bite, since premature completion is a between-steps failure. Its **demand** (how much it requires) sets **legwork** — "every modified model accounted for" forces thorough work where "produce a change list" does not — and this axis is _not_ step-bound: it can bind a body of flat reference too, which is how a skill with no steps still carries an exhaustiveness bar ("every rule applied"). The strongest criteria are both checkable and exhaustive.
|
||||
|
||||
_Avoid_: done condition, exit condition, stopping rule
|
||||
|
||||
### Legwork
|
||||
|
||||
The work an agent does behind the scenes within a single step — reading files, exploring the codebase, making changes, digging up what it needs rather than offloading to the user. It lives below the step structure: never written as its own step, latent in the wording, controlled by the agent rather than the skill. The within-step counterpart to **post-completion steps**' across-step pull. Raised by a **leading word** (_comprehensive_, _thorough_) or a **completion criterion** that demands the work be exhaustive — including the demand axis applied to flat reference, which is what drives a skill of flat reference to cover all its rungs. Goes thin either when that demand is missing or when **premature completion** cuts the step short.
|
||||
|
||||
_Avoid_: scope, effort, diligence, coverage
|
||||
|
||||
### Post-Completion Steps
|
||||
|
||||
The **steps** that follow the current step. Visible, they pull the agent forward into **premature completion** — the more it sees, the stronger the tug; the defence is to hide them by splitting the sequence of steps into two.
|
||||
|
||||
_Avoid_: horizon, fog of war, lookahead
|
||||
|
||||
### Premature Completion
|
||||
|
||||
_Failure mode._ Ending the current step before it is genuinely done, because the agent's attention slips to being done rather than to the work. A between-steps failure: it needs **steps** to occur — a skill with no steps that quits early isn't premature completion but thin **legwork** under an unmet demand. A tug-of-war between two forces: visible **post-completion steps** (the pull forward) and the **completion criterion**'s clarity (the resistance — a sharp, checkable bar holds; a vague one gives way). Fuzziness is the necessary condition: a sharp bound resists the pull no matter how many later steps are visible, so a step that never rushes needs no defending. Two levers hold a step that does, but reach for them in order: **sharpen the bound first** — it is local and cheap. Only when the criterion is irreducibly fuzzy _and_ you actually observe the rush do you **hide the later steps** — and hiding only works across a real context boundary (a user-invoked hand-off or a subagent dispatch; an inline model-invoked call leaves the later steps in context and clears nothing). One cause of thin legwork, but distinct from it: legwork can be thin even when a step runs to full completion.
|
||||
|
||||
_Avoid_: premature closure, the rush, rushing, shortcutting
|
||||
|
||||
## Pruning
|
||||
|
||||
Keeping a skill lean — each remedy paired with the failure it cures.
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
The desired state where each meaning lives in exactly one authoritative place, so a change to the skill's behaviour is a change in one place. **Duplication** is its violation.
|
||||
|
||||
_Avoid_: home, canonical location
|
||||
|
||||
### Duplication
|
||||
|
||||
_Failure mode._ The same meaning given more than one **single source of truth**. It costs maintenance (change one place, you must change the others), costs tokens, and inflates prominence — repeating a meaning weights it on the ladder past its real rank. The accidental inverse of a **leading word**, which raises attention on purpose by repeating a token, never the meaning.
|
||||
|
||||
_Avoid_: repetition, redundancy
|
||||
|
||||
### Relevance
|
||||
|
||||
Whether a line still bears on what the skill does — the lens for what to keep. A line loses relevance either by never bearing on the task (mere exposition, or a **branch** that should be disclosed) or by going stale: drifting out of date as the behaviour or world it describes changes. Shorter skills are easier to keep relevant, because each line is cheaper to check. Distinct from **no-op**: relevance asks whether a line bears on the task, not whether it changes behaviour.
|
||||
|
||||
_Avoid_: load-bearing, staleness, freshness
|
||||
|
||||
### Sediment
|
||||
|
||||
_Failure mode._ Layers of old content that settle in a skill and are never cleared, because adding feels safe and removing feels risky — so stale and irrelevant lines accumulate and you must core down through them to find what is still live. The default fate of any skill without a pruning discipline; the slow erosion of **relevance**, as opposed to **duplication**'s repeated meaning.
|
||||
|
||||
_Avoid_: accretion, bloat, cruft, rot
|
||||
|
||||
### No-Op
|
||||
|
||||
_Failure mode._ An instruction that changes nothing because the model already does it by default — you pay load to tell the agent what it would do anyway. The test: does a line change behaviour versus the default? A line can be perfectly **relevant** and still be a no-op. The same priors that make a **leading word** free make a no-op worthless.
|
||||
|
||||
A leading word is a _technique_; No-Op is a _verdict_ on a line — and they cross. A leading word too weak to beat the default is a no-op (_be thorough_ when the agent is already thorough-ish), and the fix is a stronger word that passes the verdict (_relentless_), not a different technique. So the No-Op test — does it change behaviour versus the default? — is also how you grade whether a leading word is earning its repetitions. This is model-relative, not reader-relative: two people disagreeing over whether a line is a no-op disagree about the default, and settle it by running the skill, not by debate.
|
||||
|
||||
_Avoid_: redundant instruction, restating the obvious, belaboring
|
||||
84
.agents/skills/writing-great-skills/SKILL.md
Normal file
84
.agents/skills/writing-great-skills/SKILL.md
Normal file
@@ -0,0 +1,84 @@
|
||||
---
|
||||
name: writing-great-skills
|
||||
description: Reference for writing and editing skills well — the vocabulary and principles that make a skill predictable.
|
||||
disable-model-invocation: true
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
A skill exists to wrangle determinism out of a stochastic system. **Predictability** — the agent taking the same _process_ every run, not producing the same output — is the root virtue; every lever below serves it.
|
||||
|
||||
**Bold terms** are defined in [`GLOSSARY.md`](GLOSSARY.md); look them up there for the full meaning.
|
||||
|
||||
## Invocation
|
||||
|
||||
Two choices, trading different costs:
|
||||
|
||||
- A **model-invoked** skill keeps a **description**, so the agent can fire it autonomously _and_ other skills can reach it (you can still type its name too). It contributes to **context load** — the description sits in the window every turn. Mechanics: omit `disable-model-invocation`, and write a model-facing description with rich trigger phrasing ("Use when the user wants…, mentions…").
|
||||
- A **user-invoked** skill strips the description from the agent's reach: only you, typing its name, can invoke it — and no other skill can. Zero context load, but it spends **cognitive load**: _you_ are the index that must remember it exists. Mechanics: set `disable-model-invocation: true`; the `description` becomes human-facing — a one-line summary, trigger lists stripped.
|
||||
|
||||
Pick model-invocation only when the agent must reach the skill on its own, or another skill must. If it only ever fires by hand, make it user-invoked and pay no context load.
|
||||
|
||||
When user-invoked skills multiply past what you can remember, that piled-up cognitive load is cured by a **router skill**: one user-invoked skill that names the others and when to reach for each.
|
||||
|
||||
## Writing the description
|
||||
|
||||
A model-invoked **description** does two jobs — state what the skill is, and list the **branches** that should trigger it. Every word increases **context load**, so a description earns even harder pruning than the body:
|
||||
|
||||
- **Front-load the skill's leading word** — the description is where it does its invocation work.
|
||||
- **One trigger per branch.** Synonyms that rename a single branch are **duplication** — "build features using TDD … asks for test-first development" is one branch written twice. Collapse them; keep only genuinely distinct branches.
|
||||
- **Cut identity that's already in the body.** Keep the description to triggers, plus any "when another skill needs…" reach clause.
|
||||
|
||||
## Information hierarchy
|
||||
|
||||
A skill is built from two content types — **steps** and **reference** — that mix freely: a skill can be all steps, all reference, or both. The core decision is which to use and where each sits on the **information hierarchy**, a ladder ranked by how immediately the agent needs the material:
|
||||
|
||||
1. **In-skill step** — an ordered action in `SKILL.md`, the primary tier: what the agent does, in order. Each step ends on a **completion criterion**, the condition that tells the agent the work is done. Make it _checkable_ (can the agent tell done from not-done?) and, where it matters, _exhaustive_ ("every modified model accounted for", not "produce a change list") — a vague criterion invites **premature completion**.
|
||||
2. **In-skill reference** — a definition, rule, or fact in `SKILL.md`, consulted on demand. Often a legitimately flat peer-set (every rule of a review on one rung) — a fine arrangement, not a smell. _This skill is all reference._
|
||||
3. **External reference** — reference pushed out of `SKILL.md` into a separate file, reached by a **context pointer**, loaded only when the pointer fires. (Spans _disclosed_ reference — a sibling file like `GLOSSARY.md`, still part of the skill — through fully **external reference** that lives outside the skill system and any skill can point at.)
|
||||
|
||||
A demanding completion criterion drives thorough **legwork** — the digging the agent does within the work — whether the skill has steps or not, since "every rule applied" binds flat reference just as "every step done" binds a sequence.
|
||||
|
||||
Push too little down and the top bloats; push too much and you hide material the agent actually needs. That tension is the whole decision.
|
||||
|
||||
**Progressive disclosure** is the move down the ladder — out of `SKILL.md` into a linked file — so the top stays legible. Mechanics: a linked `.md` file in the skill folder, named for what it holds (this skill discloses its full definitions to `GLOSSARY.md`). Some skills are used in more than one way, and each distinct way is a **branch** — different runs taking different paths through the skill. Branching is the cleanest disclosure test: inline what every branch needs, and push behind a pointer what only some branches reach. A **context pointer**'s _wording_, not its target, decides when and how reliably the agent reaches the material.
|
||||
|
||||
Where the ladder decides _how far down_ a piece sits, **co-location** decides _what sits beside it_ once there: keep a concept's definition, rules, and caveats under one heading rather than scattered, so reading one part brings its neighbours with it.
|
||||
|
||||
## When to split
|
||||
|
||||
**Granularity** is how finely you divide skills, and each cut spends one of the two loads, so split only when the cut earns it. Two cuts:
|
||||
|
||||
- **By invocation** — split off a **model-invoked** skill when you have a distinct **leading word** that should trigger it on its own, or another skill must reach it. You pay **context load** for the new always-loaded **description**, so that independent reach has to be worth it.
|
||||
- **By sequence** — split a run of **steps** when the steps still ahead (a step's **post-completion steps**) tempt the agent to rush the one in front of it (**premature completion**). Keeping them out of view encourages the agent to do more **legwork** on the current task.
|
||||
|
||||
## Pruning
|
||||
|
||||
Keep each meaning in a **single source of truth**: one authoritative place, so changing the behaviour is a one-place edit.
|
||||
|
||||
Check every line for **relevance**: does it still bear on what the skill does?
|
||||
|
||||
Then hunt **no-ops** sentence by sentence, not just line by line: run the no-op test on each sentence in isolation, and when one fails, delete the whole sentence rather than trim words from it. Be aggressive — most prose that fails should go, not be rewritten.
|
||||
|
||||
## Leading words
|
||||
|
||||
A **leading word** is a compact concept already living in the model's pretraining that the agent thinks with while running the skill (e.g. _lesson_, _fog of war_, _tracer bullets_). Repeated throughout the text (though not necessarily - a strong leading word might only be needed once), it accumulates a distributed definition and anchors a whole region of behaviour in the fewest tokens, by recruiting priors the model already holds.
|
||||
|
||||
It serves predictability twice. In the body it anchors _execution_: the agent reaches for the same behaviour every time the word appears. In the description it anchors _invocation_: when the same word lives in your prompts, docs, and code, the agent links that shared language to the skill and fires it more reliably.
|
||||
|
||||
Hunt for opportunities to refactor skills to use leading words. A triad spelled out at three sites (**duplication**), a description spending a sentence to gesture at one idea — each is a passage begging to **collapse** into a single token. Examples include:
|
||||
|
||||
- "fast, deterministic, low-overhead" -> _tight_ — one quality restated across a phase — into a single pretrained word (a _tight_ loop).
|
||||
- "a loop you believe in" -> _red_ — converts a fuzzy gate into a binary observable state (the loop goes _red_ on the bug, or it doesn't).
|
||||
|
||||
You win twice over: fewer tokens, _and_ a sharper hook for the agent to hang its thinking on. Assume every skill is carrying restatements that leading words retire — go find them.
|
||||
|
||||
## Failure modes
|
||||
|
||||
Use these to diagnose issues the user may be having with the skill.
|
||||
|
||||
- **Premature completion** — ending a step before it's genuinely done, attention slipping to _being done_. Defence, in order: sharpen the completion criterion first (cheap, local); only if it is irreducibly fuzzy _and_ you observe the rush, hide the post-completion steps by splitting (the sequence cut).
|
||||
- **Duplication** — the same meaning in more than one place. Costs maintenance and tokens, and inflates a meaning's prominence on the ladder past its real rank.
|
||||
- **Sediment** — stale layers that settle because adding feels safe and removing feels risky. The default fate of any skill without a pruning discipline.
|
||||
- **Sprawl** — a skill simply too long, even when every line is live and unique. Hurts readability and maintainability and wastes tokens. The cure is the ladder: disclose **reference** behind pointers, and split by **branch** or sequence so each path carries only what it needs.
|
||||
- **No-op** — a line the model already obeys by default, so you pay load to say nothing. The test: does it change behaviour versus the default? A weak leading word (_be thorough_ when the agent is already thorough-ish) is a no-op; the fix is a stronger word (_relentless_), not a different technique.
|
||||
126
.agents/skills/writing-opencode-plugins/SKILL.md
Normal file
126
.agents/skills/writing-opencode-plugins/SKILL.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: writing-opencode-plugins
|
||||
description: OpenCode plugins, @opencode-ai/plugin, @opencode-ai/plugin/tui, plugin hooks, custom tools, TUI routes, slots, keymaps, and packaging. Use when creating, editing, reviewing, testing, or publishing server or TUI plugins for OpenCode.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Writing OpenCode Plugins
|
||||
|
||||
Use this skill to implement production-quality OpenCode plugins. Treat the repository's exported types and runtime as authoritative because plugin APIs are evolving and public docs may lag.
|
||||
|
||||
## Start Here
|
||||
|
||||
1. Decide which runtime owns the feature.
|
||||
2. Read the relevant public type before writing code.
|
||||
3. Find one focused in-repository example using the same API.
|
||||
4. Implement the smallest target-specific module.
|
||||
5. Test loading, behavior, failure, and cleanup in the owning package.
|
||||
|
||||
| Need | Plugin target | Import | Configuration |
|
||||
| -------------------------------------------------------------------- | --------------------------- | ------------------------------ | ---------------------------------------------------------------- |
|
||||
| Hooks, tools, auth, providers, model parameters, shell environment | Server | `@opencode-ai/plugin` | `opencode.json` or auto-discovered `.opencode/plugins/*.{ts,js}` |
|
||||
| Commands, keybindings, routes, dialogs, slots, themes, notifications | TUI | `@opencode-ai/plugin/tui` | Explicit `tui.json` `plugin` entry |
|
||||
| Both | Two target-only entrypoints | Both imports in separate files | Package exports `./server` and `./tui` |
|
||||
|
||||
Never export `server` and `tui` from the same module. Do not use server event hooks as a substitute for interactive TUI APIs.
|
||||
|
||||
## Verify The Current Contract
|
||||
|
||||
Read these files before implementing unfamiliar behavior:
|
||||
|
||||
- `packages/plugin/src/index.ts`: authoritative server plugin and hook types.
|
||||
- `packages/plugin/src/tool.ts`: custom tool schema, context, permission, metadata, attachments, and result types.
|
||||
- `packages/plugin/src/tui.ts`: authoritative TUI API and module types.
|
||||
- `packages/opencode/specs/tui-plugins.md`: TUI loading, packaging, lifecycle, and API semantics.
|
||||
- `packages/opencode/src/plugin/shared.ts`: target validation, IDs, and entrypoint resolution.
|
||||
- `packages/opencode/src/plugin/loader.ts`: install, compatibility, and import behavior.
|
||||
|
||||
If these disagree with examples or website docs, follow exported types and runtime behavior, then update stale documentation when appropriate.
|
||||
|
||||
## Choose A Module Shape
|
||||
|
||||
Prefer the explicit module object for new server plugins:
|
||||
|
||||
```ts
|
||||
import type { Plugin, PluginModule } from '@opencode-ai/plugin';
|
||||
|
||||
const server: Plugin = async ({ client, directory }, options) => ({
|
||||
dispose: async () => {},
|
||||
});
|
||||
|
||||
export default {
|
||||
id: 'acme.example',
|
||||
server,
|
||||
} satisfies PluginModule & { id: string };
|
||||
```
|
||||
|
||||
Legacy server-only local plugins may export a plugin function directly. In a legacy module every distinct named export is interpreted as a plugin, so do not export unrelated constants. Prefer a default module object for new code.
|
||||
|
||||
TUI plugins always use a default module object:
|
||||
|
||||
```tsx
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import type { TuiPlugin, TuiPluginModule } from '@opencode-ai/plugin/tui';
|
||||
|
||||
const tui: TuiPlugin = async (api) => {
|
||||
api.ui.toast({ message: 'Plugin loaded' });
|
||||
};
|
||||
|
||||
export default {
|
||||
id: 'acme.example-tui',
|
||||
tui,
|
||||
} satisfies TuiPluginModule & { id: string };
|
||||
```
|
||||
|
||||
File plugins require a stable, non-empty `id`. npm plugins may derive the ID from the package name, but an explicit namespaced ID makes state, diagnostics, and collision handling clearer.
|
||||
|
||||
## Engineering Rules
|
||||
|
||||
- Use TypeScript and `satisfies` against the public plugin type.
|
||||
- Parse and validate `options`; they arrive as unvalidated `Record<string, unknown>`.
|
||||
- Namespace plugin IDs, command IDs, route names, modes, slot names, and shared KV keys.
|
||||
- Use the directory supplied by the plugin or tool context, not `process.cwd()`.
|
||||
- Honor `AbortSignal` for long-running or cancellable work.
|
||||
- Use `client.app.log()` for structured server logging instead of `console.log`.
|
||||
- Request permission before sensitive or consequential custom-tool work.
|
||||
- Keep notifications privacy-safe; do not expose prompts, secrets, paths, commands, or raw errors.
|
||||
- Register only needed hooks and UI resources. Avoid broad event subscriptions when a specific hook exists.
|
||||
- Make cleanup bounded, idempotent, and safe after partial initialization.
|
||||
- Do not depend on undocumented load order to resolve ownership conflicts.
|
||||
|
||||
## Testing Workflow
|
||||
|
||||
Server plugin tests belong under `packages/opencode/test/plugin/` or the closest owning subsystem. TUI runtime tests belong under `packages/opencode/test/cli/tui/`; component-level TUI tests may belong in `packages/tui`.
|
||||
|
||||
Test at least:
|
||||
|
||||
- valid loading and target/entrypoint selection;
|
||||
- configured options and malformed options;
|
||||
- the observable behavior, not a duplicate of implementation logic;
|
||||
- abort, failure, and partial-initialization behavior;
|
||||
- cleanup or disposal;
|
||||
- duplicate IDs or registrations when relevant;
|
||||
- local file and npm packaging behavior when publishing.
|
||||
|
||||
Run tests from the package directory, never the repository root. Use `bun typecheck` from the owning package for type checking.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- The feature is in the correct server or TUI runtime.
|
||||
- Module shape and import path match the target.
|
||||
- Server and TUI entrypoints are separate.
|
||||
- IDs and persistent keys are stable and namespaced.
|
||||
- Options and external data are validated.
|
||||
- Hook output mutation preserves other plugins' changes.
|
||||
- Tools use context directory, permission, metadata, and abort correctly.
|
||||
- TUI keybindings are mode-gated unless intentionally global.
|
||||
- TUI resources and custom side effects are disposed.
|
||||
- Package exports, `engines.opencode`, and config target are correct.
|
||||
- Tests cover behavior and lifecycle.
|
||||
|
||||
## References
|
||||
|
||||
- [Server plugins](references/server-plugins.md): hooks, custom tools, lifecycle, and examples.
|
||||
- [TUI plugins](references/tui-plugins.md): keymaps, routes, dialogs, slots, state, and lifecycle.
|
||||
- [Packaging and testing](references/packaging-testing.md): config, package exports, compatibility, and test locations.
|
||||
@@ -0,0 +1,129 @@
|
||||
# Packaging And Testing
|
||||
|
||||
## Local Configuration
|
||||
|
||||
Server plugin in `opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"plugin": ["./plugins/server.ts", ["package-name", { "key": "value" }]]
|
||||
}
|
||||
```
|
||||
|
||||
Server files under `.opencode/plugin/` or `.opencode/plugins/` are also auto-discovered. Relative configured paths resolve from the config file that declared them.
|
||||
|
||||
TUI plugin in `tui.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": [["./plugins/tui.tsx", { "key": "value" }]],
|
||||
"plugin_enabled": {
|
||||
"acme.demo": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`plugin_enabled` uses the resolved plugin ID, not its package or file spec. Persisted runtime enablement can override config.
|
||||
|
||||
After editing plugin or config-time files, restart OpenCode; the running session keeps its loaded configuration and modules.
|
||||
|
||||
## npm Package Shape
|
||||
|
||||
Publish separate target-only entrypoints:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "@acme/opencode-plugin",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./server": {
|
||||
"import": "./dist/server.js",
|
||||
"config": { "serverOption": true }
|
||||
},
|
||||
"./tui": {
|
||||
"import": "./dist/tui.js",
|
||||
"config": { "tuiOption": true }
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"opencode": "^1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode-ai/plugin": "^1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Server resolution prefers `exports["./server"]` and may fall back to `main`.
|
||||
- TUI resolution requires `exports["./tui"]`; it does not use `main`.
|
||||
- A package supporting both targets needs separate source and output files.
|
||||
- `exports[target].config` may provide default options written during first install.
|
||||
- Use `engines.opencode` to declare tested compatibility.
|
||||
- npm compatibility is checked; local file plugins bypass the engine check.
|
||||
- Pin package versions when reproducibility matters.
|
||||
- Plugin package install runs with lifecycle scripts disabled, so do not require `postinstall`.
|
||||
- Keep resolved entrypoints and theme paths inside the package directory.
|
||||
|
||||
Theme-only TUI packages may use `oc-themes`; consult `packages/opencode/specs/tui-plugins.md` for path and synchronization rules.
|
||||
|
||||
## Resolution And Identity
|
||||
|
||||
- npm declarations deduplicate by package name; higher-precedence/later declarations win.
|
||||
- File server and TUI specs have target-specific resolution behavior.
|
||||
- External modules may resolve/import in parallel, but activate sequentially.
|
||||
- IDs must not collide with built-ins or other loaded plugins.
|
||||
- Dynamic import failures are effectively permanent for the current process because Bun caches them.
|
||||
- `--pure` or `OPENCODE_PURE` skips external plugins.
|
||||
|
||||
Read `packages/opencode/src/plugin/shared.ts`, `loader.ts`, and `install.ts` before changing packaging behavior.
|
||||
|
||||
## Test Locations
|
||||
|
||||
Server plugin coverage:
|
||||
|
||||
- `packages/opencode/test/plugin/trigger.test.ts`: hook sequencing and failures.
|
||||
- `packages/opencode/test/plugin/loader-shared.test.ts`: resolution and module validation.
|
||||
- `packages/opencode/test/plugin/shared.test.ts`: shared target rules.
|
||||
- `packages/opencode/test/plugin/install.test.ts`: package install and config patching.
|
||||
- `packages/opencode/test/plugin/install-concurrency.test.ts`: concurrent writes.
|
||||
- `packages/opencode/test/plugin/auth-override.test.ts`: auth precedence.
|
||||
- `packages/opencode/test/tool/registry.test.ts`: schemas, results, and attachments.
|
||||
|
||||
TUI plugin coverage:
|
||||
|
||||
- `packages/opencode/test/cli/tui/plugin-loader.test.ts`: loading and ordering.
|
||||
- `packages/opencode/test/cli/tui/plugin-loader-entrypoint.test.ts`: target entrypoints.
|
||||
- `packages/opencode/test/cli/tui/plugin-lifecycle.test.ts`: rollback and cleanup.
|
||||
- `packages/opencode/test/cli/tui/plugin-toggle.test.ts`: persisted enablement.
|
||||
- `packages/opencode/test/cli/tui/plugin-add.test.ts`: runtime addition.
|
||||
- `packages/opencode/test/cli/tui/plugin-install.test.ts`: installation.
|
||||
- `packages/opencode/test/cli/tui/plugin-loader-pure.test.ts`: pure mode.
|
||||
|
||||
Use fixture helpers under `packages/opencode/test/fixture/` rather than reimplementing the loader in tests.
|
||||
|
||||
## Verification Commands
|
||||
|
||||
Run from the owning package, never the repository root:
|
||||
|
||||
```sh
|
||||
cd packages/opencode
|
||||
bun typecheck
|
||||
bun test test/plugin/trigger.test.ts
|
||||
bun test test/cli/tui/plugin-lifecycle.test.ts
|
||||
```
|
||||
|
||||
Select the smallest relevant tests first, then broader plugin suites. For interactive TUI verification, follow `packages/opencode/AGENTS.md`: run `bun dev` in detached `tmux`, capture output, and explicitly stop the session.
|
||||
|
||||
## Publishing Checklist
|
||||
|
||||
- Build output is ESM-compatible and contains no source-only path aliases.
|
||||
- Every advertised target has the correct package export.
|
||||
- Each target module exports only its own target shape.
|
||||
- Peer/runtime dependencies are classified correctly.
|
||||
- `engines.opencode` matches tested versions.
|
||||
- Default options are backward-compatible and validated at runtime.
|
||||
- Local file, pinned npm, and bare npm specs have been considered.
|
||||
- Loading, failure, cleanup, and upgrade behavior are tested.
|
||||
- README examples match the exported API and config target.
|
||||
@@ -0,0 +1,122 @@
|
||||
# Server Plugins
|
||||
|
||||
## Contract And Lifecycle
|
||||
|
||||
The public contract is `packages/plugin/src/index.ts`:
|
||||
|
||||
```ts
|
||||
type Plugin = (input: PluginInput, options?: Record<string, unknown>) => Promise<Hooks>
|
||||
```
|
||||
|
||||
`PluginInput` provides the SDK `client`, `project`, `directory`, `worktree`, `serverUrl`, Bun shell `$`, and experimental workspace registration.
|
||||
|
||||
The server runtime is `packages/opencode/src/plugin/index.ts`.
|
||||
|
||||
- Built-in plugins initialize before external plugins.
|
||||
- External modules may resolve concurrently, but activation is sequential for deterministic hook order.
|
||||
- `config` hooks run sequentially against the mutable merged config.
|
||||
- `event` subscribes to location-filtered events and is fire-and-forget.
|
||||
- Ordinary hooks run sequentially and share a mutable output object.
|
||||
- Ordinary hook failures propagate and stop later hooks for that trigger.
|
||||
- Initialization, config, and disposal failures are isolated and logged by the host.
|
||||
- `dispose` runs when the per-directory plugin scope closes.
|
||||
|
||||
Mutate hook output in place. Preserve values contributed by earlier plugins: append arrays, merge maps, and change only fields the plugin owns.
|
||||
|
||||
## Hook Selection
|
||||
|
||||
Use the narrowest hook that expresses the behavior:
|
||||
|
||||
| Goal | Hook |
|
||||
| ---------------------------------- | --------------------------------- |
|
||||
| Observe SDK events | `event` |
|
||||
| Modify merged configuration | `config` |
|
||||
| Add tools | `tool` |
|
||||
| Add provider authentication | `auth` |
|
||||
| Add or change provider models | `provider` |
|
||||
| Modify incoming user message | `chat.message` |
|
||||
| Modify LLM parameters or headers | `chat.params`, `chat.headers` |
|
||||
| Modify command parts | `command.execute.before` |
|
||||
| Validate or rewrite tool arguments | `tool.execute.before` |
|
||||
| Transform tool presentation/result | `tool.execute.after` |
|
||||
| Modify model-facing tool schemas | `tool.definition` |
|
||||
| Add shell environment variables | `shell.env` |
|
||||
| Influence permission decisions | `permission.ask` |
|
||||
| Customize compaction | `experimental.session.compacting` |
|
||||
|
||||
Read the complete `Hooks` interface before using experimental hooks.
|
||||
|
||||
## Custom Tools
|
||||
|
||||
Use `tool()` and Zod schemas from `tool.schema`:
|
||||
|
||||
```ts
|
||||
import { type Plugin, tool } from "@opencode-ai/plugin"
|
||||
|
||||
export default (async () => ({
|
||||
tool: {
|
||||
lookup_issue: tool({
|
||||
description: "Look up one issue by numeric ID",
|
||||
args: {
|
||||
id: tool.schema.number().int().positive().describe("Issue ID"),
|
||||
},
|
||||
async execute(args, context) {
|
||||
await context.ask({
|
||||
permission: "lookup_issue",
|
||||
patterns: [String(args.id)],
|
||||
always: ["*"],
|
||||
metadata: { id: args.id },
|
||||
})
|
||||
context.metadata({ title: `Issue ${args.id}` })
|
||||
|
||||
return {
|
||||
title: `Issue ${args.id}`,
|
||||
output: "Result",
|
||||
metadata: { id: args.id },
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
})) satisfies Plugin
|
||||
```
|
||||
|
||||
Tool rules:
|
||||
|
||||
- Write descriptions for the model, including when to use the tool and important constraints.
|
||||
- Describe arguments individually and constrain them in the schema.
|
||||
- Use `context.directory` and `context.worktree` for path resolution.
|
||||
- Pass `context.abort` into cancellable I/O.
|
||||
- Call `context.ask()` before performing work covered by a permission boundary.
|
||||
- Use `context.metadata()` for in-progress presentation; return final metadata in the result.
|
||||
- Return attachments only as declared file attachments with a MIME type and URL.
|
||||
- Keep output useful and bounded. The host may truncate large results and add truncation metadata.
|
||||
|
||||
Plugin tools with built-in IDs take precedence, but overriding built-ins should be explicit and tested.
|
||||
|
||||
## Auth And Providers
|
||||
|
||||
Use existing built-ins as references rather than inventing OAuth behavior:
|
||||
|
||||
- `packages/opencode/src/plugin/azure.ts`: simple API-key prompt.
|
||||
- `packages/opencode/src/plugin/xai.ts`: OAuth, refresh, and custom fetch behavior.
|
||||
- `packages/opencode/src/plugin/openai/codex.ts`: auth plus chat parameter hooks.
|
||||
- `packages/opencode/src/plugin/github-copilot/copilot.ts`: full auth/provider integration.
|
||||
|
||||
Do not log credentials, tokens, authorization codes, provider headers, or raw auth responses. Preserve provider identity and refresh semantics defined by `AuthHook`.
|
||||
|
||||
## Useful Examples
|
||||
|
||||
- `.opencode/plugins/model-task.ts`: custom subagent tool with permission, abort, metadata, and SDK calls when present in the worktree.
|
||||
- `packages/plugin/src/example.ts`: minimal package example.
|
||||
- `packages/opencode/test/fixture/agent-plugin.ts`: config mutation fixture.
|
||||
- `packages/opencode/src/plugin/*.ts`: built-in auth/provider implementations.
|
||||
|
||||
## Common Failures
|
||||
|
||||
- Exporting constants beside legacy plugin functions: every exported value may be treated as a plugin.
|
||||
- Using `process.cwd()` in a multi-directory process.
|
||||
- Replacing a shared output map or array and deleting earlier plugin contributions.
|
||||
- Forgetting that `event` is not awaited like ordinary hooks.
|
||||
- Assuming thrown hook errors are isolated.
|
||||
- Installing a missing dependency after a dynamic import failed and expecting the same process to recover; Bun caches failed imports.
|
||||
- Trusting options without validation.
|
||||
@@ -0,0 +1,130 @@
|
||||
# TUI Plugins
|
||||
|
||||
## Contract And Loading
|
||||
|
||||
The public contract is `packages/plugin/src/tui.ts`; technical behavior is documented in `packages/opencode/specs/tui-plugins.md`.
|
||||
|
||||
```ts
|
||||
type TuiPlugin = (api: TuiPluginApi, options: Record<string, unknown> | undefined, meta: TuiPluginMeta) => Promise<void>
|
||||
```
|
||||
|
||||
- Import from `@opencode-ai/plugin/tui`.
|
||||
- Export one default `{ id?, tui }` object. Named exports are ignored by the loader.
|
||||
- File plugins require an explicit non-empty ID.
|
||||
- Configure TUI plugins explicitly in `tui.json`; there is no directory auto-discovery.
|
||||
- JSX uses OpenTUI Solid, normally with `/** @jsxImportSource @opentui/solid */`.
|
||||
- TUI packages resolve only `exports["./tui"]`; they do not fall back to package `main` or root exports.
|
||||
|
||||
## API Routing
|
||||
|
||||
| Need | API |
|
||||
| -------------------------------- | ------------------------------------------ |
|
||||
| Commands and shortcuts | `api.keymap.registerLayer(...)` |
|
||||
| Temporary input context | `api.mode.push(...)` |
|
||||
| Full-screen UI | `api.route.register(...)`, `navigate(...)` |
|
||||
| Host dialogs and toast | `api.ui.dialog`, `Dialog*`, `toast(...)` |
|
||||
| Reuse the host prompt | `api.ui.Prompt` |
|
||||
| Inject host UI | `api.slots.register(...)` |
|
||||
| Theme tokens and switching | `api.theme` |
|
||||
| Synced sessions/providers/status | `api.state` |
|
||||
| SDK operations | `api.client` |
|
||||
| TUI event stream | `api.event.on(...)` |
|
||||
| Persistent shared values | `api.kv` |
|
||||
| Host-mediated notification/sound | `api.attention` |
|
||||
| Extra cleanup | `api.lifecycle.onDispose(...)` |
|
||||
|
||||
Do not use deprecated `api.command` in new plugins. Register commands and bindings through keymap layers.
|
||||
|
||||
## Commands And Modes
|
||||
|
||||
```tsx
|
||||
api.keymap.registerLayer({
|
||||
mode: "base",
|
||||
commands: [
|
||||
{
|
||||
name: "acme.demo.open",
|
||||
title: "Open demo",
|
||||
category: "Plugin",
|
||||
namespace: "palette",
|
||||
slashName: "demo",
|
||||
run() {
|
||||
api.route.navigate("acme.demo")
|
||||
},
|
||||
},
|
||||
],
|
||||
bindings: [{ key: "ctrl+shift+m", cmd: "acme.demo.open", desc: "Open demo" }],
|
||||
})
|
||||
```
|
||||
|
||||
Built-in modes are `base`, `modal`, and `autocomplete`. A layer without `mode` remains active across dialogs and autocomplete, so omit mode only for intentionally global behavior.
|
||||
|
||||
For plugin-owned full-screen interaction, push a namespaced mode inside the component and dispose it with Solid cleanup:
|
||||
|
||||
```tsx
|
||||
import { onCleanup } from "solid-js"
|
||||
|
||||
const pop_mode = api.mode.push("acme.demo")
|
||||
onCleanup(pop_mode)
|
||||
```
|
||||
|
||||
## Routes, Dialogs, And Slots
|
||||
|
||||
- Reserve `home` and `session` for host routes.
|
||||
- Namespace route names; duplicate routes are last-registration-wins.
|
||||
- Unknown routes render fallback UI rather than throwing.
|
||||
- Use host dialog components for standard interactions and `api.ui.dialog.replace()` for custom dialog content.
|
||||
- Use route params for serializable navigation state; keep component-local transient state in Solid primitives when appropriate.
|
||||
- `api.slots.register(...)` returns an assigned ID, not an unregister function.
|
||||
- Slot registration and other host API resources are scope-tracked automatically.
|
||||
- Read current slot names and props from `TuiHostSlotMap`, not copied lists.
|
||||
|
||||
## State And Persistence
|
||||
|
||||
- `api.tuiConfig` and `api.state` are live views, not initialization snapshots.
|
||||
- `api.kv` is shared by all plugins. Prefix every key with the plugin ID.
|
||||
- Check readiness where the API exposes it.
|
||||
- Persist only user preferences or durable plugin state, not derived host state.
|
||||
- Runtime enablement in KV overrides `tui.json` on startup.
|
||||
|
||||
`meta.state` is `first`, `updated`, or `same`. Use it for bounded migration or asset synchronization, not normal rendering behavior.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
The host automatically scope-tracks commands, keymap resources, routes, event subscriptions, slots, pushed modes, and sound packs.
|
||||
|
||||
- `api.lifecycle.signal` aborts before cleanup begins.
|
||||
- Use `api.lifecycle.onDispose()` for timers, sockets, file watchers, workers, or other plugin-owned resources.
|
||||
- Initialization failure rolls back tracked resources and does not prevent later plugins from loading.
|
||||
- Cleanup is reverse-order, awaited, idempotent, and constrained by a total five-second budget.
|
||||
- Keep cleanup fast and independently safe after partial initialization.
|
||||
|
||||
## UI Quality
|
||||
|
||||
- Use `api.theme.current` tokens instead of hard-coded colors.
|
||||
- Use `api.keys` to display shortcuts according to host formatting.
|
||||
- Make routes responsive to terminal dimensions and usable with keyboard-only input.
|
||||
- Avoid taking over global shortcuts without a strong reason.
|
||||
- Prefer host dialogs, prompts, and slots over visually inconsistent reimplementations.
|
||||
- Send attention through `api.attention.notify()` so the host owns focus, notification, and sound policy.
|
||||
- Keep notification text privacy-safe.
|
||||
|
||||
## Useful Examples
|
||||
|
||||
- `.opencode/plugins/tui-smoke.tsx`: broad API smoke implementation.
|
||||
- `packages/tui/src/feature-plugins/system/which-key.tsx`: focused keymap UI.
|
||||
- `packages/tui/src/feature-plugins/system/notifications.ts`: attention behavior.
|
||||
- `packages/tui/src/feature-plugins/system/diff-viewer.tsx`: route/UI integration.
|
||||
- `packages/tui/src/feature-plugins/home/tips.tsx`: host slot usage.
|
||||
- `packages/tui/src/feature-plugins/sidebar/context.tsx`: sidebar extension.
|
||||
|
||||
## Common Failures
|
||||
|
||||
- Expecting `.opencode/plugins` auto-discovery for TUI modules.
|
||||
- Exporting `{ server, tui }` from one module.
|
||||
- Omitting the default export, or relying on named exports.
|
||||
- Omitting an ID for a file plugin.
|
||||
- Registering an ungated keymap layer accidentally active in modal/autocomplete modes.
|
||||
- Treating KV as plugin-private.
|
||||
- Treating `slots.register()` as returning a disposer.
|
||||
- Expecting `plugins.install()` to activate a plugin; installation and runtime addition are separate.
|
||||
- Leaking timers or network resources because host tracking only covers host registrations.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": ["@svitejs/changesets-changelog-github-compact", { "repo": "sveltejs/ai-tools" }],
|
||||
"changelog": ["@changesets/changelog-github", { "repo": "sveltejs/ai-tools", "template": "\n- {summary} {ref}" }],
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
|
||||
@@ -5,7 +5,7 @@ on:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'packages/opencode/config.ts'
|
||||
- 'packages/opencode/config.js'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
## Summary
|
||||
Automatically generated update for OpenCode JSON schema.
|
||||
|
||||
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.ts`.
|
||||
This PR was triggered by changes to the OpenCode configuration file `packages/opencode/config.js`.
|
||||
|
||||
## Changes
|
||||
- Updated `packages/opencode/schema.json` with latest JSON schema
|
||||
|
||||
@@ -37,7 +37,7 @@ In the Settings > Developer section, click on Edit Config. It will open the fold
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
|
||||
```toml
|
||||
[mcp_servers.svelte]
|
||||
@@ -47,7 +47,7 @@ args = ["-y", "@sveltejs/mcp"]
|
||||
|
||||
## Copilot CLI
|
||||
|
||||
Use the Copilot CLI to interactively add the MCP server:
|
||||
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
|
||||
|
||||
```bash
|
||||
/mcp add
|
||||
|
||||
@@ -28,7 +28,7 @@ If you prefer you can also install the `svelte` plugin in [the Svelte Claude Cod
|
||||
|
||||
## Codex CLI
|
||||
|
||||
Add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
You can automatically configure the MCP server using the [Codex plugin](codex-plugin) (recommended). If you prefer to configure the MCP server manually, add the following to your `config.toml` (which defaults to `~/.codex/config.toml`, but refer to [the configuration documentation](https://github.com/openai/codex/blob/main/docs/config.md) for more advanced setups):
|
||||
|
||||
```toml
|
||||
experimental_use_rmcp_client = true
|
||||
@@ -38,7 +38,7 @@ url = "https://mcp.svelte.dev/mcp"
|
||||
|
||||
## Copilot CLI
|
||||
|
||||
Use the Copilot CLI to interactively add the MCP server:
|
||||
You can automatically configure the MCP server using the [Copilot plugin](copilot-plugin) (recommended). If you prefer to configure the MCP server manually, use the Copilot CLI to interactively add the MCP server:
|
||||
|
||||
```bash
|
||||
/mcp add
|
||||
|
||||
@@ -9,13 +9,11 @@ CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used when
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
````markdown
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -23,7 +21,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -37,7 +35,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
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.
|
||||
This is the list of available skills provided by Svelte. 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.
|
||||
Skills are available in the Claude Code plugin, the Codex CLI plugin, the GitHub Copilot CLI plugin, and the OpenCode plugin (`@sveltejs/opencode`). They can also be manually installed in your `.claude/skills`, `.copilot/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.
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ name: svelte-file-editor
|
||||
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
|
||||
---
|
||||
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
|
||||
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
|
||||
|
||||
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
|
||||
|
||||
## Available MCP Tools
|
||||
## Available MCP tools
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 1. Gather Context (if needed)
|
||||
### 1. Gather context (if needed)
|
||||
|
||||
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
|
||||
|
||||
1. Call `list-sections` to see available documentation
|
||||
2. Call `get-documentation` with relevant section names
|
||||
|
||||
### 2. Read the Target File
|
||||
### 2. Read the target file
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
### 3. Make changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
### 4. Validate changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
### 5. Fix any issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
|
||||
@@ -6,7 +6,13 @@ OpenCode has a [plugin system](https://opencode.ai/docs/plugins/) that allows de
|
||||
|
||||
## 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.
|
||||
With OpenCode 1.3.4 or newer, install the plugin from the command line:
|
||||
|
||||
```sh
|
||||
opencode plugin @sveltejs/opencode
|
||||
```
|
||||
|
||||
Alternatively, edit your [OpenCode config](https://opencode.ai/docs/config/) (either the global or the local one) and add `@sveltejs/opencode` to the list of plugins:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -17,9 +23,22 @@ To install the plugin you can edit your [OpenCode config](https://opencode.ai/do
|
||||
|
||||
That's it! You now have the Svelte [MCP server](mcp), [skills](skills), and the `svelte-file-editor` [subagent](subagent) configured for you.
|
||||
|
||||
### TUI configuration
|
||||
|
||||
The package also includes a TUI plugin for configuring these features interactively. Add `@sveltejs/opencode` to your global or project-local `tui.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": ["@sveltejs/opencode"]
|
||||
}
|
||||
```
|
||||
|
||||
Restart OpenCode, then run `/svelte-plugin` or select 'Configure Svelte plugin' from the command palette. Choose whether to edit the project or global configuration, then use the checkboxes and radio options to configure the plugin. Changes are saved automatically, and 'Revert changes' restores the values from when the dialog was opened.
|
||||
|
||||
## Configuration
|
||||
|
||||
By default, everything is enabled, but you can configure the plugin by adding a configuration file:
|
||||
By default, the MCP server, subagent, skills, instructions, and automatic updates are enabled. The TUI plugin writes the same configuration files that you can create or edit manually:
|
||||
|
||||
- locally, in `.opencode/svelte.json`
|
||||
- globally, in `~/.config/opencode/svelte.json` (or, if you have specified the environment variable, in `$OPENCODE_CONFIG_DIR/svelte.json`)
|
||||
@@ -49,6 +68,13 @@ By default, everything is enabled, but you can configure the plugin by adding a
|
||||
},
|
||||
"instructions": {
|
||||
"enabled": true
|
||||
}
|
||||
},
|
||||
"autoupdate": true
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic updates
|
||||
|
||||
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so it continues using the cached version until that cache is removed.
|
||||
|
||||
Automatic updates are enabled by default. After detecting a newer version, the plugin removes itself from the cache when OpenCode shuts down. OpenCode installs the latest version the next time it starts. Automatic updates only apply when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.
|
||||
|
||||
36
documentation/docs/60-plugins/40-copilot-plugin.md
Normal file
36
documentation/docs/60-plugins/40-copilot-plugin.md
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: GitHub Copilot CLI
|
||||
---
|
||||
|
||||
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a GitHub Copilot CLI [plugin marketplace](https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-finding-installing).
|
||||
|
||||
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
|
||||
|
||||
In VS Code, run the 'Install plugin from source' command and use the repository URL:
|
||||
|
||||
```text
|
||||
https://github.com/sveltejs/ai-tools
|
||||
```
|
||||
|
||||
You can also add the repository as a marketplace from the Copilot CLI:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add sveltejs/ai-tools
|
||||
```
|
||||
|
||||
Then, install the Svelte plugin:
|
||||
|
||||
```bash
|
||||
copilot plugin install svelte@ai-tools
|
||||
```
|
||||
|
||||
You can also run the same commands from an interactive Copilot CLI session:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add sveltejs/ai-tools
|
||||
/plugin install svelte@ai-tools
|
||||
```
|
||||
28
documentation/docs/60-plugins/50-codex-plugin.md
Normal file
28
documentation/docs/60-plugins/50-codex-plugin.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
title: Codex CLI
|
||||
---
|
||||
|
||||
The open source [repository](https://github.com/sveltejs/ai-tools) containing the code for the MCP server is also a Codex CLI [plugin marketplace](https://developers.openai.com/codex/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
|
||||
|
||||
Add the repository as a marketplace from the Codex CLI:
|
||||
|
||||
```bash
|
||||
codex plugin marketplace add sveltejs/ai-tools
|
||||
```
|
||||
|
||||
Then, open the plugin directory from an interactive Codex CLI session:
|
||||
|
||||
```bash
|
||||
codex
|
||||
/plugins
|
||||
```
|
||||
|
||||
Choose the Svelte marketplace, select the `svelte` plugin, and install it.
|
||||
|
||||
Codex can read the repository's legacy-compatible `.claude-plugin/marketplace.json` marketplace file, so the same marketplace source works for both Claude Code and Codex CLI.
|
||||
@@ -43,12 +43,12 @@
|
||||
],
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@changesets/changelog-github": "catalog:tooling",
|
||||
"@changesets/cli": "catalog:tooling",
|
||||
"@eslint/compat": "catalog:lint",
|
||||
"@eslint/js": "catalog:lint",
|
||||
"@modelcontextprotocol/inspector": "catalog:ai",
|
||||
"@sveltejs/adapter-vercel": "catalog:svelte",
|
||||
"@svitejs/changesets-changelog-github-compact": "catalog:tooling",
|
||||
"eslint": "catalog:lint",
|
||||
"eslint-config-prettier": "catalog:lint",
|
||||
"eslint-plugin-import": "catalog:lint",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @sveltejs/opencode
|
||||
|
||||
## 0.1.12
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- chore: show warning for old versions ([#238](https://github.com/sveltejs/ai-tools/pull/238))
|
||||
|
||||
- feat: add `autoupdate` option to reinstall the plugin when a new version is available ([#238](https://github.com/sveltejs/ai-tools/pull/238))
|
||||
|
||||
- fix: links within references in SKILLS ([#243](https://github.com/sveltejs/ai-tools/pull/243))
|
||||
|
||||
## 0.1.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- fix: ship plugin as js files instead of ts ([#236](https://github.com/sveltejs/ai-tools/pull/236))
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- feat: add tui opencode plugin ([#231](https://github.com/sveltejs/ai-tools/pull/231))
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -15,6 +15,17 @@ Add `@sveltejs/opencode` to your OpenCode config (either global or local):
|
||||
|
||||
That's it! You now have the Svelte MCP server and the file editor subagent configured automatically.
|
||||
|
||||
To configure the plugin from OpenCode's TUI, also add the package to `tui.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/tui.json",
|
||||
"plugin": ["@sveltejs/opencode"]
|
||||
}
|
||||
```
|
||||
|
||||
Run `/svelte-plugin` or choose **Configure Svelte plugin** from the command palette. The dialog lets you choose project or global scope before editing the available options, and guides you through finite choices such as built-in skills.
|
||||
|
||||
## Features
|
||||
|
||||
### Svelte MCP Server
|
||||
@@ -61,10 +72,17 @@ Create `svelte.json` to customize how the plugin configures MCP, the Svelte suba
|
||||
},
|
||||
"skills": {
|
||||
"enabled": ["svelte-code-writer", "svelte-core-bestpractices"]
|
||||
}
|
||||
},
|
||||
"autoupdate": true
|
||||
}
|
||||
```
|
||||
|
||||
### Auto update
|
||||
|
||||
The plugin checks npm for newer versions and warns you when one is available. OpenCode caches plugins, so a new version is only picked up once that cache is wiped.
|
||||
|
||||
Automatic updates are enabled by default. When a newer version is detected, the plugin removes itself from the OpenCode cache as OpenCode shuts down, so the latest version is installed on the next start. This only applies when the plugin is unpinned or explicitly uses the `latest` tag. Exact versions, ranges, and other dist-tags are left untouched because reinstalling them may resolve to the same version again. Set `"autoupdate": false` to only receive the warning.
|
||||
|
||||
### Defaults
|
||||
|
||||
If omitted, the plugin uses these defaults:
|
||||
@@ -75,6 +93,7 @@ If omitted, the plugin uses these defaults:
|
||||
- `subagent.agents`: `{}`
|
||||
- `instructions.enabled`: `true`
|
||||
- `skills.enabled`: `true`
|
||||
- `autoupdate`: `true`
|
||||
|
||||
### Configuration Options
|
||||
|
||||
@@ -89,6 +108,7 @@ If omitted, the plugin uses these defaults:
|
||||
| `subagent.agents.svelte-file-editor.maxSteps` | `number` | unlimited | Limit the number of steps the subagent can execute. |
|
||||
| `instructions.enabled` | `boolean` | `true` | Enable or disable automatic instruction-file injection. |
|
||||
| `skills.enabled` | `boolean \| string[]` | `true` | Enable all skills (`true`), disable all skills (`false`), or enable only specific skill names. |
|
||||
| `autoupdate` | `boolean` | `true` | Remove an unpinned/latest plugin from the cache on exit when a newer version is available. |
|
||||
|
||||
### Supported Skill Names
|
||||
|
||||
|
||||
11
packages/opencode/agents.js
Normal file
11
packages/opencode/agents.js
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 validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the target file\n\nRead the file to understand the current implementation.\n\n### 3. Make changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix any issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
|
||||
},
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
// This file is auto-generated by scripts/sync-opencode-plugin.ts
|
||||
// Do not edit manually — edit the markdown files in tools/agents/ instead.
|
||||
|
||||
export const agents = {
|
||||
'svelte-file-editor': {
|
||||
description:
|
||||
'Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.',
|
||||
prompt:
|
||||
"You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.\n\nIf the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.\n\nIf the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.\n\n## Available MCP Tools\n\n### 1. list-sections\n\nLists all available Svelte 5 and SvelteKit documentation sections with titles and paths. Use this first to discover what documentation is available.\n\n### 2. get-documentation\n\nRetrieves full documentation for specified sections. Accepts a single section name or an array of section names. Use after `list-sections` to fetch relevant docs for the task at hand.\n\n**Example sections:** `$state`, `$derived`, `$effect`, `$props`, `$bindable`, `snippets`, `routing`, `load functions`\n\n### 3. svelte-autofixer\n\nAnalyzes Svelte code and returns suggestions to fix issues. Pass the component code directly to this tool. It will detect common mistakes like:\n\n- Using `$effect` instead of `$derived` for computations\n- Missing cleanup in effects\n- Svelte 4 syntax (`on:click`, `export let`, `<slot>`)\n- Missing keys in `{#each}` blocks\n- And more\n\n## Workflow\n\nWhen invoked to work on a Svelte file:\n\n### 1. Gather Context (if needed)\n\nIf you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:\n\n1. Call `list-sections` to see available documentation\n2. Call `get-documentation` with relevant section names\n\n### 2. Read the Target File\n\nRead the file to understand the current implementation.\n\n### 3. Make Changes\n\nApply edits following Svelte 5 best practices:\n\n### 4. Validate Changes\n\nAfter editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.\n\n### 5. Fix Any Issues\n\nIf the autofixer reports problems, fix them and re-validate until no issues remain.\n\n## Output Format\n\nAfter completing your work, provide:\n\n1. Summary of changes made\n2. Any issues found and fixed by the autofixer\n3. Recommendations for further improvements (if any)",
|
||||
},
|
||||
} as const;
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { PluginInput } from '@opencode-ai/plugin';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { homedir } from 'os';
|
||||
import { join } from 'path';
|
||||
import * as v from 'valibot';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
|
||||
|
||||
// Schema for individual agent configuration
|
||||
const agent_config_schema = v.object({
|
||||
model: v.pipe(
|
||||
@@ -28,19 +29,20 @@ const agent_config_schema = v.object({
|
||||
|
||||
const default_config = {
|
||||
mcp: {
|
||||
type: 'remote' as 'remote' | 'local',
|
||||
type: /** @type {'remote' | 'local'} */ ('remote'),
|
||||
enabled: true,
|
||||
},
|
||||
subagent: {
|
||||
enabled: true,
|
||||
agents: {} as Record<string, v.InferInput<typeof agent_config_schema>>,
|
||||
agents: /** @type {Record<string, v.InferInput<typeof agent_config_schema>>} */ ({}),
|
||||
},
|
||||
instructions: {
|
||||
enabled: true,
|
||||
},
|
||||
skills: {
|
||||
enabled: true as boolean | string[],
|
||||
enabled: /** @type {boolean | string[]} */ (true),
|
||||
},
|
||||
autoupdate: true,
|
||||
};
|
||||
|
||||
export const config_schema = v.object({
|
||||
@@ -89,27 +91,32 @@ export const config_schema = v.object({
|
||||
'Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills.',
|
||||
),
|
||||
),
|
||||
autoupdate: v.pipe(
|
||||
v.optional(v.boolean()),
|
||||
v.description(
|
||||
'When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning.',
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
export type McpConfig = v.InferInput<typeof config_schema>;
|
||||
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
|
||||
|
||||
const GLOBAL_CONFIG_DIR = join(homedir(), '.config', 'opencode');
|
||||
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, 'svelte.json');
|
||||
|
||||
interface ConfigLoadResult {
|
||||
data: Record<string, unknown> | null;
|
||||
parse_error?: string;
|
||||
}
|
||||
/** @typedef {{ data: Record<string, unknown> | null, parse_error?: string }} ConfigLoadResult */
|
||||
|
||||
function get_config_paths() {
|
||||
// Global: ~/.config/opencode/svelte.json
|
||||
let global_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let global_path = null;
|
||||
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
||||
global_path = GLOBAL_CONFIG_PATH;
|
||||
}
|
||||
|
||||
// Custom config directory: $OPENCODE_CONFIG_DIR/svelte.json
|
||||
let config_dir_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let config_dir_path = null;
|
||||
const opencode_config_dir = process.env.OPENCODE_CONFIG_DIR;
|
||||
if (opencode_config_dir) {
|
||||
const config_json = join(opencode_config_dir, 'svelte.json');
|
||||
@@ -119,7 +126,8 @@ function get_config_paths() {
|
||||
}
|
||||
|
||||
// Project-local: ./.opencode/svelte.json (cwd)
|
||||
let project_path: string | null = null;
|
||||
/** @type {string | null} */
|
||||
let project_path = null;
|
||||
const project_config = join(process.cwd(), '.opencode', 'svelte.json');
|
||||
if (existsSync(project_config)) {
|
||||
project_path = project_config;
|
||||
@@ -129,8 +137,13 @@ function get_config_paths() {
|
||||
return [global_path, config_dir_path, project_path];
|
||||
}
|
||||
|
||||
function load_config_file(config_path: string): ConfigLoadResult {
|
||||
let file_content: string;
|
||||
/**
|
||||
* @param {string} config_path
|
||||
* @returns {ConfigLoadResult}
|
||||
*/
|
||||
function load_config_file(config_path) {
|
||||
/** @type {string} */
|
||||
let file_content;
|
||||
try {
|
||||
file_content = readFileSync(config_path, 'utf-8');
|
||||
} catch {
|
||||
@@ -144,7 +157,7 @@ function load_config_file(config_path: string): ConfigLoadResult {
|
||||
return { data: null, parse_error: 'Config file is empty or invalid' };
|
||||
}
|
||||
return { data: parsed };
|
||||
} catch (error: unknown) {
|
||||
} catch (error) {
|
||||
return {
|
||||
data: null,
|
||||
parse_error: error instanceof Error ? error.message : 'Failed to parse config',
|
||||
@@ -152,7 +165,11 @@ function load_config_file(config_path: string): ConfigLoadResult {
|
||||
}
|
||||
}
|
||||
|
||||
function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
/**
|
||||
* @param {Partial<McpConfig>} user_config
|
||||
* @returns {McpConfig}
|
||||
*/
|
||||
function merge_with_defaults(user_config) {
|
||||
return {
|
||||
mcp: {
|
||||
...default_config.mcp,
|
||||
@@ -174,12 +191,15 @@ function merge_with_defaults(user_config: Partial<McpConfig>): McpConfig {
|
||||
...default_config.skills,
|
||||
...user_config.skills,
|
||||
},
|
||||
autoupdate: user_config.autoupdate ?? default_config.autoupdate,
|
||||
};
|
||||
}
|
||||
|
||||
export function get_mcp_config(ctx: PluginInput) {
|
||||
/** @param {PluginInput} ctx */
|
||||
export function get_mcp_config(ctx) {
|
||||
const config_paths = get_config_paths();
|
||||
let merged: Partial<McpConfig> = {};
|
||||
/** @type {Partial<McpConfig>} */
|
||||
let merged = {};
|
||||
|
||||
// Iterate from lowest to highest priority, merging as we go
|
||||
for (const path of config_paths) {
|
||||
@@ -209,6 +229,7 @@ export function get_mcp_config(ctx: PluginInput) {
|
||||
},
|
||||
instructions: { ...merged.instructions, ...parsed.output.instructions },
|
||||
skills: { ...merged.skills, ...parsed.output.skills },
|
||||
autoupdate: parsed.output.autoupdate ?? merged.autoupdate,
|
||||
};
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
@@ -1,14 +1,24 @@
|
||||
import type { Plugin } from '@opencode-ai/plugin';
|
||||
import { readdir } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { agents } from './agents.ts';
|
||||
import { get_mcp_config } from './config.ts';
|
||||
import { agents } from './agents.js';
|
||||
import { get_mcp_config } from './config.js';
|
||||
import { setup_updates } from './update.js';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').Plugin} Plugin */
|
||||
|
||||
const current_dir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export const svelte_plugin: Plugin = async (ctx) => {
|
||||
/**
|
||||
* @param {Parameters<Plugin>[0]} ctx
|
||||
* @returns {ReturnType<Plugin>}
|
||||
*/
|
||||
export async function svelte_plugin(ctx) {
|
||||
const mcp_config = get_mcp_config(ctx);
|
||||
const dispose = setup_updates(ctx, mcp_config.autoupdate === true);
|
||||
|
||||
return {
|
||||
dispose,
|
||||
async config(input) {
|
||||
input.agent ??= {};
|
||||
input.mcp ??= {};
|
||||
@@ -24,15 +34,13 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
const mcp = input.mcp[name];
|
||||
if (
|
||||
(mcp?.type === 'remote' && mcp.url.includes('https://mcp.svelte.dev/mcp')) ||
|
||||
(mcp?.type === 'local' &&
|
||||
mcp.command.some((cmd: string) => cmd.includes('@sveltejs/mcp')))
|
||||
(mcp?.type === 'local' && mcp.command.some((cmd) => cmd.includes('@sveltejs/mcp')))
|
||||
) {
|
||||
// if we found the svelte MCP server, we store its name and break
|
||||
svelte_mcp_name = name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const mcp_config = get_mcp_config(ctx);
|
||||
|
||||
if (mcp_config.instructions?.enabled !== false) {
|
||||
const instructions_dir = join(current_dir, 'instructions');
|
||||
@@ -75,7 +83,8 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
if (mcp_config.subagent?.enabled !== false) {
|
||||
for (const [agent_name, agent_data] of Object.entries(agents)) {
|
||||
// we add the editor subagent that will be used when editing Svelte files to prevent wasting context on the main agent
|
||||
const default_config: (typeof input.agent)[string] = {
|
||||
/** @type {(typeof input.agent)[string]} */
|
||||
const default_config = {
|
||||
color: '#ff3e00',
|
||||
mode: 'subagent',
|
||||
prompt: agent_data.prompt,
|
||||
@@ -109,4 +118,4 @@ export const svelte_plugin: Plugin = async (ctx) => {
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@sveltejs/opencode",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.12",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/sveltejs/ai-tools#readme",
|
||||
@@ -9,19 +9,25 @@
|
||||
},
|
||||
"scripts": {
|
||||
"check": "tsc --noEmit",
|
||||
"generate-schema": "node --import node-resolve-ts/register scripts/generate-schema.ts"
|
||||
"generate-schema": "node scripts/generate-schema.js"
|
||||
},
|
||||
"files": [
|
||||
"index.ts",
|
||||
"config.ts",
|
||||
"agents.ts",
|
||||
"index.js",
|
||||
"config.js",
|
||||
"update.js",
|
||||
"tui.jsx",
|
||||
"agents.js",
|
||||
"instructions",
|
||||
"skills"
|
||||
],
|
||||
"exports": {
|
||||
"./server": {
|
||||
"types": "./index.ts",
|
||||
"import": "./index.ts"
|
||||
"types": "./index.js",
|
||||
"import": "./index.js"
|
||||
},
|
||||
"./tui": {
|
||||
"types": "./tui.jsx",
|
||||
"import": "./tui.jsx"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
@@ -33,11 +39,16 @@
|
||||
"access": "public"
|
||||
},
|
||||
"dependencies": {
|
||||
"valibot": "catalog:tooling"
|
||||
"@opentui/core": "catalog:opencode",
|
||||
"@opentui/keymap": "catalog:opencode",
|
||||
"@opentui/solid": "catalog:opencode",
|
||||
"solid-js": "catalog:opencode",
|
||||
"valibot": "catalog:tooling",
|
||||
"verkit": "catalog:tooling"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode-ai/plugin": "catalog:ai",
|
||||
"@valibot/to-json-schema": "catalog:tooling",
|
||||
"@types/node": "catalog:tooling"
|
||||
"@opencode-ai/plugin": "catalog:opencode",
|
||||
"@types/node": "catalog:tooling",
|
||||
"@valibot/to-json-schema": "catalog:tooling"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +104,10 @@
|
||||
},
|
||||
"required": [],
|
||||
"description": "Configuration for the skills. You can choose if it they should be enabled or not, or specify an array of skill names to enable only specific skills."
|
||||
},
|
||||
"autoupdate": {
|
||||
"type": "boolean",
|
||||
"description": "When a new version of an unpinned or latest-tagged plugin is available, remove it from the opencode cache on exit so that the latest version is installed the next time opencode starts. Enabled by default; set it to false to only get a warning."
|
||||
}
|
||||
},
|
||||
"required": [],
|
||||
|
||||
@@ -4,7 +4,8 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Read agent names from tools/agents/*.md files
|
||||
function get_agent_names(agents_dir: string) {
|
||||
/** @param {string} agents_dir */
|
||||
function get_agent_names(agents_dir) {
|
||||
if (!fs.existsSync(agents_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(agents_dir, { withFileTypes: true })
|
||||
@@ -12,7 +13,8 @@ function get_agent_names(agents_dir: string) {
|
||||
.map((entry) => entry.name.replace(/\.md$/, ''));
|
||||
}
|
||||
|
||||
function get_skill_names(skills_dir: string) {
|
||||
/** @param {string} skills_dir */
|
||||
function get_skill_names(skills_dir) {
|
||||
if (!fs.existsSync(skills_dir)) return [];
|
||||
return fs
|
||||
.readdirSync(skills_dir, { withFileTypes: true })
|
||||
@@ -29,10 +31,13 @@ const json_schema = toJsonSchema(schema);
|
||||
// This is the JSON Schema equivalent of `"a" | "b" | (string & {})` —
|
||||
// editors will autocomplete the known names but any string is still valid.
|
||||
if (skill_names.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const enabled = (json_schema as any).properties?.skills?.properties?.enabled;
|
||||
const enabled = /** @type {any} */ (json_schema).properties?.skills?.properties?.enabled;
|
||||
if (enabled?.anyOf) {
|
||||
const array_branch = enabled.anyOf.find((s: Record<string, unknown>) => s.type === 'array');
|
||||
const array_branch = enabled.anyOf.find(
|
||||
/** @type {(schema: Record<string, unknown>) => boolean} */ (
|
||||
(schema) => schema.type === 'array'
|
||||
),
|
||||
);
|
||||
if (array_branch) {
|
||||
array_branch.items = {
|
||||
anyOf: [{ enum: skill_names }, { type: 'string' }],
|
||||
@@ -48,8 +53,7 @@ const agents_dir = path.resolve('../../tools/agents');
|
||||
const agent_names = get_agent_names(agents_dir);
|
||||
|
||||
if (agent_names.length > 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const agents = (json_schema as any).properties?.subagent?.properties?.agents;
|
||||
const agents = /** @type {any} */ (json_schema).properties?.subagent?.properties?.agents;
|
||||
if (agents) {
|
||||
agents.propertyNames = {
|
||||
anyOf: [{ enum: agent_names }, { type: 'string' }],
|
||||
@@ -3,13 +3,11 @@ name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
|
||||
|
||||
## $inspect(...).with
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
|
||||
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## $inspect.trace(...)
|
||||
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}> Hover me </button>
|
||||
<button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>Hover me</Button>
|
||||
<Button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey/llms.txt).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments#fromAction/llms.txt), allowing you to (for example) use them with components.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
|
||||
@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
|
||||
|
||||
<!-- codeblock:start {"title":"Synchronized updates"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
<input type="number" bind:value={a}>
|
||||
<input type="number" bind:value={b}>
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p><p>{await two()}</p>
|
||||
<p>{await one(x)}</p>
|
||||
<p>{await two(y)}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
// `b` will not be created until `a` has resolved,
|
||||
// but once created they will update independently
|
||||
// even if `x` and `y` update simultaneously
|
||||
let a = $derived(await one(x));
|
||||
let b = $derived(await two(y));
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings#Client-warnings-await_waterfall/llms.txt) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary#Properties-pending/llms.txt) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect#$effect.pending/llms.txt). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte#settled/llms.txt) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
@@ -103,7 +110,7 @@ async function onclick() {
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
|
||||
|
||||
## Forking
|
||||
|
||||
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte#fork/llms.txt) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
}}
|
||||
>open menu</button>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
<Menu onclose={() => open = false} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
|
||||
|
||||
```svelte
|
||||
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
|
||||
<input bind:value={
|
||||
() => value,
|
||||
(v) => value = v.toLowerCase()}
|
||||
/>
|
||||
```
|
||||
|
||||
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
|
||||
|
||||
```svelte
|
||||
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
|
||||
<div
|
||||
bind:clientWidth={null, redraw}
|
||||
bind:clientHeight={null, redraw}
|
||||
>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
csp: { nonce },
|
||||
csp: { nonce }
|
||||
});
|
||||
```
|
||||
|
||||
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
|
||||
|
||||
```js
|
||||
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src 'nonce-${nonce}'`
|
||||
);
|
||||
```
|
||||
|
||||
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
|
||||
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
csp: { hash: true }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
|
||||
);
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{#snippet name(param1, param2, paramN)}...{/snippet}
|
||||
```
|
||||
|
||||
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
|
||||
|
||||
<!-- codeblock:start {"title":"Snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
Snippets can reference themselves and each other:
|
||||
|
||||
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
### Explicit props
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
|
||||
|
||||
<!-- codeblock:start {"title":"Explicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
<Table data={fruits} +++{header} {row}+++ />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
|
||||
|
||||
### Implicit props
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
|
||||
</Table>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
### Optional snippet props
|
||||
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
row
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
|
||||
## Exporting snippets
|
||||
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
|
||||
|
||||
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import { add } from './snippets.svelte';
|
||||
</script>
|
||||
|
||||
{@render add(1, 2)}
|
||||
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: snippets.svelte --->
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE]
|
||||
> This requires Svelte 5.5.0 or newer
|
||||
|
||||
## Programmatic snippets
|
||||
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte#createRawSnippet/llms.txt) API. This is intended for advanced use cases.
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
|
||||
@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
|
||||
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery/llms.txt):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "@opentui/solid",
|
||||
"types": ["@types/node"]
|
||||
},
|
||||
"include": ["index.ts", "config.ts", "agents.ts", "scripts/*"],
|
||||
"include": ["index.js", "config.js", "update.js", "agents.js", "tui.jsx", "scripts/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
330
packages/opencode/tui.jsx
Normal file
330
packages/opencode/tui.jsx
Normal file
@@ -0,0 +1,330 @@
|
||||
/** @jsxImportSource @opentui/solid */
|
||||
import { existsSync } from 'node:fs';
|
||||
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
||||
import { homedir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import * as v from 'valibot';
|
||||
import { config_schema } from './config.js';
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPlugin} TuiPlugin */
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginApi} TuiPluginApi */
|
||||
/** @typedef {import('@opencode-ai/plugin/tui').TuiPluginModule} TuiPluginModule */
|
||||
/** @typedef {v.InferInput<typeof config_schema>} McpConfig */
|
||||
/** @typedef {'project' | 'global'} Scope */
|
||||
/** @typedef {Partial<McpConfig>} Config */
|
||||
|
||||
const plugin_id = 'svelte.configure';
|
||||
const skill_names = ['svelte-code-writer', 'svelte-core-bestpractices'];
|
||||
const agent_name = 'svelte-file-editor';
|
||||
|
||||
/** @param {TuiPluginApi} api */
|
||||
function project_root(api) {
|
||||
const worktree = api.state.path.worktree;
|
||||
return worktree && worktree !== '/' ? worktree : api.state.path.directory;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {TuiPluginApi} api
|
||||
* @param {Scope} scope
|
||||
*/
|
||||
function config_path(api, scope) {
|
||||
if (scope === 'project') return join(project_root(api), '.opencode', 'svelte.json');
|
||||
return join(
|
||||
process.env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode'),
|
||||
'svelte.json',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {Promise<Config>}
|
||||
*/
|
||||
async function read_config(path) {
|
||||
if (!existsSync(path)) return {};
|
||||
/** @type {unknown} */
|
||||
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
||||
const result = v.safeParse(config_schema, parsed);
|
||||
if (!result.success)
|
||||
throw new Error('The existing file does not match the Svelte plugin schema.');
|
||||
// Keep schema annotations and future fields that this version does not edit.
|
||||
return /** @type {Config} */ (parsed);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @param {Config} config
|
||||
*/
|
||||
async function save_config(path, config) {
|
||||
await mkdir(dirname(path), { recursive: true });
|
||||
await writeFile(path, `${JSON.stringify(config, null, '\t')}\n`, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @param {string} [fallback]
|
||||
*/
|
||||
function display(value, fallback = 'default') {
|
||||
return value === undefined ? fallback : String(value);
|
||||
}
|
||||
|
||||
/** @type {TuiPlugin} */
|
||||
const tui = async (api) => {
|
||||
function open_scope() {
|
||||
if (!api.state.path.directory) {
|
||||
api.ui.toast({
|
||||
variant: 'warning',
|
||||
message: 'Paths are still syncing. Try again in a moment.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect
|
||||
title="Configure Svelte plugin"
|
||||
options={[
|
||||
{
|
||||
title: 'Project',
|
||||
value: 'project',
|
||||
description: 'Write .opencode/svelte.json for this project',
|
||||
},
|
||||
{
|
||||
title: 'Global',
|
||||
value: 'global',
|
||||
description: 'Write svelte.json in the OpenCode config directory',
|
||||
},
|
||||
]}
|
||||
onSelect={(option) => void open_config(/** @type {Scope} */ (option.value))}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
/** @param {Scope} scope */
|
||||
async function open_config(scope) {
|
||||
const path = config_path(api, scope);
|
||||
/** @type {Config} */
|
||||
let config;
|
||||
try {
|
||||
config = await read_config(path);
|
||||
} catch (error) {
|
||||
api.ui.toast({
|
||||
variant: 'error',
|
||||
title: 'Cannot edit Svelte configuration',
|
||||
message: error instanceof Error ? error.message : 'Failed to read configuration',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const original_config = structuredClone(config);
|
||||
/** @type {string | undefined} */
|
||||
let current_option;
|
||||
|
||||
async function persist(show_toast = true) {
|
||||
try {
|
||||
await save_config(path, config);
|
||||
if (show_toast)
|
||||
api.ui.toast({ variant: 'success', message: `Saved ${scope} Svelte configuration` });
|
||||
} catch {
|
||||
api.ui.toast({ variant: 'error', message: 'Failed to save Svelte configuration' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {'temperature' | 'top_p' | 'maxSteps'} key
|
||||
* @param {string} label
|
||||
*/
|
||||
function prompt_agent_number(key, label) {
|
||||
const agent = config.subagent?.agents?.[agent_name];
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogPrompt
|
||||
title={`${agent_name}: ${label}`}
|
||||
placeholder="Leave empty to use the default"
|
||||
value={agent?.[key] === undefined ? '' : String(agent[key])}
|
||||
onCancel={open_agent}
|
||||
onConfirm={async (value) => {
|
||||
const number = value.trim() === '' ? undefined : Number(value);
|
||||
if (number !== undefined && !Number.isFinite(number)) {
|
||||
api.ui.toast({ variant: 'warning', message: `${label} must be a number` });
|
||||
return;
|
||||
}
|
||||
config.subagent ??= {};
|
||||
config.subagent.agents ??= {};
|
||||
config.subagent.agents[agent_name] = { ...agent, [key]: number };
|
||||
await persist();
|
||||
open_agent();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function open_agent() {
|
||||
const agent = config.subagent?.agents?.[agent_name];
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect
|
||||
title={`Configure ${agent_name}`}
|
||||
options={[
|
||||
{ title: 'Model', value: 'model', description: display(agent?.model) },
|
||||
{
|
||||
title: 'Temperature',
|
||||
value: 'temperature',
|
||||
description: display(agent?.temperature),
|
||||
},
|
||||
{ title: 'Top P', value: 'top_p', description: display(agent?.top_p) },
|
||||
{ title: 'Maximum steps', value: 'maxSteps', description: display(agent?.maxSteps) },
|
||||
{ title: 'Back', value: 'back' },
|
||||
]}
|
||||
onSelect={(option) => {
|
||||
if (option.value === 'back') return open_menu();
|
||||
if (
|
||||
option.value === 'temperature' ||
|
||||
option.value === 'top_p' ||
|
||||
option.value === 'maxSteps'
|
||||
)
|
||||
return prompt_agent_number(option.value, option.title);
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogPrompt
|
||||
title={`${agent_name}: model`}
|
||||
placeholder="provider/model, or empty for default"
|
||||
value={agent?.model ?? ''}
|
||||
onCancel={open_agent}
|
||||
onConfirm={async (value) => {
|
||||
config.subagent ??= {};
|
||||
config.subagent.agents ??= {};
|
||||
config.subagent.agents[agent_name] = {
|
||||
...agent,
|
||||
model: value.trim() || undefined,
|
||||
};
|
||||
await persist();
|
||||
open_agent();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
function open_menu() {
|
||||
const skills = config.skills?.enabled;
|
||||
const selected_skills = new Set(
|
||||
Array.isArray(skills) ? skills : skills === false ? [] : skill_names,
|
||||
);
|
||||
const all_skills_selected = skill_names.every((name) => selected_skills.has(name));
|
||||
/** @param {boolean | undefined} value */
|
||||
function checked(value) {
|
||||
return value !== false ? '[x]' : '[ ]';
|
||||
}
|
||||
/** @param {'remote' | 'local'} value */
|
||||
function radio(value) {
|
||||
return (config.mcp?.type ?? 'remote') === value ? '(*)' : '( )';
|
||||
}
|
||||
api.ui.dialog.replace(() => (
|
||||
<api.ui.DialogSelect
|
||||
title={`Svelte plugin (${scope})`}
|
||||
{...(current_option === undefined ? {} : { current: current_option })}
|
||||
skipFilter
|
||||
options={[
|
||||
{
|
||||
title: `${checked(config.mcp?.enabled)} MCP server`,
|
||||
value: 'mcp-enabled',
|
||||
category: 'MCP',
|
||||
},
|
||||
{ title: `${radio('remote')} Remote`, value: 'mcp-remote', category: 'MCP transport' },
|
||||
{ title: `${radio('local')} Local`, value: 'mcp-local', category: 'MCP transport' },
|
||||
{
|
||||
title: `${checked(config.subagent?.enabled)} Subagent`,
|
||||
value: 'subagent-enabled',
|
||||
category: 'Subagent',
|
||||
},
|
||||
{ title: 'Subagent settings', value: 'agent' },
|
||||
{
|
||||
title: `${checked(config.instructions?.enabled)} Instructions`,
|
||||
value: 'instructions',
|
||||
category: 'Instructions',
|
||||
},
|
||||
{
|
||||
title: `${all_skills_selected ? '[x]' : '[ ]'} Select all`,
|
||||
value: 'skills-all',
|
||||
category: 'Skills',
|
||||
},
|
||||
...skill_names.map((name) => ({
|
||||
title: `${selected_skills.has(name) ? '[x]' : '[ ]'} ${name}`,
|
||||
value: `skill:${name}`,
|
||||
category: 'Skills',
|
||||
})),
|
||||
{
|
||||
title: `${config.autoupdate !== false ? '[x]' : '[ ]'} Auto update`,
|
||||
value: 'autoupdate',
|
||||
category: 'Updates',
|
||||
description: 'Reinstall the plugin on the next start when a new version is out',
|
||||
},
|
||||
{
|
||||
title: 'Revert changes',
|
||||
value: 'revert',
|
||||
category: 'Actions',
|
||||
description: 'Restore values from when this dialog opened',
|
||||
},
|
||||
{ title: 'Change scope', value: 'scope', category: 'Actions' },
|
||||
{ title: 'Close', value: 'close', category: 'Actions' },
|
||||
]}
|
||||
onMove={(option) => (current_option = option.value)}
|
||||
onSelect={async (option) => {
|
||||
current_option = option.value;
|
||||
if (option.value === 'close') return api.ui.dialog.clear();
|
||||
if (option.value === 'scope') return open_scope();
|
||||
if (option.value === 'agent') return open_agent();
|
||||
if (option.value === 'revert') {
|
||||
config = structuredClone(original_config);
|
||||
await persist();
|
||||
return open_menu();
|
||||
}
|
||||
if (option.value === 'mcp-enabled')
|
||||
config.mcp = { ...config.mcp, enabled: config.mcp?.enabled === false };
|
||||
if (option.value === 'mcp-remote') config.mcp = { ...config.mcp, type: 'remote' };
|
||||
if (option.value === 'mcp-local') config.mcp = { ...config.mcp, type: 'local' };
|
||||
if (option.value === 'subagent-enabled')
|
||||
config.subagent = { ...config.subagent, enabled: config.subagent?.enabled === false };
|
||||
if (option.value === 'instructions')
|
||||
config.instructions = {
|
||||
...config.instructions,
|
||||
enabled: config.instructions?.enabled === false,
|
||||
};
|
||||
if (option.value === 'skills-all') {
|
||||
config.skills = { enabled: all_skills_selected ? [] : [...skill_names] };
|
||||
}
|
||||
if (option.value === 'autoupdate') config.autoupdate = config.autoupdate === false;
|
||||
if (option.value.startsWith('skill:')) {
|
||||
const name = option.value.slice('skill:'.length);
|
||||
if (selected_skills.has(name)) {
|
||||
selected_skills.delete(name);
|
||||
} else {
|
||||
selected_skills.add(name);
|
||||
}
|
||||
config.skills = { enabled: [...selected_skills] };
|
||||
}
|
||||
await persist(false);
|
||||
open_menu();
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
open_menu();
|
||||
}
|
||||
|
||||
api.keymap.registerLayer({
|
||||
commands: [
|
||||
{
|
||||
name: `${plugin_id}.open`,
|
||||
title: 'Configure Svelte plugin',
|
||||
category: 'Plugin',
|
||||
namespace: 'palette',
|
||||
slashName: 'svelte-plugin',
|
||||
run: open_scope,
|
||||
},
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
export default /** @satisfies {TuiPluginModule & { id: string }} */ ({
|
||||
id: plugin_id,
|
||||
tui,
|
||||
});
|
||||
97
packages/opencode/update.js
Normal file
97
packages/opencode/update.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { exec } from 'node:child_process';
|
||||
import { rmSync } from 'node:fs';
|
||||
import { basename, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { compare } from 'verkit';
|
||||
import package_json from './package.json' with { type: 'json' };
|
||||
|
||||
/** @typedef {import('@opencode-ai/plugin').PluginInput} PluginInput */
|
||||
|
||||
const current_dir = dirname(fileURLToPath(import.meta.url));
|
||||
const name_segments = package_json.name.split('/');
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {number} levels
|
||||
*/
|
||||
function up(dir, levels) {
|
||||
for (let i = 0; i < levels; i++) dir = dirname(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* opencode installs every plugin in `<cache>/packages/<spec>/node_modules/<name>`, so removing
|
||||
* `<spec>` is enough to make it reinstall the plugin from scratch on the next start.
|
||||
*
|
||||
* We return `null` whenever we don't recognize that layout (for example when the plugin is linked
|
||||
* locally during development) so that we never delete a folder we don't own.
|
||||
*/
|
||||
export function get_install_dir(dir = current_dir) {
|
||||
// from `<cache>/packages/<spec>/node_modules/<name>` up to `<cache>/packages/<spec>`
|
||||
const install_dir = up(dir, name_segments.length + 1);
|
||||
// ...and from there up to `<cache>/packages`
|
||||
if (basename(up(install_dir, name_segments.length)) !== 'packages') return null;
|
||||
// Only unconstrained installs can pick up npm's latest version. Ranges and alternate tags may
|
||||
// resolve to the same installed version after every wipe.
|
||||
const package_name = name_segments.at(-1);
|
||||
if (!package_name || ![package_name, `${package_name}@latest`].includes(basename(install_dir))) {
|
||||
return null;
|
||||
}
|
||||
return install_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks npm for a newer version of the plugin and warns the user about it. If `autoupdate` is
|
||||
* enabled we also delete the cached plugin once opencode shuts down, so the next start picks up the
|
||||
* new version.
|
||||
*
|
||||
* @param {PluginInput} ctx
|
||||
* @param {boolean} autoupdate
|
||||
* @returns {() => Promise<void>} the `dispose` hook
|
||||
*/
|
||||
export function setup_updates(ctx, autoupdate) {
|
||||
/** @type {string | null} */
|
||||
let stale_dir = null;
|
||||
let wiped = false;
|
||||
|
||||
function wipe() {
|
||||
if (wiped || !stale_dir) return;
|
||||
wiped = true;
|
||||
try {
|
||||
rmSync(stale_dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// if we can't delete it there's nothing useful we can do at this point, the user will
|
||||
// just get the warning again on the next start
|
||||
}
|
||||
}
|
||||
|
||||
exec(`npm view ${package_json.name} version`, (_, version) => {
|
||||
const latest = version?.trim();
|
||||
if (!latest || compare(latest, package_json.version) !== 1) return;
|
||||
|
||||
stale_dir = autoupdate ? get_install_dir() : null;
|
||||
// `dispose` covers a graceful shutdown, `exit` is the safety net for everything else. We only
|
||||
// register it once we know we have something to delete to avoid piling up listeners.
|
||||
if (stale_dir) process.once('exit', wipe);
|
||||
|
||||
setTimeout(() => {
|
||||
ctx.client.tui.showToast({
|
||||
body: {
|
||||
title: 'Svelte: new plugin version available',
|
||||
message: `${package_json.name}@${latest} is available (you are using ${package_json.version}).\n\n${
|
||||
stale_dir
|
||||
? 'It will be installed automatically the next time you start OpenCode.'
|
||||
: 'Wipe the cache or update your OpenCode config to update.'
|
||||
}`,
|
||||
variant: 'warning',
|
||||
duration: 7000,
|
||||
},
|
||||
});
|
||||
}, 7000);
|
||||
});
|
||||
|
||||
return async () => {
|
||||
process.off('exit', wipe);
|
||||
wipe();
|
||||
};
|
||||
}
|
||||
35
packages/opencode/update.test.js
Normal file
35
packages/opencode/update.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { join } from 'node:path';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { get_install_dir } from './update.js';
|
||||
|
||||
const cache_packages = join('/cache', 'packages');
|
||||
|
||||
/**
|
||||
* @param {string} spec
|
||||
*/
|
||||
function plugin_dir(spec) {
|
||||
return join(cache_packages, '@sveltejs', spec, 'node_modules', '@sveltejs', 'opencode');
|
||||
}
|
||||
|
||||
describe('get_install_dir', () => {
|
||||
test.each([
|
||||
['an unpinned install', 'opencode'],
|
||||
['the latest tag', 'opencode@latest'],
|
||||
])('returns the cache directory for %s', (_, spec) => {
|
||||
expect(get_install_dir(plugin_dir(spec))).toBe(join(cache_packages, '@sveltejs', spec));
|
||||
});
|
||||
|
||||
test.each([
|
||||
['an exact version', 'opencode@0.1.11'],
|
||||
['an exact version with a v prefix', 'opencode@v0.1.11'],
|
||||
['a range', 'opencode@^0.1.0'],
|
||||
['an alternate dist-tag', 'opencode@beta'],
|
||||
])('ignores %s', (_, spec) => {
|
||||
expect(get_install_dir(plugin_dir(spec))).toBeNull();
|
||||
});
|
||||
|
||||
test('ignores a matching layout outside the OpenCode package cache', () => {
|
||||
const dir = join('/workspace', 'node_modules', '@sveltejs', 'opencode');
|
||||
expect(get_install_dir(dir)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "svelte",
|
||||
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.6",
|
||||
"author": {
|
||||
"name": "Svelte"
|
||||
},
|
||||
|
||||
@@ -4,13 +4,13 @@ description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when cre
|
||||
permissionMode: acceptEdits
|
||||
---
|
||||
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
|
||||
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
|
||||
|
||||
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
|
||||
|
||||
## Available MCP Tools
|
||||
## Available MCP tools
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
@@ -36,30 +36,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 1. Gather Context (if needed)
|
||||
### 1. Gather context (if needed)
|
||||
|
||||
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
|
||||
|
||||
1. Call `list-sections` to see available documentation
|
||||
2. Call `get-documentation` with relevant section names
|
||||
|
||||
### 2. Read the Target File
|
||||
### 2. Read the target file
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
### 3. Make changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
### 4. Validate changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
### 5. Fix any issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
|
||||
|
||||
## $inspect(...).with
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
|
||||
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## $inspect.trace(...)
|
||||
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}> Hover me </button>
|
||||
<button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>Hover me</Button>
|
||||
<Button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey/llms.txt).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments#fromAction/llms.txt), allowing you to (for example) use them with components.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
|
||||
@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
|
||||
|
||||
<!-- codeblock:start {"title":"Synchronized updates"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
<input type="number" bind:value={a}>
|
||||
<input type="number" bind:value={b}>
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p><p>{await two()}</p>
|
||||
<p>{await one(x)}</p>
|
||||
<p>{await two(y)}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
// `b` will not be created until `a` has resolved,
|
||||
// but once created they will update independently
|
||||
// even if `x` and `y` update simultaneously
|
||||
let a = $derived(await one(x));
|
||||
let b = $derived(await two(y));
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings#Client-warnings-await_waterfall/llms.txt) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary#Properties-pending/llms.txt) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect#$effect.pending/llms.txt). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte#settled/llms.txt) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
@@ -103,7 +110,7 @@ async function onclick() {
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
|
||||
|
||||
## Forking
|
||||
|
||||
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte#fork/llms.txt) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
}}
|
||||
>open menu</button>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
<Menu onclose={() => open = false} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
|
||||
|
||||
```svelte
|
||||
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
|
||||
<input bind:value={
|
||||
() => value,
|
||||
(v) => value = v.toLowerCase()}
|
||||
/>
|
||||
```
|
||||
|
||||
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
|
||||
|
||||
```svelte
|
||||
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
|
||||
<div
|
||||
bind:clientWidth={null, redraw}
|
||||
bind:clientHeight={null, redraw}
|
||||
>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
csp: { nonce },
|
||||
csp: { nonce }
|
||||
});
|
||||
```
|
||||
|
||||
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
|
||||
|
||||
```js
|
||||
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src 'nonce-${nonce}'`
|
||||
);
|
||||
```
|
||||
|
||||
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
|
||||
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
csp: { hash: true }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
|
||||
);
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{#snippet name(param1, param2, paramN)}...{/snippet}
|
||||
```
|
||||
|
||||
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
|
||||
|
||||
<!-- codeblock:start {"title":"Snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
Snippets can reference themselves and each other:
|
||||
|
||||
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
### Explicit props
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
|
||||
|
||||
<!-- codeblock:start {"title":"Explicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
<Table data={fruits} +++{header} {row}+++ />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
|
||||
|
||||
### Implicit props
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
|
||||
</Table>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
### Optional snippet props
|
||||
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
row
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
|
||||
## Exporting snippets
|
||||
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
|
||||
|
||||
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import { add } from './snippets.svelte';
|
||||
</script>
|
||||
|
||||
{@render add(1, 2)}
|
||||
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: snippets.svelte --->
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE]
|
||||
> This requires Svelte 5.5.0 or newer
|
||||
|
||||
## Programmatic snippets
|
||||
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte#createRawSnippet/llms.txt) API. This is intended for advanced use cases.
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
|
||||
@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
|
||||
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery/llms.txt):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "svelte",
|
||||
"description": "A plugin for all things related to Svelte development, MCP, skills, and more.",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.6",
|
||||
"author": {
|
||||
"name": "Svelte"
|
||||
},
|
||||
|
||||
@@ -3,13 +3,13 @@ name: svelte-file-editor
|
||||
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
|
||||
---
|
||||
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
|
||||
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
|
||||
|
||||
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
|
||||
|
||||
## Available MCP Tools
|
||||
## Available MCP tools
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 1. Gather Context (if needed)
|
||||
### 1. Gather context (if needed)
|
||||
|
||||
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
|
||||
|
||||
1. Call `list-sections` to see available documentation
|
||||
2. Call `get-documentation` with relevant section names
|
||||
|
||||
### 2. Read the Target File
|
||||
### 2. Read the target file
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
### 3. Make changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
### 4. Validate changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
### 5. Fix any issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
|
||||
|
||||
## $inspect(...).with
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
|
||||
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## $inspect.trace(...)
|
||||
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}> Hover me </button>
|
||||
<button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>Hover me</Button>
|
||||
<Button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey/llms.txt).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments#fromAction/llms.txt), allowing you to (for example) use them with components.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
|
||||
@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
|
||||
|
||||
<!-- codeblock:start {"title":"Synchronized updates"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
<input type="number" bind:value={a}>
|
||||
<input type="number" bind:value={b}>
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p><p>{await two()}</p>
|
||||
<p>{await one(x)}</p>
|
||||
<p>{await two(y)}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
// `b` will not be created until `a` has resolved,
|
||||
// but once created they will update independently
|
||||
// even if `x` and `y` update simultaneously
|
||||
let a = $derived(await one(x));
|
||||
let b = $derived(await two(y));
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings#Client-warnings-await_waterfall/llms.txt) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary#Properties-pending/llms.txt) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect#$effect.pending/llms.txt). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte#settled/llms.txt) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
@@ -103,7 +110,7 @@ async function onclick() {
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
|
||||
|
||||
## Forking
|
||||
|
||||
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte#fork/llms.txt) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
}}
|
||||
>open menu</button>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
<Menu onclose={() => open = false} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
|
||||
|
||||
```svelte
|
||||
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
|
||||
<input bind:value={
|
||||
() => value,
|
||||
(v) => value = v.toLowerCase()}
|
||||
/>
|
||||
```
|
||||
|
||||
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
|
||||
|
||||
```svelte
|
||||
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
|
||||
<div
|
||||
bind:clientWidth={null, redraw}
|
||||
bind:clientHeight={null, redraw}
|
||||
>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
csp: { nonce },
|
||||
csp: { nonce }
|
||||
});
|
||||
```
|
||||
|
||||
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
|
||||
|
||||
```js
|
||||
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src 'nonce-${nonce}'`
|
||||
);
|
||||
```
|
||||
|
||||
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
|
||||
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
csp: { hash: true }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
|
||||
);
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{#snippet name(param1, param2, paramN)}...{/snippet}
|
||||
```
|
||||
|
||||
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
|
||||
|
||||
<!-- codeblock:start {"title":"Snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
Snippets can reference themselves and each other:
|
||||
|
||||
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
### Explicit props
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
|
||||
|
||||
<!-- codeblock:start {"title":"Explicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
<Table data={fruits} +++{header} {row}+++ />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
|
||||
|
||||
### Implicit props
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
|
||||
</Table>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
### Optional snippet props
|
||||
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
row
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
|
||||
## Exporting snippets
|
||||
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
|
||||
|
||||
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import { add } from './snippets.svelte';
|
||||
</script>
|
||||
|
||||
{@render add(1, 2)}
|
||||
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: snippets.svelte --->
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE]
|
||||
> This requires Svelte 5.5.0 or newer
|
||||
|
||||
## Programmatic snippets
|
||||
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte#createRawSnippet/llms.txt) API. This is intended for advanced use cases.
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
|
||||
@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
|
||||
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery/llms.txt):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
|
||||
1180
pnpm-lock.yaml
generated
1180
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ catalogs:
|
||||
'@anthropic-ai/sdk': ^0.71.0
|
||||
'@mcp-ui/server': ^6.0.0
|
||||
'@modelcontextprotocol/inspector': ^0.19.0
|
||||
'@opencode-ai/plugin': ^1.1.44
|
||||
lint:
|
||||
'@eslint/compat': ^2.0.0
|
||||
'@eslint/js': ^9.36.0
|
||||
@@ -24,6 +23,12 @@ catalogs:
|
||||
prettier-plugin-svelte: ^3.3.3
|
||||
svelte-eslint-parser: ^1.7.1
|
||||
typescript-eslint: ^8.44.0
|
||||
opencode:
|
||||
'@opencode-ai/plugin': 1.17.9
|
||||
'@opentui/core': ^0.4.3
|
||||
'@opentui/keymap': ^0.4.3
|
||||
'@opentui/solid': ^0.4.3
|
||||
solid-js: 1.9.12
|
||||
svelte:
|
||||
'@sveltejs/adapter-vercel': ^6.0.0
|
||||
'@sveltejs/kit': ^2.42.2
|
||||
@@ -37,8 +42,8 @@ catalogs:
|
||||
'@tmcp/transport-stdio': ^0.4.2
|
||||
tmcp: ^1.19.3
|
||||
tooling:
|
||||
'@changesets/changelog-github': 1.0.0-next.6
|
||||
'@changesets/cli': ^2.29.7
|
||||
'@svitejs/changesets-changelog-github-compact': ^1.2.0
|
||||
'@types/estree': ^1.0.8
|
||||
'@types/node': ^24.3.1
|
||||
'@valibot/to-json-schema': ^1.5.0
|
||||
@@ -51,6 +56,7 @@ catalogs:
|
||||
tsdown: ^0.20.0
|
||||
typescript: ^5.0.0
|
||||
valibot: ^1.2.0
|
||||
verkit: ^0.1.2
|
||||
vite: ^7.0.4
|
||||
vite-plugin-devtools-json: ^1.0.0
|
||||
vitest: ^4.0.0
|
||||
@@ -67,4 +73,4 @@ minimumReleaseAgeExclude:
|
||||
- svelte-check
|
||||
- esm-env
|
||||
|
||||
useNodeVersion: 22.19.0
|
||||
useNodeVersion: 22.22.2
|
||||
|
||||
@@ -106,6 +106,22 @@ function derive_name(link: string) {
|
||||
return segments[segments.length - 1] ?? 'reference';
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes links copied into reference files point back to their pages on svelte.dev.
|
||||
*/
|
||||
export function resolve_reference_links(content: string, repo: string) {
|
||||
return content.replace(
|
||||
/\[([^\]]*)\]\((?![a-z][a-z\d+.-]*:|#|\/\/)([^)]+)\)/gi,
|
||||
(full_match, text: string, href: string) => {
|
||||
const url = href.startsWith('/')
|
||||
? `https://svelte.dev${href}/llms.txt`
|
||||
: `https://svelte.dev/docs/${repo}/${href}/llms.txt`;
|
||||
|
||||
return `[${text}](${url})`;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const content = remove_llm_ignore_blocks(
|
||||
remove_frontmatter_unneeded_fields(await get_content(file)),
|
||||
);
|
||||
@@ -203,8 +219,13 @@ for (const link of links) {
|
||||
|
||||
const ref_filename = `${name}.md`;
|
||||
const ref_path = path.join(references_dir, ref_filename);
|
||||
const reference_repo = link.is_absolute_docs ? link.clean_path.split('/')[2]! : repo;
|
||||
const reference_content = resolve_reference_links(
|
||||
remove_llm_ignore_blocks(remove_cut_preambles(fetched_content)),
|
||||
reference_repo,
|
||||
);
|
||||
|
||||
await fs.writeFile(ref_path, remove_llm_ignore_blocks(remove_cut_preambles(fetched_content)));
|
||||
await fs.writeFile(ref_path, reference_content);
|
||||
console.log(` Saved: references/${ref_filename}`);
|
||||
|
||||
// Replace the link in the markdown
|
||||
|
||||
@@ -68,7 +68,7 @@ function parse_agent_md(content: string, file_path: string): AgentData | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate agents.ts module from tools/agents/*.md files
|
||||
* Generate agents.js module from tools/agents/*.md files
|
||||
*/
|
||||
async function sync_agents() {
|
||||
const agents_dir = path.join(TOOLS_DIR, 'agents');
|
||||
@@ -96,14 +96,14 @@ async function sync_agents() {
|
||||
),
|
||||
null,
|
||||
'\t',
|
||||
)} as const;`,
|
||||
)};`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
const dest = path.join(OPENCODE_PKG_DIR, 'agents.ts');
|
||||
const dest = path.join(OPENCODE_PKG_DIR, 'agents.js');
|
||||
await fs.writeFile(dest, output);
|
||||
|
||||
console.log(`Generated agents.ts with ${agents.length} agent(s)`);
|
||||
console.log(`Generated agents.js with ${agents.length} agent(s)`);
|
||||
}
|
||||
|
||||
await sync_skills();
|
||||
|
||||
11
skills-lock.json
Normal file
11
skills-lock.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"skills": {
|
||||
"writing-great-skills": {
|
||||
"source": "mattpocock/skills",
|
||||
"sourceType": "github",
|
||||
"skillPath": "skills/productivity/writing-great-skills/SKILL.md",
|
||||
"computedHash": "dd555ce552f82784c3d2b8d13a8e26a6677a07ddc00032e142dec33bfd5438c6"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,13 @@ name: svelte-file-editor
|
||||
description: Specialized Svelte 5 code editor. MUST BE USED PROACTIVELY when creating, editing, or reviewing any .svelte file or .svelte.ts/.svelte.js module and MUST use the tools from the MCP server or the `svelte-file-editor` skill if they are available. Fetches relevant documentation and validates code using the Svelte MCP server tools.
|
||||
---
|
||||
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the svelte MCP server to fetch documentation with `get_documentation` and validating the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
You are a Svelte 5 expert responsible for writing, editing, and validating Svelte components and modules. You have access to the Svelte MCP server which provides documentation and code analysis tools. Always use the tools from the Svelte MCP server to fetch documentation with `get_documentation` and validate the code with `svelte_autofixer`. If the autofixer returns any issue or suggestions try to solve them.
|
||||
|
||||
If the MCP tools are not available you can use the `svelte-code-writer` skill to learn how to use the `@sveltejs/mcp` cli to access the same tools.
|
||||
|
||||
If the skill is not available you can run `npx @sveltejs/mcp@latest -y --help` to learn how to use it.
|
||||
|
||||
## Available MCP Tools
|
||||
## Available MCP tools
|
||||
|
||||
### 1. list-sections
|
||||
|
||||
@@ -35,30 +35,30 @@ Analyzes Svelte code and returns suggestions to fix issues. Pass the component c
|
||||
|
||||
When invoked to work on a Svelte file:
|
||||
|
||||
### 1. Gather Context (if needed)
|
||||
### 1. Gather context (if needed)
|
||||
|
||||
If you're uncertain about Svelte 5 syntax or patterns, use the MCP tools:
|
||||
|
||||
1. Call `list-sections` to see available documentation
|
||||
2. Call `get-documentation` with relevant section names
|
||||
|
||||
### 2. Read the Target File
|
||||
### 2. Read the target file
|
||||
|
||||
Read the file to understand the current implementation.
|
||||
|
||||
### 3. Make Changes
|
||||
### 3. Make changes
|
||||
|
||||
Apply edits following Svelte 5 best practices:
|
||||
|
||||
### 4. Validate Changes
|
||||
### 4. Validate changes
|
||||
|
||||
After editing, ALWAYS call `svelte-autofixer` with the updated code to check for issues.
|
||||
|
||||
### 5. Fix Any Issues
|
||||
### 5. Fix any issues
|
||||
|
||||
If the autofixer reports problems, fix them and re-validate until no issues remain.
|
||||
|
||||
## Output Format
|
||||
## Output format
|
||||
|
||||
After completing your work, provide:
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ name: svelte-code-writer
|
||||
description: CLI tools for Svelte 5 documentation lookup and code analysis. MUST be used whenever creating, editing or analyzing any Svelte component (.svelte) or Svelte module (.svelte.ts/.svelte.js). If possible, this skill should be executed within the svelte-file-editor agent for optimal results.
|
||||
---
|
||||
|
||||
# Svelte 5 Code Writer
|
||||
|
||||
## CLI Tools
|
||||
## CLI tools
|
||||
|
||||
You have access to `@sveltejs/mcp` CLI for Svelte-specific assistance. Use these commands via `npx`:
|
||||
|
||||
### List Documentation Sections
|
||||
### List documentation sections
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp list-sections
|
||||
@@ -17,7 +15,7 @@ npx @sveltejs/mcp list-sections
|
||||
|
||||
Lists all available Svelte 5 and SvelteKit documentation sections with titles and paths.
|
||||
|
||||
### Get Documentation
|
||||
### Get documentation
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp get-documentation "<section1>,<section2>,..."
|
||||
@@ -31,7 +29,7 @@ Retrieves full documentation for specified sections. Use after `list-sections` t
|
||||
npx @sveltejs/mcp get-documentation "$state,$derived,$effect"
|
||||
```
|
||||
|
||||
### Svelte Autofixer
|
||||
### Svelte autofixer
|
||||
|
||||
```bash
|
||||
npx @sveltejs/mcp svelte-autofixer "<code_or_path>" [options]
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
> [!NOTE] `$inspect` only works during development. In a production build it becomes a noop.
|
||||
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire (demo:
|
||||
The `$inspect` rune is roughly equivalent to `console.log`, with the exception that it will re-run whenever its argument changes. `$inspect` tracks reactive state deeply, meaning that updating something inside an object or array using fine-grained reactivity will cause it to re-fire:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let message = $state('hello');
|
||||
@@ -14,13 +17,18 @@ The `$inspect` rune is roughly equivalent to `console.log`, with the exception t
|
||||
<input bind:value={message} />
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
On updates, a stack trace will be printed, making it easy to find the origin of a state change (unless you're in the playground, due to technical limitations).
|
||||
|
||||
## $inspect(...).with
|
||||
|
||||
`$inspect` returns a property `with`, which you can invoke with a callback, which will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect` (demo:
|
||||
`$inspect(...)` returns an object with a `with` method, which you can invoke with a callback that will then be invoked instead of `console.log`. The first argument to the callback is either `"init"` or `"update"`; subsequent arguments are the values passed to `$inspect`:
|
||||
|
||||
<!-- codeblock:start {"title":"$inspect(...).with(...)"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
@@ -34,9 +42,11 @@ On updates, a stack trace will be printed, making it easy to find the origin of
|
||||
<button onclick={() => count++}>Increment</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## $inspect.trace(...)
|
||||
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect]($effect) or a [derived]($derived), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
This rune, added in 5.14, causes the surrounding function to be _traced_ in development. Any time the function re-runs as part of an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) or a [derived](https://svelte.dev/docs/svelte/$derived/llms.txt), information will be printed to the console about which pieces of reactive state caused the effect to fire.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Attachments are functions that run in an [effect]($effect) when an element is mounted to the DOM or when [state]($state) read inside the function updates.
|
||||
Attachments are functions that run in an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt) when an element is mounted to the DOM or when [state](https://svelte.dev/docs/svelte/$state/llms.txt) read inside the function updates.
|
||||
|
||||
Optionally, they can return a function that is called before the attachment re-runs, or after the element is later removed from the DOM.
|
||||
|
||||
@@ -48,10 +48,12 @@ A useful pattern is for a function, such as `tooltip` in this example, to _retur
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<button {@attach tooltip(content)}> Hover me </button>
|
||||
<button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</button>
|
||||
```
|
||||
|
||||
Since the `tooltip(content)` expression runs inside an [effect]($effect), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
Since the `tooltip(content)` expression runs inside an [effect](https://svelte.dev/docs/svelte/$effect/llms.txt), the attachment will be destroyed and recreated whenever `content` changes. The same thing would happen for any state read _inside_ the attachment function when it first runs. (If this isn't what you want, see [Controlling when attachments re-run](#Controlling-when-attachments-re-run).)
|
||||
|
||||
## Inline attachments
|
||||
|
||||
@@ -86,7 +88,7 @@ Falsy values like `false` or `undefined` are treated as no attachment, enabling
|
||||
|
||||
## Passing attachments to components
|
||||
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](/tutorial/svelte/spread-props) props onto an element, the element will receive those attachments.
|
||||
When used on a component, `{@attach ...}` will create a prop whose key is a [`Symbol`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol). If the component then [spreads](https://svelte.dev/tutorial/svelte/spread-props/llms.txt) props onto an element, the element will receive those attachments.
|
||||
|
||||
This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
@@ -125,12 +127,14 @@ This allows you to create _wrapper components_ that augment elements (demo:
|
||||
|
||||
<input bind:value={content} />
|
||||
|
||||
<Button {@attach tooltip(content)}>Hover me</Button>
|
||||
<Button {@attach tooltip(content)}>
|
||||
Hover me
|
||||
</Button>
|
||||
```
|
||||
|
||||
## Controlling when attachments re-run
|
||||
|
||||
Attachments, unlike [actions](use), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
Attachments, unlike [actions](https://svelte.dev/docs/svelte/use/llms.txt), are fully reactive: `{@attach foo(bar)}` will re-run on changes to `foo` _or_ `bar` (or any state read inside `foo`):
|
||||
|
||||
```js
|
||||
// @errors: 7006 2304 2552
|
||||
@@ -159,8 +163,8 @@ function foo(+++getBar+++) {
|
||||
|
||||
## Creating attachments programmatically
|
||||
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](svelte-attachments#createAttachmentKey).
|
||||
To add attachments to an object that will be spread onto a component or element, use [`createAttachmentKey`](https://svelte.dev/docs/svelte/svelte-attachments#createAttachmentKey/llms.txt).
|
||||
|
||||
## Converting actions to attachments
|
||||
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](svelte-attachments#fromAction), allowing you to (for example) use them with components.
|
||||
If you're using a library that only provides actions, you can convert them to attachments with [`fromAction`](https://svelte.dev/docs/svelte/svelte-attachments#fromAction/llms.txt), allowing you to (for example) use them with components.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
To render a [snippet](snippet), use a `{@render ...}` tag.
|
||||
To render a [snippet](https://svelte.dev/docs/svelte/snippet/llms.txt), use a `{@render ...}` tag.
|
||||
|
||||
```svelte
|
||||
{#snippet sum(a, b)}
|
||||
@@ -24,7 +24,7 @@ If the snippet is potentially undefined — for example, because it's an incomin
|
||||
{@render children?.()}
|
||||
```
|
||||
|
||||
Alternatively, use an [`{#if ...}`](if) block with an `:else` clause to render fallback content:
|
||||
Alternatively, use an [`{#if ...}`](https://svelte.dev/docs/svelte/if/llms.txt) block with an `:else` clause to render fallback content:
|
||||
|
||||
```svelte
|
||||
{#if children}
|
||||
|
||||
@@ -4,16 +4,16 @@ As of Svelte 5.36, you can use the `await` keyword inside your components in thr
|
||||
- inside `$derived(...)` declarations
|
||||
- inside your markup
|
||||
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](/docs/kit/configuration) Svelte, usually `svelte.config.js`:
|
||||
This feature is currently experimental, and you must opt in by adding the `experimental.async` option wherever you [configure](https://svelte.dev/docs/kit/configuration/llms.txt) Svelte, usually `svelte.config.js`:
|
||||
|
||||
```js
|
||||
/// file: svelte.config.js
|
||||
export default {
|
||||
compilerOptions: {
|
||||
experimental: {
|
||||
async: true,
|
||||
},
|
||||
},
|
||||
async: true
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
@@ -23,7 +23,10 @@ The experimental flag will be removed in Svelte 6.
|
||||
|
||||
When an `await` expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...
|
||||
|
||||
<!-- codeblock:start {"title":"Synchronized updates"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let a = $state(1);
|
||||
let b = $state(2);
|
||||
@@ -34,12 +37,14 @@ When an `await` expression depends on a particular piece of state, changes to th
|
||||
}
|
||||
</script>
|
||||
|
||||
<input type="number" bind:value={a} />
|
||||
<input type="number" bind:value={b} />
|
||||
<input type="number" bind:value={a}>
|
||||
<input type="number" bind:value={b}>
|
||||
|
||||
<p>{a} + {b} = {await add(a, b)}</p>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...if you increment `a`, the contents of the `<p>` will _not_ immediately update to read this —
|
||||
|
||||
```html
|
||||
@@ -55,7 +60,8 @@ Updates can overlap — a fast update will be reflected in the UI while an earli
|
||||
Svelte will do as much asynchronous work as it can in parallel. For example if you have two `await` expressions in your markup...
|
||||
|
||||
```svelte
|
||||
<p>{await one()}</p><p>{await two()}</p>
|
||||
<p>{await one(x)}</p>
|
||||
<p>{await two(y)}</p>
|
||||
```
|
||||
|
||||
...both functions will run at the same time, as they are independent expressions, even though they are _visually_ sequential.
|
||||
@@ -63,21 +69,22 @@ Svelte will do as much asynchronous work as it can in parallel. For example if y
|
||||
This does not apply to sequential `await` expressions inside your `<script>` or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent `$derived` expressions will update independently, even though they will run sequentially when they are first created:
|
||||
|
||||
```js
|
||||
// these will run sequentially the first time,
|
||||
// but will update independently
|
||||
let a = $derived(await one());
|
||||
let b = $derived(await two());
|
||||
// `b` will not be created until `a` has resolved,
|
||||
// but once created they will update independently
|
||||
// even if `x` and `y` update simultaneously
|
||||
let a = $derived(await one(x));
|
||||
let b = $derived(await two(y));
|
||||
```
|
||||
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](runtime-warnings#Client-warnings-await_waterfall) warning
|
||||
> [!NOTE] If you write code like this, expect Svelte to give you an [`await_waterfall`](https://svelte.dev/docs/svelte/runtime-warnings#Client-warnings-await_waterfall/llms.txt) warning
|
||||
|
||||
## Indicating loading states
|
||||
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](svelte-boundary#Properties-pending) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
To render placeholder UI, you can wrap content in a `<svelte:boundary>` with a [`pending`](https://svelte.dev/docs/svelte/svelte-boundary#Properties-pending/llms.txt) snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.
|
||||
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`]($effect#$effect.pending). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
After the contents of a boundary have resolved for the first time and have replaced the `pending` snippet, you can detect subsequent async work with [`$effect.pending()`](https://svelte.dev/docs/svelte/$effect#$effect.pending/llms.txt). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.
|
||||
|
||||
You can also use [`settled()`](svelte#settled) to get a promise that resolves when the current update is complete:
|
||||
You can also use [`settled()`](https://svelte.dev/docs/svelte/svelte#settled/llms.txt) to get a promise that resolves when the current update is complete:
|
||||
|
||||
```js
|
||||
import { tick, settled } from 'svelte';
|
||||
@@ -103,7 +110,7 @@ async function onclick() {
|
||||
|
||||
## Error handling
|
||||
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](svelte-boundary).
|
||||
Errors in `await` expressions will bubble to the nearest [error boundary](https://svelte.dev/docs/svelte/svelte-boundary/llms.txt).
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
@@ -125,7 +132,7 @@ If a `<svelte:boundary>` with a `pending` snippet is encountered during SSR, tha
|
||||
|
||||
## Forking
|
||||
|
||||
The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
The [`fork(...)`](https://svelte.dev/docs/svelte/svelte#fork/llms.txt) API, added in 5.42, makes it possible to run `await` expressions that you _expect_ to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
@@ -161,13 +168,13 @@ The [`fork(...)`](svelte#fork) API, added in 5.42, makes it possible to run `awa
|
||||
// in case `pending` didn't exist
|
||||
// (if it did, this is a no-op)
|
||||
open = true;
|
||||
}}>open menu</button
|
||||
>
|
||||
}}
|
||||
>open menu</button>
|
||||
|
||||
{#if open}
|
||||
<!-- any async work inside this component will start
|
||||
as soon as the fork is created -->
|
||||
<Menu onclose={() => (open = false)} />
|
||||
<Menu onclose={() => open = false} />
|
||||
{/if}
|
||||
```
|
||||
|
||||
|
||||
@@ -3,13 +3,19 @@
|
||||
You can also use `bind:property={get, set}`, where `get` and `set` are functions, allowing you to perform validation and transformation:
|
||||
|
||||
```svelte
|
||||
<input bind:value={() => value, (v) => (value = v.toLowerCase())} />
|
||||
<input bind:value={
|
||||
() => value,
|
||||
(v) => value = v.toLowerCase()}
|
||||
/>
|
||||
```
|
||||
|
||||
In the case of readonly bindings like [dimension bindings](#Dimensions), the `get` value should be `null`:
|
||||
|
||||
```svelte
|
||||
<div bind:clientWidth={null, redraw} bind:clientHeight={null, redraw}>...</div>
|
||||
<div
|
||||
bind:clientWidth={null, redraw}
|
||||
bind:clientHeight={null, redraw}
|
||||
>...</div>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
@@ -2,31 +2,31 @@ In Svelte, when you want to render asynchronous content data on the server, you
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { getUser } from 'my-database-library';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
// This will get the user on the server, render the user's name into the h1,
|
||||
// and then, during hydration on the client, it will get the user _again_,
|
||||
// blocking hydration until it's done.
|
||||
const user = await getUser();
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
```
|
||||
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
|
||||
That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API built to solve this problem. You probably won't need this very often — it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](https://svelte.dev/docs/kit/remote-functions/llms.txt).
|
||||
|
||||
To fix the example above:
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
import { hydratable } from 'svelte';
|
||||
import { getUser } from 'my-database-library';
|
||||
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
// During server rendering, this will serialize and stash the result of `getUser`, associating
|
||||
// it with the provided key and baking it into the `head` content. During hydration, it will
|
||||
// look for the serialized version, returning it instead of running `getUser`. After hydration
|
||||
// is done, if it's called again, it'll simply invoke `getUser`.
|
||||
const user = await hydratable('user', () => getUser());
|
||||
</script>
|
||||
|
||||
<h1>{user.name}</h1>
|
||||
@@ -47,13 +47,13 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2),
|
||||
};
|
||||
});
|
||||
import { hydratable } from 'svelte';
|
||||
const promises = hydratable('random', () => {
|
||||
return {
|
||||
one: Promise.resolve(1),
|
||||
two: Promise.resolve(2)
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{await promises.one}
|
||||
@@ -68,14 +68,17 @@ All data returned from a `hydratable` function must be serializable. But this do
|
||||
const nonce = crypto.randomUUID();
|
||||
|
||||
const { head, body } = await render(App, {
|
||||
csp: { nonce },
|
||||
csp: { nonce }
|
||||
});
|
||||
```
|
||||
|
||||
This will add the `nonce` to the script block, on the assumption that you will later add the same nonce to the CSP header of the document that contains it:
|
||||
|
||||
```js
|
||||
response.headers.set('Content-Security-Policy', `script-src 'nonce-${nonce}'`);
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src 'nonce-${nonce}'`
|
||||
);
|
||||
```
|
||||
|
||||
It's essential that a `nonce` — which, British slang definition aside, means 'number used once' — is only used when dynamically server rendering an individual response.
|
||||
@@ -84,7 +87,7 @@ If instead you are generating static HTML ahead of time, you must use hashes ins
|
||||
|
||||
```js
|
||||
const { head, body, hashes } = await render(App, {
|
||||
csp: { hash: true },
|
||||
csp: { hash: true }
|
||||
});
|
||||
```
|
||||
|
||||
@@ -92,9 +95,9 @@ const { head, body, hashes } = await render(App, {
|
||||
|
||||
```js
|
||||
response.headers.set(
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`,
|
||||
);
|
||||
'Content-Security-Policy',
|
||||
`script-src ${hashes.script.map((hash) => `'${hash}'`).join(' ')}`
|
||||
);
|
||||
```
|
||||
|
||||
We recommend using `nonce` over hash if you can, as `hash` will interfere with streaming SSR in the future.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{#snippet name(param1, param2, paramN)}...{/snippet}
|
||||
```
|
||||
|
||||
Snippets, and [render tags](@render), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
Snippets, and [render tags](https://svelte.dev/docs/svelte/@render/llms.txt), are a way to create reusable chunks of markup inside your components. Instead of writing duplicative code like this...
|
||||
|
||||
```svelte
|
||||
{#each images as image}
|
||||
@@ -53,9 +53,12 @@ Like function declarations, snippets can have an arbitrary number of parameters,
|
||||
|
||||
## Snippet scope
|
||||
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks (demo...
|
||||
Snippets can be declared anywhere inside your component. They can reference values declared outside themselves, for example in the `<script>` tag or in `{#each ...}` blocks...
|
||||
|
||||
<!-- codeblock:start {"title":"Snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
let { message = `it's great to see you!` } = $props();
|
||||
</script>
|
||||
@@ -68,6 +71,8 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render hello('bob')}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
...and they are 'visible' to everything in the same lexical scope (i.e. siblings, and children of those siblings):
|
||||
|
||||
```svelte
|
||||
@@ -87,9 +92,12 @@ Snippets can be declared anywhere inside your component. They can reference valu
|
||||
{@render x()}
|
||||
```
|
||||
|
||||
Snippets can reference themselves and each other (demo:
|
||||
Snippets can reference themselves and each other:
|
||||
|
||||
<!-- codeblock:start {"title":"Self-referencing snippets"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
{#snippet blastoff()}
|
||||
<span>🚀</span>
|
||||
{/snippet}
|
||||
@@ -106,20 +114,25 @@ Snippets can reference themselves and each other (demo:
|
||||
{@render countdown(10)}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
## Passing snippets to components
|
||||
|
||||
### Explicit props
|
||||
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props (demo:
|
||||
Within the template, snippets are values just like any other. As such, they can be passed to components as props:
|
||||
|
||||
<!-- codeblock:start {"title":"Explicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -137,17 +150,67 @@ Within the template, snippets are values just like any other. As such, they can
|
||||
<td>{d.qty * d.price}</td>
|
||||
{/snippet}
|
||||
|
||||
<Table data={fruits} {header} {row} />
|
||||
<Table data={fruits} +++{header} {row}+++ />
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
Think about it like passing content instead of data to a component. The concept is similar to slots in web components.
|
||||
|
||||
### Implicit props
|
||||
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component (demo:
|
||||
As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit snippet props"} -->
|
||||
|
||||
```svelte
|
||||
<!-- this is semantically the same as the above -->
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Table from './Table.svelte';
|
||||
|
||||
const fruits = [
|
||||
{ name: 'apples', qty: 5, price: 2 },
|
||||
{ name: 'bananas', qty: 10, price: 1 },
|
||||
{ name: 'cherries', qty: 20, price: 0.5 }
|
||||
];
|
||||
</script>
|
||||
|
||||
<Table data={fruits}>
|
||||
{#snippet header()}
|
||||
<th>fruit</th>
|
||||
@@ -165,12 +228,56 @@ As an authoring convenience, snippets declared directly _inside_ a component imp
|
||||
</Table>
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: Table.svelte --->
|
||||
<script>
|
||||
let { data, header, row } = $props();
|
||||
</script>
|
||||
|
||||
<table>
|
||||
{#if header}
|
||||
<thead>
|
||||
<tr>{@render header()}</tr>
|
||||
</thead>
|
||||
{/if}
|
||||
|
||||
<tbody>
|
||||
{#each data as d}
|
||||
<tr>{@render row(d)}</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<style>
|
||||
table {
|
||||
text-align: left;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(2n+1) {
|
||||
background: ButtonFace;
|
||||
}
|
||||
|
||||
table :global(th), table :global(td) {
|
||||
padding: 0.5em;
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
### Implicit `children` snippet
|
||||
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet (demo:
|
||||
Any content inside the component tags that is _not_ a snippet declaration implicitly becomes part of the `children` snippet:
|
||||
|
||||
<!-- codeblock:start {"title":"Implicit children snippet","selected":"Button.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<Button>click me</Button>
|
||||
```
|
||||
|
||||
@@ -184,6 +291,8 @@ Any content inside the component tags that is _not_ a snippet declaration implic
|
||||
<button>{@render children()}</button>
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE] Note that you cannot have a prop called `children` if you also have content inside the component — for this reason, you should avoid having props with that name
|
||||
|
||||
### Optional snippet props
|
||||
@@ -192,7 +301,7 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -202,13 +311,13 @@ You can declare snippet props as being optional. You can either use optional cha
|
||||
|
||||
```svelte
|
||||
<script>
|
||||
let { children } = $props();
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{@render children()}
|
||||
{:else}
|
||||
fallback content
|
||||
fallback content
|
||||
{/if}
|
||||
```
|
||||
|
||||
@@ -241,7 +350,7 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
let {
|
||||
data,
|
||||
children,
|
||||
row,
|
||||
row
|
||||
}: {
|
||||
data: T[];
|
||||
children: Snippet;
|
||||
@@ -252,9 +361,22 @@ We can tighten things up further by declaring a generic, so that `data` and `row
|
||||
|
||||
## Exporting snippets
|
||||
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets) (demo:
|
||||
Snippets declared at the top level of a `.svelte` file can be exported from a `<script module>` for use in other components, provided they don't reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets):
|
||||
|
||||
<!-- codeblock:start {"title":"Exported snippets","selected":"snippets.svelte"} -->
|
||||
|
||||
```svelte
|
||||
<!--- file: App.svelte --->
|
||||
<script>
|
||||
import { add } from './snippets.svelte';
|
||||
</script>
|
||||
|
||||
{@render add(1, 2)}
|
||||
|
||||
```
|
||||
|
||||
```svelte
|
||||
<!--- file: snippets.svelte --->
|
||||
<script module>
|
||||
export { add };
|
||||
</script>
|
||||
@@ -264,13 +386,15 @@ Snippets declared at the top level of a `.svelte` file can be exported from a `<
|
||||
{/snippet}
|
||||
```
|
||||
|
||||
<!-- codeblock:end -->
|
||||
|
||||
> [!NOTE]
|
||||
> This requires Svelte 5.5.0 or newer
|
||||
|
||||
## Programmatic snippets
|
||||
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](svelte#createRawSnippet) API. This is intended for advanced use cases.
|
||||
Snippets can be created programmatically with the [`createRawSnippet`](https://svelte.dev/docs/svelte/svelte#createRawSnippet/llms.txt) API. This is intended for advanced use cases.
|
||||
|
||||
## Snippets and slots
|
||||
|
||||
In Svelte 4, content can be passed to components using [slots](legacy-slots). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
In Svelte 4, content can be passed to components using [slots](https://svelte.dev/docs/svelte/legacy-slots/llms.txt). Snippets are more powerful and flexible, and so slots have been deprecated in Svelte 5.
|
||||
|
||||
@@ -17,7 +17,7 @@ If `start` returns a cleanup function, it will be called when the effect is dest
|
||||
If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects
|
||||
are active, and the returned teardown function will only be called when all effects are destroyed.
|
||||
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](/docs/svelte/svelte-reactivity#MediaQuery):
|
||||
It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery/llms.txt):
|
||||
|
||||
```js
|
||||
// @errors: 7031
|
||||
|
||||
Reference in New Issue
Block a user