From c582b5d3b559283619384dd78b77526ce1d6f722 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Feb 2025 16:47:52 -0500 Subject: [PATCH 01/23] chore: simplify flush_sync (#15346) --- packages/svelte/src/internal/client/runtime.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8a9ca9065b..8c98f948f3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -858,11 +858,8 @@ export function flush_sync(fn) { try { infinite_loop_guard(); - /** @type {Effect[]} */ - const root_effects = []; - scheduler_mode = FLUSH_SYNC; - queued_root_effects = root_effects; + queued_root_effects = []; is_micro_task_queued = false; flush_queued_root_effects(previous_queued_root_effects); @@ -870,7 +867,7 @@ export function flush_sync(fn) { var result = fn?.(); flush_tasks(); - if (queued_root_effects.length > 0 || root_effects.length > 0) { + if (queued_root_effects.length > 0) { flush_sync(); } From ac215d9b23d091b14caa1f47951997dc6acfb841 Mon Sep 17 00:00:00 2001 From: realguse Date: Fri, 21 Feb 2025 18:43:32 +0100 Subject: [PATCH 02/23] docs: rephrase copyright holder (#15333) --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index abbace7bfe..f872adf738 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2016-2025 [these people](https://github.com/sveltejs/svelte/graphs/contributors) +Copyright (c) 2016-2025 [Svelte Contributors](https://github.com/sveltejs/svelte/graphs/contributors) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From 60b22c0b5fc17bf0062d08ad16dc2ba60af12971 Mon Sep 17 00:00:00 2001 From: realguse Date: Fri, 21 Feb 2025 18:44:23 +0100 Subject: [PATCH 03/23] docs: update casing (#15321) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d2f718da7..7ea7164752 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ -[![license](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) +[![License](https://img.shields.io/npm/l/svelte.svg)](LICENSE.md) [![Chat](https://img.shields.io/discord/457912077277855764?label=chat&logo=discord)](https://svelte.dev/chat) ## What is Svelte? From 1e1aea4063eda9363db38f00ce7feb8bd3954323 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Fri, 21 Feb 2025 19:24:19 +0100 Subject: [PATCH 04/23] chore: remove unnecessary `?? ''` on some expressions (#15287) * no `?? ''` on some expressions * changeset * delete operator returns boolean * inverted conditions are a lil confusing * no need for else after return * simplify condition * may as well add special handling for undefined while we're here * use normal string literal when there are no values * omit assignment when there is no text content --------- Co-authored-by: Rich Harris --- .changeset/serious-glasses-kiss.md | 5 ++ .../client/visitors/RegularElement.js | 17 +++--- .../client/visitors/shared/utils.js | 57 +++++++++++-------- 3 files changed, 46 insertions(+), 33 deletions(-) create mode 100644 .changeset/serious-glasses-kiss.md diff --git a/.changeset/serious-glasses-kiss.md b/.changeset/serious-glasses-kiss.md new file mode 100644 index 0000000000..29a6fafeae --- /dev/null +++ b/.changeset/serious-glasses-kiss.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove unnecessary `?? ''` on some expressions diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 018bdacc5e..d4502210bd 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -364,15 +364,14 @@ export function RegularElement(node, context) { trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { - child_state.init.push( - b.stmt( - b.assignment( - '=', - b.member(context.state.node, 'textContent'), - build_template_chunk(trimmed, context.visit, child_state).value - ) - ) - ); + const { value } = build_template_chunk(trimmed, context.visit, child_state); + const empty_string = value.type === 'Literal' && value.value === ''; + + if (!empty_string) { + child_state.init.push( + b.stmt(b.assignment('=', b.member(context.state.node, 'textContent'), value)) + ); + } } else { /** @type {Expression} */ let arg = context.state.node; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index ca5094f455..c25ef3ab50 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -101,11 +101,15 @@ export function build_template_chunk( if (node.type === 'Text') { quasi.value.cooked += node.data; - } else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') { + } else if (node.expression.type === 'Literal') { if (node.expression.value != null) { quasi.value.cooked += node.expression.value + ''; } - } else { + } else if ( + node.expression.type !== 'Identifier' || + node.expression.name !== 'undefined' || + state.scope.get('undefined') + ) { let value = memoize( /** @type {Expression} */ (visit(node.expression, state)), node.metadata.expression @@ -117,31 +121,33 @@ export function build_template_chunk( // If we have a single expression, then pass that in directly to possibly avoid doing // extra work in the template_effect (instead we do the work in set_text). return { value, has_state }; - } else { - // add `?? ''` where necessary (TODO optimise more cases) - if ( - value.type === 'LogicalExpression' && - value.right.type === 'Literal' && - (value.operator === '??' || value.operator === '||') - ) { - // `foo ?? null` -=> `foo ?? ''` - // otherwise leave the expression untouched - if (value.right.value === null) { - value = { ...value, right: b.literal('') }; - } - } else if ( - state.analysis.props_id && - value.type === 'Identifier' && - value.name === state.analysis.props_id.name - ) { - // do nothing ($props.id() is never null/undefined) - } else { - value = b.logical('??', value, b.literal('')); + } + + if ( + value.type === 'LogicalExpression' && + value.right.type === 'Literal' && + (value.operator === '??' || value.operator === '||') + ) { + // `foo ?? null` -=> `foo ?? ''` + // otherwise leave the expression untouched + if (value.right.value === null) { + value = { ...value, right: b.literal('') }; } + } + + const is_defined = + value.type === 'BinaryExpression' || + (value.type === 'UnaryExpression' && value.operator !== 'void') || + (value.type === 'LogicalExpression' && value.right.type === 'Literal') || + (value.type === 'Identifier' && value.name === state.analysis.props_id?.name); - expressions.push(value); + if (!is_defined) { + // add `?? ''` where necessary (TODO optimise more cases) + value = b.logical('??', value, b.literal('')); } + expressions.push(value); + quasi = b.quasi('', i + 1 === values.length); quasis.push(quasi); } @@ -151,7 +157,10 @@ export function build_template_chunk( quasi.value.raw = sanitize_template_string(/** @type {string} */ (quasi.value.cooked)); } - const value = b.template(quasis, expressions); + const value = + expressions.length > 0 + ? b.template(quasis, expressions) + : b.literal(/** @type {string} */ (quasi.value.cooked)); return { value, has_state }; } From 3c4a8d425b8192dc11ea2af256d531c51c37ba5d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Feb 2025 14:40:46 -0500 Subject: [PATCH 05/23] chore: make Binding a class (#15359) * chore: make Binding a class * reorder * regenerate --- packages/svelte/src/compiler/phases/scope.js | 82 ++++++++++++++---- packages/svelte/src/compiler/types/index.d.ts | 85 ++++--------------- packages/svelte/types/index.d.ts | 2 +- 3 files changed, 83 insertions(+), 86 deletions(-) diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 51e9eb088d..41e1c78c05 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,6 +1,6 @@ /** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */ /** @import { Context, Visitor } from 'zimmerframe' */ -/** @import { AST, Binding, DeclarationKind } from '#compiler' */ +/** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; import { walk } from 'zimmerframe'; import { create_expression_metadata } from './nodes.js'; @@ -16,6 +16,69 @@ import { is_reserved, is_rune } from '../../utils.js'; import { determine_slot } from '../utils/slot.js'; import { validate_identifier_name } from './2-analyze/visitors/shared/utils.js'; +export class Binding { + /** @type {Scope} */ + scope; + + /** @type {Identifier} */ + node; + + /** @type {BindingKind} */ + kind; + + /** @type {DeclarationKind} */ + declaration_kind; + + /** + * What the value was initialized with. + * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` + * @type {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | AST.EachBlock | AST.SnippetBlock} + */ + initial; + + /** @type {Array<{ node: Identifier; path: AST.SvelteNode[] }>} */ + references = []; + + /** + * For `legacy_reactive`: its reactive dependencies + * @type {Binding[]} + */ + legacy_dependencies = []; + + /** + * Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() + * @type {string | null} + */ + prop_alias = null; + + /** + * Additional metadata, varies per binding type + * @type {null | { inside_rest?: boolean }} + */ + metadata = null; + + is_called = false; + mutated = false; + reassigned = false; + updated = false; + + /** + * + * @param {Scope} scope + * @param {Identifier} node + * @param {BindingKind} kind + * @param {DeclarationKind} declaration_kind + * @param {Binding['initial']} initial + */ + constructor(scope, node, kind, declaration_kind, initial) { + this.scope = scope; + this.node = node; + this.initial = initial; + this.kind = kind; + this.declaration_kind = declaration_kind; + } +} + export class Scope { /** @type {ScopeRoot} */ root; @@ -100,22 +163,7 @@ export class Scope { e.declaration_duplicate(node, node.name); } - /** @type {Binding} */ - const binding = { - node, - references: [], - legacy_dependencies: [], - initial, - reassigned: false, - mutated: false, - updated: false, - scope: this, - kind, - declaration_kind, - is_called: false, - prop_alias: null, - metadata: null - }; + const binding = new Binding(this, node, kind, declaration_kind, initial); validate_identifier_name(binding, this.function_depth); diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index b80b717e42..eec41bad9d 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -1,12 +1,5 @@ -import type { - ClassDeclaration, - Expression, - FunctionDeclaration, - Identifier, - ImportDeclaration -} from 'estree'; import type { SourceMap } from 'magic-string'; -import type { Scope } from '../phases/scope.js'; +import type { Binding } from '../phases/scope.js'; import type { AST, Namespace } from './template.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; @@ -241,6 +234,20 @@ export type ValidatedCompileOptions = ValidatedModuleCompileOptions & hmr: CompileOptions['hmr']; }; +export type BindingKind = + | 'normal' // A variable that is not in any way special + | 'prop' // A normal prop (possibly reassigned or mutated) + | 'bindable_prop' // A prop one can `bind:` to (possibly reassigned or mutated) + | 'rest_prop' // A rest prop + | 'raw_state' // A state variable + | 'state' // A deeply reactive state variable + | 'derived' // A derived variable + | 'each' // An each block parameter + | 'snippet' // A snippet parameter + | 'store_sub' // A $store value + | 'legacy_reactive' // A `$:` declaration + | 'template'; // A binding declared in the template, e.g. in an `await` block or `const` tag + export type DeclarationKind = | 'var' | 'let' @@ -251,66 +258,6 @@ export type DeclarationKind = | 'rest_param' | 'synthetic'; -export interface Binding { - node: Identifier; - /** - * - `normal`: A variable that is not in any way special - * - `prop`: A normal prop (possibly reassigned or mutated) - * - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated) - * - `rest_prop`: A rest prop - * - `state`: A state variable - * - `derived`: A derived variable - * - `each`: An each block parameter - * - `snippet`: A snippet parameter - * - `store_sub`: A $store value - * - `legacy_reactive`: A `$:` declaration - * - `template`: A binding declared in the template, e.g. in an `await` block or `const` tag - */ - kind: - | 'normal' - | 'prop' - | 'bindable_prop' - | 'rest_prop' - | 'state' - | 'raw_state' - | 'derived' - | 'each' - | 'snippet' - | 'store_sub' - | 'legacy_reactive' - | 'template' - | 'snippet'; - declaration_kind: DeclarationKind; - /** - * What the value was initialized with. - * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` - */ - initial: - | null - | Expression - | FunctionDeclaration - | ClassDeclaration - | ImportDeclaration - | AST.EachBlock - | AST.SnippetBlock; - is_called: boolean; - references: { node: Identifier; path: AST.SvelteNode[] }[]; - mutated: boolean; - reassigned: boolean; - /** `true` if mutated _or_ reassigned */ - updated: boolean; - scope: Scope; - /** For `legacy_reactive`: its reactive dependencies */ - legacy_dependencies: Binding[]; - /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */ - prop_alias: string | null; - /** Additional metadata, varies per binding type */ - metadata: { - /** `true` if is (inside) a rest parameter */ - inside_rest?: boolean; - } | null; -} - export interface ExpressionMetadata { /** All the bindings that are referenced inside this expression */ dependencies: Set; @@ -322,5 +269,7 @@ export interface ExpressionMetadata { export * from './template.js'; +export { Binding, Scope } from '../phases/scope.js'; + // TODO this chain is a bit weird export { ReactiveStatement } from '../phases/types.js'; diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 77d78477ee..b9ab8a522c 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -622,8 +622,8 @@ declare module 'svelte/animate' { } declare module 'svelte/compiler' { - import type { Expression, Identifier, ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { SourceMap } from 'magic-string'; + import type { ArrayExpression, ArrowFunctionExpression, VariableDeclaration, VariableDeclarator, Expression, Identifier, MemberExpression, Node, ObjectExpression, Pattern, Program, ChainExpression, SimpleCallExpression, SequenceExpression } from 'estree'; import type { Location } from 'locate-character'; /** * `compile` converts your `.svelte` source code into a JavaScript module that exports a component From 5a946e7905314f13ed51fb3d37872160c81ab996 Mon Sep 17 00:00:00 2001 From: Alexis Date: Mon, 24 Feb 2025 13:29:35 +0100 Subject: [PATCH 06/23] fix: Allow @const inside #key (#15377) --- .changeset/healthy-guests-itch.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/ConstTag.js | 1 + .../validator/samples/const-tag-inside-key-block/errors.json | 1 + .../samples/const-tag-inside-key-block/input.svelte | 3 +++ 4 files changed, 10 insertions(+) create mode 100644 .changeset/healthy-guests-itch.md create mode 100644 packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json create mode 100644 packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte diff --git a/.changeset/healthy-guests-itch.md b/.changeset/healthy-guests-itch.md new file mode 100644 index 0000000000..24b33f761d --- /dev/null +++ b/.changeset/healthy-guests-itch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +Allow `@const` inside `#key` diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js index 3f5e0473c5..f723f8447c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -25,6 +25,7 @@ export function ConstTag(node, context) { grand_parent?.type !== 'AwaitBlock' && grand_parent?.type !== 'SnippetBlock' && grand_parent?.type !== 'SvelteBoundary' && + grand_parent?.type !== 'KeyBlock' && ((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') || !grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot'))) ) { diff --git a/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte new file mode 100644 index 0000000000..008072bc47 --- /dev/null +++ b/packages/svelte/tests/validator/samples/const-tag-inside-key-block/input.svelte @@ -0,0 +1,3 @@ +{#key 'key'} + {@const foo = 'bar'} +{/key} From d4360af7517b6c7e980cafbd2c36dfcd55085da9 Mon Sep 17 00:00:00 2001 From: adiGuba Date: Mon, 24 Feb 2025 13:46:23 +0100 Subject: [PATCH 07/23] chore: rewrite set_class() to handle directives (#15352) * set_class with class: directives * update expected result (remove leading space) * fix * optimize literals * add test * add test for mutations on hydration * clean observer * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js unused Co-authored-by: Rich Harris * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js unused for now Co-authored-by: Rich Harris * Update packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js unused for now Co-authored-by: Rich Harris * Update packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js nit Co-authored-by: Rich Harris * Update packages/svelte/src/internal/client/dom/elements/attributes.js nit Co-authored-by: Rich Harris * Update packages/svelte/src/internal/shared/attributes.js rename clazz to value :D Co-authored-by: Rich Harris * remove unused + fix JSDoc * drive-by fix * minor style tweaks * tweak test * this is faster * tweak * tweak * this is faster * typo * tweak * changeset --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/stale-plums-drop.md | 5 + .../src/compiler/phases/2-analyze/index.js | 84 ++++------ .../client/visitors/RegularElement.js | 95 ++++++++---- .../client/visitors/SvelteElement.js | 41 +++-- .../client/visitors/shared/element.js | 144 ++++++++++++----- .../server/visitors/shared/element.js | 123 +++++++++------ .../client/dom/elements/attributes.js | 19 ++- .../src/internal/client/dom/elements/class.js | 139 ++++------------- .../src/internal/client/dom/operations.js | 2 +- packages/svelte/src/internal/client/index.js | 6 +- packages/svelte/src/internal/server/index.js | 22 +-- .../svelte/src/internal/shared/attributes.js | 42 +++++ .../undefined-with-scope/expected.html | 2 +- .../_config.js | 10 +- .../_config.js | 8 +- .../class-directive-mutations/_config.js | 43 ++++++ .../class-directive-mutations/main.svelte | 47 ++++++ .../samples/class-directive/_config.js | 145 ++++++++++++++++++ .../samples/class-directive/main.svelte | 40 +++++ 19 files changed, 688 insertions(+), 329 deletions(-) create mode 100644 .changeset/stale-plums-drop.md create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte diff --git a/.changeset/stale-plums-drop.md b/.changeset/stale-plums-drop.md new file mode 100644 index 0000000000..d39268eb72 --- /dev/null +++ b/.changeset/stale-plums-drop.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly override class attributes with class directives diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 93c438c852..ca9297279e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -767,66 +767,40 @@ export function analyze_component(root, source, options) { if (!should_ignore_unused) { warn_unused(analysis.css.ast); } + } - outer: for (const node of analysis.elements) { - if (node.metadata.scoped) { - // Dynamic elements in dom mode always use spread for attributes and therefore shouldn't have a class attribute added to them - // TODO this happens during the analysis phase, which shouldn't know anything about client vs server - if (node.type === 'SvelteElement' && options.generate === 'client') continue; - - /** @type {AST.Attribute | undefined} */ - let class_attribute = undefined; - - for (const attribute of node.attributes) { - if (attribute.type === 'SpreadAttribute') { - // The spread method appends the hash to the end of the class attribute on its own - continue outer; - } + for (const node of analysis.elements) { + if (node.metadata.scoped && is_custom_element_node(node)) { + mark_subtree_dynamic(node.metadata.path); + } - if (attribute.type !== 'Attribute') continue; - if (attribute.name.toLowerCase() !== 'class') continue; - // The dynamic class method appends the hash to the end of the class attribute on its own - if (attribute.metadata.needs_clsx) continue outer; + let has_class = false; + let has_spread = false; + let has_class_directive = false; - class_attribute = attribute; - } + for (const attribute of node.attributes) { + // The spread method appends the hash to the end of the class attribute on its own + if (attribute.type === 'SpreadAttribute') { + has_spread = true; + break; + } + has_class_directive ||= attribute.type === 'ClassDirective'; + has_class ||= attribute.type === 'Attribute' && attribute.name.toLowerCase() === 'class'; + } - if (class_attribute && class_attribute.value !== true) { - if (is_text_attribute(class_attribute)) { - class_attribute.value[0].data += ` ${analysis.css.hash}`; - } else { - /** @type {AST.Text} */ - const css_text = { - type: 'Text', - data: ` ${analysis.css.hash}`, - raw: ` ${analysis.css.hash}`, - start: -1, - end: -1 - }; - - if (Array.isArray(class_attribute.value)) { - class_attribute.value.push(css_text); - } else { - class_attribute.value = [class_attribute.value, css_text]; - } - } - } else { - node.attributes.push( - create_attribute('class', -1, -1, [ - { - type: 'Text', - data: analysis.css.hash, - raw: analysis.css.hash, - start: -1, - end: -1 - } - ]) - ); - if (is_custom_element_node(node) && node.attributes.length === 1) { - mark_subtree_dynamic(node.metadata.path); + // We need an empty class to generate the set_class() or class="" correctly + if (!has_spread && !has_class && (node.metadata.scoped || has_class_directive)) { + node.attributes.push( + create_attribute('class', -1, -1, [ + { + type: 'Text', + data: '', + raw: '', + start: -1, + end: -1 } - } - } + ]) + ); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index d4502210bd..434b49caa1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -1,4 +1,4 @@ -/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement } from 'estree' */ +/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, ObjectExpression, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { SourceLocation } from '#shared' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ @@ -20,9 +20,9 @@ import { build_getter } from '../utils.js'; import { get_attribute_name, build_attribute_value, - build_class_directives, build_style_directives, - build_set_attributes + build_set_attributes, + build_set_class } from './shared/element.js'; import { process_children } from './shared/fragment.js'; import { @@ -223,13 +223,13 @@ export function RegularElement(node, context) { build_set_attributes( attributes, + class_directives, context, node, node_id, attributes_id, (node.metadata.svg || node.metadata.mathml || is_custom_element_node(node)) && b.true, - is_custom_element_node(node) && b.true, - context.state + is_custom_element_node(node) && b.true ); // If value binding exists, that one takes care of calling $.init_select @@ -270,13 +270,22 @@ export function RegularElement(node, context) { continue; } + const name = get_attribute_name(node, attribute); if ( !is_custom_element && !cannot_be_set_statically(attribute.name) && - (attribute.value === true || is_text_attribute(attribute)) + (attribute.value === true || is_text_attribute(attribute)) && + (name !== 'class' || class_directives.length === 0) ) { - const name = get_attribute_name(node, attribute); - const value = is_text_attribute(attribute) ? attribute.value[0].data : true; + let value = is_text_attribute(attribute) ? attribute.value[0].data : true; + + if (name === 'class' && node.metadata.scoped && context.state.analysis.css.hash) { + if (value === true || value === '') { + value = context.state.analysis.css.hash; + } else { + value += ' ' + context.state.analysis.css.hash; + } + } if (name !== 'class' || value) { context.state.template.push( @@ -290,15 +299,22 @@ export function RegularElement(node, context) { continue; } - const is = is_custom_element - ? build_custom_element_attribute_update_assignment(node_id, attribute, context) - : build_element_attribute_update_assignment(node, node_id, attribute, attributes, context); + const is = + is_custom_element && name !== 'class' + ? build_custom_element_attribute_update_assignment(node_id, attribute, context) + : build_element_attribute_update_assignment( + node, + node_id, + attribute, + attributes, + class_directives, + context + ); if (is) is_attributes_reactive = true; } } - // class/style directives must be applied last since they could override class/style attributes - build_class_directives(class_directives, node_id, context, is_attributes_reactive); + // style directives must be applied last since they could override class/style attributes build_style_directives(style_directives, node_id, context, is_attributes_reactive); if ( @@ -491,6 +507,27 @@ function setup_select_synchronization(value_binding, context) { ); } +/** + * @param {AST.ClassDirective[]} class_directives + * @param {ComponentContext} context + * @return {ObjectExpression} + */ +export function build_class_directives_object(class_directives, context) { + let properties = []; + + for (const d of class_directives) { + let expression = /** @type Expression */ (context.visit(d.expression)); + + if (d.metadata.expression.has_call) { + expression = get_expression_id(context.state, expression); + } + + properties.push(b.init(d.name, expression)); + } + + return b.object(properties); +} + /** * Serializes an assignment to an element property by adding relevant statements to either only * the init or the the init and update arrays, depending on whether or not the value is dynamic. @@ -517,6 +554,7 @@ function setup_select_synchronization(value_binding, context) { * @param {Identifier} node_id * @param {AST.Attribute} attribute * @param {Array} attributes + * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @returns {boolean} */ @@ -525,6 +563,7 @@ function build_element_attribute_update_assignment( node_id, attribute, attributes, + class_directives, context ) { const state = context.state; @@ -563,19 +602,15 @@ function build_element_attribute_update_assignment( let update; if (name === 'class') { - if (attribute.metadata.needs_clsx) { - value = b.call('$.clsx', value); - } - - update = b.stmt( - b.call( - is_svg ? '$.set_svg_class' : is_mathml ? '$.set_mathml_class' : '$.set_class', - node_id, - value, - attribute.metadata.needs_clsx && context.state.analysis.css.hash - ? b.literal(context.state.analysis.css.hash) - : undefined - ) + return build_set_class( + element, + node_id, + attribute, + value, + has_state, + class_directives, + context, + !is_svg && !is_mathml ); } else if (name === 'value') { update = b.stmt(b.call('$.set_value', node_id, value)); @@ -639,14 +674,6 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive let { value, has_state } = build_attribute_value(attribute.value, context); - // We assume that noone's going to redefine the semantics of the class attribute on custom elements, i.e. it's still used for CSS classes - if (name === 'class' && attribute.metadata.needs_clsx) { - if (context.state.analysis.css.hash) { - value = b.array([value, b.literal(context.state.analysis.css.hash)]); - } - value = b.call('$.clsx', value); - } - const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); if (has_state) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index e275283655..7149f8d0e4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -7,11 +7,11 @@ import * as b from '../../../../utils/builders.js'; import { determine_namespace_for_children } from '../../utils.js'; import { build_attribute_value, - build_class_directives, build_set_attributes, + build_set_class, build_style_directives } from './shared/element.js'; -import { build_render_statement } from './shared/utils.js'; +import { build_render_statement, get_expression_id } from './shared/utils.js'; /** * @param {AST.SvelteElement} node @@ -80,31 +80,46 @@ export function SvelteElement(node, context) { // Then do attributes let is_attributes_reactive = false; - if (attributes.length === 0) { - if (context.state.analysis.css.hash) { - inner_context.state.init.push( - b.stmt(b.call('$.set_class', element_id, b.literal(context.state.analysis.css.hash))) - ); - } - } else { + if ( + attributes.length === 1 && + attributes[0].type === 'Attribute' && + attributes[0].name.toLowerCase() === 'class' + ) { + // special case when there only a class attribute + let { value, has_state } = build_attribute_value( + attributes[0].value, + context, + (value, metadata) => (metadata.has_call ? get_expression_id(context.state, value) : value) + ); + + is_attributes_reactive = build_set_class( + node, + element_id, + attributes[0], + value, + has_state, + class_directives, + inner_context, + false + ); + } else if (attributes.length) { const attributes_id = b.id(context.state.scope.generate('attributes')); // Always use spread because we don't know whether the element is a custom element or not, // therefore we need to do the "how to set an attribute" logic at runtime. is_attributes_reactive = build_set_attributes( attributes, + class_directives, inner_context, node, element_id, attributes_id, b.binary('===', b.member(element_id, 'namespaceURI'), b.id('$.NAMESPACE_SVG')), - b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')), - context.state + b.call(b.member(b.member(element_id, 'nodeName'), 'includes'), b.literal('-')) ); } - // class/style directives must be applied last since they could override class/style attributes - build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); + // style directives must be applied last since they could override class/style attributes build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive); const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag))); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index abffad0ff7..fc5fd2cc4c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -1,32 +1,34 @@ /** @import { Expression, Identifier, ObjectExpression } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../../types' */ +import { escape_html } from '../../../../../../escaping.js'; import { normalize_attribute } from '../../../../../../utils.js'; import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '../../../../../utils/builders.js'; import { build_getter } from '../../utils.js'; +import { build_class_directives_object } from '../RegularElement.js'; import { build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes + * @param {AST.ClassDirective[]} class_directives * @param {ComponentContext} context * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id * @param {Identifier} attributes_id * @param {false | Expression} preserve_attribute_case * @param {false | Expression} is_custom_element - * @param {ComponentClientTransformState} state */ export function build_set_attributes( attributes, + class_directives, context, element, element_id, attributes_id, preserve_attribute_case, - is_custom_element, - state + is_custom_element ) { let is_dynamic = false; @@ -68,6 +70,19 @@ export function build_set_attributes( } } + if (class_directives.length) { + values.push( + b.prop( + 'init', + b.array([b.id('$.CLASS')]), + build_class_directives_object(class_directives, context) + ) + ); + + is_dynamic ||= + class_directives.find((directive) => directive.metadata.expression.has_state) !== null; + } + const call = b.call( '$.set_attributes', element_id, @@ -134,39 +149,6 @@ export function build_style_directives( } } -/** - * Serializes each class directive into something like `$.class_toogle(element, class_name, value)` - * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. - * @param {AST.ClassDirective[]} class_directives - * @param {Identifier} element_id - * @param {ComponentContext} context - * @param {boolean} is_attributes_reactive - */ -export function build_class_directives( - class_directives, - element_id, - context, - is_attributes_reactive -) { - const state = context.state; - for (const directive of class_directives) { - const { has_state, has_call } = directive.metadata.expression; - let value = /** @type {Expression} */ (context.visit(directive.expression)); - - if (has_call) { - value = get_expression_id(state, value); - } - - const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); - - if (is_attributes_reactive || has_state) { - state.update.push(update); - } else { - state.init.push(update); - } - } -} - /** * @param {AST.Attribute['value']} value * @param {ComponentContext} context @@ -207,3 +189,93 @@ export function get_attribute_name(element, attribute) { return attribute.name; } + +/** + * @param {AST.RegularElement | AST.SvelteElement} element + * @param {Identifier} node_id + * @param {AST.Attribute | null} attribute + * @param {Expression} value + * @param {boolean} has_state + * @param {AST.ClassDirective[]} class_directives + * @param {ComponentContext} context + * @param {boolean} is_html + * @returns {boolean} + */ +export function build_set_class( + element, + node_id, + attribute, + value, + has_state, + class_directives, + context, + is_html +) { + if (attribute && attribute.metadata.needs_clsx) { + value = b.call('$.clsx', value); + } + + /** @type {Identifier | undefined} */ + let previous_id; + + /** @type {ObjectExpression | Identifier | undefined} */ + let prev; + + /** @type {ObjectExpression | undefined} */ + let next; + + if (class_directives.length) { + next = build_class_directives_object(class_directives, context); + has_state ||= class_directives.some((d) => d.metadata.expression.has_state); + + if (has_state) { + previous_id = b.id(context.state.scope.generate('classes')); + context.state.init.push(b.declaration('let', [b.declarator(previous_id)])); + prev = previous_id; + } else { + prev = b.object([]); + } + } + + /** @type {Expression | undefined} */ + let css_hash; + + if (element.metadata.scoped && context.state.analysis.css.hash) { + if (value.type === 'Literal' && (value.value === '' || value.value === null)) { + value = b.literal(context.state.analysis.css.hash); + } else if (value.type === 'Literal' && typeof value.value === 'string') { + value = b.literal(escape_html(value.value, true) + ' ' + context.state.analysis.css.hash); + } else { + css_hash = b.literal(context.state.analysis.css.hash); + } + } + + if (!css_hash && next) { + css_hash = b.null; + } + + /** @type {Expression} */ + let set_class = b.call( + '$.set_class', + node_id, + is_html ? b.literal(1) : b.literal(0), + value, + css_hash, + prev, + next + ); + + if (previous_id) { + set_class = b.assignment('=', previous_id, set_class); + } + + const update = b.stmt(set_class); + + if (has_state) { + context.state.update.push(update); + return true; + } + + context.state.init.push(update); + return false; +} diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index d0d800d3cb..57101af4b8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -1,4 +1,4 @@ -/** @import { Expression, Literal } from 'estree' */ +/** @import { Expression, Literal, ObjectExpression } from 'estree' */ /** @import { AST, Namespace } from '#compiler' */ /** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */ import { @@ -24,6 +24,7 @@ import { is_content_editable_binding, is_load_error_element } from '../../../../../../utils.js'; +import { escape_html } from '../../../../../../escaping.js'; const WHITESPACE_INSENSITIVE_ATTRIBUTES = ['class', 'style']; @@ -86,23 +87,15 @@ export function build_element_attributes(node, context) { } else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') { if (attribute.name === 'class') { class_index = attributes.length; - if (attribute.metadata.needs_clsx) { - const clsx_value = b.call( - '$.clsx', - /** @type {AST.ExpressionTag} */ (attribute.value).expression - ); attributes.push({ ...attribute, value: { .../** @type {AST.ExpressionTag} */ (attribute.value), - expression: context.state.analysis.css.hash - ? b.binary( - '+', - b.binary('+', clsx_value, b.literal(' ')), - b.literal(context.state.analysis.css.hash) - ) - : clsx_value + expression: b.call( + '$.clsx', + /** @type {AST.ExpressionTag} */ (attribute.value).expression + ) } }); } else { @@ -219,8 +212,9 @@ export function build_element_attributes(node, context) { } } - if (class_directives.length > 0 && !has_spread) { - const class_attribute = build_class_directives( + if ((node.metadata.scoped || class_directives.length) && !has_spread) { + const class_attribute = build_to_class( + node.metadata.scoped ? context.state.analysis.css.hash : null, class_directives, /** @type {AST.Attribute | null} */ (attributes[class_index] ?? null) ); @@ -274,9 +268,14 @@ export function build_element_attributes(node, context) { WHITESPACE_INSENSITIVE_ATTRIBUTES.includes(name) ); - context.state.template.push( - b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) - ); + // pre-escape and inline literal attributes : + if (value.type === 'Literal' && typeof value.value === 'string') { + context.state.template.push(b.literal(` ${name}="${escape_html(value.value, true)}"`)); + } else { + context.state.template.push( + b.call('$.attr', b.literal(name), value, is_boolean_attribute(name) && b.true) + ); + } } } @@ -322,7 +321,7 @@ function build_element_spread_attributes( let styles; let flags = 0; - if (class_directives.length > 0 || context.state.analysis.css.hash) { + if (class_directives.length) { const properties = class_directives.map((directive) => b.init( directive.name, @@ -331,11 +330,6 @@ function build_element_spread_attributes( : /** @type {Expression} */ (context.visit(directive.expression)) ) ); - - if (context.state.analysis.css.hash) { - properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true))); - } - classes = b.object(properties); } @@ -374,55 +368,82 @@ function build_element_spread_attributes( }) ); - const args = [object, classes, styles, flags ? b.literal(flags) : undefined]; + const css_hash = context.state.analysis.css.hash + ? b.literal(context.state.analysis.css.hash) + : b.null; + + const args = [object, css_hash, classes, styles, flags ? b.literal(flags) : undefined]; context.state.template.push(b.call('$.spread_attributes', ...args)); } /** * + * @param {string | null} hash * @param {AST.ClassDirective[]} class_directives * @param {AST.Attribute | null} class_attribute * @returns */ -function build_class_directives(class_directives, class_attribute) { - const expressions = class_directives.map((directive) => - b.conditional(directive.expression, b.literal(directive.name), b.literal('')) - ); - +function build_to_class(hash, class_directives, class_attribute) { if (class_attribute === null) { class_attribute = create_attribute('class', -1, -1, []); } - const chunks = get_attribute_chunks(class_attribute.value); - const last = chunks.at(-1); - - if (last?.type === 'Text') { - last.data += ' '; - last.raw += ' '; - } else if (last) { - chunks.push({ - type: 'Text', - start: -1, - end: -1, - data: ' ', - raw: ' ' - }); + /** @type {ObjectExpression | undefined} */ + let classes; + + if (class_directives.length) { + classes = b.object( + class_directives.map((directive) => + b.prop('init', b.literal(directive.name), directive.expression) + ) + ); + } + + /** @type {Expression} */ + let class_name; + + if (class_attribute.value === true) { + class_name = b.literal(''); + } else if (Array.isArray(class_attribute.value)) { + if (class_attribute.value.length === 0) { + class_name = b.null; + } else { + class_name = class_attribute.value + .map((val) => (val.type === 'Text' ? b.literal(val.data) : val.expression)) + .reduce((left, right) => b.binary('+', left, right)); + } + } else { + class_name = class_attribute.value.expression; } - chunks.push({ + /** @type {Expression} */ + let expression; + + if ( + hash && + !classes && + class_name.type === 'Literal' && + (class_name.value === null || class_name.value === '' || typeof class_name.value === 'string') + ) { + if (class_name.value === null || class_name.value === '') { + expression = b.literal(hash); + } else { + expression = b.literal(escape_html(class_name.value, true) + ' ' + hash); + } + } else { + expression = b.call('$.to_class', class_name, b.literal(hash), classes); + } + + class_attribute.value = { type: 'ExpressionTag', start: -1, end: -1, - expression: b.call( - b.member(b.call(b.member(b.array(expressions), 'filter'), b.id('Boolean')), b.id('join')), - b.literal(' ') - ), + expression: expression, metadata: { expression: create_expression_metadata() } - }); + }; - class_attribute.value = chunks; return class_attribute; } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 2dba2d797a..151024e85c 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -14,6 +14,10 @@ import { set_active_reaction } from '../../runtime.js'; import { clsx } from '../../../shared/attributes.js'; +import { set_class } from './class.js'; + +export const CLASS = Symbol('class'); +export const STYLE = Symbol('style'); /** * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need @@ -254,8 +258,8 @@ export function set_custom_element_data(node, prop, value) { /** * Spreads attributes onto a DOM element, taking into account the currently set attributes * @param {Element & ElementCSSInlineStyle} element - * @param {Record | undefined} prev - * @param {Record} next New attributes - this function mutates this object + * @param {Record | undefined} prev + * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] * @param {boolean} [preserve_attribute_case] * @param {boolean} [is_custom_element] @@ -289,10 +293,8 @@ export function set_attributes( if (next.class) { next.class = clsx(next.class); - } - - if (css_hash !== undefined) { - next.class = next.class ? next.class + ' ' + css_hash : css_hash; + } else if (css_hash || next[CLASS]) { + next.class = null; /* force call to set_class() */ } var setters = get_setters(element); @@ -325,7 +327,7 @@ export function set_attributes( } var prev_value = current[key]; - if (value === prev_value) continue; + if (value === prev_value && key !== 'class') continue; current[key] = value; @@ -375,6 +377,9 @@ export function set_attributes( // @ts-ignore element[`__${event_name}`] = undefined; } + } else if (key === 'class') { + var is_html = element.namespaceURI === 'http://www.w3.org/1999/xhtml'; + set_class(element, is_html, value, css_hash, prev?.[CLASS], next[CLASS]); } else if (key === 'style' && value != null) { element.style.cssText = value + ''; } else if (key === 'autofocus') { diff --git a/packages/svelte/src/internal/client/dom/elements/class.js b/packages/svelte/src/internal/client/dom/elements/class.js index 62ffb6d14b..3308709a24 100644 --- a/packages/svelte/src/internal/client/dom/elements/class.js +++ b/packages/svelte/src/internal/client/dom/elements/class.js @@ -1,120 +1,49 @@ +import { to_class } from '../../../shared/attributes.js'; import { hydrating } from '../hydration.js'; /** - * @param {SVGElement} dom - * @param {string} value - * @param {string} [hash] - * @returns {void} - */ -export function set_svg_class(dom, value, hash) { - // @ts-expect-error need to add __className to patched prototype - var prev_class_name = dom.__className; - var next_class_name = to_class(value, hash); - - if (hydrating && dom.getAttribute('class') === next_class_name) { - // In case of hydration don't reset the class as it's already correct. - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } else if ( - prev_class_name !== next_class_name || - (hydrating && dom.getAttribute('class') !== next_class_name) - ) { - if (next_class_name === '') { - dom.removeAttribute('class'); - } else { - dom.setAttribute('class', next_class_name); - } - - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } -} - -/** - * @param {MathMLElement} dom - * @param {string} value + * @param {Element} dom + * @param {boolean | number} is_html + * @param {string | null} value * @param {string} [hash] - * @returns {void} + * @param {Record} [prev_classes] + * @param {Record} [next_classes] + * @returns {Record | undefined} */ -export function set_mathml_class(dom, value, hash) { +export function set_class(dom, is_html, value, hash, prev_classes, next_classes) { // @ts-expect-error need to add __className to patched prototype - var prev_class_name = dom.__className; - var next_class_name = to_class(value, hash); - - if (hydrating && dom.getAttribute('class') === next_class_name) { - // In case of hydration don't reset the class as it's already correct. - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } else if ( - prev_class_name !== next_class_name || - (hydrating && dom.getAttribute('class') !== next_class_name) - ) { - if (next_class_name === '') { - dom.removeAttribute('class'); - } else { - dom.setAttribute('class', next_class_name); + var prev = dom.__className; + + if (hydrating || prev !== value) { + var next_class_name = to_class(value, hash, next_classes); + + if (!hydrating || next_class_name !== dom.getAttribute('class')) { + // Removing the attribute when the value is only an empty string causes + // performance issues vs simply making the className an empty string. So + // we should only remove the class if the the value is nullish + // and there no hash/directives : + if (next_class_name == null) { + dom.removeAttribute('class'); + } else if (is_html) { + dom.className = next_class_name; + } else { + dom.setAttribute('class', next_class_name); + } } // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } -} + dom.__className = value; + } else if (next_classes) { + prev_classes ??= {}; -/** - * @param {HTMLElement} dom - * @param {string} value - * @param {string} [hash] - * @returns {void} - */ -export function set_class(dom, value, hash) { - // @ts-expect-error need to add __className to patched prototype - var prev_class_name = dom.__className; - var next_class_name = to_class(value, hash); + for (var key in next_classes) { + var is_present = !!next_classes[key]; - if (hydrating && dom.className === next_class_name) { - // In case of hydration don't reset the class as it's already correct. - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; - } else if ( - prev_class_name !== next_class_name || - (hydrating && dom.className !== next_class_name) - ) { - // Removing the attribute when the value is only an empty string causes - // peformance issues vs simply making the className an empty string. So - // we should only remove the class if the the value is nullish. - if (value == null && !hash) { - dom.removeAttribute('class'); - } else { - dom.className = next_class_name; + if (is_present !== !!prev_classes[key]) { + dom.classList.toggle(key, is_present); + } } - - // @ts-expect-error need to add __className to patched prototype - dom.__className = next_class_name; } -} -/** - * @template V - * @param {V} value - * @param {string} [hash] - * @returns {string | V} - */ -function to_class(value, hash) { - return (value == null ? '' : value) + (hash ? ' ' + hash : ''); -} - -/** - * @param {Element} dom - * @param {string} class_name - * @param {boolean} value - * @returns {void} - */ -export function toggle_class(dom, class_name, value) { - if (value) { - if (dom.classList.contains(class_name)) return; - dom.classList.add(class_name); - } else { - if (!dom.classList.contains(class_name)) return; - dom.classList.remove(class_name); - } + return next_classes; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 83565d17ae..f6ac92456e 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -44,7 +44,7 @@ export function init_operations() { // @ts-expect-error element_prototype.__click = undefined; // @ts-expect-error - element_prototype.__className = ''; + element_prototype.__className = undefined; // @ts-expect-error element_prototype.__attributes = null; // @ts-expect-error diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d78f6d452e..431ac8cf24 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -39,9 +39,11 @@ export { set_checked, set_selected, set_default_checked, - set_default_value + set_default_value, + CLASS, + STYLE } from './dom/elements/attributes.js'; -export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js'; +export { set_class } from './dom/elements/class.js'; export { apply, event, delegate, replay_events } from './dom/elements/events.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { set_style } from './dom/elements/style.js'; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index e8ffeed2fe..160a1faa65 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,7 +2,7 @@ /** @import { Component, Payload, RenderOutput } from '#server' */ /** @import { Store } from '#shared' */ export { FILENAME, HMR } from '../../constants.js'; -import { attr, clsx } from '../shared/attributes.js'; +import { attr, clsx, to_class } from '../shared/attributes.js'; import { is_promise, noop } from '../shared/utils.js'; import { subscribe_to_store } from '../../store/utils.js'; import { @@ -10,7 +10,6 @@ import { ELEMENT_PRESERVE_ATTRIBUTE_CASE, ELEMENT_IS_NAMESPACED } from '../../constants.js'; - import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; import { current_component, pop, push } from './context.js'; @@ -198,12 +197,13 @@ export function css_props(payload, is_html, props, component, dynamic = false) { /** * @param {Record} attrs - * @param {Record} [classes] + * @param {string | null} css_hash + * @param {Record} [classes] * @param {Record} [styles] * @param {number} [flags] * @returns {string} */ -export function spread_attributes(attrs, classes, styles, flags = 0) { +export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { if (styles) { attrs.style = attrs.style ? style_object_to_string(merge_styles(/** @type {string} */ (attrs.style), styles)) @@ -214,16 +214,8 @@ export function spread_attributes(attrs, classes, styles, flags = 0) { attrs.class = clsx(attrs.class); } - if (classes) { - const classlist = attrs.class ? [attrs.class] : []; - - for (const key in classes) { - if (classes[key]) { - classlist.push(key); - } - } - - attrs.class = classlist.join(' '); + if (css_hash || classes) { + attrs.class = to_class(attrs.class, css_hash, classes); } let attr_str = ''; @@ -552,7 +544,7 @@ export function props_id(payload) { return uid; } -export { attr, clsx }; +export { attr, clsx, to_class }; export { html } from './blocks/html.js'; diff --git a/packages/svelte/src/internal/shared/attributes.js b/packages/svelte/src/internal/shared/attributes.js index a561501bf4..89cc17e51b 100644 --- a/packages/svelte/src/internal/shared/attributes.js +++ b/packages/svelte/src/internal/shared/attributes.js @@ -40,3 +40,45 @@ export function clsx(value) { return value ?? ''; } } + +const whitespace = [...' \t\n\r\f\u00a0\u000b\ufeff']; + +/** + * @param {any} value + * @param {string | null} [hash] + * @param {Record} [directives] + * @returns {string | null} + */ +export function to_class(value, hash, directives) { + var classname = value == null ? '' : '' + value; + + if (hash) { + classname = classname ? classname + ' ' + hash : hash; + } + + if (directives) { + for (var key in directives) { + if (directives[key]) { + classname = classname ? classname + ' ' + key : key; + } else if (classname.length) { + var len = key.length; + var a = 0; + + while ((a = classname.indexOf(key, a)) >= 0) { + var b = a + len; + + if ( + (a === 0 || whitespace.includes(classname[a - 1])) && + (b === classname.length || whitespace.includes(classname[b])) + ) { + classname = (a === 0 ? '' : classname.substring(0, a)) + classname.substring(b + 1); + } else { + a = b; + } + } + } + } + } + + return classname === '' ? null : classname; +} diff --git a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html index ddb9429bc8..5eecaa9bb2 100644 --- a/packages/svelte/tests/css/samples/undefined-with-scope/expected.html +++ b/packages/svelte/tests/css/samples/undefined-with-scope/expected.html @@ -1 +1 @@ -

Foo

\ No newline at end of file +

Foo

\ No newline at end of file diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js index c8710f9038..cbd0456e13 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-classname-with-style/_config.js @@ -1,17 +1,17 @@ import { ok, test } from '../../test'; export default test({ - html: '
', + html: '
', test({ assert, component, target }) { const div = target.querySelector('div'); ok(div); component.testName = null; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = undefined; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = undefined + ''; assert.equal(div.className, 'undefined svelte-x1o6ra'); @@ -32,10 +32,10 @@ export default test({ assert.equal(div.className, 'true svelte-x1o6ra'); component.testName = {}; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = ''; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = 'testClassName'; assert.equal(div.className, 'testClassName svelte-x1o6ra'); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js index 8d0f411b8f..081fceecf2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-null-func-classname-with-style/_config.js @@ -16,10 +16,10 @@ export default test({ assert.equal(div.className, 'testClassName svelte-x1o6ra'); component.testName = null; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = undefined; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = undefined + ''; assert.equal(div.className, 'undefined svelte-x1o6ra'); @@ -40,9 +40,9 @@ export default test({ assert.equal(div.className, 'true svelte-x1o6ra'); component.testName = {}; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); component.testName = ''; - assert.equal(div.className, ' svelte-x1o6ra'); + assert.equal(div.className, 'svelte-x1o6ra'); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js new file mode 100644 index 0000000000..dd1bc6ac1a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/_config.js @@ -0,0 +1,43 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +// This test counts mutations on hydration +// set_class() should not mutate class on hydration, except if mismatch +export default test({ + mode: ['server', 'hydrate'], + + server_props: { + browser: false + }, + + props: { + browser: true + }, + + html: ` +
+
+ + + +
+ `, + + ssrHtml: ` +
+
+ + + +
+ `, + + async test({ assert, component, instance }) { + flushSync(); + assert.deepEqual(instance.get_and_clear_mutations(), ['MAIN']); + + component.foo = false; + flushSync(); + assert.deepEqual(instance.get_and_clear_mutations(), ['DIV', 'SPAN', 'B', 'I']); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte new file mode 100644 index 0000000000..825362dcaf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive-mutations/main.svelte @@ -0,0 +1,47 @@ + + +
+
+ + + +
+ + diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js b/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js new file mode 100644 index 0000000000..2756b40493 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive/_config.js @@ -0,0 +1,145 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + `, + test({ assert, target, component }) { + component.foo = true; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ ` + ); + + component.bar = false; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ ` + ); + + component.foo = false; + flushSync(); + + assert.htmlEqual( + target.innerHTML, + ` +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte b/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte new file mode 100644 index 0000000000..966c07a78e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/class-directive/main.svelte @@ -0,0 +1,40 @@ + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + From 220c2418d167f0af732521d5ed34abf6d873760f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 07:52:46 -0500 Subject: [PATCH 08/23] Version Packages (#15358) * Version Packages * tweak --------- Co-authored-by: github-actions[bot] Co-authored-by: Rich Harris --- .changeset/healthy-guests-itch.md | 5 ----- .changeset/serious-glasses-kiss.md | 5 ----- .changeset/stale-plums-drop.md | 5 ----- packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 6 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 .changeset/healthy-guests-itch.md delete mode 100644 .changeset/serious-glasses-kiss.md delete mode 100644 .changeset/stale-plums-drop.md diff --git a/.changeset/healthy-guests-itch.md b/.changeset/healthy-guests-itch.md deleted file mode 100644 index 24b33f761d..0000000000 --- a/.changeset/healthy-guests-itch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -Allow `@const` inside `#key` diff --git a/.changeset/serious-glasses-kiss.md b/.changeset/serious-glasses-kiss.md deleted file mode 100644 index 29a6fafeae..0000000000 --- a/.changeset/serious-glasses-kiss.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove unnecessary `?? ''` on some expressions diff --git a/.changeset/stale-plums-drop.md b/.changeset/stale-plums-drop.md deleted file mode 100644 index d39268eb72..0000000000 --- a/.changeset/stale-plums-drop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: correctly override class attributes with class directives diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ab190e1cc2..5ebb088924 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.20.3 + +### Patch Changes + +- fix: allow `@const` inside `#key` ([#15377](https://github.com/sveltejs/svelte/pull/15377)) + +- fix: remove unnecessary `?? ''` on some expressions ([#15287](https://github.com/sveltejs/svelte/pull/15287)) + +- fix: correctly override class attributes with class directives ([#15352](https://github.com/sveltejs/svelte/pull/15352)) + ## 5.20.2 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 579660f9d7..5f16c54330 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.20.2", + "version": "5.20.3", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 0803fae736..12468d227d 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.2'; +export const VERSION = '5.20.3'; export const PUBLIC_VERSION = '5'; From 52d6ed1c7323b5896b37b62d42973c7365ae4b85 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:40:21 -0500 Subject: [PATCH 09/23] remove instance_scope (#15364) --- packages/svelte/src/compiler/phases/2-analyze/index.js | 3 --- packages/svelte/src/compiler/phases/2-analyze/types.d.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index ca9297279e..d7956c00b4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -267,7 +267,6 @@ export function analyze_module(ast, options) { expression: null, function_depth: 0, has_props_rune: false, - instance_scope: /** @type {any} */ (null), options: /** @type {ValidatedCompileOptions} */ (options), parent_element: null, reactive_statement: null, @@ -620,7 +619,6 @@ export function analyze_component(root, source, options) { expression: null, derived_state: [], function_depth: scope.function_depth, - instance_scope: instance.scope, reactive_statement: null, reactive_statements: new Map() }; @@ -684,7 +682,6 @@ export function analyze_component(root, source, options) { parent_element: null, has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', - instance_scope: instance.scope, reactive_statement: null, reactive_statements: analysis.reactive_statements, component_slots: new Set(), diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 70796a0d59..a114288345 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -23,7 +23,6 @@ export interface AnalysisState { function_depth: number; // legacy stuff - instance_scope: Scope; reactive_statement: null | ReactiveStatement; reactive_statements: Map; } From 53d1b17ce9c193c7fb5903eb4693fc0b7b908241 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:40:35 -0500 Subject: [PATCH 10/23] remove some unused exports (#15365) --- packages/svelte/src/internal/client/dom/task.js | 2 +- packages/svelte/src/internal/client/reactivity/deriveds.js | 2 +- packages/svelte/src/internal/client/runtime.js | 6 +++--- packages/svelte/tests/snapshot/_config.js | 3 --- 4 files changed, 5 insertions(+), 8 deletions(-) delete mode 100644 packages/svelte/tests/snapshot/_config.js diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index acb5a5b117..95526b27a7 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,7 +1,7 @@ import { run_all } from '../../shared/utils.js'; // Fallback for when requestIdleCallback is not available -export const request_idle_callback = +const request_idle_callback = typeof requestIdleCallback === 'undefined' ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 59a7ed0f16..795417cc0f 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -116,7 +116,7 @@ function get_derived_parent_effect(derived) { * @param {Derived} derived * @returns {T} */ -export function execute_derived(derived) { +function execute_derived(derived) { var value; var prev_active_effect = active_effect; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 8c98f948f3..a829aa13af 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -46,7 +46,7 @@ const FLUSH_SYNC = 1; // Used for DEV time error handling /** @param {WeakSet} value */ const handled_errors = new WeakSet(); -export let is_throwing_error = false; +let is_throwing_error = false; // Used for controlling the flush of effects. let scheduler_mode = FLUSH_MICROTASK; @@ -117,7 +117,7 @@ export function set_derived_sources(sources) { * and until a new dependency is accessed — we track this via `skipped_deps` * @type {null | Value[]} */ -export let new_deps = null; +let new_deps = null; let skipped_deps = 0; @@ -994,7 +994,7 @@ export function safe_get(signal) { * @template T * @param {() => T} fn */ -export function capture_signals(fn) { +function capture_signals(fn) { var previous_captured_signals = captured_signals; captured_signals = new Set(); diff --git a/packages/svelte/tests/snapshot/_config.js b/packages/svelte/tests/snapshot/_config.js deleted file mode 100644 index f47bee71df..0000000000 --- a/packages/svelte/tests/snapshot/_config.js +++ /dev/null @@ -1,3 +0,0 @@ -import { test } from '../../test'; - -export default test({}); From bfeb9ad448c649bc08952f9e998ecdadf187b5e6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:42:13 -0500 Subject: [PATCH 11/23] chore: create `binding.updated` getter (#15362) --- packages/svelte/src/compiler/phases/2-analyze/index.js | 2 +- .../compiler/phases/2-analyze/visitors/ExportSpecifier.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 7 ++++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d7956c00b4..54d05b46be 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -565,7 +565,7 @@ export function analyze_component(root, source, options) { binding.declaration_kind !== 'import' ) { binding.kind = 'state'; - binding.mutated = binding.updated = true; + binding.mutated = true; } } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js index cfb24970de..2a05ffb926 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ExportSpecifier.js @@ -22,7 +22,7 @@ export function ExportSpecifier(node, context) { }); const binding = context.state.scope.get(local_name); - if (binding) binding.reassigned = binding.updated = true; + if (binding) binding.reassigned = true; } } else { validate_export(node, context.state.scope, local_name); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 41e1c78c05..5d2db43b92 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -60,7 +60,6 @@ export class Binding { is_called = false; mutated = false; reassigned = false; - updated = false; /** * @@ -77,6 +76,10 @@ export class Binding { this.kind = kind; this.declaration_kind = declaration_kind; } + + get updated() { + return this.mutated || this.reassigned; + } } export class Scope { @@ -738,8 +741,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { const binding = left && scope.get(left.name); if (binding !== null && left !== binding.node) { - binding.updated = true; - if (left === expression) { binding.reassigned = true; } else { From 1b882fb1b5409da99261a1bf3a7aca6d32a1f9cf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:45:40 -0500 Subject: [PATCH 12/23] chore: remove reactive_statements from state (#15363) --- packages/svelte/src/compiler/phases/2-analyze/index.js | 7 ++----- packages/svelte/src/compiler/phases/2-analyze/types.d.ts | 2 -- .../compiler/phases/2-analyze/visitors/LabeledStatement.js | 2 +- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 54d05b46be..322293bf6b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -269,8 +269,7 @@ export function analyze_module(ast, options) { has_props_rune: false, options: /** @type {ValidatedCompileOptions} */ (options), parent_element: null, - reactive_statement: null, - reactive_statements: new Map() + reactive_statement: null }, visitors ); @@ -619,8 +618,7 @@ export function analyze_component(root, source, options) { expression: null, derived_state: [], function_depth: scope.function_depth, - reactive_statement: null, - reactive_statements: new Map() + reactive_statement: null }; walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); @@ -683,7 +681,6 @@ export function analyze_component(root, source, options) { has_props_rune: false, ast_type: ast === instance.ast ? 'instance' : ast === template.ast ? 'template' : 'module', reactive_statement: null, - reactive_statements: analysis.reactive_statements, component_slots: new Set(), expression: null, derived_state: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index a114288345..17c8123de1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -1,7 +1,6 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { AST, ExpressionMetadata, ValidatedCompileOptions } from '#compiler'; -import type { LabeledStatement } from 'estree'; export interface AnalysisState { scope: Scope; @@ -24,7 +23,6 @@ export interface AnalysisState { // legacy stuff reactive_statement: null | ReactiveStatement; - reactive_statements: Map; } export type Context = import('zimmerframe').Context< diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js index a63480feaa..514cfae53c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/LabeledStatement.js @@ -64,7 +64,7 @@ export function LabeledStatement(node, context) { } } - context.state.reactive_statements.set(node, reactive_statement); + context.state.analysis.reactive_statements.set(node, reactive_statement); if ( node.body.type === 'ExpressionStatement' && From 7958eb74df364510bed706a72d671e94a330c17e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:46:35 -0500 Subject: [PATCH 13/23] chore: remove unnecessary `binding.is_called` property (#15361) --- .changeset/green-geckos-tickle.md | 5 +++++ .../src/compiler/phases/2-analyze/visitors/Attribute.js | 2 +- .../compiler/phases/2-analyze/visitors/CallExpression.js | 8 -------- .../2-analyze/visitors/TaggedTemplateExpression.js | 9 +-------- packages/svelte/src/compiler/phases/scope.js | 1 - 5 files changed, 7 insertions(+), 18 deletions(-) create mode 100644 .changeset/green-geckos-tickle.md diff --git a/.changeset/green-geckos-tickle.md b/.changeset/green-geckos-tickle.md new file mode 100644 index 0000000000..843b3d1bda --- /dev/null +++ b/.changeset/green-geckos-tickle.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove unnecessary `binding.is_called` property diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 42e4498969..561a004526 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -162,7 +162,7 @@ function get_delegated_event(event_name, handler, context) { return unhoisted; } - if (binding !== null && binding.initial !== null && !binding.updated && !binding.is_called) { + if (binding !== null && binding.initial !== null && !binding.updated) { const binding_type = binding.initial.type; if ( diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index ce520cc980..4d09d9293f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -213,14 +213,6 @@ export function CallExpression(node, context) { break; } - if (node.callee.type === 'Identifier') { - const binding = context.state.scope.get(node.callee.name); - - if (binding !== null) { - binding.is_called = true; - } - } - // `$inspect(foo)` or `$derived(foo) should not trigger the `static-state-reference` warning if (rune === '$inspect' || rune === '$derived') { context.next({ ...context.state, function_depth: context.state.function_depth + 1 }); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js index eacb8a342a..881ee5a85e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js @@ -1,4 +1,4 @@ -/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */ +/** @import { TaggedTemplateExpression } from 'estree' */ /** @import { Context } from '../types' */ import { is_pure } from './shared/utils.js'; @@ -12,12 +12,5 @@ export function TaggedTemplateExpression(node, context) { context.state.expression.has_state = true; } - if (node.tag.type === 'Identifier') { - const binding = context.state.scope.get(node.tag.name); - - if (binding !== null) { - binding.is_called = true; - } - } context.next(); } diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5d2db43b92..f46adf4900 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -57,7 +57,6 @@ export class Binding { */ metadata = null; - is_called = false; mutated = false; reassigned = false; From 51337f22bddcbd2e2d2366f53aa4404f58b71403 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:49:33 -0500 Subject: [PATCH 14/23] chore: simplify flushing (#15348) * this appears to be unnecessary * DRY out * this doesnt appear to do anything useful * simplify * remove unused if block * simplify, make non-recursive * unused * DRY * simplify * tidy up * simplify * changeset * unused * Revert "changeset" This reverts commit 946e00dcf7f773a37ffd8a5746aa7ac36ff9fc03. * make flush_sync non-recursive * fix flushSync types * fix * unused * simplify * tidy up * tidy up * present unnecessary microtasks, avoid flushing if no function provided * simplify --- .changeset/violet-camels-heal.md | 5 + .../reactivity/kairo/kairo_avoidable.js | 4 +- .../reactivity/kairo/kairo_broad.js | 4 +- .../benchmarks/reactivity/kairo/kairo_deep.js | 4 +- .../reactivity/kairo/kairo_diamond.js | 4 +- .../benchmarks/reactivity/kairo/kairo_mux.js | 4 +- .../reactivity/kairo/kairo_repeated.js | 4 +- .../reactivity/kairo/kairo_triangle.js | 4 +- .../reactivity/kairo/kairo_unstable.js | 4 +- .../benchmarks/reactivity/mol_bench.js | 4 +- .../3-transform/client/transform-client.js | 2 +- packages/svelte/src/index-client.js | 12 +- .../src/internal/client/dom/blocks/await.js | 4 +- .../svelte/src/internal/client/dom/task.js | 47 ++-- packages/svelte/src/internal/client/index.js | 2 +- .../src/internal/client/reactivity/effects.js | 8 - .../src/internal/client/reactivity/sources.js | 26 +-- .../svelte/src/internal/client/runtime.js | 204 +++++++----------- packages/svelte/src/legacy/legacy-client.js | 6 +- .../samples/animation-css/_config.js | 2 + .../samples/animation-js-easing/_config.js | 2 + .../samples/animation-js/_config.js | 4 + .../dynamic-element-animation/_config.js | 2 + packages/svelte/tests/store/test.ts | 8 +- packages/svelte/types/index.d.ts | 45 ++-- 25 files changed, 182 insertions(+), 233 deletions(-) create mode 100644 .changeset/violet-camels-heal.md diff --git a/.changeset/violet-camels-heal.md b/.changeset/violet-camels-heal.md new file mode 100644 index 0000000000..31e72fa33d --- /dev/null +++ b/.changeset/violet-camels-heal.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update types and inline docs for flushSync diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js index 6b058cdc3c..9daea6de99 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_avoidable.js @@ -20,12 +20,12 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); assert($.get(computed5) === 6); for (let i = 0; i < 1000; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(computed5) === 6); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js index d1cde5958e..8dc5710c87 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_broad.js @@ -25,12 +25,12 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); counter = 0; for (let i = 0; i < 50; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(last) === i + 50); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js index 149457ede1..8690c85f86 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_deep.js @@ -25,12 +25,12 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); counter = 0; for (let i = 0; i < iter; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(current) === len + i); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js index 958a1bcd78..bf4e07ee89 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_diamond.js @@ -28,13 +28,13 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); assert($.get(sum) === 2 * width); counter = 0; for (let i = 0; i < 500; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(sum) === (i + 1) * width); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js index b645051c09..fc252a27b5 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_mux.js @@ -22,13 +22,13 @@ function setup() { destroy, run() { for (let i = 0; i < 10; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(heads[i], i); }); assert($.get(splited[i]) === i + 1); } for (let i = 0; i < 10; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(heads[i], i * 2); }); assert($.get(splited[i]) === i * 2 + 1); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js index 53b85acd37..3bee06ca0e 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_repeated.js @@ -25,13 +25,13 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); assert($.get(current) === size); counter = 0; for (let i = 0; i < 100; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(current) === i * size); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js index b9e2ad9fa4..11a419a52e 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_triangle.js @@ -38,13 +38,13 @@ function setup() { destroy, run() { const constant = count(width); - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); assert($.get(sum) === constant); counter = 0; for (let i = 0; i < 100; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); assert($.get(sum) === constant - width + i * width); diff --git a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js index 0e783732dc..54eb732cb2 100644 --- a/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js +++ b/benchmarking/benchmarks/reactivity/kairo/kairo_unstable.js @@ -25,13 +25,13 @@ function setup() { return { destroy, run() { - $.flush_sync(() => { + $.flush(() => { $.set(head, 1); }); assert($.get(current) === 40); counter = 0; for (let i = 0; i < 100; i++) { - $.flush_sync(() => { + $.flush(() => { $.set(head, i); }); } diff --git a/benchmarking/benchmarks/reactivity/mol_bench.js b/benchmarking/benchmarks/reactivity/mol_bench.js index c9f492f619..536b078d74 100644 --- a/benchmarking/benchmarks/reactivity/mol_bench.js +++ b/benchmarking/benchmarks/reactivity/mol_bench.js @@ -51,11 +51,11 @@ function setup() { */ run(i) { res.length = 0; - $.flush_sync(() => { + $.flush(() => { $.set(B, 1); $.set(A, 1 + i * 2); }); - $.flush_sync(() => { + $.flush(() => { $.set(A, 2 + i * 2); $.set(B, 2); }); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 2e6307a4b7..cf5ba285cb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -312,7 +312,7 @@ export function client_component(analysis, options) { const setter = b.set(key, [ b.stmt(b.call(b.id(name), b.id('$$value'))), - b.stmt(b.call('$.flush_sync')) + b.stmt(b.call('$.flush')) ]); if (analysis.runes && binding.initial) { diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index ca29d5bfbe..efcf7b727b 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -1,7 +1,7 @@ /** @import { ComponentContext, ComponentContextLegacy } from '#client' */ /** @import { EventDispatcher } from './index.js' */ /** @import { NotFunction } from './internal/types.js' */ -import { flush_sync, untrack } from './internal/client/runtime.js'; +import { untrack } from './internal/client/runtime.js'; import { is_array } from './internal/shared/utils.js'; import { user_effect } from './internal/client/index.js'; import * as e from './internal/client/errors.js'; @@ -206,15 +206,7 @@ function init_update_callbacks(context) { return (l.u ??= { a: [], b: [], m: [] }); } -/** - * Synchronously flushes any pending state changes and those that result from it. - * @param {() => void} [fn] - * @returns {void} - */ -export function flushSync(fn) { - flush_sync(fn); -} - +export { flushSync } from './internal/client/runtime.js'; export { getContext, getAllContexts, hasContext, setContext } from './internal/client/context.js'; export { hydrate, mount, unmount } from './internal/client/render.js'; export { tick, untrack } from './internal/client/runtime.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index c8c7c1c0ea..2e3d229779 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -3,7 +3,7 @@ import { DEV } from 'esm-env'; import { is_promise } from '../../../shared/utils.js'; import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; import { internal_set, mutable_source, source } from '../../reactivity/sources.js'; -import { flush_sync, set_active_effect, set_active_reaction } from '../../runtime.js'; +import { flushSync, set_active_effect, set_active_reaction } from '../../runtime.js'; import { hydrate_next, hydrate_node, hydrating } from '../hydration.js'; import { queue_micro_task } from '../task.js'; import { UNINITIALIZED } from '../../../../constants.js'; @@ -105,7 +105,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { // without this, the DOM does not update until two ticks after the promise // resolves, which is unexpected behaviour (and somewhat irksome to test) - flush_sync(); + flushSync(); } } } diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 95526b27a7..48a2fbe660 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -6,25 +6,21 @@ const request_idle_callback = ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; -let is_micro_task_queued = false; -let is_idle_task_queued = false; - /** @type {Array<() => void>} */ -let current_queued_micro_tasks = []; +let micro_tasks = []; + /** @type {Array<() => void>} */ -let current_queued_idle_tasks = []; +let idle_tasks = []; -function process_micro_tasks() { - is_micro_task_queued = false; - const tasks = current_queued_micro_tasks.slice(); - current_queued_micro_tasks = []; +function run_micro_tasks() { + var tasks = micro_tasks; + micro_tasks = []; run_all(tasks); } -function process_idle_tasks() { - is_idle_task_queued = false; - const tasks = current_queued_idle_tasks.slice(); - current_queued_idle_tasks = []; +function run_idle_tasks() { + var tasks = idle_tasks; + idle_tasks = []; run_all(tasks); } @@ -32,32 +28,33 @@ function process_idle_tasks() { * @param {() => void} fn */ export function queue_micro_task(fn) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; - queueMicrotask(process_micro_tasks); + if (micro_tasks.length === 0) { + queueMicrotask(run_micro_tasks); } - current_queued_micro_tasks.push(fn); + + micro_tasks.push(fn); } /** * @param {() => void} fn */ export function queue_idle_task(fn) { - if (!is_idle_task_queued) { - is_idle_task_queued = true; - request_idle_callback(process_idle_tasks); + if (idle_tasks.length === 0) { + request_idle_callback(run_idle_tasks); } - current_queued_idle_tasks.push(fn); + + idle_tasks.push(fn); } /** * Synchronously run any queued tasks. */ export function flush_tasks() { - if (is_micro_task_queued) { - process_micro_tasks(); + if (micro_tasks.length > 0) { + run_micro_tasks(); } - if (is_idle_task_queued) { - process_idle_tasks(); + + if (idle_tasks.length > 0) { + run_idle_tasks(); } } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 431ac8cf24..31da00dbb4 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -139,7 +139,7 @@ export { get, safe_get, invalidate_inner_signals, - flush_sync, + flushSync as flush, tick, untrack, exclude_from_object, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 9d7b5e9de6..28589ce94d 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -6,15 +6,12 @@ import { update_effect, get, is_destroying_effect, - is_flushing_effect, remove_reactions, schedule_effect, set_active_reaction, set_is_destroying_effect, - set_is_flushing_effect, set_signal_status, untrack, - skip_reaction, untracking } from '../runtime.js'; import { @@ -118,17 +115,12 @@ function create_effect(type, fn, sync, push = true) { } if (sync) { - var previously_flushing_effect = is_flushing_effect; - try { - set_is_flushing_effect(true); update_effect(effect); effect.f |= EFFECT_RAN; } catch (e) { destroy_effect(effect); throw e; - } finally { - set_is_flushing_effect(previously_flushing_effect); } } else if (fn !== null) { schedule_effect(effect); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index ded0ca0584..f6a3fd7e33 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -14,8 +14,6 @@ import { derived_sources, set_derived_sources, check_dirtiness, - set_is_flushing_effect, - is_flushing_effect, untracking } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; @@ -202,22 +200,18 @@ export function internal_set(source, value) { if (DEV && inspect_effects.size > 0) { const inspects = Array.from(inspect_effects); - var previously_flushing_effect = is_flushing_effect; - set_is_flushing_effect(true); - try { - for (const effect of inspects) { - // Mark clean inspect-effects as maybe dirty and then check their dirtiness - // instead of just updating the effects - this way we avoid overfiring. - if ((effect.f & CLEAN) !== 0) { - set_signal_status(effect, MAYBE_DIRTY); - } - if (check_dirtiness(effect)) { - update_effect(effect); - } + + for (const effect of inspects) { + // Mark clean inspect-effects as maybe dirty and then check their dirtiness + // instead of just updating the effects - this way we avoid overfiring. + if ((effect.f & CLEAN) !== 0) { + set_signal_status(effect, MAYBE_DIRTY); + } + if (check_dirtiness(effect)) { + update_effect(effect); } - } finally { - set_is_flushing_effect(previously_flushing_effect); } + inspect_effects.clear(); } } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a829aa13af..fe4104c108 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -9,7 +9,6 @@ import { } from './reactivity/effects.js'; import { EFFECT, - RENDER_EFFECT, DIRTY, MAYBE_DIRTY, CLEAN, @@ -41,28 +40,19 @@ import { } from './context.js'; import { is_firefox } from './dom/operations.js'; -const FLUSH_MICROTASK = 0; -const FLUSH_SYNC = 1; // Used for DEV time error handling /** @param {WeakSet} value */ const handled_errors = new WeakSet(); let is_throwing_error = false; -// Used for controlling the flush of effects. -let scheduler_mode = FLUSH_MICROTASK; -// Used for handling scheduling -let is_micro_task_queued = false; +let is_flushing = false; /** @type {Effect | null} */ let last_scheduled_effect = null; -export let is_flushing_effect = false; -export let is_destroying_effect = false; +let is_updating_effect = false; -/** @param {boolean} value */ -export function set_is_flushing_effect(value) { - is_flushing_effect = value; -} +export let is_destroying_effect = false; /** @param {boolean} value */ export function set_is_destroying_effect(value) { @@ -74,7 +64,6 @@ export function set_is_destroying_effect(value) { /** @type {Effect[]} */ let queued_root_effects = []; -let flush_count = 0; /** @type {Effect[]} Stack of effects, dev only */ let dev_effect_stack = []; // Handle signal reactivity tree dependencies and reactions @@ -410,10 +399,9 @@ export function update_reaction(reaction) { new_deps = /** @type {null | Value[]} */ (null); skipped_deps = 0; untracked_writes = null; - active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; skip_reaction = - (flags & UNOWNED) !== 0 && - (!is_flushing_effect || previous_reaction === null || previous_untracking); + (flags & UNOWNED) !== 0 && (untracking || !is_updating_effect || active_reaction === null); + active_reaction = (flags & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; derived_sources = null; set_component_context(reaction.ctx); @@ -559,8 +547,10 @@ export function update_effect(effect) { var previous_effect = active_effect; var previous_component_context = component_context; + var was_updating_effect = is_updating_effect; active_effect = effect; + is_updating_effect = true; if (DEV) { var previous_component_fn = dev_current_component_function; @@ -602,6 +592,7 @@ export function update_effect(effect) { } catch (error) { handle_error(error, effect, previous_effect, previous_component_context || effect.ctx); } finally { + is_updating_effect = was_updating_effect; active_effect = previous_effect; if (DEV) { @@ -620,69 +611,70 @@ function log_effect_stack() { } function infinite_loop_guard() { - if (flush_count > 1000) { - flush_count = 0; - try { - e.effect_update_depth_exceeded(); - } catch (error) { + try { + e.effect_update_depth_exceeded(); + } catch (error) { + if (DEV) { + // stack is garbage, ignore. Instead add a console.error message. + define_property(error, 'stack', { + value: '' + }); + } + // Try and handle the error so it can be caught at a boundary, that's + // if there's an effect available from when it was last scheduled + if (last_scheduled_effect !== null) { if (DEV) { - // stack is garbage, ignore. Instead add a console.error message. - define_property(error, 'stack', { - value: '' - }); - } - // Try and handle the error so it can be caught at a boundary, that's - // if there's an effect available from when it was last scheduled - if (last_scheduled_effect !== null) { - if (DEV) { - try { - handle_error(error, last_scheduled_effect, null, null); - } catch (e) { - // Only log the effect stack if the error is re-thrown - log_effect_stack(); - throw e; - } - } else { + try { handle_error(error, last_scheduled_effect, null, null); - } - } else { - if (DEV) { + } catch (e) { + // Only log the effect stack if the error is re-thrown log_effect_stack(); + throw e; } - throw error; + } else { + handle_error(error, last_scheduled_effect, null, null); } + } else { + if (DEV) { + log_effect_stack(); + } + throw error; } } - flush_count++; } -/** - * @param {Array} root_effects - * @returns {void} - */ -function flush_queued_root_effects(root_effects) { - var length = root_effects.length; - if (length === 0) { - return; - } - infinite_loop_guard(); - - var previously_flushing_effect = is_flushing_effect; - is_flushing_effect = true; - +function flush_queued_root_effects() { try { - for (var i = 0; i < length; i++) { - var effect = root_effects[i]; + var flush_count = 0; - if ((effect.f & CLEAN) === 0) { - effect.f ^= CLEAN; + while (queued_root_effects.length > 0) { + if (flush_count++ > 1000) { + infinite_loop_guard(); } - var collected_effects = process_effects(effect); - flush_queued_effects(collected_effects); + var root_effects = queued_root_effects; + var length = root_effects.length; + + queued_root_effects = []; + + for (var i = 0; i < length; i++) { + var root = root_effects[i]; + + if ((root.f & CLEAN) === 0) { + root.f ^= CLEAN; + } + + var collected_effects = process_effects(root); + flush_queued_effects(collected_effects); + } } } finally { - is_flushing_effect = previously_flushing_effect; + is_flushing = false; + + last_scheduled_effect = null; + if (DEV) { + dev_effect_stack = []; + } } } @@ -724,39 +716,17 @@ function flush_queued_effects(effects) { } } -function process_deferred() { - is_micro_task_queued = false; - if (flush_count > 1001) { - return; - } - const previous_queued_root_effects = queued_root_effects; - queued_root_effects = []; - flush_queued_root_effects(previous_queued_root_effects); - - if (!is_micro_task_queued) { - flush_count = 0; - last_scheduled_effect = null; - if (DEV) { - dev_effect_stack = []; - } - } -} - /** * @param {Effect} signal * @returns {void} */ export function schedule_effect(signal) { - if (scheduler_mode === FLUSH_MICROTASK) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; - queueMicrotask(process_deferred); - } + if (!is_flushing) { + is_flushing = true; + queueMicrotask(flush_queued_root_effects); } - last_scheduled_effect = signal; - - var effect = signal; + var effect = (last_scheduled_effect = signal); while (effect.parent !== null) { effect = effect.parent; @@ -846,42 +816,30 @@ function process_effects(effect) { } /** - * Internal version of `flushSync` with the option to not flush previous effects. - * Returns the result of the passed function, if given. - * @param {() => any} [fn] - * @returns {any} + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * @template [T=void] + * @param {(() => T) | undefined} [fn] + * @returns {T} */ -export function flush_sync(fn) { - var previous_scheduler_mode = scheduler_mode; - var previous_queued_root_effects = queued_root_effects; - - try { - infinite_loop_guard(); +export function flushSync(fn) { + var result; - scheduler_mode = FLUSH_SYNC; - queued_root_effects = []; - is_micro_task_queued = false; - - flush_queued_root_effects(previous_queued_root_effects); + if (fn) { + is_flushing = true; + flush_queued_root_effects(); + result = fn(); + } - var result = fn?.(); + flush_tasks(); + while (queued_root_effects.length > 0) { + is_flushing = true; + flush_queued_root_effects(); flush_tasks(); - if (queued_root_effects.length > 0) { - flush_sync(); - } - - flush_count = 0; - last_scheduled_effect = null; - if (DEV) { - dev_effect_stack = []; - } - - return result; - } finally { - scheduler_mode = previous_scheduler_mode; - queued_root_effects = previous_queued_root_effects; } + + return /** @type {T} */ (result); } /** @@ -890,9 +848,9 @@ export function flush_sync(fn) { */ export async function tick() { await Promise.resolve(); - // By calling flush_sync we guarantee that any pending state changes are applied after one tick. + // By calling flushSync we guarantee that any pending state changes are applied after one tick. // TODO look into whether we can make flushing subsequent updates synchronously in the future. - flush_sync(); + flushSync(); } /** diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 3a05bc0496..bb9a5a9c03 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -3,7 +3,7 @@ import { DIRTY, LEGACY_PROPS, MAYBE_DIRTY } from '../internal/client/constants.j import { user_pre_effect } from '../internal/client/reactivity/effects.js'; import { mutable_source, set } from '../internal/client/reactivity/sources.js'; import { hydrate, mount, unmount } from '../internal/client/render.js'; -import { active_effect, flush_sync, get, set_signal_status } from '../internal/client/runtime.js'; +import { active_effect, flushSync, get, set_signal_status } from '../internal/client/runtime.js'; import { lifecycle_outside_component } from '../internal/shared/errors.js'; import { define_property, is_array } from '../internal/shared/utils.js'; import * as w from '../internal/client/warnings.js'; @@ -119,9 +119,9 @@ class Svelte4Component { recover: options.recover }); - // We don't flush_sync for custom element wrappers or if the user doesn't want it + // We don't flushSync for custom element wrappers or if the user doesn't want it if (!options?.props?.$$host || options.sync === false) { - flush_sync(); + flushSync(); } this.#events = props.$$events; diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js index b6b601a96b..b6bd818e65 100644 --- a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js @@ -47,6 +47,8 @@ export default test({ { id: 1, name: 'a' } ]; + raf.tick(0); + divs = target.querySelectorAll('div'); assert.ok(divs[0].getAnimations().length > 0); assert.equal(divs[1].getAnimations().length, 0); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js index 5b7ed1c732..f4a3554b29 100644 --- a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js @@ -46,6 +46,8 @@ export default test({ { id: 1, name: 'a' } ]; + raf.tick(0); + divs = document.querySelectorAll('div'); assert.equal(divs[0].dy, 120); assert.equal(divs[4].dy, -120); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js index 3606f7d17b..a2e17b49f8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js @@ -46,6 +46,8 @@ export default test({ { id: 1, name: 'a' } ]; + raf.tick(0); + divs = document.querySelectorAll('div'); assert.equal(divs[0].dy, 120); assert.equal(divs[4].dy, -120); @@ -66,6 +68,8 @@ export default test({ { id: 5, name: 'e' } ]; + raf.tick(100); + divs = document.querySelectorAll('div'); assert.equal(divs[0].dy, 120); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js index 3d127f1375..05c2dc7304 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-animation/_config.js @@ -50,6 +50,8 @@ export default test({ { id: 1, name: 'a' } ]; + raf.tick(0); + divs = target.querySelectorAll('div'); assert.equal(divs[0].style.transform, 'translate(0px, 120px)'); assert.equal(divs[1].style.transform, ''); diff --git a/packages/svelte/tests/store/test.ts b/packages/svelte/tests/store/test.ts index b23ea195d6..77cecca7e5 100644 --- a/packages/svelte/tests/store/test.ts +++ b/packages/svelte/tests/store/test.ts @@ -602,7 +602,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flush_sync(); + $.flushSync(); assert.deepEqual(log, [0, 1]); unsubscribe(); @@ -625,7 +625,7 @@ describe('toStore', () => { assert.deepEqual(log, [0]); set(count, 1); - $.flush_sync(); + $.flushSync(); assert.deepEqual(log, [0, 1]); store.set(2); @@ -654,11 +654,11 @@ describe('fromStore', () => { assert.deepEqual(log, [0]); store.set(1); - $.flush_sync(); + $.flushSync(); assert.deepEqual(log, [0, 1]); count.current = 2; - $.flush_sync(); + $.flushSync(); assert.deepEqual(log, [0, 1, 2]); assert.equal(get(store), 2); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b9ab8a522c..4c47661af8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -408,10 +408,6 @@ declare module 'svelte' { * @deprecated Use [`$effect`](https://svelte.dev/docs/svelte/$effect) instead * */ export function afterUpdate(fn: () => void): void; - /** - * Synchronously flushes any pending state changes and those that result from it. - * */ - export function flushSync(fn?: (() => void) | undefined): void; /** * Create a snippet programmatically * */ @@ -421,6 +417,29 @@ declare module 'svelte' { }): Snippet; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + /** + * Synchronously flush any pending updates. + * Returns void if no callback is provided, otherwise returns the result of calling the callback. + * */ + export function flushSync(fn?: (() => T) | undefined): T; + /** + * Returns a promise that resolves once any pending state changes have been applied. + * */ + export function tick(): Promise; + /** + * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), + * any state read inside `fn` will not be treated as a dependency. + * + * ```ts + * $effect(() => { + * // this will run when `data` changes, but not when `time` changes + * save(data, { + * timestamp: untrack(() => time) + * }); + * }); + * ``` + * */ + export function untrack(fn: () => T): T; /** * Retrieves the context that belongs to the closest parent component with the specified `key`. * Must be called during component initialisation. @@ -494,24 +513,6 @@ declare module 'svelte' { export function unmount(component: Record, options?: { outro?: boolean; } | undefined): Promise; - /** - * Returns a promise that resolves once any pending state changes have been applied. - * */ - export function tick(): Promise; - /** - * When used inside a [`$derived`](https://svelte.dev/docs/svelte/$derived) or [`$effect`](https://svelte.dev/docs/svelte/$effect), - * any state read inside `fn` will not be treated as a dependency. - * - * ```ts - * $effect(() => { - * // this will run when `data` changes, but not when `time` changes - * save(data, { - * timestamp: untrack(() => time) - * }); - * }); - * ``` - * */ - export function untrack(fn: () => T): T; type Getters = { [K in keyof T]: () => T[K]; }; From 5f3b4d54213e65185efac8c48fe07365b56948a8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 08:50:50 -0500 Subject: [PATCH 15/23] chore: DRY out assignment validation (#15360) --- .../phases/2-analyze/visitors/BindDirective.js | 12 ++---------- .../phases/2-analyze/visitors/shared/utils.js | 4 ++-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js index 7719eee677..509fecf301 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/BindDirective.js @@ -5,7 +5,7 @@ import { is_text_attribute, object } from '../../../utils/ast.js'; -import { validate_no_const_assignment } from './shared/utils.js'; +import { validate_assignment } from './shared/utils.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { binding_properties } from '../../bindings.js'; @@ -158,7 +158,7 @@ export function BindDirective(node, context) { return; } - validate_no_const_assignment(node, node.expression, context.state.scope, true); + validate_assignment(node, node.expression, context.state); const assignee = node.expression; const left = object(assignee); @@ -184,14 +184,6 @@ export function BindDirective(node, context) { ) { e.bind_invalid_value(node.expression); } - - if (context.state.analysis.runes && binding?.kind === 'each') { - e.each_item_invalid_assignment(node); - } - - if (binding?.kind === 'snippet') { - e.snippet_parameter_assignment(node); - } } if (node.name === 'group') { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 1507123e13..04f4347a40 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -10,12 +10,12 @@ import * as b from '../../../../utils/builders.js'; import { get_rune } from '../../../scope.js'; /** - * @param {AssignmentExpression | UpdateExpression} node + * @param {AssignmentExpression | UpdateExpression | AST.BindDirective} node * @param {Pattern | Expression} argument * @param {AnalysisState} state */ export function validate_assignment(node, argument, state) { - validate_no_const_assignment(node, argument, state.scope, false); + validate_no_const_assignment(node, argument, state.scope, node.type === 'BindDirective'); if (argument.type === 'Identifier') { const binding = state.scope.get(argument.name); From bbeeed421bc975f81b192657537141d276fc4a67 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2025 08:53:14 -0500 Subject: [PATCH 16/23] Version Packages (#15380) * Version Packages * this changeset was unnecessary --------- Co-authored-by: github-actions[bot] Co-authored-by: Rich Harris --- .changeset/green-geckos-tickle.md | 5 ----- .changeset/violet-camels-heal.md | 5 ----- packages/svelte/CHANGELOG.md | 6 ++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 5 files changed, 8 insertions(+), 12 deletions(-) delete mode 100644 .changeset/green-geckos-tickle.md delete mode 100644 .changeset/violet-camels-heal.md diff --git a/.changeset/green-geckos-tickle.md b/.changeset/green-geckos-tickle.md deleted file mode 100644 index 843b3d1bda..0000000000 --- a/.changeset/green-geckos-tickle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -chore: remove unnecessary `binding.is_called` property diff --git a/.changeset/violet-camels-heal.md b/.changeset/violet-camels-heal.md deleted file mode 100644 index 31e72fa33d..0000000000 --- a/.changeset/violet-camels-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'svelte': patch ---- - -fix: update types and inline docs for flushSync diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5ebb088924..907c5a3534 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # svelte +## 5.20.4 + +### Patch Changes + +- fix: update types and inline docs for flushSync ([#15348](https://github.com/sveltejs/svelte/pull/15348)) + ## 5.20.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 5f16c54330..399d908e7a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -2,7 +2,7 @@ "name": "svelte", "description": "Cybernetically enhanced web apps", "license": "MIT", - "version": "5.20.3", + "version": "5.20.4", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 12468d227d..e893def326 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -4,5 +4,5 @@ * The current version, as set in package.json. * @type {string} */ -export const VERSION = '5.20.3'; +export const VERSION = '5.20.4'; export const PUBLIC_VERSION = '5'; From f9eb2f9f9dc5ab3eba94783458f36d048852ff9d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:01:34 -0500 Subject: [PATCH 17/23] mirror some changes from main --- .../svelte/src/internal/client/dom/task.js | 25 +++++++++---------- .../svelte/src/internal/client/runtime.js | 8 ++---- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 73e88564b3..6e6e4d8d5c 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -1,7 +1,7 @@ import { run_all } from '../../shared/utils.js'; // Fallback for when requestIdleCallback is not available -export const request_idle_callback = +const request_idle_callback = typeof requestIdleCallback === 'undefined' ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; @@ -11,10 +11,12 @@ let is_idle_task_queued = false; /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; + /** @type {Array<() => void>} */ let queued_post_microtasks = []; + /** @type {Array<() => void>} */ -let queued_idle_tasks = []; +let idle_tasks = []; export function flush_boundary_micro_tasks() { const tasks = queued_boundary_microtasks.slice(); @@ -28,13 +30,10 @@ export function flush_post_micro_tasks() { run_all(tasks); } -export function flush_idle_tasks() { - if (is_idle_task_queued) { - is_idle_task_queued = false; - const tasks = queued_idle_tasks.slice(); - queued_idle_tasks = []; - run_all(tasks); - } +export function run_idle_tasks() { + var tasks = idle_tasks; + idle_tasks = []; + run_all(tasks); } function flush_all_micro_tasks() { @@ -71,9 +70,9 @@ export function queue_micro_task(fn) { * @param {() => void} fn */ export function queue_idle_task(fn) { - if (!is_idle_task_queued) { - is_idle_task_queued = true; - request_idle_callback(flush_idle_tasks); + if (idle_tasks.length === 0) { + request_idle_callback(run_idle_tasks); } - queued_idle_tasks.push(fn); + + idle_tasks.push(fn); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index b352d1a75f..5048be3e2d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,11 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { - flush_idle_tasks, - flush_boundary_micro_tasks, - flush_post_micro_tasks -} from './dom/task.js'; +import { flush_boundary_micro_tasks, flush_post_micro_tasks, run_idle_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -937,7 +933,7 @@ export function flush_sync(fn) { flush_boundary_micro_tasks(); flush_post_micro_tasks(); - flush_idle_tasks(); + run_idle_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } From 892dc82aa207873dcf048641268870556e0b6a06 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:03:18 -0500 Subject: [PATCH 18/23] rename --- packages/svelte/src/internal/client/dom/task.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 6e6e4d8d5c..4b5cc59fca 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -7,13 +7,12 @@ const request_idle_callback = : requestIdleCallback; let is_micro_task_queued = false; -let is_idle_task_queued = false; /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; /** @type {Array<() => void>} */ -let queued_post_microtasks = []; +let micro_tasks = []; /** @type {Array<() => void>} */ let idle_tasks = []; @@ -25,8 +24,8 @@ export function flush_boundary_micro_tasks() { } export function flush_post_micro_tasks() { - const tasks = queued_post_microtasks.slice(); - queued_post_microtasks = []; + const tasks = micro_tasks.slice(); + micro_tasks = []; run_all(tasks); } @@ -63,7 +62,7 @@ export function queue_micro_task(fn) { is_micro_task_queued = true; queueMicrotask(flush_all_micro_tasks); } - queued_post_microtasks.push(fn); + micro_tasks.push(fn); } /** From 527deea929dbd96ef28b39dabd3d08edbaf6db4f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:06:55 -0500 Subject: [PATCH 19/23] more --- packages/svelte/src/internal/client/dom/task.js | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 4b5cc59fca..df9346750a 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -36,21 +36,18 @@ export function run_idle_tasks() { } function flush_all_micro_tasks() { - if (is_micro_task_queued) { - is_micro_task_queued = false; - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); - } + flush_boundary_micro_tasks(); + flush_post_micro_tasks(); } /** * @param {() => void} fn */ export function queue_boundary_micro_task(fn) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; + if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(flush_all_micro_tasks); } + queued_boundary_microtasks.push(fn); } @@ -58,10 +55,10 @@ export function queue_boundary_micro_task(fn) { * @param {() => void} fn */ export function queue_micro_task(fn) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; + if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(flush_all_micro_tasks); } + micro_tasks.push(fn); } From 5d9bd7f1ef268df23f8d8f79573f2be68cc6a400 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:07:31 -0500 Subject: [PATCH 20/23] more --- packages/svelte/src/internal/client/dom/task.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index df9346750a..85fb971cef 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -6,8 +6,6 @@ const request_idle_callback = ? (/** @type {() => void} */ cb) => setTimeout(cb, 1) : requestIdleCallback; -let is_micro_task_queued = false; - /** @type {Array<() => void>} */ let queued_boundary_microtasks = []; @@ -35,7 +33,7 @@ export function run_idle_tasks() { run_all(tasks); } -function flush_all_micro_tasks() { +function run_micro_tasks() { flush_boundary_micro_tasks(); flush_post_micro_tasks(); } @@ -45,7 +43,7 @@ function flush_all_micro_tasks() { */ export function queue_boundary_micro_task(fn) { if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { - queueMicrotask(flush_all_micro_tasks); + queueMicrotask(run_micro_tasks); } queued_boundary_microtasks.push(fn); @@ -56,7 +54,7 @@ export function queue_boundary_micro_task(fn) { */ export function queue_micro_task(fn) { if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { - queueMicrotask(flush_all_micro_tasks); + queueMicrotask(run_micro_tasks); } micro_tasks.push(fn); From ed50a6bb3fc0c305a023d74784fbeb72d0339c71 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:09:24 -0500 Subject: [PATCH 21/23] more --- packages/svelte/src/internal/client/dom/task.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 85fb971cef..77ac446ae1 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -7,7 +7,7 @@ const request_idle_callback = : requestIdleCallback; /** @type {Array<() => void>} */ -let queued_boundary_microtasks = []; +let boundary_micro_tasks = []; /** @type {Array<() => void>} */ let micro_tasks = []; @@ -16,13 +16,13 @@ let micro_tasks = []; let idle_tasks = []; export function flush_boundary_micro_tasks() { - const tasks = queued_boundary_microtasks.slice(); - queued_boundary_microtasks = []; + var tasks = boundary_micro_tasks; + boundary_micro_tasks = []; run_all(tasks); } export function flush_post_micro_tasks() { - const tasks = micro_tasks.slice(); + var tasks = micro_tasks; micro_tasks = []; run_all(tasks); } @@ -42,18 +42,18 @@ function run_micro_tasks() { * @param {() => void} fn */ export function queue_boundary_micro_task(fn) { - if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { + if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(run_micro_tasks); } - queued_boundary_microtasks.push(fn); + boundary_micro_tasks.push(fn); } /** * @param {() => void} fn */ export function queue_micro_task(fn) { - if (queued_boundary_microtasks.length === 0 && micro_tasks.length === 0) { + if (boundary_micro_tasks.length === 0 && micro_tasks.length === 0) { queueMicrotask(run_micro_tasks); } From db947906f9844e432e7c6458a68c8052621865ab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:15:03 -0500 Subject: [PATCH 22/23] more --- packages/svelte/src/internal/client/dom/task.js | 10 +++++----- packages/svelte/src/internal/client/runtime.js | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index 77ac446ae1..cec3e9d97e 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -15,13 +15,13 @@ let micro_tasks = []; /** @type {Array<() => void>} */ let idle_tasks = []; -export function flush_boundary_micro_tasks() { +function run_boundary_micro_tasks() { var tasks = boundary_micro_tasks; boundary_micro_tasks = []; run_all(tasks); } -export function flush_post_micro_tasks() { +function run_post_micro_tasks() { var tasks = micro_tasks; micro_tasks = []; run_all(tasks); @@ -33,9 +33,9 @@ export function run_idle_tasks() { run_all(tasks); } -function run_micro_tasks() { - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); +export function run_micro_tasks() { + run_boundary_micro_tasks(); + run_post_micro_tasks(); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 5048be3e2d..3e63bbb9e0 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { flush_boundary_micro_tasks, flush_post_micro_tasks, run_idle_tasks } from './dom/task.js'; +import { run_idle_tasks, run_micro_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -931,9 +931,9 @@ export function flush_sync(fn) { var result = fn?.(); - flush_boundary_micro_tasks(); - flush_post_micro_tasks(); + run_micro_tasks(); run_idle_tasks(); + if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync(); } From cbc227c75ef8b41d9130409788a4e7c823c20b1a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Feb 2025 12:18:56 -0500 Subject: [PATCH 23/23] more --- packages/svelte/src/internal/client/dom/task.js | 17 +++++++++++++++-- packages/svelte/src/internal/client/runtime.js | 5 ++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/task.js b/packages/svelte/src/internal/client/dom/task.js index cec3e9d97e..fc94d59245 100644 --- a/packages/svelte/src/internal/client/dom/task.js +++ b/packages/svelte/src/internal/client/dom/task.js @@ -27,13 +27,13 @@ function run_post_micro_tasks() { run_all(tasks); } -export function run_idle_tasks() { +function run_idle_tasks() { var tasks = idle_tasks; idle_tasks = []; run_all(tasks); } -export function run_micro_tasks() { +function run_micro_tasks() { run_boundary_micro_tasks(); run_post_micro_tasks(); } @@ -70,3 +70,16 @@ export function queue_idle_task(fn) { idle_tasks.push(fn); } + +/** + * Synchronously run any queued tasks. + */ +export function flush_tasks() { + if (boundary_micro_tasks.length > 0 || micro_tasks.length > 0) { + run_micro_tasks(); + } + + if (idle_tasks.length > 0) { + run_idle_tasks(); + } +} diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e63bbb9e0..1dd69d344f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -27,7 +27,7 @@ import { REACTION_IS_UPDATING, EFFECT_ASYNC } from './constants.js'; -import { run_idle_tasks, run_micro_tasks } from './dom/task.js'; +import { flush_tasks } from './dom/task.js'; import { internal_set } from './reactivity/sources.js'; import { destroy_derived_effects, @@ -931,8 +931,7 @@ export function flush_sync(fn) { var result = fn?.(); - run_micro_tasks(); - run_idle_tasks(); + flush_tasks(); if (queued_root_effects.length > 0 || root_effects.length > 0) { flush_sync();