Merge branch 'main' into destructure-signal-await

pull/9962/head
Nguyen Tran 3 years ago
commit 9b010742fa

@ -0,0 +1,5 @@
---
'svelte': patch
---
A transition's parameters are now evaluated when the transition is initialized.

@ -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
---
feat: add `$state.frozen` rune

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
Fix issue with assigning prop values as defaults of other props

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
Fix interopability between backticks and templates

@ -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: add types for popover attributes and events

@ -13,6 +13,7 @@
"beige-flies-wash", "beige-flies-wash",
"beige-rabbits-shave", "beige-rabbits-shave",
"brave-walls-destroy", "brave-walls-destroy",
"bright-peas-juggle",
"brown-spoons-boil", "brown-spoons-boil",
"chatty-cups-drop", "chatty-cups-drop",
"chatty-taxis-juggle", "chatty-taxis-juggle",
@ -24,8 +25,11 @@
"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",
"dull-mangos-wave", "dull-mangos-wave",
"early-ads-tie", "early-ads-tie",
"eight-steaks-shout", "eight-steaks-shout",
@ -52,10 +56,12 @@
"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",
"itchy-beans-melt", "itchy-beans-melt",
"itchy-kings-deliver",
"itchy-lions-wash", "itchy-lions-wash",
"khaki-mails-draw", "khaki-mails-draw",
"khaki-moose-arrive", "khaki-moose-arrive",
@ -69,6 +75,7 @@
"lazy-spiders-think", "lazy-spiders-think",
"lemon-geese-drum", "lemon-geese-drum",
"light-pens-watch", "light-pens-watch",
"long-buckets-lay",
"long-crews-return", "long-crews-return",
"lovely-carpets-lick", "lovely-carpets-lick",
"lovely-items-turn", "lovely-items-turn",
@ -76,6 +83,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",
@ -84,12 +92,14 @@
"odd-shoes-cheat", "odd-shoes-cheat",
"old-flies-jog", "old-flies-jog",
"old-mails-sneeze", "old-mails-sneeze",
"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",
@ -111,6 +121,7 @@
"slow-chefs-dream", "slow-chefs-dream",
"small-papayas-laugh", "small-papayas-laugh",
"smart-parents-swim", "smart-parents-swim",
"smart-zebras-pay",
"soft-clocks-remember", "soft-clocks-remember",
"soft-geese-learn", "soft-geese-learn",
"sour-forks-stare", "sour-forks-stare",
@ -120,10 +131,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",
@ -135,12 +148,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: support hydrating around `<noscript>`

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

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

@ -1,5 +1,43 @@
# svelte # svelte
## 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
### Patch Changes
- fix: evaluate transition parameters when the transition runs ([#9836](https://github.com/sveltejs/svelte/pull/9836))
- feat: add `$state.frozen` rune ([#9851](https://github.com/sveltejs/svelte/pull/9851))
- fix: correctly transform prop fallback values that use other props ([#9985](https://github.com/sveltejs/svelte/pull/9985))
- fix: escape template literal characters in text sequences ([#9973](https://github.com/sveltejs/svelte/pull/9973))
- fix: inject comment in place of `<noscript>` in client output ([#9953](https://github.com/sveltejs/svelte/pull/9953))
## 5.0.0-next.26 ## 5.0.0-next.26
### Patch Changes ### Patch Changes
@ -132,7 +170,7 @@
### Patch Changes ### Patch Changes
- feat: proxied staet ([#9739](https://github.com/sveltejs/svelte/pull/9739)) - feat: proxied state ([#9739](https://github.com/sveltejs/svelte/pull/9739))
- chore: more validation errors ([#9723](https://github.com/sveltejs/svelte/pull/9723)) - chore: more validation errors ([#9723](https://github.com/sveltejs/svelte/pull/9723))

@ -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.26", "version": "5.0.0-next.28",
"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",

@ -6,6 +6,7 @@ import { getLocator } from 'locate-character';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { validate_component_options, validate_module_options } from './validate-options.js'; import { validate_component_options, validate_module_options } from './validate-options.js';
import { convert } from './legacy.js'; import { convert } from './legacy.js';
import { CompileError } from './errors.js';
export { default as preprocess } from './preprocess/index.js'; export { default as preprocess } from './preprocess/index.js';
/** /**
@ -30,12 +31,8 @@ export function compile(source, options) {
const result = transform_component(analysis, source, combined_options); const result = transform_component(analysis, source, combined_options);
return result; return result;
} catch (e) { } catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') { if (e instanceof CompileError) {
handle_compile_error( handle_compile_error(e, options.filename, source);
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
} }
throw e; throw e;
@ -56,12 +53,8 @@ export function compileModule(source, options) {
const analysis = analyze_module(parse_acorn(source, false), validated); const analysis = analyze_module(parse_acorn(source, false), validated);
return transform_module(analysis, source, validated); return transform_module(analysis, source, validated);
} catch (e) { } catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') { if (e instanceof CompileError) {
handle_compile_error( handle_compile_error(e, options.filename, source);
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
} }
throw e; throw e;
@ -106,12 +99,8 @@ export function parse(source, options = {}) {
try { try {
ast = _parse(source); ast = _parse(source);
} catch (e) { } catch (e) {
if (/** @type {any} */ (e).name === 'CompileError') { if (e instanceof CompileError) {
handle_compile_error( handle_compile_error(e, options.filename, source);
/** @type {import('#compiler').CompileError} */ (e),
options.filename,
source
);
} }
throw e; throw e;

@ -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 =
/^\s*(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/;
@ -226,6 +227,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 +284,14 @@ 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 +309,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('|')) {

@ -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 &&
@ -596,6 +600,7 @@ const legacy_scope_tweaker = {
); );
if ( if (
binding.kind === 'state' || binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
(binding.kind === 'normal' && binding.declaration_kind === 'let') (binding.kind === 'normal' && binding.declaration_kind === 'let')
) { ) {
binding.kind = 'prop'; binding.kind = 'prop';
@ -647,18 +652,19 @@ const legacy_scope_tweaker = {
const runes_scope_js_tweaker = { const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return; if (node.init?.type !== 'CallExpression') return;
if (get_rune(node.init, state.scope) === null) return; const rune = get_rune(node.init, state.scope);
if (rune === null) return;
const callee = node.init.callee; const callee = node.init.callee;
if (callee.type !== 'Identifier') return; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name; if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived') return;
if (name !== '$state' && name !== '$derived') return;
for (const path of extract_paths(node.id)) { for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason // @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind = name === '$state' ? 'state' : 'derived'; binding.kind =
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived';
} }
} }
}; };
@ -676,28 +682,31 @@ const runes_scope_tweaker = {
VariableDeclarator(node, { state }) { VariableDeclarator(node, { state }) {
const init = unwrap_ts_expression(node.init); const init = unwrap_ts_expression(node.init);
if (!init || init.type !== 'CallExpression') return; if (!init || init.type !== 'CallExpression') return;
if (get_rune(init, state.scope) === null) return; const rune = get_rune(init, state.scope);
if (rune === null) return;
const callee = init.callee; const callee = init.callee;
if (callee.type !== 'Identifier') return; if (callee.type !== 'Identifier' && callee.type !== 'MemberExpression') return;
const name = callee.name; if (rune !== '$state' && rune !== '$state.frozen' && rune !== '$derived' && rune !== '$props')
if (name !== '$state' && name !== '$derived' && name !== '$props') return; return;
for (const path of extract_paths(node.id)) { for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason // @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name)); const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
binding.kind = binding.kind =
name === '$state' rune === '$state'
? 'state' ? 'state'
: name === '$derived' : rune === '$state.frozen'
? 'frozen_state'
: rune === '$derived'
? 'derived' ? 'derived'
: path.is_rest : path.is_rest
? 'rest_prop' ? 'rest_prop'
: 'prop'; : 'prop';
} }
if (name === '$props') { if (rune === '$props') {
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) { for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue; if (property.type !== 'Property') continue;
@ -909,7 +918,9 @@ const common_visitors = {
if ( if (
node !== binding.node && node !== binding.node &&
(binding.kind === 'state' || binding.kind === 'derived') && (binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'derived') &&
context.state.function_depth === binding.scope.function_depth context.state.function_depth === binding.scope.function_depth
) { ) {
warn(context.state.analysis.warnings, node, context.path, 'static-state-reference'); warn(context.state.analysis.warnings, node, context.path, 'static-state-reference');

@ -349,6 +349,7 @@ export const validation = {
if ( if (
!binding || !binding ||
(binding.kind !== 'state' && (binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'store_sub' && binding.kind !== 'store_sub' &&
@ -661,7 +662,7 @@ function validate_export(node, scope, name) {
error(node, 'invalid-derived-export'); error(node, 'invalid-derived-export');
} }
if (binding.kind === 'state' && binding.reassigned) { if ((binding.kind === 'state' || binding.kind === 'frozen_state') && binding.reassigned) {
error(node, 'invalid-state-export'); error(node, 'invalid-state-export');
} }
} }
@ -835,7 +836,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
is_binding, is_binding,
// This takes advantage of the fact that we don't assign initial for let directives and then/catch variables. // This takes advantage of the fact that we don't assign initial for let directives and then/catch variables.
// If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message. // If we start doing that, we need another property on the binding to differentiate, or give up on the more precise error message.
binding.kind !== 'state' && (binding.kind !== 'normal' || !binding.initial) binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
(binding.kind !== 'normal' || !binding.initial)
); );
} }
} }

@ -233,7 +233,9 @@ export function client_component(source, analysis, options) {
'$.bind_prop', '$.bind_prop',
b.id('$$props'), b.id('$$props'),
b.literal(alias ?? name), b.literal(alias ?? name),
binding?.kind === 'state' ? b.call('$.get', b.id(name)) : b.id(name) binding?.kind === 'state' || binding?.kind === 'frozen_state'
? b.call('$.get', b.id(name))
: b.id(name)
) )
); );
}); });
@ -241,7 +243,8 @@ export function client_component(source, analysis, options) {
const properties = analysis.exports.map(({ name, alias }) => { const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name); const binding = analysis.instance.scope.get(name);
const is_source = const is_source =
binding?.kind === 'state' && (!state.analysis.immutable || binding.reassigned); (binding?.kind === 'state' || binding?.kind === 'frozen_state') &&
(!state.analysis.immutable || binding.reassigned);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way. // TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode? // Should we for code size reasons make it an init in runes mode and/or non-dev mode?

@ -59,7 +59,7 @@ export interface ComponentClientTransformState extends ClientTransformState {
} }
export interface StateField { export interface StateField {
kind: 'state' | 'derived'; kind: 'state' | 'frozen_state' | 'derived';
id: PrivateIdentifier; id: PrivateIdentifier;
} }

@ -82,7 +82,8 @@ export function serialize_get_binding(node, state) {
} }
if (binding.prop_alias) { if (binding.prop_alias) {
return b.member(b.id('$$props'), b.id(binding.prop_alias)); const key = b.key(binding.prop_alias);
return b.member(b.id('$$props'), key, key.type === 'Literal');
} }
return b.member(b.id('$$props'), node); return b.member(b.id('$$props'), node);
} }
@ -92,7 +93,7 @@ export function serialize_get_binding(node, state) {
} }
if ( if (
(binding.kind === 'state' && ((binding.kind === 'state' || binding.kind === 'frozen_state') &&
(!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) || (!state.analysis.immutable || state.analysis.accessors || binding.reassigned)) ||
binding.kind === 'derived' || binding.kind === 'derived' ||
binding.kind === 'legacy_reactive' binding.kind === 'legacy_reactive'
@ -282,40 +283,53 @@ export function serialize_set_binding(node, context, fallback) {
// Handle class private/public state assignment cases // Handle class private/public state assignment cases
while (left.type === 'MemberExpression') { while (left.type === 'MemberExpression') {
if ( if (left.object.type === 'ThisExpression' && left.property.type === 'PrivateIdentifier') {
left.object.type === 'ThisExpression' && const private_state = context.state.private_state.get(left.property.name);
left.property.type === 'PrivateIdentifier' &&
context.state.private_state.has(left.property.name)
) {
const value = get_assignment_value(node, context); const value = get_assignment_value(node, context);
if (state.in_constructor) { if (private_state !== undefined) {
// See if we should wrap value in $.proxy if (state.in_constructor) {
if (context.state.analysis.runes && should_proxy(value)) { // See if we should wrap value in $.proxy
const assignment = fallback(); if (context.state.analysis.runes && should_proxy_or_freeze(value)) {
if (assignment.type === 'AssignmentExpression') { const assignment = fallback();
assignment.right = b.call('$.proxy', value); if (assignment.type === 'AssignmentExpression') {
return assignment; assignment.right =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment;
}
} }
} else {
return b.call(
'$.set',
left,
context.state.analysis.runes && should_proxy_or_freeze(value)
? private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value)
: value
);
} }
} else {
return b.call(
'$.set',
left,
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value
);
} }
} else if ( } else if (
left.object.type === 'ThisExpression' && left.object.type === 'ThisExpression' &&
left.property.type === 'Identifier' && left.property.type === 'Identifier' &&
context.state.public_state.has(left.property.name) &&
state.in_constructor state.in_constructor
) { ) {
const public_state = context.state.public_state.get(left.property.name);
const value = get_assignment_value(node, context); const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy // See if we should wrap value in $.proxy
if (context.state.analysis.runes && should_proxy(value)) { if (
context.state.analysis.runes &&
public_state !== undefined &&
should_proxy_or_freeze(value)
) {
const assignment = fallback(); const assignment = fallback();
if (assignment.type === 'AssignmentExpression') { if (assignment.type === 'AssignmentExpression') {
assignment.right = b.call('$.proxy', value); assignment.right =
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: b.call('$.proxy', value);
return assignment; return assignment;
} }
} }
@ -352,6 +366,7 @@ export function serialize_set_binding(node, context, fallback) {
if ( if (
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
@ -369,12 +384,24 @@ export function serialize_set_binding(node, context, fallback) {
return b.call(left, value); return b.call(left, value);
} else if (is_store) { } else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
} else { } else if (binding.kind === 'state') {
return b.call( return b.call(
'$.set', '$.set',
b.id(left_name), b.id(left_name),
context.state.analysis.runes && should_proxy(value) ? b.call('$.proxy', value) : value context.state.analysis.runes && should_proxy_or_freeze(value)
? b.call('$.proxy', value)
: value
); );
} else if (binding.kind === 'frozen_state') {
return b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes && should_proxy_or_freeze(value)
? b.call('$.freeze', value)
: value
);
} else {
return b.call('$.set', b.id(left_name), value);
} }
} else { } else {
if (is_store) { if (is_store) {
@ -612,12 +639,14 @@ export function create_state_declarators(declarator, scope, value) {
} }
/** @param {import('estree').Expression} node */ /** @param {import('estree').Expression} node */
export function should_proxy(node) { export function should_proxy_or_freeze(node) {
if ( if (
!node || !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;

@ -49,6 +49,7 @@ export const global_visitors = {
// use runtime functions for smaller output // use runtime functions for smaller output
if ( if (
binding?.kind === 'state' || binding?.kind === 'state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'each' || binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' || binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' || binding?.kind === 'prop' ||

@ -2,7 +2,7 @@ import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js'; import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js'; import * as b from '../../../../utils/builders.js';
import * as assert from '../../../../utils/assert.js'; import * as assert from '../../../../utils/assert.js';
import { create_state_declarators, get_prop_source, should_proxy } from '../utils.js'; import { create_state_declarators, get_prop_source, should_proxy_or_freeze } from '../utils.js';
import { unwrap_ts_expression } from '../../../../utils/ast.js'; import { unwrap_ts_expression } from '../../../../utils/ast.js';
/** @type {import('../types.js').ComponentVisitors} */ /** @type {import('../types.js').ComponentVisitors} */
@ -29,10 +29,11 @@ export const javascript_visitors_runes = {
if (definition.value?.type === 'CallExpression') { if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, state.scope); const rune = get_rune(definition.value, state.scope);
if (rune === '$state' || rune === '$derived') { if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
/** @type {import('../types.js').StateField} */ /** @type {import('../types.js').StateField} */
const field = { const field = {
kind: rune === '$state' ? 'state' : 'derived', kind:
rune === '$state' ? 'state' : rune === '$state.frozen' ? 'frozen_state' : 'derived',
// @ts-expect-error this is set in the next pass // @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null id: is_private ? definition.key : null
}; };
@ -84,7 +85,9 @@ export const javascript_visitors_runes = {
value = value =
field.kind === 'state' field.kind === 'state'
? b.call('$.source', should_proxy(init) ? b.call('$.proxy', init) : init) ? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.proxy', init) : init)
: field.kind === 'frozen_state'
? b.call('$.source', should_proxy_or_freeze(init) ? b.call('$.freeze', init) : init)
: b.call('$.derived', b.thunk(init)); : b.call('$.derived', b.thunk(init));
} else { } else {
// if no arguments, we know it's state as `$derived()` is a compile error // if no arguments, we know it's state as `$derived()` is a compile error
@ -114,6 +117,19 @@ export const javascript_visitors_runes = {
); );
} }
if (field.kind === 'frozen_state') {
// set foo(value) { this.#foo = value; }
const value = b.id('value');
body.push(
b.method(
'set',
definition.key,
[value],
[b.stmt(b.call('$.set', member, b.call('$.freeze', value)))]
)
);
}
if (field.kind === 'derived' && state.options.dev) { if (field.kind === 'derived' && state.options.dev) {
body.push( body.push(
b.method( b.method(
@ -177,7 +193,7 @@ export const javascript_visitors_runes = {
if (property.value.type === 'AssignmentPattern') { if (property.value.type === 'AssignmentPattern') {
id = property.value.left; id = property.value.left;
initial = property.value.right; initial = /** @type {import('estree').Expression} */ (visit(property.value.right));
} }
assert.equal(id.type, 'Identifier'); assert.equal(id.type, 'Identifier');
@ -217,13 +233,24 @@ export const javascript_visitors_runes = {
const binding = /** @type {import('#compiler').Binding} */ ( const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name) state.scope.get(declarator.id.name)
); );
if (should_proxy(value)) { if (should_proxy_or_freeze(value)) {
value = b.call('$.proxy', value); value = b.call('$.proxy', value);
} }
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) { if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
value = b.call('$.source', value); value = b.call('$.source', value);
} }
} else if (rune === '$state.frozen') {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy_or_freeze(value)) {
value = b.call('$.freeze', value);
}
if (binding.reassigned) {
value = b.call('$.source', value);
}
} else { } else {
value = b.call('$.derived', b.thunk(value)); value = b.call('$.derived', b.thunk(value));
} }

@ -33,6 +33,7 @@ import {
} from '../../../../../constants.js'; } from '../../../../../constants.js';
import { regex_is_valid_identifier } from '../../../patterns.js'; import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js'; import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
/** /**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
@ -1014,8 +1015,7 @@ function create_block(parent, name, nodes, context) {
context.path, context.path,
namespace, namespace,
context.state.preserve_whitespace, context.state.preserve_whitespace,
context.state.options.preserveComments, context.state.options.preserveComments
false
); );
if (hoisted.length === 0 && trimmed.length === 0) { if (hoisted.length === 0 && trimmed.length === 0) {
@ -1245,6 +1245,7 @@ function serialize_event_handler(node, { state, visit }) {
if ( if (
binding !== null && binding !== null &&
(binding.kind === 'state' || (binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'legacy_reactive' || binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' || binding.kind === 'derived' ||
binding.kind === 'prop' || binding.kind === 'prop' ||
@ -1636,7 +1637,7 @@ function serialize_template_literal(values, visit, state) {
const node = values[i]; const node = values[i];
if (node.type === 'Text') { if (node.type === 'Text') {
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += node.data; last.value.raw += sanitize_template_string(node.data);
} else { } else {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
contains_call_expression = true; contains_call_expression = true;
@ -1821,6 +1822,11 @@ export const template_visitors = {
); );
}, },
RegularElement(node, context) { RegularElement(node, context) {
if (node.name === 'noscript') {
context.state.template.push('<!>');
return;
}
const metadata = context.state.metadata; const metadata = context.state.metadata;
const child_metadata = { const child_metadata = {
...context.state.metadata, ...context.state.metadata,
@ -2012,8 +2018,7 @@ export const template_visitors = {
context.path, context.path,
child_metadata.namespace, child_metadata.namespace,
state.preserve_whitespace, state.preserve_whitespace,
state.options.preserveComments, state.options.preserveComments
false
); );
for (const node of hoisted) { for (const node of hoisted) {

@ -26,6 +26,7 @@ import { binding_properties } from '../../bindings.js';
import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js';
import { remove_types } from '../typescript.js'; import { remove_types } from '../typescript.js';
import { DOMBooleanAttributes } from '../../../../constants.js'; import { DOMBooleanAttributes } from '../../../../constants.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
/** /**
* @param {string} value * @param {string} value
@ -117,14 +118,6 @@ function serialize_template(template, out = b.id('out')) {
return statements; return statements;
} }
/**
* @param {string} str
* @returns {string}
*/
function sanitize_template_string(str) {
return str.replace(/(`|\${|\\)/g, '\\$1');
}
/** /**
* Processes an array of template nodes, joining sibling text/expression nodes and * Processes an array of template nodes, joining sibling text/expression nodes and
* recursing into child nodes. * recursing into child nodes.
@ -194,7 +187,10 @@ function process_children(nodes, parent, { visit, state }) {
const node = sequence[i]; const node = sequence[i];
if (node.type === 'Text' || node.type === 'Comment') { if (node.type === 'Text' || node.type === 'Comment') {
let last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1)); let last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data); last.value.raw +=
node.type === 'Comment'
? `<!--${node.data}-->`
: sanitize_template_string(escape_html(node.data));
} else if (node.type === 'Anchor') { } else if (node.type === 'Anchor') {
expressions.push(node.id); expressions.push(node.id);
quasis.push(b.quasi('', i + 1 === sequence.length)); quasis.push(b.quasi('', i + 1 === sequence.length));
@ -252,8 +248,7 @@ function create_block(parent, nodes, context, anchor) {
context.path, context.path,
namespace, namespace,
context.state.preserve_whitespace, context.state.preserve_whitespace,
context.state.options.preserveComments, context.state.options.preserveComments
true
); );
if (hoisted.length === 0 && trimmed.length === 0 && !anchor) { if (hoisted.length === 0 && trimmed.length === 0 && !anchor) {
@ -446,6 +441,7 @@ function serialize_set_binding(node, context, fallback) {
if ( if (
binding.kind !== 'state' && binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' && binding.kind !== 'prop' &&
binding.kind !== 'each' && binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' && binding.kind !== 'legacy_reactive' &&
@ -558,7 +554,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') { if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope); const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$derived') { if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
return { return {
...node, ...node,
value: value:
@ -1189,8 +1185,7 @@ const template_visitors = {
inner_context.path, inner_context.path,
metadata.namespace, metadata.namespace,
state.preserve_whitespace, state.preserve_whitespace,
state.options.preserveComments, state.options.preserveComments
true
); );
for (const node of hoisted) { for (const node of hoisted) {

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

@ -83,7 +83,6 @@ export function is_hoistable_function(node) {
* @param {import('#compiler').Namespace} namespace * @param {import('#compiler').Namespace} namespace
* @param {boolean} preserve_whitespace * @param {boolean} preserve_whitespace
* @param {boolean} preserve_comments * @param {boolean} preserve_comments
* @param {boolean} preserve_noscript
*/ */
export function clean_nodes( export function clean_nodes(
parent, parent,
@ -91,8 +90,7 @@ export function clean_nodes(
path, path,
namespace = 'html', namespace = 'html',
preserve_whitespace, preserve_whitespace,
preserve_comments, preserve_comments
preserve_noscript
) { ) {
/** @type {import('#compiler').SvelteNode[]} */ /** @type {import('#compiler').SvelteNode[]} */
const hoisted = []; const hoisted = [];
@ -105,10 +103,6 @@ export function clean_nodes(
continue; continue;
} }
if (node.type === 'RegularElement' && node.name === 'noscript' && !preserve_noscript) {
continue;
}
if ( if (
node.type === 'ConstTag' || node.type === 'ConstTag' ||
node.type === 'DebugTag' || node.type === 'DebugTag' ||

@ -72,6 +72,7 @@ export const ElementBindings = [
export const Runes = /** @type {const} */ ([ export const Runes = /** @type {const} */ ([
'$state', '$state',
'$state.frozen',
'$props', '$props',
'$derived', '$derived',
'$effect', '$effect',

@ -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 });
@ -680,7 +680,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}
*/ */
@ -712,5 +712,5 @@ export function get_rune(node, scope) {
const binding = scope.get(n.name); const binding = scope.get(n.name);
if (binding !== null) return null; // rune name, but references a variable or store if (binding !== null) return null; // rune name, but references a variable or store
return /** @type {Runes[number] | null} */ (joined); return /** @type {typeof Runes[number] | null} */ (joined);
} }

@ -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 {
@ -258,6 +258,7 @@ export interface Binding {
| 'prop' | 'prop'
| 'rest_prop' | 'rest_prop'
| 'state' | 'state'
| 'frozen_state'
| 'derived' | 'derived'
| 'each' | 'each'
| 'store_sub' | 'store_sub'
@ -268,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,4 +1,5 @@
import { regex_is_valid_identifier } from '../phases/patterns.js'; import { regex_is_valid_identifier } from '../phases/patterns.js';
import { sanitize_template_string } from './sanitize_template_string.js';
/** /**
* @param {Array<import('estree').Expression | import('estree').SpreadElement | null>} elements * @param {Array<import('estree').Expression | import('estree').SpreadElement | null>} elements
@ -331,7 +332,7 @@ export function prop_def(key, value, computed = false, is_static = false) {
* @returns {import('estree').TemplateElement} * @returns {import('estree').TemplateElement}
*/ */
export function quasi(cooked, tail = false) { export function quasi(cooked, tail = false) {
const raw = cooked.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$/g, '\\$'); const raw = sanitize_template_string(cooked);
return { type: 'TemplateElement', value: { raw, cooked }, tail }; return { type: 'TemplateElement', value: { raw, cooked }, tail };
} }

@ -0,0 +1,7 @@
/**
* @param {string} str
* @returns {string}
*/
export function sanitize_template_string(str) {
return str.replace(/(`|\${|\\)/g, '\\$1');
}

@ -1,4 +1,4 @@
import { define_property } from '../utils.js'; import { define_property, is_frozen } from '../utils.js';
import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js'; import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
/** /**
@ -6,8 +6,6 @@ import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject * @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject
*/ */
const is_frozen = Object.isFrozen;
/** /**
* Expects a value that was wrapped with `proxy` and makes it readonly. * Expects a value that was wrapped with `proxy` and makes it readonly.
* *

@ -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;
} }
@ -2778,6 +2782,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 +2790,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 +2802,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
);
} }
} }
}; };

@ -1,7 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { subscribe_to_store } from '../../store/utils.js'; import { subscribe_to_store } from '../../store/utils.js';
import { EMPTY_FUNC, run_all } from '../common.js'; import { EMPTY_FUNC, run_all } from '../common.js';
import { get_descriptor, get_descriptors, is_array } from './utils.js'; import { get_descriptor, get_descriptors, is_array, is_frozen, object_freeze } from './utils.js';
import { import {
PROPS_IS_LAZY_INITIAL, PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE, PROPS_IS_IMMUTABLE,
@ -9,7 +9,7 @@ import {
PROPS_IS_UPDATED PROPS_IS_UPDATED
} from '../../constants.js'; } from '../../constants.js';
import { readonly } from './proxy/readonly.js'; import { readonly } from './proxy/readonly.js';
import { proxy, unstate } from './proxy/proxy.js'; import { READONLY_SYMBOL, STATE_SYMBOL, proxy, unstate } from './proxy/proxy.js';
export const SOURCE = 1; export const SOURCE = 1;
export const DERIVED = 1 << 1; export const DERIVED = 1 << 1;
@ -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(
@ -1899,3 +1958,25 @@ if (DEV) {
throw_rune_error('$inspect'); throw_rune_error('$inspect');
throw_rune_error('$props'); throw_rune_error('$props');
} }
/**
* Expects a value that was wrapped with `freeze` and makes it frozen.
* @template {import('./proxy/proxy.js').StateObject} T
* @param {T} value
* @returns {Readonly<Record<string | symbol, any>>}
*/
export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If the object is already proxified, then unstate the value
if (STATE_SYMBOL in value) {
return object_freeze(unstate(value));
}
// If the value is already read-only then just use that
if (DEV && READONLY_SYMBOL in value) {
return value;
}
// Otherwise freeze the object
object_freeze(value);
}
return value;
}

@ -421,7 +421,6 @@ function is_transition_block(block) {
export function bind_transition(dom, get_transition_fn, props_fn, direction, global) { export function bind_transition(dom, get_transition_fn, props_fn, direction, global) {
const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect); const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect);
const block = current_block; const block = current_block;
const props = props_fn === null ? {} : props_fn();
let can_show_intro_on_mount = true; let can_show_intro_on_mount = true;
let can_apply_lazy_transitions = false; let can_apply_lazy_transitions = false;
@ -467,8 +466,9 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
const transition_fn = get_transition_fn(); const transition_fn = get_transition_fn();
/** @param {DOMRect} [from] */ /** @param {DOMRect} [from] */
const init = (from) => const init = (from) =>
untrack(() => untrack(() => {
direction === 'key' const props = props_fn === null ? {} : props_fn();
return direction === 'key'
? /** @type {import('./types.js').AnimateFn<any>} */ (transition_fn)( ? /** @type {import('./types.js').AnimateFn<any>} */ (transition_fn)(
dom, dom,
{ from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() }, { from: /** @type {DOMRect} */ (from), to: dom.getBoundingClientRect() },
@ -477,8 +477,8 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
) )
: /** @type {import('./types.js').TransitionFn<any>} */ (transition_fn)(dom, props, { : /** @type {import('./types.js').TransitionFn<any>} */ (transition_fn)(dom, props, {
direction direction
}) });
); });
transition = create_transition(dom, init, direction, transition_effect); transition = create_transition(dom, init, direction, transition_effect);
const is_intro = direction === 'in'; const is_intro = direction === 'in';

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

@ -5,6 +5,8 @@ export var array_from = Array.from;
export var object_keys = Object.keys; export var object_keys = Object.keys;
export var object_entries = Object.entries; export var object_entries = Object.entries;
export var object_assign = Object.assign; export var object_assign = Object.assign;
export var is_frozen = Object.isFrozen;
export var object_freeze = Object.freeze;
export var define_property = Object.defineProperty; export var define_property = Object.defineProperty;
export var get_descriptor = Object.getOwnPropertyDescriptor; export var get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors; export var get_descriptors = Object.getOwnPropertyDescriptors;

@ -36,7 +36,8 @@ export {
effect_active, effect_active,
user_root_effect, user_root_effect,
inspect, inspect,
unwrap unwrap,
freeze
} from './client/runtime.js'; } from './client/runtime.js';
export * from './client/each.js'; export * from './client/each.js';

@ -17,6 +17,33 @@ declare module '*.svelte' {
declare function $state<T>(initial: T): T; declare function $state<T>(initial: T): T;
declare function $state<T>(): T | undefined; declare function $state<T>(): T | undefined;
declare namespace $state {
/**
* Declares reactive read-only state that is shallowly immutable.
*
* Example:
* ```ts
* <script>
* let items = $state.frozen([0]);
*
* const addItem = () => {
* items = [...items, items.length];
* };
* </script>
*
* <button on:click={addItem}>
* {items.join(', ')}
* </button>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$state-raw
*
* @param initial The initial value
*/
export function frozen<T>(initial: T): Readonly<T>;
export function frozen<T>(): Readonly<T> | undefined;
}
/** /**
* Declares derived state, i.e. one that depends on other state variables. * Declares derived state, i.e. one that depends on other state variables.
* The expression inside `$derived(...)` should be free of side-effects. * The expression inside `$derived(...)` should be free of side-effects.

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

@ -0,0 +1,2 @@
<!--ssr:0--><noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: 0</p><!--ssr:0-->

@ -0,0 +1,9 @@
<script>
import { onMount } from "svelte";
let count = 0;
onMount(() => count++);
</script>
<noscript>JavaScript is required for this site.</noscript>
<h1>Hello!</h1><p>Count: {count}</p>

@ -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": [
{ {
@ -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,18 @@
<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;
}
</style>

@ -0,0 +1,246 @@
{
"css": {
"type": "Style",
"start": 0,
"end": 313,
"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
}
],
"content": {
"start": 7,
"end": 305,
"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"
}
},
"js": [],
"start": null,
"end": null,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [],
"transparent": false
},
"options": null
}

@ -0,0 +1,44 @@
import { test } from '../../test';
export default test({
html: `
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>6</p>
<h1>Bag'ol stores</h1>
<p>6</p>
<p></p>
<p></p>
<button>Click me!</button>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<p>7</p>
<p>8</p>
<p>9</p>
<p>10</p>
<p>11</p>
<p>12</p>
<h1>Bag'ol stores</h1>
<p>12</p>
<p>14</p>
<p>15</p>
<button>Click me!</button>
`
);
}
});

@ -0,0 +1,42 @@
<script>
import { get, writable } from 'svelte/store'
let bagOlStores = writable([1, 2, 3, writable(4), writable(5), writable(6)]);
let firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore;
([firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore] = $bagOlStores);
function changeStores() {
$bagOlStores = ([
firstNonStore,
secondNonStore,
thirdNonStore,
firstStore,
$secondStore,
thirdStore
] = [
7,
8,
9,
writable(10),
11,
writable(12),
writable(14),
writable(15)
]);
}
</script>
<p>{firstNonStore}</p>
<p>{secondNonStore}</p>
<p>{thirdNonStore}</p>
<p>{$firstStore}</p>
<p>{$secondStore}</p>
<p>{$thirdStore}</p>
<h1>Bag'ol stores</h1>
<p>{get($bagOlStores[5])}</p>
<p>{get($bagOlStores[6])}</p>
<p>{get($bagOlStores[7])}</p>
<button on:click={changeStores}>Click me!</button>

@ -0,0 +1,44 @@
import { test } from '../../test';
export default test({
html: `
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>6</p>
<h1>Bag'ol stores</h1>
<p>4</p>
<p>5</p>
<p>6</p>
<button>Click me!</button>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<p>7</p>
<p>8</p>
<p>9</p>
<p>10</p>
<p>11</p>
<p>12</p>
<h1>Bag'ol stores</h1>
<p>14</p>
<p>13</p>
<p>12</p>
<button>Click me!</button>
`
);
}
});

@ -0,0 +1,48 @@
<script>
import { get, writable } from 'svelte/store'
let bagOlStores = writable({
firstNonStore: 1,
secondNonStore: 2,
thirdNonStore: 3,
firstStore: writable(4),
secondStore: writable(5),
thirdStore: writable(6)
});
let { firstNonStore, secondNonStore, thirdNonStore, firstStore, secondStore, thirdStore } = $bagOlStores;
function changeStores() {
$bagOlStores = ({
thirdStore,
$secondStore,
$firstStore,
firstNonStore,
secondNonStore,
thirdNonStore,
} = {
firstNonStore: 7,
secondNonStore: 8,
thirdNonStore: 9,
$firstStore: 10,
$secondStore: 11,
firstStore: writable(14),
secondStore: writable(13),
thirdStore: writable(12)
});
}
</script>
<p>{firstNonStore}</p>
<p>{secondNonStore}</p>
<p>{thirdNonStore}</p>
<p>{$firstStore}</p>
<p>{$secondStore}</p>
<p>{$thirdStore}</p>
<h1>Bag'ol stores</h1>
<p>{get($bagOlStores.firstStore)}</p>
<p>{get($bagOlStores.secondStore)}</p>
<p>{get($bagOlStores.thirdStore)}</p>
<button on:click={changeStores}>Click me!</button>

@ -0,0 +1,9 @@
<script>
let z = 8;
let { a, b = a, c = b * b, d = z * b + c } = $props();
</script>
<p>{a}</p>
<p>{b}</p>
<p>{c}</p>
<p>{d}</p>

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

@ -0,0 +1,5 @@
<script>
import Test from './Test.svelte'
</script>
<Test a={5} />

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: '<div>/ $clicks: 0 `tim$es` \\</div><div>$dollars `backticks` pyramid /\\</div>'
});

@ -0,0 +1,6 @@
<div>
/ $clicks: {0} `tim${"e"}s` \
</div>
<div>
$dollars `backticks` pyramid /\
</div>

@ -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,22 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
html: `<button>0</button>`,
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(log, ['read only', 'read only']);
}
});

@ -0,0 +1,16 @@
<script>
import { log } from './log.js';
class Counter {
count = $state.frozen({ a: 0 });
}
const counter = new Counter();
</script>
<button on:click={() => {
try {
counter.count.a++
} catch (e) {
log.push('read only')
}
}}>{counter.count.a}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,8 @@
<script>
class Counter {
count = $state.frozen(0);
}
const counter = new Counter();
</script>
<button on:click={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,22 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
html: `<button>0</button>`,
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>0</button>`);
assert.deepEqual(log, ['read only', 'read only']);
}
});

@ -0,0 +1,27 @@
<script>
import { log } from './log.js';
class Counter {
#count = $state.frozen();
constructor(initial_count) {
this.#count = { a: initial_count };
}
get count() {
return this.#count;
}
set count(val) {
this.#count = val;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => {
try {
counter.count.a++
} catch (e) {
log.push('read only')
}
}}>{counter.count.a}</button>

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
html: `<button>0</button>`,
async test({ assert, target }) {
const btn = target.querySelector('button');
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>1</button>`);
await btn?.click();
assert.htmlEqual(target.innerHTML, `<button>2</button>`);
}
});

@ -0,0 +1,19 @@
<script>
class Counter {
#count = $state.frozen(0);
constructor(initial_count) {
this.#count = initial_count;
}
get count() {
return this.#count;
}
set count(val) {
this.#count = val;
}
}
const counter = new Counter(0);
</script>
<button on:click={() => counter.count++}>{counter.count}</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>

@ -0,0 +1,38 @@
import { test } from '../../test';
export default test({
html: `
<button>Update</button>
<p>0</p>
<p>b</p>
<p>true</p>
<p>0</p>
<p>10</p>
<p>12</p>
<p>15</p>
<p>16</p>
`,
async test({ assert, target, window }) {
const button = target.querySelector('button');
const clickEvent = new window.Event('click', { bubbles: true });
await button?.dispatchEvent(clickEvent);
assert.htmlEqual(
target.innerHTML,
`
<button>Update</button>
<p>5</p>
<p>d</p>
<p>false</p>
<p>3</p>
<p>100</p>
<p>120</p>
<p>25</p>
<p>26</p>
`
);
}
});

@ -0,0 +1,23 @@
<script>
let a = $state(0);
let b = $state("b");
let c = $state(true);
let d = $state([]);
let e = $state({ x: 10, y: 12 });
let f = $state({ w: 15, v: 16 });
function change() {
({ d, e, g: [f.w, f.v] } = { d: ([a, b, c] = [5, "d", false]), e: { x: 100, y: 120 }, g: [25, 26] });
}
</script>
<button on:click={change}>Update</button>
<p>{a}</p>
<p>{b}</p>
<p>{c}</p>
<p>{d.length}</p>
<p>{e.x}</p>
<p>{e.y}</p>
<p>{f.w}</p>
<p>{f.v}</p>

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
async test({ assert, target }) {
const [b1] = target.querySelectorAll('button');
b1.click();
await Promise.resolve();
assert.htmlEqual(target.innerHTML, `<button>0, 1</button>`);
}
});

@ -0,0 +1,11 @@
<script>
let items = $state.frozen([0]);
const addItem = () => {
items = [...items, items.length];
};
</script>
<button on:click={addItem}>
{items.join(', ')}
</button>

@ -0,0 +1,17 @@
import { test } from '../../test';
import { log } from './log.js';
export default test({
before_test() {
log.length = 0;
},
async test({ assert, target }) {
const [b1, b2] = target.querySelectorAll('button');
b1.click();
b2.click();
await Promise.resolve();
assert.deepEqual(log, [0, 1]);
}
});

@ -0,0 +1,2 @@
/** @type {any[]} */
export const log = [];

@ -0,0 +1,13 @@
<script>
import { log } from './log.js';
let x = $state.frozen(0);
let y = $state.frozen(0);
$effect(() => {
log.push(x);
});
</script>
<button on:click={() => x++}>{x}</button>
<button on:click={() => y++}>{y}</button>

@ -0,0 +1,12 @@
import { test } from '../../test';
export default test({
test({ assert, component, target }) {
const div = /** @type {HTMLDivElement & { foo?: number }} */ (target.querySelector('div'));
assert.equal(div.foo, undefined);
component.foo = 2;
component.visible = false;
assert.equal(div.foo, 2);
}
});

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save