Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79bab76
rename workspace `files` to `file`
jycouet Mar 21, 2026
7c06123
extract `createSetupTest` from internal test suite
jycouet Mar 21, 2026
b24d394
update addon template and snapshot to use createSetupTest
jycouet Mar 21, 2026
aafd1c2
add `transforms` API to sv-utils
jycouet Mar 21, 2026
3e3760c
migrate all addons from `parse` to `transforms`
jycouet Mar 21, 2026
3250780
refactor: move file helpers to sv-utils
jycouet Mar 21, 2026
2f33a9b
refactor: move all file helpers to sv-utils, use cwd instead of Works…
jycouet Mar 21, 2026
48665f7
refactor: rename files.ts to formatFiles.ts, import from sv-utils dir…
jycouet Mar 21, 2026
3dd34a6
fmt
jycouet Mar 21, 2026
19a95ab
Merge branch 'feat/transforms-api' into feat_com-add-rebuild
jycouet Mar 21, 2026
4e194e2
Merge branch 'refactor/files-to-file' into feat_com-add-rebuild
jycouet Mar 21, 2026
00b0a0a
Merge branch 'refactor/move-file-helpers-to-sv-utils' into feat_com-a…
jycouet Mar 21, 2026
618f1ad
feat: remaining tweaks for community add-ons
jycouet Mar 21, 2026
f03f842
docs: document transform context parameter and optional addon properties
jycouet Mar 21, 2026
d3dfcc5
docs: fix type error annotations for svelte.dev doc checker
jycouet Mar 21, 2026
7a72294
update docs
jycouet Mar 21, 2026
3a01dbd
like this
jycouet Mar 21, 2026
ae6cc71
docs: make sv-utils examples standalone with proper imports
jycouet Mar 22, 2026
eee119b
chore: trigger CI
jycouet Mar 22, 2026
e87d217
chore: trigger CI
jycouet Mar 23, 2026
4723975
merge
jycouet Mar 24, 2026
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
5 changes: 1 addition & 4 deletions documentation/docs/20-commands/20-sv-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ Prevents installing dependencies
> [!NOTE]
> Svelte maintainers have not reviewed community add-ons for malicious code!

You can find [community add-ons on npm](https://www.npmjs.com/search?q=keywords%3Asv-add) by searching for `keywords:sv-add`.
You can find [community add-ons](https://npmx.dev/search?q=keyword:sv-add) by searching for the keyword `sv-add`.

### How to install a community add-on

Expand Down Expand Up @@ -99,9 +99,6 @@ npx sv create --add eslint "@supacool"
# Scoped package: @org (preferred), we will look for @org/sv
npx sv add "@supacool"

# Regular npm package (with or without scope)
npx sv add my-cool-addon

# Local add-on
npx sv add file:../path/to/my-addon
```
Expand Down
88 changes: 65 additions & 23 deletions documentation/docs/40-api/10-add-on.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Typically, an add-on looks like this:
_hover keywords in the code to have some more context_

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

// Define options that will be prompted to the user (or passed as arguments)
Expand All @@ -37,6 +37,8 @@ const options = defineAddonOptions()
// your add-on definition, the entry point
export default defineAddon({
id: 'your-addon-name',
// shortDescription: 'does X', // optional: one-liner shown in prompts
// homepage: 'https://...', // optional: link to docs/repo

options,

Expand All @@ -46,21 +48,23 @@ export default defineAddon({
},

// actual execution of the addon
run: ({ kit, cancel, sv, options }) => {
if (!kit) return cancel('SvelteKit is required');
run: ({ isKit, cancel, sv, options, directory }) => {
if (!isKit) return cancel('SvelteKit is required');

// Add "Hello [who]!" to the root page
sv.file(kit.routesDirectory + '/+page.svelte', (content) => {
const { ast, generateCode } = parse.svelte(content);

svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);

return generateCode();
});
sv.file(
directory.routes + '/+page.svelte',
transforms.svelte((ast) => {
svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
})
);
}
});
```

> `sv` owns the file system — `sv.file()` resolves the path, reads the file, applies the transform, and writes the result.
> `@sveltejs/sv-utils` owns the content — `transforms.svelte()` handles parsing, gives you the AST, and serializes back. See [sv-utils](/docs/cli/sv-utils) for the full API.

## Development with `file:` protocol

While developing your add-on, you can test it locally using the `file:` protocol:
Expand All @@ -77,8 +81,8 @@ This allows you to iterate quickly without publishing to npm.
The `sv/testing` module provides utilities for testing your add-on:

```js
import { test, expect } from 'vitest';
import { setupTest } from 'sv/testing';
import { test, expect } from 'vitest';
import addon from './index.js';

test('adds hello message', async () => {
Expand All @@ -94,27 +98,44 @@ test('adds hello message', async () => {
});
```

## Publishing to npm
## Building and publishing

### Bundling

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

```sh
npm run build
```

### Package structure

Your add-on must have `sv` as a dependency in `package.json`:
Your add-on must have `sv` as a peer dependency and **no** `dependencies` in `package.json`:

```json
{
"name": "@your-org/sv",
"version": "1.0.0",
"type": "module",
"exports": {
".": "./dist/index.js"
".": "./src/index.js"
},
"publishConfig": {
"access": "public",
"exports": {
".": { "default": "./dist/index.js" }
}
},
"dependencies": {
"sv": "^0.11.0"
"peerDependencies": {
"sv": "^0.13.0"
},
"keywords": ["sv-add"]
}
```

- `exports` points to `./src/index.js` for local development with the `file:` protocol.
- `publishConfig.exports` overrides exports when publishing, pointing to the bundled `./dist/index.js`.

> [!NOTE]
> Add the `sv-add` keyword so users can discover your add-on on npm.

Expand All @@ -127,7 +148,7 @@ Your package can export the add-on in two ways:
```json
{
"exports": {
".": "./dist/index.js"
".": "./src/index.js"
}
}
```
Expand All @@ -136,17 +157,38 @@ Your package can export the add-on in two ways:
```json
{
"exports": {
".": "./dist/main.js",
"./sv": "./dist/addon.js"
".": "./src/main.js",
"./sv": "./src/addon.js"
}
}
```

### Naming conventions
### Publishing

Community add-ons must be scoped packages (e.g. `@your-org/sv`). Users install with `npx sv add @your-org`.

```sh
npm login
npm publish
```

> `prepublishOnly` automatically runs the build before publishing.

- **Scoped packages**: Use `@your-org/sv` as the package name. Users can then install with just `npx sv add @your-org`.
- **Regular packages**: Any name works. Users install with `npx sv add your-package-name`.
## Next steps

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

```js
// @errors: 2304 7031
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 the minimum `sv` version it requires in `package.json`. If a user's `sv` version has a different major version than what your add-on was built for, they will see a compatibility warning.
Your add-on should specify the minimum `sv` version it requires in `peerDependencies`. If a user's `sv` version has a different major version than what your add-on was built for, they will see a compatibility warning.
161 changes: 160 additions & 1 deletion documentation/docs/40-api/20-sv-utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,164 @@ title: sv-utils
`@sveltejs/sv-utils` provides utilities for parsing, transforming, and generating code in add-ons.

```sh
npm install @sveltejs/sv-utils
npm install -D @sveltejs/sv-utils
```

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

## Transforms

Transforms are typed, parser-aware functions that turn `string -> string`. 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.

```js
import { transforms, js, svelte, css, json } from '@sveltejs/sv-utils';
```

### `transforms.script`

Transform a JavaScript/TypeScript file. The callback receives the AST, comments, and a context with `language`.

```js
import { transforms, js } from '@sveltejs/sv-utils';

const addVitePlugin = transforms.script((ast, comments, { language }) => {
js.imports.addDefault(ast, { as: 'foo', from: 'foo' });
js.vite.addPlugin(ast, { code: 'foo()' });
});
```

### `transforms.svelte`

Transform a Svelte component. The engine injects `language` automatically via the context.

```js
import { transforms, js, svelte } from '@sveltejs/sv-utils';

const addFooComponent = transforms.svelte((ast, { language }) => {
svelte.ensureScript(ast, { language });
js.imports.addDefault(ast.instance.content, { as: 'Foo', from: './Foo.svelte' });
svelte.addFragment(ast, '<Foo />');
});
```

### `transforms.css`

Transform a CSS file. The callback receives the AST and a context with `language`.

```js
import { transforms, css } from '@sveltejs/sv-utils';

const addTailwind = transforms.css((ast, { language }) => {
css.addAtRule(ast, { name: 'import', params: "'tailwindcss'" });
});
```

### `transforms.json`

Transform a JSON file. Mutate the `data` object directly. The callback also receives a context with `language`.

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

const enableStrict = transforms.json((data, { language }) => {
data.compilerOptions ??= {};
data.compilerOptions.strict = true;
});
```

### `transforms.yaml` / `transforms.toml`

Same pattern as `transforms.json`, for YAML and TOML files respectively. All callbacks receive a context with `language`.

### `transforms.text`

Transform a plain text file (.env, .gitignore, etc.). No parser — string in, string out. The callback also receives a context with `language`.

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

const addDbUrl = transforms.text((content, { language }) => {
return content + '\nDATABASE_URL="file:local.db"';
});
```

### Aborting a transform

Return `false` from any transform callback to abort — the original content is returned unchanged.

```js
import { transforms, js } from '@sveltejs/sv-utils';

const myConfig = '{}';
const setupEslint = transforms.script((ast) => {
const { value: existing } = js.exports.createDefault(ast, { fallback: myConfig });
if (existing !== myConfig) {
// config already exists, don't touch it
return false;
}
// ... continue modifying ast
});
```

### Standalone usage & testing

Transforms are just functions — they work without the `sv` engine. Pass content directly, with an optional context:

```js
import { transforms, js } from '@sveltejs/sv-utils';

const addPlugin = transforms.script((ast) => {
js.imports.addDefault(ast, { as: 'foo', from: 'foo' });
});

// use standalone — pass content and context directly
const result = addPlugin('export default {}', { language: 'ts' });
```

### Composability

Add-ons can export reusable transforms that other add-ons consume:

```js
import { transforms, js, svelte } from '@sveltejs/sv-utils';

// reusable transform — export from your package
export const addFooImport = transforms.svelte((ast, { language }) => {
svelte.ensureScript(ast, { language });
js.imports.addDefault(ast.instance.content, { as: 'Foo', from: './Foo.svelte' });
});
```

## Parsers (low-level)

For cases where transforms don't fit (e.g., conditional parsing, error handling around the parser), the `parse` namespace is still available:

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

const { ast, generateCode } = parse.script(content);
const { ast, generateCode } = parse.svelte(content);
const { ast, generateCode } = parse.css(content);
const { data, generateCode } = parse.json(content);
const { data, generateCode } = parse.yaml(content);
const { data, generateCode } = parse.toml(content);
const { ast, generateCode } = parse.html(content);
```

## Language tooling

Namespaced helpers for AST manipulation:

- **`js.*`** — imports, exports, objects, arrays, variables, functions, vite config helpers, SvelteKit helpers
- **`css.*`** — rules, declarations, at-rules, imports
- **`svelte.*`** — ensureScript, addSlot, addFragment
- **`json.*`** — arrayUpsert, packageScriptsUpsert
- **`html.*`** — attribute manipulation
- **`text.*`** — upsert lines in flat files (.env, .gitignore)
1 change: 0 additions & 1 deletion packages/sv-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
},
"keywords": [
"sv",
"sv-add",
"svelte",
"sveltekit"
]
Expand Down
26 changes: 18 additions & 8 deletions packages/sv-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,31 @@ export * as text from './tooling/text.ts';
export * as json from './tooling/json.ts';
export * as svelte from './tooling/svelte/index.ts';

// Transforms — sv-utils = what to do to content, sv = where and when to do it.
export {
transforms,
isTransform,
type TransformFn,
type TransformContext
} from './tooling/transforms.ts';

/**
* Will help you `parse` code into an `ast` from all supported languages.
* Then manipulate the `ast` as you want,
* and finally `generateCode()` to write it back to the file.
* Low-level parsers. Prefer `transforms` for add-on file edits — it picks the
* right parser for you and handles `generateCode()` automatically.
*
* Use `parse` directly when you need error handling around parsing or
* conditional parser selection at runtime.
*
* ```ts
* import { parse } from '@sveltejs/sv-utils';
*
* const { ast, generateCode } = parse.css('body { color: red; }');
* const { ast, generateCode } = parse.html('<div>Hello, world!</div>');
* const { ast, generateCode } = parse.json('{ "name": "John", "age": 30 }');
* const { ast, generateCode } = parse.script('function add(a, b) { return a + b; }');
* const { ast, generateCode } = parse.svelte('<div>Hello, world!</div>');
* const { ast, generateCode } = parse.toml('name = "John"');
* const { ast, generateCode } = parse.yaml('name: John');
* const { ast, generateCode } = parse.css('body { color: red; }');
* const { data, generateCode } = parse.json('{ "name": "John", "age": 30 }');
* const { data, generateCode } = parse.yaml('name: John');
* const { data, generateCode } = parse.toml('name = "John"');
* const { ast, generateCode } = parse.html('<div>Hello, world!</div>');
* ```
*/
export const parse = {
Expand Down
Loading
Loading