make render appear in tree, keep tree in sync when rerenders occur

pull/16255/head
Simon Holthausen 3 months ago
parent 757683fb56
commit cad332a067

@ -3,7 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
import { add_svelte_meta, build_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -48,16 +48,22 @@ export function RenderTag(node, context) {
}
context.state.init.push(
b.stmt(b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args))
add_svelte_meta(
b.call('$.snippet', context.state.node, b.thunk(snippet_function), ...args),
node,
'render'
)
);
} else {
context.state.init.push(
b.stmt(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
context.state.node,
...args
)
),
node,
'render'
)
);
}

@ -1,4 +1,4 @@
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern, CallExpression, Statement } from 'estree' */
/** @import { AssignmentExpression, Expression, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, ExpressionStatement } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
@ -397,26 +397,26 @@ export function build_expression(context, expression, metadata, state = context.
/**
* Wraps a statement/expression with dev stack tracking in dev mode
* @param {CallExpression} call_expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {Expression} expression - The function call to wrap (e.g., $.if, $.each, etc.)
* @param {{ start?: number }} node - AST node for location info
* @param {'component' | 'if' | 'each' | 'await' | 'key'} type - Type of block/component
* @param {'component' | 'if' | 'each' | 'await' | 'key' | 'render'} type - Type of block/component
* @param {Record<string, number | string>} [additional] - Any additional properties to add to the dev stack entry
* @returns {Statement} - Statement with or without dev stack wrapping
* @returns {ExpressionStatement} - Statement with or without dev stack wrapping
*/
export function add_svelte_meta(call_expression, node, type, additional) {
export function add_svelte_meta(expression, node, type, additional) {
if (!dev) {
return b.stmt(call_expression);
return b.stmt(expression);
}
const location = node.start && locator(node.start);
const location = node.start !== undefined && locator(node.start);
if (!location) {
return b.stmt(call_expression);
return b.stmt(expression);
}
return b.stmt(
b.call(
'$.add_svelte_meta',
b.arrow([], call_expression),
b.arrow([], expression),
b.literal(type),
b.id(component_name),
b.literal(location.line),

@ -5,9 +5,7 @@ import { EFFECT_TRANSPARENT, ELEMENT_NODE } from '#client/constants';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
dev_current_component_function,
dev_stack,
set_dev_current_component_function,
set_dev_stack
set_dev_current_component_function
} from '../../context.js';
import { hydrate_next, hydrate_node, hydrating } from '../hydration.js';
import { create_fragment_from_html } from '../reconciler.js';
@ -63,18 +61,14 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
var original_stack = dev_stack;
const snippet = (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
var previous_stack = dev_stack;
set_dev_current_component_function(component);
set_dev_stack(original_stack);
try {
return fn(node, ...args);
} finally {
set_dev_current_component_function(previous_component_function);
set_dev_stack(previous_stack);
}
};

@ -41,7 +41,7 @@ import { DEV } from 'esm-env';
import { define_property } from '../../shared/utils.js';
import { get_next_sibling } from '../dom/operations.js';
import { derived } from './deriveds.js';
import { component_context, dev_current_component_function } from '../context.js';
import { component_context, dev_current_component_function, dev_stack } from '../context.js';
/**
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
@ -359,7 +359,11 @@ export function template_effect(fn, thunks = [], d = derived) {
* @param {number} flags
*/
export function block(fn, flags = 0) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
var effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
if (DEV) {
effect.dev_stack = dev_stack;
}
return effect;
}
/**

@ -1,4 +1,10 @@
import type { ComponentContext, Dom, Equals, TemplateNode, TransitionManager } from '#client';
import type {
ComponentContext,
DevStackEntry,
Equals,
TemplateNode,
TransitionManager
} from '#client';
export interface Signal {
/** Flags bitmask */
@ -80,6 +86,8 @@ export interface Effect extends Reaction {
parent: Effect | null;
/** Dev only */
component_function?: any;
/** Dev only. Only set for certain block effects. Contains a reference to the stack that represents the render tree */
dev_stack?: DevStackEntry | null;
}
export type Source<V = unknown> = Value<V>;

@ -35,12 +35,13 @@ import { tracing_expressions, get_stack } from './dev/tracing.js';
import {
component_context,
dev_current_component_function,
dev_stack,
is_runes,
set_component_context,
set_dev_current_component_function
set_dev_current_component_function,
set_dev_stack
} from './context.js';
import { handle_error, invoke_error_boundary } from './error-handling.js';
import { snapshot } from '../shared/clone.js';
let is_flushing = false;
@ -445,6 +446,9 @@ export function update_effect(effect) {
if (DEV) {
var previous_component_fn = dev_current_component_function;
set_dev_current_component_function(effect.component_function);
var previous_stack = /** @type {any} */ (dev_stack);
// only block effects have a dev stack, keep the current one otherwise
set_dev_stack(effect.dev_stack ?? dev_stack);
}
try {
@ -479,6 +483,7 @@ export function update_effect(effect) {
if (DEV) {
set_dev_current_component_function(previous_component_fn);
set_dev_stack(previous_stack);
}
}
}

@ -189,7 +189,7 @@ export type SourceLocation =
export interface DevStackEntry {
file: string;
type: 'component' | 'if' | 'each' | 'await' | 'key';
type: 'component' | 'if' | 'each' | 'await' | 'key' | 'render';
line: number;
column: number;
parent: DevStackEntry | null;

@ -6,11 +6,24 @@ export default test({
compileOptions: {
dev: true
},
html: `<p>no parent</p><p>if</p><p>each</p><p>loading</p><p>key</p><p>hi</p><p>hi</p><p>hi</p>`,
html: `
<p>no parent</p>
<button>toggle</button>
<p>if</p>
<p>each</p>
<p>loading</p>
<p>key</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
<p>hi</p>
`,
async test({ target, assert }) {
await tick();
const [main, if_, each, await_, key, child1, child2, child3] = target.querySelectorAll('p');
function check() {
const [main, if_, each, await_, key, child1, child2, child3, child4, dynamic] =
target.querySelectorAll('p');
// @ts-expect-error
assert.deepEqual(main.__svelte_meta.parent, null);
@ -19,7 +32,7 @@ export default test({
assert.deepEqual(if_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'if',
line: 10,
line: 12,
column: 0,
parent: null
});
@ -28,7 +41,7 @@ export default test({
assert.deepEqual(each.__svelte_meta.parent, {
file: 'main.svelte',
type: 'each',
line: 14,
line: 16,
column: 0,
parent: null
});
@ -37,7 +50,7 @@ export default test({
assert.deepEqual(await_.__svelte_meta.parent, {
file: 'main.svelte',
type: 'await',
line: 18,
line: 20,
column: 0,
parent: null
});
@ -46,7 +59,7 @@ export default test({
assert.deepEqual(key.__svelte_meta.parent, {
file: 'main.svelte',
type: 'key',
line: 24,
line: 26,
column: 0,
parent: null
});
@ -56,7 +69,7 @@ export default test({
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 28,
line: 30,
column: 0,
parent: null
});
@ -66,26 +79,108 @@ export default test({
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 31,
line: 33,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 30,
line: 32,
column: 0,
parent: null
}
}
});
// @ts-expect-error
assert.deepEqual(child3.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'Child',
line: 38,
column: 2,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 37,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'render',
line: 5,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 36,
column: 0,
parent: null
}
}
}
}
});
// @ts-expect-error
assert.deepEqual(child4.__svelte_meta.parent, {
file: 'passthrough.svelte',
type: 'render',
line: 8,
column: 1,
parent: {
file: 'passthrough.svelte',
type: 'if',
line: 7,
column: 0,
parent: {
file: 'main.svelte',
type: 'component',
componentTag: 'Passthrough',
line: 43,
column: 1,
parent: {
file: 'main.svelte',
type: 'if',
line: 42,
column: 0,
parent: null
}
}
}
});
// @ts-expect-error
assert.deepEqual(dynamic.__svelte_meta.parent, {
file: 'main.svelte',
type: 'component',
componentTag: 'x.y',
line: 34,
line: 50,
column: 0,
parent: null
});
}
await tick();
check();
// Test that stack is kept when re-rendering
const button = target.querySelector('button');
button?.click();
await tick();
button?.click();
await tick();
check();
}
});

@ -3,9 +3,11 @@
import Passthrough from "./passthrough.svelte";
let x = { y: Child }
let key = 'test';
let show = $state(true);
</script>
<p>no parent</p>
<button onclick={() => show = !show}>toggle</button>
{#if true}
<p>if</p>
@ -31,4 +33,18 @@
<Child />
</Passthrough>
<Passthrough>
<Passthrough>
<Child />
</Passthrough>
</Passthrough>
{#if show}
<Passthrough>
{#snippet named()}
<p>hi</p>
{/snippet}
</Passthrough>
{/if}
<x.y />

@ -1,5 +1,9 @@
<script>
let { children } = $props();
let { children, named } = $props();
</script>
{@render children()}
{@render children?.()}
{#if true}
{@render named?.()}
{/if}

Loading…
Cancel
Save