Merge remote-tracking branch 'origin' into elliott/variadic-snippets

pull/10320/head
S. Elliott Johnson 2 years ago
commit c4509b2de8

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: deeply unstate objects passed to inspect

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle delegated events of elements moved outside the container

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve text node output

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve style parser whitespace handling

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow input elements within button elements

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve script `lang` attribute detection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: support TypeScript's `satisfies` operator

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve pseudo class parsing

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: provide `unstate` in server environment

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve key block reactivity detection

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add types for popover attributes and events

@ -25,9 +25,13 @@
"cool-ants-leave", "cool-ants-leave",
"cuddly-pianos-drop", "cuddly-pianos-drop",
"curly-lizards-dream", "curly-lizards-dream",
"curvy-ties-shout",
"dirty-bats-punch",
"dirty-garlics-design", "dirty-garlics-design",
"dirty-tips-add", "dirty-tips-add",
"dry-clocks-grow", "dry-clocks-grow",
"dry-eggs-play",
"dry-eggs-retire",
"dull-mangos-wave", "dull-mangos-wave",
"early-ads-tie", "early-ads-tie",
"eight-steaks-shout", "eight-steaks-shout",
@ -47,6 +51,7 @@
"funny-wombats-argue", "funny-wombats-argue",
"gentle-sheep-hug", "gentle-sheep-hug",
"giant-roses-press", "giant-roses-press",
"good-cars-visit",
"good-pianos-jump", "good-pianos-jump",
"great-icons-retire", "great-icons-retire",
"green-eggs-approve", "green-eggs-approve",
@ -54,6 +59,7 @@
"happy-suits-film", "happy-suits-film",
"healthy-planes-vanish", "healthy-planes-vanish",
"heavy-ears-rule", "heavy-ears-rule",
"hip-balloons-begin",
"honest-icons-change", "honest-icons-change",
"hungry-dots-fry", "hungry-dots-fry",
"hungry-tips-unite", "hungry-tips-unite",
@ -71,6 +77,7 @@
"lazy-months-knock", "lazy-months-knock",
"lazy-spiders-think", "lazy-spiders-think",
"lemon-geese-drum", "lemon-geese-drum",
"light-humans-hang",
"light-pens-watch", "light-pens-watch",
"long-buckets-lay", "long-buckets-lay",
"long-crews-return", "long-crews-return",
@ -80,6 +87,7 @@
"lucky-schools-hang", "lucky-schools-hang",
"moody-frogs-exist", "moody-frogs-exist",
"moody-owls-cry", "moody-owls-cry",
"nasty-lions-double",
"neat-dingos-clap", "neat-dingos-clap",
"new-boats-wait", "new-boats-wait",
"ninety-dingos-walk", "ninety-dingos-walk",
@ -87,13 +95,17 @@
"odd-schools-wait", "odd-schools-wait",
"odd-shoes-cheat", "odd-shoes-cheat",
"old-flies-jog", "old-flies-jog",
"old-houses-drum",
"old-mails-sneeze", "old-mails-sneeze",
"old-oranges-compete",
"orange-dingos-poke",
"polite-dolphins-care", "polite-dolphins-care",
"polite-pumpkins-guess", "polite-pumpkins-guess",
"polite-ravens-study", "polite-ravens-study",
"poor-eggs-enjoy", "poor-eggs-enjoy",
"poor-seahorses-flash", "poor-seahorses-flash",
"popular-mangos-rest", "popular-mangos-rest",
"pretty-ties-help",
"purple-dragons-peel", "purple-dragons-peel",
"quiet-camels-mate", "quiet-camels-mate",
"rare-pears-whisper", "rare-pears-whisper",
@ -108,6 +120,7 @@
"seven-deers-jam", "seven-deers-jam",
"seven-ravens-check", "seven-ravens-check",
"sharp-gorillas-impress", "sharp-gorillas-impress",
"sharp-kids-happen",
"sharp-tomatoes-learn", "sharp-tomatoes-learn",
"shiny-baboons-play", "shiny-baboons-play",
"shiny-shrimps-march", "shiny-shrimps-march",
@ -125,10 +138,12 @@
"strong-gifts-smoke", "strong-gifts-smoke",
"strong-lemons-provide", "strong-lemons-provide",
"sweet-mangos-beg", "sweet-mangos-beg",
"sweet-pens-sniff",
"swift-donkeys-perform", "swift-donkeys-perform",
"swift-ravens-hunt", "swift-ravens-hunt",
"swift-seahorses-deliver", "swift-seahorses-deliver",
"tall-books-grin", "tall-books-grin",
"tall-garlics-try",
"tall-shrimps-worry", "tall-shrimps-worry",
"tall-tigers-wait", "tall-tigers-wait",
"tasty-numbers-perform", "tasty-numbers-perform",
@ -140,12 +155,14 @@
"thirty-ghosts-fix", "thirty-ghosts-fix",
"thirty-impalas-repair", "thirty-impalas-repair",
"thirty-wombats-relax", "thirty-wombats-relax",
"three-suits-grin",
"tiny-kings-whisper", "tiny-kings-whisper",
"twelve-dragons-join", "twelve-dragons-join",
"twelve-onions-juggle", "twelve-onions-juggle",
"two-dragons-yell", "two-dragons-yell",
"two-falcons-buy", "two-falcons-buy",
"unlucky-boxes-obey", "unlucky-boxes-obey",
"unlucky-trees-lick",
"wet-games-fly", "wet-games-fly",
"wicked-clouds-exercise", "wicked-clouds-exercise",
"wicked-doors-train", "wicked-doors-train",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: skip generating $.proxy() calls for unary and binary expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: always treat spread attributes as reactive and separate them if needed

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow pseudo classes after `:global(..)`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: bail-out event handler referencing each index

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: parse `:nth-of-type(xn+y)` correctly

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure if block is executed in correct order

@ -50,4 +50,11 @@ jobs:
with: with:
node-version: 18 node-version: 18
cache: pnpm cache: pnpm
- run: 'pnpm i && pnpm check && pnpm lint' - name: install
run: pnpm install --frozen-lockfile
- name: type check
run: pnpm check
- name: lint
run: pnpm lint
- name: build and check generated types
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }

@ -28,7 +28,11 @@ jobs:
node-version: 18.x node-version: 18.x
cache: pnpm cache: pnpm
- run: pnpm install --frozen-lockfile - name: Install
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build && { [ "`git status --porcelain=v1`" == "" ] || (echo "Generated types have changed — please regenerate types locally and commit the changes after you have reviewed them"; git diff; exit 1); }
- name: Create Release Pull Request or Publish to npm - name: Create Release Pull Request or Publish to npm
id: changesets id: changesets

@ -133,6 +133,10 @@ To typecheck the codebase, run `pnpm check` inside `packages/svelte`. To typeche
- `snake_case` for internal variable names and methods. - `snake_case` for internal variable names and methods.
- `camelCase` for public variable names and methods. - `camelCase` for public variable names and methods.
### Generating types
Types are auto-generated from the source, but the result is checked in to ensure no accidental changes slip through. Run `pnpm generate:types` to regenerate the types.
### Sending your pull request ### Sending your pull request
Please make sure the following is done when submitting a pull request: Please make sure the following is done when submitting a pull request:
@ -141,7 +145,7 @@ Please make sure the following is done when submitting a pull request:
1. Make sure your code lints (`pnpm lint`). 1. Make sure your code lints (`pnpm lint`).
1. Make sure your tests pass (`pnpm test`). 1. Make sure your tests pass (`pnpm test`).
All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. All pull requests should be opened against the `main` branch. Make sure the PR does only one thing, otherwise please split it. If this change should contribute to a version bump, run `npx changeset` at the root of the repository after a code change and select the appropriate packages.
#### Breaking changes #### Breaking changes

@ -1,4 +1,4 @@
Copyright (c) 2016-23 [these people](https://github.com/sveltejs/svelte/graphs/contributors) Copyright (c) 2016-24 [these people](https://github.com/sveltejs/svelte/graphs/contributors)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

@ -13,7 +13,7 @@
<!-- the text will flash red whenever <!-- the text will flash red whenever
the `todo` object changes --> the `todo` object changes -->
<button bind:this={btn}> <button bind:this={btn} on:click>
{todo.done ? '👍' : ''} {todo.done ? '👍' : ''}
{todo.text} {todo.text}
</button> </button>

@ -1,4 +1,5 @@
/types /types/*.map
/types/compiler
/compiler.cjs /compiler.cjs
/action.d.ts /action.d.ts

@ -1,5 +1,47 @@
# svelte # svelte
## 5.0.0-next.29
### Patch Changes
- fix: improve text node output ([#10081](https://github.com/sveltejs/svelte/pull/10081))
- fix: improve style parser whitespace handling ([#10077](https://github.com/sveltejs/svelte/pull/10077))
- fix: allow input elements within button elements ([#10083](https://github.com/sveltejs/svelte/pull/10083))
- fix: support TypeScript's `satisfies` operator ([#10068](https://github.com/sveltejs/svelte/pull/10068))
- fix: provide `unstate` in server environment ([`877ff1ee7`](https://github.com/sveltejs/svelte/commit/877ff1ee7d637e2248145d975748e1012a977396))
- fix: improve key block reactivity detection ([#10092](https://github.com/sveltejs/svelte/pull/10092))
- fix: always treat spread attributes as reactive and separate them if needed ([#10071](https://github.com/sveltejs/svelte/pull/10071))
## 5.0.0-next.28
### Patch Changes
- fix: deeply unstate objects passed to inspect ([#10056](https://github.com/sveltejs/svelte/pull/10056))
- fix: handle delegated events of elements moved outside the container ([#10060](https://github.com/sveltejs/svelte/pull/10060))
- fix: improve script `lang` attribute detection ([#10046](https://github.com/sveltejs/svelte/pull/10046))
- fix: improve pseudo class parsing ([#10055](https://github.com/sveltejs/svelte/pull/10055))
- fix: add types for popover attributes and events ([#10041](https://github.com/sveltejs/svelte/pull/10041))
- fix: skip generating $.proxy() calls for unary and binary expressions ([#9979](https://github.com/sveltejs/svelte/pull/9979))
- fix: allow pseudo classes after `:global(..)` ([#10055](https://github.com/sveltejs/svelte/pull/10055))
- fix: bail-out event handler referencing each index ([#10063](https://github.com/sveltejs/svelte/pull/10063))
- fix: parse `:nth-of-type(xn+y)` correctly ([#9970](https://github.com/sveltejs/svelte/pull/9970))
- fix: ensure if block is executed in correct order ([#10053](https://github.com/sveltejs/svelte/pull/10053))
## 5.0.0-next.27 ## 5.0.0-next.27
### Patch Changes ### Patch Changes

@ -59,6 +59,7 @@ export type WheelEventHandler<T extends EventTarget> = EventHandler<WheelEvent,
export type AnimationEventHandler<T extends EventTarget> = EventHandler<AnimationEvent, T>; export type AnimationEventHandler<T extends EventTarget> = EventHandler<AnimationEvent, T>;
export type TransitionEventHandler<T extends EventTarget> = EventHandler<TransitionEvent, T>; export type TransitionEventHandler<T extends EventTarget> = EventHandler<TransitionEvent, T>;
export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEvent, T>; export type MessageEventHandler<T extends EventTarget> = EventHandler<MessageEvent, T>;
export type ToggleEventHandler<T extends EventTarget> = EventHandler<ToggleEvent, T>;
// //
// DOM Attributes // DOM Attributes
@ -136,10 +137,13 @@ export interface DOMAttributes<T extends EventTarget> {
onerror?: EventHandler | undefined | null; // also a Media Event onerror?: EventHandler | undefined | null; // also a Media Event
onerrorcapture?: EventHandler | undefined | null; // also a Media Event onerrorcapture?: EventHandler | undefined | null; // also a Media Event
// Detail Events // Popover Events
'on:toggle'?: EventHandler<Event, T> | undefined | null; 'on:beforetoggle'?: ToggleEventHandler<T> | undefined | null;
ontoggle?: EventHandler<Event, T> | undefined | null; onbeforetoggle?: ToggleEventHandler<T> | undefined | null;
ontogglecapture?: EventHandler<Event, T> | undefined | null; onbeforetogglecapture?: ToggleEventHandler<T> | undefined | null;
'on:toggle'?: ToggleEventHandler<T> | undefined | null;
ontoggle?: ToggleEventHandler<T> | undefined | null;
ontogglecapture?: ToggleEventHandler<T> | undefined | null;
// Keyboard Events // Keyboard Events
'on:keydown'?: KeyboardEventHandler<T> | undefined | null; 'on:keydown'?: KeyboardEventHandler<T> | undefined | null;
@ -727,6 +731,7 @@ export interface HTMLAttributes<T extends EventTarget> extends AriaAttributes, D
title?: string | undefined | null; title?: string | undefined | null;
translate?: 'yes' | 'no' | '' | undefined | null; translate?: 'yes' | 'no' | '' | undefined | null;
inert?: boolean | undefined | null; inert?: boolean | undefined | null;
popover?: 'auto' | 'manual' | '' | undefined | null;
// Unknown // Unknown
radiogroup?: string | undefined | null; // <command>, <menuitem> radiogroup?: string | undefined | null; // <command>, <menuitem>
@ -873,6 +878,8 @@ export interface HTMLButtonAttributes extends HTMLAttributes<HTMLButtonElement>
name?: string | undefined | null; name?: string | undefined | null;
type?: 'submit' | 'reset' | 'button' | undefined | null; type?: 'submit' | 'reset' | 'button' | undefined | null;
value?: string | string[] | number | undefined | null; value?: string | string[] | number | undefined | null;
popovertarget?: string | undefined | null;
popovertargetaction?: 'toggle' | 'show' | 'hide' | undefined | null;
} }
export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> { export interface HTMLCanvasAttributes extends HTMLAttributes<HTMLCanvasElement> {
@ -897,6 +904,10 @@ export interface HTMLDetailsAttributes extends HTMLAttributes<HTMLDetailsElement
open?: boolean | undefined | null; open?: boolean | undefined | null;
'bind:open'?: boolean | undefined | null; 'bind:open'?: boolean | undefined | null;
'on:toggle'?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontoggle?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
ontogglecapture?: EventHandler<Event, HTMLDetailsElement> | undefined | null;
} }
export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> { export interface HTMLDelAttributes extends HTMLAttributes<HTMLModElement> {

@ -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.27", "version": "5.0.0-next.29",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -90,11 +90,12 @@
"templating" "templating"
], ],
"scripts": { "scripts": {
"build": "rollup -c && node scripts/build.js && node scripts/check-treeshakeability.js", "build": "rollup -c && pnpm generate:types && node scripts/check-treeshakeability.js",
"dev": "rollup -cw", "dev": "rollup -cw",
"check": "tsc && cd ./tests/types && tsc", "check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch", "check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js",
"prepublishOnly": "pnpm build" "prepublishOnly": "pnpm build"
}, },
"devDependencies": { "devDependencies": {
@ -106,7 +107,7 @@
"@rollup/plugin-virtual": "^3.0.2", "@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.3", "@types/aria-query": "^5.0.3",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"dts-buddy": "^0.4.0", "dts-buddy": "^0.4.3",
"esbuild": "^0.19.2", "esbuild": "^0.19.2",
"rollup": "^4.1.5", "rollup": "^4.1.5",
"source-map": "^0.7.4", "source-map": "^0.7.4",
@ -116,7 +117,7 @@
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.4.15",
"acorn": "^8.10.0", "acorn": "^8.10.0",
"acorn-typescript": "^1.4.11", "acorn-typescript": "^1.4.13",
"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",

@ -2,7 +2,7 @@ import * as acorn from 'acorn';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript'; import { tsPlugin } from 'acorn-typescript';
const ParserWithTS = acorn.Parser.extend(tsPlugin()); const ParserWithTS = acorn.Parser.extend(tsPlugin({ allowSatisfies: true }));
/** /**
* @param {string} source * @param {string} source

@ -11,7 +11,7 @@ import read_options from './read/options.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute = const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; /<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser { export class Parser {
/** /**
@ -49,7 +49,14 @@ export class Parser {
this.template = template.trimRight(); this.template = template.trimRight();
this.ts = regex_lang_attribute.exec(template)?.[2] === 'ts'; let match_lang;
do match_lang = regex_lang_attribute.exec(template);
while (match_lang && match_lang[0][1] !== 's'); // ensure it starts with '<s' to match script tags
regex_lang_attribute.lastIndex = 0; // reset matched index to pass tests - otherwise declare the regex inside the constructor
this.ts = match_lang?.[2] === 'ts';
this.root = { this.root = {
css: null, css: null,

@ -6,7 +6,8 @@ const REGEX_ATTRIBUTE_FLAGS = /^[a-zA-Z]+/; // only `i` and `s` are valid today,
const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/; const REGEX_COMBINATOR_WHITESPACE = /^\s*(\+|~|>|\|\|)\s*/;
const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/; const REGEX_COMBINATOR = /^(\+|~|>|\|\|)/;
const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/; const REGEX_PERCENTAGE = /^\d+(\.\d+)?%/;
const REGEX_NTH_OF = /^\s*(even|odd|(-?[0-9]?n?(\s*\+\s*[0-9]+)?))(\s*(?=[,)])|\s+of\s+)/; const REGEX_NTH_OF =
/^(even|odd|\+?(\d+|\d*n(\s*[+-]\s*\d+)?)|-\d*n(\s*\+\s*\d+))((?=\s*[,)])|\s+of\s+)/;
const REGEX_WHITESPACE_OR_COLON = /[\s:]/; const REGEX_WHITESPACE_OR_COLON = /[\s:]/;
const REGEX_BRACE_OR_SEMICOLON = /[{;]/; const REGEX_BRACE_OR_SEMICOLON = /[{;]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/; const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
@ -152,6 +153,8 @@ function read_selector_list(parser, inside_pseudo_class = false) {
/** @type {import('#compiler').Css.Selector[]} */ /** @type {import('#compiler').Css.Selector[]} */
const children = []; const children = [];
allow_comment_or_whitespace(parser);
const start = parser.index; const start = parser.index;
while (parser.index < parser.template.length) { while (parser.index < parser.template.length) {
@ -226,6 +229,12 @@ function read_selector(parser, inside_pseudo_class = false) {
start, start,
end: parser.index end: parser.index
}); });
// We read the inner selectors of a pseudo element to ensure it parses correctly,
// but we don't do anything with the result.
if (parser.eat('(')) {
read_selector_list(parser, true);
parser.eat(')', true);
}
} else if (parser.eat(':')) { } else if (parser.eat(':')) {
const name = read_identifier(parser); const name = read_identifier(parser);
@ -277,6 +286,15 @@ function read_selector(parser, inside_pseudo_class = false) {
value, value,
flags flags
}); });
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
// nth of matcher must come before combinator matcher to prevent collision else the '+' in '+2n-1' would be parsed as a combinator
children.push({
type: 'Nth',
value: /**@type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) { } else if (parser.match_regex(REGEX_COMBINATOR_WHITESPACE)) {
parser.allow_whitespace(); parser.allow_whitespace();
const start = parser.index; const start = parser.index;
@ -294,13 +312,6 @@ function read_selector(parser, inside_pseudo_class = false) {
start, start,
end: parser.index end: parser.index
}); });
} else if (inside_pseudo_class && parser.match_regex(REGEX_NTH_OF)) {
children.push({
type: 'Nth',
value: /** @type {string} */ (parser.read(REGEX_NTH_OF)),
start,
end: parser.index
});
} else { } else {
let name = read_identifier(parser); let name = read_identifier(parser);
if (parser.match('|')) { if (parser.match('|')) {

@ -121,15 +121,8 @@ function validate_code(code) {
// based on http://developers.whatwg.org/syntax.html#syntax-tag-omission // based on http://developers.whatwg.org/syntax.html#syntax-tag-omission
const interactive_elements = new Set([ // while `input` is also an interactive element, it is never moved by the browser, so we don't need to check for it
'a', const interactive_elements = new Set(['a', 'button', 'iframe', 'embed', 'select', 'textarea']);
'button',
'iframe',
'embed',
'input',
'select',
'textarea'
]);
/** @type {Record<string, Set<string>>} */ /** @type {Record<string, Set<string>>} */
const disallowed_contents = { const disallowed_contents = {

@ -184,7 +184,9 @@ export default class Selector {
selector.name === 'global' && selector.name === 'global' &&
block.selectors.length !== 1 && block.selectors.length !== 1 &&
(i === block.selectors.length - 1 || (i === block.selectors.length - 1 ||
block.selectors.slice(i + 1).some((s) => s.type !== 'PseudoElementSelector')) block.selectors
.slice(i + 1)
.some((s) => s.type !== 'PseudoElementSelector' && s.type !== 'PseudoClassSelector'))
) { ) {
error(selector, 'invalid-css-global-selector-list'); error(selector, 'invalid-css-global-selector-list');
} }

@ -183,6 +183,10 @@ function get_delegated_event(node, context) {
) { ) {
return non_hoistable; return non_hoistable;
} }
// If we referebnce the index within an each block, then bail-out.
if (binding !== null && binding.initial?.type === 'EachBlock') {
return non_hoistable;
}
if ( if (
binding !== null && binding !== null &&

@ -525,6 +525,8 @@ export function should_proxy_or_freeze(node) {
node.type === 'Literal' || node.type === 'Literal' ||
node.type === 'ArrowFunctionExpression' || node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' || node.type === 'FunctionExpression' ||
node.type === 'UnaryExpression' ||
node.type === 'BinaryExpression' ||
(node.type === 'Identifier' && node.name === 'undefined') (node.type === 'Identifier' && node.name === 'undefined')
) { ) {
return false; return false;

@ -289,7 +289,7 @@ function setup_select_synchronization(value_binding, context) {
* value = $.spread_attributes(element, value, [...]) * value = $.spread_attributes(element, value, [...])
* }); * });
* ``` * ```
* Returns the id of the spread_attribute variable if spread is deemed reactive, `null` otherwise. * Returns the id of the spread_attribute variable if spread isn't isolated, `null` otherwise.
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes * @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context * @param {import('../types.js').ComponentContext} context
* @param {import('#compiler').RegularElement} element * @param {import('#compiler').RegularElement} element
@ -297,7 +297,7 @@ function setup_select_synchronization(value_binding, context) {
* @returns {string | null} * @returns {string | null}
*/ */
function serialize_element_spread_attributes(attributes, context, element, element_id) { function serialize_element_spread_attributes(attributes, context, element, element_id) {
let is_reactive = false; let needs_isolation = false;
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
const values = []; const values = [];
@ -312,18 +312,32 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
} }
is_reactive ||= needs_isolation ||=
attribute.metadata.dynamic || attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
(attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression);
} }
const lowercase_attributes = const lowercase_attributes =
element.metadata.svg || is_custom_element_node(element) ? b.false : b.true; element.metadata.svg || is_custom_element_node(element) ? b.false : b.true;
if (is_reactive) { const isolated = b.stmt(
b.call(
'$.spread_attributes_effect',
element_id,
b.thunk(b.array(values)),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
)
);
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
if (needs_isolation) {
context.state.update_effects.push(isolated);
return null;
} else {
const id = context.state.scope.generate('spread_attributes'); const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id, undefined)); context.state.init.push(b.let(id));
context.state.update.push({ context.state.update.push({
singular: isolated,
grouped: b.stmt( grouped: b.stmt(
b.assignment( b.assignment(
'=', '=',
@ -340,20 +354,6 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
) )
}); });
return id; return id;
} else {
context.state.init.push(
b.stmt(
b.call(
'$.spread_attributes',
element_id,
b.literal(null),
b.array(values),
lowercase_attributes,
b.literal(context.state.analysis.stylesheet.id)
)
)
);
return null;
} }
} }
@ -365,7 +365,7 @@ function serialize_element_spread_attributes(attributes, context, element, eleme
* @param {import('estree').Identifier} element_id * @param {import('estree').Identifier} element_id
* @returns {boolean} * @returns {boolean}
*/ */
function serialize_dynamic_element_spread_attributes(attributes, context, element_id) { function serialize_dynamic_element_attributes(attributes, context, element_id) {
if (attributes.length === 0) { if (attributes.length === 0) {
if (context.state.analysis.stylesheet.id) { if (context.state.analysis.stylesheet.id) {
context.state.init.push( context.state.init.push(
@ -375,6 +375,7 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen
return false; return false;
} }
let needs_isolation = false;
let is_reactive = false; let is_reactive = false;
/** @type {import('estree').Expression[]} */ /** @type {import('estree').Expression[]} */
@ -388,13 +389,31 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen
values.push(/** @type {import('estree').Expression} */ (context.visit(attribute))); values.push(/** @type {import('estree').Expression} */ (context.visit(attribute)));
} }
is_reactive ||= attribute.metadata.dynamic; is_reactive ||=
attribute.metadata.dynamic ||
// objects could contain reactive getters -> play it safe and always assume spread attributes are reactive
attribute.type === 'SpreadAttribute';
needs_isolation ||=
attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression;
} }
if (is_reactive) { const isolated = b.stmt(
b.call(
'$.spread_dynamic_element_attributes_effect',
element_id,
b.thunk(b.array(values)),
b.literal(context.state.analysis.stylesheet.id)
)
);
if (needs_isolation) {
context.state.update_effects.push(isolated);
return false;
} else if (is_reactive) {
const id = context.state.scope.generate('spread_attributes'); const id = context.state.scope.generate('spread_attributes');
context.state.init.push(b.let(id)); context.state.init.push(b.let(id));
context.state.update.push({ context.state.update.push({
singular: isolated,
grouped: b.stmt( grouped: b.stmt(
b.assignment( b.assignment(
'=', '=',
@ -1506,6 +1525,7 @@ function process_children(nodes, parent, { visit, state }) {
expression = b.call('$.sibling', text_id); expression = b.call('$.sibling', text_id);
} }
let is_fragment = false;
for (let i = 0; i < nodes.length; i += 1) { for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i]; const node = nodes[i];
@ -1513,7 +1533,12 @@ function process_children(nodes, parent, { visit, state }) {
sequence.push(node); sequence.push(node);
} else { } else {
if (sequence.length > 0) { if (sequence.length > 0) {
flush_sequence(sequence, true); flush_sequence(sequence, is_fragment);
// Ensure we move to the next sibling for the case where we move reference within a fragment
if (!is_fragment && sequence.length === 1 && sequence[0].type === 'ExpressionTag') {
expression = b.call('$.sibling', expression);
is_fragment = true;
}
sequence = []; sequence = [];
} }
@ -2105,7 +2130,7 @@ export const template_visitors = {
// Always use spread because we don't know whether the element is a custom element or not, // Always use spread because we don't know whether the element is a custom element or not,
// therefore we need to do the "how to set an attribute" logic at runtime. // therefore we need to do the "how to set an attribute" logic at runtime.
const is_attributes_reactive = const is_attributes_reactive =
serialize_dynamic_element_spread_attributes(attributes, inner_context, element_id) !== null; serialize_dynamic_element_attributes(attributes, inner_context, element_id) !== null;
// class/style directives must be applied last since they could override class/style attributes // class/style directives must be applied last since they could override class/style attributes
serialize_class_directives(class_directives, element_id, inner_context, is_attributes_reactive); serialize_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);

@ -41,13 +41,16 @@ export const remove_types = {
TSAsExpression(node, context) { TSAsExpression(node, context) {
return context.visit(node.expression); return context.visit(node.expression);
}, },
TSSatisfiesExpression(node, context) {
return context.visit(node.expression);
},
TSNonNullExpression(node, context) { TSNonNullExpression(node, context) {
return context.visit(node.expression); return context.visit(node.expression);
}, },
TSInterfaceDeclaration(node, context) { TSInterfaceDeclaration() {
return b.empty; return b.empty;
}, },
TSTypeAliasDeclaration(node, context) { TSTypeAliasDeclaration() {
return b.empty; return b.empty;
} }
}; };

@ -65,7 +65,7 @@ export class Scope {
* @param {import('estree').Identifier} node * @param {import('estree').Identifier} node
* @param {import('#compiler').Binding['kind']} kind * @param {import('#compiler').Binding['kind']} kind
* @param {import('#compiler').DeclarationKind} declaration_kind * @param {import('#compiler').DeclarationKind} declaration_kind
* @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration} initial * @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | import('../types/template.js').EachBlock} initial
* @returns {import('#compiler').Binding} * @returns {import('#compiler').Binding}
*/ */
declare(node, kind, declaration_kind, initial = null) { declare(node, kind, declaration_kind, initial = null) {
@ -523,7 +523,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const is_keyed = const is_keyed =
node.key && node.key &&
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const'); scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const', node);
} }
if (node.key) visit(node.key, { scope }); if (node.key) visit(node.key, { scope });
@ -678,7 +678,7 @@ export function set_scope(scopes) {
/** /**
* Returns the name of the rune if the given expression is a `CallExpression` using a rune. * Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {import('estree').Node | null | undefined} node * @param {import('estree').Node | import('../types/template.js').EachBlock | null | undefined} node
* @param {Scope} scope * @param {Scope} scope
* @returns {Runes[number] | null} * @returns {Runes[number] | null}
*/ */

@ -11,7 +11,7 @@ import type { SourceMap } from 'magic-string';
import type { Context } from 'zimmerframe'; import type { Context } from 'zimmerframe';
import type { Scope } from '../phases/scope.js'; import type { Scope } from '../phases/scope.js';
import * as Css from './css.js'; import * as Css from './css.js';
import type { Namespace, SvelteNode } from './template.js'; import type { EachBlock, Namespace, SvelteNode } from './template.js';
/** The return value of `compile` from `svelte/compiler` */ /** The return value of `compile` from `svelte/compiler` */
export interface CompileResult { export interface CompileResult {
@ -269,7 +269,13 @@ export interface Binding {
* What the value was initialized with. * What the value was initialized with.
* For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()` * For destructured props such as `let { foo = 'bar' } = $props()` this is `'bar'` and not `$props()`
*/ */
initial: null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration; initial:
| null
| Expression
| FunctionDeclaration
| ClassDeclaration
| ImportDeclaration
| EachBlock;
is_called: boolean; is_called: boolean;
references: { node: Identifier; path: SvelteNode[] }[]; references: { node: Identifier; path: SvelteNode[] }[];
mutated: boolean; mutated: boolean;

@ -1,6 +1,7 @@
export const EACH_ITEM_REACTIVE = 1; export const EACH_ITEM_REACTIVE = 1;
export const EACH_INDEX_REACTIVE = 1 << 1; export const EACH_INDEX_REACTIVE = 1 << 1;
export const EACH_KEYED = 1 << 2; export const EACH_KEYED = 1 << 2;
export const EACH_PROXIED = 1 << 3;
export const EACH_IS_CONTROLLED = 1 << 3; export const EACH_IS_CONTROLLED = 1 << 3;
export const EACH_IS_ANIMATED = 1 << 4; export const EACH_IS_ANIMATED = 1 << 4;
export const EACH_IS_IMMUTABLE = 1 << 6; export const EACH_IS_IMMUTABLE = 1 << 6;

@ -351,13 +351,8 @@ function reconcile_tracked_array(
) { ) {
var a_blocks = each_block.v; var a_blocks = each_block.v;
const is_computed_key = keys !== null; const is_computed_key = keys !== null;
var is_proxied_array = STATE_SYMBOL in array && /** @type {any} */ (array[STATE_SYMBOL]).i;
var active_transitions = each_block.s; var active_transitions = each_block.s;
if (is_proxied_array) {
flags &= ~EACH_ITEM_REACTIVE;
}
/** @type {number | void} */ /** @type {number | void} */
var a = a_blocks.length; var a = a_blocks.length;

@ -1280,6 +1280,10 @@ function handle_event_propagation(root_element, event) {
const handled_at = event.__root; const handled_at = event.__root;
if (handled_at) { if (handled_at) {
const at_idx = path.indexOf(handled_at); const at_idx = path.indexOf(handled_at);
if (at_idx !== -1 && root_element === document) {
// This is the fallback document listener but the event was already handled -> ignore
return;
}
if (at_idx < path.indexOf(root_element)) { if (at_idx < path.indexOf(root_element)) {
path_idx = at_idx; path_idx = at_idx;
} }
@ -2424,10 +2428,26 @@ function get_setters(element) {
return setters; return setters;
} }
/**
* Like `spread_attributes` but self-contained
* @param {Element & ElementCSSInlineStyle} dom
* @param {() => Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes
* @param {string} css_hash
*/
export function spread_attributes_effect(dom, attrs, lowercase_attributes, css_hash) {
/** @type {Record<string, any> | undefined} */
let current = undefined;
render_effect(() => {
current = spread_attributes(dom, current, attrs(), lowercase_attributes, css_hash);
});
}
/** /**
* Spreads attributes onto a DOM element, taking into account the currently set attributes * Spreads attributes onto a DOM element, taking into account the currently set attributes
* @param {Element & ElementCSSInlineStyle} dom * @param {Element & ElementCSSInlineStyle} dom
* @param {Record<string, unknown> | null} prev * @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs * @param {Record<string, unknown>[]} attrs
* @param {boolean} lowercase_attributes * @param {boolean} lowercase_attributes
* @param {string} css_hash * @param {string} css_hash
@ -2524,18 +2544,30 @@ export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_ha
/** /**
* @param {Element} node * @param {Element} node
* @param {Record<string, unknown> | null} prev * @param {() => Record<string, unknown>[]} attrs
* @param {string} css_hash
*/
export function spread_dynamic_element_attributes_effect(node, attrs, css_hash) {
/** @type {Record<string, any> | undefined} */
let current = undefined;
render_effect(() => {
current = spread_dynamic_element_attributes(node, current, attrs(), css_hash);
});
}
/**
* @param {Element} node
* @param {Record<string, unknown> | undefined} prev
* @param {Record<string, unknown>[]} attrs * @param {Record<string, unknown>[]} attrs
* @param {string} css_hash * @param {string} css_hash
*/ */
export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) {
if (node.tagName.includes('-')) { if (node.tagName.includes('-')) {
const next = object_assign({}, ...attrs); const next = object_assign({}, ...attrs);
if (prev !== null) { for (const key in prev) {
for (const key in prev) { if (!(key in next)) {
if (!(key in next)) { next[key] = null;
next[key] = null;
}
} }
} }
for (const key in next) { for (const key in next) {
@ -2778,6 +2810,7 @@ export function mount(component, options) {
set_current_hydration_fragment(previous_hydration_fragment); set_current_hydration_fragment(previous_hydration_fragment);
} }
const bound_event_listener = handle_event_propagation.bind(null, container); const bound_event_listener = handle_event_propagation.bind(null, container);
const bound_document_event_listener = handle_event_propagation.bind(null, document);
/** @param {Array<string>} events */ /** @param {Array<string>} events */
const event_handle = (events) => { const event_handle = (events) => {
@ -2785,6 +2818,9 @@ export function mount(component, options) {
const event_name = events[i]; const event_name = events[i];
if (!registered_events.has(event_name)) { if (!registered_events.has(event_name)) {
registered_events.add(event_name); registered_events.add(event_name);
// Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
container.addEventListener( container.addEventListener(
event_name, event_name,
bound_event_listener, bound_event_listener,
@ -2794,6 +2830,17 @@ export function mount(component, options) {
} }
: undefined : undefined
); );
// The document listener ensures we catch events that originate from elements that were
// manually moved outside of the container (e.g. via manual portals).
document.addEventListener(
event_name,
bound_document_event_listener,
PassiveDelegatedEvents.includes(event_name)
? {
passive: true
}
: undefined
);
} }
} }
}; };

@ -64,7 +64,7 @@ let current_dependencies = null;
let current_dependencies_index = 0; let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */ /** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null; let current_untracked_writes = null;
/** @type {null | import('./types.js').Signal} */ /** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null; let last_inspected_signal = null;
/** If `true`, `get`ting the signal should not register it as a dependency */ /** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false; export let current_untracking = false;
@ -81,7 +81,7 @@ let captured_signals = new Set();
/** @type {Function | null} */ /** @type {Function | null} */
let inspect_fn = null; let inspect_fn = null;
/** @type {Array<import('./types.js').SourceSignal & import('./types.js').SourceSignalDebug>} */ /** @type {Array<import('./types.js').SignalDebug>} */
let inspect_captured_signals = []; let inspect_captured_signals = [];
// Handle rendering tree blocks and anchors // Handle rendering tree blocks and anchors
@ -127,7 +127,6 @@ export function batch_inspect(target, prop, receiver) {
} finally { } finally {
is_batching_effect = previously_batching_effect; is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null) { if (last_inspected_signal !== null) {
// @ts-expect-error
for (const fn of last_inspected_signal.inspect) fn(); for (const fn of last_inspected_signal.inspect) fn();
last_inspected_signal = null; last_inspected_signal = null;
} }
@ -349,7 +348,21 @@ function execute_signal_fn(signal) {
if (current_dependencies !== null) { if (current_dependencies !== null) {
let i; let i;
remove_consumer(signal, current_dependencies_index, false); if (dependencies !== null) {
const dep_length = dependencies.length;
// If we have more than 16 elements in the array then use a Set for faster performance
// TODO: evaluate if we should always just use a Set or not here?
const current_dependencies_set = dep_length > 16 ? new Set(current_dependencies) : null;
for (i = current_dependencies_index; i < dep_length; i++) {
const dependency = dependencies[i];
if (
(current_dependencies_set !== null && !current_dependencies_set.has(dependency)) ||
!current_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency, false);
}
}
}
if (dependencies !== null && current_dependencies_index > 0) { if (dependencies !== null && current_dependencies_index > 0) {
dependencies.length = current_dependencies_index + current_dependencies.length; dependencies.length = current_dependencies_index + current_dependencies.length;
@ -365,16 +378,17 @@ function execute_signal_fn(signal) {
if (!current_skip_consumer) { if (!current_skip_consumer) {
for (i = current_dependencies_index; i < dependencies.length; i++) { for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i]; const dependency = dependencies[i];
const consumers = dependency.c;
if (dependency.c === null) { if (consumers === null) {
dependency.c = [signal]; dependency.c = [signal];
} else { } else if (consumers[consumers.length - 1] !== signal) {
dependency.c.push(signal); consumers.push(signal);
} }
} }
} }
} else if (dependencies !== null && current_dependencies_index < dependencies.length) { } else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_consumer(signal, current_dependencies_index, false); remove_consumers(signal, current_dependencies_index, false);
dependencies.length = current_dependencies_index; dependencies.length = current_dependencies_index;
} }
return res; return res;
@ -390,6 +404,40 @@ function execute_signal_fn(signal) {
} }
} }
/**
* @template V
* @param {import('./types.js').ComputationSignal<V>} signal
* @param {import('./types.js').Signal<V>} dependency
* @param {boolean} remove_unowned
* @returns {void}
*/
function remove_consumer(signal, dependency, remove_unowned) {
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumers(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
}
/** /**
* @template V * @template V
* @param {import('./types.js').ComputationSignal<V>} signal * @param {import('./types.js').ComputationSignal<V>} signal
@ -397,36 +445,13 @@ function execute_signal_fn(signal) {
* @param {boolean} remove_unowned * @param {boolean} remove_unowned
* @returns {void} * @returns {void}
*/ */
function remove_consumer(signal, start_index, remove_unowned) { function remove_consumers(signal, start_index, remove_unowned) {
const dependencies = signal.d; const dependencies = signal.d;
if (dependencies !== null) { if (dependencies !== null) {
let i; let i;
for (i = start_index; i < dependencies.length; i++) { for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i]; const dependency = dependencies[i];
const consumers = dependency.c; remove_consumer(signal, dependency, remove_unowned);
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
}
}
}
if (remove_unowned && consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumer(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
} }
} }
} }
@ -446,7 +471,7 @@ function destroy_references(signal) {
if ((reference.f & IS_EFFECT) !== 0) { if ((reference.f & IS_EFFECT) !== 0) {
destroy_signal(reference); destroy_signal(reference);
} else { } else {
remove_consumer(reference, 0, true); remove_consumers(reference, 0, true);
reference.d = null; reference.d = null;
} }
} }
@ -711,8 +736,7 @@ function update_derived(signal, force_schedule) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect && force_schedule) { if (DEV && signal.inspect && force_schedule) {
// @ts-expect-error for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
for (const fn of signal.inspect) fn();
} }
} }
} }
@ -815,8 +839,7 @@ export function unsubscribe_on_destroy(stores) {
export function get(signal) { export function get(signal) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect && inspect_fn) { if (DEV && signal.inspect && inspect_fn) {
// @ts-expect-error /** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
signal.inspect.add(inspect_fn);
// @ts-expect-error // @ts-expect-error
inspect_captured_signals.push(signal); inspect_captured_signals.push(signal);
} }
@ -841,10 +864,16 @@ export function get(signal) {
!(unowned && current_effect !== null) !(unowned && current_effect !== null)
) { ) {
current_dependencies_index++; current_dependencies_index++;
} else if (current_dependencies === null) { } else if (
current_dependencies = [signal]; dependencies === null ||
} else if (signal !== current_dependencies[current_dependencies.length - 1]) { current_dependencies_index === 0 ||
current_dependencies.push(signal); dependencies[current_dependencies_index - 1] !== signal
) {
if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
}
} }
if ( if (
current_untracked_writes !== null && current_untracked_writes !== null &&
@ -1079,10 +1108,9 @@ export function set_signal_value(signal, value) {
// @ts-expect-error // @ts-expect-error
if (DEV && signal.inspect) { if (DEV && signal.inspect) {
if (is_batching_effect) { if (is_batching_effect) {
last_inspected_signal = signal; last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
} else { } else {
// @ts-expect-error for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
for (const fn of signal.inspect) fn();
} }
} }
} }
@ -1098,7 +1126,7 @@ export function destroy_signal(signal) {
const destroy = signal.y; const destroy = signal.y;
const flags = signal.f; const flags = signal.f;
destroy_references(signal); destroy_references(signal);
remove_consumer(signal, 0, true); remove_consumers(signal, 0, true);
signal.i = signal.i =
signal.r = signal.r =
signal.y = signal.y =
@ -1804,6 +1832,37 @@ function deep_read(value, visited = new Set()) {
} }
} }
/**
* Like `unstate`, but recursively traverses into normal arrays/objects to find potential states in them.
* @param {any} value
* @param {Map<any, any>} visited
* @returns {any}
*/
function deep_unstate(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) {
const unstated = unstate(value);
if (unstated !== value) {
visited.set(value, unstated);
return unstated;
}
let contains_unstated = false;
/** @type {any} */
const nested_unstated = Array.isArray(value) ? [] : {};
for (let key in value) {
const result = deep_unstate(value[key], visited);
nested_unstated[key] = result;
if (result !== value[key]) {
contains_unstated = true;
}
}
visited.set(value, contains_unstated ? nested_unstated : value);
}
return visited.get(value) ?? value;
}
// TODO remove in a few versions, before 5.0 at the latest // TODO remove in a few versions, before 5.0 at the latest
let warned_inspect_changed = false; let warned_inspect_changed = false;
@ -1817,7 +1876,7 @@ export function inspect(get_value, inspect = console.log) {
pre_effect(() => { pre_effect(() => {
const fn = () => { const fn = () => {
const value = get_value().map(unstate); const value = get_value().map((v) => deep_unstate(v));
if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) { if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.warn( console.warn(

@ -108,6 +108,8 @@ export type ComputationSignal<V = unknown> = {
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>; export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
export type SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
export type EffectSignal = ComputationSignal<null | (() => void)>; export type EffectSignal = ComputationSignal<null | (() => void)>;
export type MaybeSignal<T = unknown> = T | Signal<T>; export type MaybeSignal<T = unknown> = T | Signal<T>;

@ -20,3 +20,13 @@ export function beforeUpdate() {}
/** @returns {void} */ /** @returns {void} */
export function afterUpdate() {} export function afterUpdate() {}
/**
* @template T
* @param {T} value
* @returns {T}
*/
export function unstate(value) {
// There's no signals/proxies on the server, so just return the value
return value;
}

@ -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.27'; export const VERSION = '5.0.0-next.29';
export const PUBLIC_VERSION = '5'; export const PUBLIC_VERSION = '5';

@ -0,0 +1,4 @@
<!--should not error out-->
<script lang="ts">
let count: number;
</script>

@ -0,0 +1,132 @@
{
"css": null,
"js": [],
"start": 0,
"end": 27,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Comment",
"start": 0,
"end": 27,
"data": "should not error out",
"ignores": []
},
{
"type": "Text",
"start": 27,
"end": 28,
"raw": "\n",
"data": "\n"
}
],
"transparent": false
},
"options": null,
"instance": {
"type": "Script",
"start": 28,
"end": 76,
"context": "default",
"content": {
"type": "Program",
"start": 46,
"end": 67,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 4,
"column": 0
}
},
"body": [
{
"type": "VariableDeclaration",
"start": 48,
"end": 66,
"loc": {
"start": {
"line": 3,
"column": 1
},
"end": {
"line": 3,
"column": 19
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 52,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 5
},
"end": {
"line": 3,
"column": 18
}
},
"id": {
"type": "Identifier",
"start": 52,
"end": 18,
"loc": {
"start": {
"line": 3,
"column": 5
},
"end": {
"line": 3,
"column": 18
}
},
"name": "count",
"typeAnnotation": {
"type": "TSTypeAnnotation",
"start": 57,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 10
},
"end": {
"line": 3,
"column": 18
}
},
"typeAnnotation": {
"type": "TSNumberKeyword",
"start": 59,
"end": 65,
"loc": {
"start": {
"line": 3,
"column": 12
},
"end": {
"line": 3,
"column": 18
}
}
}
}
},
"init": null
}
],
"kind": "let"
}
],
"sourceType": "module"
}
}
}

@ -28,6 +28,18 @@
} }
h1:global(nav) { h1:global(nav) {
background: red; background: red;
}
h1:nth-of-type(10n+1){
background: red;
}
h1:nth-of-type(-2n+3){
background: red;
}
h1:nth-of-type(+12){
background: red;
}
h1:nth-of-type(+3n){
background: red;
} }
</style> </style>

@ -2,7 +2,7 @@
"css": { "css": {
"type": "Style", "type": "Style",
"start": 0, "start": 0,
"end": 586, "end": 806,
"attributes": [], "attributes": [],
"children": [ "children": [
{ {
@ -495,19 +495,19 @@
"name": "nth-child", "name": "nth-child",
"args": { "args": {
"type": "SelectorList", "type": "SelectorList",
"start": 476, "start": 485,
"end": 491, "end": 486,
"children": [ "children": [
{ {
"type": "Selector", "type": "Selector",
"start": 476, "start": 485,
"end": 491, "end": 486,
"children": [ "children": [
{ {
"type": "Nth", "type": "Nth",
"value": "\n n\n ", "value": "n",
"start": 476, "start": 485,
"end": 491 "end": 486
} }
] ]
} }
@ -601,32 +601,292 @@
}, },
"start": 530, "start": 530,
"end": 577 "end": 577
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 580,
"end": 601,
"children": [
{
"type": "Selector",
"start": 580,
"end": 601,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 580,
"end": 582
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 595,
"end": 600,
"children": [
{
"type": "Selector",
"start": 595,
"end": 600,
"children": [
{
"type": "Nth",
"value": "10n+1",
"start": 595,
"end": 600
}
]
}
]
},
"start": 582,
"end": 601
}
]
}
]
},
"block": {
"type": "Block",
"start": 601,
"end": 633,
"children": [
{
"type": "Declaration",
"start": 611,
"end": 626,
"property": "background",
"value": "red"
}
]
},
"start": 580,
"end": 633
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 636,
"end": 657,
"children": [
{
"type": "Selector",
"start": 636,
"end": 657,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 636,
"end": 638
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 651,
"end": 656,
"children": [
{
"type": "Selector",
"start": 651,
"end": 656,
"children": [
{
"type": "Nth",
"value": "-2n+3",
"start": 651,
"end": 656
}
]
}
]
},
"start": 638,
"end": 657
}
]
}
]
},
"block": {
"type": "Block",
"start": 657,
"end": 689,
"children": [
{
"type": "Declaration",
"start": 667,
"end": 682,
"property": "background",
"value": "red"
}
]
},
"start": 636,
"end": 689
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 692,
"end": 711,
"children": [
{
"type": "Selector",
"start": 692,
"end": 711,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 692,
"end": 694
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 707,
"end": 710,
"children": [
{
"type": "Selector",
"start": 707,
"end": 710,
"children": [
{
"type": "Nth",
"value": "+12",
"start": 707,
"end": 710
}
]
}
]
},
"start": 694,
"end": 711
}
]
}
]
},
"block": {
"type": "Block",
"start": 711,
"end": 743,
"children": [
{
"type": "Declaration",
"start": 721,
"end": 736,
"property": "background",
"value": "red"
}
]
},
"start": 692,
"end": 743
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 746,
"end": 765,
"children": [
{
"type": "Selector",
"start": 746,
"end": 765,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 746,
"end": 748
},
{
"type": "PseudoClassSelector",
"name": "nth-of-type",
"args": {
"type": "SelectorList",
"start": 761,
"end": 764,
"children": [
{
"type": "Selector",
"start": 761,
"end": 764,
"children": [
{
"type": "Nth",
"value": "+3n",
"start": 761,
"end": 764
}
]
}
]
},
"start": 748,
"end": 765
}
]
}
]
},
"block": {
"type": "Block",
"start": 765,
"end": 797,
"children": [
{
"type": "Declaration",
"start": 775,
"end": 790,
"property": "background",
"value": "red"
}
]
},
"start": 746,
"end": 797
} }
], ],
"content": { "content": {
"start": 7, "start": 7,
"end": 578, "end": 798,
"styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n" "styles": "\n /* test that all these are parsed correctly */\n\th1:nth-of-type(2n+1){\n background: red;\n }\n h1:nth-child(-n + 3 of li.important) {\n background: red;\n }\n h1:nth-child(1) {\n background: red;\n }\n h1:nth-child(p) {\n background: red;\n }\n h1:nth-child(n+7) {\n background: red;\n }\n h1:nth-child(even) {\n background: red;\n }\n h1:nth-child(odd) {\n background: red;\n }\n h1:nth-child(\n n\n ) {\n background: red;\n }\n h1:global(nav) {\n background: red;\n }\n\t\th1:nth-of-type(10n+1){\n background: red;\n }\n\t\th1:nth-of-type(-2n+3){\n background: red;\n }\n\t\th1:nth-of-type(+12){\n background: red;\n }\n\t\th1:nth-of-type(+3n){\n background: red;\n }\n"
} }
}, },
"js": [], "js": [],
"start": 588, "start": 808,
"end": 600, "end": 820,
"type": "Root", "type": "Root",
"fragment": { "fragment": {
"type": "Fragment", "type": "Fragment",
"nodes": [ "nodes": [
{ {
"type": "Text", "type": "Text",
"start": 586, "start": 806,
"end": 588, "end": 808,
"raw": "\n\n", "raw": "\n\n",
"data": "\n\n" "data": "\n\n"
}, },
{ {
"type": "RegularElement", "type": "RegularElement",
"start": 588, "start": 808,
"end": 600, "end": 820,
"name": "h1", "name": "h1",
"attributes": [], "attributes": [],
"fragment": { "fragment": {
@ -634,8 +894,8 @@
"nodes": [ "nodes": [
{ {
"type": "Text", "type": "Text",
"start": 592, "start": 812,
"end": 595, "end": 815,
"raw": "Foo", "raw": "Foo",
"data": "Foo" "data": "Foo"
} }

@ -0,0 +1,24 @@
<style>
/* test that all these are parsed correctly */
::view-transition-old(x-y) {
color: red;
}
:global(::view-transition-old(x-y)) {
color: red;
}
::highlight(rainbow-color-1) {
color: red;
}
custom-element::part(foo) {
color: red;
}
::slotted(.content) {
color: red;
}
:is( /*button*/
button, /*p after h1*/
h1 + p
){
color: red;
}
</style>

@ -0,0 +1,330 @@
{
"css": {
"type": "Style",
"start": 0,
"end": 386,
"attributes": [],
"children": [
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 60,
"end": 86,
"children": [
{
"type": "Selector",
"start": 60,
"end": 86,
"children": [
{
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 60,
"end": 81
}
]
}
]
},
"block": {
"type": "Block",
"start": 88,
"end": 109,
"children": [
{
"type": "Declaration",
"start": 92,
"end": 102,
"property": "color",
"value": "red"
}
]
},
"start": 60,
"end": 109
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 111,
"end": 146,
"children": [
{
"type": "Selector",
"start": 111,
"end": 146,
"children": [
{
"type": "PseudoClassSelector",
"name": "global",
"args": {
"type": "SelectorList",
"start": 119,
"end": 145,
"children": [
{
"type": "Selector",
"start": 119,
"end": 145,
"children": [
{
"type": "PseudoElementSelector",
"name": "view-transition-old",
"start": 119,
"end": 140
}
]
}
]
},
"start": 111,
"end": 146
}
]
}
]
},
"block": {
"type": "Block",
"start": 148,
"end": 169,
"children": [
{
"type": "Declaration",
"start": 152,
"end": 162,
"property": "color",
"value": "red"
}
]
},
"start": 111,
"end": 169
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 171,
"end": 199,
"children": [
{
"type": "Selector",
"start": 171,
"end": 199,
"children": [
{
"type": "PseudoElementSelector",
"name": "highlight",
"start": 171,
"end": 182
}
]
}
]
},
"block": {
"type": "Block",
"start": 200,
"end": 218,
"children": [
{
"type": "Declaration",
"start": 204,
"end": 214,
"property": "color",
"value": "red"
}
]
},
"start": 171,
"end": 218
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 220,
"end": 245,
"children": [
{
"type": "Selector",
"start": 220,
"end": 245,
"children": [
{
"type": "TypeSelector",
"name": "custom-element",
"start": 220,
"end": 234
},
{
"type": "PseudoElementSelector",
"name": "part",
"start": 234,
"end": 240
}
]
}
]
},
"block": {
"type": "Block",
"start": 246,
"end": 264,
"children": [
{
"type": "Declaration",
"start": 250,
"end": 260,
"property": "color",
"value": "red"
}
]
},
"start": 220,
"end": 264
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 266,
"end": 285,
"children": [
{
"type": "Selector",
"start": 266,
"end": 285,
"children": [
{
"type": "PseudoElementSelector",
"name": "slotted",
"start": 266,
"end": 275
}
]
}
]
},
"block": {
"type": "Block",
"start": 286,
"end": 304,
"children": [
{
"type": "Declaration",
"start": 290,
"end": 300,
"property": "color",
"value": "red"
}
]
},
"start": 266,
"end": 304
},
{
"type": "Rule",
"prelude": {
"type": "SelectorList",
"start": 306,
"end": 359,
"children": [
{
"type": "Selector",
"start": 306,
"end": 359,
"children": [
{
"type": "PseudoClassSelector",
"name": "is",
"args": {
"type": "SelectorList",
"start": 324,
"end": 355,
"children": [
{
"type": "Selector",
"start": 324,
"end": 330,
"children": [
{
"type": "TypeSelector",
"name": "button",
"start": 324,
"end": 330
}
]
},
{
"type": "Selector",
"start": 349,
"end": 355,
"children": [
{
"type": "TypeSelector",
"name": "h1",
"start": 349,
"end": 351
},
{
"type": "Combinator",
"name": "+",
"start": 352,
"end": 353
},
{
"type": "TypeSelector",
"name": "p",
"start": 354,
"end": 355
}
]
}
]
},
"start": 306,
"end": 359
}
]
}
]
},
"block": {
"type": "Block",
"start": 359,
"end": 377,
"children": [
{
"type": "Declaration",
"start": 363,
"end": 373,
"property": "color",
"value": "red"
}
]
},
"start": 306,
"end": 377
}
],
"content": {
"start": 7,
"end": 378,
"styles": "\n /* test that all these are parsed correctly */\n\t::view-transition-old(x-y) {\n\t\tcolor: red;\n }\n\t:global(::view-transition-old(x-y)) {\n\t\tcolor: red;\n }\n\t::highlight(rainbow-color-1) {\n\t\tcolor: red;\n\t}\n\tcustom-element::part(foo) {\n\t\tcolor: red;\n\t}\n\t::slotted(.content) {\n\t\tcolor: red;\n\t}\n\t:is( /*button*/\n\t\tbutton, /*p after h1*/\n\t\th1 + p\n\t\t){\n\t\tcolor: red;\n\t}\n"
}
},
"js": [],
"start": null,
"end": null,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [],
"transparent": false
},
"options": null
}

@ -0,0 +1,62 @@
import { test } from '../../test';
export default test({
html: `
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
`,
async test({ assert, target }) {
const [b1, b2, b3, b4] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="red">red</button>
<button class="red">red</button>
<button class="red">red</button>
`
);
b2?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="red">red</button>
<button class="red">red</button>
`
);
b3?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="red">red</button>
`
);
b4?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
<button class="blue">blue</button>
`
);
}
});

@ -0,0 +1,24 @@
<script>
let tag = $state('button');
let values = $state({ a: 'red', b: 'red', c: 'red', d: 'red' });
let count = 0;
const factory = (name) => {
count++;
// check that spread effects are isolated from each other
if (count > 8) throw new Error('too many calls');
return {
class: values[name],
onclick: () => {
values[name] = 'blue';
}
}
}
</script>
<button {...factory('a')}>{values.a}</button>
<button {...factory('b')}>{values.b}</button>
<svelte:element this={tag} {...factory('c')}>{values.c}</svelte:element>
<svelte:element this={tag} {...factory('d')}>{values.d}</svelte:element>

@ -0,0 +1,24 @@
import { test } from '../../test';
export default test({
html: `
<div style="color: red;"></div><div class="red"></div><div class="red"></div>
<div style="color: red;"></div><div class="red"></div><div class="red"></div>
<button>toggle</button
`,
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
`
<div class="blue" style="color: blue;"></div><div class="blue"></div><div class="blue"></div>
<div class="blue" style="color: blue;"></div><div class="blue"></div><div class="blue"></div>
<button>toggle</button
`
);
}
});

@ -1,5 +1,6 @@
<script> <script>
let value = $state('red'); let value = $state('red');
let tag = $state('div');
const getValue = () => { const getValue = () => {
return value; return value;
@ -10,9 +11,19 @@
const getSpread = () => { const getSpread = () => {
return { class: value }; return { class: value };
} }
const props = {
get class() {
return value;
}
}
</script> </script>
<div class:blue={getClass()} style:color={getValue()}></div> <div class:blue={getClass()} style:color={getValue()}></div>
<div {...getSpread()}></div> <div {...getSpread()}></div>
<button on:click={() => value = 'blue'}>toggle</button> <div {...props}></div>
<svelte:element this={tag} class:blue={getClass()} style:color={getValue()}></svelte:element>
<svelte:element this={tag} {...getSpread()}></svelte:element>
<svelte:element this={tag} {...props}></svelte:element>
<button on:click={() => value = 'blue'}>toggle</button>

@ -0,0 +1,9 @@
<script>
const { item } = $props();
</script>
<div>
{#if item}
{item.length}
{/if}
</div>

@ -0,0 +1,26 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, target, component }) {
const [b1, b2] = target.querySelectorAll('button');
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b2.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div>5</div><div>3</div><button>set null</button><button>set object</button'
);
flushSync(() => {
b1.click();
});
assert.htmlEqual(
target.innerHTML,
'<div>5</div><div></div><div>3</div><button>set null</button><button>set object</button'
);
}
});

@ -0,0 +1,12 @@
<script>
import Component from './Component.svelte';
let items = $state(['hello', 'world', 'bye']);
</script>
{#each items as item}
<Component {item} />
{/each}
<button onclick={() => (items[1] = null)}> set null </button>
<button onclick={() => (items[1] = 'hello')}> set object </button>

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

@ -0,0 +1,9 @@
<script>
let array = $state(['A', 'B', 'C']);
</script>
<p>
{#each array as a}
{a}<br/>
{/each}
</p>

@ -0,0 +1,39 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<p>test costs $1</p><p>test 2 costs $2</p><p>test costs $1</p><p>test 2 costs $2</p><button>add</button><button>change</button><button>reload</button>`,
skip_if_ssr: 'permanent',
skip_if_hydrate: 'permanent',
async test({ assert, target }) {
const [btn1, btn2, btn3] = target.querySelectorAll('button');
flushSync(() => {
btn2.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test costs $1</p><p>test 2 costs $2000</p><button>add</button><button>change</button><button>reload</button>`
);
flushSync(() => {
btn1.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test 3 costs $3</p><p>test costs $1</p><p>test 2 costs $2000</p><p>test 3 costs $3</p><button>add</button><button>change</button><button>reload</button>`
);
flushSync(() => {
btn3.click();
});
assert.htmlEqual(
target.innerHTML,
`<p>test costs $1</p><p>test 2 costs $2000</p><p>test costs $1</p><p>test 2 costs $2000</p><button>add</button><button>change</button><button>reload</button>`
);
}
});

@ -0,0 +1,54 @@
<script>
let data = $state({ items: [] });
function fetchData() {
data = {
items: [{
id: 1,
price: 1,
name: 'test'
}, {
id: 2,
price: 2,
name: 'test 2'
}]
};
}
fetchData();
function copyItems(original) {
return [...original.map((item) => ({ ...item }))];
}
let items = $state();
$effect(() => {
items = copyItems(data.items);
});
</script>
{#each items as item}
<p>{item.name} costs ${item.price}</p>
{/each}
{#each items as item (item.id)}
<p>{item.name} costs ${item.price}</p>
{/each}
<button onclick={() => {
items.push({
id: 3,
price: 3,
name: 'test 3'
})
}}>add</button>
<button onclick={() => {
data.items[1].price = 2000
}}>change</button>
<button onclick={() => {
fetchData();
}}>reload</button>

@ -1,16 +0,0 @@
import { test } from '../../test';
export default test({
html: `<div style="color: red;"></div><div class="red"></div><button>toggle</button`,
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1?.click();
await Promise.resolve();
assert.htmlEqual(
target.innerHTML,
'<div class="blue" style="color: blue;"></div><div class="blue"></div><button>toggle</button>'
);
}
});

@ -0,0 +1,23 @@
import { test } from '../../test';
// Tests that event delegation still works when the element with the event listener is moved outside the container
export default test({
async test({ assert, target }) {
const btn1 = target.parentElement?.querySelector('button');
const btn2 = target.querySelector('button');
btn1?.click();
await Promise.resolve();
assert.htmlEqual(
target.parentElement?.innerHTML ?? '',
'<main><div><button>clicks: 1</button></div></main><button>clicks: 1</button>'
);
btn2?.click();
await Promise.resolve();
assert.htmlEqual(
target.parentElement?.innerHTML ?? '',
'<main><div><button>clicks: 2</button></div></main><button>clicks: 2</button>'
);
}
});

@ -0,0 +1,12 @@
<script>
let count = $state(0);
let el;
$effect(() => {
document.getElementsByTagName('body')[0].appendChild(el);
})
</script>
<div>
<button bind:this={el} onclick={() => count++}>clicks: {count}</button>
<button onclick={() => count++}>clicks: {count}</button>
</div>

@ -0,0 +1,40 @@
import { test } from '../../test';
/**
* @type {any[]}
*/
let log;
/**
* @type {typeof console.log}}
*/
let original_log;
export default test({
compileOptions: {
dev: true
},
before_test() {
log = [];
original_log = console.log;
console.log = (...v) => {
log.push(...v);
};
},
after_test() {
console.log = original_log;
},
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.deepEqual(log, [
'init',
{ x: { count: 0 } },
[{ count: 0 }],
'update',
{ x: { count: 1 } },
[{ count: 1 }]
]);
}
});

@ -0,0 +1,7 @@
<script>
let x = $state({count: 0});
$inspect({x}, [x]);
</script>
<button on:click={() => x.count++}>{x.count}</button>

@ -10,6 +10,9 @@
import { type Bar, type Baz } from './types'; import { type Bar, type Baz } from './types';
let count = $state(0); let count = $state(0);
const person = {
message: 'goodbye'
} satisfies Goodbye;
</script> </script>
<button <button

@ -9,16 +9,18 @@ export default function Function_prop_no_getter($$anchor, $$props) {
let count = $.source(0); let count = $.source(0);
function onmouseup() { function onmouseup() {
$.set(count, $.proxy($.get(count) + 2)); $.set(count, $.get(count) + 2);
} }
const plusOne = (num) => num + 1;
/* Init */ /* Init */
var fragment = $.comment($$anchor); var fragment = $.comment($$anchor);
var node = $.child_frag(fragment); var node = $.child_frag(fragment);
Button(node, { Button(node, {
onmousedown: () => $.set(count, $.proxy($.get(count) + 1)), onmousedown: () => $.set(count, $.get(count) + 1),
onmouseup, onmouseup,
onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))),
children: ($$anchor, $$slotProps) => { children: ($$anchor, $$slotProps) => {
/* Init */ /* Init */
var node_1 = $.space($$anchor); var node_1 = $.space($$anchor);

@ -11,6 +11,7 @@ export default function Function_prop_no_getter($$payload, $$props) {
count += 2; count += 2;
} }
const plusOne = (num) => num + 1;
const anchor = $.create_anchor($$payload); const anchor = $.create_anchor($$payload);
$$payload.out += `${anchor}`; $$payload.out += `${anchor}`;
@ -18,6 +19,7 @@ export default function Function_prop_no_getter($$payload, $$props) {
Button($$payload, { Button($$payload, {
onmousedown: () => count += 1, onmousedown: () => count += 1,
onmouseup, onmouseup,
onmouseenter: () => count = plusOne(count),
children: ($$payload, $$slotProps) => { children: ($$payload, $$slotProps) => {
$$payload.out += `clicks: ${$.escape(count)}`; $$payload.out += `clicks: ${$.escape(count)}`;
} }

@ -4,8 +4,10 @@
function onmouseup() { function onmouseup() {
count += 2; count += 2;
} }
const plusOne = (num) => num + 1;
</script> </script>
<Button onmousedown={() => count += 1} {onmouseup}> <Button onmousedown={() => count += 1} {onmouseup} onmouseenter={() => count = plusOne(count)}>
clicks: {count} clicks: {count}
</Button> </Button>

@ -3,11 +3,11 @@
"code": "invalid-css-global-placement", "code": "invalid-css-global-placement",
"message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle",
"start": { "start": {
"line": 2, "line": 5,
"column": 6 "column": 6
}, },
"end": { "end": {
"line": 2, "line": 5,
"column": 19 "column": 19
} }
} }

@ -1,4 +1,7 @@
<style> <style>
.foo :global(.bar):first-child {
color: red;
}
.foo :global(.bar):first-child .baz { .foo :global(.bar):first-child .baz {
color: red; color: red;
} }

File diff suppressed because it is too large Load Diff

@ -69,8 +69,8 @@ importers:
specifier: ^8.10.0 specifier: ^8.10.0
version: 8.11.2 version: 8.11.2
acorn-typescript: acorn-typescript:
specifier: ^1.4.11 specifier: ^1.4.13
version: 1.4.11(acorn@8.11.2) version: 1.4.13(acorn@8.11.2)
aria-query: aria-query:
specifier: ^5.3.0 specifier: ^5.3.0
version: 5.3.0 version: 5.3.0
@ -121,8 +121,8 @@ importers:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5 version: 1.0.5
dts-buddy: dts-buddy:
specifier: ^0.4.0 specifier: ^0.4.3
version: 0.4.0(typescript@5.2.2) version: 0.4.3(typescript@5.2.2)
esbuild: esbuild:
specifier: ^0.19.2 specifier: ^0.19.2
version: 0.19.5 version: 0.19.5
@ -2748,8 +2748,8 @@ packages:
acorn: 8.11.2 acorn: 8.11.2
dev: true dev: true
/acorn-typescript@1.4.11(acorn@8.11.2): /acorn-typescript@1.4.13(acorn@8.11.2):
resolution: {integrity: sha512-cRGgp+4HMxMZAiMS61ZmQ3iuU/+A4g4ZYZsyLZdmvrEVN/TOwfJ40rPWcLqi3H5ut75SYAdOOJj6QGCcrkK57w==} resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==}
peerDependencies: peerDependencies:
acorn: '>=8.9.0' acorn: '>=8.9.0'
dependencies: dependencies:
@ -3646,11 +3646,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
dev: true dev: true
/dts-buddy@0.4.0(typescript@5.2.2): /dts-buddy@0.4.3(typescript@5.2.2):
resolution: {integrity: sha512-L8sHp1mmpufZhz/+HLIA40hJG6T937rsWhEIED2W2QMbGSpdg1G5pc2EK1WLKvmd1DvGZ0Qs8uoeSUaqK5mqkw==} resolution: {integrity: sha512-vytwDCQAj8rqYPbGsrjiOCRv3O2ipwyUwSc5/II1MpS/Eq6KNZNkGU1djOA31nL7jh7092W/nwbwZHCKedf8Vw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
typescript: '>=5.0.4 <5.3' typescript: '>=5.0.4 <5.4'
dependencies: dependencies:
'@jridgewell/source-map': 0.3.5 '@jridgewell/source-map': 0.3.5
'@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/sourcemap-codec': 1.4.15

@ -156,7 +156,7 @@ Within the template, snippets are values just like any other. As such, they can
<Table data={fruits} {header} {row} /> <Table data={fruits} {header} {row} />
``` ```
As an authoring convenience, snippets declare directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)): As an authoring convenience, snippets declared directly _inside_ a component implicitly become props _on_ the component ([demo](/#H4sIAAAAAAAAE41Sy27bMBD8lYVcwHYrW4kBXxRFaP-htzgHSqQsojLJkuu2BqF_74qUrfhxCHQRh7MzO9z1SSM74ZL8zSeKHUSSJz-MSdIET2Y4uD-iQ0Fnp4-2HpDC1VYaLHdqh_JgtEX4yapOQGP1AebrLJzWsXD-QjQi1lo5JMZRooNXeBuwHXoYLHOYM2OoiXkKv_GUwzYFY2VNFxvo0xtqxRR9F-7z04X8fE-uW2GtnJQ3E_tpvYV-oL9Ti0U2hVJFjMMZslcfW-5DWj9zShojEFrBuLCLZR_9CmzLQCwy-psw8rxBgvkNhhpZd8F8NppE7Stbq_8u-GTKS8_XQ9Keqnl5BZP1AzTYP2bDV7i7_9hLEeda0iocNJeNFDzJ0R5Fn142JzA-uzsdBfLhldPxPdMhIPS0H1-M1cYtlnejwdBDfBXZjHXTFOg4BhuOtvTfrVDEmAZG2ew5ezYV-Ew2fVzVAivNTyPHzwSr29AlMAe8f6g-zuWDts-GusAmdBSkv3P7qnB4GpMEEHwsRPEPV6yTe5VDJxp8iXClLRmtnGG1VHva3oCPHQd9QJsrbFd1Kzu-2Khvz8uzZsXqX3urj4rnMBNCXNUG83zf6Yp1C2yXKdxA_KJjGOfRfb0Vh7MKDShEuV-M9_4_nq6svF4EAAA=)):
```svelte ```svelte
<!-- this is semantically the same as the above --> <!-- this is semantically the same as the above -->

@ -107,3 +107,7 @@ In Svelte 4, it was possible to specify event attributes on HTML elements as a s
``` ```
This is not recommended, and is no longer possible in Svelte 5, where properties like `onclick` replace `on:click` as the mechanism for adding [event handlers](/docs/event-handlers). This is not recommended, and is no longer possible in Svelte 5, where properties like `onclick` replace `on:click` as the mechanism for adding [event handlers](/docs/event-handlers).
### `null` and `undefined` become the empty string
In Svelte 4, `null` and `undefined` were printed as the corresponding string. In 99 out of 100 cases you want this to become the empty string instead, which is also what most other frameworks out there do. Therefore, in Svelte 5, `null` and `undefined` become the empty string.

Loading…
Cancel
Save