diff --git a/.changeset/chilled-pumas-invite.md b/.changeset/chilled-pumas-invite.md new file mode 100644 index 0000000000..943f2d4836 --- /dev/null +++ b/.changeset/chilled-pumas-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: apply keyed validation only for keyed each diff --git a/.changeset/friendly-lies-camp.md b/.changeset/friendly-lies-camp.md new file mode 100644 index 0000000000..fe88999868 --- /dev/null +++ b/.changeset/friendly-lies-camp.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: warn on references to mutated non-state in template diff --git a/.changeset/green-eggs-approve.md b/.changeset/green-eggs-approve.md new file mode 100644 index 0000000000..90c7ec357e --- /dev/null +++ b/.changeset/green-eggs-approve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: adjust mount and createRoot types diff --git a/.changeset/light-pens-watch.md b/.changeset/light-pens-watch.md new file mode 100644 index 0000000000..62debd2eb4 --- /dev/null +++ b/.changeset/light-pens-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent reactive snippet from reinitializing unnecessarily diff --git a/.changeset/lovely-rules-eat.md b/.changeset/lovely-rules-eat.md new file mode 100644 index 0000000000..5ecaa2fc8d --- /dev/null +++ b/.changeset/lovely-rules-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: omit this bind this arg if we know it's not a signal diff --git a/.changeset/polite-pumpkins-guess.md b/.changeset/polite-pumpkins-guess.md new file mode 100644 index 0000000000..e364f9635d --- /dev/null +++ b/.changeset/polite-pumpkins-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take event attributes into account when checking a11y diff --git a/.changeset/pre.json b/.changeset/pre.json index c4e518fde5..7e15cda5cf 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -12,6 +12,7 @@ "afraid-moose-matter", "brave-walls-destroy", "brown-spoons-boil", + "chilled-pumas-invite", "chilly-dolphins-lick", "clean-eels-beg", "cold-birds-own", @@ -29,6 +30,7 @@ "fresh-weeks-trade", "funny-wombats-argue", "good-pianos-jump", + "green-eggs-approve", "green-hounds-play", "honest-icons-change", "hungry-dots-fry", @@ -38,6 +40,7 @@ "lazy-spiders-think", "long-crews-return", "lovely-items-turn", + "lovely-rules-eat", "lucky-schools-hang", "moody-frogs-exist", "moody-owls-cry", @@ -59,11 +62,14 @@ "sour-rules-march", "strong-lemons-provide", "tall-shrimps-worry", + "ten-worms-reflect", "thirty-flowers-sit", "thirty-ghosts-fix", "thirty-impalas-repair", + "thirty-wombats-relax", "tiny-kings-whisper", "two-falcons-buy", + "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train" ] diff --git a/.changeset/seven-ravens-check.md b/.changeset/seven-ravens-check.md new file mode 100644 index 0000000000..26063b2313 --- /dev/null +++ b/.changeset/seven-ravens-check.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support type definition in {@const} diff --git a/.changeset/slimy-clouds-talk.md b/.changeset/slimy-clouds-talk.md new file mode 100644 index 0000000000..f92019e7d3 --- /dev/null +++ b/.changeset/slimy-clouds-talk.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: ignore href attributes when hydrating diff --git a/.changeset/stale-comics-look.md b/.changeset/stale-comics-look.md new file mode 100644 index 0000000000..b7f09bd0a6 --- /dev/null +++ b/.changeset/stale-comics-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: bump esrap diff --git a/.changeset/ten-worms-reflect.md b/.changeset/ten-worms-reflect.md new file mode 100644 index 0000000000..a78d6b052f --- /dev/null +++ b/.changeset/ten-worms-reflect.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove constructor overload diff --git a/.changeset/thirty-wombats-relax.md b/.changeset/thirty-wombats-relax.md new file mode 100644 index 0000000000..0ed0f40a75 --- /dev/null +++ b/.changeset/thirty-wombats-relax.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve each block index handling diff --git a/.changeset/wet-games-fly.md b/.changeset/wet-games-fly.md new file mode 100644 index 0000000000..9f03e26882 --- /dev/null +++ b/.changeset/wet-games-fly.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: type-level back-compat for default slot and children prop diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf8e96b0ac..08820f0ed3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: os: ubuntu-latest - node-version: 20 os: ubuntu-latest + - node-version: 21 + os: ubuntu-latest + steps: - uses: actions/checkout@v3 - uses: pnpm/action-setup@v2.2.4 diff --git a/.prettierignore b/.prettierignore index 36dcaeb08f..45e12c6e3f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -37,3 +37,7 @@ sites/svelte.dev/src/lib/generated .changeset pnpm-lock.yaml pnpm-workspace.yaml + +# Temporarily ignore this file to avoid merge conflicts. +# see: https://github.com/sveltejs/svelte/pull/9609 +documentation/docs/05-misc/03-typescript.md diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 30d064c151..d8f5cd4fb2 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,25 @@ # svelte +## 5.0.0-next.13 + +### Patch Changes + +- fix: apply keyed validation only for keyed each ([#9641](https://github.com/sveltejs/svelte/pull/9641)) + +- fix: omit this bind this arg if we know it's not a signal ([#9635](https://github.com/sveltejs/svelte/pull/9635)) + +- fix: improve each block index handling ([#9644](https://github.com/sveltejs/svelte/pull/9644)) + +## 5.0.0-next.12 + +### Patch Changes + +- fix: adjust mount and createRoot types ([`63e583184`](https://github.com/sveltejs/svelte/commit/63e58318460dbb3485df93d15beb2779a86d2c9a)) + +- fix: remove constructor overload ([`cb4b1f0a1`](https://github.com/sveltejs/svelte/commit/cb4b1f0a189803bed04adcb90fbd4334782e8469)) + +- fix: type-level back-compat for default slot and children prop ([`a3bc7d569`](https://github.com/sveltejs/svelte/commit/a3bc7d5698425ec9dde86eb302f2fd56d9da8f96)) + ## 5.0.0-next.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9a4677cc2f..3e40ed5a2a 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.0.0-next.11", + "version": "5.0.0-next.13", "type": "module", "types": "./types/index.d.ts", "engines": { @@ -120,7 +120,7 @@ "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", - "esrap": "^1.2.0", + "esrap": "^1.2.1", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", diff --git a/packages/svelte/src/compiler/legacy.js b/packages/svelte/src/compiler/legacy.js index 6a0c7f1087..da20b90696 100644 --- a/packages/svelte/src/compiler/legacy.js +++ b/packages/svelte/src/compiler/legacy.js @@ -209,6 +209,33 @@ export function convert(source, ast) { }; }, // @ts-ignore + ConstTag(node) { + if ( + /** @type {import('./types/legacy-nodes.js').LegacyConstTag} */ (node).expression !== + undefined + ) { + return node; + } + + const modern_node = /** @type {import('#compiler').ConstTag} */ (node); + const { id: left } = { ...modern_node.declaration.declarations[0] }; + // @ts-ignore + delete left.typeAnnotation; + return { + type: 'ConstTag', + start: modern_node.start, + end: node.end, + expression: { + type: 'AssignmentExpression', + start: (modern_node.declaration.start ?? 0) + 'const '.length, + end: modern_node.declaration.end ?? 0, + operator: '=', + left, + right: modern_node.declaration.declarations[0].init + } + }; + }, + // @ts-ignore KeyBlock(node, { visit }) { remove_surrounding_whitespace_nodes(node.fragment.nodes); return { 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 2ad20d9cf1..76e66d9b1b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -2,8 +2,8 @@ import read_context from '../read/context.js'; import read_expression from '../read/expression.js'; import { error } from '../../../errors.js'; import { create_fragment } from '../utils/create.js'; -import { parse_expression_at } from '../acorn.js'; import { walk } from 'zimmerframe'; +import { parse } from '../acorn.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; @@ -532,21 +532,54 @@ function special(parser) { // {@const a = b} parser.require_whitespace(); - const expression = read_expression(parser); + const CONST_LENGTH = 'const '.length; + parser.index = parser.index - CONST_LENGTH; + + let end_index = parser.index; + /** @type {import('estree').VariableDeclaration | undefined} */ + let declaration = undefined; - if (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) { + const dummy_spaces = parser.template.substring(0, parser.index).replace(/[^\n]/g, ' '); + while (true) { + end_index = parser.template.indexOf('}', end_index + 1); + if (end_index === -1) break; + try { + const node = parse( + dummy_spaces + parser.template.substring(parser.index, end_index), + parser.ts + ).body[0]; + if (node?.type === 'VariableDeclaration') { + declaration = node; + break; + } + } catch (e) { + continue; + } + } + + if ( + declaration === undefined || + declaration.declarations.length !== 1 || + declaration.declarations[0].init === undefined + ) { error(start, 'invalid-const'); } - parser.allow_whitespace(); + parser.index = end_index; parser.eat('}', true); + const id = declaration.declarations[0].id; + if (id.type === 'Identifier') { + // Tidy up some stuff left behind by acorn-typescript + id.end = (id.start ?? 0) + id.name.length; + } + parser.append( /** @type {import('#compiler').ConstTag} */ ({ type: 'ConstTag', start, end: parser.index, - expression + declaration }) ); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/a11y.js index 33692ebafa..eb87b5ddbf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/a11y.js @@ -9,7 +9,7 @@ import { } from '../patterns.js'; import { warn } from '../../warnings.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; -import { is_text_attribute } from '../../utils/ast.js'; +import { is_event_attribute, is_text_attribute } from '../../utils/ast.js'; import { ContentEditableBindings } from '../constants.js'; import { walk } from 'zimmerframe'; @@ -704,10 +704,14 @@ function check_element(node, state, path) { } else if (attribute.type === 'OnDirective') { handlers.add(attribute.name); } else if (attribute.type === 'Attribute') { - attributes.push(attribute); - attribute_map.set(attribute.name, attribute); - if (attribute.name === 'contenteditable') { - has_contenteditable_attr = true; + if (is_event_attribute(attribute)) { + handlers.add(attribute.name.slice(2)); + } else { + attributes.push(attribute); + attribute_map.set(attribute.name, attribute); + if (attribute.name === 'contenteditable') { + has_contenteditable_attr = true; + } } } else if ( attribute.type === 'BindDirective' && diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c86cf1c9ee..03b99b2223 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -218,7 +218,7 @@ export function analyze_module(ast, options) { for (const [, scope] of scopes) { for (const [name, binding] of scope.declarations) { if (binding.kind === 'state' && !binding.mutated) { - warn(warnings, binding.node, [], 'state-rune-not-mutated', name); + warn(warnings, binding.node, [], 'state-not-mutated', name); } } } @@ -377,7 +377,7 @@ export function analyze_component(root, options) { for (const [, scope] of instance.scopes) { for (const [name, binding] of scope.declarations) { if (binding.kind === 'state' && !binding.mutated) { - warn(warnings, binding.node, [], 'state-rune-not-mutated', name); + warn(warnings, binding.node, [], 'state-not-mutated', name); } } } @@ -414,6 +414,30 @@ export function analyze_component(root, options) { analysis.reactive_statements = order_reactive_statements(analysis.reactive_statements); } + // warn on any nonstate declarations that are a) mutated and b) referenced in the template + for (const scope of [module.scope, instance.scope]) { + outer: for (const [name, binding] of scope.declarations) { + if (binding.kind === 'normal' && binding.mutated) { + for (const { path } of binding.references) { + if (path[0].type !== 'Fragment') continue; + for (let i = 1; i < path.length; i += 1) { + const type = path[i].type; + if ( + type === 'FunctionDeclaration' || + type === 'FunctionExpression' || + type === 'ArrowFunctionExpression' + ) { + continue; + } + } + + warn(warnings, binding.node, [], 'non-state-reference', name); + continue outer; + } + } + } + } + analysis.stylesheet.validate(analysis); for (const element of analysis.elements) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 68328e5453..9dd6775a6a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -899,7 +899,10 @@ function serialize_inline_component(node, component_name, context) { if (bind_this !== null) { const prev = fn; const assignment = b.assignment('=', bind_this, b.id('$$value')); - const bind_this_id = bind_this; + const bind_this_id = /** @type {import('estree').Expression} */ ( + // if expression is not an identifier, we know it can't be a signal + bind_this.type === 'Identifier' ? bind_this : undefined + ); fn = (node_id) => b.call( '$.bind_this', @@ -1650,19 +1653,20 @@ export const template_visitors = { ); }, ConstTag(node, { state, visit }) { + const declaration = node.declaration.declarations[0]; // TODO we can almost certainly share some code with $derived(...) - if (node.expression.left.type === 'Identifier') { + if (declaration.id.type === 'Identifier') { state.init.push( b.const( - node.expression.left, + declaration.id, b.call( '$.derived', - b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression.right))) + b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init))) ) ) ); } else { - const identifiers = extract_identifiers(node.expression.left); + const identifiers = extract_identifiers(declaration.id); const tmp = b.id(state.scope.generate('computed_const')); // Make all identifiers that are declared within the following computed regular @@ -1678,8 +1682,8 @@ export const template_visitors = { [], b.block([ b.const( - /** @type {import('estree').Pattern} */ (visit(node.expression.left)), - /** @type {import('estree').Expression} */ (visit(node.expression.right)) + /** @type {import('estree').Pattern} */ (visit(declaration.id)), + /** @type {import('estree').Expression} */ (visit(declaration.init)) ), b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) ]) @@ -1731,18 +1735,20 @@ export const template_visitors = { if (node.argument) { args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument)))); } - const snippet_function = /** @type {import('estree').Expression} */ ( + + let snippet_function = /** @type {import('estree').Expression} */ ( context.visit(node.expression) ); - const init = b.call( - context.state.options.dev ? b.call('$.validate_snippet', snippet_function) : snippet_function, - ...args - ); + if (context.state.options.dev) { + snippet_function = b.call('$.validate_snippet', snippet_function); + } if (is_reactive) { - context.state.init.push(b.stmt(b.call('$.snippet_effect', b.thunk(init)))); + context.state.init.push( + b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args)) + ); } else { - context.state.init.push(b.stmt(init)); + context.state.init.push(b.stmt(b.call(snippet_function, ...args))); } }, AnimateDirective(node, { state, visit }) { @@ -2279,12 +2285,6 @@ export const template_visitors = { ) : b.literal(null); - if (context.state.options.dev && key_function.type !== 'Literal') { - context.state.init.push( - b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) - ); - } - if (node.index && each_node_meta.contains_group_binding) { // We needed to create a unique identifier for the index above, but we want to use the // original index name in the template, therefore create another binding @@ -2292,6 +2292,12 @@ export const template_visitors = { } if ((each_type & EACH_KEYED) !== 0) { + if (context.state.options.dev && key_function.type !== 'Literal') { + context.state.init.push( + b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) + ); + } + context.state.after_update.push( b.stmt( b.call( @@ -2491,7 +2497,7 @@ export const template_visitors = { next(); }, BindDirective(node, context) { - const { state, path } = context; + const { state, path, visit } = context; /** @type {import('estree').Expression[]} */ const properties = []; @@ -2622,9 +2628,16 @@ export const template_visitors = { } case 'this': - call_expr = b.call(`$.bind_this`, state.node, setter, node.expression); + call_expr = b.call( + `$.bind_this`, + state.node, + setter, + /** @type {import('estree').Expression} */ ( + // if expression is not an identifier, we know it can't be a signal + node.expression.type === 'Identifier' ? node.expression : undefined + ) + ); break; - case 'textContent': case 'innerHTML': case 'innerText': diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index ab3b8ea85f..324fa87b50 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1098,8 +1098,9 @@ const template_visitors = { state.template.push(t_expression(id)); }, ConstTag(node, { state, visit }) { - const pattern = /** @type {import('estree').Pattern} */ (visit(node.expression.left)); - const init = /** @type {import('estree').Expression} */ (visit(node.expression.right)); + const declaration = node.declaration.declarations[0]; + const pattern = /** @type {import('estree').Pattern} */ (visit(declaration.id)); + const init = /** @type {import('estree').Expression} */ (visit(declaration.init)); state.init.push(b.declaration('const', pattern, init)); }, DebugTag(node, { state, visit }) { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 6c73002d5b..e958df604e 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -175,13 +175,13 @@ export class Scope { references.push({ node, path }); - const declaration = this.declarations.get(node.name); - if (declaration) { - declaration.references.push({ node, path }); + const binding = this.declarations.get(node.name); + if (binding) { + binding.references.push({ node, path }); } else if (this.#parent) { this.#parent.reference(node, path); } else { - // no declaration was found, and this is the top level scope, + // no binding was found, and this is the top level scope, // which means this is a global this.root.conflicts.add(node.name); } @@ -437,7 +437,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { next(); }, - VariableDeclaration(node, { state, next }) { + VariableDeclaration(node, { state, path, next }) { + const is_parent_const_tag = path.at(-1)?.type === 'ConstTag'; for (const declarator of node.declarations) { /** @type {import('#compiler').Binding[]} */ const bindings = []; @@ -445,7 +446,12 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { state.scope.declarators.set(declarator, bindings); for (const id of extract_identifiers(declarator.id)) { - const binding = state.scope.declare(id, 'normal', node.kind, declarator.init); + const binding = state.scope.declare( + id, + is_parent_const_tag ? 'derived' : 'normal', + node.kind, + declarator.init + ); bindings.push(binding); } } @@ -495,12 +501,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } if (node.index) { - scope.declare( - b.id(node.index), - // TODO see logic in EachBlock in dom.ts - node.key ? 'derived' : 'normal', - 'const' - ); + const is_keyed = + node.key && + (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); + scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const'); } if (node.key) visit(node.key, { scope }); @@ -595,7 +599,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }, ConstTag(node, { state, next }) { - for (const identifier of extract_identifiers(node.expression.left)) { + const declaration = node.declaration.declarations[0]; + for (const identifier of extract_identifiers(declaration.id)) { state.scope.declare( /** @type {import('estree').Identifier} */ (identifier), 'derived', diff --git a/packages/svelte/src/compiler/types/legacy-nodes.d.ts b/packages/svelte/src/compiler/types/legacy-nodes.d.ts index 6564dc47ab..41c1ad7a3b 100644 --- a/packages/svelte/src/compiler/types/legacy-nodes.d.ts +++ b/packages/svelte/src/compiler/types/legacy-nodes.d.ts @@ -1,6 +1,7 @@ import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler'; import type { ArrayExpression, + AssignmentExpression, Expression, Identifier, MemberExpression, @@ -168,6 +169,11 @@ export interface LegacyTitle extends BaseElement { name: 'title'; } +export interface LegacyConstTag extends BaseNode { + type: 'ConstTag'; + expression: AssignmentExpression; +} + export interface LegacyTransition extends BaseNode { type: 'Transition'; /** The 'x' in `transition:x` */ @@ -215,6 +221,7 @@ export type LegacyElementLike = | LegacyWindow; export type LegacySvelteNode = + | LegacyConstTag | LegacyElementLike | LegacyAttributeLike | LegacyAttributeShorthand diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index b69173e6b5..621e8092b2 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -2,7 +2,8 @@ import type { Binding } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, - AssignmentExpression, + VariableDeclaration, + VariableDeclarator, Expression, FunctionDeclaration, FunctionExpression, @@ -130,7 +131,9 @@ export interface Comment extends BaseNode { /** A `{@const ...}` tag */ export interface ConstTag extends BaseNode { type: 'ConstTag'; - expression: AssignmentExpression; + declaration: VariableDeclaration & { + declarations: [VariableDeclarator & { id: Identifier; init: Expression }]; + }; } /** A `{@debug ...}` tag */ diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 7103f4162c..6c38b1e052 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -22,8 +22,11 @@ const runes = { `It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` + `Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`, /** @param {string} name */ - 'state-rune-not-mutated': (name) => - `${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?` + 'state-not-mutated': (name) => + `${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?`, + /** @param {string} name */ + 'non-state-reference': (name) => + `${name} is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.` }; /** @satisfies {Warnings} */ @@ -115,7 +118,7 @@ const a11y = { 'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with elements', 'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero', 'a11y-click-events-have-key-events': () => - 'A11y: visible, non-interactive elements with an on:click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as + + `, + props: { + get log() { + return []; + } + }, + + async test({ assert, target, component }) { + const [toggle, increment] = target.querySelectorAll('button'); + + await increment?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

snippet: 1

+ + + ` + ); + assert.deepEqual(component.log, []); + + await toggle?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

component: 1

+ + + ` + ); + assert.deepEqual(component.log, [1]); + + await increment?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

component: 2

+ + + ` + ); + assert.deepEqual(component.log, [1]); + + await toggle?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

snippet: 2

+ + + ` + ); + assert.deepEqual(component.log, [1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte new file mode 100644 index 0000000000..bbe3a61043 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte @@ -0,0 +1,6 @@ + + +

component: {count}

diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte new file mode 100644 index 0000000000..682fca22dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte @@ -0,0 +1,22 @@ + + +{#snippet foo({count})} +

snippet: {count}

+{/snippet} + +{#snippet bar(props)} + +{/snippet} + +{@render snippet({ count, log })} + + + diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js new file mode 100644 index 0000000000..644f802ac6 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const1/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '

10 * 10 = 100

20 * 20 = 400

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte new file mode 100644 index 0000000000..1ec792c4ec --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const1/main.svelte @@ -0,0 +1,8 @@ + + +{#each boxes as box} + {@const area: number = box.width * box.height} +

{box.width} * {box.height} = {area}

+{/each} diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js b/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js new file mode 100644 index 0000000000..646f2812a2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const2/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '

{}

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte b/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte new file mode 100644 index 0000000000..f1096908f8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/typescript-const2/main.svelte @@ -0,0 +1,5 @@ + + +{@const name: string = "{}"} +

{name}

diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 749c648e2f..1047d56781 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -4,7 +4,8 @@ import { SvelteComponent, type ComponentEvents, type ComponentProps, - type ComponentType + type ComponentType, + mount } from 'svelte'; // --------------------------------------------------------------------------- legacy: classes @@ -15,11 +16,11 @@ class LegacyComponent extends SvelteComponent< { slot: { slotProps: boolean } } > {} -// @ts-expect-error const legacyComponent = new LegacyComponent({ target: null as any as Document | Element | ShadowRoot, props: { prop: 'foo', + // @ts-expect-error x: '' } }); @@ -56,14 +57,20 @@ class NewComponent extends SvelteComponent< anExport: string = ''; } -// @ts-expect-error new NewComponent({ - prop: 'foo', - x: '' + target: null as any, + props: { + prop: 'foo', + // @ts-expect-error + x: '' + } }); const newComponent: NewComponent = new NewComponent({ - prop: 'foo' + target: null as any, + props: { + prop: 'foo' + } }); newComponent.$$events_def.event; // @ts-expect-error @@ -97,7 +104,22 @@ const newComponentEvents2: ComponentEvents = { event: new KeyboardEvent('click') }; -const instance = createRoot(newComponent, { +mount(NewComponent, { + target: null as any as Document | Element | ShadowRoot | Text | Comment, + props: { + prop: 'foo', + // @ts-expect-error + x: '' + }, + events: { + event: new MouseEvent('click') + }, + immutable: true, + intro: false, + recover: false +}); + +const instance = createRoot(NewComponent, { target: null as any as Document | Element | ShadowRoot | Text | Comment, props: { prop: 'foo', @@ -123,11 +145,11 @@ instance.anExport === 1; // --------------------------------------------------------------------------- interop const AsLegacyComponent = asClassComponent(newComponent); -// @ts-expect-error new AsLegacyComponent({ target: null as any, props: { prop: '', + // @ts-expect-error x: '' } }); diff --git a/packages/svelte/tests/validator/samples/a11y-click-events-have-key-events/input.svelte b/packages/svelte/tests/validator/samples/a11y-click-events-have-key-events/input.svelte index 3fb1ded53d..0452a3714d 100644 --- a/packages/svelte/tests/validator/samples/a11y-click-events-have-key-events/input.svelte +++ b/packages/svelte/tests/validator/samples/a11y-click-events-have-key-events/input.svelte @@ -24,6 +24,8 @@