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':