diff --git a/.changeset/fair-laws-appear.md b/.changeset/fair-laws-appear.md new file mode 100644 index 0000000000..9a1149ff27 --- /dev/null +++ b/.changeset/fair-laws-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: match class and style directives against attribute selector diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 64bf49d77a..db99b70770 100644 --- a/documentation/docs/07-misc/02-testing.md +++ b/documentation/docs/07-misc/02-testing.md @@ -129,12 +129,12 @@ test('Effect', () => { // effects normally run after a microtask, // use flushSync to execute all pending effects synchronously flushSync(); - expect(log.value).toEqual([0]); + expect(log).toEqual([0]); count = 1; flushSync(); - expect(log.value).toEqual([0, 1]); + expect(log).toEqual([0, 1]); }); cleanup(); @@ -148,17 +148,13 @@ test('Effect', () => { */ export function logger(getValue) { /** @type {any[]} */ - let log = $state([]); + let log = []; $effect(() => { log.push(getValue()); }); - return { - get value() { - return log; - } - }; + return log; } ``` diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 020942f5fd..618a25c638 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,25 @@ # svelte +## 5.34.5 + +### Patch Changes + +- fix: keep spread non-delegated event handlers up to date ([#16180](https://github.com/sveltejs/svelte/pull/16180)) + +- fix: remove undefined attributes on hydration ([#16178](https://github.com/sveltejs/svelte/pull/16178)) + +- fix: ensure sources within nested effects still register correctly ([#16193](https://github.com/sveltejs/svelte/pull/16193)) + +- fix: avoid shadowing a variable in dynamic components ([#16185](https://github.com/sveltejs/svelte/pull/16185)) + +## 5.34.4 + +### Patch Changes + +- fix: don't set state withing `with_parent` in proxy ([#16176](https://github.com/sveltejs/svelte/pull/16176)) + +- fix: use compiler-driven reactivity in legacy mode template expressions ([#16100](https://github.com/sveltejs/svelte/pull/16100)) + ## 5.34.3 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c2a27b2595..e01691ff63 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.34.3", + "version": "5.34.5", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index fa6e666343..5d77d6a8f4 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -247,7 +247,10 @@ function open(parser) { error: null, pending: null, then: null, - catch: null + catch: null, + metadata: { + expression: create_expression_metadata() + } }); if (parser.eat('then')) { @@ -711,6 +714,9 @@ function special(parser) { declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }], start: start + 2, // start at const, not at @const end: parser.index - 1 + }, + metadata: { + expression: create_expression_metadata() } }); } @@ -737,6 +743,7 @@ function special(parser) { end: parser.index, expression: /** @type {AST.RenderTag['expression']} */ (expression), metadata: { + expression: create_expression_metadata(), dynamic: false, arguments: [], path: [], diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index fbe6ca1cd3..b9a5688a87 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -532,12 +532,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, } case 'ClassSelector': { - if ( - !attribute_matches(element, 'class', name, '~=', false) && - !element.attributes.some( - (attribute) => attribute.type === 'ClassDirective' && attribute.name === name - ) - ) { + if (!attribute_matches(element, 'class', name, '~=', false)) { return false; } @@ -633,6 +628,16 @@ function attribute_matches(node, name, expected_value, operator, case_insensitiv if (attribute.type === 'SpreadAttribute') return true; if (attribute.type === 'BindDirective' && attribute.name === name) return true; + // match attributes against the corresponding directive but bail out on exact matching + if (attribute.type === 'StyleDirective' && name.toLowerCase() === 'style') return true; + if (attribute.type === 'ClassDirective' && name.toLowerCase() === 'class') { + if (operator == '~=') { + if (attribute.name === expected_value) return true; + } else { + return true; + } + } + if (attribute.type !== 'Attribute') continue; if (attribute.name.toLowerCase() !== name.toLowerCase()) continue; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js index 673c79f2df..39358f72fc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AssignmentExpression.js @@ -23,5 +23,9 @@ export function AssignmentExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js index a71f325154..5aa04ba3b9 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/AwaitBlock.js @@ -41,5 +41,8 @@ export function AwaitBlock(node, context) { mark_subtree_dynamic(context.path); - context.next(); + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); + if (node.pending) context.visit(node.pending); + if (node.then) context.visit(node.then); + if (node.catch) context.visit(node.catch); } 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 f723f8447c..d5f5f7b2e0 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/ConstTag.js @@ -32,5 +32,8 @@ export function ConstTag(node, context) { e.const_tag_invalid_placement(node); } - context.next(); + const declaration = node.declaration.declarations[0]; + + context.visit(declaration.id); + context.visit(declaration.init, { ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js index ccb2c17955..7b0e501760 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/HtmlTag.js @@ -15,8 +15,5 @@ export function HtmlTag(node, context) { // unfortunately this is necessary in order to fix invalid HTML mark_subtree_dynamic(context.path); - context.next({ - ...context.state, - expression: node.metadata.expression - }); + context.next({ ...context.state, expression: node.metadata.expression }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index abf70769c0..cced326f9b 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -90,6 +90,7 @@ export function Identifier(node, context) { if (binding) { if (context.state.expression) { context.state.expression.dependencies.add(binding); + context.state.expression.references.add(binding); context.state.expression.has_state ||= binding.kind !== 'static' && !binding.is_function() && diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js index d0dcf8e15c..09e604ea66 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/KeyBlock.js @@ -16,10 +16,6 @@ export function KeyBlock(node, context) { mark_subtree_dynamic(context.path); - context.visit(node.expression, { - ...context.state, - expression: node.metadata.expression - }); - + context.visit(node.expression, { ...context.state, expression: node.metadata.expression }); context.visit(node.fragment); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 245a164c71..0a3b386198 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -15,8 +15,9 @@ export function MemberExpression(node, context) { } } - if (context.state.expression && !is_pure(node, context)) { - context.state.expression.has_state = true; + if (context.state.expression) { + context.state.expression.has_member_expression = true; + context.state.expression.has_state ||= !is_pure(node, context); } if (!is_safe_identifier(node, context.state.scope)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js index a8c9d408bd..1230ef6b04 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/RenderTag.js @@ -54,7 +54,7 @@ export function RenderTag(node, context) { mark_subtree_dynamic(context.path); - context.visit(callee); + context.visit(callee, { ...context.state, expression: node.metadata.expression }); for (const arg of expression.arguments) { const metadata = create_expression_metadata(); diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js index 13f4b9019e..ed48e026ac 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/UpdateExpression.js @@ -21,5 +21,9 @@ export function UpdateExpression(node, context) { } } + if (context.state.expression) { + context.state.expression.has_assignment = true; + } + context.next(); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index c892efd421..1776167850 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -13,6 +13,16 @@ export function visit_function(node, context) { scope: context.state.scope }; + if (context.state.expression) { + for (const [name] of context.state.scope.references) { + const binding = context.state.scope.get(name); + + if (binding && binding.scope.function_depth < context.state.scope.function_depth) { + context.state.expression.references.add(binding); + } + } + } + context.next({ ...context.state, function_depth: context.state.function_depth + 1, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js index 062604cacc..8b1570c7dc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AttachTag.js @@ -1,21 +1,14 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '../../../../utils/builders.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AttachTag} node * @param {ComponentContext} context */ export function AttachTag(node, context) { - context.state.init.push( - b.stmt( - b.call( - '$.attach', - context.state.node, - b.thunk(/** @type {Expression} */ (context.visit(node.expression))) - ) - ) - ); + const expression = build_expression(context, node.expression, node.metadata.expression); + context.state.init.push(b.stmt(b.call('$.attach', context.state.node, b.thunk(expression)))); context.next(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 30e370327f..7873cf3ddb 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -1,10 +1,11 @@ -/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */ +/** @import { BlockStatement, Pattern, Statement } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentClientTransformState, ComponentContext } from '../types' */ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.AwaitBlock} node @@ -14,7 +15,7 @@ export function AwaitBlock(node, context) { context.state.template.push_comment(); // Visit {#await } first to ensure that scopes are in the correct order - const expression = b.thunk(/** @type {Expression} */ (context.visit(node.expression))); + const expression = b.thunk(build_expression(context, node.expression, node.metadata.expression)); let then_block; let catch_block; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js index d58a24b455..9b86557536 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Component.js @@ -8,12 +8,6 @@ import { build_component } from './shared/component.js'; * @param {ComponentContext} context */ export function Component(node, context) { - const component = build_component( - node, - // if it's not dynamic we will just use the node name, if it is dynamic we will use the node name - // only if it's a valid identifier, otherwise we will use a default name - !node.metadata.dynamic || regex_is_valid_identifier.test(node.name) ? node.name : '$$component', - context - ); + const component = build_component(node, node.name, context); context.state.init.push(component); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 2f3c0b3d0e..c1be1e3220 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -1,4 +1,4 @@ -/** @import { Expression, Pattern } from 'estree' */ +/** @import { Pattern } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { dev } from '../../../../state.js'; @@ -6,6 +6,7 @@ import { extract_identifiers } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.ConstTag} node @@ -15,15 +16,8 @@ export function ConstTag(node, context) { const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) if (declaration.id.type === 'Identifier') { - context.state.init.push( - b.const( - declaration.id, - create_derived( - context.state, - b.thunk(/** @type {Expression} */ (context.visit(declaration.init))) - ) - ) - ); + const init = build_expression(context, declaration.init, node.metadata.expression); + context.state.init.push(b.const(declaration.id, create_derived(context.state, b.thunk(init)))); context.state.transform[declaration.id.name] = { read: get_value }; @@ -48,13 +42,15 @@ export function ConstTag(node, context) { // TODO optimise the simple `{ x } = y` case — we can just return `y` // instead of destructuring it only to return a new object + const init = build_expression( + { ...context, state: child_state }, + declaration.init, + node.metadata.expression + ); const fn = b.arrow( [], b.block([ - b.const( - /** @type {Pattern} */ (context.visit(declaration.id, child_state)), - /** @type {Expression} */ (context.visit(declaration.init, child_state)) - ), + b.const(/** @type {Pattern} */ (context.visit(declaration.id, child_state)), init), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index 64967dfc96..f5758893b2 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -1,4 +1,4 @@ -/** @import { BlockStatement, Expression, Identifier, Pattern, SequenceExpression, Statement } from 'estree' */ +/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */ /** @import { AST, Binding } from '#compiler' */ /** @import { ComponentContext } from '../types' */ /** @import { Scope } from '../../../scope' */ @@ -12,8 +12,8 @@ import { import { dev } from '../../../../state.js'; import { extract_paths, object } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; -import { build_getter } from '../utils.js'; import { get_value } from './shared/declarations.js'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.EachBlock} node @@ -24,11 +24,18 @@ export function EachBlock(node, context) { // expression should be evaluated in the parent scope, not the scope // created by the each block itself - const collection = /** @type {Expression} */ ( - context.visit(node.expression, { - ...context.state, - scope: /** @type {Scope} */ (context.state.scope.parent) - }) + const parent_scope_state = { + ...context.state, + scope: /** @type {Scope} */ (context.state.scope.parent) + }; + + const collection = build_expression( + { + ...context, + state: parent_scope_state + }, + node.expression, + node.metadata.expression ); if (!each_node_meta.is_controlled) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js index 590b32885b..64e84ef2ff 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/HtmlTag.js @@ -1,8 +1,8 @@ -/** @import { Expression } from 'estree' */ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import { is_ignored } from '../../../../state.js'; import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.HtmlTag} node @@ -12,8 +12,7 @@ export function HtmlTag(node, context) { context.state.template.push_comment(); const { has_await } = node.metadata.expression; - - const expression = /** @type {Expression} */ (context.visit(node.expression)); + const expression = build_expression(context, node.expression, node.metadata.expression); const html = has_await ? b.call('$.get', b.id('$$html')) : expression; const is_svg = context.state.metadata.namespace === 'svg'; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js index e06802f0d5..4bd0e18932 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/IfBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.IfBlock} node @@ -25,8 +26,7 @@ export function IfBlock(node, context) { } const { has_await } = node.metadata.expression; - - const expression = /** @type {Expression} */ (context.visit(node.test)); + const expression = build_expression(context, node.test, node.metadata.expression); const test = has_await ? b.call('$.get', b.id('$$condition')) : expression; /** @type {Expression[]} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js index 80b5a23227..c5b1d9def3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/KeyBlock.js @@ -2,6 +2,7 @@ /** @import { AST } from '#compiler' */ /** @import { ComponentContext } from '../types' */ import * as b from '#compiler/builders'; +import { build_expression } from './shared/utils.js'; /** * @param {AST.KeyBlock} node @@ -10,7 +11,7 @@ import * as b from '#compiler/builders'; export function KeyBlock(node, context) { context.state.template.push_comment(); - const key = /** @type {Expression} */ (context.visit(node.expression)); + const key = build_expression(context, node.expression, node.metadata.expression); const body = /** @type {Expression} */ (context.visit(node.fragment)); if (node.metadata.expression.has_await) { 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 8024b725ae..81f7229703 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 @@ -340,7 +340,7 @@ export function RegularElement(node, context) { trimmed.some((node) => node.type === 'ExpressionTag'); if (use_text_content) { - const { value } = build_template_chunk(trimmed, context.visit, child_state); + const { value } = build_template_chunk(trimmed, context, child_state); const empty_string = value.type === 'Literal' && value.value === ''; if (!empty_string) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js index 12ec2b432a..e741634c89 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RenderTag.js @@ -4,7 +4,7 @@ import { unwrap_optional } from '../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { create_derived } from '../utils.js'; -import { get_expression_id } from './shared/utils.js'; +import { get_expression_id, build_expression } from './shared/utils.js'; /** * @param {AST.RenderTag} node @@ -28,7 +28,11 @@ export function RenderTag(node, context) { const async_expressions = []; for (let i = 0; i < raw_args.length; i++) { - let expression = /** @type {Expression} */ (context.visit(raw_args[i])); + let expression = build_expression( + context, + /** @type {Expression} */ (raw_args[i]), + node.metadata.arguments[i] + ); const { has_call, has_await } = node.metadata.arguments[i]; if (has_await || has_call) { @@ -50,7 +54,11 @@ export function RenderTag(node, context) { b.var(memo.id, create_derived(context.state, b.thunk(memo.expression))) ); - let snippet_function = /** @type {Expression} */ (context.visit(callee)); + let snippet_function = build_expression( + context, + /** @type {Expression} */ (callee), + node.metadata.expression + ); if (node.metadata.dynamic) { // If we have a chain expression then ensure a nullish snippet function gets turned into an empty one diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js index 7bfdaf1850..e6f4202a01 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/TitleElement.js @@ -10,8 +10,7 @@ import { build_template_chunk } from './shared/utils.js'; export function TitleElement(node, context) { const { has_state, value } = build_template_chunk( /** @type {any} */ (node.fragment.nodes), - context.visit, - context.state + context ); const statement = b.stmt(b.assignment('=', b.id('$.document.title'), value)); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index bdfb71152c..d14a60da67 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -59,6 +59,15 @@ export function build_component(node, component_name, context) { /** @type {ExpressionStatement[]} */ const binding_initializers = []; + const is_component_dynamic = + node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic); + + // The variable name used for the component inside $.component() + const intermediate_name = + node.type === 'Component' && node.metadata.dynamic + ? context.state.scope.generate(node.name) + : '$$component'; + /** * If this component has a slot property, it is a named slot within another component. In this case * the slot scope applies to the component itself, too, and not just its children. @@ -223,7 +232,7 @@ export function build_component(node, component_name, context) { b.call( '$$ownership_validator.binding', b.literal(binding.node.name), - b.id(component_name), + b.id(is_component_dynamic ? intermediate_name : component_name), b.thunk(expression) ) ) @@ -299,7 +308,7 @@ export function build_component(node, component_name, context) { ); } - push_prop(b.prop('get', b.call('$.attachment'), expression, true)); + push_prop(b.prop('init', b.call('$.attachment'), expression, true)); } } @@ -438,8 +447,8 @@ export function build_component(node, component_name, context) { // TODO We can remove this ternary once we remove legacy mode, since in runes mode dynamic components // will be handled separately through the `$.component` function, and then the component name will // always be referenced through just the identifier here. - node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic) - ? component_name + is_component_dynamic + ? intermediate_name : /** @type {Expression} */ (context.visit(b.member_id(component_name))), node_id, props_expression @@ -461,7 +470,7 @@ export function build_component(node, component_name, context) { ) ]; - if (node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic)) { + if (is_component_dynamic) { const prev = fn; fn = (node_id) => { @@ -470,11 +479,11 @@ export function build_component(node, component_name, context) { node_id, b.thunk( /** @type {Expression} */ ( - context.visit(node.type === 'Component' ? b.member_id(node.name) : node.expression) + context.visit(node.type === 'Component' ? b.member_id(component_name) : node.expression) ) ), b.arrow( - [b.id('$$anchor'), b.id(component_name)], + [b.id('$$anchor'), b.id(intermediate_name)], b.block([...binding_initializers, b.stmt(prev(b.id('$$anchor')))]) ) ); 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 6733b6932f..30f11e3ff6 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 @@ -7,7 +7,7 @@ import { is_ignored } from '../../../../../state.js'; import { is_event_attribute } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; import { build_class_directives_object, build_style_directives_object } from '../RegularElement.js'; -import { build_template_chunk, get_expression_id } from './utils.js'; +import { build_expression, build_template_chunk, get_expression_id } from './utils.js'; /** * @param {Array} attributes @@ -125,7 +125,7 @@ export function build_attribute_value(value, context, memoize = (value) => value return { value: b.literal(chunk.data), has_state: false }; } - let expression = /** @type {Expression} */ (context.visit(chunk.expression)); + let expression = build_expression(context, chunk.expression, chunk.metadata.expression); return { value: memoize(expression, chunk.metadata.expression), @@ -133,7 +133,7 @@ export function build_attribute_value(value, context, memoize = (value) => value }; } - return build_template_chunk(value, context.visit, context.state, memoize); + return build_template_chunk(value, context, context.state, memoize); } /** diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js index 7af2c2d4aa..62d07014ee 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/fragment.js @@ -16,8 +16,8 @@ import { build_template_chunk } from './utils.js'; * @param {boolean} is_element * @param {ComponentContext} context */ -export function process_children(nodes, initial, is_element, { visit, state }) { - const within_bound_contenteditable = state.metadata.bound_contenteditable; +export function process_children(nodes, initial, is_element, context) { + const within_bound_contenteditable = context.state.metadata.bound_contenteditable; let prev = initial; let skipped = 0; @@ -48,8 +48,8 @@ export function process_children(nodes, initial, is_element, { visit, state }) { let id = expression; if (id.type !== 'Identifier') { - id = b.id(state.scope.generate(name)); - state.init.push(b.var(id, expression)); + id = b.id(context.state.scope.generate(name)); + context.state.init.push(b.var(id, expression)); } prev = () => id; @@ -64,13 +64,13 @@ export function process_children(nodes, initial, is_element, { visit, state }) { function flush_sequence(sequence) { if (sequence.every((node) => node.type === 'Text')) { skipped += 1; - state.template.push_text(sequence); + context.state.template.push_text(sequence); return; } - state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); + context.state.template.push_text([{ type: 'Text', data: ' ', raw: ' ', start: -1, end: -1 }]); - const { has_state, value } = build_template_chunk(sequence, visit, state); + const { has_state, value } = build_template_chunk(sequence, context); // if this is a standalone `{expression}`, make sure we handle the case where // no text node was created because the expression was empty during SSR @@ -80,9 +80,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) { const update = b.stmt(b.call('$.set_text', id, value)); if (has_state && !within_bound_contenteditable) { - state.update.push(update); + context.state.update.push(update); } else { - state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); + context.state.init.push(b.stmt(b.assignment('=', b.member(id, 'nodeValue'), value))); } } @@ -95,18 +95,18 @@ export function process_children(nodes, initial, is_element, { visit, state }) { sequence = []; } - let child_state = state; + let child_state = context.state; - if (is_static_element(node, state)) { + if (is_static_element(node, context.state)) { skipped += 1; } else if (node.type === 'EachBlock' && nodes.length === 1 && is_element) { node.metadata.is_controlled = true; } else { const id = flush_node(false, node.type === 'RegularElement' ? node.name : 'node'); - child_state = { ...state, node: id }; + child_state = { ...context.state, node: id }; } - visit(node, child_state); + context.visit(node, child_state); } } @@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) { // traverse to the last (n - 1) one when hydrating if (skipped > 1) { skipped -= 1; - state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); + context.state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped)))); } } 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 fa67bfe3e1..bd3820dc6a 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 @@ -1,6 +1,6 @@ -/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression } from 'estree' */ +/** @import { AssignmentExpression, Expression, ExpressionStatement, Identifier, MemberExpression, SequenceExpression, Literal, Super, UpdateExpression, Pattern } from 'estree' */ /** @import { AST, ExpressionMetadata } from '#compiler' */ -/** @import { ComponentClientTransformState, Context, MemoizedExpression } from '../../types' */ +/** @import { ComponentClientTransformState, ComponentContext, Context, MemoizedExpression } from '../../types' */ import { walk } from 'zimmerframe'; import { object } from '../../../../../utils/ast.js'; import * as b from '#compiler/builders'; @@ -8,7 +8,7 @@ import { sanitize_template_string } from '../../../../../utils/sanitize_template import { regex_is_valid_identifier } from '../../../../patterns.js'; import is_reference from 'is-reference'; import { dev, is_ignored, locator } from '../../../../../state.js'; -import { create_derived } from '../../utils.js'; +import { build_getter, create_derived } from '../../utils.js'; /** * @param {ComponentClientTransformState} state @@ -35,15 +35,15 @@ export function get_expression_id(expressions, expression) { /** * @param {Array} values - * @param {(node: AST.SvelteNode, state: any) => any} visit + * @param {ComponentContext} context * @param {ComponentClientTransformState} state * @param {(value: Expression, metadata: ExpressionMetadata) => Expression} memoize * @returns {{ value: Expression, has_state: boolean }} */ export function build_template_chunk( values, - visit, - state, + context, + state = context.state, memoize = (value, metadata) => metadata.has_call || metadata.has_await ? get_expression_id(metadata.has_await ? state.async_expressions : state.expressions, value) @@ -73,7 +73,7 @@ export function build_template_chunk( state.scope.get('undefined') ) { let value = memoize( - /** @type {Expression} */ (visit(node.expression, state)), + build_expression(context, node.expression, node.metadata.expression, state), node.metadata.expression ); @@ -377,3 +377,48 @@ export function validate_mutation(node, context, expression) { loc && b.literal(loc.column) ); } + +/** + * + * @param {ComponentContext} context + * @param {Expression} expression + * @param {ExpressionMetadata} metadata + */ +export function build_expression(context, expression, metadata, state = context.state) { + const value = /** @type {Expression} */ (context.visit(expression, state)); + + if (context.state.analysis.runes) { + return value; + } + + if (!metadata.has_call && !metadata.has_member_expression && !metadata.has_assignment) { + return value; + } + + // Legacy reactivity is coarse-grained, looking at the statically visible dependencies. Replicate that here + const sequence = b.sequence([]); + + for (const binding of metadata.references) { + if (binding.kind === 'normal' && binding.declaration_kind !== 'import') { + continue; + } + + var getter = build_getter({ ...binding.node }, state); + + if ( + binding.kind === 'bindable_prop' || + binding.kind === 'template' || + binding.declaration_kind === 'import' || + binding.node.name === '$$props' || + binding.node.name === '$$restProps' + ) { + getter = b.call('$.deep_read_state', getter); + } + + sequence.expressions.push(getter); + } + + sequence.expressions.push(b.call('$.untrack', b.thunk(value))); + + return sequence; +} diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index faf11f373d..4874554ff0 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -62,8 +62,11 @@ export function create_attribute(name, start, end, value) { export function create_expression_metadata() { return { dependencies: new Set(), + references: new Set(), has_state: false, has_call: false, + has_member_expression: false, + has_assignment: false, has_await: false }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 059e4c8839..c4f41b724a 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -284,14 +284,20 @@ export type DeclarationKind = | 'synthetic'; export interface ExpressionMetadata { - /** All the bindings that are referenced inside this expression */ + /** All the bindings that are referenced eagerly (not inside functions) in this expression */ dependencies: Set; + /** All the bindings that are referenced inside this expression, including inside functions */ + references: Set; /** True if the expression references state directly, or _might_ (via member/call expressions) */ has_state: boolean; /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ has_call: boolean; /** True if the expression contains `await` */ has_await: boolean; + /** True if the expression includes a member expression */ + has_member_expression: boolean; + /** True if the expression includes an assignment or an update */ + has_assignment: boolean; } export interface StateField { diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index f9af185826..e7abb266d0 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -155,6 +155,10 @@ export namespace AST { declaration: VariableDeclaration & { declarations: [VariableDeclarator & { id: Pattern; init: Expression }]; }; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } /** A `{@debug ...}` tag */ @@ -169,6 +173,7 @@ export namespace AST { expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression }); /** @internal */ metadata: { + expression: ExpressionMetadata; dynamic: boolean; arguments: ExpressionMetadata[]; path: SvelteNode[]; @@ -470,6 +475,10 @@ export namespace AST { pending: Fragment | null; then: Fragment | null; catch: Fragment | null; + /** @internal */ + metadata: { + expression: ExpressionMetadata; + }; } export interface KeyBlock extends BaseNode { diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index fcce0b444f..2d3d6a921d 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -345,7 +345,11 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal } var prev_value = current[key]; - if (value === prev_value) continue; + + // Skip if value is unchanged, unless it's `undefined` and the element still has the attribute + if (value === prev_value && !(value === undefined && element.hasAttribute(key))) { + continue; + } current[key] = value; @@ -483,8 +487,8 @@ export function attribute_effect( block(() => { var next = fn(...deriveds.map(get)); - - set_attributes(element, prev, next, css_hash, skip_warning); + /** @type {Record} */ + var current = set_attributes(element, prev, next, css_hash, skip_warning); if (inited && is_select && 'value' in next) { select_option(/** @type {HTMLSelectElement} */ (element), next.value, false); @@ -501,9 +505,11 @@ export function attribute_effect( if (effects[symbol]) destroy_effect(effects[symbol]); effects[symbol] = branch(() => attach(element, () => n)); } + + current[symbol] = n; } - prev = next; + prev = current; }); if (is_select) { diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 4870506699..d9063aee34 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -44,6 +44,7 @@ export function proxy(value) { var reaction = active_reaction; /** + * Executes the proxy in the context of the reaction it was originally created in, if any * @template T * @param {() => T} fn */ @@ -93,21 +94,19 @@ export function proxy(value) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/getOwnPropertyDescriptor#invariants e.state_descriptors_fixed(); } - - with_parent(() => { - var s = sources.get(prop); - - if (s === undefined) { - s = source(descriptor.value, stack); + var s = sources.get(prop); + if (s === undefined) { + s = with_parent(() => { + var s = source(descriptor.value, stack); sources.set(prop, s); - if (DEV && typeof prop === 'string') { tag(s, get_label(path, prop)); } - } else { - set(s, descriptor.value, true); - } - }); + return s; + }); + } else { + set(s, descriptor.value, true); + } return true; }, @@ -268,11 +267,8 @@ export function proxy(value) { // object property before writing to that property. if (s === undefined) { if (!has || get_descriptor(target, prop)?.writable) { - s = with_parent(() => { - var s = source(undefined, stack); - set(s, proxy(value)); - return s; - }); + s = with_parent(() => source(undefined, stack)); + set(s, proxy(value)); sources.set(prop, s); diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index b09d079479..44185e118f 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -143,7 +143,7 @@ export function set(source, value, should_proxy = false) { !untracking && is_runes() && (active_reaction.f & (DERIVED | BLOCK_EFFECT | EFFECT_ASYNC)) !== 0 && - !reaction_sources?.includes(source) + !(reaction_sources?.[1].includes(source) && reaction_sources[0] === active_reaction) ) { e.state_unsafe_mutation(); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 78b38912e2..722a0fa245 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -105,8 +105,8 @@ export function set_active_effect(effect) { /** * When sources are created within a reaction, reading and writing - * them should not cause a re-run - * @type {null | Source[]} + * them within that reaction should not cause a re-run + * @type {null | [active_reaction: Reaction, sources: Source[]]} */ export let reaction_sources = null; @@ -114,9 +114,9 @@ export let reaction_sources = null; export function push_reaction_value(value) { if (active_reaction !== null && active_reaction.f & EFFECT_IS_UPDATING) { if (reaction_sources === null) { - reaction_sources = [value]; + reaction_sources = [active_reaction, [value]]; } else { - reaction_sources.push(value); + reaction_sources[1].push(value); } } } @@ -259,7 +259,7 @@ function schedule_possible_effect_self_invalidation(signal, effect, root = true) for (var i = 0; i < reactions.length; i++) { var reaction = reactions[i]; - if (reaction_sources?.includes(signal)) continue; + if (reaction_sources?.[1].includes(signal) && reaction_sources[0] === active_reaction) continue; if ((reaction.f & DERIVED) !== 0) { schedule_possible_effect_self_invalidation(/** @type {Derived} */ (reaction), effect, false); @@ -774,7 +774,10 @@ export function get(signal) { // we don't add the dependency, because that would create a memory leak var destroyed = active_effect !== null && (active_effect.f & DESTROYED) !== 0; - if (!destroyed && !reaction_sources?.includes(signal)) { + if ( + !destroyed && + (!reaction_sources?.[1].includes(signal) || reaction_sources[0] !== active_reaction) + ) { var deps = active_reaction.deps; if ((active_reaction.f & REACTION_IS_UPDATING) !== 0) { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 01888eaa78..bffca48eec 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.34.3'; +export const VERSION = '5.34.5'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css new file mode 100644 index 0000000000..4b5e4bfd09 --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/expected.css @@ -0,0 +1,2 @@ + span[class].svelte-xyz { color: green } + div[style].svelte-xyz { color: green } \ No newline at end of file diff --git a/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte new file mode 100644 index 0000000000..2f9ab202ca --- /dev/null +++ b/packages/svelte/tests/css/samples/attribute-selector-matches-derictive/input.svelte @@ -0,0 +1,7 @@ + +
+ + diff --git a/packages/svelte/tests/css/samples/class-directive/_config.js b/packages/svelte/tests/css/samples/class-directive/_config.js new file mode 100644 index 0000000000..28e9fbc815 --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../test'; + +export default test({ + warnings: [ + { + code: 'css_unused_selector', + message: 'Unused CSS selector ".third"\nhttps://svelte.dev/e/css_unused_selector', + start: { + line: 6, + column: 2, + character: 115 + }, + end: { + line: 6, + column: 8, + character: 121 + } + } + ] +}); diff --git a/packages/svelte/tests/css/samples/class-directive/expected.css b/packages/svelte/tests/css/samples/class-directive/expected.css new file mode 100644 index 0000000000..1d7d3d4dee --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/expected.css @@ -0,0 +1,3 @@ + .first.svelte-xyz { color: green } + .second.svelte-xyz { color: green } + /* (unused) .third { color: red }*/ diff --git a/packages/svelte/tests/css/samples/class-directive/input.svelte b/packages/svelte/tests/css/samples/class-directive/input.svelte new file mode 100644 index 0000000000..cf00335964 --- /dev/null +++ b/packages/svelte/tests/css/samples/class-directive/input.svelte @@ -0,0 +1,7 @@ +
+ + \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js new file mode 100644 index 0000000000..bc74f23aac --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + server_props: { + browser: false + }, + + props: { + browser: true + } +}); diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html new file mode 100644 index 0000000000..cc789c8f51 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/_expected.html @@ -0,0 +1 @@ +
diff --git a/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte new file mode 100644 index 0000000000..1a587eeeeb --- /dev/null +++ b/packages/svelte/tests/hydration/samples/removes-undefined-attributes/main.svelte @@ -0,0 +1,9 @@ + + +
diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js new file mode 100644 index 0000000000..15adef2c9b --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
[0,0,0,0,0,0,0,0,0]`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte new file mode 100644 index 0000000000..67190669ed --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-assign/main.svelte @@ -0,0 +1,45 @@ + + +{#if a = 0}{/if} + +{#each [b = 0] as x}{x,''}{/each} + +{#key c = 0}{/key} + +{#await d = 0}{/await} + +{#snippet snip()}{/snippet} + +{@render (e = 0, snip)()} + +{@html f = 0, ''} + +
+ +{#key 1} + {@const x = (h = 0)} + {x, ''} +{/key} + +{#if 1} + {@const x = (i = 0)} + {x, ''} +{/if} + + +[{a},{b},{c},{d},{e},{f},{g},{h},{i}] + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js new file mode 100644 index 0000000000..523dcd625d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
12 - 12`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
13 - 12`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte new file mode 100644 index 0000000000..37838f091f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-fn-call/main.svelte @@ -0,0 +1,36 @@ + + +{#if fn(false)}{:else if fn(true)}{/if} + +{#each fn([]) as x}{x, ''}{/each} + +{#key fn(1)}{/key} + +{#await fn(Promise.resolve())}{/await} + +{#snippet snip()}{/snippet} + +{@render fn(snip)()} + +{@html fn('')} + +
{})}>
+ +{#key 1} + {@const x = fn('')} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js new file mode 100644 index 0000000000..0e1a5a8150 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/_config.js @@ -0,0 +1,12 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `
10 - 10`); + flushSync(() => button?.click()); + assert.htmlEqual(target.innerHTML, `
11 - 10`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte new file mode 100644 index 0000000000..4041be4f6f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/block-expression-member-access/main.svelte @@ -0,0 +1,46 @@ + + +{#if obj.false}{:else if obj.true}{/if} + +{#each obj.array as x}{x, ''}{/each} + +{#key obj.string}{/key} + +{#await obj.promise}{/await} + +{#snippet snip()}{/snippet} + +{@render obj.snippet()} + +{@html obj.string} + +
+ +{#key 1} + {@const x = obj.string} + {x} +{/key} + + +{count1} - {count2} + + diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte index b2e6cd046c..4127e857d5 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/Item.svelte @@ -5,7 +5,7 @@ export let index; export let n; - function logRender () { + function logRender (n) { order.push(`${index}: render ${n}`); return index; } @@ -24,5 +24,5 @@
  • - {logRender()} + {logRender(n)}
  • diff --git a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte index b05b1476fd..51dee3bc0c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/lifecycle-render-order-for-children/main.svelte @@ -5,7 +5,7 @@ export let n = 0; - function logRender () { + function logRender (n) { order.push(`parent: render ${n}`); return 'parent'; } @@ -23,7 +23,7 @@ }) -{logRender()} +{logRender(n)}
      {#each [1,2,3] as index} diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js new file mode 100644 index 0000000000..cbac36fee8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/_config.js @@ -0,0 +1,52 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + /** + * Ensure that sorting an array inside an $effect works correctly + * and re-runs when the array changes (e.g., when items are added). + */ + test({ assert, target }) { + const button = target.querySelector('button'); + + // initial render — array should be sorted + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      50

      +

      100

      + ` + ); + + // add first item (20); effect should re-run and sort the array + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      20

      +

      50

      +

      100

      + ` + ); + + // add second item (80); effect should re-run and sort the array + flushSync(() => button?.click()); + + assert.htmlEqual( + target.innerHTML, + ` + +

      0

      +

      20

      +

      50

      +

      80

      +

      100

      + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte new file mode 100644 index 0000000000..c529f67cf4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/array-sort-in-effect/main.svelte @@ -0,0 +1,21 @@ + + + +{#each arr as x} +

      {x}

      +{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte new file mode 100644 index 0000000000..d37c929273 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/A.svelte @@ -0,0 +1,5 @@ + + +{@render children()} diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js new file mode 100644 index 0000000000..cd1fa2b1b9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, 'test'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte new file mode 100644 index 0000000000..d0646b319b --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-component-nested/main.svelte @@ -0,0 +1,9 @@ + + + + test + diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js new file mode 100644 index 0000000000..af03eed4c9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/_config.js @@ -0,0 +1,18 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [change, increment] = target.querySelectorAll('button'); + + increment.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + + change.click(); + flushSync(); + increment.click(); + flushSync(); + assert.htmlEqual(target.innerHTML, ''); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte new file mode 100644 index 0000000000..32d4b242cc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-spread-update/main.svelte @@ -0,0 +1,27 @@ + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js new file mode 100644 index 0000000000..2e4a27cf09 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + async test() {} +}); diff --git a/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte new file mode 100644 index 0000000000..7450eff3fa --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/proxy-set-with-parent/main.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8421ae4a7c..78d7919e0f 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1021,6 +1021,41 @@ describe('signals', () => { }; }); + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + + user_effect(() => { + const raw = state(0); + const proxied = proxy({ current: 0 }); + + // We need those separate, else one working and rerunning the effect + // could mask the other one not rerunning + user_effect(() => { + logs.push($.get(raw)); + }); + + user_effect(() => { + logs.push(proxied.current); + }); + + // Important so that the updating effect is not running + // together with the reading effects + flushSync(); + + user_effect(() => { + $.untrack(() => { + set(raw, $.get(raw) + 1); + proxied.current += 1; + }); + }); + }); + + return () => { + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 1]); + }; + }); + test('proxy version state does not trigger self-dependency guard', () => { return () => { const s = proxy({ a: { b: 1 } }); diff --git a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js index a351851875..da6fdf44d8 100644 --- a/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js @@ -8,11 +8,13 @@ export default function Purity($$anchor) { var fragment = root(); var p = $.first_child(fragment); - p.textContent = '0'; + p.textContent = ( + $.untrack(() => Math.max(0, Math.min(0, 100))) + ); var p_1 = $.sibling(p, 2); - p_1.textContent = location.href; + p_1.textContent = ($.untrack(() => location.href)); var node = $.sibling(p_1, 2);