pull/16073/head
7nik 4 months ago
parent 2342c8719a
commit 1f51993b93

@ -2,18 +2,22 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.AttachTag} node
* @param {ComponentContext} context
*/
export function AttachTag(node, context) {
const expression = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression(node.expression, context);
context.state.init.push(
b.stmt(
b.call(
'$.attach',
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.expression)))
b.thunk(expression)
)
)
);

@ -5,6 +5,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.AwaitBlock} node
@ -14,7 +15,11 @@ export function AwaitBlock(node, context) {
context.state.template.push_comment();
// Visit {#await <expression>} first to ensure that scopes are in the correct order
const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression)));
const expression = b.thunk(
context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression(node.expression, context)
);
let then_block;
let catch_block;

@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.ConstTag} node
@ -15,12 +16,15 @@ export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...)
if (declaration.id.type === 'Identifier') {
const init = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(declaration.init))
: build_legacy_expression(declaration.init, context);
context.state.init.push(
b.const(
declaration.id,
create_derived(
context.state,
b.thunk(/** @type {Expression} */ (context.visit(declaration.init)))
b.thunk(init)
)
)
);
@ -48,12 +52,15 @@ export function ConstTag(node, context) {
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const init = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(declaration.init, child_state))
: build_legacy_expression(declaration.init, { ...context, state: child_state });
const fn = b.arrow(
[],
b.block([
b.const(
/** @type {Pattern} */ (context.visit(declaration.id, child_state)),
/** @type {Expression} */ (context.visit(declaration.init, child_state))
init,
),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])

@ -14,6 +14,7 @@ import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_getter } from '../utils.js';
import { get_value } from './shared/declarations.js';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.EachBlock} node
@ -24,12 +25,16 @@ export function EachBlock(node, context) {
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
);
const parent_scope_state = {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
};
const collection = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression, parent_scope_state))
: build_legacy_expression(node.expression, {
...context,
state: parent_scope_state
});
if (!each_node_meta.is_controlled) {
context.state.template.push_comment();

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { is_ignored } from '../../../../state.js';
import * as b from '#compiler/builders';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.HtmlTag} node
@ -11,7 +12,9 @@ import * as b from '#compiler/builders';
export function HtmlTag(node, context) {
context.state.template.push_comment();
const expression = /** @type {Expression} */ (context.visit(node.expression));
const expression = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression(node.expression, context);
const is_svg = context.state.metadata.namespace === 'svg';
const is_mathml = context.state.metadata.namespace === 'mathml';

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
@ -31,6 +32,10 @@ export function IfBlock(node, context) {
statements.push(b.var(b.id(alternate_id), b.arrow(alternate_args, alternate)));
}
const test = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.test))
: build_legacy_expression(node.test, context);
/** @type {Expression[]} */
const args = [
node.elseif ? b.id('$$anchor') : context.state.node,
@ -38,7 +43,7 @@ export function IfBlock(node, context) {
[b.id('$$render')],
b.block([
b.if(
/** @type {Expression} */ (context.visit(node.test)),
test,
b.stmt(b.call(b.id('$$render'), b.id(consequent_id))),
alternate_id ? b.stmt(b.call(b.id('$$render'), b.id(alternate_id), b.false)) : undefined
)
@ -46,6 +51,7 @@ export function IfBlock(node, context) {
)
];
if (node.elseif) {
// We treat this...
//

@ -2,6 +2,7 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.KeyBlock} node
@ -10,7 +11,9 @@ import * as b from '#compiler/builders';
export function KeyBlock(node, context) {
context.state.template.push_comment();
const key = /** @type {Expression} */ (context.visit(node.expression));
const key = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(node.expression))
: build_legacy_expression(node.expression, context);
const body = /** @type {Expression} */ (context.visit(node.fragment));
context.state.init.push(

@ -3,6 +3,7 @@
/** @import { ComponentContext } from '../types' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '#compiler/builders';
import { build_legacy_expression } from './shared/utils.js';
/**
* @param {AST.RenderTag} node
@ -31,7 +32,9 @@ export function RenderTag(node, context) {
}
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
let snippet_function = context.state.analysis.runes
? /** @type {Expression} */ (context.visit(callee))
: build_legacy_expression(/** @type {Expression} */(callee), context);
if (node.metadata.dynamic) {
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one

@ -1,6 +1,6 @@
/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */
/** @import { AST, ExpressionMetadata } from '#compiler' */
/** @import { ComponentClientTransformState, Context } from '../../types' */
/** @import { ComponentClientTransformState, ComponentContext, Context } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
import * as b from '#compiler/builders';
@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template
import { regex_is_valid_identifier } from '../../../../patterns.js';
import is_reference from 'is-reference';
import { dev, is_ignored, locator } from '../../../../../state.js';
import { create_derived } from '../../utils.js';
import { build_getter, create_derived } from '../../utils.js';
/**
* @param {ComponentClientTransformState} state
@ -360,3 +360,57 @@ export function validate_mutation(node, context, expression) {
loc && b.literal(loc.column)
);
}
/**
* Serializes an expression with reactivity like in Svelte 4
* @param {Expression} expression
* @param {ComponentContext} context
*/
export function build_legacy_expression(expression, context) {
// To recreate Svelte 4 behaviour, we track the dependencies
// the compiler can 'see', but we untrack the effect itself
const serialized_expression = /** @type {Expression} */ (context.visit(expression));
if (expression.type === "Identifier") return serialized_expression;
/** @type {Expression[]} */
const sequence = [];
for (const [name, nodes] of context.state.scope.references) {
const binding = context.state.scope.get(name);
if (binding === null || binding.kind === 'normal' && binding.declaration_kind !== 'import') continue;
let used = false;
for (const { node, path } of nodes) {
const expressionIdx = path.indexOf(expression);
if (expressionIdx < 0) continue;
// in Svelte 4, #if, #each and #await copy context, so assignments
// aren't propagated to the parent block / component root
const track_assignment = !path.find((node, i) =>
i < expressionIdx - 1 && ["IfBlock", "EachBlock", "AwaitBlock"].includes(node.type)
)
if (track_assignment) {
used = true;
break;
}
const assignment = /** @type {AssignmentExpression|undefined} */(path.find((node, i) => i >= expressionIdx && node.type === "AssignmentExpression"));
if (!assignment || assignment.left !== node && !path.includes(assignment.left)) {
used = true;
break;
}
}
if (!used) continue;
let serialized = build_getter(b.id(name), context.state);
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') {
serialized = b.call('$.deep_read_state', serialized);
}
sequence.push(serialized);
}
return b.sequence([...sequence, b.call('$.untrack', b.thunk(serialized_expression))]);
}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,0]`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> [0,0,0,0,0,0,0,0,1]`);
}
});

@ -0,0 +1,45 @@
<script>
let a = 0, b = 0, c = 0, d = 0, e = 0, f = 0, g = 0, h = 0, i = 0;
function inc() {
a++;
b++;
c++;
d++;
e++;
f++;
g++;
h++;
i++;
}
</script>
{#if a = 0}{/if}
{#each [b = 0] as x}{x,''}{/each}
{#key c = 0}{/key}
{#await d = 0}{/await}
{#snippet snip()}{/snippet}
{@render (e = 0, snip)()}
{@html f = 0, ''}
<div {@attach !!(g = 0)}></div>
{#key 1}
{@const x = (h = 0)}
{x, ''}
{/key}
{#if 1}
{@const x = (i = 0)}
{x, ''}
{/if}
<button on:click={inc}>inc</button>
[{a},{b},{c},{d},{e},{f},{g},{h},{i}]

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 10 - 10`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 11 - 10`);
}
});

@ -0,0 +1,36 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
</script>
{#if fn(false)}{:else if fn(true)}{/if}
{#each fn([]) as x}{x, ''}{/each}
{#key fn(1)}{/key}
{#await fn(Promise.resolve())}{/await}
{#snippet snip()}{/snippet}
{@render fn(snip)()}
{@html fn('')}
<div {@attach fn(() => {})}></div>
{#key 1}
{@const x = fn('')}
{x}
{/key}
<button on:click={() => count1++}>inc</button>
{count1} - {count2}

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target }) {
const button = target.querySelector('button');
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 10 - 10`);
flushSync(() => button?.click());
assert.htmlEqual(target.innerHTML, `<div></div><button>inc</button> 11 - 10`);
}
});

@ -0,0 +1,46 @@
<script>
let count1 = 1;
let count2 = 1;
function fn(ret) {
if (count1 > 100) return ret;
count1++;
count2++;
return ret;
}
const obj = {
get true() { return fn(true) },
get false() { return fn(false) },
get array() { return fn([]) },
get string() { return fn('') },
get promise() { return fn(Promise.resolve()) },
get snippet() { return fn(snip) },
get attachment() { return fn(() => {}) },
}
</script>
{#if obj.false}{:else if obj.true}{/if}
{#each obj.array as x}{x, ''}{/each}
{#key obj.string}{/key}
{#await obj.promise}{/await}
{#snippet snip()}{/snippet}
{@render obj.snippet()}
{@html obj.string}
<div {@attach obj.attachment}></div>
{#key 1}
{@const x = obj.string}
{x}
{/key}
<button on:click={() => count1++}>inc</button>
{count1} - {count2}
Loading…
Cancel
Save