Merge branch 'main' into log-rune

log-rune
Dominic Gannaway 10 months ago
commit 910043079a

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply keyed validation only for keyed each

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: warn on references to mutated non-state in template

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: adjust mount and createRoot types

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent reactive snippet from reinitializing unnecessarily

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: omit this bind this arg if we know it's not a signal

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: take event attributes into account when checking a11y

@ -12,6 +12,7 @@
"afraid-moose-matter", "afraid-moose-matter",
"brave-walls-destroy", "brave-walls-destroy",
"brown-spoons-boil", "brown-spoons-boil",
"chilled-pumas-invite",
"chilly-dolphins-lick", "chilly-dolphins-lick",
"clean-eels-beg", "clean-eels-beg",
"cold-birds-own", "cold-birds-own",
@ -29,6 +30,7 @@
"fresh-weeks-trade", "fresh-weeks-trade",
"funny-wombats-argue", "funny-wombats-argue",
"good-pianos-jump", "good-pianos-jump",
"green-eggs-approve",
"green-hounds-play", "green-hounds-play",
"honest-icons-change", "honest-icons-change",
"hungry-dots-fry", "hungry-dots-fry",
@ -38,6 +40,7 @@
"lazy-spiders-think", "lazy-spiders-think",
"long-crews-return", "long-crews-return",
"lovely-items-turn", "lovely-items-turn",
"lovely-rules-eat",
"lucky-schools-hang", "lucky-schools-hang",
"moody-frogs-exist", "moody-frogs-exist",
"moody-owls-cry", "moody-owls-cry",
@ -59,11 +62,14 @@
"sour-rules-march", "sour-rules-march",
"strong-lemons-provide", "strong-lemons-provide",
"tall-shrimps-worry", "tall-shrimps-worry",
"ten-worms-reflect",
"thirty-flowers-sit", "thirty-flowers-sit",
"thirty-ghosts-fix", "thirty-ghosts-fix",
"thirty-impalas-repair", "thirty-impalas-repair",
"thirty-wombats-relax",
"tiny-kings-whisper", "tiny-kings-whisper",
"two-falcons-buy", "two-falcons-buy",
"wet-games-fly",
"wicked-clouds-exercise", "wicked-clouds-exercise",
"wicked-doors-train" "wicked-doors-train"
] ]

@ -0,0 +1,5 @@
---
'svelte': minor
---
feat: support type definition in {@const}

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: ignore href attributes when hydrating

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: bump esrap

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove constructor overload

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve each block index handling

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: type-level back-compat for default slot and children prop

@ -25,6 +25,9 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
- node-version: 20 - node-version: 20
os: ubuntu-latest os: ubuntu-latest
- node-version: 21
os: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v2.2.4

@ -37,3 +37,7 @@ sites/svelte.dev/src/lib/generated
.changeset .changeset
pnpm-lock.yaml pnpm-lock.yaml
pnpm-workspace.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

@ -1,5 +1,25 @@
# svelte # 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 ## 5.0.0-next.11
### Patch Changes ### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte", "name": "svelte",
"description": "Cybernetically enhanced web apps", "description": "Cybernetically enhanced web apps",
"license": "MIT", "license": "MIT",
"version": "5.0.0-next.11", "version": "5.0.0-next.13",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -120,7 +120,7 @@
"aria-query": "^5.3.0", "aria-query": "^5.3.0",
"axobject-query": "^4.0.0", "axobject-query": "^4.0.0",
"esm-env": "^1.0.0", "esm-env": "^1.0.0",
"esrap": "^1.2.0", "esrap": "^1.2.1",
"is-reference": "^3.0.1", "is-reference": "^3.0.1",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.4", "magic-string": "^0.30.4",

@ -209,6 +209,33 @@ export function convert(source, ast) {
}; };
}, },
// @ts-ignore // @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 }) { KeyBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes); remove_surrounding_whitespace_nodes(node.fragment.nodes);
return { return {

@ -2,8 +2,8 @@ import read_context from '../read/context.js';
import read_expression from '../read/expression.js'; import read_expression from '../read/expression.js';
import { error } from '../../../errors.js'; import { error } from '../../../errors.js';
import { create_fragment } from '../utils/create.js'; import { create_fragment } from '../utils/create.js';
import { parse_expression_at } from '../acorn.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { parse } from '../acorn.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/; const regex_whitespace_with_closing_curly_brace = /^\s*}/;
@ -532,21 +532,54 @@ function special(parser) {
// {@const a = b} // {@const a = b}
parser.require_whitespace(); 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'); error(start, 'invalid-const');
} }
parser.allow_whitespace(); parser.index = end_index;
parser.eat('}', true); 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( parser.append(
/** @type {import('#compiler').ConstTag} */ ({ /** @type {import('#compiler').ConstTag} */ ({
type: 'ConstTag', type: 'ConstTag',
start, start,
end: parser.index, end: parser.index,
expression declaration
}) })
); );
} }

@ -9,7 +9,7 @@ import {
} from '../patterns.js'; } from '../patterns.js';
import { warn } from '../../warnings.js'; import { warn } from '../../warnings.js';
import fuzzymatch from '../1-parse/utils/fuzzymatch.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 { ContentEditableBindings } from '../constants.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
@ -704,10 +704,14 @@ function check_element(node, state, path) {
} else if (attribute.type === 'OnDirective') { } else if (attribute.type === 'OnDirective') {
handlers.add(attribute.name); handlers.add(attribute.name);
} else if (attribute.type === 'Attribute') { } else if (attribute.type === 'Attribute') {
attributes.push(attribute); if (is_event_attribute(attribute)) {
attribute_map.set(attribute.name, attribute); handlers.add(attribute.name.slice(2));
if (attribute.name === 'contenteditable') { } else {
has_contenteditable_attr = true; attributes.push(attribute);
attribute_map.set(attribute.name, attribute);
if (attribute.name === 'contenteditable') {
has_contenteditable_attr = true;
}
} }
} else if ( } else if (
attribute.type === 'BindDirective' && attribute.type === 'BindDirective' &&

@ -218,7 +218,7 @@ export function analyze_module(ast, options) {
for (const [, scope] of scopes) { for (const [, scope] of scopes) {
for (const [name, binding] of scope.declarations) { for (const [name, binding] of scope.declarations) {
if (binding.kind === 'state' && !binding.mutated) { 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 [, scope] of instance.scopes) {
for (const [name, binding] of scope.declarations) { for (const [name, binding] of scope.declarations) {
if (binding.kind === 'state' && !binding.mutated) { 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); 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); analysis.stylesheet.validate(analysis);
for (const element of analysis.elements) { for (const element of analysis.elements) {

@ -899,7 +899,10 @@ function serialize_inline_component(node, component_name, context) {
if (bind_this !== null) { if (bind_this !== null) {
const prev = fn; const prev = fn;
const assignment = b.assignment('=', bind_this, b.id('$$value')); 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) => fn = (node_id) =>
b.call( b.call(
'$.bind_this', '$.bind_this',
@ -1650,19 +1653,20 @@ export const template_visitors = {
); );
}, },
ConstTag(node, { state, visit }) { ConstTag(node, { state, visit }) {
const declaration = node.declaration.declarations[0];
// TODO we can almost certainly share some code with $derived(...) // TODO we can almost certainly share some code with $derived(...)
if (node.expression.left.type === 'Identifier') { if (declaration.id.type === 'Identifier') {
state.init.push( state.init.push(
b.const( b.const(
node.expression.left, declaration.id,
b.call( b.call(
'$.derived', '$.derived',
b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression.right))) b.thunk(/** @type {import('estree').Expression} */ (visit(declaration.init)))
) )
) )
); );
} else { } else {
const identifiers = extract_identifiers(node.expression.left); const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(state.scope.generate('computed_const')); const tmp = b.id(state.scope.generate('computed_const'));
// Make all identifiers that are declared within the following computed regular // Make all identifiers that are declared within the following computed regular
@ -1678,8 +1682,8 @@ export const template_visitors = {
[], [],
b.block([ b.block([
b.const( b.const(
/** @type {import('estree').Pattern} */ (visit(node.expression.left)), /** @type {import('estree').Pattern} */ (visit(declaration.id)),
/** @type {import('estree').Expression} */ (visit(node.expression.right)) /** @type {import('estree').Expression} */ (visit(declaration.init))
), ),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node)))) b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
]) ])
@ -1731,18 +1735,20 @@ export const template_visitors = {
if (node.argument) { if (node.argument) {
args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(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) context.visit(node.expression)
); );
const init = b.call( if (context.state.options.dev) {
context.state.options.dev ? b.call('$.validate_snippet', snippet_function) : snippet_function, snippet_function = b.call('$.validate_snippet', snippet_function);
...args }
);
if (is_reactive) { 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 { } else {
context.state.init.push(b.stmt(init)); context.state.init.push(b.stmt(b.call(snippet_function, ...args)));
} }
}, },
AnimateDirective(node, { state, visit }) { AnimateDirective(node, { state, visit }) {
@ -2279,12 +2285,6 @@ export const template_visitors = {
) )
: b.literal(null); : 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) { 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 // 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 // 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 ((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( context.state.after_update.push(
b.stmt( b.stmt(
b.call( b.call(
@ -2491,7 +2497,7 @@ export const template_visitors = {
next(); next();
}, },
BindDirective(node, context) { BindDirective(node, context) {
const { state, path } = context; const { state, path, visit } = context;
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const properties = []; const properties = [];
@ -2622,9 +2628,16 @@ export const template_visitors = {
} }
case 'this': 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; break;
case 'textContent': case 'textContent':
case 'innerHTML': case 'innerHTML':
case 'innerText': case 'innerText':

@ -1098,8 +1098,9 @@ const template_visitors = {
state.template.push(t_expression(id)); state.template.push(t_expression(id));
}, },
ConstTag(node, { state, visit }) { ConstTag(node, { state, visit }) {
const pattern = /** @type {import('estree').Pattern} */ (visit(node.expression.left)); const declaration = node.declaration.declarations[0];
const init = /** @type {import('estree').Expression} */ (visit(node.expression.right)); 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)); state.init.push(b.declaration('const', pattern, init));
}, },
DebugTag(node, { state, visit }) { DebugTag(node, { state, visit }) {

@ -175,13 +175,13 @@ export class Scope {
references.push({ node, path }); references.push({ node, path });
const declaration = this.declarations.get(node.name); const binding = this.declarations.get(node.name);
if (declaration) { if (binding) {
declaration.references.push({ node, path }); binding.references.push({ node, path });
} else if (this.#parent) { } else if (this.#parent) {
this.#parent.reference(node, path); this.#parent.reference(node, path);
} else { } 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 // which means this is a global
this.root.conflicts.add(node.name); this.root.conflicts.add(node.name);
} }
@ -437,7 +437,8 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
next(); 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) { for (const declarator of node.declarations) {
/** @type {import('#compiler').Binding[]} */ /** @type {import('#compiler').Binding[]} */
const bindings = []; const bindings = [];
@ -445,7 +446,12 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
state.scope.declarators.set(declarator, bindings); state.scope.declarators.set(declarator, bindings);
for (const id of extract_identifiers(declarator.id)) { 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); bindings.push(binding);
} }
} }
@ -495,12 +501,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
} }
if (node.index) { if (node.index) {
scope.declare( const is_keyed =
b.id(node.index), node.key &&
// TODO see logic in EachBlock in dom.ts (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
node.key ? 'derived' : 'normal', scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const');
'const'
);
} }
if (node.key) visit(node.key, { scope }); 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 }) { 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( state.scope.declare(
/** @type {import('estree').Identifier} */ (identifier), /** @type {import('estree').Identifier} */ (identifier),
'derived', 'derived',

@ -1,6 +1,7 @@
import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler'; import type { StyleDirective as LegacyStyleDirective, Text } from '#compiler';
import type { import type {
ArrayExpression, ArrayExpression,
AssignmentExpression,
Expression, Expression,
Identifier, Identifier,
MemberExpression, MemberExpression,
@ -168,6 +169,11 @@ export interface LegacyTitle extends BaseElement {
name: 'title'; name: 'title';
} }
export interface LegacyConstTag extends BaseNode {
type: 'ConstTag';
expression: AssignmentExpression;
}
export interface LegacyTransition extends BaseNode { export interface LegacyTransition extends BaseNode {
type: 'Transition'; type: 'Transition';
/** The 'x' in `transition:x` */ /** The 'x' in `transition:x` */
@ -215,6 +221,7 @@ export type LegacyElementLike =
| LegacyWindow; | LegacyWindow;
export type LegacySvelteNode = export type LegacySvelteNode =
| LegacyConstTag
| LegacyElementLike | LegacyElementLike
| LegacyAttributeLike | LegacyAttributeLike
| LegacyAttributeShorthand | LegacyAttributeShorthand

@ -2,7 +2,8 @@ import type { Binding } from '#compiler';
import type { import type {
ArrayExpression, ArrayExpression,
ArrowFunctionExpression, ArrowFunctionExpression,
AssignmentExpression, VariableDeclaration,
VariableDeclarator,
Expression, Expression,
FunctionDeclaration, FunctionDeclaration,
FunctionExpression, FunctionExpression,
@ -130,7 +131,9 @@ export interface Comment extends BaseNode {
/** A `{@const ...}` tag */ /** A `{@const ...}` tag */
export interface ConstTag extends BaseNode { export interface ConstTag extends BaseNode {
type: 'ConstTag'; type: 'ConstTag';
expression: AssignmentExpression; declaration: VariableDeclaration & {
declarations: [VariableDeclarator & { id: Identifier; init: Expression }];
};
} }
/** A `{@debug ...}` tag */ /** A `{@debug ...}` tag */

@ -22,8 +22,11 @@ const runes = {
`It looks like you're using the $${name} rune, but there is a local binding called ${name}. ` + `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.`, `Referencing a local variable with a $ prefix will create a store subscription. Please rename ${name} to avoid the ambiguity.`,
/** @param {string} name */ /** @param {string} name */
'state-rune-not-mutated': (name) => 'state-not-mutated': (name) =>
`${name} is declared with $state(...) but is never updated. Did you mean to create a function that changes its value?` `${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} */ /** @satisfies {Warnings} */
@ -115,7 +118,7 @@ const a11y = {
'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with <th> elements', 'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with <th> elements',
'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero', 'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero',
'a11y-click-events-have-key-events': () => '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 <button type="button"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.', 'A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type="button"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.',
'a11y-no-noninteractive-tabindex': () => 'a11y-no-noninteractive-tabindex': () =>
'A11y: noninteractive element cannot have nonnegative tabIndex value', 'A11y: noninteractive element cannot have nonnegative tabIndex value',
/** /**
@ -180,7 +183,7 @@ const a11y = {
* @param {string} accompanied_by * @param {string} accompanied_by
*/ */
'a11y-mouse-events-have-key-events': (event, accompanied_by) => 'a11y-mouse-events-have-key-events': (event, accompanied_by) =>
`A11y: on:${event} must be accompanied by on:${accompanied_by}`, `A11y: '${event}' event must be accompanied by '${accompanied_by}' event`,
/** @param {string} name */ /** @param {string} name */
'a11y-missing-content': (name) => `A11y: <${name}> element should have child content` 'a11y-missing-content': (name) => `A11y: <${name}> element should have child content`
}; };

@ -2532,6 +2532,7 @@ export function attr(dom, attribute, value) {
// (we can't just compare the strings as they can be different between client and server but result in the // (we can't just compare the strings as they can be different between client and server but result in the
// same url, so we would need to create hidden anchor elements to compare them) // same url, so we would need to create hidden anchor elements to compare them)
attribute !== 'src' && attribute !== 'src' &&
attribute !== 'href' &&
attribute !== 'srcset') attribute !== 'srcset')
) { ) {
if (value === null) { if (value === null) {
@ -2550,7 +2551,7 @@ let src_url_equal_anchor;
* @param {string} url * @param {string} url
* @returns {boolean} * @returns {boolean}
*/ */
export function src_url_equal(element_src, url) { function src_url_equal(element_src, url) {
if (element_src === url) return true; if (element_src === url) return true;
if (!src_url_equal_anchor) { if (!src_url_equal_anchor) {
src_url_equal_anchor = document.createElement('a'); src_url_equal_anchor = document.createElement('a');
@ -2566,13 +2567,13 @@ function split_srcset(srcset) {
} }
/** /**
* @param {HTMLSourceElement | HTMLImageElement} element_srcset * @param {HTMLSourceElement | HTMLImageElement} element
* @param {string | undefined | null} srcset * @param {string | undefined | null} srcset
* @returns {boolean} * @returns {boolean}
*/ */
export function srcset_url_equal(element_srcset, srcset) { export function srcset_url_equal(element, srcset) {
const element_urls = split_srcset(element_srcset.srcset); const element_urls = split_srcset(element.srcset);
const urls = split_srcset(srcset || ''); const urls = split_srcset(srcset ?? '');
return ( return (
urls.length === element_urls.length && urls.length === element_urls.length &&
@ -2595,22 +2596,20 @@ export function srcset_url_equal(element_srcset, srcset) {
* @param {string | null} value * @param {string | null} value
*/ */
function check_src_in_dev_hydration(dom, attribute, value) { function check_src_in_dev_hydration(dom, attribute, value) {
if (current_hydration_fragment !== null && (attribute === 'src' || attribute === 'srcset')) { if (!current_hydration_fragment) return;
if ( if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
(attribute === 'src' && !src_url_equal(dom.getAttribute('src') || '', value || '')) ||
(attribute === 'srcset' && if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
!srcset_url_equal(/** @type {HTMLImageElement | HTMLSourceElement} */ (dom), value || '')) if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error( console.error(
'Detected a src/srcset attribute value change during hydration. This will not be repaired during hydration, ' + `Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
'the src/srcset value that came from the server will be used. Related element:', `the ${attribute} value that came from the server will be used. Related element:`,
dom, dom,
' Differing value:', ' Differing value:',
value value
); );
}
}
} }
/** /**
@ -2778,7 +2777,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
if ( if (
current_hydration_fragment === null || current_hydration_fragment === null ||
// @ts-ignore see attr method for an explanation of src/srcset // @ts-ignore see attr method for an explanation of src/srcset
(dom[name] !== value && name !== 'src' && name !== 'srcset') (dom[name] !== value && name !== 'src' && name !== 'href' && name !== 'srcset')
) { ) {
// @ts-ignore // @ts-ignore
dom[name] = value; dom[name] = value;
@ -2915,7 +2914,7 @@ export function unwrap(value) {
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports * @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @param {import('../../main/public.js').SvelteComponent<Props, Events>} component * @param {typeof import('../../main/public.js').SvelteComponent<Props, Events>} component
* @param {{ * @param {{
* target: Node; * target: Node;
* props?: Props; * props?: Props;
@ -3034,7 +3033,7 @@ export function createRoot(component, options) {
* @template {Record<string, any>} Props * @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports * @template {Record<string, any> | undefined} Exports
* @template {Record<string, any>} Events * @template {Record<string, any>} Events
* @param {import('../../main/public.js').SvelteComponent<Props, Events>} component * @param {typeof import('../../main/public.js').SvelteComponent<Props, Events>} component
* @param {{ * @param {{
* target: Node; * target: Node;
* props?: Props; * props?: Props;
@ -3170,13 +3169,18 @@ export function sanitize_slots(props) {
} }
/** /**
* @param {() => void} create_snippet * @param {() => Function} get_snippet
* @param {Node} node
* @param {() => any} args
* @returns {void} * @returns {void}
*/ */
export function snippet_effect(create_snippet) { export function snippet_effect(get_snippet, node, args) {
const block = create_snippet_block(); const block = create_snippet_block();
render_effect(() => { render_effect(() => {
create_snippet(); // Only rerender when the snippet function itself changes,
// not when an eagerly-read prop inside the snippet function changes
const snippet = get_snippet();
untrack(() => snippet(node, args));
return () => { return () => {
if (block.d !== null) { if (block.d !== null) {
remove(block.d); remove(block.d);

@ -18,6 +18,14 @@ export interface ComponentConstructorOptions<
$$inline?: boolean; $$inline?: boolean;
} }
// Utility type for ensuring backwards compatibility on a type level: If there's a default slot, add 'children' to the props if it doesn't exist there already
type PropsWithChildren<Props, Slots> = Props &
(Props extends { children?: any }
? {}
: Slots extends { default: any }
? { children?: Snippet }
: {});
/** /**
* Can be used to create strongly typed Svelte components. * Can be used to create strongly typed Svelte components.
* *
@ -52,25 +60,18 @@ export class SvelteComponent<
Slots extends Record<string, any> = any Slots extends Record<string, any> = any
> { > {
[prop: string]: any; [prop: string]: any;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
*/
constructor(props: Props);
/** /**
* @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See * is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info. * https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
*/ */
constructor(options: ComponentConstructorOptions<Props>); constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
/** /**
* For type checking capabilities only. * For type checking capabilities only.
* Does not exist at runtime. * Does not exist at runtime.
* ### DO NOT USE! * ### DO NOT USE!
* */ * */
$$prop_def: Props; $$prop_def: PropsWithChildren<Props, Slots>;
/** /**
* For type checking capabilities only. * For type checking capabilities only.
* Does not exist at runtime. * Does not exist at runtime.

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version * https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string} * @type {string}
*/ */
export const VERSION = '5.0.0-next.11'; export const VERSION = '5.0.0-next.13';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1 @@
<!--ssr:0--><a href="/bar">foo</a><!--ssr:0-->

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
test(assert, target) {
assert.equal(target.querySelector('a')?.getAttribute('href'), '/bar');
}
});

@ -0,0 +1,5 @@
<script>
let browser = typeof window !== 'undefined';
</script>
<a href={browser ? '/foo': '/bar'}>foo</a>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<div>0</div><div>1</div>'
});

@ -0,0 +1,3 @@
{#each ["a", "b"] as result, i (i)}
<div>{i}</div>
{/each}

@ -0,0 +1,7 @@
import { test } from '../../test';
export default test({
async test({ assert, target, component }) {
assert.equal(target.querySelector('img'), component.items[0].img);
}
});

@ -0,0 +1,11 @@
<script>
let { items = [{ src: 'https://ds' }] } = $props();
</script>
{#each items as item, i}
<img
src={item.src}
bind:this={items[i].img}
alt="slider{i}"
/>
{/each}

@ -0,0 +1,62 @@
import { test } from '../../test';
export default test({
html: `
<p>snippet: 0</p>
<button>toggle</button>
<button>increase count</button>
`,
props: {
get log() {
return [];
}
},
async test({ assert, target, component }) {
const [toggle, increment] = target.querySelectorAll('button');
await increment?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>snippet: 1</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, []);
await toggle?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>component: 1</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
await increment?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>component: 2</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
await toggle?.click();
assert.htmlEqual(
target.innerHTML,
`
<p>snippet: 2</p>
<button>toggle</button>
<button>increase count</button>
`
);
assert.deepEqual(component.log, [1]);
}
});

@ -0,0 +1,6 @@
<script>
let { count, log } = $props();
log.push(count);
</script>
<p>component: {count}</p>

@ -0,0 +1,22 @@
<script>
import Inner from "./inner.svelte";
let { log } = $props();
let count = $state(0);
let show_foo = $state(true);
let snippet = $derived(show_foo ? foo : bar);
</script>
{#snippet foo({count})}
<p>snippet: {count}</p>
{/snippet}
{#snippet bar(props)}
<Inner {...props}></Inner>
{/snippet}
{@render snippet({ count, log })}
<button onclick={() => show_foo = !show_foo}>toggle</button>
<button onclick={() => count++}>increase count</button>

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<p>10 * 10 = 100</p><p>20 * 20 = 400</p>'
});

@ -0,0 +1,8 @@
<script lang="ts">
const boxes = [ { width: 10, height: 10 }, { width: 20, height: 20 } ];
</script>
{#each boxes as box}
{@const area: number = box.width * box.height}
<p>{box.width} * {box.height} = {area}</p>
{/each}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<p>{}</p>'
});

@ -0,0 +1,5 @@
<script lang="ts">
</script>
{@const name: string = "{}"}
<p>{name}</p>

@ -4,7 +4,8 @@ import {
SvelteComponent, SvelteComponent,
type ComponentEvents, type ComponentEvents,
type ComponentProps, type ComponentProps,
type ComponentType type ComponentType,
mount
} from 'svelte'; } from 'svelte';
// --------------------------------------------------------------------------- legacy: classes // --------------------------------------------------------------------------- legacy: classes
@ -15,11 +16,11 @@ class LegacyComponent extends SvelteComponent<
{ slot: { slotProps: boolean } } { slot: { slotProps: boolean } }
> {} > {}
// @ts-expect-error
const legacyComponent = new LegacyComponent({ const legacyComponent = new LegacyComponent({
target: null as any as Document | Element | ShadowRoot, target: null as any as Document | Element | ShadowRoot,
props: { props: {
prop: 'foo', prop: 'foo',
// @ts-expect-error
x: '' x: ''
} }
}); });
@ -56,14 +57,20 @@ class NewComponent extends SvelteComponent<
anExport: string = ''; anExport: string = '';
} }
// @ts-expect-error
new NewComponent({ new NewComponent({
prop: 'foo', target: null as any,
x: '' props: {
prop: 'foo',
// @ts-expect-error
x: ''
}
}); });
const newComponent: NewComponent = new NewComponent({ const newComponent: NewComponent = new NewComponent({
prop: 'foo' target: null as any,
props: {
prop: 'foo'
}
}); });
newComponent.$$events_def.event; newComponent.$$events_def.event;
// @ts-expect-error // @ts-expect-error
@ -97,7 +104,22 @@ const newComponentEvents2: ComponentEvents<NewComponent> = {
event: new KeyboardEvent('click') 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, target: null as any as Document | Element | ShadowRoot | Text | Comment,
props: { props: {
prop: 'foo', prop: 'foo',
@ -123,11 +145,11 @@ instance.anExport === 1;
// --------------------------------------------------------------------------- interop // --------------------------------------------------------------------------- interop
const AsLegacyComponent = asClassComponent(newComponent); const AsLegacyComponent = asClassComponent(newComponent);
// @ts-expect-error
new AsLegacyComponent({ new AsLegacyComponent({
target: null as any, target: null as any,
props: { props: {
prop: '', prop: '',
// @ts-expect-error
x: '' x: ''
} }
}); });

@ -24,6 +24,8 @@
<header on:click={noop} /> <header on:click={noop} />
<!-- svelte-ignore a11y-no-noninteractive-element-interactions --> <!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<footer on:click={noop} /> <footer on:click={noop} />
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<footer onclick={noop} />
<!-- should not warn --> <!-- should not warn -->
<div class="foo" /> <div class="foo" />
@ -66,6 +68,7 @@
<div on:click={noop} role="presentation" /> <div on:click={noop} role="presentation" />
<div on:click={noop} role="none" /> <div on:click={noop} role="none" />
<div on:click={noop} role={dynamicRole} /> <div on:click={noop} role={dynamicRole} />
<div onclick={noop} role={dynamicRole} />
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<svelte:element this={Math.random() ? 'button' : 'div'} on:click={noop} /> <svelte:element this={Math.random() ? 'button' : 'div'} on:click={noop} />

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 13, "line": 13,
"column": 0 "column": 0
@ -13,7 +13,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 15, "line": 15,
"column": 0 "column": 0
@ -25,7 +25,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 18, "line": 18,
"column": 0 "column": 0
@ -37,7 +37,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 20, "line": 20,
"column": 0 "column": 0
@ -49,7 +49,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 22, "line": 22,
"column": 0 "column": 0
@ -61,7 +61,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 24, "line": 24,
"column": 0 "column": 0
@ -73,7 +73,7 @@
}, },
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"line": 26, "line": 26,
"column": 0 "column": 0
@ -82,5 +82,17 @@
"line": 26, "line": 26,
"column": 26 "column": 26
} }
},
{
"code": "a11y-click-events-have-key-events",
"message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": {
"line": 28,
"column": 0
},
"end": {
"line": 28,
"column": 25
}
} }
] ]

@ -233,7 +233,7 @@
"column": 44, "column": 44,
"line": 29 "line": 29
}, },
"message": "A11y: on:mouseout must be accompanied by on:blur", "message": "A11y: 'mouseout' event must be accompanied by 'blur' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 29 "line": 29
@ -257,7 +257,7 @@
"column": 48, "column": 48,
"line": 30 "line": 30
}, },
"message": "A11y: on:mouseover must be accompanied by on:focus", "message": "A11y: 'mouseover' event must be accompanied by 'focus' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 30 "line": 30

@ -5,7 +5,7 @@
"column": 35, "column": 35,
"line": 11 "line": 11
}, },
"message": "A11y: on:mouseover must be accompanied by on:focus", "message": "A11y: 'mouseover' event must be accompanied by 'focus' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 11 "line": 11
@ -17,7 +17,7 @@
"column": 51, "column": 51,
"line": 15 "line": 15
}, },
"message": "A11y: on:mouseover must be accompanied by on:focus", "message": "A11y: 'mouseover' event must be accompanied by 'focus' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 15 "line": 15
@ -29,7 +29,7 @@
"column": 34, "column": 34,
"line": 17 "line": 17
}, },
"message": "A11y: on:mouseout must be accompanied by on:blur", "message": "A11y: 'mouseout' event must be accompanied by 'blur' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 17 "line": 17
@ -41,7 +41,7 @@
"column": 50, "column": 50,
"line": 21 "line": 21
}, },
"message": "A11y: on:mouseout must be accompanied by on:blur", "message": "A11y: 'mouseout' event must be accompanied by 'blur' event",
"start": { "start": {
"column": 0, "column": 0,
"line": 21 "line": 21

@ -0,0 +1,3 @@
import { test } from '../../test';
export default test({});

@ -0,0 +1,10 @@
<script>
let a = $state(1);
let b = 2;
let c = 3;
</script>
<button onclick={() => a += 1}>a += 1</button>
<button onclick={() => b += 1}>b += 1</button>
<button onclick={() => c += 1}>c += 1</button>
<p>{a} + {b} + {c} = {a + b + c}</p>

@ -0,0 +1,26 @@
[
{
"code": "non-state-reference",
"message": "b is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.",
"start": {
"column": 5,
"line": 3
},
"end": {
"column": 6,
"line": 3
}
},
{
"code": "non-state-reference",
"message": "c is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.",
"start": {
"column": 5,
"line": 4
},
"end": {
"column": 6,
"line": 4
}
}
]

@ -1,6 +1,6 @@
[ [
{ {
"code": "state-rune-not-mutated", "code": "state-not-mutated",
"end": { "end": {
"column": 11, "column": 11,
"line": 3 "line": 3

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"column": 1, "column": 1,
"line": 7 "line": 7

@ -1,7 +1,7 @@
[ [
{ {
"code": "a11y-click-events-have-key-events", "code": "a11y-click-events-have-key-events",
"message": "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 <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.", "message": "A11y: visible, non-interactive elements with a click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as <button type=\"button\"> or <a> might be more appropriate. See https://svelte.dev/docs/accessibility-warnings#a11y-click-events-have-key-events for more details.",
"start": { "start": {
"column": 1, "column": 1,
"line": 8 "line": 8

@ -4,18 +4,17 @@
"version": "0.0.1", "version": "0.0.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"prepare": "node scripts/create-app-svelte.js",
"dev": "vite --host", "dev": "vite --host",
"ssr": "node ./server.js", "ssr": "node ./server.js",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": {
"svelte": "workspace:*"
},
"devDependencies": { "devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.5.1", "@sveltejs/vite-plugin-svelte": "^2.5.1",
"express": "^4.18.2", "express": "^4.18.2",
"nodemon": "^3.0.1", "nodemon": "^3.0.1",
"svelte": "workspace:*",
"vite": "^4.4.9" "vite": "^4.4.9"
} }
} }

@ -0,0 +1,8 @@
<script lang="ts">
function openInEditor() {
fetch('./__open-in-editor?file=src/App.svelte');
}
</script>
<h1>Demo App</h1>
<button class="open-in-editor" on:click={openInEditor}>edit App.svelte</button>

@ -0,0 +1,6 @@
import fs from 'node:fs';
const destination = new URL('../src/App.svelte', import.meta.url);
if (!fs.existsSync(destination)) {
const template = new URL('./App.template.svelte', import.meta.url);
fs.writeFileSync(destination, fs.readFileSync(template, 'utf-8'), 'utf-8');
}

@ -2,5 +2,9 @@ import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte'; import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({ export default defineConfig({
plugins: [svelte()] plugins: [svelte()],
optimizeDeps: {
// svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change
exclude: ['svelte']
}
}); });

@ -81,8 +81,8 @@ importers:
specifier: ^1.0.0 specifier: ^1.0.0
version: 1.0.0 version: 1.0.0
esrap: esrap:
specifier: ^1.2.0 specifier: ^1.2.1
version: 1.2.0 version: 1.2.1
is-reference: is-reference:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.2 version: 3.0.2
@ -137,10 +137,6 @@ importers:
version: 0.2.9 version: 0.2.9
playgrounds/demo: playgrounds/demo:
dependencies:
svelte:
specifier: workspace:*
version: link:../../packages/svelte
devDependencies: devDependencies:
'@sveltejs/vite-plugin-svelte': '@sveltejs/vite-plugin-svelte':
specifier: ^2.5.1 specifier: ^2.5.1
@ -151,6 +147,9 @@ importers:
nodemon: nodemon:
specifier: ^3.0.1 specifier: ^3.0.1
version: 3.0.1 version: 3.0.1
svelte:
specifier: workspace:*
version: link:../../packages/svelte
vite: vite:
specifier: ^4.4.9 specifier: ^4.4.9
version: 4.5.0(@types/node@18.18.9) version: 4.5.0(@types/node@18.18.9)
@ -261,8 +260,8 @@ importers:
specifier: ^5.0.1 specifier: ^5.0.1
version: 5.0.2 version: 5.0.2
esrap: esrap:
specifier: ^1.2.0 specifier: ^1.2.1
version: 1.2.0 version: 1.2.1
publint: publint:
specifier: ^0.2.0 specifier: ^0.2.0
version: 0.2.5 version: 0.2.5
@ -4056,8 +4055,8 @@ packages:
estraverse: 5.3.0 estraverse: 5.3.0
dev: true dev: true
/esrap@1.2.0: /esrap@1.2.1:
resolution: {integrity: sha512-ZWd00MnkN45Hcj+nIV5FPiZk6Nx7RDUL8G0KGp1cg1xSKXOEac0IhMQdsrGswXB1O0fMzH+MXq288Lt0Abpg/Q==} resolution: {integrity: sha512-dhkcOLfN/aDdMFI1iwPEcy/XqAZzGNfgfEJjZozy2tia6u0dQoZyXzkRshHTckuNsM+c0CYQndY+uRFe3N+AIQ==}
dependencies: dependencies:
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15
'@types/estree': 1.0.5 '@types/estree': 1.0.5

@ -18,7 +18,7 @@
"@sveltejs/kit": "^1.22.5", "@sveltejs/kit": "^1.22.5",
"@sveltejs/site-kit": "6.0.0-next.51", "@sveltejs/site-kit": "6.0.0-next.51",
"@types/marked": "^5.0.1", "@types/marked": "^5.0.1",
"esrap": "^1.2.0", "esrap": "^1.2.1",
"marked": "^9.0.0", "marked": "^9.0.0",
"publint": "^0.2.0", "publint": "^0.2.0",
"shiki": "^0.14.4", "shiki": "^0.14.4",

Loading…
Cancel
Save