Skip to content

fix: ensure compiled css is available after reload during dev with SSR (SvelteKit) #1194

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 4 commits into from
Aug 12, 2025
Merged
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
5 changes: 5 additions & 0 deletions .changeset/fuzzy-hairs-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/vite-plugin-svelte': patch
---

fix: ensure compiled css is returned when reloading during dev with ssr (e.g. SvelteKit)
14 changes: 2 additions & 12 deletions packages/e2e-tests/build-watch/__tests__/build-watch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
sleep,
getColor,
browserLogs,
e2eServer
e2eServer,
getWatchErrors
} from '~utils';

import * as vite from 'vite';
Expand Down Expand Up @@ -63,17 +64,6 @@ describe.runIf(isBuildWatch)('build-watch', () => {
const updateApp = editFileAndWaitForBuildWatchComplete.bind(null, 'src/App.svelte');
const updateStore = editFileAndWaitForBuildWatchComplete.bind(null, 'src/stores/hmr-stores.js');

const getWatchErrors = () =>
isRolldownVite
? e2eServer.logs.watch.err.filter(
(m) =>
![
'Support for rolldown-vite in vite-plugin-svelte is experimental',
'See https://github.com/sveltejs/vite-plugin-svelte/issues/1143'
].some((s) => m.includes(s))
)
: e2eServer.logs.watch.err;

test('should have expected initial state', async () => {
// initial state, both counters 0, both labels red
expect(await getText('#hmr-test-1 .counter')).toBe('0');
Expand Down
14 changes: 13 additions & 1 deletion packages/e2e-tests/kit-node/__tests__/kit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
fetchPageText,
reloadPage,
readFileContent,
IS_SVELTE_BASELINE
IS_SVELTE_BASELINE,
getServerErrors
} from '~utils';

import glob from 'tiny-glob';
Expand Down Expand Up @@ -95,6 +96,11 @@ describe('kit-node', () => {
expect(browserLogs.some((x) => x === 'onMount dynamic imported isSSR: false')).toBe(true);
});

it('should load dynamic import with css', async () => {
expect(await getText('#dynamic-imported')).toBe("i'm blue");
expect(await getColor('#dynamic-imported')).toBe('blue');
});

it('should respect transforms', async () => {
expect(await getText('#js-transform')).toBe('Hello world');
expect(await getColor('#css-transform')).toBe('red');
Expand Down Expand Up @@ -192,9 +198,15 @@ describe('kit-node', () => {
it('should serve changes even after page reload', async () => {
expect(await getColor('h1')).toBe('green');
expect(await getText('#hmr-test2')).toBe('bar');
expect(await getText('#dynamic-imported')).toBe("i'm blue");
expect(await getColor('#dynamic-imported')).toBe('blue');
await reloadPage();
expect(await getColor('h1')).toBe('green');
expect(await getText('#hmr-test2')).toBe('bar');
await page.waitForSelector('#dynamic-imported', { strict: true });
expect(await getText('#dynamic-imported')).toBe("i'm blue");
expect(await getColor('#dynamic-imported')).toBe('blue');
expect(getServerErrors(), 'error log of `vite dev` is not empty after reload').toEqual([]);
});

describe('child component update', () => {
Expand Down
7 changes: 4 additions & 3 deletions packages/e2e-tests/kit-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"dev": "svelte-kit sync && vite dev",
"build": "svelte-kit sync && vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"sync": "svelte-kit sync || echo ''",
Expand All @@ -18,8 +18,9 @@
"@sveltejs/package": "^2.4.1",
"@sveltejs/vite-plugin-svelte": "workspace:^",
"e2e-test-dep-svelte-api-only": "file:../_test_dependencies/svelte-api-only",
"e2e-test-dep-vite-plugins": "file:../_test_dependencies/vite-plugins",
"e2e-test-dep-svelte-nested-workspace-devdep": "../_test_dependencies/svelte-nested-workspace-devdep",
"e2e-test-dep-vite-plugins": "file:../_test_dependencies/vite-plugins",
"sass": "^1.90.0",
"svelte": "^5.38.0",
"svelte-check": "^4.3.1",
"svelte-i18n": "^4.0.1",
Expand Down
9 changes: 9 additions & 0 deletions packages/e2e-tests/kit-node/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import Counter from '$lib/Counter.svelte';
import Child from '$lib/Child.svelte';
import { setSomeContext } from 'e2e-test-dep-svelte-api-only';
import { browser } from '$app/environment';

export let data = {};
$: load_status = data?.load_status ?? 'NOT_LOADED';
const jsTransform = '__JS_TRANSFORM_1__';
Expand Down Expand Up @@ -38,6 +40,13 @@
<p id="js-transform">{jsTransform}</p>
<!-- to be transformed into "hello-world" class -->
<p id="css-transform">Hello world</p>
{#if browser}
{#await import('./DynamicImported.svelte').then((m) => m.default)}
loading...
{:then DynamicImported}
<DynamicImported />
{/await}
{/if}
</main>

<!-- HMR-TEMPLATE-INJECT -->
Expand Down
7 changes: 7 additions & 0 deletions packages/e2e-tests/kit-node/src/routes/DynamicImported.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<div id="dynamic-imported">i'm blue</div>

<style lang="postcss">
#dynamic-imported {
color: blue;
}
</style>
3 changes: 3 additions & 0 deletions packages/e2e-tests/kit-node/svelte.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import node from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: node()
}
Expand Down
5 changes: 5 additions & 0 deletions packages/e2e-tests/kit-node/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,10 @@ export default {
optimizeDeps: {
// eagerly include these, otherwise vite optimizer might interfere with restarting while the test is running
include: ['svelte-i18n', 'e2e-test-dep-svelte-api-only']
},
css: {
postcss: {
plugins: [{ postcssPlugin: 'noop' }]
}
}
};
29 changes: 29 additions & 0 deletions packages/e2e-tests/testUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import {
waitForViteConnect
} from './vitestSetup.js';

import * as vite from 'vite';
//@ts-ignore
const isRolldownVite = !!vite.rolldownVersion;

import { VERSION } from 'svelte/compiler';

export const IS_SVELTE_BASELINE = VERSION === '5.0.0';
Expand Down Expand Up @@ -354,3 +358,28 @@ export function readVitePrebundleMetadata() {
}
throw new Error('Unable to find vite prebundle metadata');
}

export function getServerErrors() {
return filterMessages(e2eServer.logs.server.err);
}

export function getWatchErrors() {
return filterMessages(e2eServer.logs.watch.err);
}
function filterMessages(arr) {
if (arr.length === 0) {
return arr;
}
const excludes = [];
if (isRolldownVite) {
excludes.push(
'Support for rolldown-vite in vite-plugin-svelte is experimental',
'See https://github.com/sveltejs/vite-plugin-svelte/issues/1143'
);
}
if (excludes.length > 0) {
return arr.filter((m) => !excludes.some((e) => m.includes(e)));
} else {
return arr;
}
}
11 changes: 7 additions & 4 deletions packages/vite-plugin-svelte/src/plugins/load-compiled-css.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ const filter = { id: SVELTE_VIRTUAL_STYLE_ID_REGEX };
* @returns {import('vite').Plugin}
*/
export function loadCompiledCss(api) {
let isBuildWatch = false;
let useLocalCache = false;

/** @type{Map<string,any>} */
const buildWatchCssCache = new Map();
return {
name: 'vite-plugin-svelte:load-compiled-css',

configResolved(c) {
isBuildWatch = !!c.build?.watch;
const isDev = c.command === 'serve';
const isBuildWatch = !!c.build?.watch;
useLocalCache = isDev || isBuildWatch;
},

resolveId: {
Expand All @@ -34,9 +37,9 @@ export function loadCompiledCss(api) {
return;
}
let cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
// in build --watch getModuleInfo only returns changed module data.
// in `build --watch` or dev ssr reloads getModuleInfo only returns changed module data.
// To ensure virtual css is loaded unchanged, we cache it here separately
if (isBuildWatch) {
if (useLocalCache) {
if (cachedCss) {
buildWatchCssCache.set(svelteRequest.filename, cachedCss);
} else {
Expand Down
15 changes: 5 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading