Skip to content

refactor: modular plugins #1145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Jul 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7cdc1df
wip: first steps
dominikg Jun 18, 2025
f02a174
wip: second day, getting a hang for it
dominikg Jun 18, 2025
bca41f2
fix: don't destructure api
dominikg Jun 19, 2025
94ac814
mostly working, but hacky raw/direct need work
dominikg Jun 22, 2025
01061c9
ensure hotUpdate returns non-svelte modules, cleanup
dominikg Jun 23, 2025
dc0019c
Merge branch 'main' into refactor/modular-plugins
dominikg Jun 23, 2025
fd8c291
Merge branch 'main' into refactor/modular-plugins
dominikg Jun 23, 2025
04904c2
fix: load raw&svelte files ourselves to prevent vite asset middleware…
dominikg Jun 24, 2025
04531a9
heureka
dominikg Jun 24, 2025
7150c4c
fixes for rolldown: remove duplicate optimizer setup and use svelte5 …
dominikg Jun 24, 2025
210ff7c
fix: add back .svelte optimizer extension, remove unused file
dominikg Jun 24, 2025
40b8dab
chore: remove unused files & code
dominikg Jun 24, 2025
d700ff3
chore: add changesets
dominikg Jun 24, 2025
dfbc8e4
chore: add test for preprocessor transform order
dominikg Jun 25, 2025
a4553e1
chore: fix rolldown-vite
dominikg Jun 25, 2025
e0fe69b
fix again
dominikg Jun 25, 2025
e8089d3
docs: add section about vite-plugin-transforms for preprocessing svelte
dominikg Jun 27, 2025
25ce39c
chore: deprecate plugin.api.sveltePreprocess
dominikg Jun 27, 2025
941db5b
refactor: remove advanced raw queries
dominikg Jun 27, 2025
3ba9fec
Merge branch 'main' into refactor/modular-plugins
dominikg Jun 30, 2025
2e1fd8c
improve inspector plugin
dominikg Jun 30, 2025
afe81cc
chore: improve regex and documentation for code normalization in hotU…
dominikg Jul 1, 2025
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: 5 additions & 0 deletions .changeset/fair-pets-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': major
---

Remove experimental "advanced raw queries" feature. Basic `File.svelte?raw` is still supported.
6 changes: 6 additions & 0 deletions .changeset/shiny-hats-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@sveltejs/vite-plugin-svelte-inspector': patch
'@sveltejs/vite-plugin-svelte': patch
---

use vite environment api internally
5 changes: 5 additions & 0 deletions .changeset/thirty-roses-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': major
---

split preprocess and compile into separate plugins
5 changes: 5 additions & 0 deletions .changeset/two-facts-sink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': patch
---

deprecate `plugin.api.sveltePreprocess`
144 changes: 44 additions & 100 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,50 @@
>
> **Proceed with caution!**

## transform svelte files with vite plugins

vite-plugin-svelte uses 2 Vite plugins `vite-plugin-svelte:preprocess` and `vite-plugin-svelte:compile` to preprocess and compile input.
The preprocess plugin uses `enforce: pre` but the compile plugin does not, giving you fine-grained control to add your own transforms

```js
function mySvelteTransform() {
const plugin = {
name: 'vite-plugin-my-svelte-transformer',
configResolved(c) {
// optional, use the exact same id filter as vite-plugin-svelte itself
const svelteIdFilter = c.plugins.find((p) => p.name === 'vite-plugin-svelte:config').api
.idFilter;
plugin.transform.filter.id = svelteIdFilter;
},
transform: {
// if you don't use vite-plugin-svelte's filter make sure to include your own here
filter: { id: /your id filter here/ },
async handler(code, id) {
const s = new MagicString(code);
// do your transforms with s
return {
code: s.toString(),
map: s.generateMap({ hires: 'boundary', includeContent: false })
};
}
}
};
// To add your transform in the correct place use `enforce` and `transform.order`

// before preprocess
plugin.enforce = 'pre';
plugin.transform.order = 'pre';

// after preprocess but before compile
plugin.transform.order = 'pre'; // leave plugin.enforce undefined

// after compile
plugin.transform.order = 'post'; // leave plugin.enforce undefined

return plugin;
}
```

## custom queries

Vite supports using query parameters to request different outcomes for the same file.
Expand All @@ -19,103 +63,3 @@ The following schemes are supported by vite-plugin-svelte:
//get .svelte file content as a string
import content from 'File.svelte?raw';
```

### experimental

In addition to the plain .svelte source content, you can use special svelte queries.

> These svelte subqueries are experimental, availability, syntax and output format may change

#### raw&svelte

```js
//get output of svelte.preprocess code as string
import preprocessed from 'File.svelte?raw&svelte&type=preprocessed';
```

```js
//get output of svelte.compile js.code as string
import script from 'File.svelte?raw&svelte&type=script';
```

```js
//get output of svelte.compile css.code as string
import style from 'File.svelte?raw&svelte&type=style';
```

##### detail exports

raw&svelte exports code string as default export, but also offers named exports if you need details

```js
//get output of svelte.preprocess
import { code, map, dependencies } from 'File.svelte?raw&svelte&type=preprocessed';
```

```js
//get output of svelte.compile js
import { code, map, dependencies } from 'File.svelte?raw&svelte&type=script';
```

```js
//get output of svelte.compile css
import { code, map, dependencies } from 'File.svelte?raw&svelte&type=style';
```

```js
//get everything in one go
import * as all from 'File.svelte?raw&svelte&type=all';
import {
source,
preprocessed,
dependencies,
js,
css,
ast,
normalizedFilename,
ssr,
lang,
warnings,
stats
} from 'File.svelte?raw&svelte&type=all';
```

#### direct&svelte

```html
<!-- load and execute component script -->
<script type="application/javascript" src="File.svelte?direct&svelte&type=script&lang.js" />
<!-- embed component style as css -->
<link rel="stylesheet" type="text/css" href="File.svelte?direct&svelte&type=style&lang.css" />
```

#### sourcemap

add `&sourcemap` to `?(raw|direct)&svelte&type=(script|style|all)` queries to include sourcemaps (inline for direct)

#### compilerOptions

?raw and ?direct use default compilerOptions, even if you have different values in your svelte.config.js:

```js
const compilerOptions = {
dev: false,
generate: 'client',
css: 'external'
};
```

to get output with different compilerOptions, append them as json like this:

```js
//get ssr output of svelte.compile js as {code, map, dependencies}
import script from 'File.svelte?raw&svelte&type=script&compilerOptions={"generate":"server"}';
```

only a subset of compilerOptions is supported

- generate
- dev
- css
- customElement
- immutable
14 changes: 1 addition & 13 deletions docs/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,19 +82,7 @@ However, `cssHash` is respected in production builds as HMR is a dev-only featur

### How do I add a Svelte preprocessor from a Vite plugin?

If you are building a Vite plugin that transforms CSS or JS, you can add a `api.sveltePreprocess: PreprocessorGroup` to your Vite plugin definition and it will be added to the list of Svelte preprocessors used at runtime.

```js
const vitePluginCoolCss = {
name: 'vite-plugin-coolcss',
api: {
sveltePreprocess: {
/* your PreprocessorGroup here */
}
}
/*... your cool css plugin implementation here .. */
};
```
You don't have to anymore. See [advanced usage](advanced-usage.md) for examples how to put transform hooks before or after Svelte preprocess or compile

### What is going on with Vite and `Pre-bundling dependencies:`?

Expand Down
8 changes: 2 additions & 6 deletions packages/e2e-tests/autoprefixer-browerslist/src/main.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import App from './App.svelte';

if (App.toString().startsWith('class ')) {
new App({ target: document.body });
} else {
import('svelte').then(({ mount }) => mount(App, { target: document.body }));
}
import { mount } from 'svelte';
mount(App, { target: document.body });
2 changes: 2 additions & 0 deletions packages/e2e-tests/configfile-custom/svelte.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
console.log('default svelte config loaded');
export default {};

This file was deleted.

This file was deleted.

This file was deleted.

135 changes: 1 addition & 134 deletions packages/e2e-tests/import-queries/__tests__/import-queries.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { browserLogs, fetchFromPage, getText, isBuild, testDir } from '~utils';
import { browserLogs, getText, isBuild, testDir } from '~utils';
import { createServer, ViteDevServer } from 'vite';
import { VERSION } from 'svelte/compiler';

Expand All @@ -22,114 +22,6 @@ describe('raw', () => {
const result = await getText('#raw');
await expect(result).toMatchFileSnapshot(snapshotFilename('raw'));
});

test('Dummy.svelte?raw&svelte&type=preprocessed', async () => {
const result = await getText('#preprocessed');
await expect(result).toMatchFileSnapshot(snapshotFilename('preprocessed'));
});

test('Dummy.svelte?raw&svelte&type=script', async () => {
const result = await getText('#script');
expect(result).toContain('export default function Dummy');
});

test('Dummy.svelte?raw&svelte&type=script&compilerOptions={"customElement":true}', async () => {
const result = await getText('#wcScript');
expect(result).toContain('$.create_custom_element(Dummy,');
});

test('Dummy.svelte?raw&svelte&type=style', async () => {
const result = await getText('#style');
await expect(result).toMatchFileSnapshot(snapshotFilename('style'));
});

test('Dummy.svelte?raw&svelte&type=all&sourcemap', async () => {
const result = JSON.parse(await getText('#all'));
expect(result.ast).toBeDefined();
expect(result.js).toBeDefined();
expect(result.js.code).toBeDefined();
expect(result.js.map).toBeDefined();
expect(result.css).toBeDefined();
expect(result.css.code).toBeDefined();
expect(result.css.map).toBeDefined();
expect(result.preprocessed).toBeDefined();
expect(result.preprocessed.code).toBeDefined();
expect(result.preprocessed.map).toBeDefined();
});

describe.runIf(!isBuild)('mixed exports', () => {
test('Dummy.svelte?raw&svelte&type=preprocessed', async () => {
const module = await fetchFromPage('src/Dummy.svelte?raw&svelte&type=preprocessed').then(
(res) => res.text()
);
expect(module).toContain('export const code="<script lang=\\"ts\\">');
expect(module).toContain('export const map={');
expect(module).toContain('export const dependencies=[]');
expect(module).toContain('export default code');
});
test('Dummy.svelte?raw&svelte&type=style', async () => {
const module = await fetchFromPage('src/Dummy.svelte?raw&svelte&type=style').then((res) =>
res.text()
);
expect(module).toContain('export const code="button.');
expect(module).toContain('export const hasGlobal=false');
expect(module).toContain('export const map={');
expect(module).toContain('export default code');
});
test('Dummy.svelte?raw&svelte&type=script', async () => {
const module = await fetchFromPage('src/Dummy.svelte?raw&svelte&type=script').then((res) =>
res.text()
);
expect(module).toContain('export const code="import');
expect(module).toContain('export const map={');
expect(module).toContain('export default code');
});
test('Dummy.svelte?raw&svelte&type=all', async () => {
const module = await fetchFromPage('src/Dummy.svelte?raw&svelte&type=all').then((res) =>
res.text()
);
expect(module).toContain('export const ast={"html":');
expect(module).toContain('export const css={"code":"button');
expect(module).toContain('export const dependencies=[]');
expect(module).toContain('export const js={"code":"import ');
expect(module).toContain('export const lang="ts"');
expect(module).toContain('export const metadata={"runes":false}');
expect(module).toContain('export const normalizedFilename="/src/Dummy.svelte"');
expect(module).toContain('export const preprocessed={"code":"<script lang=\\"ts\\">');
expect(module).toContain('export const source="<script lang=\\"ts\\">');
expect(module).toContain('export const ssr=false');
expect(module).toContain('export const warnings=[]');
});
});
});

describe.runIf(!isBuild)('direct', () => {
test('Dummy.svelte?direct&svelte&type=style&sourcemap&lang.css', async () => {
const response = await fetchFromPage(
'src/Dummy.svelte?direct&svelte&type=style&sourcemap&lang.css',
{
headers: { Accept: 'text/css' }
}
);
expect(response.ok).toBe(true);
expect(response.headers.get('Content-Type')).toBe('text/css');
const css = await response.text();
expect(css).toContain('button.');
expect(css).toContain('/*# sourceMappingURL=data');
});
test('Dummy.svelte?direct&svelte&type=script&sourcemap&lang.js', async () => {
const response = await fetchFromPage(
'src/Dummy.svelte?direct&svelte&type=script&sourcemap&lang.js',
{
headers: { Accept: 'text/javascript' }
}
);
expect(response.ok).toBe(true);
expect(response.headers.get('Content-Type')).toMatch(/^(?:text|application)\/javascript$/);
const js = await response.text();
expect(js).toContain('export default function Dummy');
expect(js).toContain('//# sourceMappingURL=data');
});
});

describe.runIf(!isBuild)('ssrLoadModule', () => {
Expand Down Expand Up @@ -158,29 +50,4 @@ describe.runIf(!isBuild)('ssrLoadModule', () => {
const result = await ssrLoadDummy('?raw');
await expect(result).toMatchFileSnapshot(snapshotFilename('ssr-raw'));
});
test('?raw&svelte&type=preprocessed', async () => {
const result = await ssrLoadDummy('?raw&svelte&type=preprocessed');
await expect(result).toMatchFileSnapshot(snapshotFilename('ssr-preprocessed'));
});
test('?raw&svelte&type=script', async () => {
const result = await ssrLoadDummy('?raw&svelte&type=script');
expect(result).toContain('export default function Dummy');
});
test('?raw&svelte&type=script&compilerOptions={"customElement":true}', async () => {
const result = await ssrLoadDummy(
'?raw&svelte&type=script&compilerOptions={"customElement":true}'
);
expect(result).toContain('$.create_custom_element(Dummy,');
});
test('?raw&svelte&type=style', async () => {
const result = await ssrLoadDummy('?raw&svelte&type=style');
expect(result).toContain('button.');
});
test('?inline&svelte&type=style&lang.css', async () => {
// Preload Dummy.svelte first so its CSS is processed in the module graph, otherwise loading
// its css inlined url directly will return the raw svelte file rather than the style
await ssrLoadDummy('');
const result = await ssrLoadDummy('?inline&svelte&type=style&lang.css');
expect(result).toContain('button.');
});
});
Loading