add-on
Community add-ons are currently experimental. The API may change. Don't use them in production yet!
This guide covers how to create, test, and publish community add-ons for sv.
Quick start
The easiest way to create an add-on is using the addon template:
npx sv create --template addon my-addon
cd my-addonAdd-on structure
Typically, an add-on looks like this:
hover keywords in the code to have some more context
import { const parse: {
css: (source: string) => {
ast: Omit<_CSS.StyleSheetBase, "attributes" | "content">;
} & ParseBase;
html: (source: string) => {
ast: AST.Fragment;
} & ParseBase;
json: (source: string) => {
data: any;
} & ParseBase;
script: (source: string) => {
ast: Program;
comments: Comments;
} & ParseBase;
svelte: (source: string) => {
ast: AST.Root;
} & ParseBase;
toml: (source: string) => {
data: TomlTable;
} & ParseBase;
yaml: (source: string) => {
data: ReturnType<(content: string) => ReturnType<any>>;
} & ParseBase;
}
Will help you parse code into an ast from all supported languages.
Then manipulate the ast as you want,
and finally generateCode() to write it back to the file.
import { parse } from '@sveltejs/sv-utils';
const { ast, generateCode } = parse.css('body { color: red; }');
const { ast, generateCode } = parse.html('<div>Hello, world!</div>');
const { ast, generateCode } = parse.json('{ "name": "John", "age": 30 }');
const { ast, generateCode } = parse.script('function add(a, b) { return a + b; }');
const { ast, generateCode } = parse.svelte('<div>Hello, world!</div>');
const { ast, generateCode } = parse.toml('name = "John"');
const { ast, generateCode } = parse.yaml('name: John');
parse, svelte } from '@sveltejs/sv-utils';
import { function defineAddon<const Id$1 extends string, Args extends OptionDefinition>(config: Addon<Args, Id$1>): Addon<Args, Id$1>The entry point for your addon, It will hold every thing! (options, setup, run, nextSteps, ...)
defineAddon, function defineAddonOptions(): OptionBuilder<{}>Options for an addon.
Will be prompted to the user if there are not answered by args when calling the cli.
const options = defineAddonOptions()
.add('demo', {
question: `demo? ${color.optional('(a cool one!)')}`
type: string | boolean | number | select | multiselect,
default: true,
})
.build();
To define by args, you can do
npx sv add <addon>=<option1>:<value1>+<option2>:<value2>
defineAddonOptions } from 'sv';
// Define options that will be prompted to the user (or passed as arguments)
const const options: {
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}
options = function defineAddonOptions(): OptionBuilder<{}>Options for an addon.
Will be prompted to the user if there are not answered by args when calling the cli.
const options = defineAddonOptions()
.add('demo', {
question: `demo? ${color.optional('(a cool one!)')}`
type: string | boolean | number | select | multiselect,
default: true,
})
.build();
To define by args, you can do
npx sv add <addon>=<option1>:<value1>+<option2>:<value2>
defineAddonOptions()
.add<"who", Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>>(key: "who", question: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>): OptionBuilder<Record<"who", Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "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, multiselect
default: true,
// condition: (o) => o.previousOption === 'ok',
})
add('who', {
question: stringquestion: 'To whom should the addon say hello?',
type: "string"type: 'string' // boolean | number | select | multiselect
})
.function build(): {
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}
Finalize all options of your add-on.
build();
// your add-on definition, the entry point
export default defineAddon<"your-addon-name", {
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}>(config: Addon<{
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}, "your-addon-name">): Addon<{
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}, "your-addon-name">
The entry point for your addon, It will hold every thing! (options, setup, run, nextSteps, ...)
defineAddon({
id: "your-addon-name"id: 'your-addon-name',
options: {
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}
options,
// preparing step, check requirements and dependencies
setup?: ((workspace: Workspace & {
dependsOn: (name: keyof OfficialAddons) => void;
unsupported: (reason: string) => void;
runsAfter: (name: keyof OfficialAddons) => void;
}) => MaybePromise<...>) | undefined
Setup the addon. Will be called before the addon is run.
setup: ({ dependsOn: (name: keyof OfficialAddons) => voidOn what official addons does this addon depend on?
dependsOn }) => {
dependsOn: (name: keyof OfficialAddons) => voidOn what official addons does this addon depend on?
dependsOn('tailwindcss');
},
// actual execution of the addon
run: (workspace: Workspace & {
options: OptionValues<{
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}>;
sv: SvApi;
cancel: (reason: string) => void;
}) => MaybePromise<void>
Run the addon. The actual execution of the addon... Add files, edit files, etc.
run: ({ kit: {
libDirectory: string;
routesDirectory: string;
} | undefined
If we are in a kit project, this object will contain the lib and routes directories
kit, cancel: (reason: string) => voidCancel the addon at any time!
cancel, sv: SvApiApi to interact with the workspace.
sv, options: OptionValues<{
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}>
Add-on options
options }) => {
if (!kit: {
libDirectory: string;
routesDirectory: string;
} | undefined
If we are in a kit project, this object will contain the lib and routes directories
kit) return cancel: (reason: string) => voidCancel the addon at any time!
cancel('SvelteKit is required');
// Add "Hello [who]!" to the root page
sv: SvApiApi to interact with the workspace.
sv.file: (path: string, edit: (content: string) => string) => voidEdit a file in the workspace. (will create it if it doesn't exist)
file(kit: {
libDirectory: string;
routesDirectory: string;
}
If we are in a kit project, this object will contain the lib and routes directories
kit.routesDirectory: stringroutesDirectory + '/+page.svelte', (content: stringcontent) => {
const { const ast: AST.Rootast, const generateCode: () => stringGenerate the code after manipulating the ast.
import { svelte } from 'sv/core';
const { ast, generateCode } = parse.svelte(content);
svelte.addFragment(ast, '<p>Hello World</p>');
const code = generateCode();
generateCode } = const parse: {
css: (source: string) => {
ast: Omit<_CSS.StyleSheetBase, "attributes" | "content">;
} & ParseBase;
html: (source: string) => {
ast: AST.Fragment;
} & ParseBase;
json: (source: string) => {
data: any;
} & ParseBase;
script: (source: string) => {
ast: Program;
comments: Comments;
} & ParseBase;
svelte: (source: string) => {
ast: AST.Root;
} & ParseBase;
toml: (source: string) => {
data: TomlTable;
} & ParseBase;
yaml: (source: string) => {
data: ReturnType<(content: string) => ReturnType<any>>;
} & ParseBase;
}
Will help you parse code into an ast from all supported languages.
Then manipulate the ast as you want,
and finally generateCode() to write it back to the file.
import { parse } from '@sveltejs/sv-utils';
const { ast, generateCode } = parse.css('body { color: red; }');
const { ast, generateCode } = parse.html('<div>Hello, world!</div>');
const { ast, generateCode } = parse.json('{ "name": "John", "age": 30 }');
const { ast, generateCode } = parse.script('function add(a, b) { return a + b; }');
const { ast, generateCode } = parse.svelte('<div>Hello, world!</div>');
const { ast, generateCode } = parse.toml('name = "John"');
const { ast, generateCode } = parse.yaml('name: John');
parse.svelte: (source: string) => {
ast: AST.Root;
} & ParseBase
svelte(content: stringcontent);
svelte.index_d_exports$3.addFragment(ast: AST.Root, content: string, options?: {
mode?: "append" | "prepend";
}): void
export index_d_exports$3.addFragment
addFragment(const ast: AST.Rootast, `<p>Hello ${options: OptionValues<{
who: Question<Record<"who", {
readonly question: "To whom should the addon say hello?";
readonly type: "string";
}>>;
}>
Add-on options
options.who: "ERROR: The value for this type is invalid. Ensure that the `default` value exists in `options`."who}!</p>`);
return const generateCode: () => stringGenerate the code after manipulating the ast.
import { svelte } from 'sv/core';
const { ast, generateCode } = parse.svelte(content);
svelte.addFragment(ast, '<p>Hello World</p>');
const code = generateCode();
generateCode();
});
}
});Development with file: protocol
While developing your add-on, you can test it locally using the file: protocol:
# In your test project
npx sv add file:../path/to/my-addonThis allows you to iterate quickly without publishing to npm.
Testing with sv/testing
The sv/testing module provides utilities for testing your add-on:
import { const test: TestAPIDefines a test case with a given name and test function. The test function can optionally be configured with test options.
test, const expect: ExpectStaticexpect } from 'vitest';
import { import setupTestsetupTest } from 'sv/testing';
import import addonaddon from './index.js';
test<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)Defines a test case with a given name and test function. The test function can optionally be configured with test options.
test('adds hello message', async () => {
const { const content: anycontent } = await import setupTestsetupTest({
addon: anyaddon,
options: {
who: string;
}
options: { who: stringwho: 'World' },
files: {
'src/routes/+page.svelte': string;
}
files: {
'src/routes/+page.svelte': '<h1>Welcome</h1>'
}
});
expect<any>(actual: any, message?: string): Assertion<any> (+1 overload)expect(const content: anycontent('src/routes/+page.svelte')).JestAssertion<any>.toContain: <string>(item: string) => voidUsed when you want to check that an item is in a list.
For testing the items in the list, this uses ===, a strict equality check.
toContain('Hello World!');
});Publishing to npm
Package structure
Your add-on must have sv as a dependency in package.json:
{
"name": "@your-org/sv",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js"
},
"dependencies": {
"sv": "^0.11.0"
},
"keywords": ["sv-add"]
}Add the
sv-addkeyword so users can discover your add-on on npm.
Export options
Your package can export the add-on in two ways:
Default export (recommended for dedicated add-on packages):
{ "exports": { ".": "./dist/index.js" } }/svexport (for packages that have other functionality):{ "exports": { ".": "./dist/main.js", "./sv": "./dist/addon.js" } }
Naming conventions
- Scoped packages: Use
@your-org/svas the package name. Users can then install with justnpx sv add @your-org. - Regular packages: Any name works. Users install with
npx sv add your-package-name.
Version compatibility
Your add-on should specify the minimum sv version it requires in package.json. If a user's sv version has a different major version than what your add-on was built for, they will see a compatibility warning.