diff --git a/.changeset/afraid-dogs-matter.md b/.changeset/afraid-dogs-matter.md new file mode 100644 index 0000000000..fcd33b9e39 --- /dev/null +++ b/.changeset/afraid-dogs-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: align `beforeUpdate`/`afterUpdate` behavior better with that in Svelte 4 diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index fa0964636f..4d6b91c007 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -200,6 +200,8 @@ const runes = { `${rune} can only be called with ${list(args, 'or')} ${ args.length === 1 && args[0] === 1 ? 'argument' : 'arguments' }`, + /** @param {string} name */ + 'invalid-runes-mode-import': (name) => `${name} cannot be used in runes mode`, 'duplicate-props-rune': () => `Cannot use $props() more than once` }; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index cbc8c0ff00..6a1aaf0fec 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -492,6 +492,20 @@ const validation = { error(node, 'invalid-const-placement'); } }, + ImportDeclaration(node, context) { + if (node.source.value === 'svelte' && context.state.analysis.runes) { + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportSpecifier') { + if ( + specifier.imported.name === 'beforeUpdate' || + specifier.imported.name === 'afterUpdate' + ) { + error(specifier, 'invalid-runes-mode-import', specifier.imported.name); + } + } + } + } + }, LetDirective(node, context) { const parent = context.path.at(-1); if ( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 5102e62c09..00a9d1fab8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -265,6 +265,7 @@ export function client_component(source, analysis, options) { ...legacy_reactive_declarations, ...group_binding_declarations, .../** @type {import('estree').Statement[]} */ (instance.body), + analysis.runes ? b.empty : b.stmt(b.call('$.init')), .../** @type {import('estree').Statement[]} */ (template.body), ...static_bindings ]); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index dbd7484fa1..6f6ac4742d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,6 +1,6 @@ import { DEV } from 'esm-env'; import { subscribe_to_store } from '../../store/utils.js'; -import { noop, run_all } from '../common.js'; +import { noop, run, run_all } from '../common.js'; import { array_prototype, get_descriptor, @@ -104,18 +104,9 @@ export let current_block = null; /** @type {import('./types.js').ComponentContext | null} */ export let current_component_context = null; -export let is_ssr = false; export let updating_derived = false; -/** - * @param {boolean} ssr - * @returns {void} - */ -export function set_is_ssr(ssr) { - is_ssr = ssr; -} - /** * @param {null | import('./types.js').ComponentContext} context * @returns {boolean} @@ -147,14 +138,6 @@ export function batch_inspect(target, prop, receiver) { }; } -/** - * @param {null | import('./types.js').ComponentContext} context_stack_item - * @returns {void} - */ -export function set_current_component_context(context_stack_item) { - current_component_context = context_stack_item; -} - /** * @param {unknown} a * @param {unknown} b @@ -181,8 +164,6 @@ function create_source_signal(flags, value) { f: flags, // value v: value, - // context: We can remove this if we get rid of beforeUpdate/afterUpdate - x: null, // this is for DEV only inspect: new Set() }; @@ -195,9 +176,7 @@ function create_source_signal(flags, value) { // flags f: flags, // value - v: value, - // context: We can remove this if we get rid of beforeUpdate/afterUpdate - x: null + v: value }; } @@ -346,12 +325,6 @@ function execute_signal_fn(signal) { current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0; current_untracking = false; - // Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point - if (is_render_effect && current_component_context?.u != null) { - // update_callbacks.execute() - current_component_context.u.e(); - } - try { let res; if (is_render_effect) { @@ -924,7 +897,7 @@ export function store_set(store, value) { * @param {import('./types.js').StoreReferencesContainer} stores */ export function unsubscribe_on_destroy(stores) { - onDestroy(() => { + on_destroy(() => { let store_name; for (store_name in stores) { const ref = stores[store_name]; @@ -1150,7 +1123,7 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) { * @returns {void} */ function mark_signal_consumers(signal, to_status, force_schedule) { - const runes = is_runes(signal.x); + const runes = is_runes(null); const consumers = signal.c; if (consumers !== null) { const length = consumers.length; @@ -1192,7 +1165,7 @@ export function set_signal_value(signal, value) { !current_untracking && !ignore_mutation_validation && current_consumer !== null && - is_runes(signal.x) && + is_runes(null) && (current_consumer.f & DERIVED) !== 0 ) { throw new Error( @@ -1208,7 +1181,6 @@ export function set_signal_value(signal, value) { (signal.f & SOURCE) !== 0 && !(/** @type {import('./types.js').EqualsFunctions} */ (signal.e)(value, signal.v)) ) { - const component_context = signal.x; signal.v = value; // If the current signal is running for the first time, it won't have any // consumers as we only allocate and assign the consumers after the signal @@ -1219,7 +1191,7 @@ export function set_signal_value(signal, value) { // $effect(() => x++) // if ( - is_runes(component_context) && + is_runes(null) && current_effect !== null && current_effect.c === null && (current_effect.f & CLEAN) !== 0 @@ -1236,19 +1208,6 @@ export function set_signal_value(signal, value) { } } mark_signal_consumers(signal, DIRTY, true); - // If we have afterUpdates locally on the component, but we're within a render effect - // then we will need to manually invoke the beforeUpdate/afterUpdate logic. - // TODO: should we put this being a is_runes check and only run it in non-runes mode? - if (current_effect === null && current_queued_pre_and_render_effects.length === 0) { - const update_callbacks = component_context?.u; - if (update_callbacks != null) { - run_all(update_callbacks.b); - const managed = managed_effect(() => { - destroy_signal(managed); - run_all(update_callbacks.a); - }); - } - } // @ts-expect-error if (DEV && signal.inspect) { @@ -1299,7 +1258,7 @@ export function derived(init) { create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block) ); signal.i = init; - signal.x = current_component_context; + bind_signal_to_component_context(signal); signal.e = default_equals; if (current_consumer !== null) { push_reference(current_consumer, signal); @@ -1327,10 +1286,27 @@ export function derived_safe_equal(init) { /*#__NO_SIDE_EFFECTS__*/ export function source(initial_value) { const source = create_source_signal(SOURCE | CLEAN, initial_value); - source.x = current_component_context; + bind_signal_to_component_context(source); return source; } +/** + * This function binds a signal to the component context, so that we can fire + * beforeUpdate/afterUpdate callbacks at the correct time etc + * @param {import('./types.js').Signal} signal + */ +function bind_signal_to_component_context(signal) { + if (current_component_context === null || !current_component_context.r) return; + + const signals = current_component_context.d; + + if (signals) { + signals.push(signal); + } else { + current_component_context.d = [signal]; + } +} + /** * @template V * @param {V} initial_value @@ -1883,18 +1859,11 @@ export function value_or_fallback(value, fallback) { /** * Schedules a callback to run immediately before the component is unmounted. - * - * Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the - * only one that runs inside a server-side component. - * - * https://svelte.dev/docs/svelte#ondestroy * @param {() => any} fn * @returns {void} */ -export function onDestroy(fn) { - if (!is_ssr) { - user_effect(() => () => untrack(fn)); - } +function on_destroy(fn) { + user_effect(() => () => untrack(fn)); } /** @@ -1914,6 +1883,8 @@ export function push(props, runes = false) { m: false, // parent p: current_component_context, + // signals + d: null, // props s: props, // runes @@ -1945,6 +1916,56 @@ export function pop(accessors) { } } +/** + * Invoke the getter of all signals associated with a component + * so they can be registered to the effect this function is called in. + * @param {import('./types.js').ComponentContext} context + */ +function observe_all(context) { + if (context.d) { + for (const signal of context.d) get(signal); + } + + const props = get_descriptors(context.s); + for (const descriptor of Object.values(props)) { + if (descriptor.get) descriptor.get(); + } +} + +/** + * Legacy-mode only: Call `onMount` callbacks and set up `beforeUpdate`/`afterUpdate` effects + */ +export function init() { + const context = /** @type {import('./types.js').ComponentContext} */ (current_component_context); + const callbacks = context.u; + + if (!callbacks) return; + + // beforeUpdate + pre_effect(() => { + observe_all(context); + callbacks.b.forEach(run); + }); + + // onMount (must run before afterUpdate) + user_effect(() => { + const fns = untrack(() => callbacks.m.map(run)); + return () => { + for (const fn of fns) { + if (typeof fn === 'function') { + fn(); + } + } + }; + }); + + // afterUpdate + user_effect(() => { + observe_all(context); + callbacks.a.forEach(run); + }); +} + /** * @param {any} value * @param {Set} visited diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index a8b84ce49e..c8918fa441 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -36,6 +36,8 @@ export type Store = { // when the JS VM JITs the code. export type ComponentContext = { + /** local signals (needed for beforeUpdate/afterUpdate) */ + d: null | Signal[]; /** props */ s: Record; /** accessors */ @@ -52,12 +54,12 @@ export type ComponentContext = { r: boolean; /** update_callbacks */ u: null | { - /** before */ - b: Array<() => void>; - /** after */ + /** afterUpdate callbacks */ a: Array<() => void>; - /** execute */ - e: () => void; + /** beforeUpdate callbacks */ + b: Array<() => void>; + /** onMount callbacks */ + m: Array<() => any>; }; }; @@ -69,8 +71,6 @@ export type ComponentContext = { export type SourceSignal = { /** consumers: Signals that read from the current signal */ c: null | ComputationSignal[]; - /** context: The associated component if this signal is an effect/computed */ - x: null | ComponentContext; /** equals: For value equality */ e: null | EqualsFunctions; /** flags: The types that the signal represent, as a bitwise value */ diff --git a/packages/svelte/src/internal/common.js b/packages/svelte/src/internal/common.js index 2a4295f276..9de82f021e 100644 --- a/packages/svelte/src/internal/common.js +++ b/packages/svelte/src/internal/common.js @@ -19,3 +19,8 @@ export function run_all(arr) { arr[i](); } } + +/** @param {Function} fn */ +export function run(fn) { + return fn(); +} diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index ae1748b428..9077585a33 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -30,7 +30,6 @@ export { exclude_from_object, store_set, unsubscribe_on_destroy, - onDestroy, pop, push, reactive_import, @@ -38,7 +37,8 @@ export { user_root_effect, inspect, unwrap, - freeze + freeze, + init } from './client/runtime.js'; export * from './client/each.js'; export * from './client/render.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 7350014958..f873bc4aa5 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -1,5 +1,4 @@ import * as $ from '../client/runtime.js'; -import { set_is_ssr } from '../client/runtime.js'; import { is_promise, noop } from '../common.js'; import { subscribe_to_store } from '../../store/utils.js'; import { DOMBooleanAttributes } from '../../constants.js'; @@ -95,7 +94,6 @@ export function render(component, options) { const root_anchor = create_anchor(payload); const root_head_anchor = create_anchor(payload.head); - set_is_ssr(true); const prev_on_destroy = on_destroy; on_destroy = []; payload.out += root_anchor; @@ -112,7 +110,6 @@ export function render(component, options) { payload.out += root_anchor; for (const cleanup of on_destroy) cleanup(); on_destroy = prev_on_destroy; - set_is_ssr(false); return { head: diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index 019fc9c81f..2c67026710 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -1,12 +1,8 @@ import { current_component_context, - destroy_signal, get_or_init_context_map, - is_ssr, - managed_effect, untrack, - user_effect, - flush_local_render_effects + user_effect } from '../internal/client/runtime.js'; import { is_array } from '../internal/client/utils.js'; @@ -25,16 +21,38 @@ import { is_array } from '../internal/client/utils.js'; * @returns {void} */ export function onMount(fn) { - if (!is_ssr) { + if (current_component_context === null) { + throw new Error('onMount can only be used during component initialisation.'); + } + + if (current_component_context.r) { user_effect(() => { - const result = untrack(fn); - if (typeof result === 'function') { - return /** @type {() => any} */ (result); - } + const cleanup = untrack(fn); + if (typeof cleanup === 'function') return /** @type {() => void} */ (cleanup); }); + } else { + init_update_callbacks(current_component_context).m.push(fn); } } +/** + * Schedules a callback to run immediately before the component is unmounted. + * + * Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the + * only one that runs inside a server-side component. + * + * https://svelte.dev/docs/svelte#ondestroy + * @param {() => any} fn + * @returns {void} + */ +export function onDestroy(fn) { + if (current_component_context === null) { + throw new Error('onDestroy can only be used during component initialisation.'); + } + + onMount(() => () => untrack(fn)); +} + /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -155,46 +173,6 @@ export function createEventDispatcher() { }; } -function init_update_callbacks() { - let called_before = false; - let called_after = false; - - /** @type {NonNullable} */ - const update_callbacks = { - b: [], - a: [], - e() { - if (!called_before) { - called_before = true; - // TODO somehow beforeUpdate ran twice on mount in Svelte 4 if it causes a render - // possibly strategy to get this back if needed: analyse beforeUpdate function for assignements to state, - // if yes, add a call to the component to force-run beforeUpdate once. - untrack(() => update_callbacks.b.forEach(/** @param {any} c */ (c) => c())); - flush_local_render_effects(); - // beforeUpdate can run again once if afterUpdate causes another update, - // but afterUpdate shouldn't be called again in that case to prevent infinite loops - if (!called_after) { - user_effect(() => { - called_before = false; - called_after = true; - untrack(() => update_callbacks.a.forEach(/** @param {any} c */ (c) => c())); - // managed_effect so that it's not cleaned up when the parent effect is cleaned up - const managed = managed_effect(() => { - destroy_signal(managed); - called_after = false; - }); - }); - } else { - user_effect(() => { - called_before = false; - }); - } - } - } - }; - return update_callbacks; -} - // TODO mark beforeUpdate and afterUpdate as deprecated in Svelte 6 /** @@ -210,12 +188,15 @@ function init_update_callbacks() { * @returns {void} */ export function beforeUpdate(fn) { - const component_context = current_component_context; - if (component_context === null) { - throw new Error('beforeUpdate can only be used during component initialisation.'); + if (current_component_context === null) { + throw new Error('beforeUpdate can only be used during component initialisation'); } - (component_context.u ??= init_update_callbacks()).b.push(fn); + if (current_component_context.r) { + throw new Error('beforeUpdate cannot be used in runes mode'); + } + + init_update_callbacks(current_component_context).b.push(fn); } /** @@ -231,22 +212,25 @@ export function beforeUpdate(fn) { * @returns {void} */ export function afterUpdate(fn) { - const component_context = current_component_context; - if (component_context === null) { + if (current_component_context === null) { throw new Error('afterUpdate can only be used during component initialisation.'); } - (component_context.u ??= init_update_callbacks()).a.push(fn); + if (current_component_context.r) { + throw new Error('afterUpdate cannot be used in runes mode'); + } + + init_update_callbacks(current_component_context).a.push(fn); +} + +/** + * Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate + * @param {import('../internal/client/types.js').ComponentContext} context + */ +function init_update_callbacks(context) { + return (context.u ??= { a: [], b: [], m: [] }); } // TODO bring implementations in here // (except probably untrack — do we want to expose that, if there's also a rune?) -export { - flushSync, - createRoot, - mount, - tick, - untrack, - unstate, - onDestroy -} from '../internal/index.js'; +export { flushSync, createRoot, mount, tick, untrack, unstate } from '../internal/index.js'; diff --git a/packages/svelte/src/store/index.js b/packages/svelte/src/store/index.js index 85954fc66c..762aa4f912 100644 --- a/packages/svelte/src/store/index.js +++ b/packages/svelte/src/store/index.js @@ -1,4 +1,4 @@ -import { noop } from '../internal/common.js'; +import { noop, run } from '../internal/common.js'; import { subscribe_to_store } from './utils.js'; /** @@ -106,11 +106,6 @@ export function writable(value, start = noop) { return { set, update, subscribe }; } -/** @param {Function} fn */ -function run(fn) { - return fn(); -} - /** * @param {Function[]} fns * @returns {void} diff --git a/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/_config.js new file mode 100644 index 0000000000..d597478bb2 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-runes-mode-import', + message: 'beforeUpdate cannot be used in runes mode' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/main.svelte new file mode 100644 index 0000000000..b4a77242bb --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-before-after-update/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-browser/test.ts b/packages/svelte/tests/runtime-browser/test.ts index 150dde8089..8545f5292f 100644 --- a/packages/svelte/tests/runtime-browser/test.ts +++ b/packages/svelte/tests/runtime-browser/test.ts @@ -112,6 +112,8 @@ async function run_test( let build_result_ssr; if (hydrate) { + const ssr_entry = path.resolve(__dirname, '../../src/main/main-server.js'); + build_result_ssr = await build({ entryPoints: [`${__dirname}/driver-ssr.js`], write: false, @@ -123,6 +125,14 @@ async function run_test( { name: 'testing-runtime-browser-ssr', setup(build) { + // When running the server version of the Svelte files, + // we also want to use the server export of the Svelte package + build.onResolve({ filter: /./ }, (args) => { + if (args.path === 'svelte') { + return { path: ssr_entry }; + } + }); + build.onLoad({ filter: /\.svelte$/ }, (args) => { const compiled = compile(fs.readFileSync(args.path, 'utf-8').replace(/\r/g, ''), { generate: 'server', diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/Child.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/Child.svelte new file mode 100644 index 0000000000..29a201e16c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/Child.svelte @@ -0,0 +1,25 @@ + + +

a: {a}

diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/_config.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/_config.js new file mode 100644 index 0000000000..4050d2ef03 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/_config.js @@ -0,0 +1,45 @@ +import { flushSync } from 'svelte'; +import { log } from './log.js'; +import { test, ok } from '../../test'; + +export default test({ + before_test: () => { + log.length = 0; + }, + + test({ assert, target }) { + const [button1, button2] = target.querySelectorAll('button'); + ok(button1); + ok(button2); + + button1.click(); + flushSync(); + + button2.click(); + flushSync(); + + assert.deepEqual(log, [ + 'before', + 'before 0, 0', + 'after', + 'after 0, 0', + 'before', + 'before 1, 0', + 'after', + 'after 1, 0', + 'before', + 'before 1, 1', + 'after', + 'after 1, 1' + ]); + + assert.htmlEqual( + target.innerHTML, + ` + + +

a: 1

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/log.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/log.js new file mode 100644 index 0000000000..a119176880 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/log.js @@ -0,0 +1,2 @@ +/** @type {string[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/main.svelte new file mode 100644 index 0000000000..5b517c80ff --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-afterUpdate2/main.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js index 96817b581e..73bfd09ceb 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/_config.js @@ -46,10 +46,10 @@ export default test({ '2: render 1', '3: beforeUpdate 1', '3: render 1', - 'parent: afterUpdate 1', '1: afterUpdate 1', '2: afterUpdate 1', - '3: afterUpdate 1' + '3: afterUpdate 1', + 'parent: afterUpdate 1' ]); } }); diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 70746bcb36..8d878b64c9 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -5,6 +5,7 @@ import * as $ from "svelte/internal"; export default function Each_string_template($$anchor, $$props) { $.push($$props, false); + $.init(); /* Init */ var fragment = $.comment($$anchor); @@ -27,4 +28,4 @@ export default function Each_string_template($$anchor, $$props) { $.close_frag($$anchor, fragment); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index ee1ec7cb90..7eb39880c4 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -7,10 +7,11 @@ var frag = $.template(`

hello world

`); export default function Hello_world($$anchor, $$props) { $.push($$props, false); + $.init(); /* Init */ var h1 = $.open($$anchor, true, frag); $.close($$anchor, h1); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 6e18270cab..c6d0ad3d5a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -234,6 +234,15 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#onmount * */ export function onMount(fn: () => NotFunction | Promise> | (() => any)): void; + /** + * Schedules a callback to run immediately before the component is unmounted. + * + * Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the + * only one that runs inside a server-side component. + * + * https://svelte.dev/docs/svelte#ondestroy + * */ + export function onDestroy(fn: () => any): void; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -360,15 +369,6 @@ declare module 'svelte' { * https://svelte-5-preview.vercel.app/docs/functions#untrack * */ export function untrack(fn: () => T): T; - /** - * Schedules a callback to run immediately before the component is unmounted. - * - * Out of `onMount`, `beforeUpdate`, `afterUpdate` and `onDestroy`, this is the - * only one that runs inside a server-side component. - * - * https://svelte.dev/docs/svelte#ondestroy - * */ - export function onDestroy(fn: () => any): void; export function unstate(value: T): T; } diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md index 111fce7b17..d01ae1900a 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md @@ -90,10 +90,14 @@ Various error and warning codes have been renamed slightly. The number of valid namespaces you can pass to the compiler option `namespace` has been reduced to `html` (the default), `svg` and `foreign`. -### beforeUpdate change +### beforeUpdate/afterUpdate changes `beforeUpdate` no longer runs twice on initial render if it modifies a variable referenced in the template. +`afterUpdate` callbacks in a parent component will now run after `afterUpdate` callbacks in any child components. + +Both functions are disallowed in runes mode — use `$effect.pre(...)` and `$effect(...)` instead. + ### `contenteditable` behavior change If you have a `contenteditable` node with a corresponding binding _and_ a reactive value inside it (example: `
count is {count}
`), then the value inside the contenteditable will not be updated by updates to `count` because the binding takes full control over the content immediately and it should only be updated through it.