diff --git a/.changeset/fair-laws-appear.md b/.changeset/fair-laws-appear.md new file mode 100644 index 000000000000..9a1149ff279d --- /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/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0e1d3676041..046ad335f3ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,23 @@ jobs: - run: pnpm test env: CI: true + TestNoAsync: + permissions: {} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm playwright install chromium + - run: pnpm test runtime-runes + env: + CI: true + SVELTE_NO_ASYNC: true Lint: permissions: {} runs-on: ubuntu-latest diff --git a/documentation/docs/07-misc/02-testing.md b/documentation/docs/07-misc/02-testing.md index 64bf49d77a27..db99b7077022 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 020942f5fd8a..618a25c63827 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 c2a27b2595ec..e01691ff6317 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 fa6e66634398..5d77d6a8f4b6 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 fbe6ca1cd379..b9a5688a87d0 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 673c79f2df0f..39358f72fc1b 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 a71f325154ff..5aa04ba3b9a8 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 f723f8447cd2..d5f5f7b2e0a0 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 ccb2c17955d8..7b0e501760f0 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 abf70769c013..cced326f9baa 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 d0dcf8e15c51..09e604ea66be 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 245a164c71fb..0a3b3861986c 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 a8c9d408bdad..1230ef6b048c 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 13f4b9019e8b..ed48e026ac65 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 c892efd421d1..177616785026 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 062604cacc16..8b1570c7dc3c 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 30e370327fa1..7873cf3ddbd7 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 d58a24b45559..9b86557536d0 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 2f3c0b3d0ed1..c1be1e3220b0 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 64967dfc96a9..f5758893b2d5 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 590b32885b49..64e84ef2ffc6 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 e06802f0d547..4bd0e1893244 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 80b5a232271e..c5b1d9def3a3 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 8024b725ae92..81f7229703ed 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 12ec2b432a21..e741634c8986 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 7bfdaf1850d2..e6f4202a0189 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 bdfb71152c70..d14a60da672b 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 6733b6932f6c..30f11e3ff62b 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 7af2c2d4aaa8..62d07014eea4 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 fa67bfe3e151..bd3820dc6a9d 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 faf11f373d4c..4874554ff0fb 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 059e4c8839da..c4f41b724ac2 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 f9af18582673..e7abb266d002 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 fcce0b444f49..2d3d6a921dc1 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 487050669933..d9063aee3436 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 b09d079479ab..44185e118f69 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 78b38912e2ba..3b6886467d27 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -57,7 +57,6 @@ import { import * as w from './warnings.js'; import { current_batch, Batch, batch_deriveds } from './reactivity/batch.js'; import { handle_error, invoke_error_boundary } from './error-handling.js'; -import { snapshot } from '../shared/clone.js'; /** @type {Effect | null} */ let last_scheduled_effect = null; @@ -105,8 +104,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 +113,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 +258,12 @@ 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 ( + !async_mode_flag && + 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); @@ -299,7 +303,9 @@ export function update_reaction(reaction) { untracking = false; read_version++; - reaction.f |= EFFECT_IS_UPDATING; + if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { + reaction.f |= EFFECT_IS_UPDATING; + } if (reaction.ac !== null) { reaction.ac?.abort(STALE_REACTION); @@ -383,7 +389,9 @@ export function update_reaction(reaction) { set_component_context(previous_component_context); untracking = previous_untracking; - reaction.f ^= EFFECT_IS_UPDATING; + if (!async_mode_flag || (reaction.f & DERIVED) !== 0) { + reaction.f ^= EFFECT_IS_UPDATING; + } } } @@ -774,7 +782,12 @@ 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 && + ((async_mode_flag && (active_reaction.f & DERIVED) === 0) || + !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/internal/flags/index.js b/packages/svelte/src/internal/flags/index.js index 6920f6b8eeda..ce7bba604bff 100644 --- a/packages/svelte/src/internal/flags/index.js +++ b/packages/svelte/src/internal/flags/index.js @@ -6,6 +6,11 @@ export function enable_async_mode_flag() { async_mode_flag = true; } +/** ONLY USE THIS DURING TESTING */ +export function disable_async_mode_flag() { + async_mode_flag = false; +} + export function enable_legacy_mode_flag() { legacy_mode_flag = true; } diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 01888eaa7853..bffca48eec71 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 000000000000..4b5e4bfd091f --- /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 000000000000..2f9ab202ca80 --- /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 000000000000..28e9fbc81512 --- /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 000000000000..1d7d3d4dee61 --- /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 000000000000..cf0033596415 --- /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/helpers.js b/packages/svelte/tests/helpers.js index 591851e69237..410838829e3a 100644 --- a/packages/svelte/tests/helpers.js +++ b/packages/svelte/tests/helpers.js @@ -194,6 +194,8 @@ if (typeof window !== 'undefined') { export const fragments = /** @type {'html' | 'tree'} */ (process.env.FRAGMENTS) ?? 'html'; +export const async_mode = process.env.SVELTE_NO_ASYNC !== 'true'; + /** * @param {any[]} logs */ 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 000000000000..bc74f23aac60 --- /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 000000000000..cc789c8f5142 --- /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 000000000000..1a587eeeebc0 --- /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 000000000000..15adef2c9be7 --- /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 000000000000..67190669ed98 --- /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 000000000000..523dcd625dce --- /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 000000000000..37838f091fdf --- /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 000000000000..0e1a5a81502f --- /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 000000000000..4041be4f6fda --- /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 b2e6cd046c8e..4127e857d5d5 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 b05b1476fdcc..51dee3bc0c25 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-legacy/shared.ts b/packages/svelte/tests/runtime-legacy/shared.ts index 23759d025af1..7f3673f867bd 100644 --- a/packages/svelte/tests/runtime-legacy/shared.ts +++ b/packages/svelte/tests/runtime-legacy/shared.ts @@ -3,10 +3,10 @@ import { setImmediate } from 'node:timers/promises'; import { globSync } from 'tinyglobby'; import { createClassComponent } from 'svelte/legacy'; import { proxy } from 'svelte/internal/client'; -import { flushSync, hydrate, mount, unmount, untrack } from 'svelte'; +import { flushSync, hydrate, mount, unmount } from 'svelte'; import { render } from 'svelte/server'; import { afterAll, assert, beforeAll } from 'vitest'; -import { compile_directory, fragments } from '../helpers.js'; +import { async_mode, compile_directory, fragments } from '../helpers.js'; import { assert_html_equal, assert_html_equal_with_options } from '../html_equal.js'; import { raf } from '../animation-helpers.js'; import type { CompileOptions } from '#compiler'; @@ -45,6 +45,10 @@ export interface RuntimeTest = Record; /** Temporarily skip specific modes, without skipping the entire test */ skip_mode?: Array<'server' | 'client' | 'hydrate'>; + /** Skip if running with process.env.NO_ASYNC */ + skip_no_async?: boolean; + /** Skip if running without process.env.NO_ASYNC */ + skip_async?: boolean; html?: string; ssrHtml?: string; compileOptions?: Partial; @@ -121,7 +125,15 @@ let console_error = console.error; export function runtime_suite(runes: boolean) { return suite_with_variants( ['dom', 'hydrate', 'ssr'], - (variant, config) => { + (variant, config, test_name) => { + if (!async_mode && (config.skip_no_async || test_name.startsWith('async-'))) { + return true; + } + + if (async_mode && config.skip_async) { + return true; + } + if (variant === 'hydrate') { if (config.mode && !config.mode.includes('hydrate')) return 'no-test'; if (config.skip_mode?.includes('hydrate')) return true; @@ -169,7 +181,7 @@ async function common_setup(cwd: string, runes: boolean | undefined, config: Run dev: force_hmr ? true : undefined, hmr: force_hmr ? true : undefined, experimental: { - async: runes + async: runes && async_mode }, fragments, ...config.compileOptions, 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 000000000000..cbac36fee8ef --- /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 000000000000..c529f67cf4e5 --- /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 000000000000..d37c929273be --- /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 000000000000..cd1fa2b1b9a1 --- /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 000000000000..d0646b319b40 --- /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/effect-cleanup/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js index e55733c14810..53e938d63f40 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/_config.js @@ -1,3 +1,4 @@ +import { async_mode } from '../../../helpers'; import { test } from '../../test'; import { flushSync } from 'svelte'; @@ -10,6 +11,12 @@ export default test({ flushSync(() => { b1.click(); }); - assert.deepEqual(logs, ['init 0']); + + // With async mode (which is on by default for runtime-runes) this works as expected, without it + // it works differently: https://github.com/sveltejs/svelte/pull/15564 + assert.deepEqual( + logs, + async_mode ? ['init 0', 'cleanup 2', null, 'init 2', 'cleanup 4', null, 'init 4'] : ['init 0'] + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte index 2cdcfdfb58f2..da38374f8232 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-cleanup/main.svelte @@ -14,4 +14,4 @@ }) - + 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 000000000000..af03eed4c9e5 --- /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 000000000000..32d4b242cc55 --- /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 000000000000..2e4a27cf0912 --- /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 000000000000..7450eff3faa2 --- /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/runtime-runes/samples/set-context-after-await/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js index 0f0edc208b87..1bf7e71176d4 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-await/_config.js @@ -1,6 +1,7 @@ import { test } from '../../test'; export default test({ + skip_no_async: true, async test({ assert, logs }) { await Promise.resolve(); await Promise.resolve(); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js index cc7c483667cd..4569f42a7379 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/_config.js @@ -1,11 +1,16 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; +import { async_mode } from '../../../helpers'; export default test({ async test({ target, assert, logs }) { const button = target.querySelector('button'); flushSync(() => button?.click()); - assert.ok(logs[0].startsWith('set_context_after_init')); + assert.ok( + async_mode + ? logs[0].startsWith('set_context_after_init') + : logs[0] === 'works without experimental async but really shouldnt' + ); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte index 40145c28daa8..0c3b6c3a0fba 100644 --- a/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/set-context-after-mount/main.svelte @@ -7,6 +7,7 @@ if (condition) { try { setContext('potato', {}); + console.log('works without experimental async but really shouldnt') } catch (e) { console.log(e.message); } diff --git a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js index 18062b86fb43..b728c3c0bead 100644 --- a/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/untrack-own-deriveds/_config.js @@ -2,6 +2,8 @@ import { flushSync } from 'svelte'; import { test } from '../../test'; export default test({ + // In async mode we _do_ want to run effects that react to their own state changing + skip_async: true, test({ assert, target, logs }) { const button = target.querySelector('button'); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8421ae4a7cbf..8155aedcb082 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -9,13 +9,14 @@ import { user_effect } from '../../src/internal/client/reactivity/effects'; import { state, set, update, update_pre } from '../../src/internal/client/reactivity/sources'; -import type { Derived, Effect, Value } from '../../src/internal/client/types'; +import type { Derived, Effect, Source, Value } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; import { derived } from '../../src/internal/client/reactivity/deriveds'; import { snapshot } from '../../src/internal/shared/clone.js'; import { SvelteSet } from '../../src/reactivity/set'; import { DESTROYED } from '../../src/internal/client/constants'; import { noop } from 'svelte/internal/client'; +import { disable_async_mode_flag, enable_async_mode_flag } from '../../src/internal/flags'; /** * @param runes runes mode @@ -518,7 +519,7 @@ describe('signals', () => { }; }); - test('schedules rerun when writing to signal before reading it', (runes) => { + test.skip('schedules rerun when writing to signal before reading it', (runes) => { if (!runes) return () => {}; const error = console.error; @@ -1010,14 +1011,68 @@ describe('signals', () => { }; }); - test('effects do not depend on state they own', () => { + test('effects do depend on state they own', (runes) => { + // This behavior is important for use cases like a Resource class + // which shares its instance between multiple effects and triggers + // rerenders by self-invalidating its state. + const log: number[] = []; + + let count: any; + + if (runes) { + // We will make this the new default behavior once it's stable but until then + // we need to keep the old behavior to not break existing code. + enable_async_mode_flag(); + } + + effect(() => { + if (!count || $.get(count) < 2) { + count ||= state(0); + log.push($.get(count)); + set(count, $.get(count) + 1); + } + }); + + return () => { + try { + flushSync(); + if (runes) { + assert.deepEqual(log, [0, 1]); + } else { + assert.deepEqual(log, [0]); + } + } finally { + disable_async_mode_flag(); + } + }; + }); + + test('nested effects depend on state of upper effects', () => { + const logs: number[] = []; + let raw: Source; + let proxied: { current: number }; + user_effect(() => { - const value = state(0); - set(value, $.get(value) + 1); + raw = state(0); + 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); + }); }); return () => { flushSync(); + set(raw, $.get(raw) + 1); + proxied.current += 1; + flushSync(); + assert.deepEqual(logs, [0, 0, 1, 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 a351851875ed..da6fdf44d881 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); diff --git a/packages/svelte/tests/suite.ts b/packages/svelte/tests/suite.ts index 0ae06e727f87..6954b8b683f6 100644 --- a/packages/svelte/tests/suite.ts +++ b/packages/svelte/tests/suite.ts @@ -35,7 +35,7 @@ export function suite(fn: (config: Test, test_dir: string export function suite_with_variants( variants: Variants[], - should_skip_variant: (variant: Variants, config: Test) => boolean | 'no-test', + should_skip_variant: (variant: Variants, config: Test, test_name: string) => boolean | 'no-test', common_setup: (config: Test, test_dir: string) => Promise | Common, fn: (config: Test, test_dir: string, variant: Variants, common: Common) => void ) { @@ -46,11 +46,11 @@ export function suite_with_variants