diff --git a/.changeset/brave-points-sleep.md b/.changeset/brave-points-sleep.md new file mode 100644 index 0000000000..6412a09434 --- /dev/null +++ b/.changeset/brave-points-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: improve code generation for `bind:this` in SSR mode diff --git a/.changeset/cool-roses-trade.md b/.changeset/cool-roses-trade.md new file mode 100644 index 0000000000..ae05a8f8b4 --- /dev/null +++ b/.changeset/cool-roses-trade.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve effect over-fire on store subscription init diff --git a/.changeset/empty-bags-heal.md b/.changeset/empty-bags-heal.md new file mode 100644 index 0000000000..b19b631c51 --- /dev/null +++ b/.changeset/empty-bags-heal.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: use init properties when exporting non-state values in prod diff --git a/.changeset/forty-dogs-divide.md b/.changeset/forty-dogs-divide.md new file mode 100644 index 0000000000..e0351241c6 --- /dev/null +++ b/.changeset/forty-dogs-divide.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: improve each block item equality for immutable mode diff --git a/.changeset/pre.json b/.changeset/pre.json index cb9697daff..3d42bf927a 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -21,6 +21,7 @@ "big-eyes-carry", "big-geese-act", "blue-timers-film", + "brave-points-sleep", "brave-shrimps-kiss", "brave-walls-destroy", "breezy-carrots-flash", @@ -39,6 +40,7 @@ "cold-birds-own", "cool-ants-leave", "cool-rabbits-tickle", + "cool-roses-trade", "cuddly-pianos-drop", "curly-lizards-dream", "curvy-cups-cough", @@ -58,6 +60,7 @@ "eight-steaks-shout", "eighty-bikes-camp", "eighty-days-cheat", + "empty-bags-heal", "empty-bulldogs-exercise", "empty-crabs-think", "empty-tools-share", @@ -171,6 +174,7 @@ "popular-mangos-rest", "pretty-ties-help", "purple-dragons-peel", + "quiet-apricots-dream", "quiet-camels-mate", "quiet-crabs-nail", "quiet-timers-speak", @@ -273,6 +277,7 @@ "unlucky-boxes-obey", "unlucky-trees-lick", "violet-pigs-jam", + "weak-terms-destroy", "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train", diff --git a/.changeset/weak-terms-destroy.md b/.changeset/weak-terms-destroy.md new file mode 100644 index 0000000000..eebe8bfce3 --- /dev/null +++ b/.changeset/weak-terms-destroy.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: visit expression node in directives diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1024c16d14..c170f74345 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,27 @@ # svelte +## 5.0.0-next.60 + +### Patch Changes + +- fix: improve effect over-fire on store subscription init ([#10535](https://github.com/sveltejs/svelte/pull/10535)) + +- fix: use init properties when exporting non-state values in prod ([#10521](https://github.com/sveltejs/svelte/pull/10521)) + +## 5.0.0-next.59 + +### Patch Changes + +- chore: improve code generation for `bind:this` in SSR mode ([#10524](https://github.com/sveltejs/svelte/pull/10524)) + +- fix: visit expression node in directives ([#10527](https://github.com/sveltejs/svelte/pull/10527)) + +## 5.0.0-next.58 + +### Patch Changes + +- breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount` ([#10516](https://github.com/sveltejs/svelte/pull/10516)) + ## 5.0.0-next.57 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 35daff6671..802ef443e2 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.0.0-next.57", + "version": "5.0.0-next.60", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index f19b60c03f..8b2b806ec7 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1060,7 +1060,9 @@ const common_visitors = { parent.type === 'SvelteComponent' || parent.type === 'SvelteSelf' ) { - context.state.analysis.uses_component_bindings = true; + if (node.name !== 'this') { + context.state.analysis.uses_component_bindings = true; + } break; } else if (is_element_node(parent)) { break; 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 ee47cc33ae..80628cc2f2 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 @@ -242,9 +242,11 @@ export function client_component(source, analysis, options) { const binding = analysis.instance.scope.get(name); const is_source = binding !== null && is_state_source(binding, state); - // TODO This is always a getter because the `renamed-instance-exports` test wants it that way. - // Should we for code size reasons make it an init in runes mode and/or non-dev mode? - return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]); + if (is_source || options.dev) { + return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]); + } + + return b.init(alias ?? name, b.id(name)); }); if (analysis.accessors) { @@ -286,17 +288,12 @@ export function client_component(source, analysis, options) { ) : () => {}; - if (properties.length > 0) { - component_block.body.push( - b.var('$$accessors', b.object(properties)), - b.stmt(b.call('$.pop', b.id('$$accessors'))) - ); - append_styles(); - component_block.body.push(b.return(b.id('$$accessors'))); - } else { - component_block.body.push(b.stmt(b.call('$.pop'))); - append_styles(); - } + append_styles(); + component_block.body.push( + properties.length > 0 + ? b.return(b.call('$.pop', b.object(properties))) + : b.stmt(b.call('$.pop')) + ); if (analysis.uses_rest_props) { /** @type {string[]} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index af73c0f137..7b672502b9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -28,7 +28,7 @@ import { DOMBooleanAttributes, EACH_INDEX_REACTIVE, EACH_IS_CONTROLLED, - EACH_IS_IMMUTABLE, + EACH_IS_STRICT_EQUALS, EACH_ITEM_REACTIVE, EACH_KEYED } from '../../../../../constants.js'; @@ -2259,8 +2259,8 @@ export const template_visitors = { each_type |= EACH_IS_CONTROLLED; } - if (context.state.analysis.immutable) { - each_type |= EACH_IS_IMMUTABLE; + if (context.state.analysis.runes) { + each_type |= EACH_IS_STRICT_EQUALS; } // Find the parent each blocks which contain the arrays to invalidate diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 5640a1f372..d197ae3e85 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -853,7 +853,7 @@ function serialize_inline_component(node, component_name, context) { const value = serialize_attribute_value(attribute.value, context, false, true); push_prop(b.prop('init', b.key(attribute.name), value)); - } else if (attribute.type === 'BindDirective') { + } else if (attribute.type === 'BindDirective' && attribute.name !== 'this') { // TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child push_prop( b.get(attribute.name, [ diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index aae6b97549..79d7877f54 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -331,10 +331,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } /** - * @type {import('zimmerframe').Visitor} + * @type {import('zimmerframe').Visitor} */ - const SvelteDirective = (node, context) => { - context.state.scope.reference(b.id(node.name), context.path); + const SvelteDirective = (node, { state, path, visit }) => { + state.scope.reference(b.id(node.name), path); + + if (node.expression) { + visit(node.expression); + } }; walk(ast, state, { diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 45b4d25835..faacabcb97 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -5,7 +5,7 @@ export const EACH_KEYED = 1 << 2; /** See EachBlock interface metadata.is_controlled for an explanation what this is */ export const EACH_IS_CONTROLLED = 1 << 3; export const EACH_IS_ANIMATED = 1 << 4; -export const EACH_IS_IMMUTABLE = 1 << 6; +export const EACH_IS_STRICT_EQUALS = 1 << 6; export const PROPS_IS_IMMUTABLE = 1; export const PROPS_IS_RUNES = 1 << 1; diff --git a/packages/svelte/src/internal/client/block.js b/packages/svelte/src/internal/client/block.js index 5ca2bdd850..5ba278cf00 100644 --- a/packages/svelte/src/internal/client/block.js +++ b/packages/svelte/src/internal/client/block.js @@ -32,48 +32,6 @@ export function create_root_block(intro) { }; } -/** @returns {import('./types.js').IfBlock} */ -export function create_if_block() { - return { - // alternate transitions - a: null, - // alternate effect - ae: null, - // consequent transitions - c: null, - // consequent effect - ce: null, - // dom - d: null, - // effect - e: null, - // parent - p: /** @type {import('./types.js').Block} */ (current_block), - // transition - r: null, - // type - t: IF_BLOCK, - // value - v: false - }; -} - -/** @returns {import('./types.js').KeyBlock} */ -export function create_key_block() { - return { - // dom - d: null, - // effect - e: null, - // parent - p: /** @type {import('./types.js').Block} */ (current_block), - // transition - r: null, - // type - t: KEY_BLOCK - }; -} - /** @returns {import('./types.js').HeadBlock} */ export function create_head_block() { return { @@ -122,82 +80,6 @@ export function create_dynamic_component_block() { }; } -/** @returns {import('./types.js').AwaitBlock} */ -export function create_await_block() { - return { - // dom - d: null, - // effect - e: null, - // parent - p: /** @type {import('./types.js').Block} */ (current_block), - // pending - n: true, - // transition - r: null, - // type - t: AWAIT_BLOCK - }; -} - -/** - * @param {number} flags - * @param {Element | Comment} anchor - * @returns {import('./types.js').EachBlock} - */ -export function create_each_block(flags, anchor) { - return { - // anchor - a: anchor, - // dom - d: null, - // flags - f: flags, - // items - v: [], - // effect - e: null, - p: /** @type {import('./types.js').Block} */ (current_block), - // transition - r: null, - // transitions - s: [], - // type - t: EACH_BLOCK - }; -} - -/** - * @param {any | import('./types.js').Signal} item - * @param {number | import('./types.js').Signal} index - * @param {null | unknown} key - * @returns {import('./types.js').EachItemBlock} - */ -export function create_each_item_block(item, index, key) { - return { - // animate transition - a: null, - // dom - d: null, - // effect - e: null, - // index - i: index, - // key - k: key, - // item - v: item, - // parent - p: /** @type {import('./types.js').EachBlock} */ (current_block), - // transition - r: null, - // transitions - s: null, - // type - t: EACH_ITEM_BLOCK - }; -} - /** @returns {import('./types.js').SnippetBlock} */ export function create_snippet_block() { return { diff --git a/packages/svelte/src/internal/client/custom-element.js b/packages/svelte/src/internal/client/custom-element.js index 2502a58676..5374d6ba77 100644 --- a/packages/svelte/src/internal/client/custom-element.js +++ b/packages/svelte/src/internal/client/custom-element.js @@ -276,7 +276,7 @@ function get_custom_elements_slots(element) { * @param {any} Component A Svelte component function * @param {Record} props_definition The props to observe * @param {string[]} slots The slots to create - * @param {string[]} accessors Other accessors besides the ones for props the component has + * @param {string[]} exports Explicitly exported values, other than props * @param {boolean} use_shadow_dom Whether to use shadow DOM * @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend] */ @@ -284,7 +284,7 @@ export function create_custom_element( Component, props_definition, slots, - accessors, + exports, use_shadow_dom, extend ) { @@ -311,10 +311,10 @@ export function create_custom_element( } }); }); - accessors.forEach((accessor) => { - define_property(Class.prototype, accessor, { + exports.forEach((property) => { + define_property(Class.prototype, property, { get() { - return this.$$c?.[accessor]; + return this.$$c?.[property]; } }); }); diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js new file mode 100644 index 0000000000..39946f63f2 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -0,0 +1,201 @@ +import { is_promise } from '../../../common.js'; +import { AWAIT_BLOCK } from '../../block.js'; +import { hydrate_block_anchor } from '../../hydration.js'; +import { remove } from '../../reconciler.js'; +import { + UNINITIALIZED, + current_block, + destroy_signal, + execute_effect, + flushSync, + push_destroy_fn, + render_effect +} from '../../runtime.js'; +import { trigger_transitions } from '../../transitions.js'; + +/** @returns {import('../../types.js').AwaitBlock} */ +export function create_await_block() { + return { + // dom + d: null, + // effect + e: null, + // parent + p: /** @type {import('../../types.js').Block} */ (current_block), + // pending + n: true, + // transition + r: null, + // type + t: AWAIT_BLOCK + }; +} + +/** + * @template V + * @param {Comment} anchor_node + * @param {(() => Promise)} input + * @param {null | ((anchor: Node) => void)} pending_fn + * @param {null | ((anchor: Node, value: V) => void)} then_fn + * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn + * @returns {void} + */ +export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { + const block = create_await_block(); + + /** @type {null | import('../../types.js').Render} */ + let current_render = null; + hydrate_block_anchor(anchor_node); + + /** @type {{}} */ + let latest_token; + + /** @type {typeof UNINITIALIZED | V} */ + let resolved_value = UNINITIALIZED; + + /** @type {unknown} */ + let error = UNINITIALIZED; + let pending = false; + block.r = + /** + * @param {import('../../types.js').Transition} transition + * @returns {void} + */ + (transition) => { + const render = /** @type {import('../../types.js').Render} */ (current_render); + const transitions = render.s; + transitions.add(transition); + transition.f(() => { + transitions.delete(transition); + if (transitions.size === 0) { + // If the current render has changed since, then we can remove the old render + // effect as it's stale. + if (current_render !== render && render.e !== null) { + if (render.d !== null) { + remove(render.d); + render.d = null; + } + destroy_signal(render.e); + render.e = null; + } + } + }); + }; + const create_render_effect = () => { + /** @type {import('../../types.js').Render} */ + const render = { + d: null, + e: null, + s: new Set(), + p: current_render + }; + const effect = render_effect( + () => { + if (error === UNINITIALIZED) { + if (resolved_value === UNINITIALIZED) { + // pending = true + block.n = true; + if (pending_fn !== null) { + pending_fn(anchor_node); + } + } else if (then_fn !== null) { + // pending = false + block.n = false; + then_fn(anchor_node, resolved_value); + } + } else if (catch_fn !== null) { + // pending = false + block.n = false; + catch_fn(anchor_node, error); + } + render.d = block.d; + block.d = null; + }, + block, + true, + true + ); + render.e = effect; + current_render = render; + }; + const render = () => { + const render = current_render; + if (render === null) { + create_render_effect(); + return; + } + const transitions = render.s; + if (transitions.size === 0) { + if (render.d !== null) { + remove(render.d); + render.d = null; + } + if (render.e) { + execute_effect(render.e); + } else { + create_render_effect(); + } + } else { + create_render_effect(); + trigger_transitions(transitions, 'out'); + } + }; + const await_effect = render_effect( + () => { + const token = {}; + latest_token = token; + const promise = input(); + if (is_promise(promise)) { + promise.then( + /** @param {V} v */ + (v) => { + if (latest_token === token) { + // Ensure UI is in sync before resolving value. + flushSync(); + resolved_value = v; + pending = false; + render(); + } + }, + /** @param {unknown} _error */ + (_error) => { + error = _error; + pending = false; + render(); + } + ); + if (resolved_value !== UNINITIALIZED || error !== UNINITIALIZED) { + error = UNINITIALIZED; + resolved_value = UNINITIALIZED; + } + if (!pending) { + pending = true; + render(); + } + } else { + error = UNINITIALIZED; + resolved_value = promise; + pending = false; + render(); + } + }, + block, + false + ); + push_destroy_fn(await_effect, () => { + let render = current_render; + latest_token = {}; + while (render !== null) { + const dom = render.d; + if (dom !== null) { + remove(dom); + } + const effect = render.e; + if (effect !== null) { + destroy_signal(effect); + } + render = render.p; + } + }); + block.e = await_effect; +} diff --git a/packages/svelte/src/internal/client/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js similarity index 82% rename from packages/svelte/src/internal/client/each.js rename to packages/svelte/src/internal/client/dom/blocks/each.js index 2d81a0b070..d6367509b7 100644 --- a/packages/svelte/src/internal/client/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -2,21 +2,23 @@ import { EACH_INDEX_REACTIVE, EACH_IS_ANIMATED, EACH_IS_CONTROLLED, - EACH_IS_IMMUTABLE, + EACH_IS_STRICT_EQUALS, EACH_ITEM_REACTIVE, EACH_KEYED -} from '../../constants.js'; -import { create_each_block, create_each_item_block } from './block.js'; +} from '../../../../constants.js'; +import { noop } from '../../../common.js'; +import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../block.js'; import { current_hydration_fragment, get_hydration_fragment, hydrate_block_anchor, hydrating, set_current_hydration_fragment -} from './hydration.js'; -import { clear_text_content, empty, map_get, map_set } from './operations.js'; -import { insert, remove } from './reconciler.js'; +} from '../../hydration.js'; +import { clear_text_content, empty, map_get, map_set } from '../../operations.js'; +import { insert, remove } from '../../reconciler.js'; import { + current_block, destroy_signal, execute_effect, mutable_source, @@ -24,15 +26,71 @@ import { render_effect, set_signal_value, source -} from './runtime.js'; -import { trigger_transitions } from './transitions.js'; -import { is_array } from './utils.js'; +} from '../../runtime.js'; +import { trigger_transitions } from '../../transitions.js'; +import { is_array } from '../../utils.js'; const NEW_BLOCK = -1; const MOVED_BLOCK = 99999999; const LIS_BLOCK = -2; -function no_op() {} +/** + * @param {number} flags + * @param {Element | Comment} anchor + * @returns {import('../../types.js').EachBlock} + */ +export function create_each_block(flags, anchor) { + return { + // anchor + a: anchor, + // dom + d: null, + // flags + f: flags, + // items + v: [], + // effect + e: null, + p: /** @type {import('../../types.js').Block} */ (current_block), + // transition + r: null, + // transitions + s: [], + // type + t: EACH_BLOCK + }; +} + +/** + * @param {any | import('../../types.js').Signal} item + * @param {number | import('../../types.js').Signal} index + * @param {null | unknown} key + * @returns {import('../../types.js').EachItemBlock} + */ +export function create_each_item_block(item, index, key) { + return { + // animate transition + a: null, + // dom + d: null, + // effect + e: null, + // index + i: index, + // key + k: key, + // item + v: item, + // parent + p: /** @type {import('../../types.js').EachBlock} */ (current_block), + // transition + r: null, + // transitions + s: null, + // type + t: EACH_ITEM_BLOCK + }; +} /** * @template V @@ -40,7 +98,7 @@ function no_op() {} * @param {() => V[]} collection * @param {number} flags * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @returns {void} @@ -49,7 +107,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; const block = create_each_block(flags, anchor_node); - /** @type {null | import('./types.js').Render} */ + /** @type {null | import('../../types.js').Render} */ let current_fallback = null; hydrate_block_anchor(anchor_node, is_controlled); @@ -59,7 +117,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re /** @type {Array | null} */ let keys = null; - /** @type {null | import('./types.js').EffectSignal} */ + /** @type {null | import('../../types.js').EffectSignal} */ let render = null; /** @@ -69,9 +127,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re let mismatch = false; block.r = - /** @param {import('./types.js').Transition} transition */ + /** @param {import('../../types.js').Transition} transition */ (transition) => { - const fallback = /** @type {import('./types.js').Render} */ (current_fallback); + const fallback = /** @type {import('../../types.js').Render} */ (current_fallback); const transitions = fallback.s; transitions.add(transition); transition.f(() => { @@ -90,7 +148,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re }; const create_fallback_effect = () => { - /** @type {import('./types.js').Render} */ + /** @type {import('../../types.js').Render} */ const fallback = { d: null, e: null, @@ -131,7 +189,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re current_fallback = fallback; }; - /** @param {import('./types.js').EachBlock} block */ + /** @param {import('../../types.js').EachBlock} block */ const render_each = (block) => { const flags = block.f; const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; @@ -152,7 +210,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re if (key_fn !== null) { keys = array.map(key_fn); } else if ((flags & EACH_KEYED) === 0) { - array.map(no_op); + array.map(noop); } const length = array.length; @@ -168,7 +226,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re mismatch = true; } else if (is_each_else_comment) { // Remove the each_else comment node or else it will confuse the subsequent hydration algorithm - /** @type {import('./types.js').TemplateNode[]} */ (current_hydration_fragment).shift(); + /** @type {import('../../types.js').TemplateNode[]} */ ( + current_hydration_fragment + ).shift(); } } @@ -226,7 +286,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re } // Clear the array reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys); - destroy_signal(/** @type {import('./types.js').EffectSignal} */ (render)); + destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (render)); }); block.e = each; @@ -238,7 +298,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re * @param {() => V[]} collection * @param {number} flags * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @returns {void} */ @@ -251,7 +311,7 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa * @param {Element | Comment} anchor_node * @param {() => V[]} collection * @param {number} flags - * @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @returns {void} */ @@ -262,10 +322,10 @@ export function each_indexed(anchor_node, collection, flags, render_fn, fallback /** * @template V * @param {Array} array - * @param {import('./types.js').EachBlock} each_block + * @param {import('../../types.js').EachBlock} each_block * @param {Element | Comment | Text} dom * @param {boolean} is_controlled - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {(anchor: null, item: V, index: number | import('../../types.js').Signal) => void} render_fn * @param {number} flags * @param {boolean} apply_transitions * @returns {void} @@ -290,7 +350,7 @@ function reconcile_indexed_array( var length = Math.max(a, b); var index = 0; - /** @type {Array} */ + /** @type {Array} */ var b_blocks; var block; @@ -315,7 +375,7 @@ function reconcile_indexed_array( b_blocks = Array(b); if (hydrating) { // Hydrate block - var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ ( + var hydration_list = /** @type {import('../../types.js').TemplateNode[]} */ ( current_hydration_fragment ); var hydrating_node = hydration_list[0]; @@ -333,7 +393,7 @@ function reconcile_indexed_array( block = each_item_block(item, null, index, render_fn, flags); b_blocks[index] = block; - hydrating_node = /** @type {import('./types.js').TemplateNode} */ ( + hydrating_node = /** @type {import('../../types.js').TemplateNode} */ ( /** @type {Node} */ (/** @type {Node} */ (fragment.at(-1)).nextSibling).nextSibling ); } @@ -376,10 +436,10 @@ function reconcile_indexed_array( * https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968 * @template V * @param {Array} array - * @param {import('./types.js').EachBlock} each_block + * @param {import('../../types.js').EachBlock} each_block * @param {Element | Comment | Text} dom * @param {boolean} is_controlled - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {(anchor: null, item: V, index: number | import('../../types.js').Signal) => void} render_fn * @param {number} flags * @param {boolean} apply_transitions * @param {Array | null} keys @@ -405,7 +465,7 @@ function reconcile_tracked_array( /** @type {number} */ var b = array.length; - /** @type {Array} */ + /** @type {Array} */ var b_blocks; var block; @@ -435,7 +495,7 @@ function reconcile_tracked_array( if (hydrating) { // Hydrate block var fragment; - var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ ( + var hydration_list = /** @type {import('../../types.js').TemplateNode[]} */ ( current_hydration_fragment ); var hydrating_node = hydration_list[0]; @@ -457,7 +517,7 @@ function reconcile_tracked_array( // Get the tag of the next item in the list // The fragment array can be empty if each block has no content - hydrating_node = /** @type {import('./types.js').TemplateNode} */ ( + hydrating_node = /** @type {import('../../types.js').TemplateNode} */ ( /** @type {Node} */ ((fragment.at(-1) || hydrating_node).nextSibling).nextSibling ); } @@ -610,8 +670,8 @@ function reconcile_tracked_array( /** * The server could have rendered more list items than the client specifies. * In that case, we need to remove the remaining server-rendered nodes. - * @param {import('./types.js').TemplateNode[]} hydration_list - * @param {import('./types.js').TemplateNode | null} next_node + * @param {import('../../types.js').TemplateNode[]} hydration_list + * @param {import('../../types.js').TemplateNode | null} next_node */ function remove_excess_hydration_nodes(hydration_list, next_node) { if (next_node === null) return; @@ -695,14 +755,14 @@ function mark_lis(a) { } /** - * @param {import('./types.js').Block} block + * @param {import('../../types.js').Block} block * @param {Element | Comment | Text} dom * @param {boolean} is_controlled * @param {null | Text | Element | Comment} sibling * @returns {Text | Element | Comment} */ function insert_each_item_block(block, dom, is_controlled, sibling) { - var current = /** @type {import('./types.js').TemplateNode} */ (block.d); + var current = /** @type {import('../../types.js').TemplateNode} */ (block.d); if (sibling === null) { if (is_controlled) { @@ -716,7 +776,7 @@ function insert_each_item_block(block, dom, is_controlled, sibling) { } /** - * @param {import('./types.js').Block} block + * @param {import('../../types.js').Block} block * @returns {Text | Element | Comment} */ function get_first_child(block) { @@ -730,7 +790,7 @@ function get_first_child(block) { } /** - * @param {Array} active_transitions + * @param {Array} active_transitions * @returns {void} */ function destroy_active_transition_blocks(active_transitions) { @@ -755,7 +815,7 @@ function destroy_active_transition_blocks(active_transitions) { } /** - * @param {import('./types.js').Block} block + * @param {import('../../types.js').Block} block * @returns {Text | Element | Comment} */ export function get_first_element(block) { @@ -774,7 +834,7 @@ export function get_first_element(block) { } /** - * @param {import('./types.js').EachItemBlock} block + * @param {import('../../types.js').EachItemBlock} block * @param {any} item * @param {number} index * @param {number} type @@ -793,15 +853,15 @@ function update_each_item_block(block, item, index, type) { each_animation(block, transitions); } if (index_is_reactive) { - set_signal_value(/** @type {import('./types.js').Signal} */ (block.i), index); + set_signal_value(/** @type {import('../../types.js').Signal} */ (block.i), index); } else { block.i = index; } } /** - * @param {import('./types.js').EachItemBlock} block - * @param {null | Array} transition_block + * @param {import('../../types.js').EachItemBlock} block + * @param {null | Array} transition_block * @param {boolean} apply_transitions * @param {any} controlled * @returns {void} @@ -835,7 +895,7 @@ export function destroy_each_item_block( if (!controlled && dom !== null) { remove(dom); } - destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); + destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (block.e)); } /** @@ -843,24 +903,24 @@ export function destroy_each_item_block( * @param {V} item * @param {unknown} key * @param {number} index - * @param {(anchor: null, item: V, index: number | import('./types.js').Signal) => void} render_fn + * @param {(anchor: null, item: V, index: number | import('../../types.js').Signal) => void} render_fn * @param {number} flags - * @returns {import('./types.js').EachItemBlock} + * @returns {import('../../types.js').EachItemBlock} */ function each_item_block(item, key, index, render_fn, flags) { const each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; const item_value = each_item_not_reactive ? item - : (flags & EACH_IS_IMMUTABLE) === 0 - ? mutable_source(item) - : source(item); + : (flags & EACH_IS_STRICT_EQUALS) !== 0 + ? source(item) + : mutable_source(item); const index_value = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); const block = create_each_item_block(item_value, index_value, key); const effect = render_effect( - /** @param {import('./types.js').EachItemBlock} block */ + /** @param {import('../../types.js').EachItemBlock} block */ (block) => { render_fn(null, block.v, block.i); }, diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js new file mode 100644 index 0000000000..75cfd7faed --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -0,0 +1,186 @@ +import { IF_BLOCK } from '../../block.js'; +import { + current_hydration_fragment, + hydrate_block_anchor, + hydrating, + set_current_hydration_fragment +} from '../../hydration.js'; +import { remove } from '../../reconciler.js'; +import { + current_block, + destroy_signal, + execute_effect, + push_destroy_fn, + render_effect +} from '../../runtime.js'; +import { trigger_transitions } from '../../transitions.js'; + +/** @returns {import('../../types.js').IfBlock} */ +function create_if_block() { + return { + // alternate transitions + a: null, + // alternate effect + ae: null, + // consequent transitions + c: null, + // consequent effect + ce: null, + // dom + d: null, + // effect + e: null, + // parent + p: /** @type {import('../../types.js').Block} */ (current_block), + // transition + r: null, + // type + t: IF_BLOCK, + // value + v: false + }; +} + +/** + * @param {Comment} anchor_node + * @param {() => boolean} condition_fn + * @param {(anchor: Node) => void} consequent_fn + * @param {null | ((anchor: Node) => void)} alternate_fn + * @returns {void} + */ +export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { + const block = create_if_block(); + hydrate_block_anchor(anchor_node); + /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ + let mismatch = false; + + /** @type {null | import('../../types.js').TemplateNode | Array} */ + let consequent_dom = null; + /** @type {null | import('../../types.js').TemplateNode | Array} */ + let alternate_dom = null; + let has_mounted = false; + /** + * @type {import('../../types.js').EffectSignal | null} + */ + let current_branch_effect = null; + + const if_effect = render_effect( + () => { + const result = !!condition_fn(); + if (block.v !== result || !has_mounted) { + block.v = result; + if (has_mounted) { + const consequent_transitions = block.c; + const alternate_transitions = block.a; + if (result) { + if (alternate_transitions === null || alternate_transitions.size === 0) { + execute_effect(alternate_effect); + } else { + trigger_transitions(alternate_transitions, 'out'); + } + if (consequent_transitions === null || consequent_transitions.size === 0) { + execute_effect(consequent_effect); + } else { + trigger_transitions(consequent_transitions, 'in'); + } + } else { + if (consequent_transitions === null || consequent_transitions.size === 0) { + execute_effect(consequent_effect); + } else { + trigger_transitions(consequent_transitions, 'out'); + } + if (alternate_transitions === null || alternate_transitions.size === 0) { + execute_effect(alternate_effect); + } else { + trigger_transitions(alternate_transitions, 'in'); + } + } + } else if (hydrating) { + const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; + if ( + !comment_text || + (comment_text === 'ssr:if:true' && !result) || + (comment_text === 'ssr:if:false' && result) + ) { + // Hydration mismatch: remove everything inside the anchor and start fresh. + // This could happen using when `{#if browser} .. {/if}` in SvelteKit. + remove(current_hydration_fragment); + set_current_hydration_fragment(null); + mismatch = true; + } else { + // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm + current_hydration_fragment.shift(); + } + } + has_mounted = true; + } + }, + block, + false + ); + // Managed effect + const consequent_effect = render_effect( + ( + /** @type {any} */ _, + /** @type {import('../../types.js').EffectSignal | null} */ consequent_effect + ) => { + const result = block.v; + if (!result && consequent_dom !== null) { + remove(consequent_dom); + consequent_dom = null; + } + if (result && current_branch_effect !== consequent_effect) { + consequent_fn(anchor_node); + if (mismatch && current_branch_effect === null) { + // Set fragment so that Svelte continues to operate in hydration mode + set_current_hydration_fragment([]); + } + current_branch_effect = consequent_effect; + consequent_dom = block.d; + } + block.d = null; + }, + block, + true + ); + block.ce = consequent_effect; + // Managed effect + const alternate_effect = render_effect( + ( + /** @type {any} */ _, + /** @type {import('../../types.js').EffectSignal | null} */ alternate_effect + ) => { + const result = block.v; + if (result && alternate_dom !== null) { + remove(alternate_dom); + alternate_dom = null; + } + if (!result && current_branch_effect !== alternate_effect) { + if (alternate_fn !== null) { + alternate_fn(anchor_node); + } + if (mismatch && current_branch_effect === null) { + // Set fragment so that Svelte continues to operate in hydration mode + set_current_hydration_fragment([]); + } + current_branch_effect = alternate_effect; + alternate_dom = block.d; + } + block.d = null; + }, + block, + true + ); + block.ae = alternate_effect; + push_destroy_fn(if_effect, () => { + if (consequent_dom !== null) { + remove(consequent_dom); + } + if (alternate_dom !== null) { + remove(alternate_dom); + } + destroy_signal(consequent_effect); + destroy_signal(alternate_effect); + }); + block.e = if_effect; +} diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js new file mode 100644 index 0000000000..fda8828475 --- /dev/null +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -0,0 +1,146 @@ +import { KEY_BLOCK } from '../../block.js'; +import { hydrate_block_anchor } from '../../hydration.js'; +import { remove } from '../../reconciler.js'; +import { + UNINITIALIZED, + current_block, + destroy_signal, + execute_effect, + push_destroy_fn, + render_effect, + safe_not_equal +} from '../../runtime.js'; +import { trigger_transitions } from '../../transitions.js'; + +/** @returns {import('../../types.js').KeyBlock} */ +function create_key_block() { + return { + // dom + d: null, + // effect + e: null, + // parent + p: /** @type {import('../../types.js').Block} */ (current_block), + // transition + r: null, + // type + t: KEY_BLOCK + }; +} + +/** + * @template V + * @param {Comment} anchor_node + * @param {() => V} key + * @param {(anchor: Node) => void} render_fn + * @returns {void} + */ +export function key_block(anchor_node, key, render_fn) { + const block = create_key_block(); + + /** @type {null | import('../../types.js').Render} */ + let current_render = null; + hydrate_block_anchor(anchor_node); + + /** @type {V | typeof UNINITIALIZED} */ + let key_value = UNINITIALIZED; + let mounted = false; + block.r = + /** + * @param {import('../../types.js').Transition} transition + * @returns {void} + */ + (transition) => { + const render = /** @type {import('../../types.js').Render} */ (current_render); + const transitions = render.s; + transitions.add(transition); + transition.f(() => { + transitions.delete(transition); + if (transitions.size === 0) { + // If the current render has changed since, then we can remove the old render + // effect as it's stale. + if (current_render !== render && render.e !== null) { + if (render.d !== null) { + remove(render.d); + render.d = null; + } + destroy_signal(render.e); + render.e = null; + } + } + }); + }; + const create_render_effect = () => { + /** @type {import('../../types.js').Render} */ + const render = { + d: null, + e: null, + s: new Set(), + p: current_render + }; + const effect = render_effect( + () => { + render_fn(anchor_node); + render.d = block.d; + block.d = null; + }, + block, + true, + true + ); + render.e = effect; + current_render = render; + }; + const render = () => { + const render = current_render; + if (render === null) { + create_render_effect(); + return; + } + const transitions = render.s; + if (transitions.size === 0) { + if (render.d !== null) { + remove(render.d); + render.d = null; + } + if (render.e) { + execute_effect(render.e); + } else { + create_render_effect(); + } + } else { + trigger_transitions(transitions, 'out'); + create_render_effect(); + } + }; + const key_effect = render_effect( + () => { + const prev_key_value = key_value; + key_value = key(); + if (mounted && safe_not_equal(prev_key_value, key_value)) { + render(); + } + }, + block, + false + ); + // To ensure topological ordering of the key effect to the render effect, + // we trigger the effect after. + render(); + mounted = true; + push_destroy_fn(key_effect, () => { + let render = current_render; + while (render !== null) { + const dom = render.d; + if (dom !== null) { + remove(dom); + } + const effect = render.e; + if (effect !== null) { + destroy_signal(effect); + } + render = render.p; + } + }); + block.e = key_effect; +} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 84b7c529b1..a0efcd8e6b 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -12,9 +12,6 @@ import { } from './operations.js'; import { create_root_block, - create_if_block, - create_key_block, - create_await_block, create_dynamic_element_block, create_head_block, create_dynamic_component_block, @@ -39,12 +36,9 @@ import { is_signal, push_destroy_fn, execute_effect, - UNINITIALIZED, untrack, effect, - flushSync, flush_sync, - safe_not_equal, current_block, managed_effect, push, @@ -1453,151 +1447,6 @@ export function slot(anchor_node, slot_fn, slot_props, fallback_fn) { } } -/** - * @param {Comment} anchor_node - * @param {() => boolean} condition_fn - * @param {(anchor: Node) => void} consequent_fn - * @param {null | ((anchor: Node) => void)} alternate_fn - * @returns {void} - */ -function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) { - const block = create_if_block(); - hydrate_block_anchor(anchor_node); - /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - let mismatch = false; - - /** @type {null | import('./types.js').TemplateNode | Array} */ - let consequent_dom = null; - /** @type {null | import('./types.js').TemplateNode | Array} */ - let alternate_dom = null; - let has_mounted = false; - /** - * @type {import("./types.js").EffectSignal | null} - */ - let current_branch_effect = null; - - const if_effect = render_effect( - () => { - const result = !!condition_fn(); - if (block.v !== result || !has_mounted) { - block.v = result; - if (has_mounted) { - const consequent_transitions = block.c; - const alternate_transitions = block.a; - if (result) { - if (alternate_transitions === null || alternate_transitions.size === 0) { - execute_effect(alternate_effect); - } else { - trigger_transitions(alternate_transitions, 'out'); - } - if (consequent_transitions === null || consequent_transitions.size === 0) { - execute_effect(consequent_effect); - } else { - trigger_transitions(consequent_transitions, 'in'); - } - } else { - if (consequent_transitions === null || consequent_transitions.size === 0) { - execute_effect(consequent_effect); - } else { - trigger_transitions(consequent_transitions, 'out'); - } - if (alternate_transitions === null || alternate_transitions.size === 0) { - execute_effect(alternate_effect); - } else { - trigger_transitions(alternate_transitions, 'in'); - } - } - } else if (hydrating) { - const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; - if ( - !comment_text || - (comment_text === 'ssr:if:true' && !result) || - (comment_text === 'ssr:if:false' && result) - ) { - // Hydration mismatch: remove everything inside the anchor and start fresh. - // This could happen using when `{#if browser} .. {/if}` in SvelteKit. - remove(current_hydration_fragment); - set_current_hydration_fragment(null); - mismatch = true; - } else { - // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm - current_hydration_fragment.shift(); - } - } - has_mounted = true; - } - }, - block, - false - ); - // Managed effect - const consequent_effect = render_effect( - ( - /** @type {any} */ _, - /** @type {import("./types.js").EffectSignal | null} */ consequent_effect - ) => { - const result = block.v; - if (!result && consequent_dom !== null) { - remove(consequent_dom); - consequent_dom = null; - } - if (result && current_branch_effect !== consequent_effect) { - consequent_fn(anchor_node); - if (mismatch && current_branch_effect === null) { - // Set fragment so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); - } - current_branch_effect = consequent_effect; - consequent_dom = block.d; - } - block.d = null; - }, - block, - true - ); - block.ce = consequent_effect; - // Managed effect - const alternate_effect = render_effect( - ( - /** @type {any} */ _, - /** @type {import("./types.js").EffectSignal | null} */ alternate_effect - ) => { - const result = block.v; - if (result && alternate_dom !== null) { - remove(alternate_dom); - alternate_dom = null; - } - if (!result && current_branch_effect !== alternate_effect) { - if (alternate_fn !== null) { - alternate_fn(anchor_node); - } - if (mismatch && current_branch_effect === null) { - // Set fragment so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); - } - current_branch_effect = alternate_effect; - alternate_dom = block.d; - } - block.d = null; - }, - block, - true - ); - block.ae = alternate_effect; - push_destroy_fn(if_effect, () => { - if (consequent_dom !== null) { - remove(consequent_dom); - } - if (alternate_dom !== null) { - remove(alternate_dom); - } - destroy_signal(consequent_effect); - destroy_signal(alternate_effect); - }); - block.e = if_effect; -} -export { if_block as if }; - /** * @param {(anchor: Node | null) => void} render_fn * @returns {void} @@ -1876,293 +1725,6 @@ export function component(anchor_node, component_fn, render_fn) { block.e = component_effect; } -/** - * @template V - * @param {Comment} anchor_node - * @param {(() => Promise)} input - * @param {null | ((anchor: Node) => void)} pending_fn - * @param {null | ((anchor: Node, value: V) => void)} then_fn - * @param {null | ((anchor: Node, error: unknown) => void)} catch_fn - * @returns {void} - */ -function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { - const block = create_await_block(); - - /** @type {null | import('./types.js').Render} */ - let current_render = null; - hydrate_block_anchor(anchor_node); - - /** @type {{}} */ - let latest_token; - - /** @type {typeof UNINITIALIZED | V} */ - let resolved_value = UNINITIALIZED; - - /** @type {unknown} */ - let error = UNINITIALIZED; - let pending = false; - block.r = - /** - * @param {import('./types.js').Transition} transition - * @returns {void} - */ - (transition) => { - const render = /** @type {import('./types.js').Render} */ (current_render); - const transitions = render.s; - transitions.add(transition); - transition.f(() => { - transitions.delete(transition); - if (transitions.size === 0) { - // If the current render has changed since, then we can remove the old render - // effect as it's stale. - if (current_render !== render && render.e !== null) { - if (render.d !== null) { - remove(render.d); - render.d = null; - } - destroy_signal(render.e); - render.e = null; - } - } - }); - }; - const create_render_effect = () => { - /** @type {import('./types.js').Render} */ - const render = { - d: null, - e: null, - s: new Set(), - p: current_render - }; - const effect = render_effect( - () => { - if (error === UNINITIALIZED) { - if (resolved_value === UNINITIALIZED) { - // pending = true - block.n = true; - if (pending_fn !== null) { - pending_fn(anchor_node); - } - } else if (then_fn !== null) { - // pending = false - block.n = false; - then_fn(anchor_node, resolved_value); - } - } else if (catch_fn !== null) { - // pending = false - block.n = false; - catch_fn(anchor_node, error); - } - render.d = block.d; - block.d = null; - }, - block, - true, - true - ); - render.e = effect; - current_render = render; - }; - const render = () => { - const render = current_render; - if (render === null) { - create_render_effect(); - return; - } - const transitions = render.s; - if (transitions.size === 0) { - if (render.d !== null) { - remove(render.d); - render.d = null; - } - if (render.e) { - execute_effect(render.e); - } else { - create_render_effect(); - } - } else { - create_render_effect(); - trigger_transitions(transitions, 'out'); - } - }; - const await_effect = render_effect( - () => { - const token = {}; - latest_token = token; - const promise = input(); - if (is_promise(promise)) { - promise.then( - /** @param {V} v */ - (v) => { - if (latest_token === token) { - // Ensure UI is in sync before resolving value. - flushSync(); - resolved_value = v; - pending = false; - render(); - } - }, - /** @param {unknown} _error */ - (_error) => { - error = _error; - pending = false; - render(); - } - ); - if (resolved_value !== UNINITIALIZED || error !== UNINITIALIZED) { - error = UNINITIALIZED; - resolved_value = UNINITIALIZED; - } - if (!pending) { - pending = true; - render(); - } - } else { - error = UNINITIALIZED; - resolved_value = promise; - pending = false; - render(); - } - }, - block, - false - ); - push_destroy_fn(await_effect, () => { - let render = current_render; - latest_token = {}; - while (render !== null) { - const dom = render.d; - if (dom !== null) { - remove(dom); - } - const effect = render.e; - if (effect !== null) { - destroy_signal(effect); - } - render = render.p; - } - }); - block.e = await_effect; -} -export { await_block as await }; - -/** - * @template V - * @param {Comment} anchor_node - * @param {() => V} key - * @param {(anchor: Node) => void} render_fn - * @returns {void} - */ -export function key(anchor_node, key, render_fn) { - const block = create_key_block(); - - /** @type {null | import('./types.js').Render} */ - let current_render = null; - hydrate_block_anchor(anchor_node); - - /** @type {V | typeof UNINITIALIZED} */ - let key_value = UNINITIALIZED; - let mounted = false; - block.r = - /** - * @param {import('./types.js').Transition} transition - * @returns {void} - */ - (transition) => { - const render = /** @type {import('./types.js').Render} */ (current_render); - const transitions = render.s; - transitions.add(transition); - transition.f(() => { - transitions.delete(transition); - if (transitions.size === 0) { - // If the current render has changed since, then we can remove the old render - // effect as it's stale. - if (current_render !== render && render.e !== null) { - if (render.d !== null) { - remove(render.d); - render.d = null; - } - destroy_signal(render.e); - render.e = null; - } - } - }); - }; - const create_render_effect = () => { - /** @type {import('./types.js').Render} */ - const render = { - d: null, - e: null, - s: new Set(), - p: current_render - }; - const effect = render_effect( - () => { - render_fn(anchor_node); - render.d = block.d; - block.d = null; - }, - block, - true, - true - ); - render.e = effect; - current_render = render; - }; - const render = () => { - const render = current_render; - if (render === null) { - create_render_effect(); - return; - } - const transitions = render.s; - if (transitions.size === 0) { - if (render.d !== null) { - remove(render.d); - render.d = null; - } - if (render.e) { - execute_effect(render.e); - } else { - create_render_effect(); - } - } else { - trigger_transitions(transitions, 'out'); - create_render_effect(); - } - }; - const key_effect = render_effect( - () => { - const prev_key_value = key_value; - key_value = key(); - if (mounted && safe_not_equal(prev_key_value, key_value)) { - render(); - } - }, - block, - false - ); - // To ensure topological ordering of the key effect to the render effect, - // we trigger the effect after. - render(); - mounted = true; - push_destroy_fn(key_effect, () => { - let render = current_render; - while (render !== null) { - const dom = render.d; - if (dom !== null) { - remove(dom); - } - const effect = render.e; - if (effect !== null) { - destroy_signal(effect); - } - render = render.p; - } - }); - block.e = key_effect; -} - /** * @param {Element | Text | Comment} anchor * @param {boolean} is_html @@ -2836,7 +2398,7 @@ export function createRoot() { } /** - * Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component + * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * @template {Record} Props * @template {Record} Exports @@ -2860,7 +2422,7 @@ export function mount(component, options) { } /** - * Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component + * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * @template {Record} Props * @template {Record} Exports @@ -2932,7 +2494,7 @@ export function hydrate(component, options) { * @template {Record} Props * @template {Record} Exports * @template {Record} Events - * @param {import('../../main/public.js').ComponentType>} component + * @param {import('../../main/public.js').ComponentType>} Component * @param {{ * target: Node; * anchor: null | Text; @@ -2944,14 +2506,14 @@ export function hydrate(component, options) { * }} options * @returns {Exports} */ -function _mount(component, options) { +function _mount(Component, options) { const registered_events = new Set(); const container = options.target; const block = create_root_block(options.intro || false); /** @type {Exports} */ // @ts-expect-error will be defined because the render effect runs synchronously - let accessors = undefined; + let component = undefined; const effect = render_effect( () => { @@ -2961,7 +2523,7 @@ function _mount(component, options) { options.context; } // @ts-expect-error the public typings are not what the actual function looks like - accessors = component(options.anchor, options.props || {}) || {}; + component = Component(options.anchor, options.props || {}) || {}; if (options.context) { pop(); } @@ -3008,7 +2570,7 @@ function _mount(component, options) { event_handle(array_from(all_registerd_events)); root_event_handles.add(event_handle); - mounted_components.set(accessors, () => { + mounted_components.set(component, () => { for (const event_name of registered_events) { container.removeEventListener(event_name, bound_event_listener); } @@ -3020,11 +2582,11 @@ function _mount(component, options) { destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e)); }); - return accessors; + return component; } /** - * References of the accessors of all components that were `mount`ed or `hydrate`d. + * References of the components that were mounted or hydrated. * Uses a `WeakMap` to avoid memory leaks. */ let mounted_components = new WeakMap(); @@ -3034,12 +2596,12 @@ let mounted_components = new WeakMap(); * @param {Record} component */ export function unmount(component) { - const destroy = mounted_components.get(component); - if (DEV && !destroy) { + const fn = mounted_components.get(component); + if (DEV && !fn) { // eslint-disable-next-line no-console console.warn('Tried to unmount a component that was not mounted.'); } - destroy?.(); + fn?.(); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index dbf0036e89..8db5cfa54f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1210,8 +1210,11 @@ export function set_signal_value(signal, value) { // // $effect(() => x++) // + // We additionally want to skip this logic for when ignore_mutation_validation is + // true, as stores write to source signal on initialization. if ( is_runes(null) && + !ignore_mutation_validation && current_effect !== null && current_effect.c === null && (current_effect.f & CLEAN) !== 0 @@ -1884,8 +1887,8 @@ function on_destroy(fn) { */ export function push(props, runes = false) { current_component_context = { - // accessors - a: null, + // exports (and props, if `accessors: true`) + x: null, // context c: null, // effects @@ -1906,14 +1909,15 @@ export function push(props, runes = false) { } /** - * @param {Record} [accessors] - * @returns {void} + * @template {Record} T + * @param {T} [component] + * @returns {T} */ -export function pop(accessors) { +export function pop(component) { const context_stack_item = current_component_context; if (context_stack_item !== null) { - if (accessors !== undefined) { - context_stack_item.a = accessors; + if (component !== undefined) { + context_stack_item.x = component; } const effects = context_stack_item.e; if (effects !== null) { @@ -1925,6 +1929,9 @@ export function pop(accessors) { current_component_context = context_stack_item.p; context_stack_item.m = true; } + // Micro-optimization: Don't set .a above to the empty object + // so it can be garbage-collected when the return here is unused + return component || /** @type {T} */ ({}); } /** diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 68ae725423..01350350e9 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -9,7 +9,7 @@ import { KEY_BLOCK, ROOT_BLOCK } from './block.js'; -import { destroy_each_item_block, get_first_element } from './each.js'; +import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; import { append_child, empty } from './operations.js'; import { current_block, diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 7d34789d3b..937028a84e 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -40,8 +40,8 @@ export type ComponentContext = { d: null | Signal[]; /** props */ s: Record; - /** accessors */ - a: Record | null; + /** exports (and props, if `accessors: true`) */ + x: Record | null; /** effects */ e: null | Array; /** mounted */ diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 9077585a33..31e52917c0 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -40,7 +40,10 @@ export { freeze, init } from './client/runtime.js'; -export * from './client/each.js'; +export { await_block as await } from './client/dom/blocks/await.js'; +export { if_block as if } from './client/dom/blocks/if.js'; +export { key_block as key } from './client/dom/blocks/key.js'; +export * from './client/dom/blocks/each.js'; export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js'; diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index b7c242fb82..1b2ba77725 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -164,7 +164,7 @@ export function createEventDispatcher() { // in a server (non-DOM) environment? const event = create_custom_event(/** @type {string} */ (type), detail, options); for (const fn of callbacks) { - fn.call(component_context.a, event); + fn.call(component_context.x, event); } return !event.defaultPrevented; } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c0bd4f503f..d0adcba3c8 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.57'; +export const VERSION = '5.0.0-next.60'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/_config.js b/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/_config.js new file mode 100644 index 0000000000..90fcbfe2c0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + immutable: true, + + test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual(target.innerHTML, `
  • test !!!
`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/main.svelte b/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/main.svelte new file mode 100644 index 0000000000..0694d3f34f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/immutable-each-equals/main.svelte @@ -0,0 +1,17 @@ + + + + +
    + {#each items as item (item.id)} +
  • {item.value}
  • + {/each} +
+ diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-accessors/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-accessors/_config.js index 0dba67bb39..9f49355214 100644 --- a/packages/svelte/tests/runtime-legacy/samples/prop-accessors/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/prop-accessors/_config.js @@ -1,7 +1,12 @@ import { test } from '../../test'; export default test({ + compileOptions: { + dev: true + }, + accessors: false, + test({ assert, component }) { assert.equal(component.foo1, 42); assert.equal(component.foo2(), 42); diff --git a/packages/svelte/tests/runtime-legacy/samples/renamed-instance-exports/_config.js b/packages/svelte/tests/runtime-legacy/samples/renamed-instance-exports/_config.js index 9293327b13..29860bb9df 100644 --- a/packages/svelte/tests/runtime-legacy/samples/renamed-instance-exports/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/renamed-instance-exports/_config.js @@ -1,6 +1,10 @@ import { test } from '../../test'; export default test({ + compileOptions: { + dev: true + }, + test({ assert, component }) { assert.equal(component.bar1, 42); assert.equal(component.bar2, 42); diff --git a/packages/svelte/tests/runtime-runes/samples/store-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-directive/main.svelte index 48d774f39d..14166c48f2 100644 --- a/packages/svelte/tests/runtime-runes/samples/store-directive/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/store-directive/main.svelte @@ -1,9 +1,11 @@ -
hello
+
hello
diff --git a/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/_config.js b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/_config.js new file mode 100644 index 0000000000..13ada44925 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/_config.js @@ -0,0 +1,22 @@ +import { test } from '../../test'; +import { log } from './log.js'; + +export default test({ + html: ``, + + before_test() { + log.length = 0; + }, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + assert.deepEqual(log, [1]); + + await btn?.click(); + assert.deepEqual(log, [1, 2]); + + await btn?.click(); + assert.deepEqual(log, [1, 2, 3]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/log.js b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/log.js new file mode 100644 index 0000000000..d3df521f4d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/log.js @@ -0,0 +1,2 @@ +/** @type {any[]} */ +export const log = []; diff --git a/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/main.svelte b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/main.svelte new file mode 100644 index 0000000000..b6f3cd9c5c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/store-subscribe-effect-init/main.svelte @@ -0,0 +1,14 @@ + + + diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_config.js b/packages/svelte/tests/snapshot/samples/bind-this/_config.js new file mode 100644 index 0000000000..f47bee71df --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/bind-this/_config.js @@ -0,0 +1,3 @@ +import { test } from '../../test'; + +export default test({}); diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js new file mode 100644 index 0000000000..0f261e1c81 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -0,0 +1,17 @@ +// index.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import "svelte/internal/disclose-version"; +import * as $ from "svelte/internal"; + +export default function Bind_this($$anchor, $$props) { + $.push($$props, false); + $.init(); + + /* Init */ + var fragment = $.comment($$anchor); + var node = $.child_frag(fragment); + + $.bind_this(Foo(node, {}), ($$value) => foo = $$value, foo); + $.close_frag($$anchor, fragment); + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js new file mode 100644 index 0000000000..71b688fe37 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js @@ -0,0 +1,14 @@ +// index.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import * as $ from "svelte/internal/server"; + +export default function Bind_this($$payload, $$props) { + $.push(false); + + const anchor = $.create_anchor($$payload); + + $$payload.out += `${anchor}`; + Foo($$payload, {}); + $$payload.out += `${anchor}`; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-this/index.svelte b/packages/svelte/tests/snapshot/samples/bind-this/index.svelte new file mode 100644 index 0000000000..da5a2c5085 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/bind-this/index.svelte @@ -0,0 +1 @@ + diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 9cacea8d63..0ae06e727f 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -67,6 +67,10 @@ export function suite_with_variants( cwd: string, samples_dir = 'samples', @@ -76,13 +80,14 @@ async function for_each_dir( let created_test = false; for (const dir of fs.readdirSync(`${cwd}/${samples_dir}`)) { - if (dir[0] === '.') continue; + if (dir[0] === '.' || !filter.test(dir)) continue; - const file = `${cwd}/${samples_dir}/${dir}/_config.js`; - if (!filter.test(dir)) { + if (fs.readdirSync(`${cwd}/${samples_dir}/${dir}`).every((file) => ignored.includes(file))) { continue; } + const file = `${cwd}/${samples_dir}/${dir}/_config.js`; + created_test = true; const config = (fs.existsSync(file) ? (await import(file)).default : {}) as Test; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 38c303b94e..db6efb9fb4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -328,7 +328,7 @@ declare module 'svelte' { */ export function createRoot(): void; /** - * Mounts a component to the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component + * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ export function mount, Exports extends Record, Events extends Record>(component: ComponentType>, options: { @@ -339,7 +339,7 @@ declare module 'svelte' { intro?: boolean | undefined; }): Exports; /** - * Hydrates a component on the given target and returns the exports and potentially the accessors (if compiled with `accessors: true`) of the component + * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ export function hydrate, Exports extends Record, Events extends Record>(component: ComponentType>, options: { diff --git a/sites/svelte-5-preview/svelte.config.js b/sites/svelte-5-preview/svelte.config.js index 22e315ec92..2c3f46b343 100644 --- a/sites/svelte-5-preview/svelte.config.js +++ b/sites/svelte-5-preview/svelte.config.js @@ -15,6 +15,6 @@ export default { }, vitePlugin: { - inspector: true + inspector: false } };