Skip to main content

[create your own]

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 [path]

The project has a README.md and CONTRIBUTING.md to guide you along.

Project structure

Typically, an add-on looks like this:

import { import transformstransforms } 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';
// 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', // optional: one-liner shown in prompts shortDescription?: string | undefinedshortDescription: 'does X', // optional: link to docs/repo homepage?: string | undefinedhomepage: 'https://...', // Define options for user prompts (or passed as arguments)
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
(),
// 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) => void

On what official addons does this addon depend on?

dependsOn
}) => {
dependsOn: (name: keyof OfficialAddons) => void

On 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
: ({ isKit: anyisKit, cancel: (reason: string) => void

Cancel the addon at any time!

cancel
, sv: SvApi

Api 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
, directory: anydirectory }) => {
if (!isKit: anyisKit) return cancel: (reason: string) => void

Cancel the addon at any time!

cancel
('SvelteKit is required');
// Add "Hello [who]!" to the root page sv: SvApi

Api to interact with the workspace.

sv
.file: (path: string, edit: (content: string) => string) => void

Edit a file in the workspace. (will create it if it doesn't exist)

file
(
directory: anydirectory.kitRoutes + '/+page.svelte', import transformstransforms.svelte(({ ast: anyast, svelte: anysvelte }) => { svelte: anysvelte.addFragment(ast: anyast, `<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>`);
}) ); } });

sv is responsible for the file system - sv.file() accepts a path to the file and a callback function to modify it. @sveltejs/sv-utils is responsible for the content - transforms.svelte() provides you with the proper AST and utils to modify the file. See sv-utils for the full API.

Development

You can run your add-on locally using the file: protocol:

cd /path/to/test-project
npx sv add file:../path/to/my-addon

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.

It is not necessary to build your add-on during development.

Testing

The sv/testing module provides utilities for testing your add-on:

import { import setupTestsetupTest } from 'sv/testing';
import { const test: TestAPI

Defines a test case with a given name and test function. The test function can optionally be configured with test options.

@param
name - The name of the test or a function that will be used as a test name.
@param
optionsOrFn - Optional. The test options or the test function if no explicit name is provided.
@param
optionsOrTest - Optional. The test function or options, depending on the previous parameters.
@throws
Error If called inside another test function.
@example
// Define a simple test
test('should add two numbers', () => {
  expect(add(1, 2)).toBe(3);
});
@example
// Define a test with options
test('should subtract two numbers', { retry: 3 }, () => {
  expect(subtract(5, 2)).toBe(3);
});
test
, const expect: ExpectStaticexpect } from 'vitest';
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.

@param
name - The name of the test or a function that will be used as a test name.
@param
optionsOrFn - Optional. The test options or the test function if no explicit name is provided.
@param
optionsOrTest - Optional. The test function or options, depending on the previous parameters.
@throws
Error If called inside another test function.
@example
// Define a simple test
test('should add two numbers', () => {
  expect(add(1, 2)).toBe(3);
});
@example
// Define a test with options
test('should subtract two numbers', { retry: 3 }, () => {
  expect(subtract(5, 2)).toBe(3);
});
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) => void

Used 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.

@example

expect(items).toContain('apple'); expect(numbers).toContain(5);

toContain
('Hello World!');
});

Publishing

Bundling

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 no dependencies in package.json:

{
	"name": "@your-org/sv",
	"version": "1.0.0",
	"type": "module",
	// entrypoint during development
	"exports": {
		".": "./src/index.js"
	},
	"publishConfig": {
		"access": "public",
		// entrypoint on build
		"exports": {
			".": { "default": "./dist/index.js" }
		}
	},
	// cannot have dependencies
	"dependencies": {},
	"peerDependencies": {
		// minimum version required to run by this addon
		"sv": "^0.13.0"
	},
	// Add this keyword so users can discover your add-on
	"keywords": ["sv-add"]
}

Naming convention

Name your package @your-org/sv. Users install it by typing just the org:

# npm package: @your-org/sv
npx sv add @your-org

Unscoped packages are not supported yet

Export options

sv first tries to import your-package/sv, then falls back to the default export. This means you have two options:

  1. Default export (recommended for dedicated add-on packages):

    {
    	"exports": {
    		".": "./src/index.js"
    	}
    }
  2. ./sv export (for packages that also export other functionality):

    {
    	"exports": {
    		".": "./src/main.js",
    		"./sv": "./src/addon.js"
    	}
    }

Publish to npm

npm login
npm publish

prepublishOnly automatically runs the build before publishing.

Next steps

You can optionally display guidance in the console after your add-on runs:

import { 
const color: {
    addon: (str: string) => string;
    command: (str: string) => string;
    env: (str: string) => string;
    path: (str: string) => string;
    route: (str: string) => string;
    website: (str: string) => string;
    optional: (str: string) => string;
    dim: (str: string) => string;
    success: (str: string) => string;
    warning: (str: string) => string;
    error: (str: string) => string;
    hidden: (str: string) => string;
}
color
} from '@sveltejs/sv-utils';
export default defineAddon({ // ...
nextSteps: ({ options }: {
    options: any;
}) => string[]
nextSteps
: ({ options: anyoptions }) => [
`Run ${
const color: {
    addon: (str: string) => string;
    command: (str: string) => string;
    env: (str: string) => string;
    path: (str: string) => string;
    route: (str: string) => string;
    website: (str: string) => string;
    optional: (str: string) => string;
    dim: (str: string) => string;
    success: (str: string) => string;
    warning: (str: string) => string;
    error: (str: string) => string;
    hidden: (str: string) => string;
}
color
.command: (str: string) => stringcommand('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 user will get a compatibility warning if their sv version has a different major version than what was specified.

Examples

See the official add-on source code for some real world examples.

Architecture

The Svelte 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.

Edit this page on GitHub llms.txt

previous next