Merge branch 'main' into ast-types-v2

pull/12968/head
Simon Holthausen 2 years ago
commit a57cb2e42c

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure reactive graph is fully traversed in the marking phase for non-runes mode

@ -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: render undefined html as the empty string

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error on incorrect attributes for svelte:body

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent div/0 when generating transition keyframes

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: use WAAPI to control timing of JS-based animations

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error on invalid element name

@ -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: better compile errors for invalid tag names/placement

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow deleting non-existent `$$restProps` properties

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: insert comment before text in an each block, to prevent glued nodes

@ -21,6 +21,7 @@
"beige-cobras-smoke",
"beige-flies-wash",
"beige-gifts-appear",
"beige-lamps-ring",
"beige-mirrors-listen",
"beige-rabbits-shave",
"beige-seas-share",
@ -82,6 +83,7 @@
"clean-cats-wave",
"clean-eels-beg",
"clean-melons-wash",
"clean-shirts-yawn",
"clever-chefs-relate",
"clever-maps-travel",
"clever-rockets-burn",
@ -192,12 +194,15 @@
"few-clouds-shop",
"few-mugs-fail",
"few-teachers-know",
"few-walls-check",
"few-zoos-own",
"fifty-actors-agree",
"fifty-masks-give",
"fifty-rice-wait",
"fifty-steaks-float",
"fifty-toys-invite",
"five-maps-reflect",
"five-shirts-run",
"five-tigers-search",
"flat-feet-visit",
"flat-ghosts-fly",
@ -243,6 +248,7 @@
"fuzzy-donuts-provide",
"gentle-dolls-juggle",
"gentle-eagles-walk",
"gentle-needles-train",
"gentle-sheep-hug",
"gentle-spies-happen",
"gentle-ties-fetch",
@ -273,17 +279,20 @@
"great-fans-unite",
"great-icons-retire",
"great-plums-pretend",
"green-baboons-sip",
"green-eggs-approve",
"green-fishes-lie",
"green-hounds-play",
"green-snails-tickle",
"green-tigers-judge",
"green-walls-clap",
"green-windows-tap",
"grumpy-avocados-fetch",
"grumpy-insects-sleep",
"grumpy-jars-sparkle",
"happy-beds-scream",
"happy-dogs-jump",
"happy-dolls-joke",
"happy-lobsters-lick",
"happy-moles-live",
"happy-planets-rest",
@ -337,6 +346,7 @@
"itchy-terms-guess",
"khaki-cheetahs-refuse",
"khaki-cooks-develop",
"khaki-donkeys-jump",
"khaki-ligers-sing",
"khaki-mails-draw",
"khaki-mails-scream",
@ -375,6 +385,7 @@
"light-humans-hang",
"light-penguins-invent",
"light-pens-watch",
"light-tigers-smoke",
"little-ligers-exist",
"little-pans-jog",
"little-seals-reflect",
@ -418,6 +429,7 @@
"mighty-files-hammer",
"mighty-frogs-obey",
"mighty-paws-smash",
"mighty-poets-fix",
"mighty-shoes-nail",
"modern-apricots-promise",
"modern-fishes-double",
@ -477,6 +489,7 @@
"olive-cobras-wonder",
"olive-forks-grin",
"olive-kangaroos-brake",
"olive-llamas-warn",
"olive-mice-fix",
"olive-moons-act",
"olive-seals-sell",
@ -487,6 +500,7 @@
"orange-dingos-poke",
"orange-laws-drop",
"orange-masks-exercise",
"orange-sheep-exist",
"orange-yaks-protect",
"orange-zoos-heal",
"perfect-actors-bake",
@ -528,6 +542,7 @@
"proud-queens-sniff",
"purple-dragons-peel",
"quick-eagles-sit",
"quick-paws-wash",
"quick-pumpkins-study",
"quiet-apricots-dream",
"quiet-berries-end",
@ -536,6 +551,7 @@
"quiet-cobras-smile",
"quiet-crabs-nail",
"quiet-timers-speak",
"rare-ears-agree",
"rare-insects-tell",
"rare-mirrors-act",
"rare-pears-whisper",
@ -547,6 +563,7 @@
"red-doors-own",
"red-feet-worry",
"red-kings-draw",
"red-maps-nail",
"red-poets-study",
"red-pots-pretend",
"rich-cobras-exist",
@ -586,6 +603,7 @@
"seven-hornets-smile",
"seven-jobs-sniff",
"seven-masks-end",
"seven-news-live",
"seven-ravens-check",
"shaggy-cameras-live",
"shaggy-comics-jog",
@ -604,6 +622,7 @@
"shiny-rats-heal",
"shiny-shrimps-march",
"shiny-starfishes-cross",
"shiny-wombats-argue",
"short-buses-camp",
"short-countries-rush",
"short-starfishes-beg",
@ -622,10 +641,12 @@
"silver-points-approve",
"silver-sheep-knock",
"six-apes-peel",
"six-beans-laugh",
"six-bears-trade",
"six-boats-shave",
"six-chicken-kneel",
"six-gorillas-obey",
"six-moons-invent",
"six-vans-add",
"sixty-items-crash",
"sixty-numbers-hope",
@ -636,6 +657,7 @@
"slimy-clouds-talk",
"slimy-hairs-impress",
"slimy-laws-explode",
"slimy-news-help",
"slimy-onions-approve",
"slimy-walls-draw",
"slow-beds-shave",
@ -798,6 +820,7 @@
"twenty-gifts-develop",
"two-brooms-fail",
"two-candles-move",
"two-cats-approve",
"two-dogs-accept",
"two-dragons-yell",
"two-falcons-buy",
@ -818,7 +841,9 @@
"weak-frogs-bow",
"weak-terms-destroy",
"wet-bats-exercise",
"wet-donkeys-fry",
"wet-games-fly",
"wet-pears-buy",
"wet-pears-remain",
"wet-wombats-repeat",
"wicked-bikes-matter",

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: only traverse trailing static nodes during hydration

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: update client check for smaller bundle size

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: better generated each block code in SSR mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure event currentTarget is reset after propagation logic

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

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent binding to imports

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

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: never abort bidirectional transitions

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

@ -29,9 +29,9 @@ jobs:
os: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm
@ -44,9 +44,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm
@ -64,9 +64,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v3
- uses: actions/setup-node@v4
with:
node-version: 18
cache: pnpm

@ -18,13 +18,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout Repo
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: pnpm

@ -2,12 +2,12 @@
# see: https://github.com/sveltejs/svelte/pull/9609
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/**/build/*.js
packages/**/npm/**/*
packages/**/config/*.js
# packages/svelte
packages/svelte/messages/**/*.md
packages/svelte/src/compiler/errors.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/server/errors.js
packages/svelte/tests/migrate/samples/*/output.svelte
packages/svelte/tests/**/*.svelte
packages/svelte/tests/**/_expected*
packages/svelte/tests/**/_actual*
packages/svelte/tests/**/expected*
@ -29,6 +30,7 @@ packages/svelte/compiler/index.js
playgrounds/sandbox/input/**.svelte
playgrounds/sandbox/output
# sites/svelte.dev
sites/svelte.dev/static/svelte-app.json
sites/svelte.dev/scripts/svelte-app/
sites/svelte.dev/src/routes/_components/Supporters/contributors.jpg

@ -1,22 +1,6 @@
{
"version": "0.2.0",
"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",
"request": "launch",
@ -26,11 +10,5 @@
"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';
function setup() {
let head = $.source(0);
let head = $.state(0);
let computed1 = $.derived(() => $.get(head));
let computed2 = $.derived(() => ($.get(computed1), 0));
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';
function setup() {
let head = $.source(0);
let head = $.state(0);
let last = head;
let counter = 0;

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

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

@ -2,7 +2,7 @@ import { assert, fastest_test } from '../../utils.js';
import * as $ from '../../../packages/svelte/src/internal/client/index.js';
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(() => {
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;
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = $.derived(() => {
let result = 0;
for (let i = 0; i < size; i++) {

@ -11,7 +11,7 @@ function count(number) {
}
function setup() {
let head = $.source(0);
let head = $.state(0);
let current = head;
let list = [];
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';
function setup() {
let head = $.source(0);
let head = $.state(0);
const double = $.derived(() => $.get(head) * 2);
const inverse = $.derived(() => -$.get(head));
let current = $.derived(() => {

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

@ -20,8 +20,8 @@ const numbers = Array.from({ length: 5 }, (_, i) => i);
function setup() {
let res = [];
const A = $.source(0);
const B = $.source(0);
const A = $.state(0);
const B = $.state(0);
const C = $.derived(() => ($.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) {

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

@ -39,7 +39,8 @@ export default [
{
languageOptions: {
parserOptions: {
project: true
projectService: true,
tsconfigRootDir: import.meta.dirname
}
},
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: {
'lube/svelte-naming-convention': 'off',
'no-console': 'off'
'@typescript-eslint/await-thenable': '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/shared/warnings.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',
// contains a fork of the REPL which doesn't adhere to eslint rules

@ -18,8 +18,8 @@
"build:sites": "pnpm -r --filter=./sites/* build",
"preview-site": "npm run build --prefix sites/svelte-5-preview",
"check": "cd packages/svelte && pnpm build && cd ../../ && pnpm -r check",
"lint": "pnpm -r lint && prettier --check documentation",
"format": "pnpm -r format && prettier --check --write documentation",
"lint": "eslint && prettier --check .",
"format": "prettier --write .",
"test": "vitest run",
"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",
@ -33,18 +33,18 @@
"@sveltejs/eslint-config": "^8.0.1",
"@svitejs/changesets-changelog-github-compact": "^1.1.0",
"@types/node": "^20.11.5",
"@vitest/coverage-v8": "^1.2.1",
"eslint": "^9.6.0",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.9.1",
"eslint-plugin-lube": "^0.4.3",
"jsdom": "22.0.0",
"playwright": "^1.41.1",
"jsdom": "25.0.0",
"playwright": "^1.46.1",
"prettier": "^3.2.4",
"prettier-plugin-svelte": "^3.1.2",
"svelte": "workspace:^",
"typescript": "^5.5.2",
"typescript-eslint": "^8.0.0-alpha.34",
"typescript": "^5.5.4",
"typescript-eslint": "^8.2.0",
"v8-natives": "^1.2.5",
"vitest": "^1.2.1"
"vitest": "^2.0.5"
},
"pnpm": {
"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,91 @@
# svelte
## 5.0.0-next.242
### Patch Changes
- fix: insert comment before text in an each block, to prevent glued nodes ([#13073](https://github.com/sveltejs/svelte/pull/13073))
- feat: better generated each block code in SSR mode ([#13060](https://github.com/sveltejs/svelte/pull/13060))
## 5.0.0-next.241
### Patch Changes
- fix: prevent div/0 when generating transition keyframes ([#13058](https://github.com/sveltejs/svelte/pull/13058))
- fix: error on invalid element name ([#13057](https://github.com/sveltejs/svelte/pull/13057))
- fix: better compile errors for invalid tag names/placement ([#13045](https://github.com/sveltejs/svelte/pull/13045))
- fix: ensure event currentTarget is reset after propagation logic ([#13042](https://github.com/sveltejs/svelte/pull/13042))
## 5.0.0-next.240
### Patch Changes
- fix: use WAAPI to control timing of JS-based animations ([#13018](https://github.com/sveltejs/svelte/pull/13018))
- fix: prevent binding to imports ([#13035](https://github.com/sveltejs/svelte/pull/13035))
- fix: never abort bidirectional transitions ([#13018](https://github.com/sveltejs/svelte/pull/13018))
## 5.0.0-next.239
### Patch Changes
- fix: properly handle proxied array length mutations ([#13026](https://github.com/sveltejs/svelte/pull/13026))
- fix: repair `href` attribute mismatches ([#13032](https://github.com/sveltejs/svelte/pull/13032))
## 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
### Patch Changes
- fix: allow deleting non-existent `$restProps` properties ([#12971](https://github.com/sveltejs/svelte/pull/12971))
- feat: only traverse trailing static nodes during hydration ([#12935](https://github.com/sveltejs/svelte/pull/12935))
## 5.0.0-next.233
### 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
## 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`

@ -100,10 +100,6 @@
> This type of directive is not valid on components
## component_invalid_name
> Component name must be a valid variable name or dot notation expression
## const_tag_cycle
> Cyclical dependency detected: %cycle%
@ -136,10 +132,6 @@
> `</%name%>` attempted to close element that was already automatically closed by `<%reason%>` (cannot nest `<%reason%>` inside `<%name%>`)
## element_invalid_tag_name
> Expected valid tag name
## element_unclosed
> `<%name%>` was left open
@ -282,6 +274,10 @@ HTML restricts where certain elements can appear. In case of a violation the bro
> A component can have a single top-level `<style>` element
## svelte_body_illegal_attribute
> `<svelte:body>` does not support non-event attributes or spread attributes
## svelte_component_invalid_this
> Invalid component definition — must be an `{expression}`
@ -302,10 +298,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_invalid_slot
> `<svelte:fragment>` slot attribute must have a static value
## svelte_head_illegal_attribute
> `<svelte:head>` cannot have attributes nor directives
@ -362,6 +354,10 @@ HTML restricts where certain elements can appear. In case of a violation the bro
> `<svelte:self>` components can only exist inside `{#if}` blocks, `{#each}` blocks, `{#snippet}` blocks or slots passed to components
## tag_invalid_name
> Expected a valid element or component name. Components must have a valid variable name or dot notation expression
## tag_invalid_placement
> {@%name% ...} tag cannot be %location%

@ -1,7 +1,3 @@
## derived_iife
> Use `$derived.by(() => {...})` instead of `$derived((() => {...})())`
## export_let_unused
> 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",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.233",
"version": "5.0.0-next.242",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -112,50 +112,38 @@
"generate:version": "node ./scripts/generate-version.js",
"generate:types": "node ./scripts/generate-types.js && tsc -p tsconfig.generated.json",
"prepublishOnly": "pnpm build",
"format": "prettier --check --write .",
"lint": "prettier --check . && eslint",
"knip": "pnpm dlx knip"
},
"devDependencies": {
"@jridgewell/trace-mapping": "^0.3.22",
"@playwright/test": "^1.35.1",
"@jridgewell/trace-mapping": "^0.3.25",
"@playwright/test": "^1.46.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-virtual": "^3.0.2",
"@types/aria-query": "^5.0.4",
"@types/node": "^20.11.5",
"dts-buddy": "^0.5.3",
"esbuild": "^0.19.11",
"rollup": "^4.9.5",
"esbuild": "^0.21.5",
"rollup": "^4.21.0",
"source-map": "^0.7.4",
"tiny-glob": "^0.2.9"
"tiny-glob": "^0.2.9",
"typescript": "^5.5.4",
"vitest": "^2.0.5"
},
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.11.3",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.0",
"axobject-query": "^4.0.0",
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"is-reference": "^3.0.2",
"locate-character": "^3.0.0",
"magic-string": "^0.30.5",
"magic-string": "^0.30.11",
"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/**"
]
}
}

@ -786,15 +786,6 @@ export function component_invalid_directive(node) {
e(node, "component_invalid_directive", "This type of directive is not valid on components");
}
/**
* Component name must be a valid variable name or dot notation expression
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function component_invalid_name(node) {
e(node, "component_invalid_name", "Component name must be a valid variable name or dot notation expression");
}
/**
* Cyclical dependency detected: %cycle%
* @param {null | number | NodeLike} node
@ -872,15 +863,6 @@ export function element_invalid_closing_tag_autoclosed(node, name, reason) {
e(node, "element_invalid_closing_tag_autoclosed", `\`</${name}>\` attempted to close element that was already automatically closed by \`<${reason}>\` (cannot nest \`<${reason}>\` inside \`<${name}>\`)`);
}
/**
* Expected valid tag name
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function element_invalid_tag_name(node) {
e(node, "element_invalid_tag_name", "Expected valid tag name");
}
/**
* `<%name%>` was left open
* @param {null | number | NodeLike} node
@ -1201,6 +1183,15 @@ export function style_duplicate(node) {
e(node, "style_duplicate", "A component can have a single top-level `<style>` element");
}
/**
* `<svelte:body>` does not support non-event attributes or spread attributes
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_body_illegal_attribute(node) {
e(node, "svelte_body_illegal_attribute", "`<svelte:body>` does not support non-event attributes or spread attributes");
}
/**
* Invalid component definition must be an `{expression}`
* @param {null | number | NodeLike} node
@ -1246,15 +1237,6 @@ export function svelte_fragment_invalid_placement(node) {
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
* @param {null | number | NodeLike} node
@ -1387,6 +1369,15 @@ export function svelte_self_invalid_placement(node) {
e(node, "svelte_self_invalid_placement", "`<svelte:self>` components can only exist inside `{#if}` blocks, `{#each}` blocks, `{#snippet}` blocks or slots passed to components");
}
/**
* Expected a valid element or component name. Components must have a valid variable name or dot notation expression
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function tag_invalid_name(node) {
e(node, "tag_invalid_name", "Expected a valid element or component name. Components must have a valid variable name or dot notation expression");
}
/**
* {@%name% ...} tag cannot be %location%
* @param {null | number | NodeLike} node

@ -107,7 +107,7 @@ export function compileModule(source, options) {
*/
export function parse(source, { filename, rootDir, modern } = {}) {
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);
return to_public_ast(source, ast, modern);

@ -8,6 +8,7 @@ import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js';
import read_options from './read/options.js';
import { is_reserved } from '../../../utils.js';
import { disallow_children } from '../2-analyze/visitors/shared/special-element.js';
const regex_position_indicator = / \(\d+:\d+\)$/;
@ -124,6 +125,9 @@ export class Parser {
const options = /** @type {Ast.SvelteOptionsRaw} */ (this.root.fragment.nodes[options_index]);
this.root.fragment.nodes.splice(options_index, 1);
this.root.options = read_options(options);
disallow_children(options);
// We need this for the old AST format
Object.defineProperty(this.root.options, '__raw__', {
value: options,

@ -65,10 +65,6 @@ export function read_script(parser, start, attributes) {
if (attribute.name === 'context') {
if (attribute.value === true || !is_text_attribute(attribute)) {
throw new Error('TODO');
}
if (attribute.value.length !== 1 || attribute.value[0].type !== 'Text') {
e.script_invalid_context(attribute);
}

@ -12,12 +12,19 @@ import { create_fragment } from '../utils/create.js';
import { create_attribute, create_expression_metadata } from '../../nodes.js';
import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js';
import { closing_tag_omitted } from '../../../../html-tree-validation.js';
import { list } from '../../../utils/string.js';
// eslint-disable-next-line no-useless-escape
const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
/** Invalid attribute characters if the attribute is not surrounded by quotes */
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
const regex_invalid_unquoted_attribute_value = /^(\/>|[\s"'=<>`])/;
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
const regex_token_ending_character = /[\s=/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
const regex_valid_element_name =
/^(?:![a-zA-Z]+|[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?|[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9])$/;
const regex_valid_component_name =
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*(?:\.[A-Za-z0-9_$]+)+)$/;
/** @type {Map<string, Compiler.Ast.ElementLike['type']>} */
const root_only_meta_tags = new Map([
@ -37,47 +44,6 @@ const meta_tags = new Map([
['svelte:fragment', 'SvelteFragment']
]);
const valid_meta_tags = Array.from(meta_tags.keys());
const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
/** @param {Compiler.Ast.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (type === 'RegularElement' || type === 'Component') return false;
}
return false;
}
/** @param {Compiler.Ast.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {Compiler.Ast.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return false;
}
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
const regex_valid_component_name =
/^(?:[A-Z][A-Za-z0-9_$.]*|[a-z][A-Za-z0-9_$]*\.[A-Za-z0-9_$])[A-Za-z0-9_$.]*$/;
/** @param {Parser} parser */
export default function element(parser) {
const start = parser.index++;
@ -100,36 +66,67 @@ export default function element(parser) {
}
const is_closing_tag = parser.eat('/');
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
const name = read_tag_name(parser);
if (is_closing_tag) {
parser.allow_whitespace();
parser.eat('>', true);
if (root_only_meta_tags.has(name)) {
if (is_closing_tag) {
if (
['svelte:options', 'svelte:window', 'svelte:body', 'svelte:document'].includes(name) &&
/** @type {Compiler.Ast.ElementLike} */ (parent).fragment.nodes.length
) {
e.svelte_meta_invalid_content(
/** @type {Compiler.Ast.ElementLike} */ (parent).fragment.nodes[0].start,
name
);
}
} else {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (is_void(name)) {
e.void_element_invalid_content(start);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {Compiler.Ast.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
e.element_invalid_closing_tag(start, name);
}
}
parser.meta_tags[name] = true;
parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
}
return;
}
if (name.startsWith('svelte:') && !meta_tags.has(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.svelte_meta_invalid_tag(bounds, list(Array.from(meta_tags.keys())));
}
if (!regex_valid_element_name.test(name) && !regex_valid_component_name.test(name)) {
const bounds = { start: start + 1, end: start + 1 + name.length };
e.tag_invalid_name(bounds);
}
if (root_only_meta_tags.has(name)) {
if (name in parser.meta_tags) {
e.svelte_meta_duplicate(start, name);
}
if (parent.type !== 'Root') {
e.svelte_meta_invalid_placement(start, name);
}
parser.meta_tags[name] = true;
}
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_component_name.test(name)
: regex_valid_component_name.test(name)
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'
@ -138,10 +135,6 @@ export default function element(parser) {
? 'SlotElement'
: 'RegularElement';
if (type === 'Component' && !regex_valid_component_name.test(name)) {
e.component_invalid_name({ start: start + 1, end: start + name.length + 1 });
}
/** @type {Compiler.Ast.ElementLike} */
const element =
type === 'RegularElement'
@ -175,38 +168,7 @@ export default function element(parser) {
parser.allow_whitespace();
if (is_closing_tag) {
if (is_void(name)) {
e.void_element_invalid_content(start);
}
parser.eat('>', true);
// close any elements that don't have their own closing tags, e.g. <div><p></div>
while (/** @type {Compiler.Ast.RegularElement} */ (parent).name !== name) {
if (parent.type !== 'RegularElement') {
if (parser.last_auto_closed_tag && parser.last_auto_closed_tag.tag === name) {
e.element_invalid_closing_tag_autoclosed(start, name, parser.last_auto_closed_tag.reason);
} else {
e.element_invalid_closing_tag(start, name);
}
}
parent.end = start;
parser.pop();
parent = parser.current();
}
parent.end = parser.index;
parser.pop();
if (parser.last_auto_closed_tag && parser.stack.length < parser.last_auto_closed_tag.depth) {
parser.last_auto_closed_tag = undefined;
}
return;
} else if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
if (parent.type === 'RegularElement' && closing_tag_omitted(parent.name, name)) {
parent.end = start;
parser.pop();
parser.last_auto_closed_tag = {
@ -390,64 +352,34 @@ export default function element(parser) {
}
}
const regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/;
/** @param {Parser} parser */
function read_tag_name(parser) {
const start = parser.index;
if (parser.read(SELF)) {
// check we're inside a block, otherwise this
// will cause infinite recursion
let i = parser.stack.length;
let legal = false;
while (i--) {
const fragment = parser.stack[i];
if (
fragment.type === 'IfBlock' ||
fragment.type === 'EachBlock' ||
fragment.type === 'Component' ||
fragment.type === 'SnippetBlock'
) {
legal = true;
break;
}
}
if (!legal) {
e.svelte_self_invalid_placement(start);
}
return 'svelte:self';
}
if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(ELEMENT)) return 'svelte:element';
if (parser.read(SLOT)) return 'svelte:fragment';
const name = parser.read_until(regex_whitespace_or_slash_or_closing_tag);
if (meta_tags.has(name)) return name;
if (name.startsWith('svelte:')) {
const list = `${valid_meta_tags.slice(0, -1).join(', ')} or ${valid_meta_tags[valid_meta_tags.length - 1]}`;
e.svelte_meta_invalid_tag(start, list);
/** @param {Compiler.Ast.TemplateNode[]} stack */
function parent_is_head(stack) {
let i = stack.length;
while (i--) {
const { type } = stack[i];
if (type === 'SvelteHead') return true;
if (type === 'RegularElement' || type === 'Component') return false;
}
return false;
}
if (!valid_tag_name.test(name)) {
e.element_invalid_tag_name(start);
/** @param {Compiler.Ast.TemplateNode[]} stack */
function parent_is_shadowroot_template(stack) {
// https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#building_a_declarative_shadow_root
let i = stack.length;
while (i--) {
if (
stack[i].type === 'RegularElement' &&
/** @type {Compiler.Ast.RegularElement} */ (stack[i]).attributes.some(
(a) => a.type === 'Attribute' && a.name === 'shadowrootmode'
)
) {
return true;
}
}
return name;
return false;
}
// eslint-disable-next-line no-useless-escape
const regex_token_ending_character = /[\s=\/>"']/;
const regex_starts_with_quote_characters = /^["']/;
const regex_attribute_value = /^(?:"([^"]*)"|'([^'])*'|([^>\s]+))/;
/**
* @param {Parser} parser
* @returns {Compiler.Ast.Attribute | null}
@ -696,7 +628,7 @@ function read_attribute_value(parser) {
() => {
// handle common case of quote marks existing outside of regex for performance reasons
if (quote_mark) return parser.match(quote_mark);
return !!parser.match_regex(regex_starts_with_invalid_attr_value);
return !!parser.match_regex(regex_invalid_unquoted_attribute_value);
},
'in attribute value'
);

@ -52,11 +52,14 @@ import { SlotElement } from './visitors/SlotElement.js';
import { SnippetBlock } from './visitors/SnippetBlock.js';
import { SpreadAttribute } from './visitors/SpreadAttribute.js';
import { StyleDirective } from './visitors/StyleDirective.js';
import { SvelteBody } from './visitors/SvelteBody.js';
import { SvelteComponent } from './visitors/SvelteComponent.js';
import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { Text } from './visitors/Text.js';
import { TitleElement } from './visitors/TitleElement.js';
@ -158,11 +161,14 @@ const visitors = {
SnippetBlock,
SpreadAttribute,
StyleDirective,
SvelteHead,
SvelteBody,
SvelteComponent,
SvelteDocument,
SvelteElement,
SvelteFragment,
SvelteComponent,
SvelteHead,
SvelteSelf,
SvelteWindow,
TaggedTemplateExpression,
Text,
TitleElement,
@ -243,7 +249,7 @@ export function analyze_module(ast, options) {
return {
module: { ast, scope, scopes },
name: options.filename || 'module',
name: options.filename,
accessors: false,
runes: true,
immutable: true
@ -349,7 +355,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);
@ -390,7 +396,7 @@ export function analyze_component(root, source, options) {
hash: root.css
? options.cssHash({
css: root.css.content.styles,
filename: options.filename ?? '<unknown>',
filename: options.filename,
name: component_name,
hash
})

@ -44,10 +44,6 @@ export function BindDirective(node, context) {
e.bind_invalid_value(node.expression);
}
if (binding?.kind === 'derived') {
e.constant_binding(node.expression, 'derived state');
}
if (context.state.analysis.runes && binding?.kind === 'each') {
e.each_item_invalid_assignment(node);
}

@ -0,0 +1,22 @@
/** @import { SvelteBody } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { is_event_attribute } from '../../../utils/ast.js';
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteBody} node
* @param {Context} context
*/
export function SvelteBody(node, context) {
disallow_children(node);
for (const attribute of node.attributes) {
if (
attribute.type === 'SpreadAttribute' ||
(attribute.type === 'Attribute' && !is_event_attribute(attribute))
) {
e.svelte_body_illegal_attribute(attribute);
}
}
context.next();
}

@ -0,0 +1,12 @@
/** @import { SvelteDocument } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteDocument} node
* @param {Context} context
*/
export function SvelteDocument(node, context) {
disallow_children(node);
context.next();
}

@ -1,11 +1,24 @@
/** @import { Ast } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
/**
* @param {Ast.SvelteSelf} node
* @param {Context} context
*/
export function SvelteSelf(node, context) {
const valid = context.path.some(
(node) =>
node.type === 'IfBlock' ||
node.type === 'EachBlock' ||
node.type === 'Component' ||
node.type === 'SnippetBlock'
);
if (!valid) {
e.svelte_self_invalid_placement(node);
}
visit_component(node, context);
}

@ -0,0 +1,12 @@
/** @import { SvelteWindow } from '#compiler' */
/** @import { Context } from '../types' */
import { disallow_children } from './shared/special-element.js';
/**
* @param {SvelteWindow} node
* @param {Context} context
*/
export function SvelteWindow(node, context) {
disallow_children(node);
context.next();
}

@ -0,0 +1,16 @@
/** @import { SvelteBody, SvelteDocument, SvelteOptionsRaw, SvelteWindow } from '#compiler' */
import * as e from '../../../../errors.js';
/**
* @param {SvelteBody | SvelteDocument | SvelteOptionsRaw | SvelteWindow} node
*/
export function disallow_children(node) {
const { nodes } = node.fragment;
if (nodes.length > 0) {
const first = nodes[0];
const last = nodes[nodes.length - 1];
e.svelte_meta_invalid_content({ start: first.start, end: last.end }, node.name);
}
}

@ -71,7 +71,11 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
}
} else if (argument.type === 'Identifier') {
const binding = scope.get(argument.name);
if (binding?.declaration_kind === 'const' && binding.kind !== 'each') {
if (
binding?.kind === 'derived' ||
binding?.declaration_kind === 'import' ||
(binding?.declaration_kind === 'const' && binding.kind !== 'each')
) {
// e.invalid_const_assignment(
// node,
// is_binding,
@ -83,7 +87,12 @@ export function validate_no_const_assignment(node, argument, scope, is_binding)
// );
// TODO have a more specific error message for assignments to things like `{:then foo}`
const thing = 'constant';
const thing =
binding.declaration_kind === 'import'
? 'import'
: binding.kind === 'derived'
? 'derived state'
: 'constant';
if (is_binding) {
e.constant_binding(node, thing);

@ -207,7 +207,7 @@ export function client_component(analysis, options) {
for (const [name, binding] of analysis.instance.scope.declarations) {
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 (store_setup.length === 0) {
@ -457,7 +457,7 @@ export function client_component(analysis, options) {
analysis.uses_slots ||
analysis.slot_names.size > 0;
const body = [...state.hoisted, ...module.body];
const body = [...module.body, ...state.hoisted];
const component = b.function_declaration(
b.id(analysis.name),
@ -505,14 +505,12 @@ export function client_component(analysis, options) {
}
if (dev) {
if (filename) {
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), '$.FILENAME', true), b.literal(filename))
)
);
}
// add `App[$.FILENAME] = 'App.svelte'` so that we can print useful messages later
body.unshift(
b.stmt(
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.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 {PrivateIdentifier | string} proxy_reference
* @param {Expression} previous
*/
export function build_proxy_reassignment(value, proxy_reference) {
return dev
? b.call(
'$.proxy',
value,
b.null,
typeof proxy_reference === 'string'
? b.id(proxy_reference)
: b.member(b.this, proxy_reference)
)
: b.call('$.proxy', value);
export function build_proxy_reassignment(value, previous) {
return dev ? b.call('$.proxy', value, b.null, previous) : b.call('$.proxy', value);
}
/**

@ -48,7 +48,7 @@ function build_assignment(operator, left, right, context) {
value =
private_state.kind === 'raw_state'
? value
: build_proxy_reassignment(value, private_state.id);
: build_proxy_reassignment(value, b.member(b.this, private_state.id));
}
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);
}
}
} 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 &&
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);

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

@ -147,7 +147,7 @@ export function EachBlock(node, context) {
// which needs a reference to the index
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const item = each_node_meta.item;
const item = node.context.type === 'Identifier' ? node.context : b.id('$$item');
let uses_index = each_node_meta.contains_group_binding;
let key_uses_index = false;

@ -13,15 +13,6 @@ export function MemberExpression(node, context) {
if (field) {
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();

@ -32,7 +32,7 @@ export function VariableDeclaration(node, context) {
) {
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (context.visit(init)))
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
@ -126,7 +126,7 @@ export function VariableDeclaration(node, context) {
value = b.call('$.proxy', value);
}
if (is_state_source(binding, context.state.analysis)) {
value = b.call('$.source', value);
value = b.call('$.state', value);
}
return value;
};
@ -173,7 +173,7 @@ export function VariableDeclaration(node, context) {
let id;
let rhs = value;
if (init.arguments[0].type === 'Identifier') {
if (rune === '$derived' && init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
@ -205,7 +205,7 @@ export function VariableDeclaration(node, context) {
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (context.visit(init)))
b.const(declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
@ -291,7 +291,7 @@ export function VariableDeclaration(node, context) {
*/
function create_state_declarators(declarator, scope, value) {
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');
@ -303,7 +303,7 @@ function create_state_declarators(declarator, scope, value) {
const binding = scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
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
*/
function flush_sequence(sequence) {
if (sequence.length === 1 && sequence[0].type === 'Text') {
if (sequence.every((node) => node.type === 'Text')) {
skipped += 1;
state.template.push(sequence[0].raw);
state.template.push(sequence.map((node) => node.raw).join(''));
return;
}
@ -118,7 +118,7 @@ export function process_children(nodes, initial, is_element, { visit, state }) {
// traverse to the last (n - 1) one when hydrating
if (skipped > 1) {
skipped -= 1;
state.init.push(b.stmt(get_node(false)));
state.init.push(b.stmt(b.call('$.next', skipped !== 1 && b.literal(skipped))));
}
}

@ -80,7 +80,7 @@ export function transform_module(analysis, source, options) {
? server_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) {
program.body[0].leadingComments = [
{

@ -356,7 +356,7 @@ export function server_component(analysis, options) {
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
body.unshift(
b.stmt(

@ -14,7 +14,6 @@ export function EachBlock(node, context) {
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
const item = each_node_meta.item;
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
@ -22,11 +21,8 @@ export function EachBlock(node, context) {
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement[]} */
const each = [b.const(item, b.member(array_id, index, true))];
const each = [b.const(/** @type {Pattern} */ (node.context), b.member(array_id, index, true))];
if (node.context.type !== 'Identifier') {
each.push(b.const(/** @type {Pattern} */ (node.context), item));
}
if (index.name !== node.index && node.index != null) {
each.push(b.let(node.index, index));
}
@ -34,8 +30,11 @@ export function EachBlock(node, context) {
each.push(.../** @type {BlockStatement} */ (context.visit(node.body)).body);
const for_loop = b.for(
b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, 'length')),
b.declaration('let', [
b.declarator(index, b.literal(0)),
b.declarator('$$length', b.member(array_id, 'length'))
]),
b.binary('<', index, b.id('$$length')),
b.update('++', index, false),
b.block(each)
);

@ -287,10 +287,11 @@ export function clean_nodes(
!first.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
))),
/** if a component or snippet starts with text, we need to add an anchor comment so that its text node doesn't get fused with its surroundings */
/** if a component/snippet/each block starts with text, we need to add an anchor comment so that its text node doesn't get fused with its surroundings */
is_text_first:
(parent.type === 'Fragment' ||
parent.type === 'SnippetBlock' ||
parent.type === 'EachBlock' ||
parent.type === 'SvelteComponent' ||
parent.type === 'Component' ||
parent.type === 'SvelteSelf') &&

@ -390,7 +390,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'derived', 'const');
const binding = scope.declare(id, 'template', 'const');
bindings.push(binding);
}
} else {
@ -401,7 +401,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
start: node.start,
end: node.end
};
const binding = scope.declare(id, 'derived', 'const');
const binding = scope.declare(id, 'template', 'const');
bindings.push(binding);
}
},
@ -492,7 +492,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const id of extract_identifiers(declarator.id)) {
const binding = state.scope.declare(
id,
is_parent_const_tag ? 'derived' : 'normal',
is_parent_const_tag ? 'template' : 'normal',
node.kind,
declarator.init
);
@ -547,9 +547,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
binding.metadata = { inside_rest: is_rest_id };
}
if (node.context.type !== 'Identifier') {
scope.declare(b.id('$$item'), 'derived', 'synthetic');
}
// Visit to pick up references from default initializers
visit(node.context, { scope });
@ -557,7 +555,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', node);
scope.declare(b.id(node.index), is_keyed ? 'template' : 'normal', 'const', node);
}
if (node.key) visit(node.key, { scope });
@ -583,7 +581,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
contains_group_binding: false,
array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null,
index: scope.root.unique('$$index'),
item: node.context.type === 'Identifier' ? node.context : b.id('$$item'),
declarations: scope.declarations,
is_controlled: false
};
@ -604,7 +601,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.value, value_scope);
context.visit(node.value, { scope: value_scope });
for (const id of extract_identifiers(node.value)) {
then_scope.declare(id, 'derived', 'const');
then_scope.declare(id, 'template', 'const');
value_scope.declare(id, 'normal', 'const');
}
}
@ -618,7 +615,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
scopes.set(node.error, error_scope);
context.visit(node.error, { scope: error_scope });
for (const id of extract_identifiers(node.error)) {
catch_scope.declare(id, 'derived', 'const');
catch_scope.declare(id, 'template', 'const');
error_scope.declare(id, 'normal', 'const');
}
}

@ -8,9 +8,9 @@ import { getLocator } from 'locate-character';
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
* @type {string | undefined}
* @type {string}
*/
export let filename;
@ -76,20 +76,16 @@ export function is_ignored(node, code) {
/**
* @param {string} _source
* @param {{ dev?: boolean; filename?: string; rootDir?: string }} options
* @param {{ dev?: boolean; filename: string; rootDir?: string }} options
*/
export function reset(_source, options) {
source = _source;
const root_dir = options.rootDir?.replace(/\\/g, '/');
filename = options.filename?.replace(/\\/g, '/');
filename = options.filename.replace(/\\/g, '/');
dev = !!options.dev;
if (
typeof filename === 'string' &&
typeof root_dir === 'string' &&
filename.startsWith(root_dir)
) {
if (typeof root_dir === 'string' && filename.startsWith(root_dir)) {
// make filename relative to rootDir
filename = filename.replace(root_dir, '').replace(/^[/\\]/, '');
}

@ -54,7 +54,7 @@ export interface CompileError extends ICompileDiagnostic {}
export type CssHashGetter = (args: {
name: string;
filename: string | undefined;
filename: string;
css: string;
hash: (input: string) => string;
}) => string;
@ -217,11 +217,7 @@ export interface ModuleCompileOptions {
// The following two somewhat scary looking types ensure that certain types are required but can be undefined still
export type ValidatedModuleCompileOptions = Omit<
Required<ModuleCompileOptions>,
'filename' | 'rootDir'
> & {
filename: ModuleCompileOptions['filename'];
export type ValidatedModuleCompileOptions = Omit<Required<ModuleCompileOptions>, 'rootDir'> & {
rootDir: ModuleCompileOptions['rootDir'];
};
@ -269,6 +265,7 @@ export interface Binding {
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `template`: A binding declared in the template, e.g. in an `await` block or `const` tag
*/
kind:
| 'normal'
@ -281,7 +278,8 @@ export interface Binding {
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive';
| 'legacy_reactive'
| 'template';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.

@ -412,7 +412,6 @@ export namespace Ast {
fallback?: Fragment;
index?: string;
key?: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
keyed: boolean;
@ -420,7 +419,6 @@ export namespace Ast {
/** Set if something in the array expression is shadowed within the each block */
array_name: Identifier | null;
index: Identifier;
item: Identifier;
declarations: Map<string, Binding>;
/**
* Optimization path for each blocks: If the parent isn't a fragment and
@ -472,8 +470,10 @@ export namespace Ast {
export interface Attribute extends BaseNode {
type: 'Attribute';
name: string;
/**
* Quoted/string values are represented by an array, even if they contain a single expression like `"{x}"`
*/
value: true | ExpressionTag | Array<Text | ExpressionTag>;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/** May be set if this is an event attribute */

@ -176,27 +176,25 @@ export function logical(operator, left, right) {
/**
* @param {'const' | 'let' | 'var'} kind
* @param {string | ESTree.Pattern} pattern
* @param {ESTree.Expression} [init]
* @param {ESTree.VariableDeclarator[]} declarations
* @returns {ESTree.VariableDeclaration}
*/
export function declaration(kind, pattern, init) {
if (typeof pattern === 'string') pattern = id(pattern);
export function declaration(kind, declarations) {
return {
type: 'VariableDeclaration',
kind,
declarations: [init ? declarator(pattern, init) : declarator(pattern)]
declarations
};
}
/**
* @param {ESTree.Pattern} id
* @param {ESTree.Pattern | string} pattern
* @param {ESTree.Expression} [init]
* @returns {ESTree.VariableDeclarator}
*/
export function declarator(id, init) {
return { type: 'VariableDeclarator', id, init };
export function declarator(pattern, init) {
if (typeof pattern === 'string') pattern = id(pattern);
return { type: 'VariableDeclarator', id: pattern, init };
}
/** @type {ESTree.EmptyStatement} */
@ -352,7 +350,7 @@ export function prop(kind, key, value, computed = false) {
* @returns {ESTree.PropertyDefinition}
*/
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: [] };
}
/**
@ -491,7 +489,7 @@ const this_instance = {
* @returns {ESTree.VariableDeclaration}
*/
function let_builder(pattern, init) {
return declaration('let', pattern, init);
return declaration('let', [declarator(pattern, init)]);
}
/**
@ -500,7 +498,7 @@ function let_builder(pattern, init) {
* @returns {ESTree.VariableDeclaration}
*/
function const_builder(pattern, init) {
return declaration('const', pattern, init);
return declaration('const', [declarator(pattern, init)]);
}
/**
@ -509,7 +507,7 @@ function const_builder(pattern, init) {
* @returns {ESTree.VariableDeclaration}
*/
function var_builder(pattern, init) {
return declaration('var', pattern, init);
return declaration('var', [declarator(pattern, init)]);
}
/**
@ -541,7 +539,8 @@ export function method(kind, key, params, body, computed = false, is_static = fa
kind,
value: function_builder(null, params, block(body)),
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*)/;
// 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) {
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
// map may contain a different file name. Patch our map beforehand to align sources so merging
// 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} 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);
}

@ -9,7 +9,7 @@ import * as w from './warnings.js';
*/
const common = {
filename: string(undefined),
filename: string('(unknown)'),
// 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

@ -94,7 +94,6 @@ export const codes = [
"options_removed_hydratable",
"options_removed_loop_guard_timeout",
"options_renamed_ssr_dom",
"derived_iife",
"export_let_unused",
"legacy_component_creation",
"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");
}
/**
* 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%`
* @param {null | NodeLike} node

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

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

@ -46,15 +46,20 @@ export function html(node, get_value, svg, mathml, skip_warning) {
var value = '';
/** @type {Effect | null} */
/** @type {Effect | undefined} */
var effect;
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);
effect = null;
effect = undefined;
}
if (value === '') return;

@ -92,7 +92,7 @@ export function createRawSnippet(fn) {
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (get_first_child(fragment));
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 3)) {
if (DEV && (get_next_sibling(element) !== null || element.nodeType !== 1)) {
w.invalid_raw_snippet_render();
}

@ -89,7 +89,11 @@ export function set_attribute(element, attribute, value, skip_warning) {
if (hydrating) {
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) {
check_src_in_dev_hydration(element, attribute, value);
}
@ -388,7 +392,7 @@ function check_src_in_dev_hydration(element, attribute, value) {
w.hydration_attribute_changed(
attribute,
element.outerHTML.replace(element.innerHTML, '...'),
element.outerHTML.replace(element.innerHTML, element.innerHTML && '...'),
String(value)
);
}

@ -3,7 +3,6 @@ import { teardown } from '../../reactivity/effects.js';
import { define_property, is_array } from '../../../shared/utils.js';
import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js';
import { dev_current_component_function } from '../../runtime.js';
import { FILENAME } from '../../../../constants.js';
import * as w from '../../warnings.js';
@ -273,8 +272,8 @@ export function handle_event_propagation(event) {
} finally {
// @ts-expect-error is used above
event.__root = handler_element;
// @ts-expect-error is used above
current_target = handler_element;
// @ts-ignore remove proxy on currentTarget
delete event.currentTarget;
}
}
@ -310,11 +309,7 @@ export function apply(
handler.apply(element, args);
} else if (has_side_effects || handler != null) {
const filename = component?.[FILENAME];
const location = filename
? loc
? ` at ${filename}:${loc[0]}:${loc[1]}`
: ` in ${filename}`
: '';
const location = loc ? ` at ${filename}:${loc[0]}:${loc[1]}` : ` in ${filename}`;
const event_name = args[0].type;
const description = `\`${event_name}\` handler${location}`;

@ -1,8 +1,7 @@
/** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, Task, TransitionFn, TransitionManager } from '#client' */
/** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, TransitionFn, TransitionManager } from '#client' */
import { noop, is_function } from '../../../shared/utils.js';
import { effect } from '../../reactivity/effects.js';
import { current_effect, untrack } from '../../runtime.js';
import { raf } from '../../timing.js';
import { loop } from '../../loop.js';
import { should_intro } from '../../render.js';
import { current_each_item } from '../blocks/each.js';
@ -97,17 +96,10 @@ export function animation(element, get_fn, get_params) {
) {
const options = get_fn()(this.element, { from, to }, get_params?.());
animation = animate(
this.element,
options,
undefined,
1,
() => {
animation?.abort();
animation = undefined;
},
undefined
);
animation = animate(this.element, options, undefined, 1, () => {
animation?.abort();
animation = undefined;
});
}
},
fix() {
@ -192,14 +184,13 @@ export function transition(flags, element, get_fn, get_params) {
/** @type {Animation | undefined} */
var outro;
/** @type {(() => void) | undefined} */
var reset;
function get_options() {
// If a transition is still ongoing, we use the existing options rather than generating
// new ones. This ensures that reversible transitions reverse smoothly, rather than
// jumping to a new spot because (for example) a different `duration` was used
return (current_options ??= get_fn()(element, get_params?.(), { direction }));
return (current_options ??= get_fn()(element, get_params?.() ?? /** @type {P} */ ({}), {
direction
}));
}
/** @type {TransitionManager} */
@ -208,65 +199,43 @@ export function transition(flags, element, get_fn, get_params) {
in() {
element.inert = inert;
// abort the outro to prevent overlap with the intro
outro?.abort();
// abort previous intro (can happen if an element is intro'd, then outro'd, then intro'd again)
intro?.abort();
if (!is_intro) {
outro?.abort();
outro?.reset?.();
return;
}
if (is_intro) {
dispatch_event(element, 'introstart');
intro = animate(
element,
get_options(),
outro,
1,
() => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
},
is_both
? undefined
: () => {
intro = current_options = undefined;
}
);
} else {
reset?.();
if (!is_outro) {
// if we intro then outro then intro again, we want to abort the first intro,
// if it's not a bidirectional transition
intro?.abort();
}
dispatch_event(element, 'introstart');
intro = animate(element, get_options(), outro, 1, () => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
});
},
out(fn) {
// abort previous outro (can happen if an element is outro'd, then intro'd, then outro'd again)
outro?.abort();
if (!is_outro) {
fn?.();
current_options = undefined;
return;
}
if (is_outro) {
element.inert = true;
dispatch_event(element, 'outrostart');
outro = animate(
element,
get_options(),
intro,
0,
() => {
dispatch_event(element, 'outroend');
outro = current_options = undefined;
fn?.();
},
is_both
? undefined
: () => {
outro = current_options = undefined;
}
);
element.inert = true;
dispatch_event(element, 'outrostart');
// TODO arguably the outro should never null itself out until _all_ outros for this effect have completed...
// in that case we wouldn't need to store `reset` separately
reset = outro.reset;
} else {
outro = animate(element, get_options(), intro, 0, () => {
dispatch_event(element, 'outroend');
fn?.();
}
});
},
stop: () => {
intro?.abort();
@ -282,7 +251,7 @@ export function transition(flags, element, get_fn, get_params) {
// parent (block) effect is where the state change happened. we can determine that by
// looking at whether the block effect is currently initializing
if (is_intro && should_intro) {
let run = is_global;
var run = is_global;
if (!run) {
var block = /** @type {Effect | null} */ (e.parent);
@ -311,17 +280,16 @@ export function transition(flags, element, get_fn, get_params) {
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options
* @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value `1` for intro, `0` for outro
* @param {(() => void) | undefined} on_finish Called after successfully completing the animation
* @param {(() => void) | undefined} on_abort Called if the animation is aborted
* @param {(() => void)} on_finish Called after successfully completing the animation
* @returns {Animation}
*/
function animate(element, options, counterpart, t2, on_finish, on_abort) {
function animate(element, options, counterpart, t2, on_finish) {
var is_intro = t2 === 1;
if (is_function(options)) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
// once DOM has been updated...
// once the DOM has been updated...
/** @type {Animation} */
var a;
var aborted = false;
@ -329,7 +297,7 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
queue_micro_task(() => {
if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish, on_abort);
a = animate(element, o, counterpart, t2, on_finish);
});
// ...but we want to do so without using `async`/`await` everywhere, so
@ -341,14 +309,15 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
},
deactivate: () => a.deactivate(),
reset: () => a.reset(),
t: (now) => a.t(now)
t: () => a.t()
};
}
counterpart?.deactivate();
if (!options?.duration) {
on_finish?.();
on_finish();
return {
abort: noop,
deactivate: noop,
@ -359,90 +328,75 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
const { delay = 0, css, tick, easing = linear } = options;
var start = raf.now() + delay;
var t1 = counterpart?.t(start) ?? 1 - t2;
var delta = t2 - t1;
var keyframes = [];
var duration = options.duration * Math.abs(delta);
var end = start + duration;
if (is_intro && counterpart === undefined) {
if (tick) {
tick(0, 1); // TODO put in nested effect, to avoid interleaved reads/writes?
}
/** @type {globalThis.Animation} */
var animation;
if (css) {
var styles = css_to_keyframe(css(0, 1));
keyframes.push(styles, styles);
}
}
/** @type {Task} */
var task;
var get_t = () => 1 - t2;
if (css) {
// run after a micro task so that all transitions that are lining up and are about to run can correctly measure the DOM
queue_micro_task(() => {
// WAAPI
var keyframes = [];
var n = Math.ceil(duration / (1000 / 60)); // `n` must be an integer, or we risk missing the `t2` value
// In case of a delayed intro, apply the initial style for the duration of the delay;
// else in case of a fade-in for example the element would be visible until the animation starts
if (is_intro && delay > 0) {
let m = Math.ceil(delay / (1000 / 60));
let keyframe = css_to_keyframe(css(0, 1));
for (let i = 0; i < m; i += 1) {
keyframes.push(keyframe);
// create a dummy animation that lasts as long as the delay (but with whatever devtools
// multiplier is in effect). in the common case that it is `0`, we keep it anyway so that
// the CSS keyframes aren't created until the DOM is updated
var animation = element.animate(keyframes, { duration: delay });
animation.onfinish = () => {
// for bidirectional transitions, we start from the current position,
// rather than doing a full intro/outro
var t1 = counterpart?.t() ?? 1 - t2;
counterpart?.abort();
var delta = t2 - t1;
var duration = /** @type {number} */ (options.duration) * Math.abs(delta);
var keyframes = [];
if (duration > 0) {
if (css) {
var n = Math.ceil(duration / (1000 / 60)); // `n` must be an integer, or we risk missing the `t2` value
for (var i = 0; i <= n; i += 1) {
var t = t1 + delta * easing(i / n);
var styles = css(t, 1 - t);
keyframes.push(css_to_keyframe(styles));
}
}
for (var i = 0; i <= n; i += 1) {
var t = t1 + delta * easing(i / n);
var styles = css(t, 1 - t);
keyframes.push(css_to_keyframe(styles));
}
get_t = () => {
var time = /** @type {number} */ (
/** @type {globalThis.Animation} */ (animation).currentTime
);
animation = element.animate(keyframes, {
delay: is_intro ? 0 : delay,
duration: duration + (is_intro ? delay : 0),
easing: 'linear',
fill: 'forwards'
});
return t1 + delta * easing(time / duration);
};
animation.finished
.then(() => {
on_finish?.();
if (t2 === 1) {
animation.cancel();
}
})
.catch((e) => {
// Error for DOMException: The user aborted a request. This results in two things:
// - startTime is `null`
// - currentTime is `null`
// We can't use the existence of an AbortError as this error and error code is shared
// with other Web APIs such as fetch().
if (animation.startTime !== null && animation.currentTime !== null) {
throw e;
}
});
});
} else {
// Timer
if (t1 === 0) {
tick?.(0, 1); // TODO put in nested effect, to avoid interleaved reads/writes?
}
if (tick) {
loop(() => {
if (animation.playState !== 'running') return false;
task = loop((now) => {
if (now >= end) {
tick?.(t2, 1 - t2);
on_finish?.();
return false;
}
var t = get_t();
tick(t, 1 - t);
if (now >= start) {
var p = t1 + delta * easing((now - start) / duration);
tick?.(p, 1 - p);
return true;
});
}
}
return true;
});
}
animation = element.animate(keyframes, { duration, fill: 'forwards' });
animation.onfinish = () => {
get_t = () => t2;
tick?.(t2, 1 - t2);
on_finish();
};
};
return {
abort: () => {
@ -451,23 +405,15 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
// This prevents memory leaks in Chromium
animation.effect = null;
}
task?.abort();
on_abort?.();
on_finish = undefined;
on_abort = undefined;
},
deactivate: () => {
on_finish = undefined;
on_abort = undefined;
on_finish = noop;
},
reset: () => {
if (t2 === 0) {
tick?.(1, 0);
}
},
t: (now) => {
var t = t1 + delta * easing((now - start) / duration);
return Math.min(1, Math.max(0, t));
}
t: () => get_t()
};
}

@ -66,9 +66,16 @@ export function hydrate_template(template) {
}
}
export function next() {
export function next(count = 1) {
if (hydrating) {
hydrate_next();
var i = count;
var node = hydrate_node;
while (i--) {
node = /** @type {TemplateNode} */ (get_next_sibling(node));
}
hydrate_node = node;
}
}

@ -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}
*/
export function state_unsafe_mutation() {
export function state_unsafe_local_read() {
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';
throw error;
} else {
// 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}
*/
export function svelte_component_invalid_this_value() {
export function state_unsafe_mutation() {
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';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("svelte_component_invalid_this_value");
throw new Error("state_unsafe_mutation");
}
}

@ -105,7 +105,7 @@ export {
user_effect,
user_pre_effect
} 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 {
prop,
rest_props,

@ -19,7 +19,7 @@ import * as e from './errors.js';
* @param {T} value
* @param {ProxyMetadata | null} [parent]
* @param {Source<T>} [prev] dev mode only
* @returns {ProxyStateObject<T> | T}
* @returns {T}
*/
export function proxy(value, parent = null, prev) {
// if non-proxyable, or is already a proxy, return `value`
@ -33,10 +33,17 @@ export function proxy(value, parent = null, prev) {
return value;
}
/** @type {Map<any, Source<any>>} */
var sources = new Map();
var is_proxied_array = is_array(value);
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} */
var metadata;
@ -91,17 +98,17 @@ export function proxy(value, parent = null, prev) {
deleteProperty(target, 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);
}
if (exists) {
update_version(version);
}
return exists;
return true;
},
get(target, prop, receiver) {
@ -187,6 +194,22 @@ export function proxy(value, parent = null, prev) {
var s = sources.get(prop);
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
// 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
@ -211,14 +234,6 @@ export function proxy(value, parent = null, prev) {
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);
// 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
// will not cause the length to be out of sync.
if (is_proxied_array && typeof prop === 'string') {
var ls = sources.get('length');
if (ls !== undefined) {
var n = Number(prop);
var ls = /** @type {Source<number>} */ (sources.get('length'));
var n = Number(prop);
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
if (Number.isInteger(n) && n >= ls.v) {
set(ls, n + 1);
}
}

@ -85,3 +85,49 @@ test('does not re-proxy proxies', () => {
assert.equal(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);
});

@ -33,7 +33,8 @@ import {
UNOWNED,
CLEAN,
INSPECT_EFFECT,
HEAD_EFFECT
HEAD_EFFECT,
MAYBE_DIRTY
} from '../constants.js';
import { set } from './sources.js';
import * as e from '../errors.js';
@ -281,6 +282,12 @@ export function legacy_pre_effect_reset() {
for (var token of context.l.r1) {
var effect = token.effect;
// If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through
// the effects dependencies and correctly ensure each dependency is up-to-date.
if ((effect.f & CLEAN) !== 0) {
set_signal_status(effect, MAYBE_DIRTY);
}
if (check_dirtiness(effect)) {
update_effect(effect);
}

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

Loading…
Cancel
Save