Merge branch 'main' into opaque-rune

opaque-rune
Dominic Gannaway 3 months ago
commit 0166bc4850

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure snippet hoisting works in the correct scope

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: restore input binding selection position

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: transform everything that is not a selector inside `:global`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't emit assignment warnings for bindings

@ -99,8 +99,9 @@ function can_hoist_snippet(scope, scopes, visited = new Set()) {
if (binding.initial?.type === 'SnippetBlock') { if (binding.initial?.type === 'SnippetBlock') {
if (visited.has(binding)) continue; if (visited.has(binding)) continue;
visited.add(binding); visited.add(binding);
const snippet_scope = /** @type {Scope} */ (scopes.get(binding.initial));
if (can_hoist_snippet(binding.scope, scopes, visited)) { if (can_hoist_snippet(snippet_scope, scopes, visited)) {
continue; continue;
} }
} }

@ -169,6 +169,17 @@ function build_assignment(operator, left, right, context) {
} }
} }
// special case — ignore `bind:prop={getter, (v) => (...)}` / `bind:value={x.y}`
if (
path.at(-1) === 'Component' ||
path.at(-1) === 'SvelteComponent' ||
(path.at(-1) === 'ArrowFunctionExpression' &&
path.at(-2) === 'SequenceExpression' &&
(path.at(-3) === 'Component' || path.at(-3) === 'SvelteComponent'))
) {
should_transform = false;
}
if (left.type === 'MemberExpression' && should_transform) { if (left.type === 'MemberExpression' && should_transform) {
const callee = callees[operator]; const callee = callees[operator];

@ -78,7 +78,7 @@ const visitors = {
context.state.code.addSourcemapLocation(node.end); context.state.code.addSourcemapLocation(node.end);
context.next(); context.next();
}, },
Atrule(node, { state, next }) { Atrule(node, { state, next, path }) {
if (is_keyframes_node(node)) { if (is_keyframes_node(node)) {
let start = node.start + node.name.length + 1; let start = node.start + node.name.length + 1;
while (state.code.original[start] === ' ') start += 1; while (state.code.original[start] === ' ') start += 1;
@ -87,7 +87,7 @@ const visitors = {
if (node.prelude.startsWith('-global-')) { if (node.prelude.startsWith('-global-')) {
state.code.remove(start, start + 8); state.code.remove(start, start + 8);
} else { } else if (!is_in_global_block(path)) {
state.code.prependRight(start, `${state.hash}-`); state.code.prependRight(start, `${state.hash}-`);
} }
@ -134,7 +134,7 @@ const visitors = {
} }
} }
}, },
Rule(node, { state, next, visit }) { Rule(node, { state, next, visit, path }) {
if (state.minify) { if (state.minify) {
remove_preceding_whitespace(node.start, state); remove_preceding_whitespace(node.start, state);
remove_preceding_whitespace(node.block.end - 1, state); remove_preceding_whitespace(node.block.end - 1, state);
@ -154,7 +154,7 @@ const visitors = {
return; return;
} }
if (!is_used(node)) { if (!is_used(node) && !is_in_global_block(path)) {
if (state.minify) { if (state.minify) {
state.code.remove(node.start, node.end); state.code.remove(node.start, node.end);
} else { } else {
@ -182,20 +182,20 @@ const visitors = {
state.code.appendLeft(node.block.end, '*/'); state.code.appendLeft(node.block.end, '*/');
} }
// don't recurse into selector or body // don't recurse into selectors but visit the body
visit(node.block);
return; return;
} }
// don't recurse into body
visit(node.prelude);
return;
} }
next(); next();
}, },
SelectorList(node, { state, next, path }) { SelectorList(node, { state, next, path }) {
// Only add comments if we're not inside a complex selector that itself is unused // Only add comments if we're not inside a complex selector that itself is unused or a global block
if (!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)) { if (
!is_in_global_block(path) &&
!path.find((n) => n.type === 'ComplexSelector' && !n.metadata.used)
) {
const children = node.children; const children = node.children;
let pruning = false; let pruning = false;
let prune_start = children[0].start; let prune_start = children[0].start;
@ -359,6 +359,14 @@ const visitors = {
} }
}; };
/**
*
* @param {Array<Css.Node>} path
*/
function is_in_global_block(path) {
return path.some((node) => node.type === 'Rule' && node.metadata.is_global_block);
}
/** /**
* @param {Css.PseudoClassSelector} selector * @param {Css.PseudoClassSelector} selector
* @param {Css.Combinator | null} combinator * @param {Css.Combinator | null} combinator

@ -628,12 +628,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SnippetBlock(node, context) { SnippetBlock(node, context) {
const state = context.state; const state = context.state;
// Special-case for root-level snippets: they become part of the instance scope
const is_top_level = !context.path.at(-2);
let scope = state.scope; let scope = state.scope;
if (is_top_level) {
scope = /** @type {Scope} */ (parent);
}
scope.declare(node.expression, 'normal', 'function', node); scope.declare(node.expression, 'normal', 'function', node);
const child_scope = state.scope.child(); const child_scope = state.scope.child();

@ -1,3 +1,4 @@
import { untrack } from '../runtime.js';
import * as w from '../warnings.js'; import * as w from '../warnings.js';
import { sanitize_location } from './location.js'; import { sanitize_location } from './location.js';
@ -23,7 +24,12 @@ function compare(a, b, property, location) {
* @param {string} location * @param {string} location
*/ */
export function assign(object, property, value, location) { export function assign(object, property, value, location) {
return compare((object[property] = value), object[property], property, location); return compare(
(object[property] = value),
untrack(() => object[property]),
property,
location
);
} }
/** /**
@ -33,7 +39,12 @@ export function assign(object, property, value, location) {
* @param {string} location * @param {string} location
*/ */
export function assign_and(object, property, value, location) { export function assign_and(object, property, value, location) {
return compare((object[property] &&= value), object[property], property, location); return compare(
(object[property] &&= value),
untrack(() => object[property]),
property,
location
);
} }
/** /**
@ -43,7 +54,12 @@ export function assign_and(object, property, value, location) {
* @param {string} location * @param {string} location
*/ */
export function assign_or(object, property, value, location) { export function assign_or(object, property, value, location) {
return compare((object[property] ||= value), object[property], property, location); return compare(
(object[property] ||= value),
untrack(() => object[property]),
property,
location
);
} }
/** /**
@ -53,5 +69,10 @@ export function assign_or(object, property, value, location) {
* @param {string} location * @param {string} location
*/ */
export function assign_nullish(object, property, value, location) { export function assign_nullish(object, property, value, location) {
return compare((object[property] ??= value), object[property], property, location); return compare(
(object[property] ??= value),
untrack(() => object[property]),
property,
location
);
} }

@ -30,8 +30,17 @@ export function bind_value(input, get, set = get) {
// In runes mode, respect any validation in accessors (doesn't apply in legacy mode, // In runes mode, respect any validation in accessors (doesn't apply in legacy mode,
// because we use mutable state which ensures the render effect always runs) // because we use mutable state which ensures the render effect always runs)
if (runes && value !== (value = get())) { if (runes && value !== (value = get())) {
var start = input.selectionStart;
var end = input.selectionEnd;
// the value is coerced on assignment // the value is coerced on assignment
input.value = value ?? ''; input.value = value ?? '';
// Restore selection
if (end !== null) {
input.selectionStart = start;
input.selectionEnd = Math.min(end, input.value.length);
}
} }
}); });

@ -69,3 +69,20 @@
color: red; color: red;
} }
}*/ }*/
/* :global{*/
.x{
animation: svelte-xyz-test 1s;
}
@keyframes test-in{
to{
opacity: 1;
}
}
/*}*/
@keyframes svelte-xyz-test{
to{
opacity: 1;
}
}

@ -71,4 +71,21 @@
color: red; color: red;
} }
} }
:global{
.x{
animation: test 1s;
}
@keyframes test-in{
to{
opacity: 1;
}
}
}
@keyframes test{
to{
opacity: 1;
}
}
</style> </style>

@ -0,0 +1,11 @@
<script>
let { x = $bindable() } = $props();
$effect(() => {
x = {};
});
export function soThatTestReturnsAnObject() {
return x;
}
</script>

@ -6,19 +6,19 @@ export default test({
dev: true dev: true
}, },
html: `<button>items: null</button>`, html: `<button>items: null</button> <div>x</div>`,
test({ assert, target, warnings }) { test({ assert, target, warnings }) {
const btn = target.querySelector('button'); const btn = target.querySelector('button');
flushSync(() => btn?.click()); flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, `<button>items: []</button>`); assert.htmlEqual(target.innerHTML, `<button>items: []</button> <div>x</div>`);
flushSync(() => btn?.click()); flushSync(() => btn?.click());
assert.htmlEqual(target.innerHTML, `<button>items: [0]</button>`); assert.htmlEqual(target.innerHTML, `<button>items: [0]</button> <div>x</div>`);
assert.deepEqual(warnings, [ assert.deepEqual(warnings, [
'Assignment to `items` property (main.svelte:5:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.' 'Assignment to `items` property (main.svelte:8:24) will evaluate to the right-hand side, not the value of `items` following the assignment. This may result in unexpected behaviour.'
]); ]);
} }
}); });

@ -0,0 +1,16 @@
<script>
import Test from './Test.svelte';
let entries = $state([]);
let object = $state({ items: null });
</script>
<button onclick={() => (object.items ??= []).push(object.items.length)}>
items: {JSON.stringify(object.items)}
</button>
<!-- these should not emit warnings -->
<div bind:this={entries[0]}>x</div>
<Test bind:this={entries[1]}></Test>
<Test bind:this={() => entries[2], (v) => (entries[2] = v)}></Test>
<Test bind:x={entries[3]}></Test>

@ -5,7 +5,7 @@ export default test({
html: `<button>items: null</button>`, html: `<button>items: null</button>`,
test({ assert, target }) { test({ assert, target }) {
const [btn1, btn2] = target.querySelectorAll('button'); const [btn1] = target.querySelectorAll('button');
flushSync(() => btn1.click()); flushSync(() => btn1.click());
assert.htmlEqual(target.innerHTML, `<button>items: [0]</button>`); assert.htmlEqual(target.innerHTML, `<button>items: [0]</button>`);

@ -1,7 +0,0 @@
<script>
let object = $state({ items: null });
</script>
<button onclick={() => (object.items ??= []).push(object.items.length)}>
items: {JSON.stringify(object.items)}
</button>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: 'a'
});

@ -0,0 +1,13 @@
<script>
let abc = 'a'
</script>
{@render b()}
{#snippet a()}
{abc}
{/snippet}
{#snippet b()}
{@render a()}
{/snippet}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<h1>Hello world!</h1>'
});

@ -0,0 +1,13 @@
<script module>
export { foo }
</script>
<script>
let name = 'world';
</script>
<h1>Hello {name}!</h1>
{#snippet foo()}
oo
{/snippet}
Loading…
Cancel
Save