Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/svelte.dev/content/docs/cli/10-introduction/20-faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,18 @@ Here is a list of issues with possible solutions that users have encountered in
- [`npx sv` create does nothing](https://github.com/sveltejs/cli/issues/472)
- [`sv` command name collides with `runit`](https://github.com/sveltejs/cli/issues/259)
- [`sv` in windows powershell conflicts with `Set-Variable`](https://github.com/sveltejs/cli/issues/317)

## Why are there two packages, `sv` and `@sveltejs/sv-utils`?

They have a clear boundary:

- [`sv`](sv) decides **where and when**: paths, workspace detection, dependencies and file I/O.
- [`@sveltejs/sv-utils`](sv-utils) decides **what** to do to content: parsers and pure transforms, with no file system or workspace awareness.

This keeps transforms testable without a workspace and composable across add-ons.

## Why bundle `@sveltejs/sv-utils` in my add-on?

Most breaking changes land in `@sveltejs/sv-utils`, not `sv`. An add-on that leaves it unbundled still loads, but runs against whatever copy `sv` ships, so a future `sv` release can break it. Bundling pins the version you tested against, so users can trust your add-on keeps working.

The [add-on template](community#Quick-start) already bundles it: keep it that way. See [bundling](community#Publishing-Bundling).
13 changes: 12 additions & 1 deletion apps/svelte.dev/content/docs/cli/30-add-ons/30-playwright.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,15 @@ npx sv add playwright
- scripts added in your `package.json`
- a Playwright config file
- an updated `.gitignore`
- a demo test
- an example e2e test
- an optional demo page the e2e test runs against

## Options

### demo

Whether to generate a demo page at `/demo/playwright` (listed in the `DemoLinks` post-it) for the example e2e test. Without it, the test targets `/`.

```sh
npx sv add playwright="demo:yes"
```
11 changes: 6 additions & 5 deletions apps/svelte.dev/content/docs/cli/30-add-ons/99-community.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ export default defineAddon({
})
.build(),

setup: ({ dependsOn, isKit, unsupported, addOption }) => {
setup: ({ dependsOn, runsAfter, isKit, unsupported, addOption }) => {
if (!isKit) unsupported('Requires SvelteKit');
dependsOn('vitest');
dependsOn('vitest'); // required add-on
runsAfter('prettier'); // ordering only

// dynamically add options (e.g. based on workspace state or fetched data)
// addOption('key', { question: '...', type: 'boolean', default: true });
Expand All @@ -62,7 +63,7 @@ The CLI is split into two packages with a clear boundary:
- [**`sv`**](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`**](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.
[Why are there two packages.](faq#Why-are-there-two-packages-sv-and-sveltejs-sv-utils)

## Development

Expand All @@ -85,9 +86,9 @@ The `file:` protocol also works for custom or private add-ons that you don't int
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.

```js
import { expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import { expect } from '@playwright/test';
import { createSetupTest } from 'sv/testing';
import * as vitest from 'vitest';
import addon from './index.js';
Expand Down Expand Up @@ -146,7 +147,7 @@ export default setupGlobal({ TEST_DIR });

Community add-ons are bundled with [tsdown](https://tsdown.dev/) into a single file. Everything is bundled except `sv`. (It is a peer dependency provided at runtime.)

`sv` ships its own copy of [`@sveltejs/sv-utils`](sv-utils), so an add-on that leaves it unbundled will still load. Nothing verifies the version: your add-on runs against whatever `sv` provides, and following its breaking changes is up to you. Bundle it to stay on a version you control.
[`@sveltejs/sv-utils`](sv-utils) is bundled as well to pin its version. See [why](faq#Why-bundle-sveltejs-sv-utils-in-my-add-on).

### `package.json`

Expand Down
87 changes: 37 additions & 50 deletions apps/svelte.dev/content/docs/cli/50-api/10-sv.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ export default defineAddon({
options: defineAddonOptions().build(),

// called before run - declare dependencies, environment requirements, and dynamic options
setup: ({ dependsOn, unsupported, addOption, isKit }) => {
setup: ({ dependsOn, runsAfter, unsupported, addOption, isKit }) => {
if (!isKit) unsupported('Requires SvelteKit');
dependsOn('eslint');
runsAfter('prettier');

// dynamically add options based on workspace state or fetched data
addOption('theme', {
Expand Down Expand Up @@ -70,33 +71,13 @@ export default defineAddon({
});
```

The `sv` object in `run` provides `file`, `files`, `removeFile`, `dependency`, `devDependency`, and `execute`. For file transforms (AST-based editing of scripts, Svelte components, CSS, JSON, etc.) and package manager helpers, see [`@sveltejs/sv-utils`](sv-utils).
Official add-ons included in `dependsOn` will be installed automatically. Community add-ons must be included as part of the same command.

### Typed dynamic options
`dependsOn` also implies `runsAfter`. Use `runsAfter` alone when you only care about ordering.

If your add-on adds options dynamically in `setup` (e.g. from a fetch), you can pass a type parameter to `defineAddon` to get strong typing for those options:
`dependsOn` and `runsAfter` both accept official and community add-on ids. (`'sveltekit-adapter'`, `'better-auth'`, ...)

```ts
import { defineAddon, defineAddonOptions } from 'sv';

// ---cut---
const addon = defineAddon<{ theme: string }>()({
id: 'my-addon',
options: defineAddonOptions().build(),
setup: ({ addOption }) => {
addOption('theme', {
question: 'Which theme?',
type: 'string',
default: 'dark'
});
},
run: ({ options }) => {
options.theme; // string
}
});
```

The type parameter maps value types (`boolean`, `string`, `number`) to question definitions. Without it, `defineAddon` stays strict and only allows statically defined options.
The `sv` object in `run` provides `file`, `files`, `removeFile`, `dependency`, `devDependency`, and `execute`. For file transforms (AST-based editing of scripts, Svelte components, CSS, JSON, etc.) and package manager helpers, see [`@sveltejs/sv-utils`](sv-utils).

### Typed dynamic options

Expand Down Expand Up @@ -124,31 +105,6 @@ const addon = defineAddon<{ theme: string }>()({

The type parameter maps value types (`boolean`, `string`, `number`) to question definitions. Without it, `defineAddon` stays strict and only allows statically defined options.

### Typed dynamic options

If your add-on adds options dynamically in `setup` (e.g. from a fetch), you can pass a type parameter to `defineAddon` to get strong typing for those options:

```ts
import { defineAddon, defineAddonOptions } from 'sv';
// ---cut---
const addon = defineAddon<{ theme: string }>()({
id: 'my-addon',
options: defineAddonOptions().build(),
setup: ({ addOption }) => {
addOption('theme', {
question: 'Which theme?',
type: 'string',
default: 'dark'
});
},
run: ({ options }) => {
options.theme; // string
}
});
```

The type parameter maps value types (`boolean`, `string`, `number`) to question definitions. Without it, `defineAddon` stays strict and only allows statically defined options.

## `defineAddonOptions`

Builder for add-on options. Chained with `.add()` and finalized with `.build()`.
Expand All @@ -175,6 +131,37 @@ const options = defineAddonOptions()

Options are asked in order. The `condition` callback receives the answers collected so far — return `false` to skip the question (its value will be `undefined`).

### Exporting your option types

Pass the values your options produce to `defineAddonOptions`. `build()` then checks your questions against them, both ways, and you get a small type to export for consumers of your add-on:

```ts
import { defineAddonOptions } from 'sv';

// ---cut---
export type MyAddonOptions = { database: 'postgresql' | 'sqlite'; docker: boolean };

const options = defineAddonOptions<MyAddonOptions>()
.add('database', {
question: 'Which database?',
type: 'select',
default: 'sqlite',
options: [{ value: 'postgresql' }, { value: 'sqlite' }]
})
.add('docker', { question: 'Add a docker-compose file?', type: 'boolean', default: false })
.build();
```

Official add-ons follow the same pattern. `OfficialAddonOptions` is what `add()` accepts for them, keyed by add-on id:

```ts
import type { OfficialAddonOptions } from 'sv';

// ---cut---
// unanswered questions fall back to their default
const drizzle: OfficialAddonOptions['drizzle'] = { database: 'sqlite', docker: false };
```

## `create`

Programmatically create a new Svelte project.
Expand Down
27 changes: 27 additions & 0 deletions apps/svelte.dev/content/docs/cli/50-api/20-sv-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ title: sv-utils
npm install -D @sveltejs/sv-utils
```

> [!NOTE]
> Bundle `@sveltejs/sv-utils` in your add-on to pin its version. See [why](faq#Why-bundle-sveltejs-sv-utils-in-my-add-on).

## transforms

`transforms` is a collection of parser-aware functions that lets you modify the files via abstract syntax tree (AST). It accepts a callback function. The return value is designed to be be passed directly into `sv.file()`. 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.
Expand Down Expand Up @@ -257,6 +260,7 @@ svelteConfig.edit({ sv, cwd }, ({ ast, property, override, js }) => {

- **`property(name, { fallback })`** - get-or-create an option's value to mutate in place (arrays, nested objects).
- **`override(props, { dropLeadingComments })`** - set/replace options; `dropLeadingComments` clears a now-stale leading comment (e.g. the adapter-auto note when switching adapters).
- **`config`** - the resolved raw config object for edits that need lower-level AST operations. In a Vite config this is the object passed to `sveltekit()`.

It writes through `sv.file`, so the edit is tracked like any other. If the project has neither config file, a `svelte.config.js` is created.

Expand Down Expand Up @@ -290,6 +294,29 @@ if (packageManager === 'pnpm') {
}
```

## Demo pages

### `defineDemoPage`

Wires an add-on demo into a SvelteKit project. Every demo is listed in a floating `DemoLinks` post-it rendered from the root layout, whatever the template. It returns the pieces you spread into `sv.file()`:

```js
// @noErrors
import { defineDemoPage } from '@sveltejs/sv-utils';

const demo = defineDemoPage('my-addon', language, directory.kitRoutes);

sv.file(...demo.links); // adds `/demo/my-addon` to `<routes>/demo/DemoLinks.svelte`
sv.file(...demo.layout); // renders `<DemoLinks />` in `<routes>/+layout.svelte`
sv.file(`${demo.addonPath}/+page.svelte` /* your demo route */);
```

- **`addonPath`** - `<routes>/demo/<name>`, where your own demo route belongs.
- **`links`** - a `[path, transform]` pair for `<routes>/demo/DemoLinks.svelte` (created if missing).
- **`layout`** - a `[path, transform]` pair for `<routes>/+layout.svelte` (created if missing).

Both transforms are idempotent, so re-running an add-on won't duplicate entries. Only call them when the user asked for a demo (e.g. a `demo` option), so projects without demos don't get the post-it. To opt out, delete `DemoLinks.svelte` and its usage in the layout.

## Browser usage

The package root pulls in Node-only APIs (file system, package manager detection, shell lookups, terminal colors). For browser bundles - in-browser playgrounds, sandboxes, ... - import `@sveltejs/sv-utils/browser` instead, which exposes the environment-agnostic subset: `parse`, `transforms`, the language namespaces (`js`, `svelte`, `css`, `html`, `json`, `text`), `Walker`, `dedent`, the version helpers, `sanitizeName`, `minimizeDiff`, `createPrinter` and `downloadJson`.
Expand Down