pull/14428/head
Rich Harris 2 years ago
commit c664b96eb3

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly prune each blocks

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: provide temporary `LegacyComponentType`

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: attach spread attribute events synchronously

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: ensure last empty text node correctly hydrates

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: correctly prune key blocks

@ -1,5 +1,31 @@
# svelte
## 5.2.9
### Patch Changes
- fix: show `:then` block for `null/undefined` value ([#14440](https://github.com/sveltejs/svelte/pull/14440))
- fix: relax html parent validation ([#14442](https://github.com/sveltejs/svelte/pull/14442))
- fix: prevent memory leak when creating deriveds inside untrack ([#14443](https://github.com/sveltejs/svelte/pull/14443))
- fix: disregard TypeScript nodes when pruning CSS ([#14446](https://github.com/sveltejs/svelte/pull/14446))
## 5.2.8
### Patch Changes
- fix: correctly prune each blocks ([#14403](https://github.com/sveltejs/svelte/pull/14403))
- fix: provide temporary `LegacyComponentType` ([#14257](https://github.com/sveltejs/svelte/pull/14257))
- fix: attach spread attribute events synchronously ([#14387](https://github.com/sveltejs/svelte/pull/14387))
- fix: ensure last empty text node correctly hydrates ([#14425](https://github.com/sveltejs/svelte/pull/14425))
- fix: correctly prune key blocks ([#14403](https://github.com/sveltejs/svelte/pull/14403))
## 5.2.7
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.2.7",
"version": "5.2.9",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -7,7 +7,6 @@ import { get_attribute_chunks, is_text_attribute } from '../../../utils/ast.js';
/**
* @typedef {{
* stylesheet: Compiler.Css.StyleSheet;
* element: Compiler.AST.RegularElement | Compiler.AST.SvelteElement;
* from_render_tag: boolean;
* }} State
@ -61,9 +60,9 @@ export function prune(stylesheet, element) {
const parent = get_element_parent(element);
if (!parent) return;
walk(stylesheet, { stylesheet, element: parent, from_render_tag: true }, visitors);
walk(stylesheet, { element: parent, from_render_tag: true }, visitors);
} else {
walk(stylesheet, { stylesheet, element, from_render_tag: false }, visitors);
walk(stylesheet, { element, from_render_tag: false }, visitors);
}
}
@ -882,117 +881,63 @@ function get_element_parent(node) {
}
/**
* Finds the given node's previous sibling in the DOM
*
* The Svelte `<slot>` is just a placeholder and is not actually real. Any children nodes
* in `<slot>` are 'flattened' and considered as the same level as the `<slot>`'s siblings
*
* e.g.
* ```html
* <h1>Heading 1</h1>
* <slot>
* <h2>Heading 2</h2>
* </slot>
* ```
*
* is considered to look like:
* ```html
* <h1>Heading 1</h1>
* <h2>Heading 2</h2>
* ```
* @param {Compiler.SvelteNode} node
* @returns {Compiler.SvelteNode}
*/
function find_previous_sibling(node) {
/** @type {Compiler.SvelteNode} */
let current_node = node;
while (
// @ts-expect-error TODO
!current_node.prev &&
// @ts-expect-error TODO
current_node.parent?.type === 'SlotElement'
) {
// @ts-expect-error TODO
current_node = current_node.parent;
}
// @ts-expect-error
current_node = current_node.prev;
while (current_node?.type === 'SlotElement') {
const slot_children = current_node.fragment.nodes;
if (slot_children.length > 0) {
current_node = slot_children[slot_children.length - 1];
} else {
break;
}
}
return current_node;
}
/**
* @param {Compiler.SvelteNode} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element
* @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>}
*/
function get_possible_element_siblings(node, adjacent_only) {
function get_possible_element_siblings(element, adjacent_only) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag, NodeExistsValue>} */
const result = new Map();
const path = element.metadata.path;
/** @type {Compiler.SvelteNode} */
let prev = node;
while ((prev = find_previous_sibling(prev))) {
if (prev.type === 'RegularElement') {
if (
!prev.attributes.find(
let current = element;
let i = path.length;
while (i--) {
const fragment = /** @type {Compiler.AST.Fragment} */ (path[i--]);
let j = fragment.nodes.indexOf(current);
while (j--) {
const node = fragment.nodes[j];
if (node.type === 'RegularElement') {
const has_slot_attribute = node.attributes.some(
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
)
) {
result.set(prev, NODE_DEFINITELY_EXISTS);
}
if (adjacent_only) {
break;
}
} else if (is_block(prev)) {
const possible_last_child = get_possible_last_child(prev, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
} else if (
prev.type === 'SlotElement' ||
prev.type === 'RenderTag' ||
prev.type === 'SvelteElement'
) {
result.set(prev, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
}
}
);
if (!prev || !adjacent_only) {
/** @type {Compiler.SvelteNode | null} */
let parent = node;
if (!has_slot_attribute) {
result.set(node, NODE_DEFINITELY_EXISTS);
while (
// @ts-expect-error TODO
(parent = parent?.parent) &&
is_block(parent)
) {
const possible_siblings = get_possible_element_siblings(parent, adjacent_only);
add_to_map(possible_siblings, result);
if (adjacent_only) {
return result;
}
}
} else if (is_block(node)) {
if (node.type === 'SlotElement') {
result.set(node, NODE_PROBABLY_EXISTS);
}
// @ts-expect-error
if (parent.type === 'EachBlock' && !parent.fallback?.nodes.includes(node)) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(parent, adjacent_only), result);
const possible_last_child = get_possible_last_child(node, adjacent_only);
add_to_map(possible_last_child, result);
if (adjacent_only && has_definite_elements(possible_last_child)) {
return result;
}
} else if (node.type === 'RenderTag' || node.type === 'SvelteElement') {
result.set(node, NODE_PROBABLY_EXISTS);
// Special case: slots, render tags and svelte:element tags could resolve to no siblings,
// so we want to continue until we find a definite sibling even with the adjacent-only combinator
}
}
if (adjacent_only && has_definite_elements(possible_siblings)) {
break;
}
current = path[i];
if (!current || !is_block(current)) break;
if (current.type === 'EachBlock' && fragment === current.body) {
// `{#each ...}<a /><b />{/each}` — `<b>` can be previous sibling of `<a />`
add_to_map(get_possible_last_child(current, adjacent_only), result);
}
}
@ -1000,7 +945,7 @@ function get_possible_element_siblings(node, adjacent_only) {
}
/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock} node
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement} node
* @param {boolean} adjacent_only
* @returns {Map<Compiler.AST.RegularElement, NodeExistsValue>}
*/
@ -1024,6 +969,7 @@ function get_possible_last_child(node, adjacent_only) {
break;
case 'KeyBlock':
case 'SlotElement':
fragments.push(node.fragment);
break;
}
@ -1031,7 +977,7 @@ function get_possible_last_child(node, adjacent_only) {
/** @type {NodeMap} */
const result = new Map();
let exhaustive = true;
let exhaustive = node.type !== 'SlotElement';
for (const fragment of fragments) {
if (fragment == null) {
@ -1123,13 +1069,14 @@ function loop_child(children, adjacent_only) {
/**
* @param {Compiler.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock}
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
*/
function is_block(node) {
return (
node.type === 'IfBlock' ||
node.type === 'EachBlock' ||
node.type === 'AwaitBlock' ||
node.type === 'KeyBlock'
node.type === 'KeyBlock' ||
node.type === 'SlotElement'
);
}

@ -287,6 +287,7 @@ export function analyze_component(root, source, options) {
const store_name = name.slice(1);
const declaration = instance.scope.get(store_name);
const init = /** @type {Node | undefined} */ (declaration?.initial);
// If we're not in legacy mode through the compiler option, assume the user
// is referencing a rune and not a global store.
@ -295,9 +296,9 @@ export function analyze_component(root, source, options) {
!is_rune(name) ||
(declaration !== null &&
// const state = $state(0) is valid
(get_rune(declaration.initial, instance.scope) === null ||
(get_rune(init, instance.scope) === null ||
// rune-line names received as props are valid too (but we have to protect against $props as store)
(store_name !== 'props' && get_rune(declaration.initial, instance.scope) === '$props')) &&
(store_name !== 'props' && get_rune(init, instance.scope) === '$props')) &&
// allow `import { derived } from 'svelte/store'` in the same file as `const x = $derived(..)` because one is not a subscription to the other
!(
name === '$derived' &&

@ -259,7 +259,8 @@ export function should_proxy(node, scope) {
binding.initial.type !== 'FunctionDeclaration' &&
binding.initial.type !== 'ClassDeclaration' &&
binding.initial.type !== 'ImportDeclaration' &&
binding.initial.type !== 'EachBlock'
binding.initial.type !== 'EachBlock' &&
binding.initial.type !== 'SnippetBlock'
) {
return should_proxy(binding.initial, null);
}

@ -75,7 +75,7 @@ export class Scope {
* @param {Identifier} node
* @param {Binding['kind']} kind
* @param {DeclarationKind} declaration_kind
* @param {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock} initial
* @param {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} initial
* @returns {Binding}
*/
declare(node, kind, declaration_kind, initial = null) {
@ -632,7 +632,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (is_top_level) {
scope = /** @type {Scope} */ (parent);
}
scope.declare(node.expression, 'normal', 'function', node.expression);
scope.declare(node.expression, 'normal', 'function', node);
const child_scope = state.scope.child();
scopes.set(node, child_scope);
@ -726,7 +726,7 @@ export function set_scope(node, { next, state }) {
/**
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {Node | AST.EachBlock | null | undefined} node
* @param {Node | null | undefined} node
* @param {Scope} scope
*/
export function get_rune(node, scope) {

@ -291,7 +291,8 @@ export interface Binding {
| FunctionDeclaration
| ClassDeclaration
| ImportDeclaration
| AST.EachBlock;
| AST.EachBlock
| AST.SnippetBlock;
is_called: boolean;
references: { node: Identifier; path: SvelteNode[] }[];
mutated: boolean;

@ -167,25 +167,23 @@ export function is_tag_valid_with_ancestor(tag, ancestors) {
* Returns false if the tag is not allowed inside the parent tag such that it will result
* in the browser repairing the HTML, which will likely result in an error during hydration.
* @param {string} tag
* @param {string | null} parent_tag
* @param {string} parent_tag
* @returns {boolean}
*/
export function is_tag_valid_with_parent(tag, parent_tag) {
if (tag.includes('-') || parent_tag?.includes('-')) return true; // custom elements can be anything
if (parent_tag !== null) {
const disallowed = disallowed_children[parent_tag];
const disallowed = disallowed_children[parent_tag];
if (disallowed) {
if ('direct' in disallowed && disallowed.direct.includes(tag)) {
return false;
}
if ('descendant' in disallowed && disallowed.descendant.includes(tag)) {
return false;
}
if ('only' in disallowed && disallowed.only) {
return disallowed.only.includes(tag);
}
if (disallowed) {
if ('direct' in disallowed && disallowed.direct.includes(tag)) {
return false;
}
if ('descendant' in disallowed && disallowed.descendant.includes(tag)) {
return false;
}
if ('only' in disallowed && disallowed.only) {
return disallowed.only.includes(tag);
}
}

@ -14,6 +14,7 @@ import {
} from '../../runtime.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { UNINITIALIZED } from '../../../../constants.js';
const PENDING = 0;
const THEN = 1;
@ -40,8 +41,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {any} */
var component_function = DEV ? component_context?.function : null;
/** @type {V | Promise<V> | null} */
var input;
/** @type {V | Promise<V> | typeof UNINITIALIZED} */
var input = UNINITIALIZED;
/** @type {Effect | null} */
var pending_effect;
@ -156,8 +157,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
update(THEN, false);
}
// Set the input to null, in order to disable the promise callbacks
return () => (input = null);
// Set the input to something else, in order to disable the promise callbacks
return () => (input = UNINITIALIZED);
});
if (hydrating) {

@ -42,6 +42,11 @@ export function derived(fn) {
active_effect.f |= EFFECT_HAS_DERIVED;
}
var parent_derived =
active_reaction !== null && (active_reaction.f & DERIVED) !== 0
? /** @type {Derived} */ (active_reaction)
: null;
/** @type {Derived<V>} */
const signal = {
children: null,
@ -53,12 +58,11 @@ export function derived(fn) {
reactions: null,
v: /** @type {V} */ (null),
version: 0,
parent: active_effect
parent: parent_derived ?? active_effect
};
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
var derived = /** @type {Derived} */ (active_reaction);
(derived.children ??= []).push(signal);
if (parent_derived !== null) {
(parent_derived.children ??= []).push(signal);
}
return signal;
@ -104,6 +108,21 @@ function destroy_derived_children(derived) {
*/
let stack = [];
/**
* @param {Derived} derived
* @returns {Effect | null}
*/
function get_derived_parent_effect(derived) {
var parent = derived.parent;
while (parent !== null) {
if ((parent.f & DERIVED) === 0) {
return /** @type {Effect} */ (parent);
}
parent = parent.parent;
}
return null;
}
/**
* @template T
* @param {Derived} derived
@ -113,7 +132,7 @@ export function execute_derived(derived) {
var value;
var prev_active_effect = active_effect;
set_active_effect(derived.parent);
set_active_effect(get_derived_parent_effect(derived));
if (DEV) {
let prev_inspect_effects = inspect_effects;
@ -162,14 +181,13 @@ export function update_derived(derived) {
}
/**
* @param {Derived} signal
* @param {Derived} derived
* @returns {void}
*/
export function destroy_derived(signal) {
destroy_derived_children(signal);
remove_reactions(signal, 0);
set_signal_status(signal, DESTROYED);
export function destroy_derived(derived) {
destroy_derived_children(derived);
remove_reactions(derived, 0);
set_signal_status(derived, DESTROYED);
// TODO we need to ensure we remove the derived from any parent derives
signal.v = signal.children = signal.deps = signal.ctx = signal.reactions = null;
derived.v = derived.children = derived.deps = derived.ctx = derived.reactions = null;
}

@ -23,7 +23,6 @@ export interface Reaction extends Signal {
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
parent: Effect | null;
}
export interface Derived<V = unknown> extends Value<V>, Reaction {
@ -31,6 +30,8 @@ export interface Derived<V = unknown> extends Value<V>, Reaction {
fn: () => V;
/** Reactions created inside this signal */
children: null | Reaction[];
/** Parent effect or derived */
parent: Effect | Derived | null;
}
export interface Effect extends Reaction {
@ -58,6 +59,8 @@ export interface Effect extends Reaction {
first: null | Effect;
/** Last child effect created inside this signal */
last: null | Effect;
/** Parent effect */
parent: Effect | null;
/** Dev only */
component_function?: any;
}

@ -767,9 +767,24 @@ export function get(signal) {
} else if (is_derived && /** @type {Derived} */ (signal).deps === null) {
var derived = /** @type {Derived} */ (signal);
var parent = derived.parent;
var target = derived;
if (parent !== null && !parent.deriveds?.includes(derived)) {
(parent.deriveds ??= []).push(derived);
while (parent !== null) {
// Attach the derived to the nearest parent effect, if there are deriveds
// in between then we also need to attach them too
if ((parent.f & DERIVED) !== 0) {
var parent_derived = /** @type {Derived} */ (parent);
target = parent_derived;
parent = parent_derived.parent;
} else {
var parent_effect = /** @type {Effect} */ (parent);
if (!parent_effect.deriveds?.includes(target)) {
(parent_effect.deriveds ??= []).push(target);
}
break;
}
}
}

@ -34,14 +34,12 @@ function stringify(element) {
/**
* @param {Payload} payload
* @param {Element | null} parent
* @param {Element} parent
* @param {Element} child
*/
function print_error(payload, parent, child) {
var message =
(parent === null
? `node_invalid_placement_ssr: ${stringify(child)} needs a valid parent element\n\n`
: `node_invalid_placement_ssr: ${stringify(parent)} cannot contain ${stringify(child)}\n\n`) +
`node_invalid_placement_ssr: ${stringify(parent)} cannot contain ${stringify(child)}\n\n` +
'This can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.';
if ((seen ??= new Set()).has(message)) return;
@ -85,8 +83,6 @@ export function push_element(payload, tag, line, column) {
}
ancestor = ancestor.parent;
}
} else if (!is_tag_valid_with_parent(tag, null)) {
print_error(payload, null, child);
}
parent = child;

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.2.7';
export const VERSION = '5.2.9';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,20 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css_unused_selector',
end: {
character: 127,
column: 28,
line: 10
},
message: 'Unused CSS selector "[data-active=\'true\'] > span"',
start: {
character: 100,
column: 1,
line: 10
}
}
]
});

@ -0,0 +1,4 @@
/* (unused) [data-active='true'] > span {
background-color: red;
}*/

@ -0,0 +1,13 @@
<script lang="ts">
//
</script>
<div data-active={false as true}>
<span></span>
</div>
<style>
[data-active='true'] > span {
background-color: red;
}
</style>

@ -1,9 +1,27 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
import { ok, test } from '../../test';
export default test({
compileOptions: {
dev: true
},
test() {}
test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button');
const p = target.querySelector('p');
ok(p);
assert.htmlEqual(p.outerHTML, `<p></p>`);
btn1.click();
flushSync();
assert.htmlEqual(p.outerHTML, `<p>1</p>`);
btn2.click();
flushSync();
assert.htmlEqual(p.outerHTML, `<p></p>`);
btn1.click();
flushSync();
assert.htmlEqual(p.outerHTML, `<p>1</p>`);
}
});

@ -1,9 +1,14 @@
<script>
let count = $state(43);
let count = $state();
</script>
{#await count}
loading
{:then count}
{count}
{/await}
<button onclick={() => count = 1}>number</button>
<button onclick={() => count = null}>nullify</button>
<p>
{#await count}
loading
{:then count}
{count}
{/await}
</p>

@ -5,7 +5,7 @@ export default test({
dev: true
},
html: `<p></p><h1>foo</h1><p></p><form></form> hello`,
html: `<p></p><h1>foo</h1><p></p><form></form>`,
recover: true,
@ -13,8 +13,7 @@ export default test({
errors: [
'node_invalid_placement_ssr: `<p>` (main.svelte:6:0) cannot contain `<h1>` (h1.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.',
'node_invalid_placement_ssr: `<form>` (main.svelte:9:0) cannot contain `<form>` (form.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.',
'node_invalid_placement_ssr: `<td>` (main.svelte:12:0) needs a valid parent element\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
'node_invalid_placement_ssr: `<form>` (main.svelte:9:0) cannot contain `<form>` (form.svelte:1:0)\n\nThis can cause content to shift around as the browser repairs the HTML, and will likely result in a `hydration_mismatch` warning.'
],
warnings: [

@ -739,4 +739,30 @@ describe('signals', () => {
assert.deepEqual(a.reactions, null);
};
});
test('nested deriveds clean up the relationships when used with untrack', () => {
return () => {
let a = render_effect(() => {});
const destroy = effect_root(() => {
a = render_effect(() => {
$.untrack(() => {
const b = derived(() => {
const c = derived(() => {});
$.untrack(() => {
$.get(c);
});
});
$.get(b);
});
});
});
assert.deepEqual(a.deriveds?.length, 1);
destroy();
assert.deepEqual(a.deriveds, null);
};
});
});

Loading…
Cancel
Save