[create your own]
Les add-ons communautaires sont actuellement expérimentaux. Leur API peut changer. Ne les utilisez pas encore en production !
Ce guide vous explique comment créer, tester, et publier des add-ons communautaires pour sv.
Démarrage rapide
La façon la plus simple de créer un add-on est d'utiliser le template addon :
npx sv create --template addon [path]Le projet nouvellement créé aura des fichiers README.md et CONTRIBUTING.md pour vous
accompagner.
Structure du projet
Un add-on ressemble généralement à ceci :
import { const transforms: {
script(cb: (file: {
ast: Program;
comments: Comments;
content: string;
js: typeof index_d_exports$3;
}) => void | false, options?: TransformOptions): (content: string) => string;
svelte(cb: (file: {
ast: AST.Root;
content: string;
svelte: typeof index_d_exports$4;
js: typeof index_d_exports$3;
}) => void | false, options?: TransformOptions): (content: string) => string;
... 6 more ...;
text(cb: (file: {
content: string;
text: typeof text_d_exports;
}) => string | false): TransformFn;
}
File transform primitives that know their format.
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().
transforms } from '@sveltejs/sv-utils';
import { function defineAddon<const Id extends string, Args extends OptionDefinition>(config: Addon<Args, Id>): Addon<Args, Id>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';
export default defineAddon<"addon-name", {
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}>(config: Addon<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}, "addon-name">): Addon<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}, "addon-name">
The entry point for your addon, It will hold every thing! (options, setup, run, nextSteps, ...)
defineAddon({
id: "addon-name"id: 'addon-name',
shortDescription?: string | undefinedshortDescription: 'une meilleure description de ce que fait votre add-on ;)',
options: {
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
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: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>>(key: "who", question: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>): OptionBuilder<Record<"who", Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
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: 'À qui cet add-on doit-il dire bonjour ?',
type: "string"type: 'string' // boolean | number | select | multiselect
})
.function build(): {
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}
build(),
setup?: ((workspace: Workspace & {
dependsOn: (name: keyof OfficialAddons) => void;
unsupported: (reason: string) => void;
runsAfter: (name: keyof OfficialAddons) => void;
}) => MaybePromise<...>) | undefined
setup: ({ dependsOn: (name: keyof OfficialAddons) => voidOn what official addons does this addon depend on?
dependsOn, isKit: booleanisKit, unsupported: (reason: string) => voidWhy is this addon not supported?
unsupported }) => {
if (!isKit: booleanisKit) unsupported: (reason: string) => voidWhy is this addon not supported?
unsupported('Nécessite SvelteKit');
dependsOn: (name: keyof OfficialAddons) => voidOn what official addons does this addon depend on?
dependsOn('vitest');
},
run: (workspace: Workspace & {
options: OptionValues<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}>;
sv: SvApi;
cancel: (reason: string) => void;
}) => MaybePromise<void>
run: ({ isKit: booleanisKit, cancel: (reason: string) => voidCancel the addon at any time!
cancel, sv: SvApisv, options: OptionValues<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}>
Add-on options
options, 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;
}
file, language: "ts" | "js"language, directory: {
src: string;
lib: string;
kitRoutes: string;
}
directory }) => {
// Ajoute "Bonjour [who] !" sur la page racine
sv: SvApisv.file: (path: string, edit: (content: string) => string | false) => voidEdit 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.
file(
directory: {
src: string;
lib: string;
kitRoutes: string;
}
directory.kitRoutes: stringkitRoutes + '/+page.svelte',
const transforms: {
script(cb: (file: {
ast: Program;
comments: Comments;
content: string;
js: typeof index_d_exports$3;
}) => void | false, options?: TransformOptions): (content: string) => string;
svelte(cb: (file: {
ast: AST.Root;
content: string;
svelte: typeof index_d_exports$4;
js: typeof index_d_exports$3;
}) => void | false, options?: TransformOptions): (content: string) => string;
... 6 more ...;
text(cb: (file: {
content: string;
text: typeof text_d_exports;
}) => string | false): TransformFn;
}
File transform primitives that know their format.
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().
transforms.function svelte(cb: (file: {
ast: AST.Root;
content: string;
svelte: typeof index_d_exports$4;
js: typeof index_d_exports$3;
}) => void | false, options?: TransformOptions): (content: string) => string
Transform a Svelte component file.
Return false from the callback to abort - the original content is returned unchanged.
svelte(({ ast: AST.Rootast, svelte: typeof index_d_exports$4svelte }) => {
svelte: typeof index_d_exports$4svelte.index_d_exports$4.addFragment(ast: AST.Root, content: string, options?: {
mode?: "append" | "prepend";
}): void
export index_d_exports$4.addFragment
addFragment(ast: AST.Rootast, `<p>Bonjour ${options: OptionValues<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
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>`);
})
);
},
nextSteps?: ((workspace: Workspace & {
options: OptionValues<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}>;
}) => string[]) | undefined
nextSteps: ({ options: OptionValues<{
who: Question<Record<"who", {
readonly question: "À qui cet add-on doit-il dire bonjour ?";
readonly type: "string";
}>>;
}>
options }) => ['profitez de cet add-on!']
});Le CLI de Svelte est divisé en deux paquets avec un frontière claire :
sv= où et quand. Ce paquet gère les paths, la détection de workspace, le suivi de dépendances, et l'I/O de fichier. Le moteur orchestre l'exécution de l'add-on.@sveltejs/sv-utils= quoi. Ce paquet fournit des parsers, de l'outillage de language, et des transformations typées. Tout ici est pur — pas de système de fichiers, pas de conscience du workspace.
Cette séparation implique que les transformations sont testables sans aucun workspace, et composables entre add-ons.
Développement
Vous pouvez exécuter votre add-on localement avec le protocole file: :
cd /path/to/test-project
npx sv add file:../path/to/my-addonCeci vous permet d'itérer rapidement sans publier sur npm.
Le protocole file: fonctionne également pour les add-ons privés ou personnalisés que vous n'avez
pas l'intention de publier — par exemple, pour standardiser la mise en place d'un projet au sein de
votre équipe ou de votre organisation.
Le scipt
demo-addcompile automatiquement votre add-on avant de l'exécuter.
Tests
Le module sv/testing fournit des utilitaires pour tester votre add-on. createSetupTest est une
usine qui prend vos imports vitest et renvoie une fonction setupTest. Elle crée des vrais projets
SvelteKit à partir de templates, exécute votre add-on, et vous donne accès aux fichiers ainsi
générés.
import { import expectexpect } from '@playwright/test';
import module "node:fs"fs from 'node:fs';
import const path: path.PlatformPathpath from 'node:path';
import { function createSetupTest(vitest: VitestContext, playwright?: PlaywrightContext): <Addons extends AddonMap>(addons: Addons, options?: SetupTestOptions<Addons>) => {
test: vitest.TestAPI<Fixtures>;
testCases: Array<AddonTestCase<AddonMap>>;
prepareServer: typeof prepareServer;
}
createSetupTest } from 'sv/testing';
import * as import vitestvitest from 'vitest';
import import addonaddon from './index.js';
const { const test: vitest.TestAPI<Fixtures>test, const testCases: AddonTestCase<AddonMap>[]testCases } = function createSetupTest(vitest: VitestContext, playwright?: PlaywrightContext): <Addons extends AddonMap>(addons: Addons, options?: SetupTestOptions<Addons>) => {
test: vitest.TestAPI<Fixtures>;
testCases: Array<AddonTestCase<AddonMap>>;
prepareServer: typeof prepareServer;
}
createSetupTest(import vitestvitest)(
{ addon: anyaddon },
{
kinds: {
type: string;
options: OptionMap<{
addon: any;
}>;
}[]
kinds: [
{
type: stringtype: 'default',
options: OptionMap<{
addon: any;
}>
options: {
'your-addon-name': { who: stringwho: 'World' }
}
}
],
filter?: ((addonTestCase: AddonTestCase<{
addon: any;
}>) => boolean) | undefined
filter: (testCase: AddonTestCase<{
addon: any;
}>
testCase) => testCase: AddonTestCase<{
addon: any;
}>
testCase.variant: ProjectVariantvariant.String.includes(searchString: string, position?: number): booleanReturns 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.
includes('kit'),
browser?: boolean | undefinedbrowser: false
}
);
const test: vitest.TestAPI<Fixtures>test.concurrent: ChainableFunction<"concurrent" | "sequential" | "only" | "skip" | "todo" | "fails", TestCollectorCallable<Fixtures>, {
each: TestEachFunction;
for: TestForFunction<Fixtures>;
}>
concurrent.for: TestForFunction
<AddonTestCase<AddonMap>>(cases: readonly AddonTestCase<AddonMap>[]) => TestForFunctionReturn<AddonTestCase<AddonMap>, vitest.TestContext & Fixtures> (+1 overload)
for(const testCases: AddonTestCase<AddonMap>[]testCases)('my-addon $kind.type $variant', async (testCase: AddonTestCase<AddonMap>testCase, ctx: vitest.TestContext & Fixturesctx) => {
const const cwd: stringcwd = ctx: vitest.TestContext & Fixturesctx.function cwd(addonTestCase: AddonTestCase<any>): stringcwd(testCase: AddonTestCase<AddonMap>testCase);
const const page: stringpage = module "node:fs"fs.function readFileSync(path: fs.PathOrFileDescriptor, options: {
encoding: BufferEncoding;
flag?: string | undefined;
} | BufferEncoding): string (+2 overloads)
Synchronously reads the entire contents of a file.
readFileSync(const path: path.PlatformPathpath.path.PlatformPath.resolve(...paths: string[]): stringThe 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.
resolve(const cwd: stringcwd, 'src/routes/+page.svelte'), 'utf8');
import expectexpect(const page: stringpage).toContain('Bonjour tout le monde !');
});Votre fichier vitest.config.js doit contenir la configuration globale de sv/testing :
import { function defineConfig(config: UserConfig): UserConfig (+4 overloads)defineConfig } from 'vitest/config';
export default function defineConfig(config: UserConfig): UserConfig (+4 overloads)defineConfig({
UserConfig.test?: InlineConfig | undefinedOptions for Vitest
test: {
InlineConfig.include?: string[] | undefinedA list of glob patterns that match your test files.
include: ['tests/**/*.test.{js,ts}'],
InlineConfig.globalSetup?: string | string[] | undefinedPath to global setup files
globalSetup: ['tests/setup/global.js']
}
});Le script tests/setup/global.js étant le suivant :
import { function fileURLToPath(url: string | URL, options?: FileUrlToPathOptions): stringThis function ensures the correct decodings of percent-encoded characters as
well as ensuring a cross-platform valid absolute path string.
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
new URL('file:///C:/path/').pathname; // Incorrect: /C:/path/
fileURLToPath('file:///C:/path/'); // Correct: C:\path\ (Windows)
new URL('file://nas/foo.txt').pathname; // Incorrect: /foo.txt
fileURLToPath('file://nas/foo.txt'); // Correct: \\nas\foo.txt (Windows)
new URL('file:///你好.txt').pathname; // Incorrect: /%E4%BD%A0%E5%A5%BD.txt
fileURLToPath('file:///你好.txt'); // Correct: /你好.txt (POSIX)
new URL('file:///hello world').pathname; // Incorrect: /hello%20world
fileURLToPath('file:///hello world'); // Correct: /hello world (POSIX)
fileURLToPath } from 'node:url';
import { function setupGlobal({ TEST_DIR, pre, post }: {
TEST_DIR: string;
pre?: () => Promise<void>;
post?: () => Promise<void>;
}): ({ provide }: TestProject) => Promise<() => Promise<void>>
setupGlobal } from 'sv/testing';
const const TEST_DIR: stringTEST_DIR = function fileURLToPath(url: string | URL, options?: FileUrlToPathOptions): stringThis function ensures the correct decodings of percent-encoded characters as
well as ensuring a cross-platform valid absolute path string.
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
new URL('file:///C:/path/').pathname; // Incorrect: /C:/path/
fileURLToPath('file:///C:/path/'); // Correct: C:\path\ (Windows)
new URL('file://nas/foo.txt').pathname; // Incorrect: /foo.txt
fileURLToPath('file://nas/foo.txt'); // Correct: \\nas\foo.txt (Windows)
new URL('file:///你好.txt').pathname; // Incorrect: /%E4%BD%A0%E5%A5%BD.txt
fileURLToPath('file:///你好.txt'); // Correct: /你好.txt (POSIX)
new URL('file:///hello world').pathname; // Incorrect: /hello%20world
fileURLToPath('file:///hello world'); // Correct: /hello world (POSIX)
fileURLToPath(new var URL: new (url: string | URL, base?: string | URL) => URLThe URL interface is used to parse, construct, normalize, and encode URL.
URL class is a global reference for import { URL } from 'node:url'
https://nodejs.org/api/url.html#the-whatwg-url-api
URL('../../.test-output/', import.meta.ImportMeta.url: stringThe absolute file: URL of the module.
url));
export default function setupGlobal({ TEST_DIR, pre, post }: {
TEST_DIR: string;
pre?: () => Promise<void>;
post?: () => Promise<void>;
}): ({ provide }: TestProject) => Promise<() => Promise<void>>
setupGlobal({ type TEST_DIR: stringTEST_DIR });Publier
Bundling
Les add-on communautaires sont compilés avec tsdown en un seul fichier. Tout est compilé, à l'exception de sv (qui est une dépendance-paire, fournie lors de l'exécution.)
package.json
Votre add-on doit avoir sv comme dépendance-paire et aucune dépendance dans votre
package.json :
{
"name": "@my-org/sv",
"version": "1.0.0",
"type": "module",
// point d'entrée compilé (tsdown génère des .mjs pour les ESM)
"exports": {
".": { "default": "./dist/index.mjs" }
},
"publishConfig": {
"access": "public"
},
// ne peut pas avoir de dépendances
"dependencies": {},
"peerDependencies": {
// la version minimum requise pour exécuter cet add-on
"sv": "^0.13.0"
},
// Ajoutez le mot-clé "sv-add" afin que les gens puissent trouver votre add-on
"keywords": ["sv-add", "svelte", "sveltekit"]
}Conventions de nommage
Noms de paquets
Si vous appelez vos paquet @my-org/sv, les gens pourront l'installer juste en tapant le nom de
votre organisation :
npx sv add @my-orgIl est également possible de publier avec un nom comme @my-org/core, les gens devront alors taper
le nom complet du paquet :
npx sv add @my-org/coreIl est également possible de demander une version précise :
npx sv add @my-org/sv@1.2.3Lorsqu'aucune version n'est demandée, latest est utilisé.
Les paquets non scopés ne sont pas encore supportés
Options d'export
sv essaye d'abord d'importer your-package/sv, puis utilise l'export par défaut s'il n'y arrive
pas. Ceci signifie que vous avez deux options :
L'export par défaut (pour les paquets d'add-on dédiés) :
{ "exports": { ".": "./src/index.js" } }L'export
./sv(pour les paquets qui exportent également d'autres fonctionnalités):{ "exports": { ".": "./src/main.js", "./sv": "./src/addon.js" } }
Publier sur npm
npm login
npm publish
prepublishOnlyexécute automatiquement le build avant de publier.
Prochaines étapes
Vous pouvez de manière optionnelle afficher des indications à afficher lors de l'exécution de votre add-on :
import { const color: {
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;
}
color } from '@sveltejs/sv-utils';
export default defineAddon({
// ...
nextSteps: ({ options }: {
options: any;
}) => string[]
nextSteps: ({ options: anyoptions }) => [
`Lancez ${const color: {
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;
}
color.command: (str: ColorInput) => stringcommand('npm run dev')} pour commencer le développement`,
`Retrouvez la documentation sur https://...`
]
});Compatibilité de version
Votre add-on doit préciser la version minimale de sv requises dans vos peerDependencies. Si la
version de sv d'un utilisateur ou d'un utilisatrice a une majeure différente de celle pour
laquelle votre add-on a été conçu, ils et elles verront un avertissement de compatibilité.
Exemples
Consultez le code source des add-ons officiels pour trouver des exemples concrets.