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-rabbits-shave",
"brave-walls-destroy",
"bright-peas-juggle",
"brown-spoons-boil",
"chatty-cups-drop",
"chatty-taxis-juggle",
@ -24,8 +25,11 @@
"cool-ants-leave",
"cuddly-pianos-drop",
"curly-lizards-dream",
"curvy-ties-shout",
"dirty-bats-punch",
"dirty-garlics-design",
"dirty-tips-add",
"dry-clocks-grow",
"dull-mangos-wave",
"early-ads-tie",
"eight-steaks-shout",
@ -52,10 +56,12 @@
"happy-suits-film",
"healthy-planes-vanish",
"heavy-ears-rule",
"hip-balloons-begin",
"honest-icons-change",
"hungry-dots-fry",
"hungry-tips-unite",
"itchy-beans-melt",
"itchy-kings-deliver",
"itchy-lions-wash",
"khaki-mails-draw",
"khaki-moose-arrive",
@ -69,6 +75,7 @@
"lazy-spiders-think",
"lemon-geese-drum",
"light-pens-watch",
"long-buckets-lay",
"long-crews-return",
"lovely-carpets-lick",
"lovely-items-turn",
@ -76,6 +83,7 @@
"lucky-schools-hang",
"moody-frogs-exist",
"moody-owls-cry",
"nasty-lions-double",
"neat-dingos-clap",
"new-boats-wait",
"ninety-dingos-walk",
@ -84,12 +92,14 @@
"odd-shoes-cheat",
"old-flies-jog",
"old-mails-sneeze",
"orange-dingos-poke",
"polite-dolphins-care",
"polite-pumpkins-guess",
"polite-ravens-study",
"poor-eggs-enjoy",
"poor-seahorses-flash",
"popular-mangos-rest",
"pretty-ties-help",
"purple-dragons-peel",
"quiet-camels-mate",
"rare-pears-whisper",
@ -111,6 +121,7 @@
"slow-chefs-dream",
"small-papayas-laugh",
"smart-parents-swim",
"smart-zebras-pay",
"soft-clocks-remember",
"soft-geese-learn",
"sour-forks-stare",
@ -120,10 +131,12 @@
"strong-gifts-smoke",
"strong-lemons-provide",
"sweet-mangos-beg",
"sweet-pens-sniff",
"swift-donkeys-perform",
"swift-ravens-hunt",
"swift-seahorses-deliver",
"tall-books-grin",
"tall-garlics-try",
"tall-shrimps-worry",
"tall-tigers-wait",
"tasty-numbers-perform",
@ -135,12 +148,14 @@
"thirty-ghosts-fix",
"thirty-impalas-repair",
"thirty-wombats-relax",
"three-suits-grin",
"tiny-kings-whisper",
"twelve-dragons-join",
"twelve-onions-juggle",
"two-dragons-yell",
"two-falcons-buy",
"unlucky-boxes-obey",
"unlucky-trees-lick",
"wet-games-fly",
"wicked-clouds-exercise",
"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:
node-version: 18
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
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
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.
- `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
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 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

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

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

@ -1,5 +1,43 @@
# 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
### Patch Changes
@ -132,7 +170,7 @@
### 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))

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

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.26",
"version": "5.0.0-next.28",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -90,11 +90,12 @@
"templating"
],
"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",
"check": "tsc && cd ./tests/types && tsc",
"check:watch": "tsc --watch",
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js",
"prepublishOnly": "pnpm build"
},
"devDependencies": {
@ -106,7 +107,7 @@
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.3",
"@types/estree": "^1.0.5",
"dts-buddy": "^0.4.0",
"dts-buddy": "^0.4.3",
"esbuild": "^0.19.2",
"rollup": "^4.1.5",
"source-map": "^0.7.4",
@ -116,7 +117,7 @@
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"acorn": "^8.10.0",
"acorn-typescript": "^1.4.11",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.0",
"axobject-query": "^4.0.0",
"esm-env": "^1.0.0",

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

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

@ -11,7 +11,7 @@ import read_options from './read/options.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
const regex_lang_attribute =
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/;
/<!--[^]*?-->|<script\s+(?:[^>]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/g;
export class Parser {
/**
@ -49,7 +49,14 @@ export class Parser {
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 = {
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 = /^(\+|~|>|\|\|)/;
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_BRACE_OR_SEMICOLON = /[{;]/;
const REGEX_LEADING_HYPHEN_OR_DIGIT = /-?\d/;
@ -226,6 +227,12 @@ function read_selector(parser, inside_pseudo_class = false) {
start,
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(':')) {
const name = read_identifier(parser);
@ -277,6 +284,14 @@ function read_selector(parser, inside_pseudo_class = false) {
value,
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)) {
parser.allow_whitespace();
const start = parser.index;
@ -294,13 +309,6 @@ function read_selector(parser, inside_pseudo_class = false) {
start,
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 {
let name = read_identifier(parser);
if (parser.match('|')) {

@ -184,7 +184,9 @@ export default class Selector {
selector.name === 'global' &&
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');
}

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

@ -349,6 +349,7 @@ export const validation = {
if (
!binding ||
(binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'store_sub' &&
@ -661,7 +662,7 @@ function validate_export(node, scope, name) {
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');
}
}
@ -835,7 +836,9 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
is_binding,
// 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.
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',
b.id('$$props'),
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 binding = analysis.instance.scope.get(name);
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.
// 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 {
kind: 'state' | 'derived';
kind: 'state' | 'frozen_state' | 'derived';
id: PrivateIdentifier;
}

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

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

@ -2,7 +2,7 @@ import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.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';
/** @type {import('../types.js').ComponentVisitors} */
@ -29,10 +29,11 @@ export const javascript_visitors_runes = {
if (definition.value?.type === 'CallExpression') {
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} */
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
id: is_private ? definition.key : null
};
@ -84,7 +85,9 @@ export const javascript_visitors_runes = {
value =
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));
} else {
// 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) {
body.push(
b.method(
@ -177,7 +193,7 @@ export const javascript_visitors_runes = {
if (property.value.type === 'AssignmentPattern') {
id = property.value.left;
initial = property.value.right;
initial = /** @type {import('estree').Expression} */ (visit(property.value.right));
}
assert.equal(id.type, 'Identifier');
@ -217,13 +233,24 @@ export const javascript_visitors_runes = {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(declarator.id.name)
);
if (should_proxy(value)) {
if (should_proxy_or_freeze(value)) {
value = b.call('$.proxy', value);
}
if (!state.analysis.immutable || state.analysis.accessors || binding.reassigned) {
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 {
value = b.call('$.derived', b.thunk(value));
}

@ -33,6 +33,7 @@ import {
} from '../../../../../constants.js';
import { regex_is_valid_identifier } from '../../../patterns.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
@ -1014,8 +1015,7 @@ function create_block(parent, name, nodes, context) {
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments,
false
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
@ -1245,6 +1245,7 @@ function serialize_event_handler(node, { state, visit }) {
if (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
@ -1636,7 +1637,7 @@ function serialize_template_literal(values, visit, state) {
const node = values[i];
if (node.type === 'Text') {
const last = /** @type {import('estree').TemplateElement} */ (quasis.at(-1));
last.value.raw += node.data;
last.value.raw += sanitize_template_string(node.data);
} else {
if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) {
contains_call_expression = true;
@ -1821,6 +1822,11 @@ export const template_visitors = {
);
},
RegularElement(node, context) {
if (node.name === 'noscript') {
context.state.template.push('<!>');
return;
}
const metadata = context.state.metadata;
const child_metadata = {
...context.state.metadata,
@ -2012,8 +2018,7 @@ export const template_visitors = {
context.path,
child_metadata.namespace,
state.preserve_whitespace,
state.options.preserveComments,
false
state.options.preserveComments
);
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 { remove_types } from '../typescript.js';
import { DOMBooleanAttributes } from '../../../../constants.js';
import { sanitize_template_string } from '../../../utils/sanitize_template_string.js';
/**
* @param {string} value
@ -117,14 +118,6 @@ function serialize_template(template, out = b.id('out')) {
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
* recursing into child nodes.
@ -194,7 +187,10 @@ function process_children(nodes, parent, { visit, state }) {
const node = sequence[i];
if (node.type === 'Text' || node.type === 'Comment') {
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') {
expressions.push(node.id);
quasis.push(b.quasi('', i + 1 === sequence.length));
@ -252,8 +248,7 @@ function create_block(parent, nodes, context, anchor) {
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments,
true
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0 && !anchor) {
@ -446,6 +441,7 @@ function serialize_set_binding(node, context, fallback) {
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
@ -558,7 +554,7 @@ const javascript_visitors_runes = {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, state.scope);
if (rune === '$state' || rune === '$derived') {
if (rune === '$state' || rune === '$state.frozen' || rune === '$derived') {
return {
...node,
value:
@ -1189,8 +1185,7 @@ const template_visitors = {
inner_context.path,
metadata.namespace,
state.preserve_whitespace,
state.options.preserveComments,
true
state.options.preserveComments
);
for (const node of hoisted) {

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

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

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

@ -65,7 +65,7 @@ export class Scope {
* @param {import('estree').Identifier} node
* @param {import('#compiler').Binding['kind']} 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}
*/
declare(node, kind, declaration_kind, initial = null) {
@ -523,7 +523,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
const is_keyed =
node.key &&
(node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index);
scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const');
scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const', node);
}
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.
* @param {import('estree').Node | null | undefined} node
* @param {import('estree').Node | import('../types/template.js').EachBlock | null | undefined} node
* @param {Scope} scope
* @returns {Runes[number] | null}
*/
@ -712,5 +712,5 @@ export function get_rune(node, scope) {
const binding = scope.get(n.name);
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 { Scope } from '../phases/scope.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` */
export interface CompileResult {
@ -258,6 +258,7 @@ export interface Binding {
| 'prop'
| 'rest_prop'
| 'state'
| 'frozen_state'
| 'derived'
| 'each'
| 'store_sub'
@ -268,7 +269,13 @@ export interface Binding {
* What the value was initialized with.
* 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;
references: { node: Identifier; path: SvelteNode[] }[];
mutated: boolean;

@ -1,4 +1,5 @@
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
@ -331,7 +332,7 @@ export function prop_def(key, value, computed = false, is_static = false) {
* @returns {import('estree').TemplateElement}
*/
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 };
}

@ -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';
/**
@ -6,8 +6,6 @@ import { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
* @typedef {T & { [READONLY_SYMBOL]: Proxy<T> }} StateObject
*/
const is_frozen = Object.isFrozen;
/**
* 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;
if (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)) {
path_idx = at_idx;
}
@ -2778,6 +2782,7 @@ export function mount(component, options) {
set_current_hydration_fragment(previous_hydration_fragment);
}
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 */
const event_handle = (events) => {
@ -2785,6 +2790,9 @@ export function mount(component, options) {
const event_name = events[i];
if (!registered_events.has(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(
event_name,
bound_event_listener,
@ -2794,6 +2802,17 @@ export function mount(component, options) {
}
: 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 { subscribe_to_store } from '../../store/utils.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 {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
@ -9,7 +9,7 @@ import {
PROPS_IS_UPDATED
} from '../../constants.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 DERIVED = 1 << 1;
@ -64,7 +64,7 @@ let current_dependencies = null;
let current_dependencies_index = 0;
/** @type {null | import('./types.js').Signal[]} */
let current_untracked_writes = null;
/** @type {null | import('./types.js').Signal} */
/** @type {null | import('./types.js').SignalDebug} */
let last_inspected_signal = null;
/** If `true`, `get`ting the signal should not register it as a dependency */
export let current_untracking = false;
@ -81,7 +81,7 @@ let captured_signals = new Set();
/** @type {Function | 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 = [];
// Handle rendering tree blocks and anchors
@ -127,7 +127,6 @@ export function batch_inspect(target, prop, receiver) {
} finally {
is_batching_effect = previously_batching_effect;
if (last_inspected_signal !== null) {
// @ts-expect-error
for (const fn of last_inspected_signal.inspect) fn();
last_inspected_signal = null;
}
@ -349,7 +348,21 @@ function execute_signal_fn(signal) {
if (current_dependencies !== null) {
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) {
dependencies.length = current_dependencies_index + current_dependencies.length;
@ -365,16 +378,17 @@ function execute_signal_fn(signal) {
if (!current_skip_consumer) {
for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
const consumers = dependency.c;
if (dependency.c === null) {
if (consumers === null) {
dependency.c = [signal];
} else {
dependency.c.push(signal);
} else if (consumers[consumers.length - 1] !== signal) {
consumers.push(signal);
}
}
}
} 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;
}
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
* @param {import('./types.js').ComputationSignal<V>} signal
@ -397,36 +445,13 @@ function execute_signal_fn(signal) {
* @param {boolean} remove_unowned
* @returns {void}
*/
function remove_consumer(signal, start_index, remove_unowned) {
function remove_consumers(signal, start_index, remove_unowned) {
const dependencies = signal.d;
if (dependencies !== null) {
let i;
for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
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_consumer(
/** @type {import('./types.js').ComputationSignal<V>} **/ (dependency),
0,
true
);
}
remove_consumer(signal, dependency, remove_unowned);
}
}
}
@ -446,7 +471,7 @@ function destroy_references(signal) {
if ((reference.f & IS_EFFECT) !== 0) {
destroy_signal(reference);
} else {
remove_consumer(reference, 0, true);
remove_consumers(reference, 0, true);
reference.d = null;
}
}
@ -711,8 +736,7 @@ function update_derived(signal, force_schedule) {
// @ts-expect-error
if (DEV && signal.inspect && force_schedule) {
// @ts-expect-error
for (const fn of signal.inspect) fn();
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
@ -815,8 +839,7 @@ export function unsubscribe_on_destroy(stores) {
export function get(signal) {
// @ts-expect-error
if (DEV && signal.inspect && inspect_fn) {
// @ts-expect-error
signal.inspect.add(inspect_fn);
/** @type {import('./types.js').SignalDebug} */ (signal).inspect.add(inspect_fn);
// @ts-expect-error
inspect_captured_signals.push(signal);
}
@ -841,10 +864,16 @@ export function get(signal) {
!(unowned && current_effect !== null)
) {
current_dependencies_index++;
} else if (current_dependencies === null) {
current_dependencies = [signal];
} else if (signal !== current_dependencies[current_dependencies.length - 1]) {
current_dependencies.push(signal);
} else if (
dependencies === null ||
current_dependencies_index === 0 ||
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 (
current_untracked_writes !== null &&
@ -1079,10 +1108,9 @@ export function set_signal_value(signal, value) {
// @ts-expect-error
if (DEV && signal.inspect) {
if (is_batching_effect) {
last_inspected_signal = signal;
last_inspected_signal = /** @type {import('./types.js').SignalDebug} */ (signal);
} else {
// @ts-expect-error
for (const fn of signal.inspect) fn();
for (const fn of /** @type {import('./types.js').SignalDebug} */ (signal).inspect) fn();
}
}
}
@ -1098,7 +1126,7 @@ export function destroy_signal(signal) {
const destroy = signal.y;
const flags = signal.f;
destroy_references(signal);
remove_consumer(signal, 0, true);
remove_consumers(signal, 0, true);
signal.i =
signal.r =
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
let warned_inspect_changed = false;
@ -1817,7 +1876,7 @@ export function inspect(get_value, inspect = console.log) {
pre_effect(() => {
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) {
// eslint-disable-next-line no-console
console.warn(
@ -1899,3 +1958,25 @@ if (DEV) {
throw_rune_error('$inspect');
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) {
const transition_effect = /** @type {import('./types.js').EffectSignal} */ (current_effect);
const block = current_block;
const props = props_fn === null ? {} : props_fn();
let can_show_intro_on_mount = true;
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();
/** @param {DOMRect} [from] */
const init = (from) =>
untrack(() =>
direction === 'key'
untrack(() => {
const props = props_fn === null ? {} : props_fn();
return direction === 'key'
? /** @type {import('./types.js').AnimateFn<any>} */ (transition_fn)(
dom,
{ 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, {
direction
})
);
});
});
transition = create_transition(dom, init, direction, transition_effect);
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 SignalDebug<V = unknown> = SourceSignalDebug & Signal<V>;
export type EffectSignal = ComputationSignal<null | (() => void)>;
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_entries = Object.entries;
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 get_descriptor = Object.getOwnPropertyDescriptor;
export var get_descriptors = Object.getOwnPropertyDescriptors;

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

@ -17,6 +17,33 @@ declare module '*.svelte' {
declare function $state<T>(initial: T): T;
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.
* The expression inside `$derived(...)` should be free of side-effects.

@ -20,3 +20,13 @@ export function beforeUpdate() {}
/** @returns {void} */
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
* @type {string}
*/
export const VERSION = '5.0.0-next.26';
export const VERSION = '5.0.0-next.28';
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) {
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>

@ -2,7 +2,7 @@
"css": {
"type": "Style",
"start": 0,
"end": 586,
"end": 806,
"attributes": [],
"children": [
{
@ -601,32 +601,292 @@
},
"start": 530,
"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": {
"start": 7,
"end": 578,
"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"
"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\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": [],
"start": 588,
"end": 600,
"start": 808,
"end": 820,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 586,
"end": 588,
"start": 806,
"end": 808,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "RegularElement",
"start": 588,
"end": 600,
"start": 808,
"end": 820,
"name": "h1",
"attributes": [],
"fragment": {
@ -634,8 +894,8 @@
"nodes": [
{
"type": "Text",
"start": 592,
"end": 595,
"start": 812,
"end": 815,
"raw": "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