Merge branch 'main' into svelte-custom-renderer

svelte-custom-renderer-single-type-argument
Paolo Ricciuti 3 months ago committed by GitHub
commit d7e01b7634
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle parens in template expressions more robustly

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correct types for `ontoggle` on `<details>` elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: unskip branches of earlier batches after commit

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: skip rebase logic in non-async mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: don't reset status of uninitialized deriveds

@ -42,3 +42,9 @@ even over `!important` properties:
<div style:color="red" style="color: blue">This will be red</div>
<div style:color="red" style="color: blue !important">This will still be red</div>
```
You can set CSS custom properties:
```svelte
<div style:--columns={columns}>...</div>
```

@ -952,9 +952,9 @@ export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement
'bind:open'?: boolean | undefined | null;
'on:toggle'?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontoggle?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontogglecapture?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
'on:toggle'?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
ontoggle?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
ontogglecapture?: ToggleEventHandler<HTMLDetailsElement> | undefined | null;
}
export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {

@ -1,5 +1,6 @@
/** @import { Comment, Program } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { Parser } from './index.js' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from '@sveltejs/acorn-typescript';
@ -66,26 +67,22 @@ export function parse(source, comments, typescript, is_script) {
}
/**
* @param {Parser} parser
* @param {string} source
* @param {Comment[]} comments
* @param {boolean} typescript
* @param {number} index
* @returns {acorn.Expression & { leadingComments?: CommentWithLocation[]; trailingComments?: CommentWithLocation[]; }}
*/
export function parse_expression_at(source, comments, typescript, index) {
const parser = typescript ? ParserWithTS : acorn.Parser;
export function parse_expression_at(parser, source, index) {
const _ = parser.ts ? ParserWithTS : acorn.Parser;
const { onComment, add_comments } = get_comment_handlers(
source,
/** @type {CommentWithLocation[]} */ (comments),
index
);
const { onComment, add_comments } = get_comment_handlers(source, parser.root.comments, index);
const ast = parser.parseExpressionAt(source, index, {
const ast = _.parseExpressionAt(source, index, {
onComment,
sourceType: 'module',
ecmaVersion: 16,
locations: true
locations: true,
preserveParens: true
});
add_comments(ast);
@ -93,6 +90,18 @@ export function parse_expression_at(source, comments, typescript, index) {
return ast;
}
/**
* @param {acorn.Expression} node
* @returns {acorn.Expression}
*/
export function remove_parens(node) {
return walk(node, null, {
ParenthesizedExpression(node, context) {
return context.visit(node.expression);
}
});
}
/**
* Acorn doesn't add comments to the AST by itself. This factory returns the capabilities
* to add them after the fact. They are needed in order to support `svelte-ignore` comments

@ -1,7 +1,7 @@
/** @import { Pattern } from 'estree' */
/** @import { Parser } from '../index.js' */
import { match_bracket } from '../utils/bracket.js';
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_not_newline_characters } from '../../patterns.js';
import * as e from '../../../errors.js';
@ -49,14 +49,12 @@ export default function read_pattern(parser) {
space_with_newline =
space_with_newline.slice(0, first_space) + space_with_newline.slice(first_space + 1);
const expression = /** @type {any} */ (
parse_expression_at(
`${space_with_newline}(${pattern_string} = 1)`,
parser.root.comments,
parser.ts,
start - 1
)
).left;
/** @type {any} */
let expression = remove_parens(
parse_expression_at(parser, `${space_with_newline}(${pattern_string} = 1)`, start - 1)
);
expression = expression.left;
expression.typeAnnotation = read_type_annotation(parser);
if (expression.typeAnnotation) {
@ -92,13 +90,13 @@ function read_type_annotation(parser) {
// parameters as part of a sequence expression instead, and will then error on optional
// parameters (`?:`). Therefore replace that sequence with something that will not error.
parser.template.slice(parser.index).replace(/\?\s*:/g, ':');
let expression = parse_expression_at(template, parser.root.comments, parser.ts, a);
let expression = remove_parens(parse_expression_at(parser, template, a));
// `foo: bar = baz` gets mangled — fix it
if (expression.type === 'AssignmentExpression') {
let b = expression.right.start;
while (template[b] !== '=') b -= 1;
expression = parse_expression_at(template.slice(0, b), parser.root.comments, parser.ts, a);
expression = remove_parens(parse_expression_at(parser, template.slice(0, b), a));
}
// `array as item: string, index` becomes `string, index`, which is mistaken as a sequence expression - fix that

@ -1,6 +1,6 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { parse_expression_at, remove_parens } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
import { find_matching_bracket } from '../utils/bracket.js';
@ -34,50 +34,16 @@ export function get_loose_identifier(parser, opening_token) {
*/
export default function read_expression(parser, opening_token, disallow_loose) {
try {
let comment_index = parser.root.comments.length;
const node = parse_expression_at(
parser.template,
parser.root.comments,
parser.ts,
parser.index
);
let num_parens = 0;
let i = parser.root.comments.length;
while (i-- > comment_index) {
const comment = parser.root.comments[i];
if (comment.end < node.start) {
parser.index = comment.end;
break;
}
}
for (let i = parser.index; i < /** @type {number} */ (node.start); i += 1) {
if (parser.template[i] === '(') num_parens += 1;
}
const node = parse_expression_at(parser, parser.template, parser.index);
let index = /** @type {number} */ (node.end);
const last_comment = parser.root.comments.at(-1);
if (last_comment && last_comment.end > index) index = last_comment.end;
while (num_parens > 0) {
const char = parser.template[index];
if (char === ')') {
num_parens -= 1;
} else if (!regex_whitespace.test(char)) {
e.expected_token(index, ')');
}
index += 1;
}
parser.index = index;
return /** @type {Expression} */ (node);
return /** @type {Expression} */ (remove_parens(node));
} catch (err) {
// If we are in an each loop we need the error to be thrown in cases like
// `as { y = z }` so we still throw and handle the error there

@ -392,12 +392,7 @@ function open(parser) {
let function_expression = matched
? /** @type {ArrowFunctionExpression} */ (
parse_expression_at(
prelude + `${params} => {}`,
parser.root.comments,
parser.ts,
params_start
)
parse_expression_at(parser, prelude + `${params} => {}`, params_start)
)
: { params: [] };

@ -172,6 +172,12 @@ export class Batch {
*/
#skipped_branches = new Map();
/**
* Inverse of #skipped_branches which we need to tell prior batches to unskip them when committing
* @type {Set<Effect>}
*/
#unskipped_branches = new Set();
is_fork = false;
#decrement_queued = false;
@ -215,28 +221,31 @@ export class Batch {
if (!this.#skipped_branches.has(effect)) {
this.#skipped_branches.set(effect, { d: [], m: [] });
}
this.#unskipped_branches.delete(effect);
}
/**
* Remove an effect from the #skipped_branches map and reschedule
* any tracked dirty/maybe_dirty child effects
* @param {Effect} effect
* @param {(e: Effect) => void} callback
*/
unskip_effect(effect) {
unskip_effect(effect, callback = (e) => this.schedule(e)) {
var tracked = this.#skipped_branches.get(effect);
if (tracked) {
this.#skipped_branches.delete(effect);
for (var e of tracked.d) {
set_signal_status(e, DIRTY);
this.schedule(e);
callback(e);
}
for (e of tracked.m) {
set_signal_status(e, MAYBE_DIRTY);
this.schedule(e);
callback(e);
}
}
this.#unskipped_branches.add(effect);
}
#process() {
@ -349,7 +358,9 @@ export class Batch {
next_batch.#process();
}
if (!batches.has(this)) {
// In sync mode flushSync can cause #commit to wrongfully think that there needs to be a rebase, so we only do it in async mode
// TODO fix the underlying cause, otherwise this will likely regress when non-async mode is removed
if (async_mode_flag && !batches.has(this)) {
this.#commit();
}
}
@ -530,6 +541,19 @@ export class Batch {
invariant(batch.#roots.length === 0, 'Batch has scheduled roots');
}
// A batch was unskipped in a later batch -> tell prior batches to unskip it, too
if (is_earlier) {
for (const unskipped of this.#unskipped_branches) {
batch.unskip_effect(unskipped, (e) => {
if ((e.f & (BLOCK_EFFECT | ASYNC)) !== 0) {
batch.schedule(e);
} else {
batch.#defer_effects([e]);
}
});
}
}
batch.activate();
/** @type {Set<Value>} */
@ -791,6 +815,7 @@ export class Batch {
}
}
// TODO Svelte@6 think about removing the callback argument.
/**
* Synchronously flush any pending updates.
* Returns void if no callback is provided, otherwise returns the result of calling the callback.

@ -400,7 +400,14 @@ function remove_reaction(signal, dependency) {
derived.f &= ~WAS_MARKED;
}
update_derived_status(derived);
// In a fork it's possible that a derived is executed and gets reactions, then commits, but is
// never re-executed. This is possible when the derived is only executed once in the context
// of a new branch which happens before fork.commit() runs. In this case, the derived still has
// UNINITIALIZED as its value, and then when it's loosing its reactions we need to ensure it stays
// DIRTY so it is reexecuted once someone wants its value again.
if (derived.v !== UNINITIALIZED) {
update_derived_status(derived);
}
// freeze any effects inside this derived
freeze_derived_effects(derived);

@ -0,0 +1,61 @@
{
"css": null,
"js": [],
"start": 0,
"end": 11,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "ExpressionTag",
"start": 0,
"end": 11,
"expression": {
"type": "Literal",
"start": 7,
"end": 9,
"loc": {
"start": {
"line": 1,
"column": 7
},
"end": {
"line": 1,
"column": 9
}
},
"value": 42,
"raw": "42",
"leadingComments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6
}
]
}
}
]
},
"options": null,
"comments": [
{
"type": "Block",
"value": "",
"start": 2,
"end": 6,
"loc": {
"start": {
"line": 1,
"column": 2
},
"end": {
"line": 1,
"column": 6
}
}
}
]
}

@ -0,0 +1,8 @@
<svelte:options customElement={{
tag: "my-inner",
props: { value: { reflect: true }}
}} />
<script>
export let value;
</script>
{value}

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [btn] = target.querySelectorAll('button');
btn.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>inc</button>
<my-inner value="2"></my-inner>
2
`
);
}
});

@ -0,0 +1,12 @@
<script>
import "./Inner.svelte"
let count = 1;
</script>
<button on:click={() => count++}>inc</button>
<!-- updating value prop will cause a flushSync -->
<my-inner value={count}></my-inner>
<!-- updating count will cause an internal_set -->
{#each [count] as row}
{row}
{/each}

@ -0,0 +1,20 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [, toggle] = target.querySelectorAll('button');
toggle?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button> 0`);
toggle?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button>`);
toggle?.click();
await tick();
assert.htmlEqual(target.innerHTML, `<button>0</button> <button>toggle</button> 0`);
}
});

@ -0,0 +1,13 @@
<script>
import { fork } from 'svelte';
let show = $state(false);
let count = $state(0);
let d_count = $derived(count);
</script>
<button onclick={() => count += 1}>{count}</button> <!-- just here so count is compiled as a source -->
<button onclick={() => fork(() => show = !show).commit()}>toggle</button>
{#if show}
{d_count}
{/if}

@ -0,0 +1,30 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target }) {
await tick();
const [load, resolve] = target.querySelectorAll('button');
load.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
<button>load</button>
<button>resolve</button>
`
);
resolve.click();
await tick();
assert.htmlEqual(
target.innerHTML,
`
search search search search
<button>load</button>
<button>resolve</button>
`
);
}
});

@ -0,0 +1,40 @@
<script>
let query = $state('');
// changing the query results in a new promise with loading initialized to true
const promise = $derived(push(query));
const resolvers = [];
function push(value) {
if (!value) return Promise.resolve(value);
const { promise, resolve } = Promise.withResolvers();
resolvers.push(() => {
// before resolving, set loading to false - this makes it run in a different batch
loading = false;
resolve(value);
});
let loading = $state(true);
Object.defineProperty(promise, 'loading', {
get() {
return loading;
}
});
return promise
}
</script>
{query} {await promise}
{#if !promise.loading}
{query}
{/if}
{#if !promise.loading}
{await query}
{/if}
<button onclick={() => query = 'search'}>load</button>
<button onclick={() => resolvers.shift()?.()}>resolve</button>
Loading…
Cancel
Save