diff --git a/.changeset/plenty-eyes-talk.md b/.changeset/plenty-eyes-talk.md
new file mode 100644
index 000000000..1bb7c3946
--- /dev/null
+++ b/.changeset/plenty-eyes-talk.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/vite-plugin-svelte': minor
+---
+
+scope css to js module to enable treeshaking scoped css from unused components. Requires vite 6.2 and svelte 5.26
diff --git a/packages/e2e-tests/css-treeshake/__tests__/css-treeshake.spec.ts b/packages/e2e-tests/css-treeshake/__tests__/css-treeshake.spec.ts
new file mode 100644
index 000000000..d9edac1c8
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/__tests__/css-treeshake.spec.ts
@@ -0,0 +1,37 @@
+import { browserLogs, findAssetFile, getColor, getEl, getText, isBuild } from '~utils';
+import { expect } from 'vitest';
+
+test('should not have failed requests', async () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404');
+ });
+});
+
+test('should apply css from used components', async () => {
+ expect(await getText('#app')).toBe('App');
+ expect(await getColor('#app')).toBe('blue');
+ expect(await getText('#a')).toBe('A');
+ expect(await getColor('#a')).toBe('red');
+});
+
+test('should apply css from unused components that contain global styles', async () => {
+ expect(await getEl('head style[src]'));
+ expect(await getColor('#test')).toBe('green'); // from B.svelte
+});
+
+test('should not render unused components', async () => {
+ expect(await getEl('#b')).toBeNull();
+ expect(await getEl('#c')).toBeNull();
+});
+
+if (isBuild) {
+ test('should include unscoped global styles from unused components', async () => {
+ const cssOutput = findAssetFile(/index-.*\.css/);
+ expect(cssOutput).toContain('#test{color:green}'); // from B.svelte
+ });
+ test('should not include scoped styles from unused components', async () => {
+ const cssOutput = findAssetFile(/index-.*\.css/);
+ // from C.svelte
+ expect(cssOutput).not.toContain('.unused');
+ });
+}
diff --git a/packages/e2e-tests/css-treeshake/index.html b/packages/e2e-tests/css-treeshake/index.html
new file mode 100644
index 000000000..5ec38e6d2
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Svelte app
+
+
+
+
+
+
diff --git a/packages/e2e-tests/css-treeshake/package.json b/packages/e2e-tests/css-treeshake/package.json
new file mode 100644
index 000000000..7119507d4
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "e2e-tests-css-treeshake",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "workspace:^",
+ "sass": "^1.85.1",
+ "svelte": "^5.20.5",
+ "vite": "^6.2.0"
+ }
+}
diff --git a/packages/e2e-tests/css-treeshake/src/A.svelte b/packages/e2e-tests/css-treeshake/src/A.svelte
new file mode 100644
index 000000000..ef94d98f3
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/A.svelte
@@ -0,0 +1,7 @@
+A
+
+
diff --git a/packages/e2e-tests/css-treeshake/src/App.svelte b/packages/e2e-tests/css-treeshake/src/App.svelte
new file mode 100644
index 000000000..ca9ca9a52
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/App.svelte
@@ -0,0 +1,13 @@
+
+
+test
+App
+
+
+
diff --git a/packages/e2e-tests/css-treeshake/src/B.svelte b/packages/e2e-tests/css-treeshake/src/B.svelte
new file mode 100644
index 000000000..26f088447
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/B.svelte
@@ -0,0 +1,10 @@
+B
+
+
diff --git a/packages/e2e-tests/css-treeshake/src/C.svelte b/packages/e2e-tests/css-treeshake/src/C.svelte
new file mode 100644
index 000000000..b452d6408
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/C.svelte
@@ -0,0 +1,14 @@
+C
+
+
diff --git a/packages/e2e-tests/css-treeshake/src/barrel.js b/packages/e2e-tests/css-treeshake/src/barrel.js
new file mode 100644
index 000000000..718d05e83
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/barrel.js
@@ -0,0 +1,4 @@
+export { default as A } from './A.svelte';
+// B and C are unused, their css should not be included
+export { default as B } from './B.svelte';
+export { default as C } from './C.svelte';
diff --git a/packages/e2e-tests/css-treeshake/src/main.js b/packages/e2e-tests/css-treeshake/src/main.js
new file mode 100644
index 000000000..071c75dc7
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/main.js
@@ -0,0 +1,3 @@
+import App from './App.svelte';
+import { mount } from 'svelte';
+mount(App, { target: document.body });
diff --git a/packages/e2e-tests/css-treeshake/src/vite-env.d.ts b/packages/e2e-tests/css-treeshake/src/vite-env.d.ts
new file mode 100644
index 000000000..4078e7476
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/src/vite-env.d.ts
@@ -0,0 +1,2 @@
+///
+///
diff --git a/packages/e2e-tests/css-treeshake/svelte.config.js b/packages/e2e-tests/css-treeshake/svelte.config.js
new file mode 100644
index 000000000..76bab5483
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/svelte.config.js
@@ -0,0 +1,5 @@
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+export default {
+ preprocess: [vitePreprocess()]
+};
diff --git a/packages/e2e-tests/css-treeshake/vite.config.js b/packages/e2e-tests/css-treeshake/vite.config.js
new file mode 100644
index 000000000..6f9c7da64
--- /dev/null
+++ b/packages/e2e-tests/css-treeshake/vite.config.js
@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { env } from 'node:process';
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [svelte()]
+});
diff --git a/packages/vite-plugin-svelte/src/index.js b/packages/vite-plugin-svelte/src/index.js
index 9ec43e9ec..aed46b80c 100644
--- a/packages/vite-plugin-svelte/src/index.js
+++ b/packages/vite-plugin-svelte/src/index.js
@@ -118,8 +118,15 @@ export function svelte(inlineOptions) {
};
} else {
if (query.svelte && query.type === 'style') {
- const css = cache.getCSS(svelteRequest);
- if (css) {
+ const cachedCss = cache.getCSS(svelteRequest);
+ if (cachedCss) {
+ const { hasGlobal, ...css } = cachedCss;
+ if (hasGlobal === false) {
+ // hasGlobal was added in svelte 5.26.0, so make sure it is boolean false
+ css.meta ??= {};
+ css.meta.vite ??= {};
+ css.meta.vite.cssScopeTo = [svelteRequest.filename, 'default'];
+ }
return css;
}
}
diff --git a/packages/vite-plugin-svelte/src/types/compile.d.ts b/packages/vite-plugin-svelte/src/types/compile.d.ts
index d6ba48e0f..1f28fcfc1 100644
--- a/packages/vite-plugin-svelte/src/types/compile.d.ts
+++ b/packages/vite-plugin-svelte/src/types/compile.d.ts
@@ -1,6 +1,7 @@
import type { Processed, CompileResult } from 'svelte/compiler';
import type { SvelteRequest } from './id.d.ts';
import type { ResolvedOptions } from './options.d.ts';
+import type { CustomPluginOptionsVite } from 'vite';
export type CompileSvelte = (
svelteRequest: SvelteRequest,
@@ -12,6 +13,10 @@ export interface Code {
code: string;
map?: any;
dependencies?: any[];
+ hasGlobal?: boolean;
+ meta?: {
+ vite?: CustomPluginOptionsVite;
+ };
}
export interface CompileData {
diff --git a/packages/vite-plugin-svelte/src/utils/compile.js b/packages/vite-plugin-svelte/src/utils/compile.js
index 4c0e524c9..f7999fe72 100644
--- a/packages/vite-plugin-svelte/src/utils/compile.js
+++ b/packages/vite-plugin-svelte/src/utils/compile.js
@@ -1,5 +1,4 @@
import * as svelte from 'svelte/compiler';
-
import { safeBase64Hash } from './hash.js';
import { log } from './log.js';
@@ -133,6 +132,7 @@ export function createCompileSvelte() {
let compiled;
try {
compiled = svelte.compile(finalCode, { ...finalCompileOptions, filename });
+
// patch output with partial accept until svelte does it
// TODO remove later
if (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5cade5757..787f30008 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -262,6 +262,21 @@ importers:
specifier: ^6.3.2
version: 6.3.2(@types/node@20.17.30)(sass@1.86.3)(stylus@0.64.0)(yaml@2.7.0)
+ packages/e2e-tests/css-treeshake:
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: workspace:^
+ version: link:../../vite-plugin-svelte
+ sass:
+ specifier: ^1.85.1
+ version: 1.86.3
+ svelte:
+ specifier: ^5.28.1
+ version: 5.28.1
+ vite:
+ specifier: ^6.3.2
+ version: 6.3.2(@types/node@20.17.30)(sass@1.86.3)(stylus@0.64.0)(yaml@2.7.0)
+
packages/e2e-tests/custom-extensions:
devDependencies:
'@sveltejs/vite-plugin-svelte':