diff --git a/.changeset/spotty-mirrors-pick.md b/.changeset/spotty-mirrors-pick.md
new file mode 100644
index 000000000..58de19bb6
--- /dev/null
+++ b/.changeset/spotty-mirrors-pick.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/vite-plugin-svelte': patch
+---
+
+fix: ensure compiled svelte css is loaded correctly when rebuilding in `build --watch`
diff --git a/.gitignore b/.gitignore
index c5eb7a2ad..a2f4e6129 100644
--- a/.gitignore
+++ b/.gitignore
@@ -52,3 +52,5 @@ coverage
packages/playground/**/*
!packages/playground/README.md
+# vite plugin inspect
+.vite-inspect
diff --git a/.prettierrc.js b/.prettierrc.js
index 044917ed6..8d1916460 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -25,7 +25,8 @@ export default {
'**/vite.config.js.timestamp-*.mjs',
'packages/e2e-tests/dynamic-compile-options/src/components/A.svelte',
'packages/playground/big/src/pages/**', // lots of generated files
- 'packages/e2e-tests/scan-deps/src/Svelte*.svelte' // various syntax tests that require no format
+ 'packages/e2e-tests/scan-deps/src/Svelte*.svelte', // various syntax tests that require no format
+ '**/.vite-inspect/**'
],
options: {
rangeEnd: 0
diff --git a/eslint.config.js b/eslint.config.js
index f5b840710..3064952de 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -15,7 +15,8 @@ export default [
'packages/*/types/index.d.ts',
'packages/*/types/index.d.ts.map',
'packages/*/CHANGELOG.md',
- 'packages/e2e-tests/**/logs/**'
+ 'packages/e2e-tests/**/logs/**',
+ '**/.vite-inspect/**'
]
},
...svelteOrgEslintConfig, // contains setup for svelte and typescript
diff --git a/package.json b/package.json
index c5b6c4c36..f07451283 100644
--- a/package.json
+++ b/package.json
@@ -3,10 +3,11 @@
"private": true,
"type": "module",
"scripts": {
- "test": "run-s -c test:unit \"test:build {@}\" \"test:serve {@}\" --",
+ "test": "run-s -c test:unit \"test:build {@}\" \"test:serve {@}\" \"test:build:watch {@}\" --",
"test:unit": "vitest run",
"test:serve": "vitest run -c vitest.config.e2e.ts",
"test:build": "cross-env TEST_BUILD=1 vitest run -c vitest.config.e2e.ts",
+ "test:build:watch": "cross-env TEST_BUILD_WATCH=1 vitest run -c vitest.config.e2e.ts",
"check": "run-p -c check:*",
"check:audit": "pnpm audit --prod",
"check:publint": "pnpm --filter \"./packages/*\" --parallel check:publint",
@@ -78,7 +79,8 @@
"vite": "$vite",
"@types/node@<=20.12.0": "20.19.9",
"send@<0.19.0": "^0.19.1",
- "@sveltejs/kit>cookie@<0.7.0": "^0.7.2"
+ "@sveltejs/kit>cookie@<0.7.0": "^0.7.2",
+ "vite-plugin-inspect": "/home/dominikg/develop/vite-plugin-inspect"
},
"onlyBuiltDependencies": [
"esbuild"
diff --git a/packages/e2e-tests/_test_dependencies/vite-plugins/index.js b/packages/e2e-tests/_test_dependencies/vite-plugins/index.js
index 12e635d67..a6c1ea36d 100644
--- a/packages/e2e-tests/_test_dependencies/vite-plugins/index.js
+++ b/packages/e2e-tests/_test_dependencies/vite-plugins/index.js
@@ -1,5 +1,15 @@
import path from 'node:path';
import fs from 'node:fs';
+import MagicString from 'magic-string';
+
+function replaceWithSourceMap(code, value, replacement) {
+ const s = new MagicString(code);
+ s.replaceAll(value, replacement);
+ return {
+ code: s.toString(),
+ map: s.generateMap({ hires: 'boundary' })
+ };
+}
/**
* Ensure transform flow is not interrupted
* @returns {import('vite').Plugin[]}
@@ -11,9 +21,9 @@ export function transformValidation() {
enforce: 'pre',
transform(code, id) {
if (id.endsWith('.svelte')) {
- return code.replaceAll('__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__');
+ return replaceWithSourceMap(code, '__JS_TRANSFORM_1__', '__JS_TRANSFORM_2__');
} else if (id.endsWith('.css')) {
- return code.replaceAll('__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__');
+ return replaceWithSourceMap(code, '__CSS_TRANSFORM_1__', '__CSS_TRANSFORM_2__');
}
}
},
@@ -21,9 +31,9 @@ export function transformValidation() {
name: 'transform-validation:2',
transform(code, id) {
if (id.endsWith('.svelte')) {
- return code.replaceAll('__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__');
+ return replaceWithSourceMap(code, '__JS_TRANSFORM_2__', '__JS_TRANSFORM_3__');
} else if (id.endsWith('.css')) {
- return code.replaceAll('__CSS_TRANSFORM_2__', 'red');
+ return replaceWithSourceMap(code, '__CSS_TRANSFORM_2__', 'red');
}
}
},
@@ -32,7 +42,7 @@ export function transformValidation() {
enforce: 'post',
transform(code, id) {
if (id.endsWith('.svelte')) {
- return code.replaceAll('__JS_TRANSFORM_3__', 'Hello world');
+ return replaceWithSourceMap(code, '__JS_TRANSFORM_3__', 'Hello world');
}
// can't handle css here as in build, it would be `export default {}`
}
diff --git a/packages/e2e-tests/_test_dependencies/vite-plugins/package.json b/packages/e2e-tests/_test_dependencies/vite-plugins/package.json
index 0159e694a..4ca56d865 100644
--- a/packages/e2e-tests/_test_dependencies/vite-plugins/package.json
+++ b/packages/e2e-tests/_test_dependencies/vite-plugins/package.json
@@ -6,5 +6,8 @@
"main": "./index.js",
"files": [
"index.js"
- ]
+ ],
+ "dependencies": {
+ "magic-string": "^0.30.17"
+ }
}
diff --git a/packages/e2e-tests/build-watch/__tests__/build-watch.spec.ts b/packages/e2e-tests/build-watch/__tests__/build-watch.spec.ts
new file mode 100644
index 000000000..c99b5b8af
--- /dev/null
+++ b/packages/e2e-tests/build-watch/__tests__/build-watch.spec.ts
@@ -0,0 +1,157 @@
+import {
+ isBuildWatch,
+ getEl,
+ getText,
+ editFileAndWaitForBuildWatchComplete,
+ hmrCount,
+ untilMatches,
+ sleep,
+ getColor,
+ browserLogs,
+ e2eServer
+} from '~utils';
+
+import * as vite from 'vite';
+// @ts-ignore
+const isRolldownVite = !!vite.rolldownVersion;
+
+describe.runIf(isBuildWatch)('build-watch', () => {
+ test('should render App', async () => {
+ expect(await getText('#app-header')).toBe('Test-App');
+ });
+
+ test('should render static import', async () => {
+ expect(await getText('#static-import .label')).toBe('static-import');
+ });
+
+ test('should render dependency import', async () => {
+ expect(await getText('#dependency-import .label')).toBe('dependency-import');
+ });
+
+ test('should render dynamic import', async () => {
+ expect(await getEl('#dynamic-import')).toBe(null);
+ const dynamicImportButton = await getEl('#button-import-dynamic');
+ expect(dynamicImportButton).toBeDefined();
+ await dynamicImportButton.click();
+ await untilMatches(
+ () => getText('#dynamic-import .label'),
+ 'dynamic-import',
+ 'dynamic import loaded after click'
+ );
+ });
+
+ test('should not have failed requests', async () => {
+ browserLogs.forEach((msg) => {
+ expect(msg).not.toMatch('404');
+ });
+ });
+
+ test('should respect transforms', async () => {
+ expect(await getText('#js-transform')).toBe('Hello world');
+ expect(await getColor('#css-transform')).toBe('red');
+ });
+
+ describe('edit files', () => {
+ const updateHmrTest = editFileAndWaitForBuildWatchComplete.bind(
+ null,
+ 'src/components/HmrTest.svelte'
+ );
+ const updateModuleContext = editFileAndWaitForBuildWatchComplete.bind(
+ null,
+ 'src/components/partial-hmr/ModuleContext.svelte'
+ );
+ 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');
+ expect(await getText('#hmr-test-2 .counter')).toBe('0');
+ expect(await getText('#hmr-test-1 .label')).toBe('hmr-test');
+ expect(await getText('#hmr-test-2 .label')).toBe('hmr-test');
+ expect(await getColor('#hmr-test-1 .label')).toBe('red');
+ expect(await getColor('#hmr-test-2 .label')).toBe('red');
+ });
+
+ test('should have working increment button', async () => {
+ // increment counter of one instance to have local state to verify after build updates
+ await (await getEl('#hmr-test-1 .increment')).click();
+ await sleep(50);
+
+ // counter1 = 1, counter2 = 0
+ expect(await getText('#hmr-test-1 .counter')).toBe('1');
+ expect(await getText('#hmr-test-2 .counter')).toBe('0');
+ });
+
+ test('should apply css changes in HmrTest.svelte', async () => {
+ // update style, change label color from red to green
+ await updateHmrTest((content) => content.replace('color: red', 'color: green'));
+
+ // color should have changed
+ expect(await getColor('#hmr-test-1 .label')).toBe('green');
+ expect(await getColor('#hmr-test-2 .label')).toBe('green');
+ expect(getWatchErrors(), 'error log of `build --watch` is not empty').toEqual([]);
+ });
+
+ test('should apply js change in HmrTest.svelte ', async () => {
+ // update script, change label value
+ await updateHmrTest((content) =>
+ content.replace("const label = 'hmr-test'", "const label = 'hmr-test-updated'")
+ );
+ expect(await getText('#hmr-test-1 .label')).toBe('hmr-test-updated');
+ expect(await getText('#hmr-test-2 .label')).toBe('hmr-test-updated');
+ expect(getWatchErrors(), 'error log of `build --watch` is not empty').toEqual([]);
+ });
+
+ test('should reset state of external store used by HmrTest.svelte when editing App.svelte', async () => {
+ // update App, add a new instance of HmrTest
+ await updateApp((content) =>
+ content.replace(
+ '',
+ '\n'
+ )
+ );
+ // counter state is reset
+ expect(await getText('#hmr-test-1 .counter')).toBe('0');
+ expect(await getText('#hmr-test-2 .counter')).toBe('0');
+ // a third instance has been added
+ expect(await getText('#hmr-test-3 .counter')).toBe('0');
+ expect(getWatchErrors(), 'error log of `build --watch` is not empty').toEqual([]);
+ });
+
+ test('should reset state of store when editing hmr-stores.js', async () => {
+ // change state
+ await (await getEl('#hmr-test-2 .increment')).click();
+ await sleep(50);
+ expect(await getText('#hmr-test-2 .counter')).toBe('1');
+ await updateStore((content) => `${content}\n/*trigger change*/\n`);
+ // counter state is reset
+ expect(await getText('#hmr-test-2 .counter')).toBe('0');
+ expect(getWatchErrors(), 'error log of `build --watch` is not empty').toEqual([]);
+ });
+
+ test('should work when editing script context="module"', async () => {
+ expect(await getText('#hmr-with-context')).toContain('x=0 y=1 slot=1');
+ expect(await getText('#hmr-without-context')).toContain('x=0 y=1 slot=');
+ expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(0);
+ expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0);
+ await updateModuleContext((content) => content.replace('y = 1', 'y = 2'));
+ expect(await getText('#hmr-with-context')).toContain('x=0 y=2 slot=2');
+ expect(await getText('#hmr-without-context')).toContain('x=0 y=2 slot=');
+ expect(hmrCount('UsingNamed.svelte'), 'updates for UsingNamed.svelte').toBe(0);
+ expect(hmrCount('UsingDefault.svelte'), 'updates for UsingDefault.svelte').toBe(0);
+ expect(getWatchErrors(), 'error log of `build --watch` is not empty').toEqual([]);
+ });
+ });
+});
diff --git a/packages/e2e-tests/build-watch/index.html b/packages/e2e-tests/build-watch/index.html
new file mode 100644
index 000000000..5203079ba
--- /dev/null
+++ b/packages/e2e-tests/build-watch/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+ Svelte App
+
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/package.json b/packages/e2e-tests/build-watch/package.json
new file mode 100644
index 000000000..ed2615ab0
--- /dev/null
+++ b/packages/e2e-tests/build-watch/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "e2e-tests-build-watch",
+ "private": true,
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "build:watch": "vite build --watch",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "e2e-test-dep-svelte-simple": "file:../_test_dependencies/svelte-simple"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "workspace:^",
+ "e2e-test-dep-vite-plugins": "file:../_test_dependencies/vite-plugins",
+ "node-fetch": "^3.3.2",
+ "svelte": "^5.36.13",
+ "vite": "^7.0.5",
+ "vite-plugin-inspect": "^11.3.2"
+ },
+ "type": "module"
+}
diff --git a/packages/e2e-tests/build-watch/public/favicon.png b/packages/e2e-tests/build-watch/public/favicon.png
new file mode 100644
index 000000000..7e6f5eb5a
Binary files /dev/null and b/packages/e2e-tests/build-watch/public/favicon.png differ
diff --git a/packages/e2e-tests/build-watch/src/App.svelte b/packages/e2e-tests/build-watch/src/App.svelte
new file mode 100644
index 000000000..399824ca6
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/App.svelte
@@ -0,0 +1,40 @@
+
+
+
+
+{jsTransform}
+
+Hello world
+
+
+{#if !dynamicImportComponent}
+
+{:else}
+
+{/if}
+
+
+
+
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/assets/dynamic.png b/packages/e2e-tests/build-watch/src/assets/dynamic.png
new file mode 100644
index 000000000..7e6f5eb5a
Binary files /dev/null and b/packages/e2e-tests/build-watch/src/assets/dynamic.png differ
diff --git a/packages/e2e-tests/build-watch/src/assets/static.png b/packages/e2e-tests/build-watch/src/assets/static.png
new file mode 100644
index 000000000..7e6f5eb5a
Binary files /dev/null and b/packages/e2e-tests/build-watch/src/assets/static.png differ
diff --git a/packages/e2e-tests/build-watch/src/components/DynamicImport.svelte b/packages/e2e-tests/build-watch/src/components/DynamicImport.svelte
new file mode 100644
index 000000000..af9cb3229
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/DynamicImport.svelte
@@ -0,0 +1,23 @@
+
+
+
+
{label} 
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/components/HmrTest.svelte b/packages/e2e-tests/build-watch/src/components/HmrTest.svelte
new file mode 100644
index 000000000..53222ac21
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/HmrTest.svelte
@@ -0,0 +1,21 @@
+
+
+
+ {label}
+
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/components/StaticImport.svelte b/packages/e2e-tests/build-watch/src/components/StaticImport.svelte
new file mode 100644
index 000000000..1644dcc64
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/StaticImport.svelte
@@ -0,0 +1,23 @@
+
+
+
+
{label} 
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/components/partial-hmr/ModuleContext.svelte b/packages/e2e-tests/build-watch/src/components/partial-hmr/ModuleContext.svelte
new file mode 100644
index 000000000..d3059687c
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/partial-hmr/ModuleContext.svelte
@@ -0,0 +1,12 @@
+
+
+
+
+
+ x={x} y={y} slot=
+
diff --git a/packages/e2e-tests/build-watch/src/components/partial-hmr/PartialHmr.svelte b/packages/e2e-tests/build-watch/src/components/partial-hmr/PartialHmr.svelte
new file mode 100644
index 000000000..45bbce756
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/partial-hmr/PartialHmr.svelte
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingNamed.svelte b/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingNamed.svelte
new file mode 100644
index 000000000..5c7eee9ca
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingNamed.svelte
@@ -0,0 +1,5 @@
+
+
+{y}
diff --git a/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingOnlyDefault.svelte b/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingOnlyDefault.svelte
new file mode 100644
index 000000000..d5474f66a
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/components/partial-hmr/UsingOnlyDefault.svelte
@@ -0,0 +1,5 @@
+
+
+
diff --git a/packages/e2e-tests/build-watch/src/index.js b/packages/e2e-tests/build-watch/src/index.js
new file mode 100644
index 000000000..071c75dc7
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/index.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/build-watch/src/stores/hmr-stores.js b/packages/e2e-tests/build-watch/src/stores/hmr-stores.js
new file mode 100644
index 000000000..1e247644f
--- /dev/null
+++ b/packages/e2e-tests/build-watch/src/stores/hmr-stores.js
@@ -0,0 +1,16 @@
+import { writable } from 'svelte/store';
+let stores = {};
+
+export function getStore(id, initialValue) {
+ return stores[id] || (stores[id] = writable(initialValue));
+}
+
+if (import.meta.hot) {
+ if (import.meta.hot.data.stores) {
+ stores = import.meta.hot.data.stores;
+ }
+ import.meta.hot.accept();
+ import.meta.hot.dispose(() => {
+ import.meta.hot.data.stores = stores;
+ });
+}
diff --git a/packages/e2e-tests/build-watch/vite.config.js b/packages/e2e-tests/build-watch/vite.config.js
new file mode 100644
index 000000000..f195467f2
--- /dev/null
+++ b/packages/e2e-tests/build-watch/vite.config.js
@@ -0,0 +1,43 @@
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+import { defineConfig } from 'vite';
+import { transformValidation } from 'e2e-test-dep-vite-plugins';
+// import inspect from 'vite-plugin-inspect';
+
+export default defineConfig(({ command, mode }) => {
+ return {
+ plugins: [
+ transformValidation(),
+ svelte()
+ /*
+ inspect({ build: true, outputDir: '.vite-inspect' }),
+ {
+ name: 'vite-plugin-no-ssr-fallback-env',
+ enforce: 'post',
+ config: {
+ order: 'post',
+ handler(c) {
+ delete c.ssr; // workaround to avoid vite-plugin-inspect never finishing due to unused ssr build
+ }
+ }
+ }
+ */
+ ],
+ build: {
+ minify: false,
+ target: 'esnext',
+ commonjsOptions: {
+ // pnpm only symlinks packages, and vite wont process cjs deps not in
+ // node_modules, so we add the cjs dep here
+ include: [/node_modules/, /cjs-only/]
+ }
+ },
+ server: {
+ watch: {
+ // During tests we edit the files too fast and sometimes chokidar
+ // misses change events, so enforce polling for consistency
+ usePolling: true,
+ interval: 100
+ }
+ }
+ };
+});
diff --git a/packages/e2e-tests/e2e-server.js b/packages/e2e-tests/e2e-server.js
index 25bdc25e1..ead83d20b 100644
--- a/packages/e2e-tests/e2e-server.js
+++ b/packages/e2e-tests/e2e-server.js
@@ -46,7 +46,33 @@ async function startedOnPort(serverProcess, port, timeout) {
});
}
-export async function serve(root, isBuild, port) {
+async function buildWatchIdle(watchProcess, timeout) {
+ let id;
+ let stdoutListener;
+ const timerPromise = new Promise(
+ (_, reject) =>
+ (id = setTimeout(() => {
+ reject(`timeout for server start after ${timeout}`);
+ }, timeout))
+ );
+ const startedPromise = new Promise((resolve, reject) => {
+ stdoutListener = (data) => {
+ const str = data.toString();
+ const match = str.match(/built in \d+ms\./);
+ if (match) {
+ resolve();
+ }
+ };
+ watchProcess.stdout.on('data', stdoutListener);
+ });
+
+ return Promise.race([timerPromise, startedPromise]).finally(() => {
+ watchProcess.stdout.off('data', stdoutListener);
+ clearTimeout(id);
+ });
+}
+
+export async function serve(root, testMode, port) {
const logDir = path.join(root, 'logs');
const logs = {
server: null,
@@ -88,7 +114,7 @@ export async function serve(root, isBuild, port) {
}
}
- if (isBuild) {
+ if (testMode === 'build') {
let buildResult;
let hasErr = false;
const out = [];
@@ -120,50 +146,72 @@ export async function serve(root, isBuild, port) {
throw buildResult;
}
}
+ let watchProcess;
+ if (testMode === 'build:watch') {
+ watchProcess = execa('pnpm', ['build', '--watch'], {
+ cwd: root,
+ stdio: 'pipe'
+ });
+ logs.watch = { out: [], err: [] };
+ collectLogs(watchProcess, logs.watch);
+ await buildWatchIdle(watchProcess, 10000);
+ }
- const serverProcess = execa('pnpm', [isBuild ? 'preview' : 'dev', '--port', port], {
- cwd: root,
- stdio: 'pipe'
- });
- const out = [],
- err = [];
- logs.server = { out, err };
+ const serverProcess = execa(
+ 'pnpm',
+ [testMode === 'serve' ? 'dev' : 'preview', '--port', port, '--strictPort'],
+ {
+ cwd: root,
+ stdio: 'pipe'
+ }
+ );
+ logs.server = { out: [], err: [] };
collectLogs(serverProcess, logs.server);
const closeServer = async () => {
- if (serverProcess) {
- if (serverProcess.pid) {
- await new Promise((resolve) => {
- treeKill(serverProcess.pid, (err) => {
- if (err) {
- console.error(`failed to treekill serverprocess ${serverProcess.pid}`, err);
- }
- resolve();
+ for (const p of [watchProcess, serverProcess]) {
+ if (p) {
+ if (p.pid) {
+ await new Promise((resolve) => {
+ treeKill(p.pid, (err) => {
+ if (err) {
+ console.error(
+ `failed to treekill ${p === watchProcess ? 'watchprocess' : 'serverprocess'} ${p.pid}`,
+ err
+ );
+ }
+ resolve();
+ });
});
- });
- } else {
- serverProcess.cancel();
- }
-
- try {
- await serverProcess;
- } catch (e) {
- if (e.stdout) {
- pushLines(e.stdout, out);
- }
- if (e.stderr) {
- pushLines(e.stderr, err);
+ } else {
+ p.cancel();
}
- if (!!process.env.DEBUG && !isWin) {
- // treekill on windows uses taskkill and that ends up here always
- console.debug(`e2e server process did not exit gracefully. dir: ${root}`, e);
+
+ try {
+ await p;
+ } catch (e) {
+ const { out, err } = p === watchProcess ? logs.watch : logs.server;
+ if (e.stdout) {
+ pushLines(e.stdout, out);
+ }
+ if (e.stderr) {
+ pushLines(e.stderr, err);
+ }
+ if (!!process.env.DEBUG && !isWin) {
+ // treekill on windows uses taskkill and that ends up here always
+ console.debug(`e2e server process did not exit gracefully. dir: ${root}`, e);
+ }
}
}
}
+
await writeLogs('server', logs.server);
+ if (logs.watch) {
+ await writeLogs('watch', logs.watch);
+ }
};
try {
- await startedOnPort(serverProcess, port, 20000);
+ await startedOnPort(serverProcess, port, 10000);
return {
port,
logs,
diff --git a/packages/e2e-tests/testUtils.ts b/packages/e2e-tests/testUtils.ts
index 6f5ffa2ca..b749ee3d7 100644
--- a/packages/e2e-tests/testUtils.ts
+++ b/packages/e2e-tests/testUtils.ts
@@ -8,7 +8,7 @@ import colors from 'css-color-names';
import { ElementHandle } from 'playwright-core';
import fetch from 'node-fetch';
import {
- isBuild,
+ testMode,
isWin,
isCI,
page,
@@ -80,7 +80,7 @@ export function readFileContent(filename: string) {
}
export function editFile(filename: string, replacer: (str: string) => string) {
- if (isBuild) return;
+ if (testMode === 'build') return;
filename = path.resolve(testDir, filename);
const content = fs.readFileSync(filename, 'utf-8');
const modified = replacer(content);
@@ -113,7 +113,7 @@ export async function untilMatches(
matches: string,
msg: string
) {
- if (isBuild) return;
+ if (testMode === 'build') return;
const maxTries = process.env.CI ? 100 : 20;
for (let tries = 0; tries < maxTries; tries++) {
@@ -204,6 +204,29 @@ export async function editFileAndWaitForHmrComplete(file, replacer, fileUpdateTo
}
}
+export async function editFileAndWaitForBuildWatchComplete(file, replacer) {
+ const newContent = editFile(file, replacer);
+
+ try {
+ await waitForBuildWatchAndPageReload(hmrUpdateTimeout);
+ } catch (e) {
+ const maxTries = isCI && isWin ? 3 : 1;
+ let lastErr;
+ for (let i = 1; i <= maxTries; i++) {
+ try {
+ console.log(`retry #${i} of build:watch update for ${file}`);
+ editFile(file, () => newContent + '\n'.repeat(i));
+ await waitForBuildWatchAndPageReload(hmrUpdateTimeout);
+ return;
+ } catch (e) {
+ lastErr = e;
+ }
+ }
+ await saveScreenshot(`failed_update_${file}`);
+ throw lastErr;
+ }
+}
+
export function hmrCount(file) {
return browserLogs.filter((line) => line.includes('hot updated') && line.includes(file)).length;
}
@@ -231,11 +254,35 @@ export async function saveScreenshot(name: string) {
export async function editViteConfig(replacer: (str: string) => string) {
editFile('vite.config.js', replacer);
- if (!isBuild) {
+ if (testMode === 'serve') {
await waitForServerRestartAndPageReload();
}
}
+export async function waitForBuildWatchAndPageReload(timeout = 10000) {
+ const logs = e2eServer.logs.watch.out;
+ const startIdx = logs.length;
+ let timeleft = timeout;
+ const pollInterval = 50;
+ let completed = false;
+ while (timeleft > 0) {
+ await sleep(pollInterval);
+ if (logs.some((text, i) => i > startIdx && text.match(/\d+ modules transformed\./))) {
+ completed = true;
+ break;
+ }
+ timeleft -= pollInterval;
+ }
+ if (!completed) {
+ console.log('logs', logs.slice(startIdx));
+ throw new Error(`watch rebuild did not finish after ${timeout}ms`);
+ }
+
+ // wait for page to completely load
+ await sleep(100);
+ await page.reload();
+}
+
export async function waitForServerRestartAndPageReload(timeout = 10000) {
const logs = e2eServer.logs.server.out;
const startIdx = logs.length;
diff --git a/packages/e2e-tests/vite-ssr-esm/package.json b/packages/e2e-tests/vite-ssr-esm/package.json
index e5c695066..bab87e7a8 100644
--- a/packages/e2e-tests/vite-ssr-esm/package.json
+++ b/packages/e2e-tests/vite-ssr-esm/package.json
@@ -5,9 +5,7 @@
"type": "module",
"scripts": {
"dev": "node server",
- "build": "run-s build:client build:server",
- "build:client": "vite build --ssrManifest .vite/ssr-manifest.json --outDir dist/client",
- "build:server": "vite build --ssr src/entry-server.js --outDir dist/server",
+ "build": "vite build --app",
"preview": "cross-env NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
diff --git a/packages/e2e-tests/vite-ssr-esm/server.js b/packages/e2e-tests/vite-ssr-esm/server.js
index babf04a9a..9b63bf11a 100644
--- a/packages/e2e-tests/vite-ssr-esm/server.js
+++ b/packages/e2e-tests/vite-ssr-esm/server.js
@@ -17,12 +17,6 @@ if (portArgPos > 0) {
async function createServer(root = process.cwd(), isProd = process.env.NODE_ENV === 'production') {
const resolve = (p) => path.resolve(root, p);
- const indexProd = isProd ? fs.readFileSync(resolve('dist/client/index.html'), 'utf-8') : '';
-
- const manifest = isProd
- ? JSON.parse(fs.readFileSync(resolve('dist/client/.vite/ssr-manifest.json'), 'utf-8'))
- : {};
-
const app = polka();
/**
@@ -69,10 +63,10 @@ async function createServer(root = process.cwd(), isProd = process.env.NODE_ENV
template = await vite.transformIndexHtml(url, template);
render = (await vite.ssrLoadModule('/src/entry-server.js')).render;
} else {
- template = indexProd;
+ template = fs.readFileSync(resolve('dist/client/index.html'), 'utf-8');
render = (await import(pathToFileURL(resolve('dist/server/entry-server.js')).href)).render;
}
- const rendered = await render(req.originalUrl, manifest);
+ const rendered = await render(req.originalUrl);
const appHtml = rendered.html;
const headElements = rendered.head || '';
// TODO what do we do with rendered.css here. find out if emitCss was used and vite took care of it
diff --git a/packages/e2e-tests/vite-ssr-esm/src/entry-server.js b/packages/e2e-tests/vite-ssr-esm/src/entry-server.js
index cce2cdf54..c91531def 100644
--- a/packages/e2e-tests/vite-ssr-esm/src/entry-server.js
+++ b/packages/e2e-tests/vite-ssr-esm/src/entry-server.js
@@ -5,6 +5,6 @@ import { render as ssr } from 'svelte/server';
console.log(esm());
console.log(decamelize('helloWorld'));
-export async function render(url, manifest) {
+export async function render(url) {
return ssr(App, { props: { name: 'world' } });
}
diff --git a/packages/e2e-tests/vite-ssr-esm/vite.config.js b/packages/e2e-tests/vite-ssr-esm/vite.config.js
index 63c8aadaa..38c339fdd 100644
--- a/packages/e2e-tests/vite-ssr-esm/vite.config.js
+++ b/packages/e2e-tests/vite-ssr-esm/vite.config.js
@@ -1,9 +1,32 @@
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';
+import process from 'node:process';
+const isWatch = !!process.env.TEST_BUILD_WATCH;
export default defineConfig(({ command, mode }) => {
return {
plugins: [svelte()],
+ environments: {
+ client: {
+ build: {
+ rollupOptions: {
+ output: {
+ dir: 'dist/client'
+ }
+ }
+ }
+ },
+ ssr: {
+ build: {
+ rollupOptions: {
+ input: 'src/entry-server.js',
+ output: {
+ dir: 'dist/server'
+ }
+ }
+ }
+ }
+ },
build: {
target: 'esnext',
minify: false,
@@ -12,7 +35,8 @@ export default defineConfig(({ command, mode }) => {
output: {
format: 'esm'
}
- }
+ },
+ watch: isWatch ? {} : undefined
},
server: {
watch: {
diff --git a/packages/e2e-tests/vitestSetup.ts b/packages/e2e-tests/vitestSetup.ts
index a8b2adff6..d3084249d 100644
--- a/packages/e2e-tests/vitestSetup.ts
+++ b/packages/e2e-tests/vitestSetup.ts
@@ -6,10 +6,12 @@ import { beforeAll, type File } from 'vitest';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
-export const isBuild = !!process.env.TEST_BUILD;
export const isWin = process.platform === 'win32';
export const isCI = !!process.env.CI;
+export const isBuildWatch = !!process.env.TEST_BUILD_WATCH;
+export const isBuild = isBuildWatch || !!process.env.TEST_BUILD;
+export const testMode = isBuildWatch ? 'build:watch' : process.env.TEST_BUILD ? 'build' : 'serve';
/**
* Path to the current test file
*/
@@ -38,7 +40,11 @@ export function setViteUrl(url: string) {
export interface E2EServer {
port: number;
- logs: { server?: { out: string[]; err: string[] }; build?: { out: string[]; err: string[] } };
+ logs: {
+ server?: { out: string[]; err: string[] };
+ build?: { out: string[]; err: string[] };
+ watch?: { out: string[]; err: string[] };
+ };
close: () => Promise;
}
@@ -60,9 +66,9 @@ const onConsole = (msg) => {
*
* @param testRoot
* @param testName
- * @param isBuild
+ * @param testMode
*/
-const getUniqueTestPort = async (testRoot, testName, isBuild) => {
+const getUniqueTestPort = async (testRoot, testName, testMode) => {
const testDirs = await fs.readdir(testRoot, { withFileTypes: true });
const idx = testDirs
.filter((f) => f.isDirectory())
@@ -71,7 +77,8 @@ const getUniqueTestPort = async (testRoot, testName, isBuild) => {
if (idx < 0) {
throw new Error(`failed to find ${testName} in ${testRoot}`);
}
- return (isBuild ? 5500 : 3500) + idx;
+ const basePort = testMode === 'build:watch' ? 7500 : testMode === 'build' ? 5500 : 3500;
+ return basePort + idx;
};
const DIR = path.join(os.tmpdir(), 'vitest_playwright_global_setup');
@@ -105,7 +112,7 @@ beforeAll(
const srcDir = path.resolve(e2eTestsRoot, testName);
- tempDir = path.resolve(e2eTestsRoot, '../../temp', isBuild ? 'build' : 'serve', testName);
+ tempDir = path.resolve(e2eTestsRoot, '../../temp', testMode.replaceAll(':', '_'), testName);
const directoriesToIgnore = [
'node_modules',
'__tests__',
@@ -163,11 +170,11 @@ beforeAll(
const hasCustomServer = fs.existsSync(customServerScript);
const serverScript = hasCustomServer ? customServerScript : defaultServerScript;
const { serve } = await import(serverScript);
- const port = await getUniqueTestPort(e2eTestsRoot, testName, isBuild);
- server = await serve(tempDir, isBuild, port);
+ const port = await getUniqueTestPort(e2eTestsRoot, testName, testMode);
+ server = await serve(tempDir, testMode, port);
e2eServer = server;
const url = (viteTestUrl = `http://localhost:${port}`);
- await (isBuild ? page.goto(url) : goToUrlAndWaitForViteWSConnect(page, url));
+ await (testMode !== 'serve' ? page.goto(url) : goToUrlAndWaitForViteWSConnect(page, url));
}
} catch (e) {
console.error(`beforeAll failed for ${testName}.`, e);
@@ -236,7 +243,7 @@ async function goToUrlAndWaitForViteWSConnect(page: Page, url: string) {
}
export async function waitForViteConnect(page: Page, timeoutMS = 10000) {
- if (isBuild) {
+ if (testMode !== 'serve') {
return Promise.resolve(); // no vite websocket on build
}
let timerId;
diff --git a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js
index dc319e155..b2616b198 100644
--- a/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js
+++ b/packages/vite-plugin-svelte/src/plugins/load-compiled-css.js
@@ -8,9 +8,16 @@ const filter = { id: SVELTE_VIRTUAL_STYLE_ID_REGEX };
* @returns {import('vite').Plugin}
*/
export function loadCompiledCss(api) {
+ let isBuildWatch = false;
+ /** @type{Map} */
+ const buildWatchCssCache = new Map();
return {
name: 'vite-plugin-svelte:load-compiled-css',
+ configResolved(c) {
+ isBuildWatch = !!c.build?.watch;
+ },
+
resolveId: {
filter, // same filter in load to ensure minimal work
handler(id) {
@@ -26,7 +33,17 @@ export function loadCompiledCss(api) {
if (!svelteRequest) {
return;
}
- const cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
+ let cachedCss = this.getModuleInfo(svelteRequest.filename)?.meta.svelte?.css;
+ // in build --watch getModuleInfo only returns changed module data.
+ // To ensure virtual css is loaded unchanged, we cache it here separately
+ if (isBuildWatch) {
+ if (cachedCss) {
+ buildWatchCssCache.set(svelteRequest.filename, cachedCss);
+ } else {
+ cachedCss = buildWatchCssCache.get(svelteRequest.filename);
+ }
+ }
+
if (cachedCss) {
const { hasGlobal, ...css } = cachedCss;
if (hasGlobal === false) {
@@ -37,6 +54,8 @@ export function loadCompiledCss(api) {
}
css.moduleType = 'css';
return css;
+ } else {
+ log.warn(`failed to load virtual css module ${id}`, undefined, 'load');
}
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index e65e5d866..95cf72c37 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -14,6 +14,7 @@ overrides:
'@types/node@<=20.12.0': 20.19.9
send@<0.19.0: ^0.19.1
'@sveltejs/kit>cookie@<0.7.0': ^0.7.2
+ vite-plugin-inspect: /home/dominikg/develop/vite-plugin-inspect
importers:
@@ -199,7 +200,11 @@ importers:
packages/e2e-tests/_test_dependencies/types-only: {}
- packages/e2e-tests/_test_dependencies/vite-plugins: {}
+ packages/e2e-tests/_test_dependencies/vite-plugins:
+ dependencies:
+ magic-string:
+ specifier: ^0.30.17
+ version: 0.30.17
packages/e2e-tests/autoprefixer-browerslist:
dependencies:
@@ -229,6 +234,31 @@ importers:
specifier: ^7.0.5
version: 7.0.5(@types/node@22.16.5)(sass@1.89.2)(stylus@0.64.0)(yaml@2.8.0)
+ packages/e2e-tests/build-watch:
+ dependencies:
+ e2e-test-dep-svelte-simple:
+ specifier: file:../_test_dependencies/svelte-simple
+ version: file:packages/e2e-tests/_test_dependencies/svelte-simple
+ devDependencies:
+ '@sveltejs/vite-plugin-svelte':
+ specifier: workspace:^
+ version: link:../../vite-plugin-svelte
+ e2e-test-dep-vite-plugins:
+ specifier: file:../_test_dependencies/vite-plugins
+ version: file:packages/e2e-tests/_test_dependencies/vite-plugins
+ node-fetch:
+ specifier: ^3.3.2
+ version: 3.3.2
+ svelte:
+ specifier: ^5.36.13
+ version: 5.36.13
+ vite:
+ specifier: ^7.0.5
+ version: 7.0.5(@types/node@22.16.5)(sass@1.89.2)(stylus@0.64.0)(yaml@2.8.0)
+ vite-plugin-inspect:
+ specifier: /home/dominikg/develop/vite-plugin-inspect
+ version: link:../../../../../vite-plugin-inspect
+
packages/e2e-tests/configfile-custom:
dependencies:
e2e-test-dep-svelte-simple:
@@ -464,7 +494,7 @@ importers:
version: 5.36.13
svelte-check:
specifier: ^4.3.0
- version: 4.3.0(picomatch@4.0.2)(svelte@5.36.13)(typescript@5.8.3)
+ version: 4.3.0(picomatch@4.0.3)(svelte@5.36.13)(typescript@5.8.3)
svelte-i18n:
specifier: ^4.0.1
version: 4.0.1(svelte@5.36.13)
@@ -2805,6 +2835,10 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
+ picomatch@4.0.3:
+ resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+ engines: {node: '>=12'}
+
pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
@@ -4701,7 +4735,9 @@ snapshots:
dependencies:
e2e-test-dep-cjs-only: file:packages/e2e-tests/_test_dependencies/cjs-only
- e2e-test-dep-vite-plugins@file:packages/e2e-tests/_test_dependencies/vite-plugins: {}
+ e2e-test-dep-vite-plugins@file:packages/e2e-tests/_test_dependencies/vite-plugins:
+ dependencies:
+ magic-string: 0.30.17
eastasianwidth@0.2.0: {}
@@ -5024,6 +5060,10 @@ snapshots:
optionalDependencies:
picomatch: 4.0.2
+ fdir@6.4.6(picomatch@4.0.3):
+ optionalDependencies:
+ picomatch: 4.0.3
+
fetch-blob@3.2.0:
dependencies:
node-domexception: 1.0.0
@@ -5620,6 +5660,9 @@ snapshots:
picomatch@4.0.2: {}
+ picomatch@4.0.3:
+ optional: true
+
pidtree@0.6.0: {}
pify@4.0.1: {}
@@ -5902,6 +5945,18 @@ snapshots:
transitivePeerDependencies:
- picomatch
+ svelte-check@4.3.0(picomatch@4.0.3)(svelte@5.36.13)(typescript@5.8.3):
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.25
+ chokidar: 4.0.3
+ fdir: 6.4.6(picomatch@4.0.3)
+ picocolors: 1.1.1
+ sade: 1.8.1
+ svelte: 5.36.13
+ typescript: 5.8.3
+ transitivePeerDependencies:
+ - picomatch
+
svelte-eslint-parser@1.3.0(svelte@5.36.13):
dependencies:
eslint-scope: 8.4.0
diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts
index bf43f97d5..3d323a836 100644
--- a/vitest.config.e2e.ts
+++ b/vitest.config.e2e.ts
@@ -21,6 +21,17 @@ const exclude = [
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**'
];
+const include = ['./packages/e2e-tests/**/*.spec.[tj]s'];
+
+const isBuildWatch = !!process.env.TEST_BUILD_WATCH;
+const buildWatchPatterns = ['./packages/e2e-tests/build-watch/**/*.spec.[tj]s'];
+
+if (isBuildWatch) {
+ include.length = 0;
+ include.push(...buildWatchPatterns);
+} else {
+ exclude.push(...buildWatchPatterns);
+}
export default defineConfig({
resolve: {
@@ -29,7 +40,7 @@ export default defineConfig({
}
},
test: {
- include: ['./packages/e2e-tests/**/*.spec.[tj]s'],
+ include,
exclude,
setupFiles: ['./packages/e2e-tests/vitestSetup.ts'],
globalSetup: ['./packages/e2e-tests/vitestGlobalSetup.ts'],