From 79500f8c7af3dfbf45d04f13a18f84292a8d052d Mon Sep 17 00:00:00 2001 From: "svelte-docs-bot[bot]" <196124396+svelte-docs-bot[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 07:34:07 +0000 Subject: [PATCH] sync cli docs --- .../docs/cli/10-introduction/20-faq.md | 15 ++++ .../docs/cli/30-add-ons/30-playwright.md | 13 ++- .../docs/cli/30-add-ons/99-community.md | 11 +-- .../content/docs/cli/50-api/10-sv.md | 87 ++++++++----------- .../content/docs/cli/50-api/20-sv-utils.md | 27 ++++++ 5 files changed, 97 insertions(+), 56 deletions(-) diff --git a/apps/svelte.dev/content/docs/cli/10-introduction/20-faq.md b/apps/svelte.dev/content/docs/cli/10-introduction/20-faq.md index 9346feb9c0..345573a9ee 100644 --- a/apps/svelte.dev/content/docs/cli/10-introduction/20-faq.md +++ b/apps/svelte.dev/content/docs/cli/10-introduction/20-faq.md @@ -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). diff --git a/apps/svelte.dev/content/docs/cli/30-add-ons/30-playwright.md b/apps/svelte.dev/content/docs/cli/30-add-ons/30-playwright.md index aff9b6ae87..932d7d60e2 100644 --- a/apps/svelte.dev/content/docs/cli/30-add-ons/30-playwright.md +++ b/apps/svelte.dev/content/docs/cli/30-add-ons/30-playwright.md @@ -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" +``` diff --git a/apps/svelte.dev/content/docs/cli/30-add-ons/99-community.md b/apps/svelte.dev/content/docs/cli/30-add-ons/99-community.md index f08f0fbca3..91abcd42f9 100644 --- a/apps/svelte.dev/content/docs/cli/30-add-ons/99-community.md +++ b/apps/svelte.dev/content/docs/cli/30-add-ons/99-community.md @@ -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 }); @@ -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 @@ -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'; @@ -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` diff --git a/apps/svelte.dev/content/docs/cli/50-api/10-sv.md b/apps/svelte.dev/content/docs/cli/50-api/10-sv.md index 99f6e738bd..56c72f32b3 100644 --- a/apps/svelte.dev/content/docs/cli/50-api/10-sv.md +++ b/apps/svelte.dev/content/docs/cli/50-api/10-sv.md @@ -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', { @@ -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 @@ -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()`. @@ -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() + .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. diff --git a/apps/svelte.dev/content/docs/cli/50-api/20-sv-utils.md b/apps/svelte.dev/content/docs/cli/50-api/20-sv-utils.md index ede95ba383..0643ec2c19 100644 --- a/apps/svelte.dev/content/docs/cli/50-api/20-sv-utils.md +++ b/apps/svelte.dev/content/docs/cli/50-api/20-sv-utils.md @@ -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. @@ -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. @@ -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 `/demo/DemoLinks.svelte` +sv.file(...demo.layout); // renders `` in `/+layout.svelte` +sv.file(`${demo.addonPath}/+page.svelte` /* your demo route */); +``` + +- **`addonPath`** - `/demo/`, where your own demo route belongs. +- **`links`** - a `[path, transform]` pair for `/demo/DemoLinks.svelte` (created if missing). +- **`layout`** - a `[path, transform]` pair for `/+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`.