diff --git a/.changeset/dull-cows-tie.md b/.changeset/dull-cows-tie.md new file mode 100644 index 0000000000..9835805dce --- /dev/null +++ b/.changeset/dull-cows-tie.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle parens in template expressions more robustly diff --git a/.changeset/great-toes-behave.md b/.changeset/great-toes-behave.md new file mode 100644 index 0000000000..5f2845e364 --- /dev/null +++ b/.changeset/great-toes-behave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correct types for `ontoggle` on `
` elements diff --git a/.changeset/petite-signs-flash.md b/.changeset/petite-signs-flash.md new file mode 100644 index 0000000000..38a1c7b47c --- /dev/null +++ b/.changeset/petite-signs-flash.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: unskip branches of earlier batches after commit diff --git a/.changeset/ripe-mails-wave.md b/.changeset/ripe-mails-wave.md new file mode 100644 index 0000000000..6ccc1c724d --- /dev/null +++ b/.changeset/ripe-mails-wave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip rebase logic in non-async mode diff --git a/.changeset/silent-rings-yell.md b/.changeset/silent-rings-yell.md new file mode 100644 index 0000000000..0b417103f7 --- /dev/null +++ b/.changeset/silent-rings-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: don't reset status of uninitialized deriveds diff --git a/documentation/docs/03-template-syntax/17-style.md b/documentation/docs/03-template-syntax/17-style.md index 8b25c221d6..6ddb128f4a 100644 --- a/documentation/docs/03-template-syntax/17-style.md +++ b/documentation/docs/03-template-syntax/17-style.md @@ -42,3 +42,9 @@ even over `!important` properties:
This will be red
This will still be red
``` + +You can set CSS custom properties: + +```svelte +
...
+``` diff --git a/packages/svelte/elements.d.ts b/packages/svelte/elements.d.ts index 885004dd2a..f18b7dea98 100644 --- a/packages/svelte/elements.d.ts +++ b/packages/svelte/elements.d.ts @@ -952,9 +952,9 @@ export interface HTMLDetailsAttributes extends HTMLAttributes | undefined | null; - ontoggle?: EventHandler | undefined | null; - ontogglecapture?: EventHandler | undefined | null; + 'on:toggle'?: ToggleEventHandler | undefined | null; + ontoggle?: ToggleEventHandler | undefined | null; + ontogglecapture?: ToggleEventHandler | undefined | null; } export interface HTMLDelAttributes extends HTMLAttributes { diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 77ce4a461c..797ab4cea5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index f90d59fa0b..24b7e2c6b0 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/1-parse/read/expression.js b/packages/svelte/src/compiler/phases/1-parse/read/expression.js index 5d21f85792..16d4c4e50f 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/expression.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/expression.js @@ -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 diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index d9518c726f..ff153128a5 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -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: [] }; diff --git a/packages/svelte/src/internal/client/reactivity/batch.js b/packages/svelte/src/internal/client/reactivity/batch.js index 23cc73ffc8..9c0832150a 100644 --- a/packages/svelte/src/internal/client/reactivity/batch.js +++ b/packages/svelte/src/internal/client/reactivity/batch.js @@ -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} + */ + #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} */ @@ -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. diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3d9f1d684b..33784fedd7 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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); diff --git a/packages/svelte/tests/parser-modern/samples/parens/input.svelte b/packages/svelte/tests/parser-modern/samples/parens/input.svelte new file mode 100644 index 0000000000..ad4f4b7940 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/parens/input.svelte @@ -0,0 +1 @@ +{(/**/ 42)} diff --git a/packages/svelte/tests/parser-modern/samples/parens/output.json b/packages/svelte/tests/parser-modern/samples/parens/output.json new file mode 100644 index 0000000000..7a5b4b38d8 --- /dev/null +++ b/packages/svelte/tests/parser-modern/samples/parens/output.json @@ -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 + } + } + } + ] +} diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte new file mode 100644 index 0000000000..6d7c982db0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/Inner.svelte @@ -0,0 +1,8 @@ + + +{value} diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js new file mode 100644 index 0000000000..2798f533c6 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/_config.js @@ -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, + ` + + + 2 + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte new file mode 100644 index 0000000000..3be1202c97 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/flush-sync-each-block/main.svelte @@ -0,0 +1,12 @@ + + + + + + +{#each [count] as row} + {row} +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js new file mode 100644 index 0000000000..4a03a54e7a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/_config.js @@ -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, ` 0`); + + toggle?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` `); + + toggle?.click(); + await tick(); + assert.htmlEqual(target.innerHTML, ` 0`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte new file mode 100644 index 0000000000..519857a9ce --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-fork-if-derived/main.svelte @@ -0,0 +1,13 @@ + + + + + +{#if show} + {d_count} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js new file mode 100644 index 0000000000..d578def483 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/_config.js @@ -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, + ` + + + ` + ); + + resolve.click(); + await tick(); + assert.htmlEqual( + target.innerHTML, + ` + search search search search + + + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte new file mode 100644 index 0000000000..36ebcd26d1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/async-if-block-unskip/main.svelte @@ -0,0 +1,40 @@ + + +{query} {await promise} + +{#if !promise.loading} + {query} +{/if} + +{#if !promise.loading} + {await query} +{/if} + + +