CLI • Add-ons

[create your own]

On this page

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 the Svelte CLI.

Quick start

The easiest way to create an add-on is by using the addon template:

npx sv create --template addon [path]

The newly created project will have a README.md and CONTRIBUTING.md to guide you along.

Project structure

Typically, an add-on looks like this:

import { transforms } from '@sveltejs/sv-utils';
import { defineAddon, defineAddonOptions } from 'sv';

export default defineAddon({
	id: 'addon-name',

	shortDescription: 'a better description of what your addon does ;)',

	options: defineAddonOptions()
		.add('who', {
			question: 'To whom should the addon say hello?',
			type: 'string' // boolean | number | select | multiselect
		})
		.build(),

	setup: ({ dependsOn, isKit, unsupported, addOption }) => {
		if (!isKit) unsupported('Requires SvelteKit');
		dependsOn('vitest');

		// dynamically add options (e.g. based on workspace state or fetched data)
		// addOption('key', { question: '...', type: 'boolean', default: true });
	},

	run: ({ isKit, cancel, sv, options, file, language, directory }) => {
		// Add "Hello [who]!" to the root page
		sv.file(
			directory.kitRoutes + '/+page.svelte',
			transforms.svelte(({ ast, svelte }) => {
				svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
			})
		);
	},

	nextSteps: ({ options }) => ['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:

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.

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.

import { expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { createSetupTest } from 'sv/testing';
import * as vitest from 'vitest';
import addon from './index.js';

const { test, testCases } = createSetupTest(vitest)(
	{ addon },
	{
		kinds: [
			{
				type: 'default',
				options: {
					'your-addon-name': { who: 'World' }
				}
			}
		],
		filter: (testCase) => testCase.variant.includes('kit'),
		browser: false
	}
);

test.concurrent.for(testCases)('my-addon $kind.type $variant', async (testCase, ctx) => {
	const cwd = ctx.cwd(testCase);

	const page = fs.readFileSync(path.resolve(cwd, 'src/routes/+page.svelte'), 'utf8');
	expect(page).toContain('Hello World!');
});

Your vitest.config.js must include the global setup from sv/testing:

import { function defineConfig(config: UserConfig): UserConfig (+4 overloads)function defineConfig(config: UserConfig): UserConfig (+4 overloads)defineConfig } from 'vitest/config';

export default function defineConfig(config: UserConfig): UserConfig (+4 overloads)function defineConfig(config: UserConfig): UserConfig (+4 overloads)defineConfig({
	UserConfig.test?: InlineConfig | undefined

Options for Vitest

UserConfig.test?: InlineConfig | undefined

Options for Vitest

test
: {
InlineConfig.include?: string[] | undefined

A list of glob patterns that match your test files.

@default['**\/*.{test,spec}.?(c|m)[jt]s?(x)']@see{@link https://vitest.dev/config/include}
InlineConfig.include?: string[] | undefined

A list of glob patterns that match your test files.

@default['**\/*.{test,spec}.?(c|m)[jt]s?(x)']@see{@link https://vitest.dev/config/include}
include
: ['tests/**/*.test.{js,ts}'],
InlineConfig.globalSetup?: string | string[] | undefined

Path to global setup files

InlineConfig.globalSetup?: string | string[] | undefined

Path to global setup files

globalSetup
: ['tests/setup/global.js']
} });

And the global test setup script tests/setup/global.js:

import { fileURLToPath } from 'node:url';
import { setupGlobal } from 'sv/testing';

const TEST_DIR = fileURLToPath(new URL('../../.test-output/', import.meta.url));

export default setupGlobal({ TEST_DIR });

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": "@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"]
}

Package names

Packages must be published under an npm org:

# ✓ GOOD
npx sv add @my-org/sv
npx sv add @my-org/core

# ✗ BAD
npx sv add my-lib

If your package is published with the sv scope, it can be omitted. The following all resolves to the same package:

npx sv add @my-org
npx sv add @my-org/sv
npx sv add @my-org/sv@latest

For a specific version, append @<version>:

npx sv add @my-org/sv@1.2.3

Entry points

The CLI looks for ./sv first. If that is not found, it defaults to the . entry point. This means you have two options:

  1. Default export (for a dedicated add-on package):
{
	"name": "@my-org/sveltekit-addon",
	"exports": {
		".": "./dist/addon.mjs"
	}
}
  1. ./sv export (for packages that also export other functionality):
{
	"name": "@my-org/sveltekit-addon",
	"exports": {
		".": "./dist/main.mjs",
		"./sv": "./dist/addon.mjs"
	}
}

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 { color } from '@sveltejs/sv-utils';

export default defineAddon({
	// ...

	nextSteps: ({ options }) => [
		`Run ${color.command('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.

Examples

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