Merge branch 'main' into gh-12624

gh-12624
Rich Harris 1 year ago
commit 6e08f0cb53

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: always return true from `deleteProperty` trap

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: throw error if derived creates state and then depends on it

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure assignments to state field inside constructor trigger effects

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure $inspect works with SvelteMap and SvelteSet

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle deletions of previously-unread state proxy properties

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly handle proxied array length mutations

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: default options.filename to "(unknown)"

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly transform destructured `$derived.by` declarations

@ -21,6 +21,7 @@
"beige-cobras-smoke", "beige-cobras-smoke",
"beige-flies-wash", "beige-flies-wash",
"beige-gifts-appear", "beige-gifts-appear",
"beige-lamps-ring",
"beige-mirrors-listen", "beige-mirrors-listen",
"beige-rabbits-shave", "beige-rabbits-shave",
"beige-seas-share", "beige-seas-share",
@ -82,6 +83,7 @@
"clean-cats-wave", "clean-cats-wave",
"clean-eels-beg", "clean-eels-beg",
"clean-melons-wash", "clean-melons-wash",
"clean-shirts-yawn",
"clever-chefs-relate", "clever-chefs-relate",
"clever-maps-travel", "clever-maps-travel",
"clever-rockets-burn", "clever-rockets-burn",
@ -198,6 +200,7 @@
"fifty-rice-wait", "fifty-rice-wait",
"fifty-steaks-float", "fifty-steaks-float",
"fifty-toys-invite", "fifty-toys-invite",
"five-maps-reflect",
"five-tigers-search", "five-tigers-search",
"flat-feet-visit", "flat-feet-visit",
"flat-ghosts-fly", "flat-ghosts-fly",
@ -273,12 +276,14 @@
"great-fans-unite", "great-fans-unite",
"great-icons-retire", "great-icons-retire",
"great-plums-pretend", "great-plums-pretend",
"green-baboons-sip",
"green-eggs-approve", "green-eggs-approve",
"green-fishes-lie", "green-fishes-lie",
"green-hounds-play", "green-hounds-play",
"green-snails-tickle", "green-snails-tickle",
"green-tigers-judge", "green-tigers-judge",
"green-walls-clap", "green-walls-clap",
"green-windows-tap",
"grumpy-avocados-fetch", "grumpy-avocados-fetch",
"grumpy-insects-sleep", "grumpy-insects-sleep",
"grumpy-jars-sparkle", "grumpy-jars-sparkle",
@ -419,6 +424,7 @@
"mighty-files-hammer", "mighty-files-hammer",
"mighty-frogs-obey", "mighty-frogs-obey",
"mighty-paws-smash", "mighty-paws-smash",
"mighty-poets-fix",
"mighty-shoes-nail", "mighty-shoes-nail",
"modern-apricots-promise", "modern-apricots-promise",
"modern-fishes-double", "modern-fishes-double",
@ -478,6 +484,7 @@
"olive-cobras-wonder", "olive-cobras-wonder",
"olive-forks-grin", "olive-forks-grin",
"olive-kangaroos-brake", "olive-kangaroos-brake",
"olive-llamas-warn",
"olive-mice-fix", "olive-mice-fix",
"olive-moons-act", "olive-moons-act",
"olive-seals-sell", "olive-seals-sell",
@ -538,6 +545,7 @@
"quiet-cobras-smile", "quiet-cobras-smile",
"quiet-crabs-nail", "quiet-crabs-nail",
"quiet-timers-speak", "quiet-timers-speak",
"rare-ears-agree",
"rare-insects-tell", "rare-insects-tell",
"rare-mirrors-act", "rare-mirrors-act",
"rare-pears-whisper", "rare-pears-whisper",
@ -606,6 +614,7 @@
"shiny-rats-heal", "shiny-rats-heal",
"shiny-shrimps-march", "shiny-shrimps-march",
"shiny-starfishes-cross", "shiny-starfishes-cross",
"shiny-wombats-argue",
"short-buses-camp", "short-buses-camp",
"short-countries-rush", "short-countries-rush",
"short-starfishes-beg", "short-starfishes-beg",
@ -628,6 +637,7 @@
"six-boats-shave", "six-boats-shave",
"six-chicken-kneel", "six-chicken-kneel",
"six-gorillas-obey", "six-gorillas-obey",
"six-moons-invent",
"six-vans-add", "six-vans-add",
"sixty-items-crash", "sixty-items-crash",
"sixty-numbers-hope", "sixty-numbers-hope",
@ -820,7 +830,9 @@
"weak-frogs-bow", "weak-frogs-bow",
"weak-terms-destroy", "weak-terms-destroy",
"wet-bats-exercise", "wet-bats-exercise",
"wet-donkeys-fry",
"wet-games-fly", "wet-games-fly",
"wet-pears-buy",
"wet-pears-remain", "wet-pears-remain",
"wet-wombats-repeat", "wet-wombats-repeat",
"wicked-bikes-matter", "wicked-bikes-matter",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly hydrate empty raw blocks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make internal sources ownerless

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: repair `href` attribute mismatches

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: join text nodes separated by comments

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow non-synchronous legacy component instantiation

@ -2,12 +2,12 @@
# see: https://github.com/sveltejs/svelte/pull/9609 # see: https://github.com/sveltejs/svelte/pull/9609
documentation/docs/05-misc/03-typescript.md documentation/docs/05-misc/03-typescript.md
# The following are all duplicated with prettierignore configs in the packages,
# which is necessary because of https://github.com/prettier/prettier-vscode/issues/3424
packages/**/dist/*.js packages/**/dist/*.js
packages/**/build/*.js packages/**/build/*.js
packages/**/npm/**/* packages/**/npm/**/*
packages/**/config/*.js packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md packages/svelte/messages/**/*.md
packages/svelte/src/compiler/errors.js packages/svelte/src/compiler/errors.js
packages/svelte/src/compiler/warnings.js packages/svelte/src/compiler/warnings.js
@ -17,6 +17,7 @@ packages/svelte/src/internal/shared/errors.js
packages/svelte/src/internal/shared/warnings.js packages/svelte/src/internal/shared/warnings.js
packages/svelte/src/internal/server/errors.js packages/svelte/src/internal/server/errors.js
packages/svelte/tests/migrate/samples/*/output.svelte packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected* packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual* packages/svelte/tests/**/_actual*
packages/svelte/tests/**/expected* packages/svelte/tests/**/expected*
@ -29,6 +30,7 @@ packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output playgrounds/sandbox/output
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json sites/svelte.dev/static/svelte-app.json
sites/svelte.dev/scripts/svelte-app/ sites/svelte.dev/scripts/svelte-app/
sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg

@ -1,22 +1,6 @@
{ {
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Playground: Browser",
"url": "http://localhost:10001"
},
{
"type": "node",
"request": "launch",
"runtimeArgs": ["--watch"],
"name": "Playground: Server",
"outputCapture": "std",
"program": "start.js",
"cwd": "${workspaceFolder}/playgrounds/demo",
"cascadeTerminateToConfigurations": ["Playground: Browser"]
},
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
@ -26,11 +10,5 @@
"NODE_OPTIONS": "--stack-trace-limit=10000" "NODE_OPTIONS": "--stack-trace-limit=10000"
} }
} }
],
"compounds": [
{
"name": "Playground: Full",
"configurations": ["Playground: Server", "Playground: Browser"]
}
] ]
} }

@ -3,7 +3,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
import { busy } from './util.js'; import { busy } from './util.js';
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let computed1 = $.derived(() => $.get(head)); let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0)); let computed2 = $.derived(() => ($.get(computed1), 0));
let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation let computed3 = $.derived(() => (busy(), $.get(computed2) + 1)); // heavy computation

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let last = head; let last = head;
let counter = 0; let counter = 0;

@ -5,7 +5,7 @@ let len = 50;
const iter = 50; const iter = 50;
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = head; let current = head;
for (let i = 0; i < len; i++) { for (let i = 0; i < len; i++) {
let c = current; let c = current;

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let width = 5; let width = 5;
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = []; let current = [];
for (let i = 0; i < width; i++) { for (let i = 0; i < width; i++) {
current.push( current.push(

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() { function setup() {
let heads = new Array(100).fill(null).map((_) => $.source(0)); let heads = new Array(100).fill(null).map((_) => $.state(0));
const mux = $.derived(() => { const mux = $.derived(() => {
return Object.fromEntries(heads.map((h) => $.get(h)).entries()); return Object.fromEntries(heads.map((h) => $.get(h)).entries());
}); });

@ -4,7 +4,7 @@ import * as $ from '../../../packages/svelte/src/internal/client/index.js';
let size = 30; let size = 30;
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = $.derived(() => { let current = $.derived(() => {
let result = 0; let result = 0;
for (let i = 0; i < size; i++) { for (let i = 0; i < size; i++) {

@ -11,7 +11,7 @@ function count(number) {
} }
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
let current = head; let current = head;
let list = []; let list = [];
for (let i = 0; i < width; i++) { for (let i = 0; i < width; i++) {

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js'; import * as $ from '../../../packages/svelte/src/internal/client/index.js';
function setup() { function setup() {
let head = $.source(0); let head = $.state(0);
const double = $.derived(() => $.get(head) * 2); const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head)); const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => { let current = $.derived(() => {

@ -1,7 +1,6 @@
export function busy() { export function busy() {
let a = 0; let a = 0;
for (let i = 0; i < 1_00; i++) { for (let i = 0; i < 1_00; i++) {
a++; a++;
} }
} }

@ -20,8 +20,8 @@ const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() { function setup() {
let res = []; let res = [];
const A = $.source(0); const A = $.state(0);
const B = $.source(0); const B = $.state(0);
const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2)); const C = $.derived(() => ($.get(A) % 2) + ($.get(B) % 2));
const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2))); const D = $.derived(() => numbers.map((i) => i + ($.get(A) % 2) - ($.get(B) % 2)));
D.equals = function (/** @type {number[]} */ l) { D.equals = function (/** @type {number[]} */ l) {

@ -9,7 +9,7 @@ const COUNT = 1e5;
*/ */
function create_data_signals(n, sources) { function create_data_signals(n, sources) {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
sources[i] = $.source(i); sources[i] = $.state(i);
} }
return sources; return sources;
} }

@ -33,6 +33,8 @@ To run _side-effects_ when the component is mounted to the DOM, and when values
The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied. The function passed to `$effect` will run when the component mounts, and will re-run after any changes to the values it reads that were declared with `$state` or `$derived` (including those passed in with `$props`). Re-runs are batched (i.e. changing `color` and `size` in the same moment won't cause two separate runs), and happen after any DOM updates have been applied.
You can place `$effect` anywhere, not just at the top level of a component, as long as it is called during component initialization (or while a parent effect is active). It is then tied to the lifecycle of the component (or parent effect) and will therefore destroy itself when the component unmounts (or the parent effect is destroyed).
You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)). You can return a function from `$effect`, which will run immediately before the effect re-runs, and before it is destroyed ([demo](/#H4sIAAAAAAAAE42SzW6DMBCEX2Vl5RDaVCQ9JoDUY--9lUox9lKsGBvZC1GEePcaKPnpqSe86_m0M2t6ViqNnu0_e2Z4jWzP3pqGbRhdmrHwHWrCUHvbOjF2Ei-caijLTU4aCYRtDUEKK0-ccL2NDstNrbRWHoU10t8Eu-121gTVCssSBa3XEaQZ9GMrpziGj0p5OAccCgSHwmEgJZwrNNihg6MyhK7j-gii4uYb_YyGUZ5guQwzPdL7b_U4ZNSOvp9T2B3m1rB5cLx4zMkhtc7AHz7YVCVwEFzrgosTBMuNs52SKDegaPbvWnMH8AhUXaNUIY6-hHCldQhUIcyLCFlfAuHvkCKaYk8iYevGGgy2wyyJnpy9oLwG0sjdNe2yhGhJN32HsUzi2xOapNpl_bSLIYnDeeoVLZE1YI3QSpzSfo7-8J5PKbwOmdf2jC6JZyD7HxpPaMk93aHhF6utVKVCyfbkWhy-hh9Z3o_2nQIAAA==)).
```svelte ```svelte

@ -39,7 +39,8 @@ export default [
{ {
languageOptions: { languageOptions: {
parserOptions: { parserOptions: {
project: true projectService: true,
tsconfigRootDir: import.meta.dirname
} }
}, },
plugins: { plugins: {
@ -64,10 +65,14 @@ export default [
} }
}, },
{ {
files: ['playgrounds/**/*'], // If you get an error along the lines of "@typescript-eslint/await-thenable needs a project service configured", then that likely means
// that eslint rules that need to be type-aware run through a Svelte file which seems unsupported at the moment. In that case, ensure that
// these are excluded to run on Svelte files.
files: ['**/*.svelte'],
rules: { rules: {
'lube/svelte-naming-convention': 'off', '@typescript-eslint/await-thenable': 'off',
'no-console': 'off' '@typescript-eslint/prefer-promise-reject-errors': 'off',
'@typescript-eslint/require-await': 'off'
} }
}, },
{ {
@ -87,6 +92,12 @@ export default [
'packages/svelte/src/internal/client/warnings.js', 'packages/svelte/src/internal/client/warnings.js',
'packages/svelte/src/internal/shared/warnings.js', 'packages/svelte/src/internal/shared/warnings.js',
'packages/svelte/compiler/index.js', 'packages/svelte/compiler/index.js',
// stuff we don't want to lint
'benchmarking/**',
'coverage/**',
'playgrounds/sandbox/**',
// exclude top level config files
'*.config.js',
// documentation can contain invalid examples // documentation can contain invalid examples
'documentation', 'documentation',
// contains a fork of the REPL which doesn't adhere to eslint rules // contains a fork of the REPL which doesn't adhere to eslint rules

@ -18,8 +18,8 @@
"build:sites": "pnpm -r --filter=./sites/* build", "build:sites": "pnpm -r --filter=./sites/* build",
"preview-site": "npm run build --prefix sites/svelte-5-preview", "preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check", "check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"lint": "pnpm -r lint && prettier --check documentation", "lint": "eslint && prettier --check .",
"format": "pnpm -r format && prettier --check --write documentation", "format": "prettier --write .",
"test": "vitest run", "test": "vitest run",
"test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json", "test-output": "vitest run --coverage --reporter=json --outputFile=sites/svelte-5-preview/src/routes/status/results.json",
"changeset:version": "changeset version && pnpm -r generate:version && git add --all", "changeset:version": "changeset version && pnpm -r generate:version && git add --all",
@ -33,18 +33,18 @@
"@sveltejs/eslint-config": "^8.0.1", "@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0", "@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5", "@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1", "@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.6.0", "eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3", "eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0", "jsdom": "25.0.0",
"playwright": "^1.41.1", "playwright": "^1.46.1",
"prettier": "^3.2.4", "prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"svelte": "workspace:^", "svelte": "workspace:^",
"typescript": "^5.5.2", "typescript": "^5.5.4",
"typescript-eslint": "^8.0.0-alpha.34", "typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5", "v8-natives": "^1.2.5",
"vitest": "^1.2.1" "vitest": "^2.0.5"
}, },
"pnpm": { "pnpm": {
"overrides": { "overrides": {

@ -1,22 +0,0 @@
dist/*.js
build/*.js
npm/**/*
config/*.js
messages/**/*.md
src/compiler/errors.js
src/compiler/warnings.js
src/internal/client/errors.js
src/internal/client/warnings.js
src/internal/shared/errors.js
src/internal/shared/warnings.js
src/internal/server/errors.js
tests/**/*.svelte
tests/**/_expected*
tests/**/_actual*
tests/**/expected*
tests/**/_output
tests/**/shards/*.test.js
tests/hydration/samples/*/_expected.html
tests/hydration/samples/*/_override.html
types
compiler/index.js

@ -1,5 +1,45 @@
# svelte # svelte
## 5.0.0-next.238
### Patch Changes
- fix: always return true from `deleteProperty` trap ([#13008](https://github.com/sveltejs/svelte/pull/13008))
- fix: handle deletions of previously-unread state proxy properties ([#13008](https://github.com/sveltejs/svelte/pull/13008))
- fix: make internal sources ownerless ([#13013](https://github.com/sveltejs/svelte/pull/13013))
- fix: join text nodes separated by comments ([#13009](https://github.com/sveltejs/svelte/pull/13009))
## 5.0.0-next.237
### Patch Changes
- breaking: throw error if derived creates state and then depends on it ([#12985](https://github.com/sveltejs/svelte/pull/12985))
- fix: ensure assignments to state field inside constructor trigger effects ([#12985](https://github.com/sveltejs/svelte/pull/12985))
- fix: ensure $inspect works with SvelteMap and SvelteSet ([#12994](https://github.com/sveltejs/svelte/pull/12994))
- chore: default options.filename to "(unknown)" ([#12997](https://github.com/sveltejs/svelte/pull/12997))
- feat: allow non-synchronous legacy component instantiation ([#12970](https://github.com/sveltejs/svelte/pull/12970))
## 5.0.0-next.236
### Patch Changes
- fix: properly transform destructured `$derived.by` declarations ([#12984](https://github.com/sveltejs/svelte/pull/12984))
## 5.0.0-next.235
### Patch Changes
- chore: update client check for smaller bundle size ([#12975](https://github.com/sveltejs/svelte/pull/12975))
- fix: correctly hydrate empty raw blocks ([#12979](https://github.com/sveltejs/svelte/pull/12979))
## 5.0.0-next.234 ## 5.0.0-next.234
### Patch Changes ### Patch Changes

@ -0,0 +1,18 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js",
"tests/**/*.ts",
"!tests/**/*.svelte",
"!tests/**/*.svelte.js",
"!tests/**/_output",
"!tests/runtime-browser/driver.js",
"!tests/runtime-browser/driver-ssr.js",
"!tests/types/component.ts"
],
"project": ["src/**"]
}

@ -72,10 +72,10 @@
> Cannot set prototype of `$state` object > Cannot set prototype of `$state` object
## state_unsafe_mutation ## state_unsafe_local_read
> Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state` > Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
## svelte_component_invalid_this_value ## state_unsafe_mutation
> The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined > Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`

@ -302,10 +302,6 @@ HTML restricts where certain elements can appear. In case of a violation the bro
> `<svelte:fragment>` must be the direct child of a component > `<svelte:fragment>` must be the direct child of a component
## svelte_fragment_invalid_slot
> `<svelte:fragment>` slot attribute must have a static value
## svelte_head_illegal_attribute ## svelte_head_illegal_attribute
> `<svelte:head>` cannot have attributes nor directives > `<svelte:head>` cannot have attributes nor directives

@ -1,7 +1,3 @@
## derived_iife
> Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
## export_let_unused ## export_let_unused
> Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%` > Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`

@ -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.234", "version": "5.0.0-next.238",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -112,50 +112,38 @@
"generate:version": "node ./scripts/generate-version.js", "generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json", "generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build", "prepublishOnly": "pnpm build",
"format": "prettier --check --write .",
"lint": "prettier --check . && eslint",
"knip": "pnpm dlx knip" "knip": "pnpm dlx knip"
}, },
"devDependencies": { "devDependencies": {
"@jridgewell/trace-mapping": "^0.3.22", "@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.35.1", "@playwright/test": "^1.46.1",
"@rollup/plugin-commonjs": "^25.0.7", "@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2", "@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4", "@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.1", "dts-buddy": "^0.5.1",
"esbuild": "^0.19.11", "esbuild": "^0.21.5",
"rollup": "^4.9.5", "rollup": "^4.21.0",
"source-map": "^0.7.4", "source-map": "^0.7.4",
"tiny-glob": "^0.2.9" "tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
}, },
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5", "@types/estree": "^1.0.5",
"acorn": "^8.11.3", "acorn": "^8.12.1",
"acorn-typescript": "^1.4.13", "acorn-typescript": "^1.4.13",
"aria-query": "^5.3.0", "aria-query": "^5.3.0",
"axobject-query": "^4.0.0", "axobject-query": "^4.1.0",
"esm-env": "^1.0.0", "esm-env": "^1.0.0",
"esrap": "^1.2.2", "esrap": "^1.2.2",
"is-reference": "^3.0.2", "is-reference": "^3.0.2",
"locate-character": "^3.0.0", "locate-character": "^3.0.0",
"magic-string": "^0.30.5", "magic-string": "^0.30.11",
"zimmerframe": "^1.1.2" "zimmerframe": "^1.1.2"
},
"knip": {
"entry": [
"src/*/index.js",
"src/index-client.ts",
"src/index-server.ts",
"src/index.d.ts",
"tests/**/*.js",
"tests/**/*.ts"
],
"project": [
"src/**"
]
} }
} }

@ -1246,15 +1246,6 @@ export function svelte_fragment_invalid_placement(node) {
e(node, "svelte_fragment_invalid_placement", "`<svelte:fragment>` must be the direct child of a component"); e(node, "svelte_fragment_invalid_placement", "`<svelte:fragment>` must be the direct child of a component");
} }
/**
* `<svelte:fragment>` slot attribute must have a static value
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_fragment_invalid_slot(node) {
e(node, "svelte_fragment_invalid_slot", "`<svelte:fragment>` slot attribute must have a static value");
}
/** /**
* `<svelte:head>` cannot have attributes nor directives * `<svelte:head>` cannot have attributes nor directives
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -106,7 +106,7 @@ export function compileModule(source, options) {
*/ */
export function parse(source, { filename, rootDir, modern } = {}) { export function parse(source, { filename, rootDir, modern } = {}) {
state.reset_warning_filter(() => false); state.reset_warning_filter(() => false);
state.reset(source, { filename, rootDir }); // TODO it's weird to require filename/rootDir here. reconsider the API state.reset(source, { filename: filename ?? '(unknown)', rootDir });
const ast = _parse(source); const ast = _parse(source);
return to_public_ast(source, ast, modern); return to_public_ast(source, ast, modern);

@ -243,7 +243,7 @@ export function analyze_module(ast, options) {
return { return {
module: { ast, scope, scopes }, module: { ast, scope, scopes },
name: options.filename || 'module', name: options.filename,
accessors: false, accessors: false,
runes: true, runes: true,
immutable: true immutable: true
@ -349,7 +349,7 @@ export function analyze_component(root, source, options) {
} }
} }
const component_name = get_component_name(options.filename ?? 'Component'); const component_name = get_component_name(options.filename);
const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune); const runes = options.runes ?? Array.from(module.scope.references.keys()).some(is_rune);
@ -390,7 +390,7 @@ export function analyze_component(root, source, options) {
hash: root.css hash: root.css
? options.cssHash({ ? options.cssHash({
css: root.css.content.styles, css: root.css.content.styles,
filename: options.filename ?? '<unknown>', filename: options.filename,
name: component_name, name: component_name,
hash hash
}) })

@ -207,7 +207,7 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) { for (const [name, binding] of analysis.instance.scope.declarations) {
if (binding.kind === 'legacy_reactive') { if (binding.kind === 'legacy_reactive') {
legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_source'))); legacy_reactive_declarations.push(b.const(name, b.call('$.mutable_state')));
} }
if (binding.kind === 'store_sub') { if (binding.kind === 'store_sub') {
if (store_setup.length === 0) { if (store_setup.length === 0) {
@ -505,14 +505,12 @@ export function client_component(analysis, options) {
} }
if (dev) { if (dev) {
if (filename) { // add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later body.unshift(
body.unshift( b.stmt(
b.stmt( b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename)) )
) );
);
}
body.unshift(b.stmt(b.call(b.id('$.mark_module_start')))); body.unshift(b.stmt(b.call(b.id('$.mark_module_start'))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name)))); body.push(b.stmt(b.call(b.id('$.mark_module_end'), b.id(analysis.name))));

@ -47,19 +47,10 @@ export function build_getter(node, state) {
/** /**
* @param {Expression} value * @param {Expression} value
* @param {PrivateIdentifier | string} proxy_reference * @param {Expression} previous
*/ */
export function build_proxy_reassignment(value, proxy_reference) { export function build_proxy_reassignment(value, previous) {
return dev return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
? b.call(
'$.proxy',
value,
b.null,
typeof proxy_reference === 'string'
? b.id(proxy_reference)
: b.member(b.this, proxy_reference)
)
: b.call('$.proxy', value);
} }
/** /**

@ -48,7 +48,7 @@ function build_assignment(operator, left, right, context) {
value = value =
private_state.kind === 'raw_state' private_state.kind === 'raw_state'
? value ? value
: build_proxy_reassignment(value, private_state.id); : build_proxy_reassignment(value, b.member(b.this, private_state.id));
} }
if (!context.state.in_constructor) { if (!context.state.in_constructor) {
@ -57,20 +57,6 @@ function build_assignment(operator, left, right, context) {
return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value); return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
} }
} }
} else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name);
if (public_state !== undefined && should_proxy(right, context.state.scope)) {
const value = /** @type {Expression} */ (context.visit(right));
return b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, public_state.id)
);
}
} }
} }
@ -109,7 +95,7 @@ function build_assignment(operator, left, right, context) {
context.state.analysis.runes && context.state.analysis.runes &&
should_proxy(value, context.state.scope) should_proxy(value, context.state.scope)
) { ) {
value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object.name); value = binding.kind === 'raw_state' ? value : build_proxy_reassignment(value, object);
} }
return transform.assign(object, value); return transform.assign(object, value);

@ -113,17 +113,17 @@ export function ClassBody(node, context) {
value = value =
field.kind === 'state' field.kind === 'state'
? b.call( ? b.call(
'$.source', '$.state',
should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init should_proxy(init, context.state.scope) ? b.call('$.proxy', init) : init
) )
: field.kind === 'raw_state' : field.kind === 'raw_state'
? b.call('$.source', init) ? b.call('$.state', init)
: field.kind === 'derived_by' : field.kind === 'derived_by'
? b.call('$.derived', init) ? b.call('$.derived', 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
value = b.call('$.source'); value = b.call('$.state');
} }
if (is_private) { if (is_private) {
@ -139,12 +139,14 @@ export function ClassBody(node, context) {
if (field.kind === 'state') { if (field.kind === 'state') {
// set foo(value) { this.#foo = value; } // set foo(value) { this.#foo = value; }
const value = b.id('value'); const value = b.id('value');
const prev = b.member(b.this, field.id);
body.push( body.push(
b.method( b.method(
'set', 'set',
definition.key, definition.key,
[value], [value],
[b.stmt(b.call('$.set', member, build_proxy_reassignment(value, field.id)))] [b.stmt(b.call('$.set', member, build_proxy_reassignment(value, prev)))]
) )
); );
} }

@ -13,15 +13,6 @@ export function MemberExpression(node, context) {
if (field) { if (field) {
return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node); return context.state.in_constructor ? b.member(node, 'v') : b.call('$.get', node);
} }
} else if (node.object.type === 'ThisExpression') {
// rewrite `this.foo` as `this.#foo.v` inside a constructor
if (node.property.type === 'Identifier' && !node.computed) {
const field = context.state.public_state.get(node.property.name);
if (field && context.state.in_constructor) {
return b.member(b.member(b.this, field.id), 'v');
}
}
} }
context.next(); context.next();

@ -126,7 +126,7 @@ export function VariableDeclaration(node, context) {
value = b.call('$.proxy', value); value = b.call('$.proxy', value);
} }
if (is_state_source(binding, context.state.analysis)) { if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value); value = b.call('$.state', value);
} }
return value; return value;
}; };
@ -173,7 +173,7 @@ export function VariableDeclaration(node, context) {
let id; let id;
let rhs = value; let rhs = value;
if (init.arguments[0].type === 'Identifier') { if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0]; id = init.arguments[0];
} else { } else {
id = b.id(context.state.scope.generate('$$d')); id = b.id(context.state.scope.generate('$$d'));
@ -291,7 +291,7 @@ export function VariableDeclaration(node, context) {
*/ */
function create_state_declarators(declarator, scope, value) { function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') { if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, b.call('$.mutable_source', value))]; return [b.declarator(declarator.id, b.call('$.mutable_state', value))];
} }
const tmp = scope.generate('tmp'); const tmp = scope.generate('tmp');
@ -303,7 +303,7 @@ function create_state_declarators(declarator, scope, value) {
const binding = scope.get(/** @type {Identifier} */ (path.node).name); const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator( return b.declarator(
path.node, path.node,
binding?.kind === 'state' ? b.call('$.mutable_source', value) : value binding?.kind === 'state' ? b.call('$.mutable_state', value) : value
); );
}) })
]; ];

@ -60,9 +60,9 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
* @param {Sequence} sequence * @param {Sequence} sequence
*/ */
function flush_sequence(sequence) { function flush_sequence(sequence) {
if (sequence.length === 1 && sequence[0].type === 'Text') { if (sequence.every((node) => node.type === 'Text')) {
skipped += 1; skipped += 1;
state.template.push(sequence[0].raw); state.template.push(sequence.map((node) => node.raw).join(''));
return; return;
} }

@ -80,7 +80,7 @@ export function transform_module(analysis, source, options) {
? server_module(analysis, options) ? server_module(analysis, options)
: client_module(analysis, options); : client_module(analysis, options);
const basename = (options.filename ?? 'Module').split(/[/\\]/).at(-1); const basename = options.filename.split(/[/\\]/).at(-1);
if (program.body.length > 0) { if (program.body.length > 0) {
program.body[0].leadingComments = [ program.body[0].leadingComments = [
{ {

@ -356,7 +356,7 @@ export function server_component(analysis, options) {
body.push(b.export_default(component_function)); body.push(b.export_default(component_function));
} }
if (dev && filename) { if (dev) {
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later // add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift( body.unshift(
b.stmt( b.stmt(

@ -8,9 +8,9 @@ import { getLocator } from 'locate-character';
export let warnings = []; export let warnings = [];
/** /**
* The filename (if specified in the compiler options) relative to the rootDir (if specified). * The filename relative to the rootDir (if specified).
* This should not be used in the compiler output except in dev mode * This should not be used in the compiler output except in dev mode
* @type {string | undefined} * @type {string}
*/ */
export let filename; export let filename;
@ -77,20 +77,16 @@ export function is_ignored(node, code) {
/** /**
* @param {string} _source * @param {string} _source
* @param {{ dev?: boolean; filename?: string; rootDir?: string }} options * @param {{ dev?: boolean; filename: string; rootDir?: string }} options
*/ */
export function reset(_source, options) { export function reset(_source, options) {
source = _source; source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/'); const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/'); filename = options.filename.replace(/\\/g, '/');
dev = !!options.dev; dev = !!options.dev;
if ( if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
typeof filename === 'string' &&
typeof root_dir === 'string' &&
filename.startsWith(root_dir)
) {
// make filename relative to rootDir // make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, ''); filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
} }

@ -56,7 +56,7 @@ export interface CompileError extends ICompileDiagnostic {}
export type CssHashGetter = (args: { export type CssHashGetter = (args: {
name: string; name: string;
filename: string | undefined; filename: string;
css: string; css: string;
hash: (input: string) => string; hash: (input: string) => string;
}) => string; }) => string;
@ -219,11 +219,7 @@ export interface ModuleCompileOptions {
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still // The following two somewhat scary looking types ensure that certain types are required but can be undefined still
export type ValidatedModuleCompileOptions = Omit< export type ValidatedModuleCompileOptions = Omit<Required<ModuleCompileOptions>, 'rootDir'> & {
Required<ModuleCompileOptions>,
'filename' | 'rootDir'
> & {
filename: ModuleCompileOptions['filename'];
rootDir: ModuleCompileOptions['rootDir']; rootDir: ModuleCompileOptions['rootDir'];
}; };

@ -352,7 +352,7 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition} * @returns {ESTree.PropertyDefinition}
*/ */
export function prop_def(key, value, computed = false, is_static = false) { export function prop_def(key, value, computed = false, is_static = false) {
return { type: 'PropertyDefinition', key, value, computed, static: is_static }; return { type: 'PropertyDefinition', key, value, computed, static: is_static, decorators: [] };
} }
/** /**
@ -541,7 +541,8 @@ export function method(kind, key, params, body, computed = false, is_static = fa
kind, kind,
value: function_builder(null, params, block(body)), value: function_builder(null, params, block(body)),
computed, computed,
static: is_static static: is_static,
decorators: []
}; };
} }

@ -328,7 +328,7 @@ function apply_preprocessor_sourcemap(filename, svelte_map, preprocessor_map_inp
} }
} }
}); });
return /** @type {SourceMap} */ (result_map); return /** @type {any} */ (result_map);
} }
const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/; const regex_data_uri = /data:(?:application|text)\/json;(?:charset[:=]\S+?;)?base64,(\S*)/;
// parse attached sourcemap in processed.code // parse attached sourcemap in processed.code
@ -393,7 +393,7 @@ export function parse_attached_sourcemap(processed, tag_name) {
*/ */
export function merge_with_preprocessor_map(result, options, source_name) { export function merge_with_preprocessor_map(result, options, source_name) {
if (options.sourcemap) { if (options.sourcemap) {
const file_basename = get_basename(options.filename || 'input.svelte'); const file_basename = get_basename(options.filename);
// The preprocessor map is expected to contain `sources: [basename_of_filename]`, but our own // The preprocessor map is expected to contain `sources: [basename_of_filename]`, but our own
// map may contain a different file name. Patch our map beforehand to align sources so merging // map may contain a different file name. Patch our map beforehand to align sources so merging
// with the preprocessor map works correctly. // with the preprocessor map works correctly.
@ -442,11 +442,10 @@ export function get_basename(filename) {
} }
/** /**
* @param {string | undefined} filename * @param {string} filename
* @param {string | undefined} output_filename * @param {string | undefined} output_filename
* @param {string} fallback * @param {string} fallback
*/ */
export function get_source_name(filename, output_filename, fallback) { export function get_source_name(filename, output_filename, fallback) {
if (!filename) return fallback;
return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename); return output_filename ? get_relative_path(output_filename, filename) : get_basename(filename);
} }

@ -9,7 +9,7 @@ import * as w from './warnings.js';
*/ */
const common = { const common = {
filename: string(undefined), filename: string('(unknown)'),
// default to process.cwd() where it exists to replicate svelte4 behavior // default to process.cwd() where it exists to replicate svelte4 behavior
// see https://github.com/sveltejs/svelte/blob/b62fc8c8fd2640c9b99168f01b9d958cb2f7574f/packages/svelte/src/compiler/compile/Component.js#L211 // see https://github.com/sveltejs/svelte/blob/b62fc8c8fd2640c9b99168f01b9d958cb2f7574f/packages/svelte/src/compiler/compile/Component.js#L211

@ -94,7 +94,6 @@ export const codes = [
"options_removed_hydratable", "options_removed_hydratable",
"options_removed_loop_guard_timeout", "options_removed_loop_guard_timeout",
"options_renamed_ssr_dom", "options_renamed_ssr_dom",
"derived_iife",
"export_let_unused", "export_let_unused",
"legacy_component_creation", "legacy_component_creation",
"non_reactive_update", "non_reactive_update",
@ -574,14 +573,6 @@ export function options_renamed_ssr_dom(node) {
w(node, "options_renamed_ssr_dom", "`generate: \"dom\"` and `generate: \"ssr\"` options have been renamed to \"client\" and \"server\" respectively"); w(node, "options_renamed_ssr_dom", "`generate: \"dom\"` and `generate: \"ssr\"` options have been renamed to \"client\" and \"server\" respectively");
} }
/**
* Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
* @param {null | NodeLike} node
*/
export function derived_iife(node) {
w(node, "derived_iife", "Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`");
}
/** /**
* Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%` * Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%`
* @param {null | NodeLike} node * @param {null | NodeLike} node

@ -17,6 +17,8 @@ export interface ComponentConstructorOptions<
context?: Map<any, any>; context?: Map<any, any>;
hydrate?: boolean; hydrate?: boolean;
intro?: boolean; intro?: boolean;
recover?: boolean;
sync?: boolean;
$$inline?: boolean; $$inline?: boolean;
} }

@ -52,10 +52,8 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) {
/** @type {Effect | null} */ /** @type {Effect | null} */
var catch_effect; var catch_effect;
var input_source = runes var input_source = (runes ? source : mutable_source)(/** @type {V} */ (undefined));
? source(/** @type {V} */ (undefined)) var error_source = (runes ? source : mutable_source)(undefined);
: mutable_source(/** @type {V} */ (undefined));
var error_source = runes ? source(undefined) : mutable_source(undefined);
var resolved = false; var resolved = false;
/** /**

@ -47,15 +47,20 @@ export function html(node, get_value, svg, mathml, skip_warning) {
var value = ''; var value = '';
/** @type {Effect | null} */ /** @type {Effect | undefined} */
var effect; var effect;
block(() => { block(() => {
if (value === (value = get_value())) return; if (value === (value = get_value())) {
if (hydrating) {
hydrate_next();
}
return;
}
if (effect) { if (effect !== undefined) {
destroy_effect(effect); destroy_effect(effect);
effect = null; effect = undefined;
} }
if (value === '') return; if (value === '') return;

@ -90,7 +90,11 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (hydrating) { if (hydrating) {
attributes[attribute] = element.getAttribute(attribute); attributes[attribute] = element.getAttribute(attribute);
if (attribute === 'src' || attribute === 'href' || attribute === 'srcset') { if (
attribute === 'src' ||
attribute === 'srcset' ||
(attribute === 'href' && element.nodeName === 'LINK')
) {
if (!skip_warning) { if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value); check_src_in_dev_hydration(element, attribute, value);
} }
@ -407,7 +411,7 @@ function check_src_in_dev_hydration(element, attribute, value) {
w.hydration_attribute_changed( w.hydration_attribute_changed(
attribute, attribute,
element.outerHTML.replace(element.innerHTML, '...'), element.outerHTML.replace(element.innerHTML, element.innerHTML && '...'),
String(value) String(value)
); );
} }

@ -310,11 +310,7 @@ export function apply(
handler.apply(element, args); handler.apply(element, args);
} else if (has_side_effects || handler != null) { } else if (has_side_effects || handler != null) {
const filename = component?.[FILENAME]; const filename = component?.[FILENAME];
const location = filename const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
? loc
? ` at ${filename}:${loc[0]}:${loc[1]}`
: ` in ${filename}`
: '';
const event_name = args[0].type; const event_name = args[0].type;
const description = `\`${event_name}\` handler${location}`; const description = `\`${event_name}\` handler${location}`;

@ -311,33 +311,33 @@ export function state_prototype_fixed() {
} }
/** /**
* Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state` * Reading state that was created inside the same derived is forbidden. Consider using `untrack` to read locally created state
* @returns {never} * @returns {never}
*/ */
export function state_unsafe_mutation() { export function state_unsafe_local_read() {
if (DEV) { if (DEV) {
const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived is forbidden. If the value should not be reactive, declare it without \`$state\``); const error = new Error(`state_unsafe_local_read\nReading state that was created inside the same derived is forbidden. Consider using \`untrack\` to read locally created state`);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
// TODO print a link to the documentation // TODO print a link to the documentation
throw new Error("state_unsafe_mutation"); throw new Error("state_unsafe_local_read");
} }
} }
/** /**
* The `this={...}` property of a `<svelte:component>` must be a Svelte component, if defined * Updating state inside a derived is forbidden. If the value should not be reactive, declare it without `$state`
* @returns {never} * @returns {never}
*/ */
export function svelte_component_invalid_this_value() { export function state_unsafe_mutation() {
if (DEV) { if (DEV) {
const error = new Error(`svelte_component_invalid_this_value\nThe \`this={...}\` property of a \`<svelte:component>\` must be a Svelte component, if defined`); const error = new Error(`state_unsafe_mutation\nUpdating state inside a derived is forbidden. If the value should not be reactive, declare it without \`$state\``);
error.name = 'Svelte error'; error.name = 'Svelte error';
throw error; throw error;
} else { } else {
// TODO print a link to the documentation // TODO print a link to the documentation
throw new Error("svelte_component_invalid_this_value"); throw new Error("state_unsafe_mutation");
} }
} }

@ -106,7 +106,7 @@ export {
user_effect, user_effect,
user_pre_effect user_pre_effect
} from './reactivity/effects.js'; } from './reactivity/effects.js';
export { mutable_source, mutate, source, set } from './reactivity/sources.js'; export { mutable_state, mutate, set, state } from './reactivity/sources.js';
export { export {
prop, prop,
rest_props, rest_props,

@ -19,7 +19,7 @@ import * as e from './errors.js';
* @param {T} value * @param {T} value
* @param {ProxyMetadata | null} [parent] * @param {ProxyMetadata | null} [parent]
* @param {Source<T>} [prev] dev mode only * @param {Source<T>} [prev] dev mode only
* @returns {ProxyStateObject<T> | T} * @returns {T}
*/ */
export function proxy(value, parent = null, prev) { export function proxy(value, parent = null, prev) {
// if non-proxyable, or is already a proxy, return `value` // if non-proxyable, or is already a proxy, return `value`
@ -33,10 +33,17 @@ export function proxy(value, parent = null, prev) {
return value; return value;
} }
/** @type {Map<any, Source<any>>} */
var sources = new Map(); var sources = new Map();
var is_proxied_array = is_array(value); var is_proxied_array = is_array(value);
var version = source(0); var version = source(0);
if (is_proxied_array) {
// We need to create the length source eagerly to ensure that
// mutations to the array are properly synced with our proxy
sources.set('length', source(/** @type {any[]} */ (value).length));
}
/** @type {ProxyMetadata} */ /** @type {ProxyMetadata} */
var metadata; var metadata;
@ -91,17 +98,17 @@ export function proxy(value, parent = null, prev) {
deleteProperty(target, prop) { deleteProperty(target, prop) {
var s = sources.get(prop); var s = sources.get(prop);
var exists = s !== undefined ? s.v !== UNINITIALIZED : prop in target;
if (s !== undefined) { if (s === undefined) {
if (prop in target) {
sources.set(prop, source(UNINITIALIZED));
}
} else {
set(s, UNINITIALIZED); set(s, UNINITIALIZED);
}
if (exists) {
update_version(version); update_version(version);
} }
return exists; return true;
}, },
get(target, prop, receiver) { get(target, prop, receiver) {
@ -187,6 +194,22 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop); var s = sources.get(prop);
var has = prop in target; var has = prop in target;
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < /** @type {Source<number>} */ (s).v; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) {
set(other_s, UNINITIALIZED);
} else if (i in target) {
// If the item exists in the original, we need to create a uninitialized source,
// else a later read of the property would result in a source being created with
// the value of the original item at that index.
other_s = source(UNINITIALIZED);
sources.set(i + '', other_s);
}
}
}
// If we haven't yet created a source for this property, we need to ensure // If we haven't yet created a source for this property, we need to ensure
// we do so otherwise if we read it later, then the write won't be tracked and // we do so otherwise if we read it later, then the write won't be tracked and
// the heuristics of effects will be different vs if we had read the proxied // the heuristics of effects will be different vs if we had read the proxied
@ -211,14 +234,6 @@ export function proxy(value, parent = null, prev) {
check_ownership(metadata); check_ownership(metadata);
} }
// variable.length = value -> clear all signals with index >= value
if (is_proxied_array && prop === 'length') {
for (var i = value; i < target.length; i += 1) {
var other_s = sources.get(i + '');
if (other_s !== undefined) set(other_s, UNINITIALIZED);
}
}
var descriptor = Reflect.getOwnPropertyDescriptor(target, prop); var descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
// Set the new value before updating any signals so that any listeners get the new value // Set the new value before updating any signals so that any listeners get the new value
@ -232,14 +247,11 @@ export function proxy(value, parent = null, prev) {
// to ensure that iterating over the array as a result of a metadata update // to ensure that iterating over the array as a result of a metadata update
// will not cause the length to be out of sync. // will not cause the length to be out of sync.
if (is_proxied_array && typeof prop === 'string') { if (is_proxied_array && typeof prop === 'string') {
var ls = sources.get('length'); var ls = /** @type {Source<number>} */ (sources.get('length'));
var n = Number(prop);
if (ls !== undefined) {
var n = Number(prop);
if (Number.isInteger(n) && n >= ls.v) { if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1); set(ls, n + 1);
}
} }
} }

@ -85,3 +85,49 @@ test('does not re-proxy proxies', () => {
assert.equal(inner.count, 1); assert.equal(inner.count, 1);
assert.equal(outer.inner.count, 1); assert.equal(outer.inner.count, 1);
}); });
test('deletes a property', () => {
const state = proxy({ a: 1, b: 2 } as { a?: number; b?: number; c?: number });
delete state.a;
assert.equal(JSON.stringify(state), '{"b":2}');
delete state.a;
// deleting a non-existent property should succeed
delete state.c;
});
test('handles array.push', () => {
const original = [1, 2, 3];
const state = proxy(original);
state.push(4);
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(state.length, 4);
assert.deepEqual(state, [1, 2, 3, 4]);
});
test('handles array mutation', () => {
const original = [1, 2, 3];
const state = proxy(original);
state[3] = 4;
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(state.length, 4);
assert.deepEqual(state, [1, 2, 3, 4]);
});
test('handles array length mutation', () => {
const original = [1, 2, 3];
const state = proxy(original);
state.length = 0;
assert.deepEqual(original.length, 3);
assert.deepEqual(original, [1, 2, 3]);
assert.deepEqual(original[0], 1);
assert.deepEqual(state.length, 0);
assert.deepEqual(state, []);
assert.deepEqual(state[0], undefined);
});

@ -1,4 +1,4 @@
/** @import { Derived, Effect, Source, Value } from '#client' */ /** @import { Derived, Effect, Reaction, Source, Value } from '#client' */
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { import {
current_component_context, current_component_context,
@ -13,7 +13,9 @@ import {
set_signal_status, set_signal_status,
untrack, untrack,
increment_version, increment_version,
update_effect update_effect,
derived_sources,
set_derived_sources
} from '../runtime.js'; } from '../runtime.js';
import { equals, safe_equals } from './equality.js'; import { equals, safe_equals } from './equality.js';
import { import {
@ -34,7 +36,6 @@ let inspect_effects = new Set();
* @param {V} v * @param {V} v
* @returns {Source<V>} * @returns {Source<V>}
*/ */
/*#__NO_SIDE_EFFECTS__*/
export function source(v) { export function source(v) {
return { return {
f: 0, // TODO ideally we could skip this altogether, but it causes type errors f: 0, // TODO ideally we could skip this altogether, but it causes type errors
@ -45,6 +46,14 @@ export function source(v) {
}; };
} }
/**
* @template V
* @param {V} v
*/
export function state(v) {
return push_derived_source(source(v));
}
/** /**
* @template V * @template V
* @param {V} initial_value * @param {V} initial_value
@ -64,6 +73,32 @@ export function mutable_source(initial_value) {
return s; return s;
} }
/**
* @template V
* @param {V} v
* @returns {Source<V>}
*/
export function mutable_state(v) {
return push_derived_source(mutable_source(v));
}
/**
* @template V
* @param {Source<V>} source
*/
/*#__NO_SIDE_EFFECTS__*/
function push_derived_source(source) {
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
if (derived_sources === null) {
set_derived_sources([source]);
} else {
derived_sources.push(source);
}
}
return source;
}
/** /**
* @template V * @template V
* @param {Value<V>} source * @param {Value<V>} source
@ -84,7 +119,14 @@ export function mutate(source, value) {
* @returns {V} * @returns {V}
*/ */
export function set(source, value) { export function set(source, value) {
if (current_reaction !== null && is_runes() && (current_reaction.f & DERIVED) !== 0) { if (
current_reaction !== null &&
is_runes() &&
(current_reaction.f & DERIVED) !== 0 &&
// If the source was created locally within the current derived, then
// we allow the mutation.
(derived_sources === null || !derived_sources.includes(source))
) {
e.state_unsafe_mutation(); e.state_unsafe_mutation();
} }

@ -80,6 +80,20 @@ export function set_current_effect(effect) {
current_effect = effect; current_effect = effect;
} }
/**
* When sources are created within a derived, we record them so that we can safely allow
* local mutations to these sources without the side-effect error being invoked unnecessarily.
* @type {null | Source[]}
*/
export let derived_sources = null;
/**
* @param {Source[] | null} sources
*/
export function set_derived_sources(sources) {
derived_sources = sources;
}
/** /**
* The dependencies of the reaction that is currently being executed. In many cases, * The dependencies of the reaction that is currently being executed. In many cases,
* the dependencies are unchanged between runs, and so this will be `null` unless * the dependencies are unchanged between runs, and so this will be `null` unless
@ -230,12 +244,14 @@ function handle_error(error, effect, component_context) {
let current_context = component_context; let current_context = component_context;
while (current_context !== null) { while (current_context !== null) {
/** @type {string} */ if (DEV) {
var filename = current_context.function?.[FILENAME]; /** @type {string} */
var filename = current_context.function?.[FILENAME];
if (filename) { if (filename) {
const file = filename.split('/').pop(); const file = filename.split('/').pop();
component_stack.push(file); component_stack.push(file);
}
} }
current_context = current_context.p; current_context = current_context.p;
@ -279,12 +295,14 @@ export function update_reaction(reaction) {
var previous_untracked_writes = current_untracked_writes; var previous_untracked_writes = current_untracked_writes;
var previous_reaction = current_reaction; var previous_reaction = current_reaction;
var previous_skip_reaction = current_skip_reaction; var previous_skip_reaction = current_skip_reaction;
var prev_derived_sources = derived_sources;
new_deps = /** @type {null | Value[]} */ (null); new_deps = /** @type {null | Value[]} */ (null);
skipped_deps = 0; skipped_deps = 0;
current_untracked_writes = null; current_untracked_writes = null;
current_reaction = (reaction.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null; current_reaction = (reaction.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 ? reaction : null;
current_skip_reaction = !is_flushing_effect && (reaction.f & UNOWNED) !== 0; current_skip_reaction = !is_flushing_effect && (reaction.f & UNOWNED) !== 0;
derived_sources = null;
try { try {
var result = /** @type {Function} */ (0, reaction.fn)(); var result = /** @type {Function} */ (0, reaction.fn)();
@ -321,6 +339,7 @@ export function update_reaction(reaction) {
current_untracked_writes = previous_untracked_writes; current_untracked_writes = previous_untracked_writes;
current_reaction = previous_reaction; current_reaction = previous_reaction;
current_skip_reaction = previous_skip_reaction; current_skip_reaction = previous_skip_reaction;
derived_sources = prev_derived_sources;
} }
} }
@ -698,6 +717,9 @@ export function get(signal) {
// Register the dependency on the current reaction signal. // Register the dependency on the current reaction signal.
if (current_reaction !== null) { if (current_reaction !== null) {
if (derived_sources !== null && derived_sources.includes(signal)) {
e.state_unsafe_local_read();
}
var deps = current_reaction.deps; var deps = current_reaction.deps;
// If the signal is accessing the same dependencies in the same // If the signal is accessing the same dependencies in the same

@ -102,7 +102,7 @@ export function validate_binding(binding, get_object, get_property, line, column
ran = true; ran = true;
if (effect.deps === null) { if (effect.deps === null) {
var location = filename && `${filename}:${line}:${column}`; var location = `${filename}:${line}:${column}`;
w.binding_property_non_reactive(binding, location); w.binding_property_non_reactive(binding, location);
warned = true; warned = true;

@ -56,6 +56,9 @@ function clone(value, cloned, path, paths) {
const unwrapped = cloned.get(value); const unwrapped = cloned.get(value);
if (unwrapped !== undefined) return unwrapped; if (unwrapped !== undefined) return unwrapped;
if (value instanceof Map) return /** @type {Snapshot<T>} */ (new Map(value));
if (value instanceof Set) return /** @type {Snapshot<T>} */ (new Set(value));
if (is_array(value)) { if (is_array(value)) {
const copy = /** @type {Snapshot<any>} */ ([]); const copy = /** @type {Snapshot<any>} */ ([]);
cloned.set(value, copy); cloned.set(value, copy);

@ -17,9 +17,6 @@ import { define_property } from '../internal/shared/utils.js';
* *
* @param {ComponentConstructorOptions<Props> & { * @param {ComponentConstructorOptions<Props> & {
* component: ComponentType<SvelteComponent<Props, Events, Slots>> | Component<Props>; * component: ComponentType<SvelteComponent<Props, Events, Slots>> | Component<Props>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: boolean;
* }} options * }} options
* @returns {SvelteComponent<Props, Events, Slots> & Exports} * @returns {SvelteComponent<Props, Events, Slots> & Exports}
*/ */
@ -64,9 +61,6 @@ class Svelte4Component {
/** /**
* @param {ComponentConstructorOptions & { * @param {ComponentConstructorOptions & {
* component: any; * component: any;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options * }} options
*/ */
constructor(options) { constructor(options) {
@ -110,8 +104,8 @@ class Svelte4Component {
recover: options.recover recover: options.recover
}); });
// We don't flush_sync for custom element wrappers // We don't flush_sync for custom element wrappers or if the user doesn't want it
if (!options?.props?.$$host) { if (!options?.props?.$$host || options.sync === false) {
flush_sync(); flush_sync();
} }

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

@ -10,10 +10,10 @@ export default test({
}, },
test(assert, target) { test(assert, target) {
assert.equal(target.querySelector('a')?.getAttribute('href'), '/bar'); assert.equal(target.querySelector('link')?.getAttribute('href'), '/bar');
}, },
errors: [ errors: [
'The `href` attribute on `<a href="/bar">...</a>` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value' 'The `href` attribute on `<link href="/bar">` changed its value between server and client renders. The client value, `/foo`, will be ignored in favour of the server value'
] ]
}); });

@ -2,4 +2,4 @@
let { browser } = $props(); let { browser } = $props();
</script> </script>
<a href={browser ? '/foo': '/bar'}>foo</a> <link href={browser ? '/foo' : '/bar'} />

@ -12,6 +12,6 @@ export default test({
assert.htmlEqual(target.innerHTML, '<img src="server.jpg" alt="">'); assert.htmlEqual(target.innerHTML, '<img src="server.jpg" alt="">');
}, },
errors: [ errors: [
'The `src` attribute on `...<img src="server.jpg" alt="">` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value' 'The `src` attribute on `<img src="server.jpg" alt="">` changed its value between server and client renders. The client value, `client.jpg`, will be ignored in favour of the server value'
] ]
}); });

@ -0,0 +1,15 @@
import { test } from '../../test';
export default test({
server_props: {
html: '<div></div>'
},
props: {
html: '<div></div>'
},
test(assert, target) {
assert.htmlEqual(target.innerHTML, '<div></div>');
}
});

@ -0,0 +1,11 @@
import { test } from '../../test';
export default test({
server_props: {
browser: false
},
props: {
browser: true
}
});

@ -0,0 +1,5 @@
<script>
let { browser } = $props();
</script>
<a href={browser ? '/foo' : '/bar'}>foo</a>

@ -30,6 +30,7 @@ const { test, run } = suite<PreprocessTest>(async (config, cwd) => {
expect(result.dependencies).toEqual(config.dependencies || []); expect(result.dependencies).toEqual(config.dependencies || []);
if (fs.existsSync(`${cwd}/expected_map.json`)) { if (fs.existsSync(`${cwd}/expected_map.json`)) {
delete (result.map as any).ignoreList;
const expected_map = JSON.parse(fs.readFileSync(`${cwd}/expected_map.json`, 'utf-8')); const expected_map = JSON.parse(fs.readFileSync(`${cwd}/expected_map.json`, 'utf-8'));
// You can use https://sokra.github.io/source-map-visualization/#custom to visualize the source map // You can use https://sokra.github.io/source-map-visualization/#custom to visualize the source map
expect(JSON.parse(JSON.stringify(result.map))).toEqual(expected_map); expect(JSON.parse(JSON.stringify(result.map))).toEqual(expected_map);

@ -6,6 +6,6 @@ export default test({
const p = target.querySelector('p'); const p = target.querySelector('p');
ok(p); ok(p);
assert.equal(window.getComputedStyle(p).color, 'red'); assert.equal(window.getComputedStyle(p).color, 'rgb(255, 0, 0)');
} }
}); });

@ -5,9 +5,9 @@ export default test({
const [control, test] = target.querySelectorAll('p'); const [control, test] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(control).color, ''); assert.equal(window.getComputedStyle(control).color, '');
assert.equal(window.getComputedStyle(control).backgroundColor, ''); assert.equal(window.getComputedStyle(control).backgroundColor, 'rgba(0, 0, 0, 0)');
assert.equal(window.getComputedStyle(test).color, 'red'); assert.equal(window.getComputedStyle(test).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(test).backgroundColor, 'black'); assert.equal(window.getComputedStyle(test).backgroundColor, 'rgb(0, 0, 0)');
} }
}); });

@ -9,7 +9,7 @@ export default test({
const p = target.querySelector('p'); const p = target.querySelector('p');
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.height, '40px'); assert.equal(styles.height, '40px');
} }
}); });

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.height, '40px'); assert.equal(styles.height, '40px');
} }
}); });

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
let styles = window.getComputedStyle(p); let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
component.myColor = 'pink'; component.myColor = 'pink';
component.width = '100vh'; component.width = '100vh';
@ -22,7 +22,7 @@ export default test({
target.innerHTML, target.innerHTML,
'<p style="color: pink; width: 100vh; font-weight: 100; position: absolute;"></p>' '<p style="color: pink; width: 100vh; font-weight: 100; position: absolute;"></p>'
); );
assert.equal(styles.color, 'pink'); assert.equal(styles.color, 'rgb(255, 192, 203)');
assert.equal(styles.width, '100vh'); assert.equal(styles.width, '100vh');
assert.equal(styles.fontWeight, '100'); assert.equal(styles.fontWeight, '100');
assert.equal(styles.position, 'absolute'); assert.equal(styles.position, 'absolute');

@ -11,8 +11,8 @@ export default test({
test({ assert, target, window }) { test({ assert, target, window }) {
const [p1, p2] = target.querySelectorAll('p'); const [p1, p2] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(p1).color, 'red'); assert.equal(window.getComputedStyle(p1).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(p2).color, 'red'); assert.equal(window.getComputedStyle(p2).color, 'rgb(255, 0, 0)');
const btn = target.querySelector('button'); const btn = target.querySelector('button');
btn?.click(); btn?.click();
@ -27,7 +27,7 @@ export default test({
` `
); );
assert.equal(window.getComputedStyle(p1).color, 'green'); assert.equal(window.getComputedStyle(p1).color, 'rgb(0, 128, 0)');
assert.equal(window.getComputedStyle(p2).color, 'green'); assert.equal(window.getComputedStyle(p2).color, 'rgb(0, 128, 0)');
} }
}); });

@ -9,8 +9,8 @@ export default test({
test({ assert, component, target, window }) { test({ assert, component, target, window }) {
const [p1, p2] = target.querySelectorAll('p'); const [p1, p2] = target.querySelectorAll('p');
assert.equal(window.getComputedStyle(p1).color, 'red'); assert.equal(window.getComputedStyle(p1).color, 'rgb(255, 0, 0)');
assert.equal(window.getComputedStyle(p2).color, 'red'); assert.equal(window.getComputedStyle(p2).color, 'rgb(255, 0, 0)');
component.color = 'blue'; component.color = 'blue';
assert.htmlEqual( assert.htmlEqual(
@ -21,7 +21,7 @@ export default test({
` `
); );
assert.equal(window.getComputedStyle(p1).color, 'blue'); assert.equal(window.getComputedStyle(p1).color, 'rgb(0, 0, 255)');
assert.equal(window.getComputedStyle(p2).color, 'blue'); assert.equal(window.getComputedStyle(p2).color, 'rgb(0, 0, 255)');
} }
}); });

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
let styles = window.getComputedStyle(p); let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green'); assert.equal(styles.color, 'rgb(0, 128, 0)');
component.color = null; component.color = null;
assert.htmlEqual(target.innerHTML, '<p style=""></p>'); assert.htmlEqual(target.innerHTML, '<p style=""></p>');
@ -29,12 +29,12 @@ export default test({
assert.htmlEqual(target.innerHTML, '<p style="background-color: green;"></p>'); assert.htmlEqual(target.innerHTML, '<p style="background-color: green;"></p>');
styles = window.getComputedStyle(p); styles = window.getComputedStyle(p);
assert.equal(styles.color, ''); assert.equal(styles.color, '');
assert.equal(styles.backgroundColor, 'green'); assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
component.color = 'purple'; component.color = 'purple';
assert.htmlEqual(target.innerHTML, '<p style="background-color: green; color: purple;"></p>'); assert.htmlEqual(target.innerHTML, '<p style="background-color: green; color: purple;"></p>');
styles = window.getComputedStyle(p); styles = window.getComputedStyle(p);
assert.equal(styles.color, 'purple'); assert.equal(styles.color, 'rgb(128, 0, 128)');
assert.equal(styles.backgroundColor, 'green'); assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
} }
}); });

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'blue'); assert.equal(styles.color, 'rgb(0, 0, 255)');
assert.equal(styles.width, '65px'); assert.equal(styles.width, '65px');
assert.equal(p.id, 'my-id'); assert.equal(p.id, 'my-id');

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'blue'); assert.equal(styles.color, 'rgb(0, 0, 255)');
assert.equal(styles.width, '65px'); assert.equal(styles.width, '65px');
assert.equal(p.id, 'my-id'); assert.equal(p.id, 'my-id');
} }

@ -10,7 +10,7 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green'); assert.equal(styles.color, 'rgb(0, 128, 0)');
assert.equal(styles.transform, 'translateX(45px)'); assert.equal(styles.transform, 'translateX(45px)');
assert.equal(styles.border, '100px solid pink'); assert.equal(styles.border, '100px solid pink');

@ -10,6 +10,6 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
} }
}); });

@ -9,7 +9,7 @@ export default test({
const p = target.querySelector('p'); const p = target.querySelector('p');
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.backgroundColor, 'green'); assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.fontSize, '12px'); assert.equal(styles.fontSize, '12px');
{ {
@ -17,7 +17,7 @@ export default test({
const p = target.querySelector('p'); const p = target.querySelector('p');
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.backgroundColor, 'green'); assert.equal(styles.backgroundColor, 'rgb(0, 128, 0)');
assert.equal(styles.fontSize, '50px'); assert.equal(styles.fontSize, '50px');
} }
} }

@ -12,6 +12,6 @@ export default test({
ok(p); ok(p);
const styles = window.getComputedStyle(p); const styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
} }
}); });

@ -10,12 +10,12 @@ export default test({
ok(p); ok(p);
let styles = window.getComputedStyle(p); let styles = window.getComputedStyle(p);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
assert.equal(styles.fontSize, '20px'); assert.equal(styles.fontSize, '20px');
component.color = 'green'; component.color = 'green';
styles = window.getComputedStyle(p); styles = window.getComputedStyle(p);
assert.equal(styles.color, 'green'); assert.equal(styles.color, 'rgb(0, 128, 0)');
} }
}); });

@ -11,7 +11,7 @@ export default test({
let styles = window.getComputedStyle(p); let styles = window.getComputedStyle(p);
assert.equal(styles.opacity, '0.5'); assert.equal(styles.opacity, '0.5');
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
component.styles = 'font-size: 20px'; component.styles = 'font-size: 20px';

@ -10,6 +10,6 @@ export default test({
ok(div); ok(div);
const styles = window.getComputedStyle(div); const styles = window.getComputedStyle(div);
assert.equal(styles.color, 'red'); assert.equal(styles.color, 'rgb(255, 0, 0)');
} }
}); });

@ -351,7 +351,6 @@ async function run_test_variant(
component: mod.default, component: mod.default,
props: config.props, props: config.props,
target, target,
immutable: config.immutable,
intro: config.intro, intro: config.intro,
recover: config.recover ?? false, recover: config.recover ?? false,
hydrate: variant === 'hydrate' hydrate: variant === 'hydrate'

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>11</button>`);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>12</button>`);
assert.deepEqual(logs, [0, 10, 11, 12]);
}
});

@ -0,0 +1,16 @@
<script>
class Counter {
count = $state(0);
constructor(initial) {
$effect.pre(() => {
console.log(this.count);
});
this.count = initial;
}
}
const counter = $derived(new Counter(10));
counter;
</script>
<button onclick={() => counter.count++}>{counter.count}</button>

@ -0,0 +1,20 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
html: `<button>10</button>`,
test({ assert, target, logs }) {
const btn = target.querySelector('button');
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>11</button>`);
btn?.click();
flushSync();
assert.htmlEqual(target.innerHTML, `<button>12</button>`);
assert.deepEqual(logs, [0, 10, 11, 12]);
}
});

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

Loading…
Cancel
Save