From c582b5d3b559283619384dd78b77526ce1d6f722 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 20 Feb 2025 16:47:52 -0500 Subject: [PATCH 01/17] 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 8a9ca9065b47..8c98f948f3f0 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/17] docs: rephrase copyright holder (#15333) --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index abbace7bfe03..f872adf738de 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/17] docs: update casing (#15321) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7d2f718da75c..7ea71647521a 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/17] 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 000000000000..29a6fafeae41 --- /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 018bdacc5ecd..d4502210bdfd 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 ca5094f455c6..c25ef3ab50e3 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/17] 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 51e9eb088dc9..41e1c78c057b 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 b80b717e426c..eec41bad9d25 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 77d78477ee93..b9ab8a522ccb 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/17] 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 000000000000..24b33f761d62 --- /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 3f5e0473c5fd..f723f8447cd2 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 000000000000..fe51488c7066 --- /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 000000000000..008072bc4756 --- /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/17] 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 000000000000..d39268eb729b --- /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 93c438c8529f..ca9297279ee2 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 d4502210bdfd..434b49caa1e6 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 e27528365518..7149f8d0e4af 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 abffad0ff7a4..fc5fd2cc4c53 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 d0d800d3cbc5..57101af4b823 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 2dba2d797a4a..151024e85c2e 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 62ffb6d14b5c..3308709a247f 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 83565d17ae68..f6ac92456e78 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 d78f6d452e84..431ac8cf2492 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 e8ffeed2fef5..160a1faa653e 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 a561501bf4f6..89cc17e51b9d 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 ddb9429bc891..5eecaa9bb256 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 c8710f9038b9..cbd0456e1335 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 8d0f411b8fd2..081fceecf279 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 000000000000..dd1bc6ac1a79 --- /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 000000000000..825362dcaf82 --- /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 000000000000..2756b40493e2 --- /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 000000000000..966c07a78e8d --- /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/17] 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 24b33f761d62..000000000000 --- 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 29a6fafeae41..000000000000 --- 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 d39268eb729b..000000000000 --- 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 ab190e1cc2af..5ebb088924b2 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 579660f9d771..5f16c5433012 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 0803fae7363f..12468d227d1a 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/17] 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 ca9297279ee2..d7956c00b435 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 70796a0d59b5..a11428834579 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/17] 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 acb5a5b117f0..95526b27a769 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 59a7ed0f16d6..795417cc0fdb 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 8c98f948f3f0..a829aa13afdd 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 f47bee71df87..000000000000 --- 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/17] 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 d7956c00b435..54d05b46bec9 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 cfb24970de21..2a05ffb92605 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 41e1c78c057b..5d2db43b9257 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/17] 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 54d05b46bec9..322293bf6b91 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 a11428834579..17c8123de1fe 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 a63480feaabc..514cfae53ce4 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/17] 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 000000000000..843b3d1bda71 --- /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 42e449896928..561a00452684 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 ce520cc98055..4d09d9293fb2 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 eacb8a342ac2..881ee5a85edd 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 5d2db43b9257..f46adf49006e 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/17] 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 000000000000..31e72fa33d53 --- /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 6b058cdc3c1d..9daea6de99cb 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 d1cde5958edf..8dc5710c87db 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 149457ede156..8690c85f864a 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 958a1bcd7890..bf4e07ee8962 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 b645051c09bf..fc252a27b5f8 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 53b85acd3766..3bee06ca0e8f 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 b9e2ad9fa4a3..11a419a52e7b 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 0e783732dc67..54eb732cb29d 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 c9f492f61967..536b078d74a4 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 2e6307a4b7a6..cf5ba285cbf3 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 ca29d5bfbe3c..efcf7b727b8d 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 c8c7c1c0ea77..2e3d22977914 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 95526b27a769..48a2fbe660eb 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 431ac8cf2492..31da00dbb448 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 9d7b5e9de624..28589ce94df1 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 ded0ca05846b..f6a3fd7e330a 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 a829aa13afdd..fe4104c1081d 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 3a05bc04963f..bb9a5a9c039b 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 b6b601a96b1d..b6bd818e65db 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 5b7ed1c73209..f4a3554b29f9 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 3606f7d17b77..a2e17b49f869 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 3d127f1375e7..05c2dc73048a 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 b23ea195d6c9..77cecca7e525 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 b9ab8a522ccb..4c47661af897 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/17] 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 7719eee6772e..509fecf301cc 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 1507123e1342..04f4347a40bb 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/17] 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 843b3d1bda71..000000000000 --- 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 31e72fa33d53..000000000000 --- 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 5ebb088924b2..907c5a353474 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 5f16c5433012..399d908e7ab4 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 12468d227d1a..e893def32688 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 c2ec0d9ac29441ca387d74f0814dd27becf64369 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 24 Feb 2025 23:38:01 +0100 Subject: [PATCH 17/17] fix: allow for duplicate `var` declarations (#15382) --- .changeset/spotty-drinks-tan.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 8 ++++++-- .../validator/samples/multiple-var-same-name/errors.json | 1 + .../validator/samples/multiple-var-same-name/input.svelte | 6 ++++++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 .changeset/spotty-drinks-tan.md create mode 100644 packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json create mode 100644 packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte diff --git a/.changeset/spotty-drinks-tan.md b/.changeset/spotty-drinks-tan.md new file mode 100644 index 000000000000..2150c8cffd72 --- /dev/null +++ b/.changeset/spotty-drinks-tan.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow for duplicate `var` declarations diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index f46adf49006e..7d9f90982afb 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -161,8 +161,12 @@ export class Scope { } if (this.declarations.has(node.name)) { - // This also errors on var/function types, but that's arguably a good thing - e.declaration_duplicate(node, node.name); + const binding = this.declarations.get(node.name); + if (binding && binding.declaration_kind !== 'var' && declaration_kind !== 'var') { + // This also errors on function types, but that's arguably a good thing + // declaring function twice is also caught by acorn in the parse phase + e.declaration_duplicate(node, node.name); + } } const binding = new Binding(this, node, kind, declaration_kind, initial); diff --git a/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/errors.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte new file mode 100644 index 000000000000..19a8ef7722ae --- /dev/null +++ b/packages/svelte/tests/validator/samples/multiple-var-same-name/input.svelte @@ -0,0 +1,6 @@ + + +{test} \ No newline at end of file