Skip to content

Commit 06f285e

Browse files
committed
feat: community add-ons - the revenge of the rebase
Simplify transforms API (flat args + TransformContext), move file helpers back to sv, enforce scoped addon names, sv as peerDep, zero dependencies.
1 parent 504bce9 commit 06f285e

42 files changed

Lines changed: 840 additions & 1118 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

documentation/docs/20-commands/20-sv-add.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ Prevents installing dependencies
7171
> [!NOTE]
7272
> Svelte maintainers have not reviewed community add-ons for malicious code!
7373
74-
You can find [community add-ons on npm](https://www.npmjs.com/search?q=keywords%3Asv-add) by searching for `keywords:sv-add`.
74+
You can find [community add-ons](https://npmx.dev/search?q=keyword:sv-add) by searching for the keyword `sv-add`.
7575

7676
### How to install a community add-on
7777

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

102-
# Regular npm package (with or without scope)
103-
npx sv add my-cool-addon
104-
105102
# Local add-on
106103
npx sv add file:../path/to/my-addon
107104
```

documentation/docs/40-api/10-add-on.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ The easiest way to create an add-on is using the addon template:
1313

1414
```sh
1515
npx sv create --template addon my-addon
16+
cd my-addon
1617
```
1718

1819
## Add-on structure
@@ -22,8 +23,7 @@ Typically, an add-on looks like this:
2223
_hover keywords in the code to have some more context_
2324

2425
```js
25-
// @noErrors
26-
import { transforms } from '@sveltejs/sv-utils';
26+
import { transforms, svelte } from '@sveltejs/sv-utils';
2727
import { defineAddon, defineAddonOptions } from 'sv';
2828

2929
// Define options that will be prompted to the user (or passed as arguments)
@@ -52,15 +52,18 @@ export default defineAddon({
5252
if (!isKit) return cancel('SvelteKit is required');
5353

5454
// Add "Hello [who]!" to the root page
55-
sv.file(directory.routes + '/+page.svelte', transforms.svelte(({ ast, svelte }) => {
56-
svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
57-
}));
55+
sv.file(
56+
directory.routes + '/+page.svelte',
57+
transforms.svelte((ast) => {
58+
svelte.addFragment(ast, `<p>Hello ${options.who}!</p>`);
59+
})
60+
);
5861
}
5962
});
6063
```
6164

62-
> `sv` owns the file system - `sv.file()` resolves the path, reads the file, applies the edit function, and writes the result.
63-
> `@sveltejs/sv-utils` owns the content - `transforms.svelte()` returns a curried function that handles parsing, gives you the AST and utils, and serializes back. See [sv-utils](/docs/cli/sv-utils) for the full API.
65+
> `sv` owns the file system `sv.file()` resolves the path, reads the file, applies the transform, and writes the result.
66+
> `@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.
6467
6568
## Development with `file:` protocol
6669

@@ -176,7 +179,7 @@ npm publish
176179
You can optionally display guidance after your add-on runs:
177180

178181
```js
179-
// @noErrors
182+
// @errors: 2304 7031
180183
export default defineAddon({
181184
// ...
182185
nextSteps: ({ options }) => [

documentation/docs/40-api/20-sv-utils.md

Lines changed: 45 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -16,158 +16,128 @@ npm install -D @sveltejs/sv-utils
1616
The Svelte CLI is split into two packages with a clear boundary:
1717

1818
- **`sv`** = **where and when** to do it. It owns paths, workspace detection, dependency tracking, and file I/O. The engine orchestrates add-on execution.
19-
- **`@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.
19+
- **`@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.
2020

2121
This separation means transforms are testable without a workspace and composable across add-ons.
2222

2323
## Transforms
2424

25-
Transforms are curried, parser-aware functions that turn `string -> string`. Call a transform with your callback to get a function that plugs 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.
26-
27-
Each transform injects relevant utilities into the callback, so you only need one import:
25+
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.
2826

2927
```js
30-
import { transforms } from '@sveltejs/sv-utils';
28+
import { transforms, js, svelte, css, json } from '@sveltejs/sv-utils';
3129
```
3230

3331
### `transforms.script`
3432

35-
Transform a JavaScript/TypeScript file. The callback receives `{ ast, comments, content, js }`.
33+
Transform a JavaScript/TypeScript file. The callback receives the AST, comments, and a context with `language`.
3634

3735
```js
38-
// @noErrors
39-
import { transforms } from '@sveltejs/sv-utils';
36+
import { transforms, js } from '@sveltejs/sv-utils';
4037

41-
sv.file(files.viteConfig, transforms.script(({ ast, js }) => {
38+
const addVitePlugin = transforms.script((ast, comments, { language }) => {
4239
js.imports.addDefault(ast, { as: 'foo', from: 'foo' });
4340
js.vite.addPlugin(ast, { code: 'foo()' });
44-
}));
41+
});
4542
```
4643

4744
### `transforms.svelte`
4845

49-
Transform a Svelte component. The callback receives `{ ast, content, svelte, js }`.
46+
Transform a Svelte component. The engine injects `language` automatically via the context.
5047

5148
```js
52-
// @noErrors
53-
import { transforms } from '@sveltejs/sv-utils';
54-
55-
sv.file(layoutPath, transforms.svelte(({ ast, svelte }) => {
56-
svelte.addFragment(ast, '<Foo />');
57-
}));
58-
```
49+
import { transforms, js, svelte } from '@sveltejs/sv-utils';
5950

60-
### `transforms.svelteScript`
61-
62-
Transform a Svelte component with a `<script>` block guaranteed. Pass `{ language }` as the first argument. The callback receives `{ ast, content, svelte, js }` where `ast.instance` is always non-null.
63-
64-
```js
65-
// @noErrors
66-
import { transforms } from '@sveltejs/sv-utils';
67-
68-
sv.file(layoutPath, transforms.svelteScript({ language }, ({ ast, svelte, js }) => {
51+
const addFooComponent = transforms.svelte((ast, { language }) => {
52+
svelte.ensureScript(ast, { language });
6953
js.imports.addDefault(ast.instance.content, { as: 'Foo', from: './Foo.svelte' });
7054
svelte.addFragment(ast, '<Foo />');
71-
}));
55+
});
7256
```
7357

7458
### `transforms.css`
7559

76-
Transform a CSS file. The callback receives `{ ast, content, css }`.
60+
Transform a CSS file. The callback receives the AST and a context with `language`.
7761

7862
```js
79-
// @noErrors
80-
import { transforms } from '@sveltejs/sv-utils';
63+
import { transforms, css } from '@sveltejs/sv-utils';
8164

82-
sv.file(files.stylesheet, transforms.css(({ ast, css }) => {
65+
const addTailwind = transforms.css((ast, { language }) => {
8366
css.addAtRule(ast, { name: 'import', params: "'tailwindcss'" });
84-
}));
67+
});
8568
```
8669

8770
### `transforms.json`
8871

89-
Transform a JSON file. Mutate the `data` object directly. The callback receives `{ data, content, json }`.
72+
Transform a JSON file. Mutate the `data` object directly. The callback also receives a context with `language`.
9073

9174
```js
92-
// @noErrors
9375
import { transforms } from '@sveltejs/sv-utils';
9476

95-
sv.file(files.tsconfig, transforms.json(({ data }) => {
77+
const enableStrict = transforms.json((data, { language }) => {
9678
data.compilerOptions ??= {};
9779
data.compilerOptions.strict = true;
98-
}));
80+
});
9981
```
10082
10183
### `transforms.yaml` / `transforms.toml`
10284
103-
Same pattern as `transforms.json`, for YAML and TOML files respectively. The callback receives `{ data, content }`.
85+
Same pattern as `transforms.json`, for YAML and TOML files respectively. All callbacks receive a context with `language`.
10486
10587
### `transforms.text`
10688
107-
Transform a plain text file (.env, .gitignore, etc.). No parser - string in, string out. The callback receives `{ content, text }`.
89+
Transform a plain text file (.env, .gitignore, etc.). No parser string in, string out. The callback also receives a context with `language`.
10890
10991
```js
110-
// @noErrors
11192
import { transforms } from '@sveltejs/sv-utils';
11293

113-
sv.file('.env', transforms.text(({ content }) => {
94+
const addDbUrl = transforms.text((content, { language }) => {
11495
return content + '\nDATABASE_URL="file:local.db"';
115-
}));
96+
});
11697
```
11798
11899
### Aborting a transform
119100
120-
Return `false` from any transform callback to abort - the original content is returned unchanged.
101+
Return `false` from any transform callback to abort the original content is returned unchanged.
121102
122103
```js
123-
// @noErrors
124-
import { transforms } from '@sveltejs/sv-utils';
104+
import { transforms, js } from '@sveltejs/sv-utils';
125105

126-
sv.file(files.eslintConfig, transforms.script(({ ast, js }) => {
106+
const myConfig = '{}';
107+
const setupEslint = transforms.script((ast) => {
127108
const { value: existing } = js.exports.createDefault(ast, { fallback: myConfig });
128109
if (existing !== myConfig) {
129110
// config already exists, don't touch it
130111
return false;
131112
}
132113
// ... continue modifying ast
133-
}));
114+
});
134115
```
135116
136117
### Standalone usage & testing
137118
138-
Transforms are curried functions - call them with the callback, then apply to content:
119+
Transforms are just functions — they work without the `sv` engine. Pass content directly, with an optional context:
139120
140121
```js
141-
import { transforms } from '@sveltejs/sv-utils';
122+
import { transforms, js } from '@sveltejs/sv-utils';
142123

143-
const result = transforms.script(({ ast, js }) => {
124+
const addPlugin = transforms.script((ast) => {
144125
js.imports.addDefault(ast, { as: 'foo', from: 'foo' });
145-
})('export default {}');
126+
});
127+
128+
// use standalone — pass content and context directly
129+
const result = addPlugin('export default {}', { language: 'ts' });
146130
```
147131
148132
### Composability
149133
150-
For cases where you need to mix transforms and raw edits, use `sv.file` with a content callback and invoke the curried transform manually:
134+
Add-ons can export reusable transforms that other add-ons consume:
151135
152136
```js
153-
// @noErrors
154-
sv.file(path, (content) => {
155-
content = transforms.script(({ ast, js }) => {
156-
js.imports.addDefault(ast, { as: 'foo', from: 'foo' });
157-
})(content);
158-
content = content.replace('foo', 'bar');
159-
return content;
160-
});
161-
```
162-
163-
Add-ons can also export reusable transform functions:
164-
165-
```js
166-
// @errors: 7006
167-
import { transforms } from '@sveltejs/sv-utils';
137+
import { transforms, js, svelte } from '@sveltejs/sv-utils';
168138

169-
// reusable - export from your package
170-
export const addFooImport = transforms.svelte(({ ast, svelte, js }) => {
139+
// reusable transform — export from your package
140+
export const addFooImport = transforms.svelte((ast, { language }) => {
171141
svelte.ensureScript(ast, { language });
172142
js.imports.addDefault(ast.instance.content, { as: 'Foo', from: './Foo.svelte' });
173143
});
@@ -178,7 +148,6 @@ export const addFooImport = transforms.svelte(({ ast, svelte, js }) => {
178148
For cases where transforms don't fit (e.g., conditional parsing, error handling around the parser), the `parse` namespace is still available:
179149
180150
```js
181-
// @noErrors
182151
import { parse } from '@sveltejs/sv-utils';
183152

184153
const { ast, generateCode } = parse.script(content);
@@ -194,9 +163,9 @@ const { ast, generateCode } = parse.html(content);
194163
195164
Namespaced helpers for AST manipulation:
196165
197-
- **`js.*`** - imports, exports, objects, arrays, variables, functions, vite config helpers, SvelteKit helpers
198-
- **`css.*`** - rules, declarations, at-rules, imports
199-
- **`svelte.*`** - ensureScript, addSlot, addFragment
200-
- **`json.*`** - arrayUpsert, packageScriptsUpsert
201-
- **`html.*`** - attribute manipulation
202-
- **`text.*`** - upsert lines in flat files (.env, .gitignore)
166+
- **`js.*`** imports, exports, objects, arrays, variables, functions, vite config helpers, SvelteKit helpers
167+
- **`css.*`** rules, declarations, at-rules, imports
168+
- **`svelte.*`** ensureScript, addSlot, addFragment
169+
- **`json.*`** arrayUpsert, packageScriptsUpsert
170+
- **`html.*`** attribute manipulation
171+
- **`text.*`** upsert lines in flat files (.env, .gitignore)

packages/sv-utils/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
},
3939
"keywords": [
4040
"sv",
41-
"sv-add",
4241
"svelte",
4342
"sveltekit"
4443
]

packages/sv-utils/src/index.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ export * as json from './tooling/json.ts';
2929
export * as svelte from './tooling/svelte/index.ts';
3030

3131
// Transforms — sv-utils = what to do to content, sv = where and when to do it.
32-
export { transforms } from './tooling/transforms.ts';
32+
export {
33+
transforms,
34+
isTransform,
35+
type TransformFn,
36+
type TransformContext
37+
} from './tooling/transforms.ts';
3338

3439
/**
3540
* Low-level parsers. Prefer `transforms` for add-on file edits — it picks the
@@ -66,20 +71,8 @@ export { createPrinter } from './utils.ts';
6671
export { sanitizeName } from './sanitize.ts';
6772
export { downloadJson } from './downloadJson.ts';
6873

69-
// File system helpers
70-
export {
71-
commonFilePaths,
72-
fileExists,
73-
getPackageJson,
74-
installPackages,
75-
readFile,
76-
writeFile,
77-
type Package
78-
} from './files.ts';
79-
8074
// Terminal styling
8175
export { color } from './color.ts';
8276

8377
// Types
8478
export type { Comments, AstTypes, SvelteAst } from './tooling/index.ts';
85-
export type { TransformFn } from './tooling/transforms.ts';

0 commit comments

Comments
 (0)