diff --git a/.changeset/some-rings-appear.md b/.changeset/some-rings-appear.md new file mode 100644 index 000000000..249478187 --- /dev/null +++ b/.changeset/some-rings-appear.md @@ -0,0 +1,6 @@ +--- +'sv': minor +'@sveltejs/sv-utils': minor +--- + +feat: community add-ons are now **experimental** diff --git a/documentation/docs/20-commands/20-sv-add.md b/documentation/docs/20-commands/20-sv-add.md index 7c0b56ada..ccc60f755 100644 --- a/documentation/docs/20-commands/20-sv-add.md +++ b/documentation/docs/20-commands/20-sv-add.md @@ -63,3 +63,31 @@ Prevents installing dependencies - [`tailwindcss`](tailwind) - [`vitest`](vitest) +## Community add-ons + +> [!NOTE] +> Community add-ons are currently **experimental**. The API may change. Don't use them in production yet! + +> [!NOTE] +> Svelte maintainers have not reviewed community add-ons for malicious code! + +Community add-ons are npm packages published by the community. Look out for add-ons from your favourite libraries and tools. _(soon)_ Many developers are building `sv` add-ons to make their integrations a one-liner. You can find them on [npmx](https://www.npmx.dev/search?q=keyword:sv-add) by searching for the keyword: `sv-add`. + +```sh +# Install a community add-on by org name (it will look at @org/sv) +npx sv add @supacool + +# Use a local add-on (for development or internal use) +npx sv add file:../path/to/my-addon + +# Mix and match official and community add-ons +npx sv add eslint @supacool + +# Also works when creating a new project directly +npx sv create --add eslint @supacool +``` + +> [!NOTE] +> On Windows PowerShell, `@` is a special character that should be escaped with single quotes. For example: `npx sv add '@supacool'`. + +Want to create your own? Check the [Add-on Docs](community). diff --git a/documentation/docs/30-add-ons/99-community.md b/documentation/docs/30-add-ons/99-community.md index 4e7823093..a7e8d1a1b 100644 --- a/documentation/docs/30-add-ons/99-community.md +++ b/documentation/docs/30-add-ons/99-community.md @@ -2,5 +2,257 @@ title: [create your own] --- +> [!NOTE] +> Community add-ons are currently **experimental**. The API may change. Don't use them in production yet! +This guide covers how to create, test, and publish community add-ons for `sv`. +## Quick start + +The easiest way to create an add-on is by using the `addon` template: + +```sh +npx sv create --template addon [path] +``` + +The newly created project will have a `README.md` and `CONTRIBUTING.md` to guide you along. + +## Project structure + +Typically, an add-on looks like this: + +```js +import { transforms } from '@sveltejs/sv-utils'; +import { defineAddon, defineAddonOptions } from 'sv'; + +export default defineAddon({ + id: 'addon-name', + + shortDescription: 'a better description of what your addon does ;)', + + options: defineAddonOptions() + .add('who', { + question: 'To whom should the addon say hello?', + type: 'string' // boolean | number | select | multiselect + }) + .build(), + + setup: ({ dependsOn, isKit, unsupported }) => { + if (!isKit) unsupported('Requires SvelteKit'); + dependsOn('vitest'); + }, + + run: ({ isKit, cancel, sv, options, file, language, directory }) => { + // Add "Hello [who]!" to the root page + sv.file( + directory.kitRoutes + '/+page.svelte', + transforms.svelte(({ ast, svelte }) => { + svelte.addFragment(ast, `
Hello ${options.who}!
`); + }) + ); + }, + + nextSteps: ({ options }) => ['enjoy the add-on!'] +}); +``` + +The Svelte 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. + +## Development + +You can run your add-on locally using the `file:` protocol: + +```sh +cd /path/to/test-project +npx sv add file:../path/to/my-addon +``` + +This allows you to iterate quickly without publishing to npm. + +The `file:` protocol also works for custom or private add-ons that you don't intend to publish - for example, to standardize project setup across your team or organization. + +> [!NOTE] +> The `demo-add` script automatically builds your add-on before running it. + +## Testing + +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 { createSetupTest } from 'sv/testing'; +import * as vitest from 'vitest'; +import addon from './index.js'; + +const { test, testCases } = createSetupTest(vitest)( + { addon }, + { + kinds: [ + { + type: 'default', + options: { + 'your-addon-name': { who: 'World' } + } + } + ], + filter: (testCase) => testCase.variant.includes('kit'), + browser: false + } +); + +test.concurrent.for(testCases)('my-addon $kind.type $variant', async (testCase, ctx) => { + const cwd = ctx.cwd(testCase); + + const page = fs.readFileSync(path.resolve(cwd, 'src/routes/+page.svelte'), 'utf8'); + expect(page).toContain('Hello World!'); +}); +``` + +Your `vitest.config.js` must include the global setup from `sv/testing`: + +```js +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['tests/**/*.test.{js,ts}'], + globalSetup: ['tests/setup/global.js'] + } +}); +``` + +And the global test setup script `tests/setup/global.js`: + +```js +import { fileURLToPath } from 'node:url'; +import { setupGlobal } from 'sv/testing'; + +const TEST_DIR = fileURLToPath(new URL('../../.test-output/', import.meta.url)); + +export default setupGlobal({ TEST_DIR }); +``` + +## Publishing + +### Bundling + +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.) + +### `package.json` + +Your add-on must have `sv` as a peer dependency and **no** `dependencies` in `package.json`: + +```jsonc +{ + "name": "@my-org/sv", + "version": "1.0.0", + "type": "module", + // bundled entrypoint (tsdown outputs .mjs for ESM) + "exports": { + ".": { "default": "./dist/index.mjs" } + }, + "publishConfig": { + "access": "public" + }, + // cannot have dependencies + "dependencies": {}, + "peerDependencies": { + // minimum version required to run by this add-on + "sv": "^0.13.0" + }, + // Add the "sv-add" keyword so users can discover your add-on + "keywords": ["sv-add", "svelte", "sveltekit"] +} +``` + +### Naming convention + +#### packages names + +If you name your package `@my-org/sv`, users can install it by typing just the org handle: + +```sh +npx sv add @my-org +``` + +It's also possible to publish like `@my-org/core`, just users will need to type the full package name. + +```sh +npx sv add @my-org/core +``` + +Users can also ask for a specific version: + +```sh +npx sv add @my-org/sv@1.2.3 +``` + +When no version is specified, `latest` is used. + +> [!NOTE] +> Unscoped packages are not supported yet + +#### export options + +`sv` first tries to import `your-package/sv`, then falls back to the default export. This means you have two options: + +1. **Default export** (for dedicated add-on packages): + + ```json + { + "exports": { + ".": "./src/index.js" + } + } + ``` + +2. **`./sv` export** (for packages that also export other functionality): + ```json + { + "exports": { + ".": "./src/main.js", + "./sv": "./src/addon.js" + } + } + ``` + +### Publish to npm + +```sh +npm login +npm publish +``` + +> `prepublishOnly` automatically runs the build before publishing. + +## Next steps + +You can optionally display guidance in the console after your add-on runs: + +```js +import { color } from '@sveltejs/sv-utils'; + +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 a minimum `sv` version in `peerDependencies`. Your users will get a compatibility warning if their `sv` version has a different major version than what was specified. + +## Examples + +See the [official add-on source code](https://github.com/sveltejs/cli/tree/main/packages/sv/src/addons) for some real world examples. diff --git a/documentation/docs/50-api/10-sv.md b/documentation/docs/50-api/10-sv.md new file mode 100644 index 000000000..70809023a --- /dev/null +++ b/documentation/docs/50-api/10-sv.md @@ -0,0 +1,110 @@ +--- +title: sv +--- + +`sv` exposes a programmatic API for creating projects and running add-ons. + +## `defineAddon` + +Creates an add-on definition. See [create your own](community) for a full guide. + +```js +import { transforms } from '@sveltejs/sv-utils'; +import { defineAddon, defineAddonOptions } from 'sv'; + +export default defineAddon({ + id: 'my-addon', + options: defineAddonOptions().build(), + + // called before run — declare dependencies and environment requirements + setup: ({ dependsOn, unsupported, isKit }) => { + if (!isKit) unsupported('Requires SvelteKit'); + dependsOn('eslint'); + }, + + // the actual work — add files, edit files, declare dependencies + run: ({ sv, options, cancel }) => { + // add a dependency + sv.devDependency('my-lib', '^1.0.0'); + + // create or edit files using transforms from @sveltejs/sv-utils + sv.file('src/lib/foo.ts', (content) => { + return 'export const foo = true;'; + }); + + sv.file( + 'src/routes/+page.svelte', + transforms.svelte(({ ast, svelte }) => { + svelte.addFragment(ast, 'Hello!
'); + }) + ); + + // cancel at any point if something is wrong + // cancel('reason'); + }, + + // displayed after the add-on runs + nextSteps: ({ options }) => ['Run `npm run dev` to get started'] +}); +``` + +The `sv` object in `run` provides `file`, `dependency`, `devDependency`, `execute`, and `pnpmBuildDependency`. For file transforms (AST-based editing of scripts, Svelte components, CSS, JSON, etc.), see [`@sveltejs/sv-utils`](sv-utils). + +## `defineAddonOptions` + +Builder for add-on options. Chained with `.add()` and finalized with `.build()`. + +```js +import { defineAddonOptions } from 'sv'; + +const options = defineAddonOptions() + .add('database', { + question: 'Which database?', + type: 'select', + default: 'postgresql', + options: [ + { value: 'postgresql' }, + { value: 'mysql' }, + { value: 'sqlite' } + ] + }) + .add('docker', { + question: 'Add a docker-compose file?', + type: 'boolean', + default: false, + // only ask when database is not sqlite + condition: (opts) => opts.database !== 'sqlite' + }) + .build(); +``` + +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`). + +## `create` + +Programmatically create a new Svelte project. + +```js +import { create } from 'sv'; + +create('./my-app', { + name: 'my-app', + template: 'minimal', + types: 'typescript' +}); +``` + +## `add` + +Programmatically run add-ons against an existing project. + +```js +import { add, officialAddons } from 'sv'; + +await add({ + cwd: './my-app', + addons: { prettier: officialAddons.prettier }, + options: { prettier: {} }, + packageManager: 'npm' +}); +``` diff --git a/documentation/docs/50-api/20-sv-utils.md b/documentation/docs/50-api/20-sv-utils.md index 5e364bd61..600ca9a0c 100644 --- a/documentation/docs/50-api/20-sv-utils.md +++ b/documentation/docs/50-api/20-sv-utils.md @@ -2,3 +2,230 @@ title: sv-utils --- +> [!NOTE] +> `@sveltejs/sv-utils` is currently **experimental**. The API may change. + +`@sveltejs/sv-utils` is an add-on utilty for parsing, transforming, and generating code.. + +```sh +npm install -D @sveltejs/sv-utils +``` + +## 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. + +Each transform injects relevant utilities into the callback, so you only need one import: + +```js +import { transforms } from '@sveltejs/sv-utils'; + +transforms.script(/* ... */); +transforms.svelte(/* ... */); +// ... +``` + +### `transforms.script` + +Transform a JavaScript/TypeScript file. The callback receives `{ ast, comments, content, js }`. + +```js +// @noErrors +import { transforms } from '@sveltejs/sv-utils'; + +sv.file( + file.viteConfig, + transforms.script(({ ast, js }) => { + js.imports.addDefault(ast, { as: 'foo', from: 'foo' }); + js.vite.addPlugin(ast, { code: 'foo()' }); + }) +); +``` + +### `transforms.svelte` + +Transform a Svelte component. The callback receives `{ ast, content, svelte, js }`. + +```js +// @noErrors +import { transforms } from '@sveltejs/sv-utils'; + +sv.file( + layoutPath, + transforms.svelte(({ ast, svelte }) => { + svelte.addFragment(ast, '