pull/11773/head
Rich Harris 2 years ago
commit 72326584dc

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: relax `Component` type

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: populate `this.#sources` when constructing reactive map

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: sort `{@const ...}` tags topologically in legacy mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: more robust handling of events in spread attributes

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve reactive Map and Set implementations

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: keep default values of props a proxy after reassignment

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: omit `state_referenced_locally` warning for component exports

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make `legacy.componentApi` option more visible

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve controlled each block cleanup performance

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure event.target is correct for delegation

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: deprecate html in favour of body for render()

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: speed up regex

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: bind `activeElement` and `pointerLockElement` in `<svelte:document>`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: address map reactivity regression

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow for more svelte-ignore to work

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: append start/end info to `AssignmentPattern` and `VariableDeclarator`

@ -57,6 +57,7 @@
"chilled-pumas-invite", "chilled-pumas-invite",
"chilled-seas-jog", "chilled-seas-jog",
"chilly-dolphins-lick", "chilly-dolphins-lick",
"chilly-laws-juggle",
"chilly-pans-raise", "chilly-pans-raise",
"chilly-rocks-hug", "chilly-rocks-hug",
"chilly-snakes-scream", "chilly-snakes-scream",
@ -66,8 +67,10 @@
"clever-rockets-burn", "clever-rockets-burn",
"clever-sloths-push", "clever-sloths-push",
"clever-stingrays-shout", "clever-stingrays-shout",
"cold-beans-tease",
"cold-birds-own", "cold-birds-own",
"cold-cheetahs-judge", "cold-cheetahs-judge",
"cold-lamps-accept",
"cold-masks-learn", "cold-masks-learn",
"cool-actors-tan", "cool-actors-tan",
"cool-ants-leave", "cool-ants-leave",
@ -110,6 +113,7 @@
"eight-carrots-hunt", "eight-carrots-hunt",
"eight-cougars-watch", "eight-cougars-watch",
"eight-hornets-punch", "eight-hornets-punch",
"eight-jeans-compare",
"eight-pianos-raise", "eight-pianos-raise",
"eight-steaks-shout", "eight-steaks-shout",
"eighty-bikes-camp", "eighty-bikes-camp",
@ -123,6 +127,7 @@
"empty-crabs-think", "empty-crabs-think",
"empty-flowers-change", "empty-flowers-change",
"empty-geckos-pretend", "empty-geckos-pretend",
"empty-horses-tell",
"empty-tools-share", "empty-tools-share",
"fair-bags-smoke", "fair-bags-smoke",
"fair-crabs-check", "fair-crabs-check",
@ -137,6 +142,7 @@
"few-clouds-shop", "few-clouds-shop",
"few-mugs-fail", "few-mugs-fail",
"few-teachers-know", "few-teachers-know",
"few-zoos-own",
"fifty-masks-give", "fifty-masks-give",
"fifty-rice-wait", "fifty-rice-wait",
"fifty-steaks-float", "fifty-steaks-float",
@ -170,6 +176,7 @@
"gentle-dolls-juggle", "gentle-dolls-juggle",
"gentle-sheep-hug", "gentle-sheep-hug",
"gentle-spies-happen", "gentle-spies-happen",
"gentle-ties-fetch",
"gentle-toys-chew", "gentle-toys-chew",
"gentle-trees-exercise", "gentle-trees-exercise",
"gentle-wasps-pull", "gentle-wasps-pull",
@ -212,7 +219,9 @@
"honest-icons-change", "honest-icons-change",
"honest-nails-share", "honest-nails-share",
"honest-pans-kick", "honest-pans-kick",
"hot-cooks-repair",
"hot-jobs-tap", "hot-jobs-tap",
"hot-sloths-clap",
"hungry-boxes-relate", "hungry-boxes-relate",
"hungry-dots-fry", "hungry-dots-fry",
"hungry-pants-push", "hungry-pants-push",
@ -226,6 +235,7 @@
"itchy-kings-deliver", "itchy-kings-deliver",
"itchy-lions-wash", "itchy-lions-wash",
"itchy-panthers-shave", "itchy-panthers-shave",
"itchy-peaches-compare",
"itchy-terms-guess", "itchy-terms-guess",
"khaki-cooks-develop", "khaki-cooks-develop",
"khaki-ligers-sing", "khaki-ligers-sing",
@ -270,10 +280,12 @@
"loud-numbers-flow", "loud-numbers-flow",
"loud-ravens-drop", "loud-ravens-drop",
"loud-socks-look", "loud-socks-look",
"lovely-bugs-sneeze",
"lovely-carpets-lick", "lovely-carpets-lick",
"lovely-houses-own", "lovely-houses-own",
"lovely-items-turn", "lovely-items-turn",
"lovely-rules-eat", "lovely-rules-eat",
"lovely-zebras-own",
"lucky-colts-remember", "lucky-colts-remember",
"lucky-geckos-swim", "lucky-geckos-swim",
"lucky-schools-hang", "lucky-schools-hang",
@ -310,6 +322,7 @@
"nine-cooks-join", "nine-cooks-join",
"ninety-dingos-walk", "ninety-dingos-walk",
"ninety-dots-train", "ninety-dots-train",
"ninety-rockets-battle",
"odd-buckets-lie", "odd-buckets-lie",
"odd-needles-joke", "odd-needles-joke",
"odd-schools-wait", "odd-schools-wait",
@ -327,6 +340,7 @@
"olive-seals-sell", "olive-seals-sell",
"olive-shirts-complain", "olive-shirts-complain",
"olive-socks-kick", "olive-socks-kick",
"orange-comics-prove",
"orange-crews-rescue", "orange-crews-rescue",
"orange-dingos-poke", "orange-dingos-poke",
"orange-masks-exercise", "orange-masks-exercise",
@ -412,6 +426,8 @@
"shiny-baboons-play", "shiny-baboons-play",
"shiny-mayflies-clean", "shiny-mayflies-clean",
"shiny-melons-love", "shiny-melons-love",
"shiny-months-tease",
"shiny-pillows-relax",
"shiny-rats-heal", "shiny-rats-heal",
"shiny-shrimps-march", "shiny-shrimps-march",
"short-buses-camp", "short-buses-camp",
@ -425,11 +441,13 @@
"silly-lies-film", "silly-lies-film",
"silly-ways-wash", "silly-ways-wash",
"silver-points-approve", "silver-points-approve",
"silver-sheep-knock",
"six-bears-trade", "six-bears-trade",
"six-boats-shave", "six-boats-shave",
"sixty-items-crash", "sixty-items-crash",
"sixty-numbers-hope", "sixty-numbers-hope",
"sixty-pandas-rush", "sixty-pandas-rush",
"sixty-plants-cover",
"sleepy-cats-eat", "sleepy-cats-eat",
"slimy-clouds-talk", "slimy-clouds-talk",
"slimy-hairs-impress", "slimy-hairs-impress",
@ -497,6 +515,7 @@
"tall-tigers-wait", "tall-tigers-wait",
"tame-cycles-kneel", "tame-cycles-kneel",
"tame-dots-battle", "tame-dots-battle",
"tame-goats-bow",
"tame-spies-drum", "tame-spies-drum",
"tasty-cheetahs-appear", "tasty-cheetahs-appear",
"tasty-numbers-perform", "tasty-numbers-perform",
@ -513,6 +532,7 @@
"thick-pans-tell", "thick-pans-tell",
"thick-shirts-deliver", "thick-shirts-deliver",
"thick-swans-type", "thick-swans-type",
"thin-colts-yawn",
"thin-foxes-lick", "thin-foxes-lick",
"thin-years-rhyme", "thin-years-rhyme",
"thirty-flowers-sit", "thirty-flowers-sit",
@ -530,6 +550,8 @@
"three-suits-grin", "three-suits-grin",
"tidy-buses-whisper", "tidy-buses-whisper",
"tidy-chefs-taste", "tidy-chefs-taste",
"tidy-deers-hope",
"tidy-fans-relax",
"tidy-starfishes-allow", "tidy-starfishes-allow",
"tiny-kings-whisper", "tiny-kings-whisper",
"tiny-meals-deliver", "tiny-meals-deliver",

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: reevaluate namespace in slots

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: assign message to error object in `handle_error` using `Object.defineProperty`

@ -0,0 +1,5 @@
---
"svelte": patch
---
chore: improve $state.frozen performance in prod

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: removed deferred event updates

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: always create wrapper `<div>` for `<svelte:component>` with CSS custom properties

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: add svelte/events package and export `on` function

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: relax slot prop validation on components

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correctly backport `svelte:element` to old AST

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add `unused-export-let` to legacy lint replacements

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure frozen objects in state are correctly skipped

@ -30,9 +30,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
@ -47,9 +45,7 @@ jobs:
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 18

@ -21,9 +21,7 @@ jobs:
with: with:
# This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits
fetch-depth: 0 fetch-depth: 0
- uses: pnpm/action-setup@v2.2.4 - uses: pnpm/action-setup@v4
with:
version: 9
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

@ -268,7 +268,9 @@ As with `<svelte:window>`, this element may only appear the top level of your co
You can also bind to the following properties: You can also bind to the following properties:
- `activeElement`
- `fullscreenElement` - `fullscreenElement`
- `pointerLockElement`
- `visibilityState` - `visibilityState`
All are readonly. All are readonly.

@ -5,10 +5,18 @@ import lube from 'eslint-plugin-lube';
export default [ export default [
...svelte_config, ...svelte_config,
{ {
languageOptions: {
parserOptions: {
project: true
}
},
plugins: { plugins: {
lube lube
}, },
rules: { rules: {
'@typescript-eslint/await-thenable': 'error',
'@typescript-eslint/prefer-promise-reject-errors': 'error',
'@typescript-eslint/require-await': 'error',
'no-console': 'error', 'no-console': 'error',
'lube/svelte-naming-convention': ['error', { fixSameNames: true }], 'lube/svelte-naming-convention': ['error', { fixSameNames: true }],
// eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off // eslint isn't that well-versed with JSDoc to know that `foo: /** @type{..} */ (foo)` isn't a violation of this rule, so turn it off
@ -43,7 +51,6 @@ export default [
'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
'sites/svelte-5-preview/**', 'sites/svelte-5-preview/**',
'playgrounds/demo/src/**',
'tmp/**', 'tmp/**',
// wasn't checked previously, reenable at some point // wasn't checked previously, reenable at some point
'sites/svelte.dev/**' 'sites/svelte.dev/**'

@ -5,6 +5,7 @@
"private": true, "private": true,
"type": "module", "type": "module",
"license": "MIT", "license": "MIT",
"packageManager": "pnpm@9.2.0",
"engines": { "engines": {
"pnpm": "^9.0.0" "pnpm": "^9.0.0"
}, },

@ -1,5 +1,73 @@
# svelte # svelte
## 5.0.0-next.151
### Patch Changes
- fix: relax `Component` type ([#11929](https://github.com/sveltejs/svelte/pull/11929))
- fix: sort `{@const ...}` tags topologically in legacy mode ([#11908](https://github.com/sveltejs/svelte/pull/11908))
- chore: deprecate html in favour of body for render() ([#11927](https://github.com/sveltejs/svelte/pull/11927))
- fix: append start/end info to `AssignmentPattern` and `VariableDeclarator` ([#11930](https://github.com/sveltejs/svelte/pull/11930))
- fix: relax slot prop validation on components ([#11923](https://github.com/sveltejs/svelte/pull/11923))
## 5.0.0-next.150
### Patch Changes
- fix: populate `this.#sources` when constructing reactive map ([#11913](https://github.com/sveltejs/svelte/pull/11913))
- fix: omit `state_referenced_locally` warning for component exports ([#11905](https://github.com/sveltejs/svelte/pull/11905))
- fix: ensure event.target is correct for delegation ([#11900](https://github.com/sveltejs/svelte/pull/11900))
- chore: speed up regex ([#11918](https://github.com/sveltejs/svelte/pull/11918))
- feat: bind `activeElement` and `pointerLockElement` in `<svelte:document>` ([#11879](https://github.com/sveltejs/svelte/pull/11879))
- fix: correctly backport `svelte:element` to old AST ([#11917](https://github.com/sveltejs/svelte/pull/11917))
- fix: add `unused-export-let` to legacy lint replacements ([#11896](https://github.com/sveltejs/svelte/pull/11896))
## 5.0.0-next.149
### Patch Changes
- fix: keep default values of props a proxy after reassignment ([#11860](https://github.com/sveltejs/svelte/pull/11860))
- fix: address map reactivity regression ([#11882](https://github.com/sveltejs/svelte/pull/11882))
- fix: assign message to error object in `handle_error` using `Object.defineProperty` ([#11675](https://github.com/sveltejs/svelte/pull/11675))
- fix: ensure frozen objects in state are correctly skipped ([#11889](https://github.com/sveltejs/svelte/pull/11889))
## 5.0.0-next.148
### Patch Changes
- chore: improve $state.frozen performance in prod ([#11852](https://github.com/sveltejs/svelte/pull/11852))
- breaking: removed deferred event updates ([#11855](https://github.com/sveltejs/svelte/pull/11855))
## 5.0.0-next.147
### Patch Changes
- fix: improve reactive Map and Set implementations ([#11827](https://github.com/sveltejs/svelte/pull/11827))
- fix: improve controlled each block cleanup performance ([#11839](https://github.com/sveltejs/svelte/pull/11839))
## 5.0.0-next.146
### Patch Changes
- fix: allow for more svelte-ignore to work ([#11833](https://github.com/sveltejs/svelte/pull/11833))
- fix: reevaluate namespace in slots ([#11849](https://github.com/sveltejs/svelte/pull/11849))
## 5.0.0-next.145 ## 5.0.0-next.145
### Patch Changes ### Patch Changes

@ -1364,7 +1364,9 @@ export interface SvelteMediaTimeRange {
} }
export interface SvelteDocumentAttributes extends HTMLAttributes<Document> { export interface SvelteDocumentAttributes extends HTMLAttributes<Document> {
readonly 'bind:activeElement'?: Document['activeElement'] | undefined | null;
readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null; readonly 'bind:fullscreenElement'?: Document['fullscreenElement'] | undefined | null;
readonly 'bind:pointerLockElement'?: Document['pointerLockElement'] | undefined | null;
readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null; readonly 'bind:visibilityState'?: Document['visibilityState'] | undefined | null;
} }

@ -14,6 +14,10 @@
> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information > %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
## component_api_invalid_new
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `legacy.componentApi` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
## each_key_duplicate ## each_key_duplicate
> Keyed each block has duplicate key at indexes %a% and %b% > Keyed each block has duplicate key at indexes %a% and %b%

@ -96,6 +96,10 @@
> This type of directive is not valid on components > This type of directive is not valid on components
## const_tag_cycle
> Cyclical dependency detected: %cycle%
## const_tag_invalid_expression ## const_tag_invalid_expression
> {@const ...} must consist of a single variable declaration > {@const ...} must consist of a single variable declaration

@ -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.145", "version": "5.0.0-next.151",
"type": "module", "type": "module",
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"engines": { "engines": {
@ -81,6 +81,10 @@
"./transition": { "./transition": {
"types": "./types/index.d.ts", "types": "./types/index.d.ts",
"default": "./src/transition/index.js" "default": "./src/transition/index.js"
},
"./events": {
"types": "./types/index.d.ts",
"default": "./src/events/index.js"
} }
}, },
"repository": { "repository": {

@ -5,6 +5,9 @@ import virtual from '@rollup/plugin-virtual';
import { nodeResolve } from '@rollup/plugin-node-resolve'; import { nodeResolve } from '@rollup/plugin-node-resolve';
import { compile } from 'svelte/compiler'; import { compile } from 'svelte/compiler';
/**
* @param {string} entry
*/
async function bundle_code(entry) { async function bundle_code(entry) {
const bundle = await rollup({ const bundle = await rollup({
input: '__entry__', input: '__entry__',

@ -33,6 +33,7 @@ await createBundle({
[`${pkg.name}/server`]: `${dir}/src/server/index.js`, [`${pkg.name}/server`]: `${dir}/src/server/index.js`,
[`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`, [`${pkg.name}/store`]: `${dir}/src/store/public.d.ts`,
[`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`, [`${pkg.name}/transition`]: `${dir}/src/transition/public.d.ts`,
[`${pkg.name}/events`]: `${dir}/src/events/index.js`,
// TODO remove in Svelte 6 // TODO remove in Svelte 6
[`${pkg.name}/types/compiler/preprocess`]: `${dir}/src/compiler/preprocess/legacy-public.d.ts`, [`${pkg.name}/types/compiler/preprocess`]: `${dir}/src/compiler/preprocess/legacy-public.d.ts`,
[`${pkg.name}/types/compiler/interfaces`]: `${dir}/src/compiler/types/legacy-interfaces.d.ts` [`${pkg.name}/types/compiler/interfaces`]: `${dir}/src/compiler/types/legacy-interfaces.d.ts`

@ -9,6 +9,8 @@ const messages = {};
const seen = new Set(); const seen = new Set();
for (const category of fs.readdirSync('messages')) { for (const category of fs.readdirSync('messages')) {
if (category.startsWith('.')) continue;
messages[category] = {}; messages[category] = {};
for (const file of fs.readdirSync(`messages/${category}`)) { for (const file of fs.readdirSync(`messages/${category}`)) {
@ -54,11 +56,23 @@ for (const category of fs.readdirSync('messages')) {
} }
} }
/**
* @param {string} name
* @param {string} dest
*/
function transform(name, dest) { function transform(name, dest) {
const source = fs const source = fs
.readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8') .readFileSync(new URL(`./templates/${name}.js`, import.meta.url), 'utf-8')
.replace(/\r\n/g, '\n'); .replace(/\r\n/g, '\n');
/**
* @type {Array<{
* type: string;
* value: string;
* start: number;
* end: number
* }>}
*/
const comments = []; const comments = [];
let ast = acorn.parse(source, { let ast = acorn.parse(source, {
@ -135,6 +149,7 @@ function transform(name, dest) {
for (const code in category) { for (const code in category) {
const { messages } = category[code]; const { messages } = category[code];
/** @type {string[]} */
const vars = []; const vars = [];
const group = messages.map((text, i) => { const group = messages.map((text, i) => {
@ -225,7 +240,7 @@ function transform(name, dest) {
Block(node, context) { Block(node, context) {
if (!node.value.includes('PARAMETER')) return; if (!node.value.includes('PARAMETER')) return;
const value = node.value const value = /** @type {string} */ (node.value)
.split('\n') .split('\n')
.map((line) => { .map((line) => {
if (line === ' * MESSAGE') { if (line === ' * MESSAGE') {

@ -1,4 +1,4 @@
import { filename, locator, warnings, ignore_stack } from './state.js'; import { filename, locator, warnings, ignore_stack, ignore_map } from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
@ -8,7 +8,11 @@ import { filename, locator, warnings, ignore_stack } from './state.js';
* @param {string} message * @param {string} message
*/ */
function w(node, code, message) { function w(node, code, message) {
if (ignore_stack.at(-1)?.has(code)) return; let stack = ignore_stack;
if (node) {
stack = ignore_map.get(node) ?? ignore_stack;
}
if (stack && stack.at(-1)?.has(code)) return;
warnings.push({ warnings.push({
code, code,

@ -752,6 +752,16 @@ export function component_invalid_directive(node) {
e(node, "component_invalid_directive", "This type of directive is not valid on components"); e(node, "component_invalid_directive", "This type of directive is not valid on components");
} }
/**
* Cyclical dependency detected: %cycle%
* @param {null | number | NodeLike} node
* @param {string} cycle
* @returns {never}
*/
export function const_tag_cycle(node, cycle) {
e(node, "const_tag_cycle", `Cyclical dependency detected: ${cycle}`);
}
/** /**
* {@const ...} must consist of a single variable declaration * {@const ...} must consist of a single variable declaration
* @param {null | number | NodeLike} node * @param {null | number | NodeLike} node

@ -478,7 +478,11 @@ export function convert(source, ast) {
SvelteElement(node, { visit }) { SvelteElement(node, { visit }) {
/** @type {import('estree').Expression | string} */ /** @type {import('estree').Expression | string} */
let tag = node.tag; let tag = node.tag;
if (tag.type === 'Literal' && typeof tag.value === 'string') { if (
tag.type === 'Literal' &&
typeof tag.value === 'string' &&
source[/** @type {number} */ (node.tag.start) - 1] !== '{'
) {
tag = tag.value; tag = tag.value;
} }

@ -7,7 +7,6 @@ import full_char_code_at from './utils/full_char_code_at.js';
import * as e from '../../errors.js'; import * as e from '../../errors.js';
import { create_fragment } from './utils/create.js'; import { create_fragment } from './utils/create.js';
import read_options from './read/options.js'; import read_options from './read/options.js';
import { locator } from '../../state.js';
const regex_position_indicator = / \(\d+:\d+\)$/; const regex_position_indicator = / \(\d+:\d+\)$/;

@ -74,6 +74,9 @@ const visitors = {
TSEnumDeclaration() { TSEnumDeclaration() {
return b.empty; return b.empty;
}, },
TSParameterProperty(node) {
return node.parameter;
},
Identifier(node) { Identifier(node) {
if (node.typeAnnotation) { if (node.typeAnnotation) {
return { return {

@ -14,7 +14,7 @@ const valid_tag_name = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
/** Invalid attribute characters if the attribute is not surrounded by quotes */ /** Invalid attribute characters if the attribute is not surrounded by quotes */
const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/; const regex_starts_with_invalid_attr_value = /^(\/>|[\s"'=<>`])/;
/** @type {Map<string, import('#compiler').SvelteNode['type']>} */ /** @type {Map<string, import('#compiler').ElementLike['type']>} */
const root_only_meta_tags = new Map([ const root_only_meta_tags = new Map([
['svelte:head', 'SvelteHead'], ['svelte:head', 'SvelteHead'],
['svelte:options', 'SvelteOptions'], ['svelte:options', 'SvelteOptions'],
@ -23,7 +23,7 @@ const root_only_meta_tags = new Map([
['svelte:body', 'SvelteBody'] ['svelte:body', 'SvelteBody']
]); ]);
/** @type {Map<string, import('#compiler').SvelteNode['type']>} */ /** @type {Map<string, import('#compiler').ElementLike['type']>} */
const meta_tags = new Map([ const meta_tags = new Map([
...root_only_meta_tags, ...root_only_meta_tags,
['svelte:element', 'SvelteElement'], ['svelte:element', 'SvelteElement'],
@ -132,11 +132,10 @@ export default function tag(parser) {
: 'RegularElement'; : 'RegularElement';
/** @type {import('#compiler').ElementLike} */ /** @type {import('#compiler').ElementLike} */
// @ts-expect-error TODO can't figure out this error
const element = const element =
type === 'RegularElement' type === 'RegularElement'
? { ? {
type: /** @type {import('#compiler').ElementLike['type']} */ (type), type: type,
start, start,
end: -1, end: -1,
name, name,
@ -144,12 +143,14 @@ export default function tag(parser) {
fragment: create_fragment(true), fragment: create_fragment(true),
metadata: { metadata: {
svg: false, svg: false,
mathml: false,
scoped: false,
has_spread: false has_spread: false
}, },
parent: null parent: null
} }
: { : /** @type {import('#compiler').ElementLike} */ ({
type: /** @type {import('#compiler').ElementLike['type']} */ (type), type,
start, start,
end: -1, end: -1,
name, name,
@ -159,7 +160,7 @@ export default function tag(parser) {
metadata: { metadata: {
svg: false svg: false
} }
}; });
parser.allow_whitespace(); parser.allow_whitespace();

@ -281,10 +281,13 @@ function open(parser) {
parser.allow_whitespace(); parser.allow_whitespace();
if (parser.eat('=')) { if (parser.eat('=')) {
parser.allow_whitespace(); parser.allow_whitespace();
const right = read_expression(parser);
pattern = { pattern = {
type: 'AssignmentPattern', type: 'AssignmentPattern',
left: pattern, left: pattern,
right: read_expression(parser) right: right,
start: pattern.start,
end: right.end
}; };
} }
@ -572,7 +575,7 @@ function special(parser) {
declaration: { declaration: {
type: 'VariableDeclaration', type: 'VariableDeclaration',
kind: 'const', kind: 'const',
declarations: [{ type: 'VariableDeclarator', id, init }], declarations: [{ type: 'VariableDeclarator', id, init, start: id.start, end: init.end }],
start: start + 2, // start at const, not at @const start: start + 2, // start at const, not at @const
end: parser.index - 1 end: parser.index - 1
} }

@ -30,7 +30,7 @@ import { prune } from './css/css-prune.js';
import { hash } from './utils.js'; import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js'; import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js'; import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { pop_ignore, push_ignore } from '../../state.js'; import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
/** /**
* @param {import('#compiler').Script | null} script * @param {import('#compiler').Script | null} script
@ -1107,6 +1107,7 @@ function is_safe_identifier(expression, scope) {
/** @type {import('./types').Visitors} */ /** @type {import('./types').Visitors} */
const common_visitors = { const common_visitors = {
_(node, { state, next, path }) { _(node, { state, next, path }) {
ignore_map.set(node, structuredClone(ignore_stack));
const parent = path.at(-1); const parent = path.at(-1);
if (parent?.type === 'Fragment' && node.type !== 'Comment' && node.type !== 'Text') { if (parent?.type === 'Fragment' && node.type !== 'Comment' && node.type !== 'Text') {
const idx = parent.nodes.indexOf(/** @type {any} */ (node)); const idx = parent.nodes.indexOf(/** @type {any} */ (node));
@ -1129,6 +1130,7 @@ const common_visitors = {
if (ignores.length > 0) { if (ignores.length > 0) {
push_ignore(ignores); push_ignore(ignores);
ignore_map.set(node, structuredClone(ignore_stack));
next(); next();
pop_ignore(); pop_ignore();
} }
@ -1148,6 +1150,7 @@ const common_visitors = {
} }
if (ignores.length > 0) { if (ignores.length > 0) {
push_ignore(ignores); push_ignore(ignores);
ignore_map.set(node, structuredClone(ignore_stack));
next(); next();
pop_ignore(); pop_ignore();
} }
@ -1241,6 +1244,12 @@ const common_visitors = {
context.state.expression.metadata.dynamic = true; context.state.expression.metadata.dynamic = true;
} }
// TODO it would be better to just bail out when we hit the ExportSpecifier node but that's
// not currently possibly because of our visitor merging, which I desperately want to nuke
const is_export_specifier =
/** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)).type ===
'ExportSpecifier';
if ( if (
context.state.analysis.runes && context.state.analysis.runes &&
node !== binding.node && node !== binding.node &&
@ -1255,6 +1264,7 @@ const common_visitors = {
!should_proxy_or_freeze(binding.initial.arguments[0], context.state.scope)))) || !should_proxy_or_freeze(binding.initial.arguments[0], context.state.scope)))) ||
binding.kind === 'frozen_state' || binding.kind === 'frozen_state' ||
binding.kind === 'derived') && binding.kind === 'derived') &&
!is_export_specifier &&
// We're only concerned with reads here // We're only concerned with reads here
(parent.type !== 'AssignmentExpression' || parent.left !== node) && (parent.type !== 'AssignmentExpression' || parent.left !== node) &&
parent.type !== 'UpdateExpression' parent.type !== 'UpdateExpression'

@ -87,7 +87,7 @@ function validate_component(node, context) {
validate_attribute_name(attribute); validate_attribute_name(attribute);
if (attribute.name === 'slot') { if (attribute.name === 'slot') {
validate_slot_attribute(context, attribute); validate_slot_attribute(context, attribute, true);
} }
} }
} }
@ -253,8 +253,9 @@ function validate_attribute_name(attribute) {
/** /**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context * @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute * @param {import('#compiler').Attribute} attribute
* @param {boolean} is_component
*/ */
function validate_slot_attribute(context, attribute) { function validate_slot_attribute(context, attribute, is_component = false) {
let owner = undefined; let owner = undefined;
let i = context.path.length; let i = context.path.length;
@ -310,7 +311,7 @@ function validate_slot_attribute(context, attribute) {
} }
} }
} }
} else { } else if (!is_component) {
e.slot_attribute_invalid_placement(attribute); e.slot_attribute_invalid_placement(attribute);
} }
} }

@ -48,55 +48,6 @@ export function client_component(source, analysis, options) {
scopes: analysis.template.scopes, scopes: analysis.template.scopes,
hoisted: [b.import_all('$', 'svelte/internal/client')], hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node node: /** @type {any} */ (null), // populated by the root node
// these should be set by create_block - if they're called outside, it's a bug
get before_init() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('before_init.push should not be called outside create_block');
};
return a;
},
get init() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('init.push should not be called outside create_block');
};
return a;
},
get update() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('update.push should not be called outside create_block');
};
return a;
},
get after_update() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('after_update.push should not be called outside create_block');
};
return a;
},
get template() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('template.push should not be called outside create_block');
};
return a;
},
get locations() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('locations.push should not be called outside create_block');
};
return a;
},
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
metadata: { metadata: {
context: { context: {
@ -110,7 +61,15 @@ export function client_component(source, analysis, options) {
preserve_whitespace: options.preserveWhitespace, preserve_whitespace: options.preserveWhitespace,
public_state: new Map(), public_state: new Map(),
private_state: new Map(), private_state: new Map(),
in_constructor: false in_constructor: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
before_init: /** @type {any} */ (null),
init: /** @type {any} */ (null),
update: /** @type {any} */ (null),
after_update: /** @type {any} */ (null),
template: /** @type {any} */ (null),
locations: /** @type {any} */ (null)
}; };
const module = /** @type {import('estree').Program} */ ( const module = /** @type {import('estree').Program} */ (
@ -493,7 +452,7 @@ export function client_component(source, analysis, options) {
body.unshift(b.imports([['createClassComponent', '$$_createClassComponent']], 'svelte/legacy')); body.unshift(b.imports([['createClassComponent', '$$_createClassComponent']], 'svelte/legacy'));
component_block.body.unshift( component_block.body.unshift(
b.if( b.if(
b.binary('===', b.id('new.target'), b.id(analysis.name)), b.id('new.target'),
b.return( b.return(
b.call( b.call(
'$$_createClassComponent', '$$_createClassComponent',
@ -504,15 +463,7 @@ export function client_component(source, analysis, options) {
) )
); );
} else if (options.dev) { } else if (options.dev) {
component_block.body.unshift( component_block.body.unshift(b.stmt(b.call('$.check_target', b.id('new.target'))));
b.if(
b.binary('===', b.id('new.target'), b.id(analysis.name)),
b.throw_error(
`Instantiating a component with \`new\` is no longer valid in Svelte 5. ` +
'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information'
)
)
);
} }
if (state.events.size > 0) { if (state.events.size > 0) {

@ -49,9 +49,9 @@ export interface ComponentClientTransformState extends ClientTransformState {
namespace: Namespace; namespace: Namespace;
bound_contenteditable: boolean; bound_contenteditable: boolean;
/** /**
* Stuff that is set within the children of one `create_block` that is relevant * Stuff that is set within the children of one `Fragment` visitor that is relevant
* to said `create_block`. Shouldn't be destructured or otherwise spread unless * to said fragment. Shouldn't be destructured or otherwise spread unless inside the
* inside `create_block` to keep the object reference intact (it's also nested * `Fragment` visitor to keep the object reference intact (it's also nested
* within `metadata` for this reason). * within `metadata` for this reason).
*/ */
context: { context: {

@ -292,7 +292,13 @@ export function serialize_set_binding(node, context, fallback, options) {
const serialize = () => { const serialize = () => {
if (left === node.left) { if (left === node.left) {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { const is_initial_proxy =
binding.initial !== null &&
should_proxy_or_freeze(
/**@type {import("estree").Expression}*/ (binding.initial),
context.state.scope
);
if ((binding.kind === 'prop' || binding.kind === 'bindable_prop') && !is_initial_proxy) {
return b.call(left, value); return b.call(left, value);
} else if (is_store) { } else if (is_store) {
return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value);
@ -318,6 +324,18 @@ export function serialize_set_binding(node, context, fallback, options) {
? b.call('$.freeze', value) ? b.call('$.freeze', value)
: value : value
); );
} else if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
is_initial_proxy
) {
call = b.call(
left,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? serialize_proxy_reassignment(value, left_name, state)
: value
);
} else { } else {
call = b.call('$.set', b.id(left_name), value); call = b.call('$.set', b.id(left_name), value);
} }

@ -839,26 +839,37 @@ function serialize_inline_component(node, component_name, context) {
} }
} }
children[slot_name] = children[slot_name] || []; (children[slot_name] ||= []).push(child);
children[slot_name].push(child);
} }
// Serialize each slot // Serialize each slot
/** @type {import('estree').Property[]} */ /** @type {import('estree').Property[]} */
const serialized_slots = []; const serialized_slots = [];
for (const slot_name of Object.keys(children)) { for (const slot_name of Object.keys(children)) {
const body = create_block( const block = /** @type {import('estree').BlockStatement} */ (
node, context.visit(
node.fragment, {
`${node.name}_${slot_name}`, ...node.fragment,
children[slot_name], // @ts-expect-error
context nodes: children[slot_name]
},
{
...context.state,
scope:
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
context.state.scope
}
)
); );
if (body.length === 0) continue;
if (block.body.length === 0) continue;
const slot_fn = b.arrow( const slot_fn = b.arrow(
[b.id('$$anchor'), b.id('$$slotProps')], [b.id('$$anchor'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) b.block([
...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []),
...block.body
])
); );
if (slot_name === 'default' && !has_children_prop) { if (slot_name === 'default' && !has_children_prop) {
@ -905,6 +916,27 @@ function serialize_inline_component(node, component_name, context) {
); );
} }
if (node.type === 'SvelteComponent') {
const prev = fn;
fn = (node_id) => {
let component = b.call(
'$.component',
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow(
[b.id(component_name)],
b.block([
b.stmt(
context.state.options.dev
? b.call('$.validate_dynamic_component', b.thunk(prev(node_id)))
: prev(node_id)
)
])
)
);
return component;
};
}
if (Object.keys(custom_css_props).length > 0) { if (Object.keys(custom_css_props).length > 0) {
const prev = fn; const prev = fn;
fn = (node_id) => fn = (node_id) =>
@ -1016,189 +1048,6 @@ function serialize_locations(locations) {
); );
} }
/**
* Creates a new block which looks roughly like this:
* ```js
* // hoisted:
* const block_name = $.template(`...`);
*
* // for the main block:
* const id = block_name();
* // init stuff and possibly render effect
* $.append($$anchor, id);
* ```
* Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block.
* @param {import('#compiler').SvelteNode} parent
* @param {import('#compiler').Fragment} fragment
* @param {string} name
* @param {import('#compiler').SvelteNode[]} nodes
* @param {import('../types.js').ComponentContext} context
* @returns {import('estree').Statement[]}
*/
function create_block(parent, fragment, name, nodes, context) {
const namespace = infer_namespace(context.state.metadata.namespace, parent, nodes);
const { hoisted, trimmed } = clean_nodes(
parent,
nodes,
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
return [];
}
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const template_name = context.state.scope.root.unique(name);
/** @type {import('estree').Statement[]} */
const body = [];
/** @type {import('estree').Statement | undefined} */
let close = undefined;
/** @type {import('../types').ComponentClientTransformState} */
const state = {
...context.state,
scope: context.state.scopes.get(fragment) ?? context.state.scope,
before_init: [],
init: [],
update: [],
after_update: [],
template: [],
locations: [],
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
};
for (const node of hoisted) {
context.visit(node, state);
}
/**
* @param {import('estree').Identifier} template_name
* @param {import('estree').Expression[]} args
*/
const add_template = (template_name, args) => {
let call = b.call(get_template_function(namespace, state), ...args);
if (context.state.options.dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(context.state.analysis.name), b.id('filename')),
serialize_locations(state.locations)
);
}
context.state.hoisted.push(b.var(template_name, call));
};
if (is_single_element) {
const element = /** @type {import('#compiler').RegularElement} */ (trimmed[0]);
const id = b.id(context.state.scope.generate(element.name));
context.visit(element, {
...state,
node: id
});
/** @type {import('estree').Expression[]} */
const args = [b.template([b.quasi(state.template.join(''), true)], [])];
if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
}
add_template(template_name, args);
body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init);
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
...context,
state
});
body.push(b.var(id, b.call('$.text', b.id('$$anchor'))), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) =>
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
process_children(trimmed, expression, false, { ...context, state });
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []),
b.literal(flags)
]);
body.push(b.var(id, b.call(template_name)));
}
body.push(...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
} else {
body.push(...state.before_init, ...state.init);
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state));
}
body.push(...state.after_update);
if (close !== undefined) {
// It's important that close is the last statement in the block, as any previous statements
// could contain element insertions into the template, which the close statement needs to
// know of when constructing the list of current inner elements.
body.push(close);
}
return body;
}
/** /**
* *
* @param {import('#compiler').Namespace} namespace * @param {import('#compiler').Namespace} namespace
@ -1688,7 +1537,184 @@ function serialize_template_literal(values, visit, state) {
/** @type {import('../types').ComponentVisitors} */ /** @type {import('../types').ComponentVisitors} */
export const template_visitors = { export const template_visitors = {
Fragment(node, context) { Fragment(node, context) {
const body = create_block(context.path.at(-1) ?? node, node, 'root', node.nodes, context); // Creates a new block which looks roughly like this:
// ```js
// // hoisted:
// const block_name = $.template(`...`);
//
// // for the main block:
// const id = block_name();
// // init stuff and possibly render effect
// $.append($$anchor, id);
// ```
// Adds the hoisted parts to `context.state.hoisted` and returns the statements of the main block.
const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
const { hoisted, trimmed } = clean_nodes(
parent,
node.nodes,
context.path,
namespace,
context.state,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
return b.block([]);
}
const is_single_element = trimmed.length === 1 && trimmed[0].type === 'RegularElement';
const is_single_child_not_needing_template =
trimmed.length === 1 &&
(trimmed[0].type === 'SvelteFragment' || trimmed[0].type === 'TitleElement');
const template_name = context.state.scope.root.unique('root'); // TODO infer name from parent
/** @type {import('estree').Statement[]} */
const body = [];
/** @type {import('estree').Statement | undefined} */
let close = undefined;
/** @type {import('../types').ComponentClientTransformState} */
const state = {
...context.state,
before_init: [],
init: [],
update: [],
after_update: [],
template: [],
locations: [],
metadata: {
context: {
template_needs_import_node: false,
template_contains_script_tag: false
},
namespace,
bound_contenteditable: context.state.metadata.bound_contenteditable
}
};
for (const node of hoisted) {
context.visit(node, state);
}
/**
* @param {import('estree').Identifier} template_name
* @param {import('estree').Expression[]} args
*/
const add_template = (template_name, args) => {
let call = b.call(get_template_function(namespace, state), ...args);
if (context.state.options.dev) {
call = b.call(
'$.add_locations',
call,
b.member(b.id(context.state.analysis.name), b.id('filename')),
serialize_locations(state.locations)
);
}
context.state.hoisted.push(b.var(template_name, call));
};
if (is_single_element) {
const element = /** @type {import('#compiler').RegularElement} */ (trimmed[0]);
const id = b.id(context.state.scope.generate(element.name));
context.visit(element, {
...state,
node: id
});
/** @type {import('estree').Expression[]} */
const args = [b.template([b.quasi(state.template.join(''), true)], [])];
if (state.metadata.context.template_needs_import_node) {
args.push(b.literal(TEMPLATE_USE_IMPORT_NODE));
}
add_template(template_name, args);
body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else if (is_single_child_not_needing_template) {
context.visit(trimmed[0], state);
body.push(...state.before_init, ...state.init);
} else if (trimmed.length > 0) {
const id = b.id(context.state.scope.generate('fragment'));
const use_space_template =
trimmed.some((node) => node.type === 'ExpressionTag') &&
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
if (use_space_template) {
// special case — we can use `$.text` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
...context,
state
});
body.push(
b.var(id, b.call('$.text', b.id('$$anchor'))),
...state.before_init,
...state.init
);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
const expression = (is_text) =>
is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id);
process_children(trimmed, expression, false, { ...context, state });
const use_comment_template = state.template.length === 1 && state.template[0] === '<!>';
if (use_comment_template) {
// special case — we can use `$.comment` instead of creating a unique template
body.push(b.var(id, b.call('$.comment')));
} else {
let flags = TEMPLATE_FRAGMENT;
if (state.metadata.context.template_needs_import_node) {
flags |= TEMPLATE_USE_IMPORT_NODE;
}
add_template(template_name, [
b.template([b.quasi(state.template.join(''), true)], []),
b.literal(flags)
]);
body.push(b.var(id, b.call(template_name)));
}
body.push(...state.before_init, ...state.init);
close = b.stmt(b.call('$.append', b.id('$$anchor'), id));
}
} else {
body.push(...state.before_init, ...state.init);
}
if (state.update.length > 0) {
body.push(serialize_render_stmt(state));
}
body.push(...state.after_update);
if (close !== undefined) {
// It's important that close is the last statement in the block, as any previous statements
// could contain element insertions into the template, which the close statement needs to
// know of when constructing the list of current inner elements.
body.push(close);
}
return b.block(body); return b.block(body);
}, },
Comment(node, context) { Comment(node, context) {
@ -2116,6 +2142,7 @@ export const template_visitors = {
node.fragment.nodes, node.fragment.nodes,
context.path, context.path,
child_metadata.namespace, child_metadata.namespace,
state,
node.name === 'script' || state.preserve_whitespace, node.name === 'script' || state.preserve_whitespace,
state.options.preserveComments state.options.preserveComments
); );
@ -2229,16 +2256,15 @@ export const template_visitors = {
} }
inner.push(...inner_context.state.after_update); inner.push(...inner_context.state.after_update);
inner.push( inner.push(
...create_block(node, node.fragment, 'dynamic_element', node.fragment.nodes, { .../** @type {import('estree').BlockStatement} */ (
...context, context.visit(node.fragment, {
state: {
...context.state, ...context.state,
metadata: { metadata: {
...context.state.metadata, ...context.state.metadata,
namespace: determine_namespace_for_children(node, context.state.metadata.namespace) namespace: determine_namespace_for_children(node, context.state.metadata.namespace)
} }
}
}) })
).body
); );
const location = context.state.options.dev && locator(node.start); const location = context.state.options.dev && locator(node.start);
@ -2456,8 +2482,7 @@ export const template_visitors = {
} }
} }
// TODO should use context.visit? const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.body));
const children = create_block(node, node.body, 'each_block', node.body.nodes, context);
const key_function = node.key const key_function = node.key
? b.arrow( ? b.arrow(
@ -2490,7 +2515,7 @@ export const template_visitors = {
b.literal(each_type), b.literal(each_type),
each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection),
key_function, key_function,
b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))) b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(block.body)))
]; ];
if (node.fallback) { if (node.fallback) {
@ -2743,10 +2768,9 @@ export const template_visitors = {
'$.bind_property', '$.bind_property',
b.literal(node.name), b.literal(node.name),
b.literal(property.event), b.literal(property.event),
b.literal(property.type ?? 'get'),
state.node, state.node,
getter, setter,
setter property.bidirectional && getter
); );
} else { } else {
// special cases // special cases
@ -2773,6 +2797,11 @@ export const template_visitors = {
call_expr = b.call('$.bind_window_size', b.literal(node.name), setter); call_expr = b.call('$.bind_window_size', b.literal(node.name), setter);
break; break;
// document
case 'activeElement':
call_expr = b.call('$.bind_active_element', setter);
break;
// media // media
case 'muted': case 'muted':
call_expr = b.call(`$.bind_muted`, state.node, getter, setter); call_expr = b.call(`$.bind_muted`, state.node, getter, setter);
@ -2930,7 +2959,6 @@ export const template_visitors = {
b.stmt( b.stmt(
b.call( b.call(
'$.component', '$.component',
context.state.node,
// TODO use untrack here to not update when binding changes? // TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this // Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk( b.thunk(
@ -2954,19 +2982,8 @@ export const template_visitors = {
context.state.template.push('<!>'); context.state.template.push('<!>');
let component = serialize_inline_component(node, '$$component', context); let component = serialize_inline_component(node, '$$component', context);
if (context.state.options.dev) {
component = b.stmt(b.call('$.validate_dynamic_component', b.thunk(b.block([component])))); context.state.init.push(component);
}
context.state.init.push(
b.stmt(
b.call(
'$.component',
context.state.node,
b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.expression))),
b.arrow([b.id('$$component')], b.block([component]))
)
)
);
}, },
Attribute(node, context) { Attribute(node, context) {
if (is_event_attribute(node)) { if (is_event_attribute(node)) {
@ -3027,13 +3044,7 @@ export const template_visitors = {
context.state.init.push(...lets); context.state.init.push(...lets);
context.state.init.push( context.state.init.push(
...create_block( .../** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)).body
node,
node.fragment,
'slot_template',
/** @type {import('#compiler').SvelteNode[]} */ (node.fragment.nodes),
context
)
); );
}, },
SlotElement(node, context) { SlotElement(node, context) {
@ -3083,12 +3094,13 @@ export const template_visitors = {
spreads.length === 0 spreads.length === 0
? b.object(props) ? b.object(props)
: b.call('$.spread_props', b.object(props), ...spreads); : b.call('$.spread_props', b.object(props), ...spreads);
const fallback = const fallback =
node.fragment.nodes.length === 0 node.fragment.nodes.length === 0
? b.literal(null) ? b.literal(null)
: b.arrow( : b.arrow(
[b.id('$$anchor')], [b.id('$$anchor')],
b.block(create_block(node, node.fragment, 'fallback', node.fragment.nodes, context)) /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment))
); );
const expression = is_default const expression = is_default
@ -3106,7 +3118,7 @@ export const template_visitors = {
'$.head', '$.head',
b.arrow( b.arrow(
[b.id('$$anchor')], [b.id('$$anchor')],
b.block(create_block(node, node.fragment, 'head', node.fragment.nodes, context)) /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment))
) )
) )
) )

@ -240,63 +240,6 @@ function process_children(nodes, parent, { visit, state }) {
} }
} }
/**
* @param {import('#compiler').SvelteNode} parent
* @param {import('#compiler').Fragment} fragment
* @param {import('#compiler').SvelteNode[]} nodes
* @param {import('./types').ComponentContext} context
* @param {import('./types').Anchor} [anchor]
* @returns {import('estree').Statement[]}
*/
function create_block(parent, fragment, nodes, context, anchor) {
const namespace = infer_namespace(context.state.metadata.namespace, parent, nodes);
const { hoisted, trimmed } = clean_nodes(
parent,
nodes,
context.path,
namespace,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0 && !anchor) {
return [];
}
/** @type {import('./types').ComponentServerTransformState} */
const state = {
...context.state,
scope: context.state.scopes.get(fragment) ?? context.state.scope,
init: [],
template: [],
metadata: {
namespace
}
};
for (const node of hoisted) {
context.visit(node, state);
}
process_children(anchor ? [anchor, ...trimmed, anchor] : trimmed, parent, {
...context,
state
});
/** @type {import('estree').Statement[]} */
const body = [];
if (state.template.length > 0) {
body.push(...state.init);
body.push(...serialize_template(state.template));
} else {
body.push(...state.init);
}
return body;
}
/** /**
* @param {import('estree').VariableDeclarator} declarator * @param {import('estree').VariableDeclarator} declarator
* @param {import('../../scope').Scope} scope * @param {import('../../scope').Scope} scope
@ -1090,12 +1033,30 @@ function serialize_inline_component(node, component_name, context) {
const serialized_slots = []; const serialized_slots = [];
for (const slot_name of Object.keys(children)) { for (const slot_name of Object.keys(children)) {
const body = create_block(node, node.fragment, children[slot_name], context); const block = /** @type {import('estree').BlockStatement} */ (
if (body.length === 0) continue; context.visit(
{
...node.fragment,
// @ts-expect-error
nodes: children[slot_name]
},
{
...context.state,
scope:
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
context.state.scope
}
)
);
if (block.body.length === 0) continue;
const slot_fn = b.arrow( const slot_fn = b.arrow(
[b.id('$$payload'), b.id('$$slotProps')], [b.id('$$payload'), b.id('$$slotProps')],
b.block([...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []), ...body]) b.block([
...(slot_name === 'default' && !slot_scope_applies_to_itself ? lets : []),
...block.body
])
); );
if (slot_name === 'default' && !has_children_prop) { if (slot_name === 'default' && !has_children_prop) {
@ -1273,7 +1234,46 @@ const javascript_visitors_legacy = {
/** @type {import('./types').ComponentVisitors} */ /** @type {import('./types').ComponentVisitors} */
const template_visitors = { const template_visitors = {
Fragment(node, context) { Fragment(node, context) {
const body = create_block(context.path.at(-1) ?? node, node, node.nodes, context); const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes);
const { hoisted, trimmed } = clean_nodes(
parent,
node.nodes,
context.path,
namespace,
context.state,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
if (hoisted.length === 0 && trimmed.length === 0) {
return b.block([]);
}
/** @type {import('./types').ComponentServerTransformState} */
const state = {
...context.state,
init: [],
template: [],
metadata: {
namespace
}
};
for (const node of hoisted) {
context.visit(node, state);
}
process_children(trimmed, parent, { ...context, state });
/** @type {import('estree').Statement[]} */
const body = [...state.init];
if (state.template.length > 0) {
body.push(...serialize_template(state.template));
}
return b.block(body); return b.block(body);
}, },
HtmlTag(node, context) { HtmlTag(node, context) {
@ -1384,6 +1384,10 @@ const template_visitors = {
node.fragment.nodes, node.fragment.nodes,
inner_context.path, inner_context.path,
metadata.namespace, metadata.namespace,
{
...context.state,
scope: /** @type {import('../../scope').Scope} */ (context.state.scopes.get(node.fragment))
},
state.preserve_whitespace, state.preserve_whitespace,
state.options.preserveComments state.options.preserveComments
); );
@ -1475,10 +1479,12 @@ const template_visitors = {
} }
}; };
const main = create_block(node, node.fragment, node.fragment.nodes, { const main = /** @type {import('estree').BlockStatement} */ (
...context, context.visit(node.fragment, {
state: { ...context.state, metadata } ...context.state,
}); metadata
})
);
serialize_element_attributes(node, inner_context); serialize_element_attributes(node, inner_context);
@ -1504,7 +1510,7 @@ const template_visitors = {
...serialize_template(inner_context.state.template) ...serialize_template(inner_context.state.template)
]) ])
), ),
b.thunk(b.block(main)) b.thunk(main)
) )
) )
) )
@ -1543,11 +1549,7 @@ const template_visitors = {
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value)))); each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value))));
each.push( each.push(.../** @type {import('estree').BlockStatement} */ (context.visit(node.body)).body);
.../** @type {import('estree').Statement[]} */ (
create_block(node, node.body, children, context)
)
);
each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value)))); each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value))));
@ -1561,16 +1563,20 @@ const template_visitors = {
const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))); const close = b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)));
if (node.fallback) { if (node.fallback) {
const fallback = create_block(node, node.fallback, node.fallback.nodes, context); const fallback = /** @type {import('estree').BlockStatement} */ (
context.visit(node.fallback)
);
fallback.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))); fallback.body.push(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
);
state.template.push( state.template.push(
t_statement( t_statement(
b.if( b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)), b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
b.block([for_loop, close]), b.block([for_loop, close]),
b.block(fallback) fallback
) )
) )
); );
@ -1582,23 +1588,22 @@ const template_visitors = {
const state = context.state; const state = context.state;
state.template.push(block_open); state.template.push(block_open);
const consequent = create_block(node, node.consequent, node.consequent.nodes, context); const test = /** @type {import('estree').Expression} */ (context.visit(node.test));
const alternate = node.alternate
? create_block(node, node.alternate, node.alternate.nodes, context)
: [];
consequent.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE)))); const consequent = /** @type {import('estree').BlockStatement} */ (
alternate.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))); context.visit(node.consequent)
);
state.template.push( const alternate = node.alternate
t_statement( ? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate))
b.if( : b.block([]);
/** @type {import('estree').Expression} */ (context.visit(node.test)),
b.block(consequent), consequent.body.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE))));
b.block(alternate) alternate.body.push(
) b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_CLOSE_ELSE)))
)
); );
state.template.push(t_statement(b.if(test, consequent, alternate)));
}, },
AwaitBlock(node, context) { AwaitBlock(node, context) {
const state = context.state; const state = context.state;
@ -1641,8 +1646,8 @@ const template_visitors = {
KeyBlock(node, context) { KeyBlock(node, context) {
const state = context.state; const state = context.state;
state.template.push(block_open); state.template.push(block_open);
const body = create_block(node, node.fragment, node.fragment.nodes, context); const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
state.template.push(t_statement(b.block(body))); state.template.push(t_statement(block));
state.template.push(block_close); state.template.push(block_close);
}, },
SnippetBlock(node, context) { SnippetBlock(node, context) {
@ -1731,9 +1736,9 @@ const template_visitors = {
} }
} }
const body = create_block(node, node.fragment, node.fragment.nodes, context); const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(t_statement(b.block(body))); context.state.template.push(t_statement(block));
}, },
TitleElement(node, context) { TitleElement(node, context) {
const state = context.state; const state = context.state;
@ -1801,7 +1806,7 @@ const template_visitors = {
const fallback = const fallback =
node.fragment.nodes.length === 0 node.fragment.nodes.length === 0
? b.literal(null) ? b.literal(null)
: b.thunk(b.block(create_block(node, node.fragment, node.fragment.nodes, context))); : b.thunk(/** @type {import('estree').BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
state.template.push(t_statement(b.stmt(slot))); state.template.push(t_statement(b.stmt(slot)));
@ -1809,11 +1814,10 @@ const template_visitors = {
}, },
SvelteHead(node, context) { SvelteHead(node, context) {
const state = context.state; const state = context.state;
const body = create_block(node, node.fragment, node.fragment.nodes, context); const block = /** @type {import('estree').BlockStatement} */ (context.visit(node.fragment));
state.template.push( state.template.push(
t_statement( t_statement(b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block))))
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], b.block(body))))
)
); );
}, },
// @ts-ignore: need to extract this out somehow // @ts-ignore: need to extract this out somehow
@ -2170,23 +2174,9 @@ export function server_component(analysis, options) {
scopes: analysis.template.scopes, scopes: analysis.template.scopes,
hoisted: [b.import_all('$', 'svelte/internal/server')], hoisted: [b.import_all('$', 'svelte/internal/server')],
legacy_reactive_statements: new Map(), legacy_reactive_statements: new Map(),
// these should be set by create_block - if they're called outside, it's a bug // these are set inside the `Fragment` visitor, and cannot be used until then
get init() { init: /** @type {any} */ (null),
/** @type {any[]} */ template: /** @type {any} */ (null),
const a = [];
a.push = () => {
throw new Error('init.push should not be called outside create_block');
};
return a;
},
get template() {
/** @type {any[]} */
const a = [];
a.push = () => {
throw new Error('template.push should not be called outside create_block');
};
return a;
},
metadata: { metadata: {
namespace: options.namespace namespace: options.namespace
}, },

@ -4,7 +4,12 @@ import {
regex_starts_with_whitespaces regex_starts_with_whitespaces
} from '../patterns.js'; } from '../patterns.js';
import * as b from '../../utils/builders.js'; import * as b from '../../utils/builders.js';
import * as e from '../../errors.js';
import { walk } from 'zimmerframe'; import { walk } from 'zimmerframe';
import { extract_identifiers } from '../../utils/ast.js';
import check_graph_for_cycles from '../2-analyze/utils/check_graph_for_cycles.js';
import is_reference from 'is-reference';
import { set_scope } from '../scope.js';
/** /**
* @param {import('estree').Node} node * @param {import('estree').Node} node
@ -21,6 +26,102 @@ export function is_hoistable_function(node) {
return false; return false;
} }
/**
* Match Svelte 4 behaviour by sorting ConstTag nodes in topological order
* @param {import("#compiler").SvelteNode[]} nodes
* @param {import('./types.js').TransformState} state
*/
function sort_const_tags(nodes, state) {
/**
* @typedef {{
* node: import('#compiler').ConstTag;
* deps: Set<import('#compiler').Binding>;
* }} Tag
*/
const other = [];
/** @type {Map<import('#compiler').Binding, Tag>} */
const tags = new Map();
const { _ } = set_scope(state.scopes);
for (const node of nodes) {
if (node.type === 'ConstTag') {
const declaration = node.declaration.declarations[0];
const bindings = extract_identifiers(declaration.id).map((id) => {
return /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
});
/** @type {Set<import('#compiler').Binding>} */
const deps = new Set();
walk(declaration.init, state, {
_,
Identifier(node, context) {
const parent = /** @type {import('estree').Expression} */ (context.path.at(-1));
if (is_reference(node, parent)) {
const binding = context.state.scope.get(node.name);
if (binding) deps.add(binding);
}
}
});
for (const binding of bindings) {
tags.set(binding, { node, deps });
}
} else {
other.push(node);
}
}
if (tags.size === 0) {
return nodes;
}
/** @type {Array<[import('#compiler').Binding, import('#compiler').Binding]>} */
const edges = [];
for (const [id, tag] of tags) {
for (const dep of tag.deps) {
if (tags.has(dep)) {
edges.push([id, dep]);
}
}
}
const cycle = check_graph_for_cycles(edges);
if (cycle?.length) {
const tag = /** @type {Tag} */ (tags.get(cycle[0]));
e.const_tag_cycle(tag.node, cycle.map((binding) => binding.node.name).join(' → '));
}
/** @type {import('#compiler').ConstTag[]} */
const sorted = [];
/** @param {Tag} tag */
function add(tag) {
if (sorted.includes(tag.node)) {
return;
}
for (const dep of tag.deps) {
const dep_tag = tags.get(dep);
if (dep_tag) add(dep_tag);
}
sorted.push(tag.node);
}
for (const tag of tags.values()) {
add(tag);
}
return [...sorted, ...other];
}
/** /**
* Extract nodes that are hoisted and trim whitespace according to the following rules: * Extract nodes that are hoisted and trim whitespace according to the following rules:
* - trim leading and trailing whitespace, regardless of surroundings * - trim leading and trailing whitespace, regardless of surroundings
@ -32,6 +133,7 @@ export function is_hoistable_function(node) {
* @param {import('#compiler').SvelteNode[]} nodes * @param {import('#compiler').SvelteNode[]} nodes
* @param {import('#compiler').SvelteNode[]} path * @param {import('#compiler').SvelteNode[]} path
* @param {import('#compiler').Namespace} namespace * @param {import('#compiler').Namespace} namespace
* @param {import('./types.js').TransformState} state
* @param {boolean} preserve_whitespace * @param {boolean} preserve_whitespace
* @param {boolean} preserve_comments * @param {boolean} preserve_comments
*/ */
@ -40,9 +142,17 @@ export function clean_nodes(
nodes, nodes,
path, path,
namespace = 'html', namespace = 'html',
state,
// TODO give these defaults (state.options.preserveWhitespace and state.options.preserveComments).
// first, we need to make `Component(Client|Server)TransformState` inherit from a new `ComponentTransformState`
// rather than from `ClientTransformState` and `ServerTransformState`
preserve_whitespace, preserve_whitespace,
preserve_comments preserve_comments
) { ) {
if (!state.analysis.runes) {
nodes = sort_const_tags(nodes, state);
}
/** @type {import('#compiler').SvelteNode[]} */ /** @type {import('#compiler').SvelteNode[]} */
const hoisted = []; const hoisted = [];
@ -176,7 +286,8 @@ export function infer_namespace(namespace, parent, nodes) {
parent.type === 'Component' || parent.type === 'Component' ||
parent.type === 'SvelteComponent' || parent.type === 'SvelteComponent' ||
parent.type === 'SvelteFragment' || parent.type === 'SvelteFragment' ||
parent.type === 'SnippetBlock' parent.type === 'SnippetBlock' ||
parent.type === 'SlotElement'
) { ) {
const new_namespace = check_nodes_for_namespace(nodes, 'keep'); const new_namespace = check_nodes_for_namespace(nodes, 'keep');
if (new_namespace !== 'keep' && new_namespace !== 'maybe_html') { if (new_namespace !== 'keep' && new_namespace !== 'maybe_html') {

@ -2,7 +2,7 @@
* @typedef BindingProperty * @typedef BindingProperty
* @property {string} [event] This is set if the binding corresponds to the property name on the dom element it's bound to * @property {string} [event] This is set if the binding corresponds to the property name on the dom element it's bound to
* and there's an event that notifies of a change to that property * and there's an event that notifies of a change to that property
* @property {string} [type] Set this to `set` if updates are written to the dom property * @property {boolean} [bidirectional] Set this to `true` if updates are written to the dom property
* @property {boolean} [omit_in_ssr] Set this to true if the binding should not be included in SSR * @property {boolean} [omit_in_ssr] Set this to true if the binding should not be included in SSR
* @property {string[]} [valid_elements] If this is set, the binding is only valid on the given elements * @property {string[]} [valid_elements] If this is set, the binding is only valid on the given elements
* @property {string[]} [invalid_elements] If this is set, the binding is invalid on the given elements * @property {string[]} [invalid_elements] If this is set, the binding is invalid on the given elements
@ -86,11 +86,20 @@ export const binding_properties = {
omit_in_ssr: true omit_in_ssr: true
}, },
// document // document
activeElement: {
valid_elements: ['svelte:document'],
omit_in_ssr: true
},
fullscreenElement: { fullscreenElement: {
valid_elements: ['svelte:document'], valid_elements: ['svelte:document'],
event: 'fullscreenchange', event: 'fullscreenchange',
omit_in_ssr: true omit_in_ssr: true
}, },
pointerLockElement: {
valid_elements: ['svelte:document'],
event: 'pointerlockchange',
omit_in_ssr: true
},
visibilityState: { visibilityState: {
valid_elements: ['svelte:document'], valid_elements: ['svelte:document'],
event: 'visibilitychange', event: 'visibilitychange',
@ -166,7 +175,7 @@ export const binding_properties = {
// checkbox/radio // checkbox/radio
indeterminate: { indeterminate: {
event: 'change', event: 'change',
type: 'set', bidirectional: true,
valid_elements: ['input'], valid_elements: ['input'],
omit_in_ssr: true // no corresponding attribute omit_in_ssr: true // no corresponding attribute
}, },
@ -191,7 +200,7 @@ export const binding_properties = {
}, },
open: { open: {
event: 'toggle', event: 'toggle',
type: 'set', bidirectional: true,
valid_elements: ['details'] valid_elements: ['details']
}, },
value: { value: {

@ -248,9 +248,9 @@ function stringify_tag_attributes(attributes) {
} }
const regex_style_tags = const regex_style_tags =
/<!--[^]*?-->|<style((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g; /<!--[^]*?-->|<style((?:\s+[^=>'"/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/style>)/g;
const regex_script_tags = const regex_script_tags =
/<!--[^]*?-->|<script((?:\s+[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g; /<!--[^]*?-->|<script((?:\s+[^=>'"/\s]+=(?:"[^"]*"|'[^']*'|[^>\s]+)|\s+[^=>'"/\s]+)*\s*)(?:\/>|>([\S\s]*?)<\/script>)/g;
/** /**
* Calculate the updates required to process all instances of the specified tag. * Calculate the updates required to process all instances of the specified tag.

@ -14,9 +14,20 @@ export let filename;
export let locator = getLocator('', { offsetLine: 1 }); export let locator = getLocator('', { offsetLine: 1 });
/** @type {Set<string>[]} */ /**
* The current stack of ignored warnings
* @type {Set<string>[]}
*/
export let ignore_stack = []; export let ignore_stack = [];
/**
* For each node the list of warnings that should be ignored for that node.
* Exists in addition to `ignore_stack` because not all warnings are emitted
* while the stack is being built.
* @type {Map<import("./types").SvelteNode | NodeLike, Set<string>[]>}
*/
export let ignore_map = new Map();
/** /**
* @param {string[]} ignores * @param {string[]} ignores
*/ */
@ -49,4 +60,5 @@ export function reset(source, options) {
locator = getLocator(source, { offsetLine: 1 }); locator = getLocator(source, { offsetLine: 1 });
warnings = []; warnings = [];
ignore_stack = []; ignore_stack = [];
ignore_map.clear();
} }

@ -12,7 +12,8 @@ const replacements = {
'invalid-html-attribute': 'attribute_invalid_property_name', 'invalid-html-attribute': 'attribute_invalid_property_name',
'a11y-structure': 'a11y_figcaption_parent', 'a11y-structure': 'a11y_figcaption_parent',
'illegal-attribute-character': 'attribute_illegal_colon', 'illegal-attribute-character': 'attribute_illegal_colon',
'invalid-rest-eachblock-binding': 'bind_invalid_each_rest' 'invalid-rest-eachblock-binding': 'bind_invalid_each_rest',
'unused-export-let': 'export_let_unused'
}; };
/** /**

@ -1,6 +1,12 @@
/* This file is generated by scripts/process-messages/index.js. Do not edit! */ /* This file is generated by scripts/process-messages/index.js. Do not edit! */
import { filename, locator, warnings, ignore_stack } from './state.js'; import {
filename,
locator,
warnings,
ignore_stack,
ignore_map
} from './state.js';
/** @typedef {{ start?: number, end?: number }} NodeLike */ /** @typedef {{ start?: number, end?: number }} NodeLike */
/** /**
@ -9,7 +15,13 @@ import { filename, locator, warnings, ignore_stack } from './state.js';
* @param {string} message * @param {string} message
*/ */
function w(node, code, message) { function w(node, code, message) {
if (ignore_stack.at(-1)?.has(code)) return; let stack = ignore_stack;
if (node) {
stack = ignore_map.get(node) ?? ignore_stack;
}
if (stack && stack.at(-1)?.has(code)) return;
warnings.push({ warnings.push({
code, code,

@ -0,0 +1 @@
export { on } from '../internal/client/dom/elements/events.js';

@ -129,7 +129,7 @@ export class SvelteComponent<
export interface Component< export interface Component<
Props extends Record<string, any> = {}, Props extends Record<string, any> = {},
Exports extends Record<string, any> = {}, Exports extends Record<string, any> = {},
Bindings extends keyof Props | '' = '' Bindings extends keyof Props | '' = string
> { > {
/** /**
* @param internal An internal object used by Svelte. Do not use or modify. * @param internal An internal object used by Svelte. Do not use or modify.

@ -18,4 +18,5 @@ export const EFFECT_TRANSPARENT = 1 << 15;
export const LEGACY_DERIVED_PROP = 1 << 16; export const LEGACY_DERIVED_PROP = 1 << 16;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const STATE_FROZEN_SYMBOL = Symbol('$state.frozen');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');

@ -1,6 +1,7 @@
import { block, branch, destroy_effect } from '../reactivity/effects.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { set_should_intro } from '../render.js'; import { set_should_intro } from '../render.js';
import { get } from '../runtime.js'; import { get } from '../runtime.js';
import { check_target } from './legacy.js';
/** /**
* @template {(anchor: Comment, props: any) => any} Component * @template {(anchor: Comment, props: any) => any} Component
@ -11,7 +12,7 @@ export function hmr(source) {
* @param {Comment} anchor * @param {Comment} anchor
* @param {any} props * @param {any} props
*/ */
return (anchor, props) => { return function (anchor, props) {
let instance = {}; let instance = {};
/** @type {import("#client").Effect} */ /** @type {import("#client").Effect} */
@ -31,7 +32,10 @@ export function hmr(source) {
// preserve getters/setters // preserve getters/setters
Object.defineProperties( Object.defineProperties(
instance, instance,
Object.getOwnPropertyDescriptors(component(anchor, props)) Object.getOwnPropertyDescriptors(
// @ts-expect-error
new.target ? new component(anchor, props) : component(anchor, props)
)
); );
set_should_intro(true); set_should_intro(true);
}); });

@ -62,16 +62,6 @@ export function inspect(get_value, inspector = console.log) {
*/ */
function deep_snapshot(value, visited = new Map()) { function deep_snapshot(value, visited = new Map()) {
if (typeof value === 'object' && value !== null && !visited.has(value)) { if (typeof value === 'object' && value !== null && !visited.has(value)) {
if (DEV) {
// When dealing with ReactiveMap or ReactiveSet, return normal versions
// so that console.log provides better output versions
if (value instanceof Map && value.constructor !== Map) {
return new Map(value);
}
if (value instanceof Set && value.constructor !== Set) {
return new Set(value);
}
}
const unstated = snapshot(value); const unstated = snapshot(value);
if (unstated !== value) { if (unstated !== value) {

@ -2,6 +2,13 @@ import * as e from '../errors.js';
import { current_component_context } from '../runtime.js'; import { current_component_context } from '../runtime.js';
import { get_component } from './ownership.js'; import { get_component } from './ownership.js';
/** @param {Function & { filename: string }} target */
export function check_target(target) {
if (target) {
e.component_api_invalid_new(target.filename ?? 'a component', target.name);
}
}
export function legacy_api() { export function legacy_api() {
const component = current_component_context?.function; const component = current_component_context?.function;

@ -57,9 +57,9 @@ export function index(_, i) {
* subsequent destruction. Used in each blocks * subsequent destruction. Used in each blocks
* @param {import('#client').EachItem[]} items * @param {import('#client').EachItem[]} items
* @param {null | Node} controlled_anchor * @param {null | Node} controlled_anchor
* @param {() => void} [callback] * @param {Map<any, import("#client").EachItem>} items_map
*/ */
function pause_effects(items, controlled_anchor, callback) { function pause_effects(items, controlled_anchor, items_map) {
/** @type {import('#client').TransitionManager[]} */ /** @type {import('#client').TransitionManager[]} */
var transitions = []; var transitions = [];
var length = items.length; var length = items.length;
@ -68,20 +68,29 @@ function pause_effects(items, controlled_anchor, callback) {
pause_children(items[i].e, transitions, true); pause_children(items[i].e, transitions, true);
} }
var is_controlled = length > 0 && transitions.length === 0 && controlled_anchor !== null;
// If we have a controlled anchor, it means that the each block is inside a single // If we have a controlled anchor, it means that the each block is inside a single
// DOM element, so we can apply a fast-path for clearing the contents of the element. // DOM element, so we can apply a fast-path for clearing the contents of the element.
if (length > 0 && transitions.length === 0 && controlled_anchor !== null) { if (is_controlled) {
var parent_node = /** @type {Element} */ (controlled_anchor.parentNode); var parent_node = /** @type {Element} */ (
/** @type {Element} */ (controlled_anchor).parentNode
);
clear_text_content(parent_node); clear_text_content(parent_node);
parent_node.append(controlled_anchor); parent_node.append(/** @type {Element} */ (controlled_anchor));
items_map.clear();
link(items[0].prev, items[length - 1].next);
} }
run_out_transitions(transitions, () => { run_out_transitions(transitions, () => {
for (var i = 0; i < length; i++) { for (var i = 0; i < length; i++) {
destroy_effect(items[i].e); var item = items[i];
if (!is_controlled) {
items_map.delete(item.k);
item.o.remove();
link(item.prev, item.next);
}
destroy_effect(item.e, !is_controlled);
} }
if (callback !== undefined) callback();
}); });
} }
@ -391,31 +400,27 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
const to_destroy = Array.from(seen); const to_destroy = Array.from(seen);
while (current) { while (current !== null) {
to_destroy.push(current); to_destroy.push(current);
current = current.next; current = current.next;
} }
var destroy_length = to_destroy.length;
if (destroy_length > 0) {
var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null; var controlled_anchor = (flags & EACH_IS_CONTROLLED) !== 0 && length === 0 ? anchor : null;
if (is_animated) { if (is_animated) {
for (i = 0; i < to_destroy.length; i += 1) { for (i = 0; i < destroy_length; i += 1) {
to_destroy[i].a?.measure(); to_destroy[i].a?.measure();
} }
for (i = 0; i < to_destroy.length; i += 1) { for (i = 0; i < destroy_length; i += 1) {
to_destroy[i].a?.fix(); to_destroy[i].a?.fix();
} }
} }
pause_effects(to_destroy, controlled_anchor, () => { pause_effects(to_destroy, controlled_anchor, items);
for (var i = 0; i < to_destroy.length; i += 1) {
var item = to_destroy[i];
items.delete(item.k);
item.o.remove();
link(item.prev, item.next);
} }
});
if (is_animated) { if (is_animated) {
effect(() => { effect(() => {

@ -5,12 +5,11 @@ import { block, branch, pause_effect } from '../../reactivity/effects.js';
/** /**
* @template P * @template P
* @template {(props: P) => void} C * @template {(props: P) => void} C
* @param {Comment} anchor
* @param {() => C} get_component * @param {() => C} get_component
* @param {(component: C) => import('#client').Dom | void} render_fn * @param {(component: C) => import('#client').Dom | void} render_fn
* @returns {void} * @returns {void}
*/ */
export function component(anchor, get_component, render_fn) { export function component(get_component, render_fn) {
/** @type {C} */ /** @type {C} */
let component; let component;

@ -143,12 +143,14 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio
anchor.before(element); anchor.before(element);
if (!hydrating) {
if (prev_element) { if (prev_element) {
swap_block_dom(parent_effect, prev_element, element); swap_block_dom(parent_effect, prev_element, element);
prev_element.remove(); prev_element.remove();
} else if (!hydrating) { } else {
push_template_node(element, parent_effect); push_template_node(element, parent_effect);
} }
}
}); });
} }

@ -144,6 +144,7 @@ export function set_custom_element_data(node, prop, value) {
*/ */
export function set_attributes(element, prev, next, lowercase_attributes, css_hash) { export function set_attributes(element, prev, next, lowercase_attributes, css_hash) {
var has_hash = css_hash.length !== 0; var has_hash = css_hash.length !== 0;
var current = prev || {};
for (var key in prev) { for (var key in prev) {
if (!(key in next)) { if (!(key in next)) {
@ -167,7 +168,10 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
for (const key in next) { for (const key in next) {
// let instead of var because referenced in a closure // let instead of var because referenced in a closure
let value = next[key]; let value = next[key];
if (value === prev?.[key]) continue; var prev_value = current[key];
if (value === prev_value) continue;
current[key] = value;
var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2) var prefix = key[0] + key[1]; // this is faster than key.slice(0, 2)
if (prefix === '$$') continue; if (prefix === '$$') continue;
@ -175,6 +179,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
if (prefix === 'on') { if (prefix === 'on') {
/** @type {{ capture?: true }} */ /** @type {{ capture?: true }} */
const opts = {}; const opts = {};
const event_handle_key = '$$' + key;
let event_name = key.slice(2); let event_name = key.slice(2);
var delegated = DelegatedEvents.includes(event_name); var delegated = DelegatedEvents.includes(event_name);
@ -183,21 +188,35 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
opts.capture = true; opts.capture = true;
} }
if (!delegated && prev?.[key]) { if (!delegated && prev_value) {
element.removeEventListener(event_name, /** @type {any} */ (prev[key]), opts); // Listening to same event but different handler -> our handle function below takes care of this
// If we were to remove and add listeners in this case, it could happen that the event is "swallowed"
// (the browser seems to not know yet that a new one exists now) and doesn't reach the handler
// https://github.com/sveltejs/svelte/issues/11903
if (value != null) continue;
element.removeEventListener(event_name, current[event_handle_key], opts);
current[event_handle_key] = null;
} }
if (value != null) { if (value != null) {
if (!delegated) { if (!delegated) {
// we use `addEventListener` here because these events are not delegated /**
* @this {any}
* @param {Event} evt
*/
function handle(evt) {
current[key].call(this, evt);
}
if (!prev) { if (!prev) {
events.push([ events.push([
key, key,
value, value,
() => (next[key] = create_event(event_name, element, value, opts)) () => (current[event_handle_key] = create_event(event_name, element, handle, opts))
]); ]);
} else { } else {
next[key] = create_event(event_name, element, value, opts); current[event_handle_key] = create_event(event_name, element, handle, opts);
} }
} else { } else {
// @ts-ignore // @ts-ignore
@ -252,7 +271,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
effect(() => { effect(() => {
if (!element.isConnected) return; if (!element.isConnected) return;
for (const [key, value, evt] of events) { for (const [key, value, evt] of events) {
if (next[key] === value) { if (current[key] === value) {
evt(); evt();
} }
} }
@ -261,7 +280,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha
}); });
} }
return next; return current;
} }
/** /**

@ -0,0 +1,17 @@
import { listen } from './shared.js';
/**
* @param {(activeElement: Element | null) => void} update
* @returns {void}
*/
export function bind_active_element(update) {
listen(document, ['focusin', 'focusout'], (event) => {
if (event && event.type === 'focusout' && /** @type {FocusEvent} */ (event).relatedTarget) {
// The tests still pass if we remove this, because of JSDOM limitations, but it is necessary
// to avoid temporarily resetting to `document.body`
return;
}
update(document.activeElement);
});
}

@ -4,7 +4,6 @@ import { stringify } from '../../../render.js';
import { listen_to_event_and_reset_event } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import * as e from '../../../errors.js'; import * as e from '../../../errors.js';
import { get_proxied_value, is } from '../../../proxy.js'; import { get_proxied_value, is } from '../../../proxy.js';
import { yield_event_updates } from '../../../runtime.js';
/** /**
* @param {HTMLInputElement} input * @param {HTMLInputElement} input
@ -19,9 +18,7 @@ export function bind_value(input, get_value, update) {
e.bind_invalid_checkbox_value(); e.bind_invalid_checkbox_value();
} }
yield_event_updates(() => update(is_numberlike_input(input) ? to_number(input.value) : input.value);
update(is_numberlike_input(input) ? to_number(input.value) : input.value)
);
}); });
render_effect(() => { render_effect(() => {
@ -87,10 +84,10 @@ export function bind_group(inputs, group_index, input, get_value, update) {
value = get_binding_group_value(binding_group, value, input.checked); value = get_binding_group_value(binding_group, value, input.checked);
} }
yield_event_updates(() => update(value)); update(value);
}, },
// TODO better default value handling // TODO better default value handling
() => yield_event_updates(() => update(is_checkbox ? [] : null)) () => update(is_checkbox ? [] : null)
); );
render_effect(() => { render_effect(() => {
@ -131,7 +128,7 @@ export function bind_group(inputs, group_index, input, get_value, update) {
export function bind_checked(input, get_value, update) { export function bind_checked(input, get_value, update) {
listen_to_event_and_reset_event(input, 'change', () => { listen_to_event_and_reset_event(input, 'change', () => {
var value = input.checked; var value = input.checked;
yield_event_updates(() => update(value)); update(value);
}); });
if (get_value() == undefined) { if (get_value() == undefined) {
@ -190,7 +187,7 @@ function to_number(value) {
*/ */
export function bind_files(input, get_value, update) { export function bind_files(input, get_value, update) {
listen_to_event_and_reset_event(input, 'change', () => { listen_to_event_and_reset_event(input, 'change', () => {
yield_event_updates(() => update(input.files)); update(input.files);
}); });
render_effect(() => { render_effect(() => {
input.files = get_value(); input.files = get_value();

@ -1,7 +1,6 @@
import { hydrating } from '../../hydration.js'; import { hydrating } from '../../hydration.js';
import { render_effect, effect } from '../../../reactivity/effects.js'; import { render_effect, effect } from '../../../reactivity/effects.js';
import { listen } from './shared.js'; import { listen } from './shared.js';
import { yield_event_updates } from '../../../runtime.js';
/** @param {TimeRanges} ranges */ /** @param {TimeRanges} ranges */
function time_ranges_to_array(ranges) { function time_ranges_to_array(ranges) {
@ -36,7 +35,7 @@ export function bind_current_time(media, get_value, update) {
} }
updating = true; updating = true;
yield_event_updates(() => update(media.currentTime)); update(media.currentTime);
}; };
raf_id = requestAnimationFrame(callback); raf_id = requestAnimationFrame(callback);
@ -61,9 +60,7 @@ export function bind_current_time(media, get_value, update) {
* @param {(array: Array<{ start: number; end: number }>) => void} update * @param {(array: Array<{ start: number; end: number }>) => void} update
*/ */
export function bind_buffered(media, update) { export function bind_buffered(media, update) {
listen(media, ['loadedmetadata', 'progress'], () => listen(media, ['loadedmetadata', 'progress'], () => update(time_ranges_to_array(media.buffered)));
yield_event_updates(() => update(time_ranges_to_array(media.buffered)))
);
} }
/** /**
@ -79,9 +76,7 @@ export function bind_seekable(media, update) {
* @param {(array: Array<{ start: number; end: number }>) => void} update * @param {(array: Array<{ start: number; end: number }>) => void} update
*/ */
export function bind_played(media, update) { export function bind_played(media, update) {
listen(media, ['timeupdate'], () => listen(media, ['timeupdate'], () => update(time_ranges_to_array(media.played)));
yield_event_updates(() => update(time_ranges_to_array(media.played)))
);
} }
/** /**
@ -89,7 +84,7 @@ export function bind_played(media, update) {
* @param {(seeking: boolean) => void} update * @param {(seeking: boolean) => void} update
*/ */
export function bind_seeking(media, update) { export function bind_seeking(media, update) {
listen(media, ['seeking', 'seeked'], () => yield_event_updates(() => update(media.seeking))); listen(media, ['seeking', 'seeked'], () => update(media.seeking));
} }
/** /**
@ -97,7 +92,7 @@ export function bind_seeking(media, update) {
* @param {(seeking: boolean) => void} update * @param {(seeking: boolean) => void} update
*/ */
export function bind_ended(media, update) { export function bind_ended(media, update) {
listen(media, ['timeupdate', 'ended'], () => yield_event_updates(() => update(media.ended))); listen(media, ['timeupdate', 'ended'], () => update(media.ended));
} }
/** /**
@ -108,7 +103,7 @@ export function bind_ready_state(media, update) {
listen( listen(
media, media,
['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'], ['loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough', 'playing', 'waiting', 'emptied'],
() => yield_event_updates(() => update(media.readyState)) () => update(media.readyState)
); );
} }
@ -132,7 +127,7 @@ export function bind_playback_rate(media, get_value, update) {
} }
listen(media, ['ratechange'], () => { listen(media, ['ratechange'], () => {
if (!updating) yield_event_updates(() => update(media.playbackRate)); if (!updating) update(media.playbackRate);
updating = false; updating = false;
}); });
}); });
@ -150,7 +145,7 @@ export function bind_paused(media, get_value, update) {
var callback = () => { var callback = () => {
if (paused !== media.paused) { if (paused !== media.paused) {
paused = media.paused; paused = media.paused;
yield_event_updates(() => update((paused = media.paused))); update((paused = media.paused));
} }
}; };
@ -175,7 +170,7 @@ export function bind_paused(media, get_value, update) {
media.pause(); media.pause();
} else { } else {
media.play().catch(() => { media.play().catch(() => {
yield_event_updates(() => update((paused = true))); update((paused = true));
}); });
} }
}; };
@ -239,7 +234,7 @@ export function bind_muted(media, get_value, update) {
var callback = () => { var callback = () => {
updating = true; updating = true;
yield_event_updates(() => update(media.muted)); update(media.muted);
}; };
if (get_value() == null) { if (get_value() == null) {

@ -1,4 +1,3 @@
import { yield_event_updates } from '../../../runtime.js';
import { listen } from './shared.js'; import { listen } from './shared.js';
/** /**
@ -7,6 +6,6 @@ import { listen } from './shared.js';
*/ */
export function bind_online(update) { export function bind_online(update) {
listen(window, ['online', 'offline'], () => { listen(window, ['online', 'offline'], () => {
yield_event_updates(() => update(navigator.onLine)); update(navigator.onLine);
}); });
} }

@ -1,6 +1,6 @@
import { effect } from '../../../reactivity/effects.js'; import { effect } from '../../../reactivity/effects.js';
import { listen_to_event_and_reset_event } from './shared.js'; import { listen_to_event_and_reset_event } from './shared.js';
import { untrack, yield_event_updates } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
import { is } from '../../../proxy.js'; import { is } from '../../../proxy.js';
/** /**
@ -90,7 +90,7 @@ export function bind_select_value(select, get_value, update) {
value = selected_option && get_option_value(selected_option); value = selected_option && get_option_value(selected_option);
} }
yield_event_updates(() => update(value)); update(value);
}); });
// Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated

@ -4,9 +4,9 @@ import { add_form_reset_listener } from '../misc.js';
/** /**
* Fires the handler once immediately (unless corresponding arg is set to `false`), * Fires the handler once immediately (unless corresponding arg is set to `false`),
* then listens to the given events until the render effect context is destroyed * then listens to the given events until the render effect context is destroyed
* @param {Element | Window} target * @param {EventTarget} target
* @param {Array<string>} events * @param {Array<string>} events
* @param {() => void} handler * @param {(event?: Event) => void} handler
* @param {any} call_handler_immediately * @param {any} call_handler_immediately
*/ */
export function listen(target, events, handler, call_handler_immediately = true) { export function listen(target, events, handler, call_handler_immediately = true) {

@ -1,5 +1,5 @@
import { effect, render_effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack, yield_event_updates } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
/** /**
* Resize observer singleton. * Resize observer singleton.
@ -88,10 +88,7 @@ export function bind_resize_observer(element, type, update) {
? resize_observer_border_box ? resize_observer_border_box
: resize_observer_device_pixel_content_box; : resize_observer_device_pixel_content_box;
var unsub = observer.observe( var unsub = observer.observe(element, /** @param {any} entry */ (entry) => update(entry[type]));
element,
/** @param {any} entry */ (entry) => yield_event_updates(() => update(entry[type]))
);
render_effect(() => unsub); render_effect(() => unsub);
} }
@ -104,7 +101,7 @@ export function bind_element_size(element, type, update) {
var unsub = resize_observer_border_box.observe(element, () => update(element[type])); var unsub = resize_observer_border_box.observe(element, () => update(element[type]));
effect(() => { effect(() => {
yield_event_updates(() => untrack(() => update(element[type]))); update(element[type]);
return unsub; return unsub;
}); });
} }

@ -1,6 +1,6 @@
import { STATE_SYMBOL } from '../../../constants.js'; import { STATE_SYMBOL } from '../../../constants.js';
import { effect, render_effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack, yield_event_updates } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
import { queue_micro_task } from '../../task.js'; import { queue_micro_task } from '../../task.js';
/** /**
@ -37,14 +37,12 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
untrack(() => { untrack(() => {
if (element_or_component !== get_value(...parts)) { if (element_or_component !== get_value(...parts)) {
yield_event_updates(() => {
update(element_or_component, ...parts); update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at // If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
// the previous position if it isn't already taken over by a different effect. // the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) { if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_parts); update(null, ...old_parts);
} }
});
} }
}); });
}); });

@ -33,41 +33,37 @@ export function bind_content_editable(property, element, get_value, update) {
/** /**
* @param {string} property * @param {string} property
* @param {string} event_name * @param {string} event_name
* @param {'get' | 'set'} type
* @param {Element} element * @param {Element} element
* @param {() => unknown} get_value * @param {(value: unknown) => void} set
* @param {(value: unknown) => void} update * @param {() => unknown} [get]
* @returns {void} * @returns {void}
*/ */
export function bind_property(property, event_name, type, element, get_value, update) { export function bind_property(property, event_name, element, set, get) {
var target_handler = () => { var handler = () => {
// @ts-ignore // @ts-ignore
update(element[property]); set(element[property]);
}; };
element.addEventListener(event_name, target_handler); element.addEventListener(event_name, handler);
if (type === 'set') { if (get) {
render_effect(() => { render_effect(() => {
// @ts-ignore // @ts-ignore
element[property] = get_value(); element[property] = get();
}); });
} else {
handler();
} }
if (type === 'get') {
// @ts-ignore
update(element[property]);
}
render_effect(() => {
// @ts-ignore // @ts-ignore
if (element === document.body || element === window || element === document) { if (element === document.body || element === window || element === document) {
render_effect(() => {
return () => { return () => {
element.removeEventListener(event_name, target_handler); element.removeEventListener(event_name, handler);
}; };
}
}); });
} }
}
/** /**
* @param {HTMLElement} element * @param {HTMLElement} element

@ -1,5 +1,4 @@
import { effect, render_effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { yield_event_updates } from '../../../runtime.js';
import { listen } from './shared.js'; import { listen } from './shared.js';
/** /**
@ -16,7 +15,7 @@ export function bind_window_scroll(type, get_value, update) {
clearTimeout(timeout); clearTimeout(timeout);
timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?) timeout = setTimeout(clear, 100); // TODO use scrollend event if supported (or when supported everywhere?)
yield_event_updates(() => update(window[is_scrolling_x ? 'scrollX' : 'scrollY'])); update(window[is_scrolling_x ? 'scrollX' : 'scrollY']);
}; };
addEventListener('scroll', target_handler, { addEventListener('scroll', target_handler, {
@ -64,5 +63,5 @@ export function bind_window_scroll(type, get_value, update) {
* @param {(size: number) => void} update * @param {(size: number) => void} update
*/ */
export function bind_window_size(type, update) { export function bind_window_size(type, update) {
listen(window, ['resize'], () => yield_event_updates(() => update(window[type]))); listen(window, ['resize'], () => update(window[type]));
} }

@ -1,6 +1,5 @@
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { all_registered_events, root_event_handles } from '../../render.js'; import { all_registered_events, root_event_handles } from '../../render.js';
import { yield_event_updates } from '../../runtime.js';
import { define_property, is_array } from '../../utils.js'; import { define_property, is_array } from '../../utils.js';
import { hydrating } from '../hydration.js'; import { hydrating } from '../hydration.js';
import { queue_micro_task } from '../task.js'; import { queue_micro_task } from '../task.js';
@ -48,7 +47,7 @@ export function create_event(event_name, dom, handler, options) {
handle_event_propagation(dom, event); handle_event_propagation(dom, event);
} }
if (!event.cancelBubble) { if (!event.cancelBubble) {
return yield_event_updates(() => handler.call(this, event)); return handler.call(this, event);
} }
} }
@ -67,6 +66,24 @@ export function create_event(event_name, dom, handler, options) {
return target_handler; return target_handler;
} }
/**
* Attaches an event handler to an element and returns a function that removes the handler. Using this
* rather than `addEventListener` will preserve the correct order relative to handlers added declaratively
* (with attributes like `onclick`), which use event delegation for performance reasons
*
* @param {Element} element
* @param {string} type
* @param {EventListener} handler
* @param {AddEventListenerOptions} [options]
*/
export function on(element, type, handler, options = {}) {
var target_handler = create_event(type, element, handler, options);
return () => {
element.removeEventListener(type, target_handler, options);
};
}
/** /**
* @param {string} event_name * @param {string} event_name
* @param {Element} dom * @param {Element} dom
@ -114,13 +131,6 @@ export function handle_event_propagation(handler_element, event) {
var path = event.composedPath?.() || []; var path = event.composedPath?.() || [];
var current_target = /** @type {null | Element} */ (path[0] || event.target); var current_target = /** @type {null | Element} */ (path[0] || event.target);
if (event.target !== current_target) {
define_property(event, 'target', {
configurable: true,
value: current_target
});
}
// composedPath contains list of nodes the event has propagated through. // composedPath contains list of nodes the event has propagated through.
// We check __root to skip all nodes below it in case this is a // We check __root to skip all nodes below it in case this is a
// parent of the __root node, which indicates that there's nested // parent of the __root node, which indicates that there's nested
@ -173,38 +183,59 @@ export function handle_event_propagation(handler_element, event) {
} }
}); });
/** @param {Element} next_target */ try {
function next(next_target) { /**
current_target = next_target; * @type {unknown}
*/
var throw_error;
/**
* @type {unknown[]}
*/
var other_errors = [];
while (current_target !== null) {
/** @type {null | Element} */ /** @type {null | Element} */
var parent_element = next_target.parentNode || /** @type {any} */ (next_target).host || null; var parent_element =
current_target.parentNode || /** @type {any} */ (current_target).host || null;
try { try {
// @ts-expect-error // @ts-expect-error
var delegated = next_target['__' + event_name]; var delegated = current_target['__' + event_name];
if (delegated !== undefined && !(/** @type {any} */ (next_target).disabled)) { if (delegated !== undefined && !(/** @type {any} */ (current_target).disabled)) {
if (is_array(delegated)) { if (is_array(delegated)) {
var [fn, ...data] = delegated; var [fn, ...data] = delegated;
fn.apply(next_target, [event, ...data]); fn.apply(current_target, [event, ...data]);
} else { } else {
delegated.call(next_target, event); delegated.call(current_target, event);
}
}
} catch (error) {
if (throw_error) {
other_errors.push(error);
} else {
throw_error = error;
} }
} }
} finally {
if ( if (
!event.cancelBubble && event.cancelBubble ||
parent_element !== handler_element && parent_element === handler_element ||
parent_element !== null && parent_element === null ||
next_target !== handler_element current_target === handler_element
) { ) {
next(parent_element); break;
}
} }
current_target = parent_element;
} }
try { if (throw_error) {
yield_event_updates(() => next(/** @type {Element} */ (current_target))); for (let error of other_errors) {
// Throw the rest of the errors, one-by-one on a microtask
queueMicrotask(() => {
throw error;
});
}
throw throw_error;
}
} finally { } finally {
// @ts-expect-error is used above // @ts-expect-error is used above
event.__root = handler_element; event.__root = handler_element;

@ -10,14 +10,14 @@ let is_micro_task_queued = false;
let is_idle_task_queued = false; let is_idle_task_queued = false;
/** @type {Array<() => void>} */ /** @type {Array<() => void>} */
let current_queued_miro_tasks = []; let current_queued_micro_tasks = [];
/** @type {Array<() => void>} */ /** @type {Array<() => void>} */
let current_queued_idle_tasks = []; let current_queued_idle_tasks = [];
function process_micro_tasks() { function process_micro_tasks() {
is_micro_task_queued = false; is_micro_task_queued = false;
const tasks = current_queued_miro_tasks.slice(); const tasks = current_queued_micro_tasks.slice();
current_queued_miro_tasks = []; current_queued_micro_tasks = [];
run_all(tasks); run_all(tasks);
} }
@ -36,7 +36,7 @@ export function queue_micro_task(fn) {
is_micro_task_queued = true; is_micro_task_queued = true;
queueMicrotask(process_micro_tasks); queueMicrotask(process_micro_tasks);
} }
current_queued_miro_tasks.push(fn); current_queued_micro_tasks.push(fn);
} }
/** /**

@ -75,6 +75,24 @@ export function component_api_changed(parent, method, component) {
} }
} }
/**
* Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `legacy.componentApi` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
* @param {string} component
* @param {string} name
* @returns {never}
*/
export function component_api_invalid_new(component, name) {
if (DEV) {
const error = new Error(`${"component_api_invalid_new"}\n${`Attempted to instantiate ${component} with \`new ${name}\`, which is no longer valid in Svelte 5. If this component is not under your control, set the \`legacy.componentApi\` compiler option to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information`}`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("component_api_invalid_new");
}
}
/** /**
* Keyed each block has duplicate key `%value%` at indexes %a% and %b% * Keyed each block has duplicate key `%value%` at indexes %a% and %b%
* @param {string} a * @param {string} a

@ -7,7 +7,7 @@ export {
mark_module_end, mark_module_end,
add_owner_effect add_owner_effect
} from './dev/ownership.js'; } from './dev/ownership.js';
export { legacy_api } from './dev/legacy.js'; export { check_target, legacy_api } from './dev/legacy.js';
export { inspect } from './dev/inspect.js'; export { inspect } from './dev/inspect.js';
export { await_block as await } from './dom/blocks/await.js'; export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js'; export { if_block as if } from './dom/blocks/if.js';
@ -36,6 +36,7 @@ export { event, delegate, replay_events } from './dom/elements/events.js';
export { autofocus, remove_textarea_child } from './dom/elements/misc.js'; export { autofocus, remove_textarea_child } from './dom/elements/misc.js';
export { set_style } from './dom/elements/style.js'; export { set_style } from './dom/elements/style.js';
export { animation, transition } from './dom/elements/transitions.js'; export { animation, transition } from './dom/elements/transitions.js';
export { bind_active_element } from './dom/elements/bindings/document.js';
export { bind_checked, bind_files, bind_group, bind_value } from './dom/elements/bindings/input.js'; export { bind_checked, bind_files, bind_group, bind_value } from './dom/elements/bindings/input.js';
export { export {
bind_buffered, bind_buffered,

@ -18,7 +18,7 @@ import {
} from './utils.js'; } from './utils.js';
import { check_ownership, widen_ownership } from './dev/ownership.js'; import { check_ownership, widen_ownership } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js'; import { mutable_source, source, set } from './reactivity/sources.js';
import { STATE_SYMBOL } from './constants.js'; import { STATE_FROZEN_SYMBOL, STATE_SYMBOL } from './constants.js';
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import * as e from './errors.js'; import * as e from './errors.js';
@ -31,7 +31,12 @@ import * as e from './errors.js';
* @returns {import('#client').ProxyStateObject<T> | T} * @returns {import('#client').ProxyStateObject<T> | T}
*/ */
export function proxy(value, immutable = true, parent = null, prev) { export function proxy(value, immutable = true, parent = null, prev) {
if (typeof value === 'object' && value != null && !is_frozen(value)) { if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
!(STATE_FROZEN_SYMBOL in value)
) {
// If we have an existing proxy, return it... // If we have an existing proxy, return it...
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
const metadata = /** @type {import('#client').ProxyMetadata<T>} */ (value[STATE_SYMBOL]); const metadata = /** @type {import('#client').ProxyMetadata<T>} */ (value[STATE_SYMBOL]);

@ -311,16 +311,17 @@ export function execute_effect_teardown(effect) {
/** /**
* @param {import('#client').Effect} effect * @param {import('#client').Effect} effect
* @param {boolean} [remove_dom]
* @returns {void} * @returns {void}
*/ */
export function destroy_effect(effect) { export function destroy_effect(effect, remove_dom = true) {
var dom = effect.dom; var dom = effect.dom;
if (dom !== null) { if (dom !== null && remove_dom) {
remove(dom); remove(dom);
} }
destroy_effect_children(effect); destroy_effect_children(effect, remove_dom);
remove_reactions(effect, 0); remove_reactions(effect, 0);
set_signal_status(effect, DESTROYED); set_signal_status(effect, DESTROYED);

@ -1,5 +1,11 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { get_descriptors, get_prototype_of, is_frozen, object_freeze } from './utils.js'; import {
define_property,
get_descriptors,
get_prototype_of,
is_frozen,
object_freeze
} from './utils.js';
import { snapshot } from './proxy.js'; import { snapshot } from './proxy.js';
import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js'; import { destroy_effect, effect, execute_effect_teardown } from './reactivity/effects.js';
import { import {
@ -17,7 +23,8 @@ import {
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP, LEGACY_DERIVED_PROP,
DISCONNECTED DISCONNECTED,
STATE_FROZEN_SYMBOL
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -29,7 +36,6 @@ import { lifecycle_outside_component } from '../shared/errors.js';
const FLUSH_MICROTASK = 0; const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1; const FLUSH_SYNC = 1;
export const FLUSH_YIELD = 2;
// Used for DEV time error handling // Used for DEV time error handling
/** @param {WeakSet<Error>} value */ /** @param {WeakSet<Error>} value */
@ -38,7 +44,6 @@ const handled_errors = new WeakSet();
let current_scheduler_mode = FLUSH_MICROTASK; let current_scheduler_mode = FLUSH_MICROTASK;
// Used for handling scheduling // Used for handling scheduling
let is_micro_task_queued = false; let is_micro_task_queued = false;
let is_yield_task_queued = false;
export let is_flushing_effect = false; export let is_flushing_effect = false;
export let is_destroying_effect = false; export let is_destroying_effect = false;
@ -306,7 +311,9 @@ function handle_error(error, effect, component_context) {
} }
const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t'; const indent = /Firefox/.test(navigator.userAgent) ? ' ' : '\t';
error.message += `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`; define_property(error, 'message', {
value: error.message + `\n${component_stack.map((name) => `\n${indent}in ${name}`).join('')}\n`
});
const stack = error.stack; const stack = error.stack;
@ -321,7 +328,9 @@ function handle_error(error, effect, component_context) {
} }
new_lines.push(line); new_lines.push(line);
} }
error.stack = new_lines.join('\n'); define_property(error, 'stack', {
value: error.stack + new_lines.join('\n')
});
} }
handled_errors.add(error); handled_errors.add(error);
@ -478,16 +487,17 @@ export function remove_reactions(signal, start_index) {
/** /**
* @param {import('#client').Reaction} signal * @param {import('#client').Reaction} signal
* @param {boolean} [remove_dom]
* @returns {void} * @returns {void}
*/ */
export function destroy_effect_children(signal) { export function destroy_effect_children(signal, remove_dom = true) {
let effect = signal.first; let effect = signal.first;
signal.first = null; signal.first = null;
signal.last = null; signal.last = null;
var sibling; var sibling;
while (effect !== null) { while (effect !== null) {
sibling = effect.next; sibling = effect.next;
destroy_effect(effect); destroy_effect(effect, remove_dom);
effect = sibling; effect = sibling;
} }
} }
@ -599,29 +609,17 @@ function flush_queued_effects(effects) {
function process_deferred() { function process_deferred() {
is_micro_task_queued = false; is_micro_task_queued = false;
is_yield_task_queued = false;
if (flush_count > 1001) { if (flush_count > 1001) {
return; return;
} }
const previous_queued_root_effects = current_queued_root_effects; const previous_queued_root_effects = current_queued_root_effects;
current_queued_root_effects = []; current_queued_root_effects = [];
flush_queued_root_effects(previous_queued_root_effects); flush_queued_root_effects(previous_queued_root_effects);
if (!is_micro_task_queued && !is_yield_task_queued) { if (!is_micro_task_queued) {
flush_count = 0; flush_count = 0;
} }
} }
async function yield_tick() {
// TODO: replace this with scheduler.yield when it becomes standard
await new Promise((fulfil) => {
requestAnimationFrame(() => {
setTimeout(fulfil, 0);
});
// In case of being within background tab, the rAF won't fire
setTimeout(fulfil, 100);
});
}
/** /**
* @param {import('#client').Effect} signal * @param {import('#client').Effect} signal
* @returns {void} * @returns {void}
@ -632,11 +630,6 @@ export function schedule_effect(signal) {
is_micro_task_queued = true; is_micro_task_queued = true;
queueMicrotask(process_deferred); queueMicrotask(process_deferred);
} }
} else if (current_scheduler_mode === FLUSH_YIELD) {
if (!is_yield_task_queued) {
is_yield_task_queued = true;
yield_tick().then(process_deferred);
}
} }
var effect = signal; var effect = signal;
@ -735,19 +728,6 @@ function process_effects(effect, collected_effects) {
} }
} }
/**
* @param {{ (): void; (): any; }} fn
*/
export function yield_event_updates(fn) {
const previous_scheduler_mode = current_scheduler_mode;
try {
current_scheduler_mode = FLUSH_YIELD;
return fn();
} finally {
current_scheduler_mode = previous_scheduler_mode;
}
}
/** /**
* Internal version of `flushSync` with the option to not flush previous effects. * Internal version of `flushSync` with the option to not flush previous effects.
* Returns the result of the passed function, if given. * Returns the result of the passed function, if given.
@ -767,7 +747,6 @@ export function flush_sync(fn, flush_previous = true) {
current_scheduler_mode = FLUSH_SYNC; current_scheduler_mode = FLUSH_SYNC;
current_queued_root_effects = root_effects; current_queued_root_effects = root_effects;
is_yield_task_queued = false;
is_micro_task_queued = false; is_micro_task_queued = false;
if (flush_previous) { if (flush_previous) {
@ -795,7 +774,7 @@ export function flush_sync(fn, flush_previous = true) {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
export async function tick() { export async function tick() {
await yield_tick(); await Promise.resolve();
// By calling flush_sync we guarantee that any pending state changes are applied after one tick. // By calling flush_sync we guarantee that any pending state changes are applied after one tick.
// TODO look into whether we can make flushing subsequent updates synchronously in the future. // TODO look into whether we can make flushing subsequent updates synchronously in the future.
flush_sync(); flush_sync();
@ -1352,19 +1331,31 @@ if (DEV) {
} }
/** /**
* Expects a value that was wrapped with `freeze` and makes it frozen. * Expects a value that was wrapped with `freeze` and makes it frozen in DEV.
* @template T * @template T
* @param {T} value * @param {T} value
* @returns {Readonly<T>} * @returns {Readonly<T>}
*/ */
export function freeze(value) { export function freeze(value) {
if (typeof value === 'object' && value != null && !is_frozen(value)) { if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
!(STATE_FROZEN_SYMBOL in value)
) {
// If the object is already proxified, then snapshot the value // If the object is already proxified, then snapshot the value
if (STATE_SYMBOL in value) { if (STATE_SYMBOL in value) {
return object_freeze(snapshot(value)); value = snapshot(value);
} }
// Otherwise freeze the object define_property(value, STATE_FROZEN_SYMBOL, {
value: true,
writable: true,
enumerable: false
});
// Freeze the object in DEV
if (DEV) {
object_freeze(value); object_freeze(value);
} }
}
return value; return value;
} }

@ -13,13 +13,6 @@ import { current_component, pop, push } from './context.js';
import { BLOCK_ANCHOR, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { BLOCK_ANCHOR, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js';
import { validate_store } from '../shared/validate.js'; import { validate_store } from '../shared/validate.js';
/**
* @typedef {{
* head: string;
* html: string;
* }} RenderOutput
*/
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
// https://infra.spec.whatwg.org/#noncharacter // https://infra.spec.whatwg.org/#noncharacter
const INVALID_ATTR_NAME_CHAR_REGEX = const INVALID_ATTR_NAME_CHAR_REGEX =
@ -102,7 +95,7 @@ export let on_destroy = [];
/** /**
* @param {typeof import('svelte').SvelteComponent} component * @param {typeof import('svelte').SvelteComponent} component
* @param {{ props: Record<string, any>; context?: Map<any, any> }} options * @param {{ props: Record<string, any>; context?: Map<any, any> }} options
* @returns {RenderOutput} * @returns {import('#server').RenderOutput}
*/ */
export function render(component, options) { export function render(component, options) {
const payload = create_payload(); const payload = create_payload();
@ -129,7 +122,8 @@ export function render(component, options) {
return { return {
head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '', head: payload.head.out || payload.head.title ? payload.head.out + payload.head.title : '',
html: payload.out html: payload.out,
body: payload.out
}; };
} }

@ -20,3 +20,12 @@ export interface Payload {
anchor: number; anchor: number;
}; };
} }
export interface RenderOutput {
/** HTML that goes into the `<head>` */
head: string;
/** @deprecated use `body` instead */
html: string;
/** HTML that goes somewhere into the `<body>` */
body: string;
}

@ -27,7 +27,7 @@ export function asClassComponent(component) {
return { return {
css: { code: '', map: null }, css: { code: '', map: null },
head: result.head, head: result.head,
html: result.html html: result.body
}; };
}; };
// @ts-expect-error this is present for SSR // @ts-expect-error this is present for SSR

@ -1,8 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { UNINITIALIZED } from '../constants.js'; import { increment } from './utils.js';
import { map } from './utils.js';
/** /**
* @template K * @template K
@ -10,7 +9,7 @@ import { map } from './utils.js';
* @extends {Map<K, V>} * @extends {Map<K, V>}
*/ */
export class ReactiveMap extends Map { export class ReactiveMap extends Map {
/** @type {Map<K, import('#client').Source<V>>} */ /** @type {Map<K, import('#client').Source<number>>} */
#sources = new Map(); #sources = new Map();
#version = source(0); #version = source(0);
#size = source(0); #size = source(0);
@ -25,31 +24,30 @@ export class ReactiveMap extends Map {
if (DEV) new Map(value); if (DEV) new Map(value);
if (value) { if (value) {
var sources = this.#sources;
for (var [key, v] of value) { for (var [key, v] of value) {
sources.set(key, source(v)); super.set(key, v);
} }
this.#size.v = super.size;
this.#size.v = sources.size;
} }
} }
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {K} key */ /** @param {K} key */
has(key) { has(key) {
var s = this.#sources.get(key); var sources = this.#sources;
var s = sources.get(key);
if (s === undefined) { if (s === undefined) {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
sources.set(key, s);
} else {
// We should always track the version in case // We should always track the version in case
// the Set ever gets this value in the future. // the Set ever gets this value in the future.
get(this.#version); get(this.#version);
return false; return false;
} }
}
get(s); get(s);
return true; return true;
@ -60,25 +58,30 @@ export class ReactiveMap extends Map {
* @param {any} [this_arg] * @param {any} [this_arg]
*/ */
forEach(callbackfn, this_arg) { forEach(callbackfn, this_arg) {
get(this.#version); this.#read_all();
super.forEach(callbackfn, this_arg);
var bound_callbackfn = callbackfn.bind(this_arg);
this.#sources.forEach((s, key) => bound_callbackfn(s.v, key, this));
} }
/** @param {K} key */ /** @param {K} key */
get(key) { get(key) {
var s = this.#sources.get(key); var sources = this.#sources;
var s = sources.get(key);
if (s === undefined) { if (s === undefined) {
var ret = super.get(key);
if (ret !== undefined) {
s = source(0);
sources.set(key, s);
} else {
// We should always track the version in case // We should always track the version in case
// the Set ever gets this value in the future. // the Set ever gets this value in the future.
get(this.#version); get(this.#version);
return undefined; return undefined;
} }
}
return get(s); get(s);
return super.get(key);
} }
/** /**
@ -88,65 +91,80 @@ export class ReactiveMap extends Map {
set(key, value) { set(key, value) {
var sources = this.#sources; var sources = this.#sources;
var s = sources.get(key); var s = sources.get(key);
var prev_res = super.get(key);
var res = super.set(key, value);
if (s === undefined) { if (s === undefined) {
sources.set(key, source(value)); sources.set(key, source(0));
set(this.#size, sources.size); set(this.#size, super.size);
this.#increment_version(); increment(this.#version);
} else { } else if (prev_res !== value) {
set(s, value); increment(s);
} }
return this; return res;
} }
/** @param {K} key */ /** @param {K} key */
delete(key) { delete(key) {
var sources = this.#sources; var sources = this.#sources;
var s = sources.get(key); var s = sources.get(key);
var res = super.delete(key);
if (s !== undefined) { if (s !== undefined) {
var removed = sources.delete(key); sources.delete(key);
set(this.#size, sources.size); set(this.#size, super.size);
set(s, /** @type {V} */ (UNINITIALIZED)); set(s, -1);
this.#increment_version(); increment(this.#version);
return removed;
} }
return false; return res;
} }
clear() { clear() {
var sources = this.#sources; var sources = this.#sources;
if (sources.size !== 0) { if (super.size !== 0) {
set(this.#size, 0); set(this.#size, 0);
for (var s of sources.values()) { for (var s of sources.values()) {
set(s, /** @type {V} */ (UNINITIALIZED)); set(s, -1);
}
increment(this.#version);
sources.clear();
} }
this.#increment_version(); super.clear();
} }
sources.clear(); #read_all() {
get(this.#version);
var sources = this.#sources;
if (this.#size.v !== sources.size) {
for (var key of super.keys()) {
if (!sources.has(key)) {
sources.set(key, source(0));
}
}
}
for (var [, s] of this.#sources) {
get(s);
}
} }
keys() { keys() {
get(this.#version); get(this.#version);
return this.#sources.keys(); return super.keys();
} }
values() { values() {
get(this.#version); this.#read_all();
return map(this.#sources.values(), get, 'Map Iterator'); return super.values();
} }
entries() { entries() {
get(this.#version); this.#read_all();
return map( return super.entries();
this.#sources.entries(),
([key, source]) => /** @type {[K, V]} */ ([key, get(source)]),
'Map Iterator'
);
} }
[Symbol.iterator]() { [Symbol.iterator]() {
@ -154,6 +172,7 @@ export class ReactiveMap extends Map {
} }
get size() { get size() {
return get(this.#size); get(this.#size);
return super.size;
} }
} }

@ -36,7 +36,30 @@ test('map.values()', () => {
map.clear(); map.clear();
}); });
assert.deepEqual(log, [5, true, [1, 2, 3, 4, 5], 4, false, [1, 2, 4, 5], 0, false, []]); flushSync(() => {
map.set(3, 3);
});
flushSync(() => {
map.set(3, 4);
});
assert.deepEqual(log, [
5,
true,
[1, 2, 3, 4, 5],
4,
false,
[1, 2, 4, 5],
0,
false,
[],
1,
true,
[3],
true,
[4]
]);
cleanup(); cleanup();
}); });
@ -183,3 +206,35 @@ test('map handling of undefined values', () => {
cleanup(); cleanup();
}); });
test('not invoking reactivity when value is not in the map after changes', () => {
const map = new ReactiveMap([[1, 1]]);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push(map.get(1));
});
render_effect(() => {
log.push(map.get(2));
});
flushSync(() => {
map.delete(1);
});
flushSync(() => {
map.set(1, 1);
});
});
assert.deepEqual(log, [1, undefined, undefined, undefined, 1, undefined]);
cleanup();
});
test('Map.instanceOf', () => {
assert.equal(new ReactiveMap() instanceof Map, true);
});

@ -1,7 +1,7 @@
import { DEV } from 'esm-env'; import { DEV } from 'esm-env';
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { map } from './utils.js'; import { increment } from './utils.js';
var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf']; var read_methods = ['forEach', 'isDisjointFrom', 'isSubsetOf', 'isSupersetOf'];
var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union']; var set_like_methods = ['difference', 'intersection', 'symmetricDifference', 'union'];
@ -28,13 +28,10 @@ export class ReactiveSet extends Set {
if (DEV) new Set(value); if (DEV) new Set(value);
if (value) { if (value) {
var sources = this.#sources;
for (var element of value) { for (var element of value) {
sources.set(element, source(true)); super.add(element);
} }
this.#size.v = super.size;
this.#size.v = sources.size;
} }
if (!inited) this.#init(); if (!inited) this.#init();
@ -51,11 +48,8 @@ export class ReactiveSet extends Set {
// @ts-ignore // @ts-ignore
proto[method] = function (...v) { proto[method] = function (...v) {
get(this.#version); get(this.#version);
// We don't populate the underlying Set, so we need to create a clone using
// our internal values and then pass that to the method.
var clone = new Set(this.values());
// @ts-ignore // @ts-ignore
return set_proto[method].apply(clone, v); return set_proto[method].apply(this, v);
}; };
} }
@ -63,89 +57,95 @@ export class ReactiveSet extends Set {
// @ts-ignore // @ts-ignore
proto[method] = function (...v) { proto[method] = function (...v) {
get(this.#version); get(this.#version);
// We don't populate the underlying Set, so we need to create a clone using
// our internal values and then pass that to the method.
var clone = new Set(this.values());
// @ts-ignore // @ts-ignore
var set = /** @type {Set<T>} */ (set_proto[method].apply(clone, v)); var set = /** @type {Set<T>} */ (set_proto[method].apply(this, v));
return new ReactiveSet(set); return new ReactiveSet(set);
}; };
} }
} }
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** @param {T} value */ /** @param {T} value */
has(value) { has(value) {
var s = this.#sources.get(value); var sources = this.#sources;
var s = sources.get(value);
if (s === undefined) { if (s === undefined) {
var ret = super.has(value);
if (ret) {
s = source(true);
sources.set(value, s);
} else {
// We should always track the version in case // We should always track the version in case
// the Set ever gets this value in the future. // the Set ever gets this value in the future.
get(this.#version); get(this.#version);
return false; return false;
} }
}
return get(s); get(s);
return super.has(value);
} }
/** @param {T} value */ /** @param {T} value */
add(value) { add(value) {
var sources = this.#sources; var sources = this.#sources;
var res = super.add(value);
var s = sources.get(value);
if (!sources.has(value)) { if (s === undefined) {
sources.set(value, source(true)); sources.set(value, source(true));
set(this.#size, sources.size); set(this.#size, super.size);
this.#increment_version(); increment(this.#version);
} else {
set(s, true);
} }
return this; return res;
} }
/** @param {T} value */ /** @param {T} value */
delete(value) { delete(value) {
var sources = this.#sources; var sources = this.#sources;
var s = sources.get(value); var s = sources.get(value);
var res = super.delete(value);
if (s !== undefined) { if (s !== undefined) {
var removed = sources.delete(value); sources.delete(value);
set(this.#size, sources.size); set(this.#size, super.size);
set(s, false); set(s, false);
this.#increment_version(); increment(this.#version);
return removed;
} }
return false; return res;
} }
clear() { clear() {
var sources = this.#sources; var sources = this.#sources;
if (sources.size !== 0) { if (super.size !== 0) {
set(this.#size, 0); set(this.#size, 0);
for (var s of sources.values()) { for (var s of sources.values()) {
set(s, false); set(s, false);
} }
this.#increment_version(); increment(this.#version);
}
sources.clear(); sources.clear();
} }
super.clear();
}
keys() { keys() {
get(this.#version); get(this.#version);
return map(this.#sources.keys(), (key) => key, 'Set Iterator'); return super.keys();
} }
values() { values() {
return this.keys(); get(this.#version);
return super.values();
} }
entries() { entries() {
return map(this.keys(), (key) => /** @type {[T, T]} */ ([key, key]), 'Set Iterator'); get(this.#version);
return super.entries();
} }
[Symbol.iterator]() { [Symbol.iterator]() {

@ -106,3 +106,54 @@ test('set.forEach()', () => {
cleanup(); cleanup();
}); });
test('not invoking reactivity when value is not in the set after changes', () => {
const set = new ReactiveSet([1, 2]);
const log: any = [];
const cleanup = effect_root(() => {
render_effect(() => {
log.push('has 1', set.has(1));
});
render_effect(() => {
log.push('has 2', set.has(2));
});
render_effect(() => {
log.push('has 3', set.has(3));
});
});
flushSync(() => {
set.delete(2);
});
flushSync(() => {
set.add(2);
});
assert.deepEqual(log, [
'has 1',
true,
'has 2',
true,
'has 3',
false,
'has 2',
false,
'has 3',
false,
'has 2',
true,
'has 3',
false
]);
cleanup();
});
test('Set.instanceOf', () => {
assert.equal(new ReactiveSet() instanceof Set, true);
});

@ -1,5 +1,6 @@
import { source, set } from '../internal/client/reactivity/sources.js'; import { source, set } from '../internal/client/reactivity/sources.js';
import { get } from '../internal/client/runtime.js'; import { get } from '../internal/client/runtime.js';
import { increment } from './utils.js';
const REPLACE = Symbol(); const REPLACE = Symbol();
@ -155,10 +156,6 @@ export class ReactiveURL extends URL {
export class ReactiveURLSearchParams extends URLSearchParams { export class ReactiveURLSearchParams extends URLSearchParams {
#version = source(0); #version = source(0);
#increment_version() {
set(this.#version, this.#version.v + 1);
}
/** /**
* @param {URLSearchParams} params * @param {URLSearchParams} params
*/ */
@ -171,7 +168,7 @@ export class ReactiveURLSearchParams extends URLSearchParams {
super.append(key, value); super.append(key, value);
} }
this.#increment_version(); increment(this.#version);
} }
/** /**
@ -180,7 +177,7 @@ export class ReactiveURLSearchParams extends URLSearchParams {
* @returns {void} * @returns {void}
*/ */
append(name, value) { append(name, value) {
this.#increment_version(); increment(this.#version);
return super.append(name, value); return super.append(name, value);
} }
@ -190,7 +187,7 @@ export class ReactiveURLSearchParams extends URLSearchParams {
* @returns {void} * @returns {void}
*/ */
delete(name, value) { delete(name, value) {
this.#increment_version(); increment(this.#version);
return super.delete(name, value); return super.delete(name, value);
} }
@ -233,12 +230,12 @@ export class ReactiveURLSearchParams extends URLSearchParams {
* @returns {void} * @returns {void}
*/ */
set(name, value) { set(name, value) {
this.#increment_version(); increment(this.#version);
return super.set(name, value); return super.set(name, value);
} }
sort() { sort() {
this.#increment_version(); increment(this.#version);
return super.sort(); return super.sort();
} }

@ -1,3 +1,5 @@
import { set } from '../internal/client/reactivity/sources.js';
/** /**
* @template T * @template T
* @template U * @template U
@ -27,3 +29,8 @@ export function map(iterable, fn, name) {
function get_this() { function get_this() {
return this; return this;
} }
/** @param {import('#client').Source<number>} source */
export function increment(source) {
set(source, source.v + 1);
}

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

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'const_tag_cycle',
message: 'Cyclical dependency detected: a → b → a',
position: [12, 26]
}
});

@ -0,0 +1,5 @@
{#if true}
{@const a = b}
{@const b = a}
<h1>hello {a}</h1>
{/if}

@ -1,2 +1,7 @@
<svelte:element this="div"></svelte:element>
<!-- prettier-ignore -->
<svelte:element this='div'></svelte:element>
<svelte:element this={"div"}></svelte:element> <svelte:element this={"div"}></svelte:element>
<!-- prettier-ignore -->
<svelte:element this={'div'}></svelte:element>
<svelte:element this={"div"} class="foo"></svelte:element> <svelte:element this={"div"} class="foo"></svelte:element>

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

Loading…
Cancel
Save