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 docs/src/lib/components/Example.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import LucideFilePen from '~icons/lucide/file-pen';
import LucideGripVertical from '~icons/lucide/grip-vertical';
import LucideDownload from '~icons/lucide/download';
import LucideTerminal from '~icons/lucide/terminal';

import { page } from '$app/state';
import { openInStackBlitz } from '$lib/utils/stackblitz.svelte';
Expand Down Expand Up @@ -306,6 +307,20 @@
>
Edit
</Button>

<Tooltip title="Copy: npx sv add @layerchart/sv=demo:components/{component}/{name}">
<Button
icon={LucideTerminal}
class="text-surface-content/70 py-1"
on:click={() => {
navigator.clipboard.writeText(
`npx sv add @layerchart/sv=demo:components/${component}/${name}`
);
}}
>
sv add
</Button>
</Tooltip>
{/if}

<Toggle let:on={open} let:toggle let:toggleOff>
Expand Down
27 changes: 27 additions & 0 deletions packages/sv/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
node_modules
demo/
.test-output/

# Output
.output
.vercel
.netlify
.wrangler
/.svelte-kit
/build
/dist

# OS
.DS_Store
Thumbs.db

# Env
.env
.env.*
!.env.example
!.env.test

# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

48 changes: 48 additions & 0 deletions packages/sv/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Contributing Guide

Cheatsheet: [All official add-ons source code](https://github.com/sveltejs/cli/tree/main/packages/sv/src/addons)

---

Some convenient scripts are provided to help develop the add-on.

```sh
## create a new minimal project in the `demo` directory
npm run demo-create

## add your current add-on to the demo project
npm run demo-add

## run the tests
npm run test
```

## Key things to note

Your `add-on` should:

- export a function that returns a `defineAddon` object.
- have a `package.json` with an `exports` field that points to the main entry point of the add-on.

## Building

Your add-on is bundled with [tsdown](https://tsdown.dev/) into a single file in `dist/`. This bundles everything except `sv` (which is a peer dependency provided at runtime).

```sh
npm run build
```

## Publishing

When you're ready to publish your add-on to npm:

```sh
npm login
npm publish
```

> `prepublishOnly` will automatically run the build before publishing.

## Things to be aware of

Community add-ons must have `sv` as a `peerDependency` and should **not** have any `dependencies`. Everything else (including `@sveltejs/sv-utils`) is bundled at build time by tsdown.
29 changes: 29 additions & 0 deletions packages/sv/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# [sv](https://svelte.dev/docs/cli/overview) community add-on: [@layerchart/sv](https://github.com/@layerchart/sv)

> [!IMPORTANT]
> Svelte maintainers have not reviewed community add-ons for malicious code. Use at your discretion

## Usage

To install the add-on, run:

```shell
npx sv add @layerchart
```

## What you get [TO BE FILLED...]

- A super cool stuff
- Another one!

## Options [TO BE FILLED...]

### `who`

The name of the person to say hello to.

Default: `you`

```shell
npx sv add @layerchart="who:your-name"
```
9 changes: 9 additions & 0 deletions packages/sv/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"checkJs": true,
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
42 changes: 42 additions & 0 deletions packages/sv/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@layerchart/sv",
"description": "sv add-on for @layerchart/sv",
"version": "0.0.1",
"type": "module",
"license": "MIT",
"scripts": {
"demo-create": "sv create demo --types ts --template minimal --no-add-ons --no-install",
"demo-add": "pnpm build && sv add file:../ --cwd demo --no-git-check --no-install",
"demo-add:ci": "pnpm build && sv add file:../=demo:components/ArcChart/gradient-with-text --cwd demo --no-git-check --no-download-check --no-install",
"build": "tsdown",
"prepublishOnly": "npm run build",
"test": "vitest run"
},
"files": [
"dist"
],
"exports": {
".": {
"default": "./dist/index.mjs"
}
},
"publishConfig": {
"access": "public"
},
"peerDependencies": {
"sv": ">=0.13.1"
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@sveltejs/sv-utils": "latest",
"@types/node": "^25.2.1",
"sv": "latest",
"tsdown": "^0.21.4",
"vitest": "^4.1.0"
},
"keywords": [
"sv-add",
"svelte",
"sveltekit"
]
}
114 changes: 114 additions & 0 deletions packages/sv/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { transforms } from '@sveltejs/sv-utils';
import { defineAddon, defineAddonOptions } from 'sv';

const GITHUB_RAW_BASE =
'https://raw.githubusercontent.com/techniq/layerchart/next/docs/src/examples';

const options = defineAddonOptions()
.add('demo', {
question: 'Which demo? (e.g. components/ArcChart/gradient-with-text)',
type: 'string',
default: 'components/ArcChart/gradient-with-text'
})
.build();

/**
* Parse import statements from svelte/ts source code.
* Returns a map of package name -> set of imported identifiers.
*/
function parseImports(/** @type {string} */ source) {
/** @type {Map<string, Set<string>>} */
const imports = new Map();

// Match: import { X, Y } from 'package'
// Match: import X from 'package'
const importRegex = /import\s+(?:\{([^}]+)\}|(\w+))\s+from\s+['"]([^'"]+)['"]/g;
let match;
while ((match = importRegex.exec(source)) !== null) {
const pkg = match[3];
// Skip relative/alias imports
if (pkg.startsWith('.') || pkg.startsWith('$')) continue;

if (!imports.has(pkg)) {
imports.set(pkg, new Set());
}
/** @type {Set<string>} */
const names = /** @type {Set<string>} */ (imports.get(pkg));
if (match[1]) {
// Named imports
match[1].split(',').forEach((n) => names.add(n.trim()));
} else if (match[2]) {
// Default import
names.add(match[2]);
}
}

return imports;
}

/**
* Fetch a demo example from the layerchart GitHub repo.
* @param {string} demo - Path like "components/ArcChart/gradient-with-text"
* @returns {Promise<string>} The source code
*/
async function fetchDemo(demo) {
const url = `${GITHUB_RAW_BASE}/${demo}.svelte`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch demo "${demo}" from ${url} (${res.status})`);
}
return res.text();
}

export default defineAddon({
id: '@layerchart/sv',
options,

setup: ({ isKit, unsupported }) => {
if (!isKit) unsupported('Requires SvelteKit');
},

run: async ({ directory, sv, options }) => {
const demo = options.demo;

// Fetch the demo source
const source = await fetchDemo(demo);

// Parse imports to determine dependencies
const imports = parseImports(source);

// Install detected packages (examples are from layerchart v2/next)
for (const [pkg] of imports) {
sv.dependency(pkg, pkg === 'layerchart' ? 'next' : 'latest');
}

// Always ensure layerchart is installed
if (!imports.has('layerchart')) {
sv.dependency('layerchart', 'next');
}

// Extract the demo name for the file path
// e.g. "components/ArcChart/gradient-with-text" -> "ArcChart/gradient-with-text"
const parts = demo.split('/');
const demoName = parts.slice(1).join('/');

// Write the demo component
sv.file(
`${directory.lib}/layerchart/demos/${demoName}.svelte`,
transforms.text(() => source)
);

// Add a route that renders the demo
sv.file(
directory.kitRoutes + '/+page.svelte',
transforms.svelteScript({ language: 'ts' }, ({ ast, svelte, js }) => {
js.imports.addDefault(ast.instance.content, {
as: 'Demo',
from: `$lib/layerchart/demos/${demoName}.svelte`
});

svelte.addFragment(ast, '<Demo />');
})
);
}
});
52 changes: 52 additions & 0 deletions packages/sv/tests/addon.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { expect } from '@playwright/test';
import fs from 'node:fs';
import path from 'node:path';
import addon from '../src/index.js';
import { setupTest } from './setup/suite.js';

// set to true to enable browser testing
const browser = false;

const { test, prepareServer, testCases } = setupTest(
{ addon },
{
kinds: [
{
type: 'default',
options: { '@layerchart/sv': { demo: 'components/ArcChart/gradient-with-text' } }
}
],
filter: (testCase) => testCase.variant.includes('kit'),
browser
}
);

test.concurrent.for(testCases)(
'@layerchart/sv $kind.type $variant',
async (testCase, { page, ...ctx }) => {
const cwd = ctx.cwd(testCase);

// Check demo component was created
const demoPath = path.resolve(cwd, 'src/lib/layerchart/demos/ArcChart/gradient-with-text.svelte');
const demoContent = fs.readFileSync(demoPath, 'utf8');
expect(demoContent).toContain('layerchart');
expect(demoContent).toContain('ArcChart');

// Check route was updated to import the demo
const routePath = path.resolve(cwd, 'src/routes/+page.svelte');
const routeContent = fs.readFileSync(routePath, 'utf8');
expect(routeContent).toContain('Demo');
expect(routeContent).toContain('$lib/layerchart/demos/ArcChart/gradient-with-text.svelte');

// Check package.json has layerchart dependency
const pkgPath = path.resolve(cwd, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
expect(pkg.dependencies?.layerchart || pkg.devDependencies?.layerchart).toBeTruthy();

// For browser testing
if (browser) {
const { close } = await prepareServer({ cwd, page });
ctx.onTestFinished(async () => await close());
}
}
);
14 changes: 14 additions & 0 deletions packages/sv/tests/setup/global.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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,
pre: async () => {
// global setup (e.g. spin up docker containers)
},
post: async () => {
// tear down... (e.g. cleanup docker containers)
}
});
4 changes: 4 additions & 0 deletions packages/sv/tests/setup/suite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { createSetupTest } from 'sv/testing';
import * as vitest from 'vitest';

export const setupTest = createSetupTest(vitest);
6 changes: 6 additions & 0 deletions packages/sv/tsdown.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'tsdown';

export default defineConfig({
entry: ['src/index.js'],
format: 'esm'
});
Loading
Loading