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",
"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"
]

@ -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
- node-version: 20
os: ubuntu-latest
- node-version: 21
os: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4

@ -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

@ -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

@ -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",

@ -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 {

@ -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;
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 (!(expression.type === 'AssignmentExpression' && expression.operator === '=')) {
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
})
);
}

@ -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,11 +704,15 @@ function check_element(node, state, path) {
} else if (attribute.type === 'OnDirective') {
handlers.add(attribute.name);
} else if (attribute.type === 'Attribute') {
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' &&
ContentEditableBindings.includes(attribute.name)

@ -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) {

@ -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':

@ -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 }) {

@ -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',

@ -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

@ -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 */

@ -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 <th> 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 <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: noninteractive element cannot have nonnegative tabIndex value',
/**
@ -180,7 +183,7 @@ const a11y = {
* @param {string} 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 */
'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
// same url, so we would need to create hidden anchor elements to compare them)
attribute !== 'src' &&
attribute !== 'href' &&
attribute !== 'srcset')
) {
if (value === null) {
@ -2550,7 +2551,7 @@ let src_url_equal_anchor;
* @param {string} url
* @returns {boolean}
*/
export function src_url_equal(element_src, url) {
function src_url_equal(element_src, url) {
if (element_src === url) return true;
if (!src_url_equal_anchor) {
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
* @returns {boolean}
*/
export function srcset_url_equal(element_srcset, srcset) {
const element_urls = split_srcset(element_srcset.srcset);
const urls = split_srcset(srcset || '');
export function srcset_url_equal(element, srcset) {
const element_urls = split_srcset(element.srcset);
const urls = split_srcset(srcset ?? '');
return (
urls.length === element_urls.length &&
@ -2595,23 +2596,21 @@ export function srcset_url_equal(element_srcset, srcset) {
* @param {string | null} value
*/
function check_src_in_dev_hydration(dom, attribute, value) {
if (current_hydration_fragment !== null && (attribute === 'src' || attribute === 'srcset')) {
if (
(attribute === 'src' && !src_url_equal(dom.getAttribute('src') || '', value || '')) ||
(attribute === 'srcset' &&
!srcset_url_equal(/** @type {HTMLImageElement | HTMLSourceElement} */ (dom), value || ''))
) {
if (!current_hydration_fragment) return;
if (attribute !== 'src' && attribute !== 'href' && attribute !== 'srcset') return;
if (attribute === 'srcset' && srcset_url_equal(dom, value)) return;
if (src_url_equal(dom.getAttribute(attribute) ?? '', value ?? '')) return;
// eslint-disable-next-line no-console
console.error(
'Detected a src/srcset 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:',
`Detected a ${attribute} attribute value change during hydration. This will not be repaired during hydration, ` +
`the ${attribute} value that came from the server will be used. Related element:`,
dom,
' Differing value:',
value
);
}
}
}
/**
* @param {Element} dom
@ -2778,7 +2777,7 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
if (
current_hydration_fragment === null ||
// @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
dom[name] = value;
@ -2915,7 +2914,7 @@ export function unwrap(value) {
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @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 {{
* target: Node;
* props?: Props;
@ -3034,7 +3033,7 @@ export function createRoot(component, options) {
* @template {Record<string, any>} Props
* @template {Record<string, any> | undefined} Exports
* @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 {{
* target: Node;
* 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}
*/
export function snippet_effect(create_snippet) {
export function snippet_effect(get_snippet, node, args) {
const block = create_snippet_block();
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 () => {
if (block.d !== null) {
remove(block.d);

@ -18,6 +18,14 @@ export interface ComponentConstructorOptions<
$$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.
*
@ -52,25 +60,18 @@ export class SvelteComponent<
Slots extends Record<string, any> = 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
* 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.
*/
constructor(options: ComponentConstructorOptions<Props>);
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: Props;
$$prop_def: PropsWithChildren<Props, Slots>;
/**
* For type checking capabilities only.
* Does not exist at runtime.

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.11';
export const VERSION = '5.0.0-next.13';
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,
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({
target: null as any,
props: {
prop: 'foo',
// @ts-expect-error
x: ''
}
});
const newComponent: NewComponent = new NewComponent({
target: null as any,
props: {
prop: 'foo'
}
});
newComponent.$$events_def.event;
// @ts-expect-error
@ -97,7 +104,22 @@ const newComponentEvents2: ComponentEvents<NewComponent> = {
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: ''
}
});

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

@ -1,7 +1,7 @@
[
{
"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": {
"line": 13,
"column": 0
@ -13,7 +13,7 @@
},
{
"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": {
"line": 15,
"column": 0
@ -25,7 +25,7 @@
},
{
"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": {
"line": 18,
"column": 0
@ -37,7 +37,7 @@
},
{
"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": {
"line": 20,
"column": 0
@ -49,7 +49,7 @@
},
{
"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": {
"line": 22,
"column": 0
@ -61,7 +61,7 @@
},
{
"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": {
"line": 24,
"column": 0
@ -73,7 +73,7 @@
},
{
"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": {
"line": 26,
"column": 0
@ -82,5 +82,17 @@
"line": 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,
"line": 29
},
"message": "A11y: on:mouseout must be accompanied by on:blur",
"message": "A11y: 'mouseout' event must be accompanied by 'blur' event",
"start": {
"column": 0,
"line": 29
@ -257,7 +257,7 @@
"column": 48,
"line": 30
},
"message": "A11y: on:mouseover must be accompanied by on:focus",
"message": "A11y: 'mouseover' event must be accompanied by 'focus' event",
"start": {
"column": 0,
"line": 30

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

@ -1,7 +1,7 @@
[
{
"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": {
"column": 1,
"line": 7

@ -1,7 +1,7 @@
[
{
"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": {
"column": 1,
"line": 8

@ -4,18 +4,17 @@
"version": "0.0.1",
"type": "module",
"scripts": {
"prepare": "node scripts/create-app-svelte.js",
"dev": "vite --host",
"ssr": "node ./server.js",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"svelte": "workspace:*"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^2.5.1",
"express": "^4.18.2",
"nodemon": "^3.0.1",
"svelte": "workspace:*",
"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';
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
version: 1.0.0
esrap:
specifier: ^1.2.0
version: 1.2.0
specifier: ^1.2.1
version: 1.2.1
is-reference:
specifier: ^3.0.1
version: 3.0.2
@ -137,10 +137,6 @@ importers:
version: 0.2.9
playgrounds/demo:
dependencies:
svelte:
specifier: workspace:*
version: link:../../packages/svelte
devDependencies:
'@sveltejs/vite-plugin-svelte':
specifier: ^2.5.1
@ -151,6 +147,9 @@ importers:
nodemon:
specifier: ^3.0.1
version: 3.0.1
svelte:
specifier: workspace:*
version: link:../../packages/svelte
vite:
specifier: ^4.4.9
version: 4.5.0(@types/node@18.18.9)
@ -261,8 +260,8 @@ importers:
specifier: ^5.0.1
version: 5.0.2
esrap:
specifier: ^1.2.0
version: 1.2.0
specifier: ^1.2.1
version: 1.2.1
publint:
specifier: ^0.2.0
version: 0.2.5
@ -4056,8 +4055,8 @@ packages:
estraverse: 5.3.0
dev: true
/esrap@1.2.0:
resolution: {integrity: sha512-ZWd00MnkN45Hcj+nIV5FPiZk6Nx7RDUL8G0KGp1cg1xSKXOEac0IhMQdsrGswXB1O0fMzH+MXq288Lt0Abpg/Q==}
/esrap@1.2.1:
resolution: {integrity: sha512-dhkcOLfN/aDdMFI1iwPEcy/XqAZzGNfgfEJjZozy2tia6u0dQoZyXzkRshHTckuNsM+c0CYQndY+uRFe3N+AIQ==}
dependencies:
'@jridgewell/sourcemap-codec': 1.4.15
'@types/estree': 1.0.5

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

Loading…
Cancel
Save