merge conflicts

log-rune
Dominic Gannaway 10 months ago
commit aac227d6bc

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add $effect.root rune

@ -1,5 +1,5 @@
---
'svelte': minor
'svelte': patch
---
feat: support type definition in {@const}

@ -2,4 +2,4 @@
'svelte': patch
---
feat: ignore href attributes when hydrating
feat: ignore `src`, `srcset`, and `href` attributes when hydrating

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve `<svelte:element>` generated code

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: prevent some unused variable creation

@ -519,6 +519,12 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-rune-args-length', '$effect.active', [0]);
}
}
if (rune === '$effect.root') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', '$effect.root', [1]);
}
}
}
/**

@ -135,7 +135,12 @@ export const javascript_visitors_runes = {
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active' || rune.startsWith('$log')) {
if (
!rune ||
rune === '$effect.active' ||
rune === '$effect.root' ||
rune.startsWith('$log')
) {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
@ -208,7 +213,6 @@ export const javascript_visitors_runes = {
// TODO
continue;
}
const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
const value =
args.length === 0
@ -336,6 +340,13 @@ export const javascript_visitors_runes = {
return b.unary('void', b.literal(0));
}
if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.user_root_effect', ...args);
}
next();
}
};

@ -347,6 +347,15 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
* @returns {boolean}
*/
function serialize_dynamic_element_spread_attributes(attributes, context, element_id) {
if (attributes.length === 0) {
if (context.state.analysis.stylesheet.id) {
context.state.init.push(
b.stmt(b.call('$.class_name', element_id, b.literal(context.state.analysis.stylesheet.id)))
);
}
return false;
}
let is_reactive = false;
/** @type {import('estree').Expression[]} */
@ -1589,37 +1598,8 @@ function serialize_template_literal(values, visit, state) {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
contains_call_expression = true;
}
let expression = visit(node.expression);
if (node.expression.type === 'Identifier') {
const name = node.expression.name;
const binding = scope.get(name);
// When we combine expressions as part of a single template element, we might
// be referencing variables that can be mutated, but are not actually state.
// In order to prevent this undesired behavior, we need ensure we cache the
// latest value we have of that variable before we process the template, enforcing
// the value remains static through the lifetime of the template.
if (binding !== null && binding.kind === 'normal' && binding.mutated) {
let has_already_cached = false;
// Check if we already create a const of this expression
for (let node of state.init) {
if (
node.type === 'VariableDeclaration' &&
node.declarations[0].id.type === 'Identifier' &&
node.declarations[0].id.name === name + '_const'
) {
has_already_cached = true;
expression = b.id(name + '_const');
break;
}
}
if (!has_already_cached) {
const tmp_id = scope.generate(name + '_const');
state.init.push(b.const(tmp_id, expression));
expression = b.id(tmp_id);
}
}
}
expressions.push(b.call('$.stringify', expression));
expressions.push(b.call('$.stringify', visit(node.expression)));
quasis.push(b.quasi('', i + 1 === values.length));
}
}
@ -2104,7 +2084,9 @@ export const template_visitors = {
'$.element',
context.state.node,
get_tag,
b.arrow([element_id, b.id('$$anchor')], b.block(inner)),
inner.length === 0
? /** @type {any} */ (undefined)
: b.arrow([element_id, b.id('$$anchor')], b.block(inner)),
namespace === 'http://www.w3.org/2000/svg'
? b.literal(true)
: /** @type {any} */ (undefined)

@ -77,6 +77,7 @@ export const Runes = [
'$effect',
'$effect.pre',
'$effect.active',
'$effect.root',
'$log',
'$log.break',
'$log.trace',

@ -1542,7 +1542,7 @@ function swap_block_dom(block, from, to) {
/**
* @param {Comment} anchor_node
* @param {() => string} tag_fn
* @param {null | ((element: Element, anchor: Node) => void)} render_fn
* @param {undefined | ((element: Element, anchor: Node) => void)} render_fn
* @param {any} is_svg
* @returns {void}
*/
@ -1582,7 +1582,7 @@ export function element(anchor_node, tag_fn, render_fn, is_svg = false) {
block.d = null;
}
element = next_element;
if (element !== null && render_fn !== null) {
if (element !== null && render_fn !== undefined) {
let anchor;
if (current_hydration_fragment !== null) {
// Use the existing ssr comment as the anchor so that the inner open and close

@ -1250,6 +1250,17 @@ export function user_effect(init) {
return effect;
}
/**
* @param {() => void | (() => void)} init
* @returns {() => void}
*/
export function user_root_effect(init) {
const effect = managed_render_effect(init);
return () => {
destroy_signal(effect);
};
}
/**
* @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal}

@ -37,6 +37,7 @@ export {
push,
reactive_import,
effect_active,
user_root_effect,
log,
log_trace,
log_break,

@ -90,6 +90,34 @@ declare namespace $effect {
* https://svelte-5-preview.vercel.app/docs/runes#$effect-active
*/
export function active(): boolean;
/**
* The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
* nested effects that you want to manually control. This rune also allows for creation of effects outside of the component
* initialisation phase.
*
* Example:
* ```svelte
* <script>
* let count = $state(0);
*
* const cleanup = $effect.root(() => {
* $effect(() => {
* console.log(count);
* })
*
* return () => {
* console.log('effect root cleanup');
* }
* });
* </script>
*
* <button onclick={() => cleanup()}>cleanup</button>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$effect-root
*/
export function root(fn: () => void | (() => void)): () => void;
}
/**

@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
get props() {
return { log: [] };
},
async test({ assert, target, component }) {
const [b1, b2, b3] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(component.log, [0, 1]);
flushSync(() => {
b3.click();
});
assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']);
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']);
}
});

@ -0,0 +1,27 @@
<script>
let { log} = $props();
let x = $state(0);
let y = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
log.push(x);
});
const nested_cleanup = $effect.root(() => {
return () => {
log.push('cleanup 2') ;
}
});
return () => {
log.push('cleanup 1');
nested_cleanup();
}
});
</script>
<button on:click={() => x++}>{x}</button>
<button on:click={() => y++}>{y}</button>
<button on:click={() => cleanup()}>cleanup</button>

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -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 Svelte_element($$anchor, $$props) {
$.push($$props, true);
let tag = $.prop_source($$props, "tag", 'hr', false);
/* Init */
var fragment = $.comment($$anchor);
var node = $.child_frag(fragment);
$.element(node, () => $.get(tag));
$.close_frag($$anchor, fragment);
$.pop();
}

@ -0,0 +1,27 @@
// index.svelte (Svelte VERSION)
// Note: compiler output will change before 5.0 is released!
import * as $ from "svelte/internal/server";
export default function Svelte_element($$payload, $$props) {
$.push(true);
let { tag = 'hr' } = $$props;
const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`;
if (tag) {
const anchor_1 = $.create_anchor($$payload);
$$payload.out += `<${tag}>`;
if (!$.VoidElements.has(tag)) {
$$payload.out += `${anchor_1}`;
$$payload.out += `${anchor_1}</${tag}>`;
}
}
$$payload.out += `${anchor}`;
$.bind_props($$props, { tag });
$.pop();
}

@ -0,0 +1,5 @@
<script>
let { tag = 'hr' } = $props();
</script>
<svelte:element this={tag} />

@ -186,6 +186,27 @@ The `$effect.active` rune is an advanced feature that tells you whether or not t
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
## `$effect.root`
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase.
```svelte
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('effect root cleanup');
};
});
</script>
```
## `$props`
To declare component props, use the `$props` rune:

Loading…
Cancel
Save