diff --git a/.changeset/three-icons-trade.md b/.changeset/three-icons-trade.md new file mode 100644 index 0000000000..0bafcf9b73 --- /dev/null +++ b/.changeset/three-icons-trade.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: run `onDestroy` callbacks during SSR diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 12d9a3c130..7350014958 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -79,6 +79,12 @@ export function assign_payload(p1, p2) { p1.anchor = p2.anchor; } +/** + * Array of `onDestroy` callbacks that should be called at the end of the server render function + * @type {Function[]} + */ +export let on_destroy = []; + /** * @param {(...args: any[]) => void} component * @param {{ props: Record; context?: Map }} options @@ -90,6 +96,8 @@ export function render(component, options) { const root_head_anchor = create_anchor(payload.head); set_is_ssr(true); + const prev_on_destroy = on_destroy; + on_destroy = []; payload.out += root_anchor; if (options.context) { @@ -102,6 +110,8 @@ export function render(component, options) { $.pop(); } payload.out += root_anchor; + for (const cleanup of on_destroy) cleanup(); + on_destroy = prev_on_destroy; set_is_ssr(false); return { diff --git a/packages/svelte/src/main/main-server.js b/packages/svelte/src/main/main-server.js index 844aec50d1..a9e1148cc3 100644 --- a/packages/svelte/src/main/main-server.js +++ b/packages/svelte/src/main/main-server.js @@ -1,3 +1,5 @@ +import { on_destroy } from '../internal/server/index.js'; + export { createRoot, createEventDispatcher, @@ -6,7 +8,6 @@ export { getContext, hasContext, mount, - onDestroy, setContext, tick, untrack @@ -15,6 +16,11 @@ export { /** @returns {void} */ export function onMount() {} +/** @param {Function} fn */ +export function onDestroy(fn) { + on_destroy.push(fn); +} + /** @returns {void} */ export function beforeUpdate() {} diff --git a/packages/svelte/tests/runtime-legacy/samples/ondestroy-deep/_config.js b/packages/svelte/tests/runtime-legacy/samples/ondestroy-deep/_config.js index 0957674943..3bf5c9d6d1 100644 --- a/packages/svelte/tests/runtime-legacy/samples/ondestroy-deep/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/ondestroy-deep/_config.js @@ -10,5 +10,9 @@ export default test({ assert.deepEqual(destroyed, ['A', 'B', 'C']); reset(); + }, + + test_ssr({ assert }) { + assert.deepEqual(destroyed, ['A', 'B', 'C']); } }); diff --git a/vitest.config.js b/vitest.config.js index 2af320902d..df5654131f 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -5,34 +5,27 @@ import { configDefaults, defineConfig } from 'vitest/config'; const pkg = JSON.parse(fs.readFileSync('packages/svelte/package.json', 'utf8')); export default defineConfig({ - // We need both a plugin and an alias for some reason resolve: { alias: [ { find: /^svelte\/?/, - customResolver: (id) => { - // For some reason this turns up as "undefined" instead of "svelte" - const exported = pkg.exports[id.replace('undefined', '.')]; + customResolver: (id, importer) => { + // For some reason this turns up as "undefined" instead of "svelte/" + const exported = pkg.exports[id === 'undefined' ? '.' : id.replace('undefined', './')]; if (!exported) return; - return path.resolve('packages/svelte', exported.browser ?? exported.default); + // When running the server version of the Svelte files, + // we also want to use the server export of the Svelte package + return path.resolve( + 'packages/svelte', + importer?.includes('_output/server') + ? exported.default + : exported.browser ?? exported.default + ); } } ] }, - plugins: [ - { - name: 'resolve-svelte', - resolveId(id) { - if (/^svelte\/?/.test(id)) { - const exported = pkg.exports[id.replace('svelte', '.')]; - if (!exported) return; - - return path.resolve('packages/svelte', exported.browser ?? exported.default); - } - } - } - ], test: { dir: '.', reporters: ['dot'],