sv-utils = what to do to content, sv = where and when to do it.
Each transform wraps: parse -> callback({ast/data, utils}) -> generateCode().
The parser choice is baked into the transform type - you can't accidentally
parse a vite config as svelte because you never call a parser yourself.
Transforms are curried: call with the callback to get a (content: string) => string
function that plugs directly into sv.file().
import{transforms}from'@sveltejs/sv-utils';// use with sv.file() - curried form plugs in directlysv.file(files.viteConfig,transforms.script(({ast,js})=>{js.vite.addPlugin(ast,{code:'kitRoutes()'});}));// standalone usage / testingconstresult=transforms.script(({ast,js})=>{js.imports.addDefault(ast,{as:'foo',from:'foo'});})(fileContent);
}from'sv';exportdefaultdefineAddon(alias)defineAddon<"addon-name",{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>(config:Addon<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;},"addon-name",Record<string,unknown>>):Addon<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;},"addon-name",Record<string,unknown>>(+1overload)importdefineAddon
The entry point for your addon, It will hold every thing! (options, setup, run, nextSteps, ...)
For dynamic options added via addOption in setup, use the generic to get strong typing:
({id(property)id:"addon-name":'addon-name',shortDescription(property)shortDescription?:string|undefined:'a better description of what your addon does ;)',options(property)options:{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}:defineAddonOptions(alias)defineAddonOptions():OptionBuilder<{}>importdefineAddonOptions
Options for an addon.
Will be prompted to the user if there are not answered by args when calling the cli.
().add(method)add<"who",Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>>(key:"who",question:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>):OptionBuilder<Record<"who",Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>>>
This type is a bit complex, but in usage, it's quite simple!
The idea is to add() options one by one, with the key and the question.
.add('demo',{question:'Do you want to add a demo?',type:'boolean',// string, number, select, multiselectdefault:true,// condition: (o) => o.previousOption === 'ok',})
('who',{question(property)question:string:'To whom should the addon say hello?',type(property)type:"string":'string'// boolean | number | select | multiselect}).build(method)build():{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}(),setup(property)setup?:((workspace:Workspace&{dependsOn:(name:keyofOfficialAddons)=>void;unsupported:(reason:string)=>void;runsAfter:(name:keyofOfficialAddons)=>void;addOption:<Kextendsstring>(key:K,question:BaseQuestion<any>&Question<any>)=>void;})=>MaybePromise<void>)|undefined:({dependsOn(parameter)dependsOn:(name:keyofOfficialAddons)=>void
On what official addons does this addon depend on?
On what official addons does this addon depend on?
('vitest'); // dynamically add options (e.g. based on workspace state or fetched data) // addOption('key', { question: '...', type: 'boolean', default: true });},run(property)run:(workspace:Workspace&{options:OptionValues<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>&Record<string,unknown>;sv:SvApi;cancel:(reason:string)=>void;})=>MaybePromise<void>:({isKit(parameter)isKit:boolean,cancel(parameter)cancel:(reason:string)=>void
Cancel the addon at any time!
,sv(parameter)sv:SvApi,options(parameter)options:OptionValues<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>&Record<string,unknown>
Add-on options (includes dynamically added options from setup)
,file(parameter)file:{viteConfig:"vite.config.js"|"vite.config.ts";svelteConfig:"svelte.config.js"|"svelte.config.ts";typeConfig:"jsconfig.json"|"tsconfig.json"|undefined;stylesheet:`${string}/layout.css`|"src/app.css";package:"package.json";gitignore:".gitignore";prettierignore:".prettierignore";prettierrc:".prettierrc";eslintConfig:"eslint.config.js";vscodeSettings:".vscode/settings.json";vscodeExtensions:".vscode/extensions.json";getRelative:({from,to}:{from?:string;to:string;})=>string;findUp:(filename:string)=>string;},language(parameter)language:"ts"|"js",directory(parameter)directory:{src:string;lib:string;kitRoutes:string;}})=>{ // Add "Hello [who]!" to the root pagesv(parameter)sv:SvApi.file(property)file:(path:string,edit:(content:string)=>string|false)=>void
Edit a file in the workspace. (will create it if it doesn't exist)
Return false from the callback to abort - the original content is returned unchanged.
sv-utils = what to do to content, sv = where and when to do it.
Each transform wraps: parse -> callback({ast/data, utils}) -> generateCode().
The parser choice is baked into the transform type - you can't accidentally
parse a vite config as svelte because you never call a parser yourself.
Transforms are curried: call with the callback to get a (content: string) => string
function that plugs directly into sv.file().
import{transforms}from'@sveltejs/sv-utils';// use with sv.file() - curried form plugs in directlysv.file(files.viteConfig,transforms.script(({ast,js})=>{js.vite.addPlugin(ast,{code:'kitRoutes()'});}));// standalone usage / testingconstresult=transforms.script(({ast,js})=>{js.imports.addDefault(ast,{as:'foo',from:'foo'});})(fileContent);
Return false from the callback to abort - the original content is returned unchanged.
(({ast(parameter)ast:AST.Root,svelte(parameter)svelte:typeofindex_d_exports$4})=>{svelte(parameter)svelte:typeofindex_d_exports$4.addFragment(alias)index_d_exports$4.addFragment(ast:AST.Root,content:string,options?:{mode?:"append"|"prepend";language?:"ts"|"js";}):voidexportindex_d_exports$4.addFragment(ast(parameter)ast:AST.Root,`<p>Hello${options(parameter)options:OptionValues<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>&Record<string,unknown>
Add-on options (includes dynamically added options from setup)
.who(property)who:unknown}!</p>`);}));},nextSteps(property)nextSteps?:((workspace:Workspace&{options:OptionValues<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>&Record<string,unknown>;})=>string[])|undefined:({options(parameter)options:OptionValues<{who:Question<Record<"who",{readonlyquestion:"To whom should the addon say hello?";readonlytype:"string";}>>;}>&Record<string,unknown>})=>['enjoy the add-on!']});
The CLI is split into two packages with a clear boundary:
sv = where and when to do it. It owns paths, workspace detection, dependency tracking, and file I/O. The engine orchestrates add-on execution.
@sveltejs/sv-utils = what to do to content. It provides parsers, language tooling, and typed transforms. Everything here is pure - no file system, no workspace awareness.
This separation means transforms are testable without a workspace and composable across add-ons.
Development
You can run your add-on locally using the file: protocol:
This allows you to iterate quickly without publishing to npm.
The file: protocol also works for custom or private add-ons that you don’t intend to publish - for example, to standardize project setup across your team or organization.
The demo-add script automatically builds your add-on before running it.
Testing
The sv/testing module provides utilities for testing your add-on. createSetupTest is a factory that takes your vitest imports and returns a setupTest function. It creates real SvelteKit projects from templates, runs your add-on, and gives you access to the resulting files.
Returns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
searchString
search string
position
If position is undefined, 0 is assumed, so as to search all of the String.
Synchronously reads the entire contents of a file.
path
A path to a file. If a URL is provided, it must use the file: protocol.
If a file descriptor is provided, the underlying file will not be closed automatically.
options
Either the encoding for the result, or an object that contains the encoding and an optional flag.
If a flag is not provided, it defaults to 'r'.
The right-most parameter is considered {to}. Other parameters are considered an array of {from}.
Starting from leftmost {from} parameter, resolves {to} to an absolute path.
If {to} isn't already absolute, {from} arguments are prepended in right to left order,
until an absolute path is found. If after using all {from} paths still no absolute path is found,
the current working directory is used as well. The resulting path is normalized,
and trailing slashes are removed unless the path gets resolved to the root directory.
paths
A sequence of paths or path segments.
{TypeError} if any of the arguments is not a string.
The URL interface is used to parse, construct, normalize, and encode URLs. It works by providing properties which allow you to easily read and modify the components of a URL.
Community add-ons are bundled with tsdown into a single file. Everything is bundled except sv. (It is a peer dependency provided at runtime.)
package.json
Your add-on must have sv as a peer dependency and nodependencies in package.json:
{"name":"@my-org/sv","version":"1.0.0","type":"module",// bundled entry point (tsdown outputs .mjs for ESM)"exports":{".":{"default":"./dist/index.mjs"}},"publishConfig":{"access":"public"},// cannot have dependencies"dependencies":{},"peerDependencies":{// minimum version required to run by this add-on"sv":"^0.13.0"},// Add the "sv-add" keyword so users can discover your add-on with https://www.npmx.dev/search?q=keyword:sv-add"keywords":["sv-add","svelte","sveltekit"]}
prepublishOnly automatically runs the build before publishing.
Next steps
You can optionally display guidance in the console after your add-on runs:
import{color(alias)constcolor:{addon:(str:ColorInput)=>string;command:(str:ColorInput)=>string;env:(str:ColorInput)=>string;path:(str:ColorInput)=>string;route:(str:ColorInput)=>string;website:(str:ColorInput)=>string;optional:(str:ColorInput)=>string;dim:(str:ColorInput)=>string;success:(str:ColorInput)=>string;warning:(str:ColorInput)=>string;error:(str:ColorInput)=>string;hidden:(str:ColorInput)=>string;}importcolor}from'@sveltejs/sv-utils';exportdefaultdefineAddonany({ // ...nextSteps(property)nextSteps:({options}:{options:any;})=>string[]:({options(parameter)options:any})=>[`Run${color(alias)constcolor:{addon:(str:ColorInput)=>string;command:(str:ColorInput)=>string;env:(str:ColorInput)=>string;path:(str:ColorInput)=>string;route:(str:ColorInput)=>string;website:(str:ColorInput)=>string;optional:(str:ColorInput)=>string;dim:(str:ColorInput)=>string;success:(str:ColorInput)=>string;warning:(str:ColorInput)=>string;error:(str:ColorInput)=>string;hidden:(str:ColorInput)=>string;}importcolor.command(property)command:(str:ColorInput)=>string('npm run dev')}to start developing`,`Check out the docs at https://...`]});
Version compatibility
Your add-on should specify a minimum sv version in peerDependencies. Your users will get a compatibility warning if their sv version has a different major version than what was specified.