Merge branch 'main' into gh-10511

pull/10512/head
Simon Holthausen 3 years ago
commit b7f3b88761

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly scope CSS selectors with descendant combinators

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: implement support for `:is(...)` and `:where(...)`

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: improve code generation for `bind:this` in SSR mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: treeshake unused store subscriptions in SSR mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve bind:this support for each blocks

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve effect over-fire on store subscription init

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: use init properties when exporting non-state values in prod

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: implement nested CSS support

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve each block item equality for immutable mode

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: handle member expressions in directives

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve handling of unowned derived signals

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: better handling of empty text node hydration

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve $inspect handling of derived objects

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove memory leak

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure update methods of actions and reactive statements work with fine-grained `$state`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: don't execute scripts inside `@html` when instantiated on the client

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: make `bind_this` implementation more robust

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: only escape characters in SSR template

@ -14,26 +14,33 @@
"angry-books-jam",
"angry-plums-punch",
"beige-flies-wash",
"beige-mirrors-listen",
"beige-rabbits-shave",
"big-cars-serve",
"big-eggs-flash",
"big-eyes-carry",
"big-geese-act",
"blue-timers-film",
"brave-points-sleep",
"brave-shrimps-kiss",
"brave-walls-destroy",
"breezy-carrots-flash",
"bright-peas-juggle",
"bright-snakes-sing",
"brown-months-fry",
"brown-spoons-boil",
"chatty-cups-drop",
"chatty-taxis-juggle",
"chilled-pumas-invite",
"chilly-dolphins-lick",
"chilly-snakes-scream",
"clean-eels-beg",
"clever-chefs-relate",
"clever-rockets-burn",
"cold-birds-own",
"cool-ants-leave",
"cool-rabbits-tickle",
"cool-roses-trade",
"cuddly-pianos-drop",
"curly-lizards-dream",
"curvy-cups-cough",
@ -53,6 +60,7 @@
"eight-steaks-shout",
"eighty-bikes-camp",
"eighty-days-cheat",
"empty-bags-heal",
"empty-bulldogs-exercise",
"empty-crabs-think",
"empty-tools-share",
@ -67,8 +75,10 @@
"fifty-steaks-float",
"five-tigers-search",
"flat-melons-protect",
"fluffy-dolls-share",
"fluffy-humans-worry",
"forty-comics-invent",
"forty-dogs-divide",
"forty-dolls-wave",
"forty-peaches-unite",
"forty-suns-smile",
@ -84,13 +94,16 @@
"giant-roses-press",
"good-cars-visit",
"good-pianos-jump",
"good-rivers-yawn",
"gorgeous-singers-rest",
"great-fans-unite",
"great-icons-retire",
"green-eggs-approve",
"green-hounds-play",
"green-tigers-judge",
"happy-suits-film",
"healthy-planes-vanish",
"heavy-comics-move",
"heavy-ears-rule",
"hip-balloons-begin",
"honest-buses-add",
@ -104,10 +117,12 @@
"itchy-kings-deliver",
"itchy-lions-wash",
"itchy-terms-guess",
"khaki-cooks-develop",
"khaki-mails-draw",
"khaki-moose-arrive",
"kind-baboons-approve",
"kind-deers-lay",
"kind-dots-sort",
"kind-eagles-join",
"kind-rings-flash",
"large-clouds-carry",
@ -118,6 +133,7 @@
"lazy-spiders-think",
"lemon-geese-call",
"lemon-geese-drum",
"light-days-clean",
"light-humans-hang",
"light-pens-watch",
"long-buckets-lay",
@ -132,10 +148,12 @@
"lucky-toes-begin",
"many-trees-fix",
"mighty-files-hammer",
"moody-carrots-lay",
"moody-frogs-exist",
"moody-owls-cry",
"nasty-lions-double",
"nasty-yaks-peel",
"neat-boats-shake",
"neat-dingos-clap",
"nervous-spoons-relax",
"new-boats-wait",
@ -165,6 +183,7 @@
"popular-mangos-rest",
"pretty-ties-help",
"purple-dragons-peel",
"quiet-apricots-dream",
"quiet-camels-mate",
"quiet-crabs-nail",
"quiet-timers-speak",
@ -172,13 +191,17 @@
"rare-worms-hunt",
"real-guests-do",
"real-items-suffer",
"real-pandas-brush",
"red-doors-own",
"red-feet-worry",
"rich-cobras-exist",
"rich-olives-yell",
"rich-sheep-burn",
"rich-tables-sing",
"rich-waves-mix",
"rotten-bags-type",
"rotten-buckets-develop",
"rotten-experts-relax",
"selfish-dragons-knock",
"selfish-tools-hide",
"serious-kids-deliver",
@ -188,6 +211,7 @@
"seven-hornets-smile",
"seven-jobs-sniff",
"seven-ravens-check",
"shaggy-cameras-live",
"sharp-gorillas-impress",
"sharp-kids-happen",
"sharp-tomatoes-learn",
@ -211,6 +235,7 @@
"soft-geese-learn",
"soft-months-grab",
"soft-tigers-wink",
"sour-bags-fail",
"sour-forks-stare",
"sour-rules-march",
"sour-weeks-fix",
@ -219,6 +244,7 @@
"spotty-pens-agree",
"spotty-rocks-destroy",
"spotty-spiders-compare",
"spotty-turkeys-sparkle",
"stale-books-perform",
"stale-comics-look",
"stale-jeans-refuse",
@ -247,6 +273,7 @@
"ten-worms-reflect",
"thick-cycles-rule",
"thick-pans-tell",
"thick-shirts-deliver",
"thin-foxes-lick",
"thirty-flowers-sit",
"thirty-ghosts-fix",
@ -255,17 +282,20 @@
"thirty-wombats-relax",
"three-camels-sell",
"three-icons-trade",
"three-papayas-buy",
"three-suits-grin",
"tidy-buses-whisper",
"tidy-starfishes-allow",
"tiny-kings-whisper",
"twelve-dragons-join",
"twelve-onions-juggle",
"twelve-worms-jog",
"two-dragons-yell",
"two-falcons-buy",
"unlucky-boxes-obey",
"unlucky-trees-lick",
"violet-pigs-jam",
"weak-terms-destroy",
"wet-games-fly",
"wicked-clouds-exercise",
"wicked-doors-train",
@ -276,6 +306,9 @@
"wise-jobs-admire",
"wise-radios-exercise",
"witty-camels-warn",
"witty-steaks-dream"
"witty-steaks-dream",
"witty-tomatoes-care",
"witty-years-crash",
"yellow-taxis-double"
]
}

@ -0,0 +1,5 @@
---
"svelte": patch
---
breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: call beforeUpdate/afterUpdate callbacks when props are mutated

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow state/derived/props to be explicitly exported from components

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: replace proxy-based readonly validation with stack-trace-based ownership tracking

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: permit whitespace within template scripts

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: wire up `events` in `mount` correctly and fix its types

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: inherit ownerlessness when creating child proxies

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: allow boolean `contenteditable` attribute

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)`

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve props spreading logic

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: correct context applied to batch_inspect

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: visit expression node in directives

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: better handling of derived signals that have no dependencies

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve state store mutation compiler output

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: improve import event handler support

@ -1,5 +1,107 @@
# svelte
## 5.0.0-next.65
### Patch Changes
- fix: improve $inspect handling of derived objects ([#10584](https://github.com/sveltejs/svelte/pull/10584))
- fix: permit whitespace within template scripts ([#10591](https://github.com/sveltejs/svelte/pull/10591))
- fix: allow boolean `contenteditable` attribute ([#10590](https://github.com/sveltejs/svelte/pull/10590))
- fix: improve import event handler support ([#10592](https://github.com/sveltejs/svelte/pull/10592))
## 5.0.0-next.64
### Patch Changes
- fix: inherit ownerlessness when creating child proxies ([#10577](https://github.com/sveltejs/svelte/pull/10577))
## 5.0.0-next.63
### Patch Changes
- fix: handle member expressions in directives ([#10576](https://github.com/sveltejs/svelte/pull/10576))
- fix: remove memory leak ([#10570](https://github.com/sveltejs/svelte/pull/10570))
- fix: call beforeUpdate/afterUpdate callbacks when props are mutated ([#10570](https://github.com/sveltejs/svelte/pull/10570))
- fix: improve props spreading logic ([#10574](https://github.com/sveltejs/svelte/pull/10574))
## 5.0.0-next.62
### Patch Changes
- feat: allow state/derived/props to be explicitly exported from components ([#10523](https://github.com/sveltejs/svelte/pull/10523))
- fix: replace proxy-based readonly validation with stack-trace-based ownership tracking ([#10464](https://github.com/sveltejs/svelte/pull/10464))
- fix: correct context applied to batch_inspect ([#10569](https://github.com/sveltejs/svelte/pull/10569))
## 5.0.0-next.61
### Patch Changes
- fix: improve each block item equality for immutable mode ([#10537](https://github.com/sveltejs/svelte/pull/10537))
- fix: improve handling of unowned derived signals ([#10565](https://github.com/sveltejs/svelte/pull/10565))
- fix: better handling of empty text node hydration ([#10545](https://github.com/sveltejs/svelte/pull/10545))
- fix: ensure update methods of actions and reactive statements work with fine-grained `$state` ([#10543](https://github.com/sveltejs/svelte/pull/10543))
- fix: don't execute scripts inside `@html` when instantiated on the client ([#10556](https://github.com/sveltejs/svelte/pull/10556))
- fix: only escape characters in SSR template ([#10555](https://github.com/sveltejs/svelte/pull/10555))
- fix: wire up `events` in `mount` correctly and fix its types ([#10553](https://github.com/sveltejs/svelte/pull/10553))
- fix: better handling of derived signals that have no dependencies ([#10558](https://github.com/sveltejs/svelte/pull/10558))
- fix: improve state store mutation compiler output ([#10561](https://github.com/sveltejs/svelte/pull/10561))
## 5.0.0-next.60
### Patch Changes
- fix: improve effect over-fire on store subscription init ([#10535](https://github.com/sveltejs/svelte/pull/10535))
- fix: use init properties when exporting non-state values in prod ([#10521](https://github.com/sveltejs/svelte/pull/10521))
## 5.0.0-next.59
### Patch Changes
- chore: improve code generation for `bind:this` in SSR mode ([#10524](https://github.com/sveltejs/svelte/pull/10524))
- fix: visit expression node in directives ([#10527](https://github.com/sveltejs/svelte/pull/10527))
## 5.0.0-next.58
### Patch Changes
- breaking: remove `createRoot`, adjust `mount`/`hydrate` APIs, introduce `unmount` ([#10516](https://github.com/sveltejs/svelte/pull/10516))
## 5.0.0-next.57
### Patch Changes
- fix: correctly scope CSS selectors with descendant combinators ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- feat: implement support for `:is(...)` and `:where(...)` ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- chore: treeshake unused store subscriptions in SSR mode ([#10506](https://github.com/sveltejs/svelte/pull/10506))
- fix: warn against accidental global event referenced ([#10442](https://github.com/sveltejs/svelte/pull/10442))
- fix: improve bind:this support for each blocks ([#10510](https://github.com/sveltejs/svelte/pull/10510))
- feat: implement nested CSS support ([#10490](https://github.com/sveltejs/svelte/pull/10490))
- breaking: encapsulate/remove selectors inside `:is(...)` and `:where(...)` ([#10490](https://github.com/sveltejs/svelte/pull/10490))
## 5.0.0-next.56
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.56",
"version": "5.0.0-next.65",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -107,7 +107,8 @@ const css = {
'invalid-css-global-selector-list': () =>
`:global(...) must not contain type or universal selectors when used in a compound selector`,
'invalid-css-selector': () => `Invalid selector`,
'invalid-css-identifier': () => 'Expected a valid CSS identifier'
'invalid-css-identifier': () => 'Expected a valid CSS identifier',
'invalid-nesting-selector': () => `Nesting selectors can only be used inside a rule`
};
/** @satisfies {Errors} */
@ -170,11 +171,9 @@ const runes = {
/** @param {string} rune */
'invalid-rune-usage': (rune) => `Cannot use ${rune} rune in non-runes mode`,
'invalid-state-export': () =>
`Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
`Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties`,
'invalid-derived-export': () =>
`Cannot export derived state. To expose the current derived value, export a function returning its value`,
'invalid-prop-export': () =>
`Cannot export properties. To expose the current value of a property, export a function returning its value`,
`Cannot export derived state from a module. To expose the current derived value, export a function returning its value`,
'invalid-props-id': () => `$props() can only be used with an object destructuring pattern`,
'invalid-props-pattern': () =>
`$props() assignment must not contain nested properties or computed keys`,
@ -372,10 +371,6 @@ const errors = {
// message:
// "'contenteditable' attribute is required for textContent, innerHTML and innerText two-way bindings"
// },
// dynamic_contenteditable_attribute: {
// code: 'dynamic-contenteditable-attribute',
// message: "'contenteditable' attribute cannot be dynamic if element uses two-way binding"
// },
// textarea_duplicate_value: {
// code: 'textarea-duplicate-value',
// message:

@ -83,36 +83,10 @@ function read_at_rule(parser) {
let block = null;
if (parser.match('{')) {
// if the parser could easily distinguish between rules and declarations, this wouldn't be necessary.
// but this approach is much simpler. in future, when we support CSS nesting, the parser _will_ need
// to be able to distinguish between them, but since we'll also need other changes to support that
// this remains a TODO
const contains_declarations = [
'color-profile',
'counter-style',
'font-face',
'font-palette-values',
'page',
'property'
].includes(name);
if (contains_declarations) {
// e.g. `@media (...) {...}`
block = read_block(parser);
} else {
const start = parser.index;
parser.eat('{', true);
const children = read_body(parser, '}');
parser.eat('}', true);
block = {
type: 'Block',
start,
end: parser.index,
children
};
}
} else {
// e.g. `@import '...'`
parser.eat(';', true);
}
@ -138,7 +112,11 @@ function read_rule(parser) {
prelude: read_selector_list(parser),
block: read_block(parser),
start,
end: parser.index
end: parser.index,
metadata: {
parent_rule: null,
has_local_selectors: false
}
};
}
@ -216,7 +194,14 @@ function read_selector(parser, inside_pseudo_class = false) {
while (parser.index < parser.template.length) {
let start = parser.index;
if (parser.eat('*')) {
if (parser.eat('&')) {
relative_selector.selectors.push({
type: 'NestingSelector',
name: '&',
start,
end: parser.index
});
} else if (parser.eat('*')) {
let name = '*';
if (parser.eat('|')) {
@ -356,6 +341,7 @@ function read_selector(parser, inside_pseudo_class = false) {
end: index,
children,
metadata: {
rule: null,
used: false
}
};
@ -432,7 +418,7 @@ function read_block(parser) {
parser.eat('{', true);
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule>} */
/** @type {Array<import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule>} */
const children = [];
while (parser.index < parser.template.length) {
@ -441,7 +427,7 @@ function read_block(parser) {
if (parser.match('}')) {
break;
} else {
children.push(read_declaration(parser));
children.push(read_block_item(parser));
}
}
@ -455,6 +441,27 @@ function read_block(parser) {
};
}
/**
* Reads a declaration, rule or at-rule
*
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration | import('#compiler').Css.Rule | import('#compiler').Css.Atrule}
*/
function read_block_item(parser) {
if (parser.match('@')) {
return read_at_rule(parser);
}
// read ahead to understand whether we're dealing with a declaration or a nested rule.
// this involves some duplicated work, but avoids a try-catch that would disguise errors
const start = parser.index;
read_value(parser);
const char = parser.template[parser.index];
parser.index = start;
return char === '{' ? read_rule(parser) : read_declaration(parser);
}
/**
* @param {import('../index.js').Parser} parser
* @returns {import('#compiler').Css.Declaration}

@ -1,3 +1,4 @@
import { walk } from 'zimmerframe';
import { error } from '../../../errors.js';
import { is_keyframes_node } from '../../css.js';
import { merge } from '../../visitors.js';
@ -5,7 +6,10 @@ import { merge } from '../../visitors.js';
/**
* @typedef {import('zimmerframe').Visitors<
* import('#compiler').Css.Node,
* NonNullable<import('../../types.js').ComponentAnalysis['css']>
* {
* keyframes: string[];
* rule: import('#compiler').Css.Rule | null;
* }
* >} Visitors
*/
@ -24,7 +28,7 @@ function is_global(relative_selector) {
}
/** @type {Visitors} */
const analysis = {
const analysis_visitors = {
Atrule(node, context) {
if (is_keyframes_node(node)) {
if (!node.prelude.startsWith('-global-')) {
@ -35,6 +39,8 @@ const analysis = {
ComplexSelector(node, context) {
context.next(); // analyse relevant selectors first
node.metadata.rule = context.state.rule;
node.metadata.used = node.children.every(
({ metadata }) => metadata.is_global || metadata.is_host || metadata.is_root
);
@ -59,11 +65,25 @@ const analysis = {
);
context.next();
},
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
context.next({
...context.state,
rule: node
});
node.metadata.has_local_selectors = node.prelude.children.some((selector) => {
return selector.children.some(
({ metadata }) => !metadata.is_global && !metadata.is_host && !metadata.is_root
);
});
}
};
/** @type {Visitors} */
const validation = {
const validation_visitors = {
ComplexSelector(node, context) {
// ensure `:global(...)` is not used in the middle of a selector
{
@ -118,7 +138,21 @@ const validation = {
}
}
}
},
NestingSelector(node, context) {
const rule = /** @type {import('#compiler').Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
error(node, 'invalid-nesting-selector');
}
}
};
export const css_visitors = merge(analysis, validation);
const css_visitors = merge(analysis_visitors, validation_visitors);
/**
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('../../types.js').ComponentAnalysis} analysis
*/
export function analyze_css(stylesheet, analysis) {
walk(stylesheet, { keyframes: analysis.css.keyframes, rule: null }, css_visitors);
}

@ -1,6 +1,7 @@
import { walk } from 'zimmerframe';
import { get_possible_values } from './utils.js';
import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../patterns.js';
import { error } from '../../../errors.js';
/**
* @typedef {{
@ -8,254 +9,379 @@ import { regex_ends_with_whitespace, regex_starts_with_whitespace } from '../../
* element: import('#compiler').RegularElement | import('#compiler').SvelteElement;
* }} State
*/
/** @typedef {typeof NodeExist[keyof typeof NodeExist]} NodeExistsValue */
/** @typedef {NODE_PROBABLY_EXISTS | NODE_DEFINITELY_EXISTS} NodeExistsValue */
const NO_MATCH = 'NO_MATCH';
const POSSIBLE_MATCH = 'POSSIBLE_MATCH';
const UNKNOWN_SELECTOR = 'UNKNOWN_SELECTOR';
const NodeExist = /** @type {const} */ ({
Probably: 0,
Definitely: 1
});
const NODE_PROBABLY_EXISTS = 0;
const NODE_DEFINITELY_EXISTS = 1;
const whitelist_attribute_selector = new Map([
['details', ['open']],
['dialog', ['open']]
]);
/** @type {import('#compiler').Css.Combinator} */
const descendant_combinator = {
type: 'Combinator',
name: ' ',
start: -1,
end: -1
};
/** @type {import('#compiler').Css.RelativeSelector} */
const nesting_selector = {
type: 'RelativeSelector',
start: -1,
end: -1,
combinator: null,
selectors: [
{
type: 'NestingSelector',
name: '&',
start: -1,
end: -1
}
],
metadata: {
is_global: false,
is_host: false,
is_root: false,
scoped: false
}
};
/**
*
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
export function prune(stylesheet, element) {
/** @type {State} */
const state = { stylesheet, element };
walk(stylesheet, state, visitors);
walk(stylesheet, { stylesheet, element }, visitors);
}
/** @type {import('zimmerframe').Visitors<import('#compiler').Css.Node, State>} */
const visitors = {
ComplexSelector(node, context) {
context.next();
const selectors = truncate(node);
const inner = selectors[selectors.length - 1];
const i = node.children.findLastIndex((child) => {
return !child.metadata.is_global && !child.metadata.is_host && !child.metadata.is_root;
});
if (node.metadata.rule?.metadata.parent_rule) {
const has_explicit_nesting_selector = selectors.some((selector) =>
selector.selectors.some((s) => s.type === 'NestingSelector')
);
const relative_selectors = node.children.slice(0, i + 1);
if (!has_explicit_nesting_selector) {
selectors[0] = {
...selectors[0],
combinator: descendant_combinator
};
if (apply_selector(relative_selectors, context.state.element, context.state.stylesheet)) {
selectors.unshift(nesting_selector);
}
}
if (
apply_selector(
selectors,
/** @type {import('#compiler').Css.Rule} */ (node.metadata.rule),
context.state.element,
context.state.stylesheet
)
) {
mark(inner, context.state.element);
node.metadata.used = true;
}
},
RelativeSelector(node, context) {
// for now, don't visit children (i.e. inside `:foo(...)`)
// this will likely change when we implement `:is(...)` etc
// note: we don't call context.next() here, we only recurse into
// selectors that don't belong to rules (i.e. inside `:is(...)` etc)
// when we encounter them below
}
};
/**
* Discard trailing `:global(...)` selectors, these are unused for scoping purposes
* @param {import('#compiler').Css.ComplexSelector} node
*/
function truncate(node) {
const i = node.children.findLastIndex(({ metadata }) => {
return !metadata.is_global && !metadata.is_host && !metadata.is_root;
});
return node.children.slice(0, i + 1);
}
/**
* @param {import('#compiler').Css.RelativeSelector[]} relative_selectors
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} element
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function apply_selector(relative_selectors, element, stylesheet) {
if (!element) {
return relative_selectors.every(({ metadata }) => metadata.is_global || metadata.is_host);
}
function apply_selector(relative_selectors, rule, element, stylesheet) {
const parent_selectors = relative_selectors.slice();
const relative_selector = parent_selectors.pop();
const relative_selector = relative_selectors.pop();
if (!relative_selector) return false;
const applies = relative_selector_might_apply_to_node(relative_selector, element);
const possible_match = relative_selector_might_apply_to_node(
relative_selector,
rule,
element,
stylesheet
);
if (applies === NO_MATCH) {
if (!possible_match) {
return false;
}
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
function mark(relative_selector, element) {
relative_selector.metadata.scoped = true;
element.metadata.scoped = true;
return true;
}
if (relative_selector.combinator) {
const name = relative_selector.combinator.name;
if (applies === UNKNOWN_SELECTOR) {
return mark(relative_selector, element);
}
switch (name) {
case ' ':
case '>': {
let parent = /** @type {import('#compiler').TemplateNode | null} */ (element.parent);
if (relative_selector.combinator) {
if (
relative_selector.combinator.type === 'Combinator' &&
relative_selector.combinator.name === ' '
) {
for (const ancestor_selector of relative_selectors) {
if (ancestor_selector.metadata.is_global) {
continue;
}
let parent_matched = false;
let crossed_component_boundary = false;
if (ancestor_selector.metadata.is_host) {
return mark(relative_selector, element);
while (parent) {
if (parent.type === 'Component' || parent.type === 'SvelteComponent') {
crossed_component_boundary = true;
}
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | null} */
let parent = element;
let matched = false;
while ((parent = get_element_parent(parent))) {
if (relative_selector_might_apply_to_node(ancestor_selector, parent) !== NO_MATCH) {
mark(ancestor_selector, parent);
matched = true;
if (parent.type === 'RegularElement' || parent.type === 'SvelteElement') {
if (apply_selector(parent_selectors, rule, parent, stylesheet)) {
// TODO the `name === ' '` causes false positives, but removing it causes false negatives...
if (name === ' ' || crossed_component_boundary) {
mark(parent_selectors[parent_selectors.length - 1], parent);
}
parent_matched = true;
}
if (matched) {
return mark(relative_selector, element);
if (name === '>') return parent_matched;
}
parent = /** @type {import('#compiler').TemplateNode | null} */ (parent.parent);
}
if (relative_selectors.every((relative_selector) => relative_selector.metadata.is_global)) {
return mark(relative_selector, element);
return parent_matched || parent_selectors.every((selector) => is_global(selector, rule));
}
return false;
case '+':
case '~': {
const siblings = get_possible_element_siblings(element, name === '+');
let sibling_matched = false;
for (const possible_sibling of siblings.keys()) {
if (apply_selector(parent_selectors, rule, possible_sibling, stylesheet)) {
mark(relative_selector, element);
sibling_matched = true;
}
}
if (relative_selector.combinator.name === '>') {
const has_global_parent = relative_selectors.every(
(relative_selector) => relative_selector.metadata.is_global
return (
sibling_matched ||
(get_element_parent(element) === null &&
parent_selectors.every((selector) => is_global(selector, rule)))
);
}
if (
has_global_parent ||
apply_selector(relative_selectors, get_element_parent(element), stylesheet)
) {
return mark(relative_selector, element);
default:
// TODO other combinators
return true;
}
}
return false;
// if this is the left-most non-global selector, mark it — we want
// `x y z {...}` to become `x.blah y z.blah {...}`
const parent = parent_selectors[parent_selectors.length - 1];
if (!parent || is_global(parent, rule)) {
mark(relative_selector, element);
}
if (relative_selector.combinator.name === '+' || relative_selector.combinator.name === '~') {
const siblings = get_possible_element_siblings(
element,
relative_selector.combinator.name === '+'
);
return true;
}
let has_match = false;
// NOTE: if we have :global(), we couldn't figure out what is selected within `:global` due to the
// css-tree limitation that does not parse the inner selector of :global
// so unless we are sure there will be no sibling to match, we will consider it as matched
const has_global = relative_selectors.some(
(relative_selector) => relative_selector.metadata.is_global
);
/**
* Mark both the compound selector and the node it selects as encapsulated,
* for transformation in a later step
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
*/
function mark(relative_selector, element) {
relative_selector.metadata.scoped = true;
element.metadata.scoped = true;
}
if (has_global) {
if (siblings.size === 0 && get_element_parent(element) !== null) {
return false;
}
return mark(relative_selector, element);
/**
* Returns `true` if the relative selector is global, meaning
* it's a `:global(...)` or `:host` or `:root` selector, or
* is an `:is(...)` or `:where(...)` selector that contains
* a global selector
* @param {import('#compiler').Css.RelativeSelector} selector
* @param {import('#compiler').Css.Rule} rule
*/
function is_global(selector, rule) {
if (selector.metadata.is_global || selector.metadata.is_host || selector.metadata.is_root) {
return true;
}
for (const possible_sibling of siblings.keys()) {
if (apply_selector(relative_selectors.slice(), possible_sibling, stylesheet)) {
mark(relative_selector, element);
has_match = true;
for (const s of selector.selectors) {
/** @type {import('#compiler').Css.SelectorList | null} */
let selector_list = null;
let owner = rule;
if (s.type === 'PseudoClassSelector') {
if ((s.name === 'is' || s.name === 'where') && s.args) {
selector_list = s.args;
}
}
return has_match;
if (s.type === 'NestingSelector') {
owner = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
selector_list = owner.prelude;
}
// TODO other combinators
return mark(relative_selector, element);
const has_global_selectors = selector_list?.children.some((complex_selector) => {
return complex_selector.children.every((relative_selector) =>
is_global(relative_selector, owner)
);
});
if (!has_global_selectors) {
return false;
}
}
return mark(relative_selector, element);
return true;
}
const regex_backslash_and_following_character = /\\(.)/g;
/**
* Ensure that `element` satisfies each simple selector in `relative_selector`
*
* @param {import('#compiler').Css.RelativeSelector} relative_selector
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @returns {NO_MATCH | POSSIBLE_MATCH | UNKNOWN_SELECTOR}
* @param {import('#compiler').Css.Rule} rule
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Css.StyleSheet} stylesheet
* @returns {boolean}
*/
function relative_selector_might_apply_to_node(relative_selector, node) {
if (relative_selector.metadata.is_host || relative_selector.metadata.is_root) return NO_MATCH;
let i = relative_selector.selectors.length;
while (i--) {
const selector = relative_selector.selectors[i];
function relative_selector_might_apply_to_node(relative_selector, rule, element, stylesheet) {
for (const selector of relative_selector.selectors) {
if (selector.type === 'Percentage' || selector.type === 'Nth') continue;
const name = selector.name.replace(regex_backslash_and_following_character, '$1');
if (selector.type === 'PseudoClassSelector' && (name === 'host' || name === 'root')) {
return NO_MATCH;
switch (selector.type) {
case 'PseudoClassSelector': {
if (name === 'host' || name === 'root') {
return false;
}
if (
relative_selector.selectors.length === 1 &&
selector.type === 'PseudoClassSelector' &&
name === 'global'
) {
return NO_MATCH;
if (name === 'global' && relative_selector.selectors.length === 1) {
const args = /** @type {import('#compiler').Css.SelectorList} */ (selector.args);
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, stylesheet);
}
if (selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector') {
continue;
if ((name === 'is' || name === 'where') && selector.args) {
let matched = false;
for (const complex_selector of selector.args.children) {
if (apply_selector(truncate(complex_selector), rule, element, stylesheet)) {
complex_selector.metadata.used = true;
matched = true;
}
}
if (!matched) {
return false;
}
}
if (selector.type === 'AttributeSelector') {
const whitelisted = whitelist_attribute_selector.get(node.name.toLowerCase());
break;
}
case 'PseudoElementSelector': {
break;
}
case 'AttributeSelector': {
const whitelisted = whitelist_attribute_selector.get(element.name.toLowerCase());
if (
!whitelisted?.includes(selector.name.toLowerCase()) &&
!attribute_matches(
node,
element,
selector.name,
selector.value && unquote(selector.value),
selector.matcher,
selector.flags?.includes('i') ?? false
)
) {
return NO_MATCH;
return false;
}
} else {
if (selector.type === 'ClassSelector') {
break;
}
case 'ClassSelector': {
if (
!attribute_matches(node, 'class', name, '~=', false) &&
!node.attributes.some(
!attribute_matches(element, 'class', name, '~=', false) &&
!element.attributes.some(
(attribute) => attribute.type === 'ClassDirective' && attribute.name === name
)
) {
return NO_MATCH;
return false;
}
} else if (selector.type === 'IdSelector') {
if (!attribute_matches(node, 'id', name, '=', false)) return NO_MATCH;
} else if (selector.type === 'TypeSelector') {
break;
}
case 'IdSelector': {
if (!attribute_matches(element, 'id', name, '=', false)) {
return false;
}
break;
}
case 'TypeSelector': {
if (
node.name.toLowerCase() !== name.toLowerCase() &&
element.name.toLowerCase() !== name.toLowerCase() &&
name !== '*' &&
node.type !== 'SvelteElement'
element.type !== 'SvelteElement'
) {
return NO_MATCH;
return false;
}
} else {
return UNKNOWN_SELECTOR;
break;
}
case 'NestingSelector': {
let matched = false;
const parent = /** @type {import('#compiler').Css.Rule} */ (rule.metadata.parent_rule);
for (const complex_selector of parent.prelude.children) {
if (apply_selector(truncate(complex_selector), parent, element, stylesheet)) {
complex_selector.metadata.used = true;
matched = true;
}
}
if (!matched) {
return false;
}
return POSSIBLE_MATCH;
break;
}
}
}
// possible match
return true;
}
/**
@ -481,7 +607,7 @@ function get_possible_element_siblings(node, adjacent_only) {
(attr) => attr.type === 'Attribute' && attr.name.toLowerCase() === 'slot'
)
) {
result.set(prev, NodeExist.Definitely);
result.set(prev, NODE_DEFINITELY_EXISTS);
}
if (adjacent_only) {
break;
@ -600,7 +726,7 @@ function get_possible_last_child(relative_selector, adjacent_only) {
function has_definite_elements(result) {
if (result.size === 0) return false;
for (const exist of result.values()) {
if (exist === NodeExist.Definitely) {
if (exist === NODE_DEFINITELY_EXISTS) {
return true;
}
}
@ -632,7 +758,7 @@ function higher_existence(exist1, exist2) {
/** @param {Map<import('#compiler').RegularElement, NodeExistsValue>} result */
function mark_as_probably(result) {
for (const key of result.keys()) {
result.set(key, NodeExist.Probably);
result.set(key, NODE_PROBABLY_EXISTS);
}
}
@ -646,7 +772,7 @@ function loop_child(children, adjacent_only) {
for (let i = children.length - 1; i >= 0; i--) {
const child = children[i];
if (child.type === 'RegularElement') {
result.set(child, NodeExist.Definitely);
result.set(child, NODE_DEFINITELY_EXISTS);
if (adjacent_only) {
break;
}

@ -20,7 +20,7 @@ import { regex_starts_with_newline } from '../patterns.js';
import { create_attribute, is_element_node } from '../nodes.js';
import { DelegatedEvents, namespace_svg } from '../../../constants.js';
import { should_proxy_or_freeze } from '../3-transform/client/utils.js';
import { css_visitors } from './css/css-analyze.js';
import { analyze_css } from './css/css-analyze.js';
import { prune } from './css/css-prune.js';
import { hash } from './utils.js';
@ -460,8 +460,7 @@ export function analyze_component(root, options) {
}
if (analysis.css.ast) {
// validate
walk(analysis.css.ast, analysis.css, css_visitors);
analyze_css(analysis.css.ast, analysis);
// mark nodes as scoped/unused/empty etc
for (const element of analysis.elements) {
@ -1061,7 +1060,9 @@ const common_visitors = {
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf'
) {
if (node.name !== 'this') {
context.state.analysis.uses_component_bindings = true;
}
break;
} else if (is_element_node(parent)) {
break;

@ -475,7 +475,7 @@ const validation = {
);
if (!contenteditable) {
error(node, 'missing-contenteditable-attribute');
} else if (!is_text_attribute(contenteditable)) {
} else if (!is_text_attribute(contenteditable) && contenteditable.value !== true) {
error(contenteditable, 'dynamic-contenteditable-attribute');
}
}
@ -733,10 +733,6 @@ function validate_export(node, scope, name) {
const binding = scope.get(name);
if (!binding) return;
if (binding.kind === 'prop') {
error(node, 'invalid-prop-export');
}
if (binding.kind === 'derived') {
error(node, 'invalid-derived-export');
}
@ -964,25 +960,12 @@ export const validation_runes = merge(validation, a11y_validators, {
if (node.label.name !== '$' || path.at(-1)?.type !== 'Program') return;
error(node, 'invalid-legacy-reactive-statement');
},
ExportNamedDeclaration(node, { state, next }) {
ExportNamedDeclaration(node, { state }) {
if (node.declaration?.type !== 'VariableDeclaration') return;
// visit children, so bindings are correctly initialised
next();
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
validate_export(node, state.scope, id.name);
}
}
if (state.analysis.instance.scope !== state.scope) return;
if (node.declaration.kind !== 'let') return;
if (state.analysis.instance.scope !== state.scope) return;
error(node, 'invalid-legacy-export');
},
ExportSpecifier(node, { state }) {
validate_export(node, state.scope, node.local.name);
},
CallExpression(node, { state, path }) {
validate_call_expression(node, state.scope, path);
},

@ -225,26 +225,24 @@ export function client_component(source, analysis, options) {
// Bind static exports to props so that people can access them with bind:x
const static_bindings = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
return b.stmt(
b.call(
'$.bind_prop',
b.id('$$props'),
b.literal(alias ?? name),
binding?.kind === 'state' || binding?.kind === 'frozen_state'
? b.call('$.get', b.id(name))
: b.id(name)
serialize_get_binding(b.id(name), instance_state)
)
);
});
const properties = analysis.exports.map(({ name, alias }) => {
const binding = analysis.instance.scope.get(name);
const is_source = binding !== null && is_state_source(binding, state);
const expression = serialize_get_binding(b.id(name), instance_state);
// TODO This is always a getter because the `renamed-instance-exports` test wants it that way.
// Should we for code size reasons make it an init in runes mode and/or non-dev mode?
return b.get(alias ?? name, [b.return(is_source ? b.call('$.get', b.id(name)) : b.id(name))]);
if (expression.type === 'Identifier' && !options.dev) {
return b.init(alias ?? name, expression);
}
return b.get(alias ?? name, [b.return(expression)]);
});
if (analysis.accessors) {
@ -260,8 +258,11 @@ export function client_component(source, analysis, options) {
}
}
const push_args = [b.id('$$props'), b.literal(analysis.runes)];
if (options.dev) push_args.push(b.id(analysis.name));
const component_block = b.block([
b.stmt(b.call('$.push', b.id('$$props'), b.literal(analysis.runes))),
b.stmt(b.call('$.push', ...push_args)),
...store_setup,
...legacy_reactive_declarations,
...group_binding_declarations,
@ -286,17 +287,12 @@ export function client_component(source, analysis, options) {
)
: () => {};
if (properties.length > 0) {
append_styles();
component_block.body.push(
b.var('$$accessors', b.object(properties)),
b.stmt(b.call('$.pop', b.id('$$accessors')))
properties.length > 0
? b.return(b.call('$.pop', b.object(properties)))
: b.stmt(b.call('$.pop'))
);
append_styles();
component_block.body.push(b.return(b.id('$$accessors')));
} else {
component_block.body.push(b.stmt(b.call('$.pop')));
append_styles();
}
if (analysis.uses_rest_props) {
/** @type {string[]} */
@ -346,6 +342,27 @@ export function client_component(source, analysis, options) {
)
];
if (options.dev) {
if (options.filename) {
let filename = options.filename;
if (/(\/|\w:)/.test(options.filename)) {
// filename is absolute — truncate it
const parts = filename.split(/[/\\]/);
filename = parts.length > 3 ? ['...', ...parts.slice(-3)].join('/') : filename;
}
// add `App.filename = 'App.svelte'` so that we can print useful messages later
body.push(
b.stmt(
b.assignment('=', b.member(b.id(analysis.name), b.id('filename')), b.literal(filename))
)
);
}
body.unshift(b.stmt(b.call(b.id('$.mark_module_start'), b.id(analysis.name))));
body.push(b.stmt(b.call(b.id('$.mark_module_end'))));
}
if (options.discloseVersion) {
body.unshift(b.imports([], 'svelte/internal/disclose-version'));
}

@ -431,7 +431,7 @@ export function serialize_set_binding(node, context, fallback, options) {
return {
...node,
object: visit_node(/** @type {import("estree").Expression} */ (node.object)),
property: /** @type {import("estree").Expression} */ (visit(node.property))
property: /** @type {import("estree").MemberExpression} */ (visit(node)).property
};
}
if (node.type === 'Identifier') {

@ -167,8 +167,10 @@ export const javascript_visitors_legacy = {
const name = binding.node.name;
let serialized = serialize_get_binding(b.id(name), state);
if (name === '$$props' || name === '$$restProps') {
serialized = b.call('$.access_props', serialized);
// If the binding is a prop, we need to deep read it because it could be fine-grained $state
// from a runes-component, where mutations don't trigger an update on the prop as a whole.
if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') {
serialized = b.call('$.deep_read', serialized);
}
sequence.push(serialized);

@ -28,13 +28,14 @@ import {
DOMBooleanAttributes,
EACH_INDEX_REACTIVE,
EACH_IS_CONTROLLED,
EACH_IS_IMMUTABLE,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED
} from '../../../../../constants.js';
import { regex_is_valid_identifier } from '../../../patterns.js';
import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { walk } from 'zimmerframe';
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
@ -764,6 +765,8 @@ function serialize_inline_component(node, component_name, context) {
/** @type {import('estree').Identifier | import('estree').MemberExpression | null} */
let bind_this = null;
const binding_initializers = [];
/**
* If this component has a slot property, it is a named slot within another component. In this case
* the slot scope applies to the component itself, too, and not just its children.
@ -843,8 +846,6 @@ function serialize_inline_component(node, component_name, context) {
arg = b.call('$.get', id);
}
if (context.state.options.dev) arg = b.call('$.readonly', arg);
push_prop(b.get(attribute.name, [b.return(arg)]));
} else {
push_prop(b.init(attribute.name, value));
@ -853,13 +854,22 @@ function serialize_inline_component(node, component_name, context) {
if (attribute.name === 'this') {
bind_this = attribute.expression;
} else {
push_prop(
b.get(attribute.name, [
b.return(
/** @type {import('estree').Expression} */ (context.visit(attribute.expression))
const expression = /** @type {import('estree').Expression} */ (
context.visit(attribute.expression)
);
if (context.state.options.dev) {
binding_initializers.push(
b.stmt(
b.call(
b.id('$.pre_effect'),
b.thunk(b.call(b.id('$.add_owner'), expression, b.id(component_name)))
)
)
])
);
}
push_prop(b.get(attribute.name, [b.return(expression)]));
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
@ -969,20 +979,11 @@ function serialize_inline_component(node, component_name, context) {
if (bind_this !== null) {
const prev = fn;
const assignment = b.assignment('=', bind_this, b.id('$$value'));
const bind_this_id = /** @type {import('estree').Expression} */ (
// if expression is not an identifier, we know it can't be a signal
bind_this.type === 'Identifier' ? bind_this : undefined
);
fn = (node_id) =>
b.call(
'$.bind_this',
prev(node_id),
b.arrow(
[b.id('$$value')],
serialize_set_binding(assignment, context, () => context.visit(assignment))
),
bind_this_id
serialize_bind_this(
/** @type {import('estree').Identifier | import('estree').MemberExpression} */ (bind_this),
context,
prev(node_id)
);
}
@ -1000,14 +1001,73 @@ function serialize_inline_component(node, component_name, context) {
);
}
/** @type {import('estree').Statement} */
let statement = b.stmt(fn(context.state.node));
const statements = [
...snippet_declarations,
...binding_initializers,
b.stmt(fn(context.state.node))
];
return statements.length > 1 ? b.block(statements) : statements[0];
}
/**
* Serializes `bind:this` for components and elements.
* @param {import('estree').Identifier | import('estree').MemberExpression} bind_this
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').ComponentClientTransformState>} context
* @param {import('estree').Expression} node
* @returns
*/
function serialize_bind_this(bind_this, context, node) {
let i = 0;
/** @type {Map<import('#compiler').Binding, [arg_idx: number, transformed: import('estree').Expression, expression: import('#compiler').Binding['expression']]>} */
const each_ids = new Map();
// Transform each reference to an each block context variable into a $$value_<i> variable
// by temporarily changing the `expression` of the corresponding binding.
// These $$value_<i> variables will be filled in by the bind_this runtime function through its last argument.
// Note that we only do this for each context variables, the consequence is that the value might be stale in
// some scenarios where the value is a member expression with changing computed parts or using a combination of multiple
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
walk(
bind_this,
{},
{
Identifier(node) {
const binding = context.state.scope.get(node.name);
if (!binding || each_ids.has(binding)) return;
const associated_node = Array.from(context.state.scopes.entries()).find(
([_, scope]) => scope === binding?.scope
)?.[0];
if (associated_node?.type === 'EachBlock') {
each_ids.set(binding, [
i,
/** @type {import('estree').Expression} */ (context.visit(node)),
binding.expression
]);
binding.expression = b.id('$$value_' + i);
i++;
}
}
}
);
const bind_this_id = /** @type {import('estree').Expression} */ (context.visit(bind_this));
const ids = Array.from(each_ids.values()).map((id) => b.id('$$value_' + id[0]));
const assignment = b.assignment('=', bind_this, b.id('$$value'));
const update = serialize_set_binding(assignment, context, () => context.visit(assignment));
for (const [binding, [, , expression]] of each_ids) {
// reset expressions to what they were before
binding.expression = expression;
}
if (snippet_declarations.length > 0) {
statement = b.block([...snippet_declarations, statement]);
/** @type {import('estree').Expression[]} */
const args = [node, b.arrow([b.id('$$value'), ...ids], update), b.arrow([...ids], bind_this_id)];
if (each_ids.size) {
args.push(b.thunk(b.array(Array.from(each_ids.values()).map((id) => id[1]))));
}
return statement;
return b.call('$.bind_this', ...args);
}
/**
@ -1123,7 +1183,7 @@ function create_block(parent, name, nodes, context) {
trimmed.every((node) => node.type === 'Text' || node.type === 'ExpressionTag');
if (use_space_template) {
// special case — we can use `$.space` instead of creating a unique template
// special case — we can use `$.space_frag` instead of creating a unique template
const id = b.id(context.state.scope.generate('text'));
process_children(trimmed, () => id, false, {
@ -1131,7 +1191,7 @@ function create_block(parent, name, nodes, context) {
state
});
body.push(b.var(id, b.call('$.space', b.id('$$anchor'))), ...state.init);
body.push(b.var(id, b.call('$.space_frag', b.id('$$anchor'))), ...state.init);
close = b.stmt(b.call('$.close', b.id('$$anchor'), id));
} else {
/** @type {(is_text: boolean) => import('estree').Expression} */
@ -1302,6 +1362,7 @@ function serialize_event_handler(node, { state, visit }) {
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.declaration_kind === 'import' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
@ -1491,7 +1552,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
state.template.push(' ');
const text_id = get_node_id(expression(true), state, 'text');
const text_id = get_node_id(b.call('$.space', expression(true)), state, 'text');
const singular = b.stmt(
b.call(
@ -2068,7 +2129,7 @@ export const template_visitors = {
node.fragment.nodes,
context.path,
child_metadata.namespace,
state.preserve_whitespace,
node.name === 'script' || state.preserve_whitespace,
state.options.preserveComments
);
@ -2255,8 +2316,8 @@ export const template_visitors = {
each_type |= EACH_IS_CONTROLLED;
}
if (context.state.analysis.immutable) {
each_type |= EACH_IS_IMMUTABLE;
if (context.state.analysis.runes) {
each_type |= EACH_IS_STRICT_EQUALS;
}
// Find the parent each blocks which contain the arrays to invalidate
@ -2736,15 +2797,7 @@ export const template_visitors = {
}
case 'this':
call_expr = b.call(
`$.bind_this`,
state.node,
setter,
/** @type {import('estree').Expression} */ (
// if expression is not an identifier, we know it can't be a signal
expression.type === 'Identifier' ? expression : undefined
)
);
call_expr = serialize_bind_this(node.expression, context, state.node);
break;
case 'textContent':
case 'innerHTML':

@ -3,7 +3,18 @@ import { walk } from 'zimmerframe';
import { is_keyframes_node, regex_css_name_boundary, remove_css_prefix } from '../../css.js';
import { merge_with_preprocessor_map } from '../../../utils/mapped_code.js';
/** @typedef {{ code: MagicString, dev: boolean, hash: string, selector: string, keyframes: string[] }} State */
/**
* @typedef {{
* code: MagicString;
* dev: boolean;
* hash: string;
* selector: string;
* keyframes: string[];
* specificity: {
* bumped: boolean
* }
* }} State
*/
/**
*
@ -20,7 +31,10 @@ export function render_stylesheet(source, analysis, options) {
dev: options.dev,
hash: analysis.css.hash,
selector: `.${analysis.css.hash}`,
keyframes: analysis.css.keyframes
keyframes: analysis.css.keyframes,
specificity: {
bumped: false
}
};
const ast = /** @type {import('#compiler').Css.StyleSheet} */ (analysis.css.ast);
@ -112,9 +126,7 @@ const visitors = {
return;
}
const used = node.prelude.children.filter((s) => s.metadata.used);
if (used.length === 0) {
if (!is_used(node)) {
state.code.prependRight(node.start, '/* (unused) ');
state.code.appendLeft(node.end, '*/');
escape_comment_close(node, state.code);
@ -122,12 +134,14 @@ const visitors = {
return;
}
if (used.length < node.prelude.children.length) {
next();
},
SelectorList(node, { state, next, path }) {
let pruning = false;
let last = node.prelude.children[0].start;
let last = node.children[0].start;
for (let i = 0; i < node.prelude.children.length; i += 1) {
const selector = node.prelude.children[i];
for (let i = 0; i < node.children.length; i += 1) {
const selector = node.children[i];
if (selector.metadata.used === pruning) {
if (pruning) {
@ -152,9 +166,29 @@ const visitors = {
if (pruning) {
state.code.appendLeft(last, '*/');
}
// if we're in a `:is(...)` or whatever, keep existing specificity bump state
let specificity = state.specificity;
// if this selector list belongs to a rule, require a specificity bump for the
// first scoped selector but only if we're at the top level
let parent = path.at(-1);
if (parent?.type === 'Rule') {
specificity = { bumped: false };
/** @type {import('#compiler').Css.Rule | null} */
let rule = parent.metadata.parent_rule;
while (rule) {
if (rule.metadata.has_local_selectors) {
specificity = { bumped: true };
break;
}
rule = rule.metadata.parent_rule;
}
}
next();
next({ ...state, specificity });
},
ComplexSelector(node, context) {
/** @param {import('#compiler').Css.SimpleSelector} selector */
@ -164,21 +198,35 @@ const visitors = {
.remove(selector.end - 1, selector.end);
}
let first = true;
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]);
continue;
}
if (relative_selector.metadata.scoped) {
if (relative_selector.selectors.length === 1) {
// skip standalone :is/:where/& selectors
const selector = relative_selector.selectors[0];
if (
selector.type === 'PseudoClassSelector' &&
(selector.name === 'is' || selector.name === 'where')
) {
continue;
}
}
if (relative_selector.selectors.every((s) => s.type === 'NestingSelector')) {
continue;
}
// for the first occurrence, we use a classname selector, so that every
// encapsulated selector gets a +0-1-0 specificity bump. thereafter,
// we use a `:where` selector, which does not affect specificity
let modifier = context.state.selector;
if (!first) modifier = `:where(${modifier})`;
if (context.state.specificity.bumped) modifier = `:where(${modifier})`;
first = false;
context.state.specificity.bumped = true;
// TODO err... can this happen?
for (const selector of relative_selector.selectors) {
@ -209,20 +257,54 @@ const visitors = {
break;
}
first = false;
}
}
context.next();
},
PseudoClassSelector(node, context) {
if (node.name === 'is' || node.name === 'where') {
context.next();
}
}
};
/** @param {import('#compiler').Css.Rule} rule */
function is_empty(rule) {
if (rule.block.children.length > 0) return false;
for (const child of rule.block.children) {
if (child.type === 'Declaration') {
return false;
}
if (child.type === 'Rule') {
if (is_used(child) && !is_empty(child)) return false;
}
if (child.type === 'Atrule') {
return false; // TODO
}
}
return true;
}
/** @param {import('#compiler').Css.Rule} rule */
function is_used(rule) {
for (const selector of rule.prelude.children) {
if (selector.metadata.used) return true;
}
for (const child of rule.block.children) {
if (child.type === 'Rule' && is_used(child)) return true;
if (child.type === 'Atrule') {
return true; // TODO
}
}
return false;
}
/**
*
* @param {import('#compiler').Css.Rule} node

@ -32,10 +32,11 @@ function t_string(value) {
/**
* @param {import('estree').Expression} value
* @param {boolean} [needs_escaping]
* @returns {import('./types').TemplateExpression}
*/
function t_expression(value) {
return { type: 'expression', value };
function t_expression(value, needs_escaping = false) {
return { type: 'expression', value, needs_escaping };
}
/**
@ -94,7 +95,8 @@ function serialize_template(template, out = b.id('out')) {
} else if (template_item.type === 'expression') {
const value = template_item.value;
if (value.type === 'TemplateLiteral') {
last.value.raw += sanitize_template_string(value.quasis[0].value.raw);
const raw = value.quasis[0].value.raw;
last.value.raw += template_item.needs_escaping ? sanitize_template_string(raw) : raw;
quasis.push(...value.quasis.slice(1));
expressions.push(...value.expressions);
continue;
@ -198,7 +200,7 @@ function process_children(nodes, parent, { visit, state }) {
}
}
state.template.push(t_expression(b.template(quasis, expressions)));
state.template.push(t_expression(b.template(quasis, expressions), true));
}
for (let i = 0; i < nodes.length; i += 1) {
@ -322,7 +324,7 @@ function serialize_get_binding(node, state) {
const store_id = b.id(node.name.slice(1));
return b.call(
'$.store_get',
b.id('$$store_subs'),
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(node.name),
serialize_get_binding(store_id, state)
);
@ -460,7 +462,7 @@ function serialize_set_binding(node, context, fallback) {
} else if (is_store) {
return b.call(
'$.mutate_store',
b.id('$$store_subs'),
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(left.name),
b.id(left_name),
b.assignment(node.operator, /** @type {import('estree').Pattern} */ (visit(node.left)), value)
@ -506,7 +508,11 @@ const global_visitors = {
if (node.prefix) fn += '_pre';
/** @type {import('estree').Expression[]} */
const args = [b.id('$$store_subs'), b.literal(argument.name), b.id(argument.name.slice(1))];
const args = [
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(argument.name),
b.id(argument.name.slice(1))
];
if (node.operator === '--') {
args.push(b.literal(-1));
}
@ -849,7 +855,7 @@ function serialize_inline_component(node, component_name, context) {
const value = serialize_attribute_value(attribute.value, context, false, true);
push_prop(b.prop('init', b.key(attribute.name), value));
} else if (attribute.type === 'BindDirective') {
} else if (attribute.type === 'BindDirective' && attribute.name !== 'this') {
// TODO this needs to turn the whole thing into a while loop because the binding could be mutated eagerly in the child
push_prop(
b.get(attribute.name, [
@ -2094,8 +2100,10 @@ export function server_component(analysis, options) {
(binding) => binding.kind === 'store_sub'
)
) {
instance.body.unshift(b.const('$$store_subs', b.object([])));
template.body.push(b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs'))));
instance.body.unshift(b.var('$$store_subs'));
template.body.push(
b.if(b.id('$$store_subs'), b.stmt(b.call('$.unsubscribe_stores', b.id('$$store_subs'))))
);
}
// Propagate values of bound props upwards if they're undefined in the parent and have a value.
// Don't do this as part of the props retrieval because people could eagerly mutate the prop in the instance script.

@ -12,6 +12,7 @@ import type { ComponentAnalysis } from '../../types.js';
export type TemplateExpression = {
type: 'expression';
value: Expression;
needs_escaping: boolean;
};
export type TemplateString = {

@ -331,10 +331,14 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @type {import('zimmerframe').Visitor<import('#compiler').Directive, State, import('#compiler').SvelteNode>}
* @type {import('zimmerframe').Visitor<import('#compiler').AnimateDirective | import('#compiler').TransitionDirective | import('#compiler').UseDirective, State, import('#compiler').SvelteNode>}
*/
const SvelteDirective = (node, context) => {
context.state.scope.reference(b.id(node.name), context.path);
const SvelteDirective = (node, { state, path, visit }) => {
state.scope.reference(b.id(node.name.split('.')[0]), path);
if (node.expression) {
visit(node.expression);
}
};
walk(ast, state, {

@ -25,6 +25,10 @@ export interface Rule extends BaseNode {
type: 'Rule';
prelude: SelectorList;
block: Block;
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
};
}
export interface SelectorList extends BaseNode {
@ -36,6 +40,7 @@ export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
children: RelativeSelector[];
metadata: {
rule: null | Rule;
used: boolean;
};
}
@ -91,6 +96,11 @@ export interface Percentage extends BaseNode {
value: string;
}
export interface NestingSelector extends BaseNode {
type: 'NestingSelector';
name: '&';
}
export interface Nth extends BaseNode {
type: 'Nth';
value: string;
@ -104,7 +114,8 @@ export type SimpleSelector =
| PseudoElementSelector
| PseudoClassSelector
| Percentage
| Nth;
| Nth
| NestingSelector;
export interface Combinator extends BaseNode {
type: 'Combinator';
@ -127,6 +138,8 @@ export type Node =
| StyleSheet
| Rule
| Atrule
| SelectorList
| Block
| ComplexSelector
| RelativeSelector
| Combinator

@ -5,7 +5,7 @@ export const EACH_KEYED = 1 << 2;
/** See EachBlock interface metadata.is_controlled for an explanation what this is */
export const EACH_IS_CONTROLLED = 1 << 3;
export const EACH_IS_ANIMATED = 1 << 4;
export const EACH_IS_IMMUTABLE = 1 << 6;
export const EACH_IS_STRICT_EQUALS = 1 << 6;
export const PROPS_IS_IMMUTABLE = 1;
export const PROPS_IS_RUNES = 1 << 1;

@ -1,16 +1,12 @@
import {
ROOT_BLOCK,
HEAD_BLOCK,
DYNAMIC_ELEMENT_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
SNIPPET_BLOCK
} from './constants.js';
import { current_block } from './runtime.js';
export const ROOT_BLOCK = 0;
export const IF_BLOCK = 1;
export const EACH_BLOCK = 2;
export const EACH_ITEM_BLOCK = 3;
export const AWAIT_BLOCK = 4;
export const KEY_BLOCK = 5;
export const HEAD_BLOCK = 6;
export const DYNAMIC_COMPONENT_BLOCK = 7;
export const DYNAMIC_ELEMENT_BLOCK = 8;
export const SNIPPET_BLOCK = 9;
/**
* @param {boolean} intro
* @returns {import('./types.js').RootBlock}
@ -32,48 +28,6 @@ export function create_root_block(intro) {
};
}
/** @returns {import('./types.js').IfBlock} */
export function create_if_block() {
return {
// alternate transitions
a: null,
// alternate effect
ae: null,
// consequent transitions
c: null,
// consequent effect
ce: null,
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// type
t: IF_BLOCK,
// value
v: false
};
}
/** @returns {import('./types.js').KeyBlock} */
export function create_key_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// type
t: KEY_BLOCK
};
}
/** @returns {import('./types.js').HeadBlock} */
export function create_head_block() {
return {
@ -122,82 +76,6 @@ export function create_dynamic_component_block() {
};
}
/** @returns {import('./types.js').AwaitBlock} */
export function create_await_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('./types.js').Block} */ (current_block),
// pending
n: true,
// transition
r: null,
// type
t: AWAIT_BLOCK
};
}
/**
* @param {number} flags
* @param {Element | Comment} anchor
* @returns {import('./types.js').EachBlock}
*/
export function create_each_block(flags, anchor) {
return {
// anchor
a: anchor,
// dom
d: null,
// flags
f: flags,
// items
v: [],
// effect
e: null,
p: /** @type {import('./types.js').Block} */ (current_block),
// transition
r: null,
// transitions
s: [],
// type
t: EACH_BLOCK
};
}
/**
* @param {any | import('./types.js').Signal<any>} item
* @param {number | import('./types.js').Signal<number>} index
* @param {null | unknown} key
* @returns {import('./types.js').EachItemBlock}
*/
export function create_each_item_block(item, index, key) {
return {
// animate transition
a: null,
// dom
d: null,
// effect
e: null,
// index
i: index,
// key
k: key,
// item
v: item,
// parent
p: /** @type {import('./types.js').EachBlock} */ (current_block),
// transition
r: null,
// transitions
s: null,
// type
t: EACH_ITEM_BLOCK
};
}
/** @returns {import('./types.js').SnippetBlock} */
export function create_snippet_block() {
return {

@ -0,0 +1,26 @@
export const SOURCE = 1;
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const PRE_EFFECT = 1 << 3;
export const RENDER_EFFECT = 1 << 4;
export const MANAGED = 1 << 6;
export const UNOWNED = 1 << 7;
export const CLEAN = 1 << 8;
export const DIRTY = 1 << 9;
export const MAYBE_DIRTY = 1 << 10;
export const INERT = 1 << 11;
export const DESTROYED = 1 << 12;
export const ROOT_BLOCK = 0;
export const IF_BLOCK = 1;
export const EACH_BLOCK = 2;
export const EACH_ITEM_BLOCK = 3;
export const AWAIT_BLOCK = 4;
export const KEY_BLOCK = 5;
export const HEAD_BLOCK = 6;
export const DYNAMIC_COMPONENT_BLOCK = 7;
export const DYNAMIC_ELEMENT_BLOCK = 8;
export const SNIPPET_BLOCK = 9;
export const UNINITIALIZED = Symbol();
export const STATE_SYMBOL = Symbol('$state');

@ -1,5 +1,6 @@
import { createClassComponent } from '../../legacy/legacy-client.js';
import { render_effect, destroy_signal } from './runtime.js';
import { destroy_signal } from './runtime.js';
import { render_effect } from './reactivity/computations.js';
import { open, close } from './render.js';
import { define_property } from './utils.js';
@ -276,7 +277,7 @@ function get_custom_elements_slots(element) {
* @param {any} Component A Svelte component function
* @param {Record<string, CustomElementPropDefinition>} props_definition The props to observe
* @param {string[]} slots The slots to create
* @param {string[]} accessors Other accessors besides the ones for props the component has
* @param {string[]} exports Explicitly exported values, other than props
* @param {boolean} use_shadow_dom Whether to use shadow DOM
* @param {(ce: new () => HTMLElement) => new () => HTMLElement} [extend]
*/
@ -284,7 +285,7 @@ export function create_custom_element(
Component,
props_definition,
slots,
accessors,
exports,
use_shadow_dom,
extend
) {
@ -311,10 +312,10 @@ export function create_custom_element(
}
});
});
accessors.forEach((accessor) => {
define_property(Class.prototype, accessor, {
exports.forEach((property) => {
define_property(Class.prototype, property, {
get() {
return this.$$c?.[accessor];
return this.$$c?.[property];
}
});
});

@ -0,0 +1,154 @@
/** @typedef {{ file: string, line: number, column: number }} Location */
import { STATE_SYMBOL } from '../constants.js';
import { untrack } from '../runtime.js';
/** @type {Record<string, Array<{ start: Location, end: Location, component: Function }>>} */
const boundaries = {};
const chrome_pattern = /at (?:.+ \()?(.+):(\d+):(\d+)\)?$/;
const firefox_pattern = /@(.+):(\d+):(\d+)$/;
export function get_stack() {
const stack = new Error().stack;
if (!stack) return null;
const entries = [];
for (const line of stack.split('\n')) {
let match = chrome_pattern.exec(line) ?? firefox_pattern.exec(line);
if (match) {
entries.push({
file: match[1],
line: +match[2],
column: +match[3]
});
}
}
return entries;
}
/**
* Determines which `.svelte` component is responsible for a given state change
* @returns {Function | null}
*/
export function get_component() {
const stack = get_stack();
if (!stack) return null;
for (const entry of stack) {
const modules = boundaries[entry.file];
if (!modules) continue;
for (const module of modules) {
if (module.start.line < entry.line && module.end.line > entry.line) {
return module.component;
}
}
}
return null;
}
/**
* Together with `mark_module_end`, this function establishes the boundaries of a `.svelte` file,
* such that subsequent calls to `get_component` can tell us which component is responsible
* for a given state change
* @param {Function} component
*/
export function mark_module_start(component) {
const start = get_stack()?.[2];
if (start) {
(boundaries[start.file] ??= []).push({
start,
// @ts-expect-error
end: null,
component
});
}
}
export function mark_module_end() {
const end = get_stack()?.[2];
if (end) {
// @ts-expect-error
boundaries[end.file].at(-1).end = end;
}
}
/**
*
* @param {any} object
* @param {any} owner
*/
export function add_owner(object, owner) {
untrack(() => {
add_owner_to_object(object, owner);
});
}
/**
* @param {any} object
* @param {Function} owner
*/
function add_owner_to_object(object, owner) {
if (object?.[STATE_SYMBOL]?.o && !object[STATE_SYMBOL].o.has(owner)) {
object[STATE_SYMBOL].o.add(owner);
for (const key in object) {
add_owner_to_object(object[key], owner);
}
}
}
/**
* @param {any} object
*/
export function strip_owner(object) {
untrack(() => {
strip_owner_from_object(object);
});
}
/**
* @param {any} object
*/
function strip_owner_from_object(object) {
if (object?.[STATE_SYMBOL]?.o) {
object[STATE_SYMBOL].o = null;
for (const key in object) {
strip_owner(object[key]);
}
}
}
/**
* @param {Set<Function>} owners
*/
export function check_ownership(owners) {
const component = get_component();
if (component && !owners.has(component)) {
let original = [...owners][0];
let message =
// @ts-expect-error
original.filename !== component.filename
? // @ts-expect-error
`${component.filename} mutated a value owned by ${original.filename}. This is strongly discouraged`
: 'Mutating a value outside the component that created it is strongly discouraged';
// eslint-disable-next-line no-console
console.warn(
`${message}. Consider passing values to child components with \`bind:\`, or use a callback instead.`
);
// eslint-disable-next-line no-console
console.trace();
}
}

@ -0,0 +1,200 @@
import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../../hydration.js';
import { remove } from '../../reconciler.js';
import {
current_block,
destroy_signal,
execute_effect,
flushSync,
push_destroy_fn
} from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { trigger_transitions } from '../../transitions.js';
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js';
/** @returns {import('../../types.js').AwaitBlock} */
export function create_await_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('../../types.js').Block} */ (current_block),
// pending
n: true,
// transition
r: null,
// type
t: AWAIT_BLOCK
};
}
/**
* @template V
* @param {Comment} anchor_node
* @param {(() => Promise<V>)} input
* @param {null | ((anchor: Node) => void)} pending_fn
* @param {null | ((anchor: Node, value: V) => void)} then_fn
* @param {null | ((anchor: Node, error: unknown) => void)} catch_fn
* @returns {void}
*/
export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
const block = create_await_block();
/** @type {null | import('../../types.js').Render} */
let current_render = null;
hydrate_block_anchor(anchor_node);
/** @type {{}} */
let latest_token;
/** @type {typeof UNINITIALIZED | V} */
let resolved_value = UNINITIALIZED;
/** @type {unknown} */
let error = UNINITIALIZED;
let pending = false;
block.r =
/**
* @param {import('../../types.js').Transition} transition
* @returns {void}
*/
(transition) => {
const render = /** @type {import('../../types.js').Render} */ (current_render);
const transitions = render.s;
transitions.add(transition);
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
destroy_signal(render.e);
render.e = null;
}
}
});
};
const create_render_effect = () => {
/** @type {import('../../types.js').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
const effect = render_effect(
() => {
if (error === UNINITIALIZED) {
if (resolved_value === UNINITIALIZED) {
// pending = true
block.n = true;
if (pending_fn !== null) {
pending_fn(anchor_node);
}
} else if (then_fn !== null) {
// pending = false
block.n = false;
then_fn(anchor_node, resolved_value);
}
} else if (catch_fn !== null) {
// pending = false
block.n = false;
catch_fn(anchor_node, error);
}
render.d = block.d;
block.d = null;
},
block,
true,
true
);
render.e = effect;
current_render = render;
};
const render = () => {
const render = current_render;
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
if (render.e) {
execute_effect(render.e);
} else {
create_render_effect();
}
} else {
create_render_effect();
trigger_transitions(transitions, 'out');
}
};
const await_effect = render_effect(
() => {
const token = {};
latest_token = token;
const promise = input();
if (is_promise(promise)) {
promise.then(
/** @param {V} v */
(v) => {
if (latest_token === token) {
// Ensure UI is in sync before resolving value.
flushSync();
resolved_value = v;
pending = false;
render();
}
},
/** @param {unknown} _error */
(_error) => {
error = _error;
pending = false;
render();
}
);
if (resolved_value !== UNINITIALIZED || error !== UNINITIALIZED) {
error = UNINITIALIZED;
resolved_value = UNINITIALIZED;
}
if (!pending) {
pending = true;
render();
}
} else {
error = UNINITIALIZED;
resolved_value = promise;
pending = false;
render();
}
},
block,
false
);
push_destroy_fn(await_effect, () => {
let render = current_render;
latest_token = {};
while (render !== null) {
const dom = render.d;
if (dom !== null) {
remove(dom);
}
const effect = render.e;
if (effect !== null) {
destroy_signal(effect);
}
render = render.p;
}
});
block.e = await_effect;
}

@ -2,37 +2,94 @@ import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
EACH_IS_CONTROLLED,
EACH_IS_IMMUTABLE,
EACH_IS_STRICT_EQUALS,
EACH_ITEM_REACTIVE,
EACH_KEYED
} from '../../constants.js';
import { create_each_block, create_each_item_block } from './block.js';
} from '../../../../constants.js';
import { noop } from '../../../common.js';
import {
current_hydration_fragment,
get_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from './hydration.js';
import { clear_text_content, empty, map_get, map_set } from './operations.js';
import { insert, remove } from './reconciler.js';
} from '../../hydration.js';
import { clear_text_content, empty, map_get, map_set } from '../../operations.js';
import { insert, remove } from '../../reconciler.js';
import {
current_block,
destroy_signal,
execute_effect,
mutable_source,
push_destroy_fn,
render_effect,
set_signal_value,
source
} from './runtime.js';
import { trigger_transitions } from './transitions.js';
import { is_array } from './utils.js';
set_signal_value
} from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { source, mutable_source } from '../../reactivity/sources.js';
import { trigger_transitions } from '../../transitions.js';
import { is_array } from '../../utils.js';
import { EACH_BLOCK, EACH_ITEM_BLOCK } from '../../constants.js';
const NEW_BLOCK = -1;
const MOVED_BLOCK = 99999999;
const LIS_BLOCK = -2;
function no_op() {}
/**
* @param {number} flags
* @param {Element | Comment} anchor
* @returns {import('../../types.js').EachBlock}
*/
export function create_each_block(flags, anchor) {
return {
// anchor
a: anchor,
// dom
d: null,
// flags
f: flags,
// items
v: [],
// effect
e: null,
p: /** @type {import('../../types.js').Block} */ (current_block),
// transition
r: null,
// transitions
s: [],
// type
t: EACH_BLOCK
};
}
/**
* @param {any | import('../../types.js').Signal<any>} item
* @param {number | import('../../types.js').Signal<number>} index
* @param {null | unknown} key
* @returns {import('../../types.js').EachItemBlock}
*/
export function create_each_item_block(item, index, key) {
return {
// animate transition
a: null,
// dom
d: null,
// effect
e: null,
// index
i: index,
// key
k: key,
// item
v: item,
// parent
p: /** @type {import('../../types.js').EachBlock} */ (current_block),
// transition
r: null,
// transitions
s: null,
// type
t: EACH_ITEM_BLOCK
};
}
/**
* @template V
@ -40,7 +97,7 @@ function no_op() {}
* @param {() => V[]} collection
* @param {number} flags
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
@ -49,7 +106,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
const block = create_each_block(flags, anchor_node);
/** @type {null | import('./types.js').Render} */
/** @type {null | import('../../types.js').Render} */
let current_fallback = null;
hydrate_block_anchor(anchor_node, is_controlled);
@ -59,7 +116,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
/** @type {Array<string> | null} */
let keys = null;
/** @type {null | import('./types.js').EffectSignal} */
/** @type {null | import('../../types.js').EffectSignal} */
let render = null;
/**
@ -69,9 +126,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
let mismatch = false;
block.r =
/** @param {import('./types.js').Transition} transition */
/** @param {import('../../types.js').Transition} transition */
(transition) => {
const fallback = /** @type {import('./types.js').Render} */ (current_fallback);
const fallback = /** @type {import('../../types.js').Render} */ (current_fallback);
const transitions = fallback.s;
transitions.add(transition);
transition.f(() => {
@ -90,7 +147,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
};
const create_fallback_effect = () => {
/** @type {import('./types.js').Render} */
/** @type {import('../../types.js').Render} */
const fallback = {
d: null,
e: null,
@ -131,7 +188,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
current_fallback = fallback;
};
/** @param {import('./types.js').EachBlock} block */
/** @param {import('../../types.js').EachBlock} block */
const render_each = (block) => {
const flags = block.f;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -152,7 +209,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
if (key_fn !== null) {
keys = array.map(key_fn);
} else if ((flags & EACH_KEYED) === 0) {
array.map(no_op);
array.map(noop);
}
const length = array.length;
@ -168,7 +225,9 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
mismatch = true;
} else if (is_each_else_comment) {
// Remove the each_else comment node or else it will confuse the subsequent hydration algorithm
/** @type {import('./types.js').TemplateNode[]} */ (current_hydration_fragment).shift();
/** @type {import('../../types.js').TemplateNode[]} */ (
current_hydration_fragment
).shift();
}
}
@ -226,7 +285,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
}
// Clear the array
reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys);
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (render));
destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (render));
});
block.e = each;
@ -238,7 +297,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
* @param {() => V[]} collection
* @param {number} flags
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
@ -251,7 +310,7 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa
* @param {Element | Comment} anchor_node
* @param {() => V[]} collection
* @param {number} flags
* @param {(anchor: null, item: V, index: import('./types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
@ -262,10 +321,10 @@ export function each_indexed(anchor_node, collection, flags, render_fn, fallback
/**
* @template V
* @param {Array<V>} array
* @param {import('./types.js').EachBlock} each_block
* @param {import('../../types.js').EachBlock} each_block
* @param {Element | Comment | Text} dom
* @param {boolean} is_controlled
* @param {(anchor: null, item: V, index: number | import('./types.js').Signal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn
* @param {number} flags
* @param {boolean} apply_transitions
* @returns {void}
@ -290,7 +349,7 @@ function reconcile_indexed_array(
var length = Math.max(a, b);
var index = 0;
/** @type {Array<import('./types.js').EachItemBlock>} */
/** @type {Array<import('../../types.js').EachItemBlock>} */
var b_blocks;
var block;
@ -315,7 +374,7 @@ function reconcile_indexed_array(
b_blocks = Array(b);
if (hydrating) {
// Hydrate block
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
var hydration_list = /** @type {import('../../types.js').TemplateNode[]} */ (
current_hydration_fragment
);
var hydrating_node = hydration_list[0];
@ -333,7 +392,7 @@ function reconcile_indexed_array(
block = each_item_block(item, null, index, render_fn, flags);
b_blocks[index] = block;
hydrating_node = /** @type {import('./types.js').TemplateNode} */ (
hydrating_node = /** @type {import('../../types.js').TemplateNode} */ (
/** @type {Node} */ (/** @type {Node} */ (fragment.at(-1)).nextSibling).nextSibling
);
}
@ -376,10 +435,10 @@ function reconcile_indexed_array(
* https://github.com/localvoid/ivi/blob/9f1bd0918f487da5b131941228604763c5d8ef56/packages/ivi/src/client/core.ts#L968
* @template V
* @param {Array<V>} array
* @param {import('./types.js').EachBlock} each_block
* @param {import('../../types.js').EachBlock} each_block
* @param {Element | Comment | Text} dom
* @param {boolean} is_controlled
* @param {(anchor: null, item: V, index: number | import('./types.js').Signal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn
* @param {number} flags
* @param {boolean} apply_transitions
* @param {Array<string> | null} keys
@ -405,7 +464,7 @@ function reconcile_tracked_array(
/** @type {number} */
var b = array.length;
/** @type {Array<import('./types.js').EachItemBlock>} */
/** @type {Array<import('../../types.js').EachItemBlock>} */
var b_blocks;
var block;
@ -435,7 +494,7 @@ function reconcile_tracked_array(
if (hydrating) {
// Hydrate block
var fragment;
var hydration_list = /** @type {import('./types.js').TemplateNode[]} */ (
var hydration_list = /** @type {import('../../types.js').TemplateNode[]} */ (
current_hydration_fragment
);
var hydrating_node = hydration_list[0];
@ -457,7 +516,7 @@ function reconcile_tracked_array(
// Get the <!--ssr:..--> tag of the next item in the list
// The fragment array can be empty if each block has no content
hydrating_node = /** @type {import('./types.js').TemplateNode} */ (
hydrating_node = /** @type {import('../../types.js').TemplateNode} */ (
/** @type {Node} */ ((fragment.at(-1) || hydrating_node).nextSibling).nextSibling
);
}
@ -610,8 +669,8 @@ function reconcile_tracked_array(
/**
* The server could have rendered more list items than the client specifies.
* In that case, we need to remove the remaining server-rendered nodes.
* @param {import('./types.js').TemplateNode[]} hydration_list
* @param {import('./types.js').TemplateNode | null} next_node
* @param {import('../../types.js').TemplateNode[]} hydration_list
* @param {import('../../types.js').TemplateNode | null} next_node
*/
function remove_excess_hydration_nodes(hydration_list, next_node) {
if (next_node === null) return;
@ -695,14 +754,14 @@ function mark_lis(a) {
}
/**
* @param {import('./types.js').Block} block
* @param {import('../../types.js').Block} block
* @param {Element | Comment | Text} dom
* @param {boolean} is_controlled
* @param {null | Text | Element | Comment} sibling
* @returns {Text | Element | Comment}
*/
function insert_each_item_block(block, dom, is_controlled, sibling) {
var current = /** @type {import('./types.js').TemplateNode} */ (block.d);
var current = /** @type {import('../../types.js').TemplateNode} */ (block.d);
if (sibling === null) {
if (is_controlled) {
@ -716,7 +775,7 @@ function insert_each_item_block(block, dom, is_controlled, sibling) {
}
/**
* @param {import('./types.js').Block} block
* @param {import('../../types.js').Block} block
* @returns {Text | Element | Comment}
*/
function get_first_child(block) {
@ -730,7 +789,7 @@ function get_first_child(block) {
}
/**
* @param {Array<import('./types.js').EachItemBlock>} active_transitions
* @param {Array<import('../../types.js').EachItemBlock>} active_transitions
* @returns {void}
*/
function destroy_active_transition_blocks(active_transitions) {
@ -755,7 +814,7 @@ function destroy_active_transition_blocks(active_transitions) {
}
/**
* @param {import('./types.js').Block} block
* @param {import('../../types.js').Block} block
* @returns {Text | Element | Comment}
*/
export function get_first_element(block) {
@ -774,7 +833,7 @@ export function get_first_element(block) {
}
/**
* @param {import('./types.js').EachItemBlock} block
* @param {import('../../types.js').EachItemBlock} block
* @param {any} item
* @param {number} index
* @param {number} type
@ -793,15 +852,15 @@ function update_each_item_block(block, item, index, type) {
each_animation(block, transitions);
}
if (index_is_reactive) {
set_signal_value(/** @type {import('./types.js').Signal<number>} */ (block.i), index);
set_signal_value(/** @type {import('../../types.js').Signal<number>} */ (block.i), index);
} else {
block.i = index;
}
}
/**
* @param {import('./types.js').EachItemBlock} block
* @param {null | Array<import('./types.js').Block>} transition_block
* @param {import('../../types.js').EachItemBlock} block
* @param {null | Array<import('../../types.js').Block>} transition_block
* @param {boolean} apply_transitions
* @param {any} controlled
* @returns {void}
@ -835,7 +894,7 @@ export function destroy_each_item_block(
if (!controlled && dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').EffectSignal} */ (block.e));
destroy_signal(/** @type {import('../../types.js').EffectSignal} */ (block.e));
}
/**
@ -843,24 +902,24 @@ export function destroy_each_item_block(
* @param {V} item
* @param {unknown} key
* @param {number} index
* @param {(anchor: null, item: V, index: number | import('./types.js').Signal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: number | import('../../types.js').Signal<number>) => void} render_fn
* @param {number} flags
* @returns {import('./types.js').EachItemBlock}
* @returns {import('../../types.js').EachItemBlock}
*/
function each_item_block(item, key, index, render_fn, flags) {
const each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0;
const item_value = each_item_not_reactive
? item
: (flags & EACH_IS_IMMUTABLE) === 0
? mutable_source(item)
: source(item);
: (flags & EACH_IS_STRICT_EQUALS) !== 0
? source(item)
: mutable_source(item);
const index_value = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
const block = create_each_item_block(item_value, index_value, key);
const effect = render_effect(
/** @param {import('./types.js').EachItemBlock} block */
/** @param {import('../../types.js').EachItemBlock} block */
(block) => {
render_fn(null, block.v, block.i);
},

@ -0,0 +1,181 @@
import { IF_BLOCK } from '../../constants.js';
import {
current_hydration_fragment,
hydrate_block_anchor,
hydrating,
set_current_hydration_fragment
} from '../../hydration.js';
import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { trigger_transitions } from '../../transitions.js';
/** @returns {import('../../types.js').IfBlock} */
function create_if_block() {
return {
// alternate transitions
a: null,
// alternate effect
ae: null,
// consequent transitions
c: null,
// consequent effect
ce: null,
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('../../types.js').Block} */ (current_block),
// transition
r: null,
// type
t: IF_BLOCK,
// value
v: false
};
}
/**
* @param {Comment} anchor_node
* @param {() => boolean} condition_fn
* @param {(anchor: Node) => void} consequent_fn
* @param {null | ((anchor: Node) => void)} alternate_fn
* @returns {void}
*/
export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) {
const block = create_if_block();
hydrate_block_anchor(anchor_node);
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;
/** @type {null | import('../../types.js').TemplateNode | Array<import('../../types.js').TemplateNode>} */
let consequent_dom = null;
/** @type {null | import('../../types.js').TemplateNode | Array<import('../../types.js').TemplateNode>} */
let alternate_dom = null;
let has_mounted = false;
/**
* @type {import('../../types.js').EffectSignal | null}
*/
let current_branch_effect = null;
const if_effect = render_effect(
() => {
const result = !!condition_fn();
if (block.v !== result || !has_mounted) {
block.v = result;
if (has_mounted) {
const consequent_transitions = block.c;
const alternate_transitions = block.a;
if (result) {
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'out');
}
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'in');
}
} else {
if (consequent_transitions === null || consequent_transitions.size === 0) {
execute_effect(consequent_effect);
} else {
trigger_transitions(consequent_transitions, 'out');
}
if (alternate_transitions === null || alternate_transitions.size === 0) {
execute_effect(alternate_effect);
} else {
trigger_transitions(alternate_transitions, 'in');
}
}
} else if (hydrating) {
const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data;
if (
!comment_text ||
(comment_text === 'ssr:if:true' && !result) ||
(comment_text === 'ssr:if:false' && result)
) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen using when `{#if browser} .. {/if}` in SvelteKit.
remove(current_hydration_fragment);
set_current_hydration_fragment(null);
mismatch = true;
} else {
// Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm
current_hydration_fragment.shift();
}
}
has_mounted = true;
}
},
block,
false
);
// Managed effect
const consequent_effect = render_effect(
(
/** @type {any} */ _,
/** @type {import('../../types.js').EffectSignal | null} */ consequent_effect
) => {
const result = block.v;
if (!result && consequent_dom !== null) {
remove(consequent_dom);
consequent_dom = null;
}
if (result && current_branch_effect !== consequent_effect) {
consequent_fn(anchor_node);
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = consequent_effect;
consequent_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ce = consequent_effect;
// Managed effect
const alternate_effect = render_effect(
(
/** @type {any} */ _,
/** @type {import('../../types.js').EffectSignal | null} */ alternate_effect
) => {
const result = block.v;
if (result && alternate_dom !== null) {
remove(alternate_dom);
alternate_dom = null;
}
if (!result && current_branch_effect !== alternate_effect) {
if (alternate_fn !== null) {
alternate_fn(anchor_node);
}
if (mismatch && current_branch_effect === null) {
// Set fragment so that Svelte continues to operate in hydration mode
set_current_hydration_fragment([]);
}
current_branch_effect = alternate_effect;
alternate_dom = block.d;
}
block.d = null;
},
block,
true
);
block.ae = alternate_effect;
push_destroy_fn(if_effect, () => {
if (consequent_dom !== null) {
remove(consequent_dom);
}
if (alternate_dom !== null) {
remove(alternate_dom);
}
destroy_signal(consequent_effect);
destroy_signal(alternate_effect);
});
block.e = if_effect;
}

@ -0,0 +1,140 @@
import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../../hydration.js';
import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/computations.js';
import { trigger_transitions } from '../../transitions.js';
import { safe_not_equal } from '../../reactivity/equality.js';
/** @returns {import('../../types.js').KeyBlock} */
function create_key_block() {
return {
// dom
d: null,
// effect
e: null,
// parent
p: /** @type {import('../../types.js').Block} */ (current_block),
// transition
r: null,
// type
t: KEY_BLOCK
};
}
/**
* @template V
* @param {Comment} anchor_node
* @param {() => V} key
* @param {(anchor: Node) => void} render_fn
* @returns {void}
*/
export function key_block(anchor_node, key, render_fn) {
const block = create_key_block();
/** @type {null | import('../../types.js').Render} */
let current_render = null;
hydrate_block_anchor(anchor_node);
/** @type {V | typeof UNINITIALIZED} */
let key_value = UNINITIALIZED;
let mounted = false;
block.r =
/**
* @param {import('../../types.js').Transition} transition
* @returns {void}
*/
(transition) => {
const render = /** @type {import('../../types.js').Render} */ (current_render);
const transitions = render.s;
transitions.add(transition);
transition.f(() => {
transitions.delete(transition);
if (transitions.size === 0) {
// If the current render has changed since, then we can remove the old render
// effect as it's stale.
if (current_render !== render && render.e !== null) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
destroy_signal(render.e);
render.e = null;
}
}
});
};
const create_render_effect = () => {
/** @type {import('../../types.js').Render} */
const render = {
d: null,
e: null,
s: new Set(),
p: current_render
};
const effect = render_effect(
() => {
render_fn(anchor_node);
render.d = block.d;
block.d = null;
},
block,
true,
true
);
render.e = effect;
current_render = render;
};
const render = () => {
const render = current_render;
if (render === null) {
create_render_effect();
return;
}
const transitions = render.s;
if (transitions.size === 0) {
if (render.d !== null) {
remove(render.d);
render.d = null;
}
if (render.e) {
execute_effect(render.e);
} else {
create_render_effect();
}
} else {
trigger_transitions(transitions, 'out');
create_render_effect();
}
};
const key_effect = render_effect(
() => {
const prev_key_value = key_value;
key_value = key();
if (mounted && safe_not_equal(prev_key_value, key_value)) {
render();
}
},
block,
false
);
// To ensure topological ordering of the key effect to the render effect,
// we trigger the effect after.
render();
mounted = true;
push_destroy_fn(key_effect, () => {
let render = current_render;
while (render !== null) {
const dom = render.d;
if (dom !== null) {
remove(dom);
}
const effect = render.e;
if (effect !== null) {
destroy_signal(effect);
}
render = render.p;
}
});
block.e = key_effect;
}

@ -0,0 +1,59 @@
import { run_all } from '../../common.js';
let is_task_queued = false;
let is_raf_queued = false;
/** @type {Array<() => void>} */
let current_queued_tasks = [];
/** @type {Array<() => void>} */
let current_raf_tasks = [];
function process_task() {
is_task_queued = false;
const tasks = current_queued_tasks.slice();
current_queued_tasks = [];
run_all(tasks);
}
function process_raf_task() {
is_raf_queued = false;
const tasks = current_raf_tasks.slice();
current_raf_tasks = [];
run_all(tasks);
}
/**
* @param {() => void} fn
* @returns {void}
*/
export function schedule_task(fn) {
if (!is_task_queued) {
is_task_queued = true;
setTimeout(process_task, 0);
}
current_queued_tasks.push(fn);
}
/**
* @param {() => void} fn
* @returns {void}
*/
export function schedule_raf_task(fn) {
if (!is_raf_queued) {
is_raf_queued = true;
requestAnimationFrame(process_raf_task);
}
current_raf_tasks.push(fn);
}
/**
* Synchronously run any queued tasks.
*/
export function flush_tasks() {
if (is_task_queued) {
process_task();
}
if (is_raf_queued) {
process_raf_task();
}
}

@ -1,7 +1,7 @@
// Handle hydration
import { schedule_task } from './dom/task.js';
import { empty } from './operations.js';
import { schedule_task } from './runtime.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out

@ -14,7 +14,10 @@ var element_prototype;
var text_prototype;
/** @type {Map<any, any>} */
var map_prototype;
var map_prototype = Map.prototype;
var map_set_method = map_prototype.set;
var map_get_method = map_prototype.get;
var map_delete_method = map_prototype.delete;
/** @type {typeof Node.prototype.appendChild} */
var append_child_method;
@ -22,15 +25,6 @@ var append_child_method;
/** @type {typeof Node.prototype.cloneNode} */
var clone_node_method;
/** @type {typeof Map.prototype.set} */
var map_set_method;
/** @type {typeof Map.prototype.get} */
var map_get_method;
/** @type {typeof Map.prototype.delete} */
var map_delete_method;
/** @type {(this: Node) => ChildNode | null} */
var first_child_get;
@ -65,13 +59,9 @@ export function init_operations() {
node_prototype = Node.prototype;
element_prototype = Element.prototype;
text_prototype = Text.prototype;
map_prototype = Map.prototype;
append_child_method = node_prototype.appendChild;
clone_node_method = node_prototype.cloneNode;
map_set_method = map_prototype.set;
map_get_method = map_prototype.get;
map_delete_method = map_prototype.delete;
$window = window;
$document = document;

@ -1,15 +1,13 @@
import { DEV } from 'esm-env';
import {
effect_active,
get,
set,
update,
source,
updating_derived,
UNINITIALIZED,
mutable_source,
batch_inspect
batch_inspect,
current_component_context
} from './runtime.js';
import { effect_active } from './reactivity/computations.js';
import {
array_prototype,
define_property,
@ -20,24 +18,38 @@ import {
is_frozen,
object_prototype
} from './utils.js';
export const STATE_SYMBOL = Symbol('$state');
export const READONLY_SYMBOL = Symbol('readonly');
import { add_owner, check_ownership, strip_owner } from './dev/ownership.js';
import { mutable_source, source } from './reactivity/sources.js';
import { STATE_SYMBOL, UNINITIALIZED } from './constants.js';
/**
* @template T
* @param {T} value
* @param {boolean} [immutable]
* @param {Function[]} [owners]
* @returns {import('./types.js').ProxyStateObject<T> | T}
*/
export function proxy(value, immutable = true) {
export function proxy(value, immutable = true, owners) {
if (typeof value === 'object' && value != null && !is_frozen(value)) {
// If we have an existing proxy, return it...
if (STATE_SYMBOL in value) {
const metadata = /** @type {import('./types.js').ProxyMetadata<T>} */ (value[STATE_SYMBOL]);
// ...unless the proxy belonged to a different object, because
// someone copied the state symbol using `Reflect.ownKeys(...)`
if (metadata.t === value || metadata.p === value) return metadata.p;
if (metadata.t === value || metadata.p === value) {
if (DEV) {
// update ownership
if (owners) {
for (const owner of owners) {
add_owner(value, owner);
}
} else {
strip_owner(value);
}
}
return metadata.p;
}
}
const prototype = get_prototype_of(value);
@ -59,6 +71,19 @@ export function proxy(value, immutable = true) {
enumerable: false
});
if (DEV) {
// set ownership — either of the parent proxy's owners (if provided) or,
// when calling `$.proxy(...)`, to the current component if such there be
// @ts-expect-error
value[STATE_SYMBOL].o =
owners === undefined
? current_component_context
? // @ts-expect-error
new Set([current_component_context.function])
: null
: owners && new Set(owners);
}
return proxy;
}
}
@ -95,7 +120,7 @@ function unwrap(value, already_unwrapped) {
already_unwrapped.set(value, obj);
for (const key of keys) {
if (key === STATE_SYMBOL || (DEV && key === READONLY_SYMBOL)) continue;
if (key === STATE_SYMBOL) continue;
if (descriptors[key].get) {
define_property(obj, key, descriptors[key]);
} else {
@ -130,7 +155,7 @@ const state_proxy_handler = {
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(descriptor.value, metadata.i));
if (s !== undefined) set(s, proxy(descriptor.value, metadata.i, metadata.o));
}
return Reflect.defineProperty(target, prop, descriptor);
@ -163,9 +188,6 @@ const state_proxy_handler = {
},
get(target, prop, receiver) {
if (DEV && prop === READONLY_SYMBOL) {
return Reflect.get(target, READONLY_SYMBOL);
}
if (prop === STATE_SYMBOL) {
return Reflect.get(target, STATE_SYMBOL);
}
@ -180,7 +202,7 @@ const state_proxy_handler = {
(effect_active() || updating_derived) &&
(!(prop in target) || get_descriptor(target, prop)?.writable)
) {
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i));
s = (metadata.i ? source : mutable_source)(proxy(target[prop], metadata.i, metadata.o));
metadata.s.set(prop, s);
}
@ -212,9 +234,6 @@ const state_proxy_handler = {
},
has(target, prop) {
if (DEV && prop === READONLY_SYMBOL) {
return Reflect.has(target, READONLY_SYMBOL);
}
if (prop === STATE_SYMBOL) {
return true;
}
@ -225,7 +244,7 @@ const state_proxy_handler = {
if (s !== undefined || (effect_active() && (!has || get_descriptor(target, prop)?.writable))) {
if (s === undefined) {
s = (metadata.i ? source : mutable_source)(
has ? proxy(target[prop], metadata.i) : UNINITIALIZED
has ? proxy(target[prop], metadata.i, metadata.o) : UNINITIALIZED
);
metadata.s.set(prop, s);
}
@ -238,16 +257,16 @@ const state_proxy_handler = {
},
set(target, prop, value) {
if (DEV && prop === READONLY_SYMBOL) {
target[READONLY_SYMBOL] = value;
return true;
}
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
if (s !== undefined) set(s, proxy(value, metadata.i));
if (s !== undefined) set(s, proxy(value, metadata.i, metadata.o));
const is_array = metadata.a;
const not_has = !(prop in target);
if (DEV && metadata.o) {
check_ownership(metadata.o);
}
// variable.length = value -> clear all signals with index >= value
if (is_array && prop === 'length') {
for (let i = value; i < target.length; i += 1) {
@ -292,69 +311,3 @@ if (DEV) {
throw new Error('Cannot set prototype of $state object');
};
}
/**
* Expects a value that was wrapped with `proxy` and makes it readonly.
*
* @template {Record<string | symbol, any>} T
* @template {import('./types.js').ProxyReadonlyObject<T> | T} U
* @param {U} value
* @returns {Proxy<U> | U}
*/
export function readonly(value) {
const proxy = value && value[READONLY_SYMBOL];
if (proxy) {
const metadata = value[STATE_SYMBOL];
// Check that the incoming value is the same proxy that this readonly symbol was created for:
// If someone copies over the readonly symbol to a new object (using Reflect.ownKeys) the referenced
// proxy could be stale and we should not return it.
if (metadata.p === value) return proxy;
}
if (
typeof value === 'object' &&
value != null &&
!is_frozen(value) &&
STATE_SYMBOL in value && // TODO handle Map and Set as well
!(READONLY_SYMBOL in value)
) {
const proxy = new Proxy(
value,
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject<U>>} */ (
readonly_proxy_handler
)
);
define_property(value, READONLY_SYMBOL, { value: proxy, writable: false });
return proxy;
}
return value;
}
/**
* @param {any} _
* @param {string} prop
* @returns {never}
*/
const readonly_error = (_, prop) => {
throw new Error(
`Non-bound props cannot be mutated — to make the \`${prop}\` settable, ensure the object it is used within is bound as a prop \`bind:<prop>={...}\`. Fallback values can never be mutated.`
);
};
/** @type {ProxyHandler<import('./types.js').ProxyReadonlyObject>} */
const readonly_proxy_handler = {
defineProperty: readonly_error,
deleteProperty: readonly_error,
set: readonly_error,
get(target, prop, receiver) {
const value = Reflect.get(target, prop, receiver);
if (!(prop in target)) {
return readonly(value);
}
return value;
}
};

@ -0,0 +1,258 @@
import { DEV } from 'esm-env';
import {
current_block,
current_component_context,
current_consumer,
current_effect,
destroy_signal,
flush_local_render_effects,
schedule_effect
} from '../runtime.js';
import { default_equals, safe_equal } from './equality.js';
import {
DIRTY,
MANAGED,
RENDER_EFFECT,
EFFECT,
PRE_EFFECT,
DERIVED,
UNOWNED,
CLEAN,
UNINITIALIZED
} from '../constants.js';
/**
* @template V
* @param {import('../types.js').SignalFlags} flags
* @param {V} value
* @param {import('../types.js').Block | null} block
*/
function create_computation_signal(flags, value, block) {
/** @type {import('../types.js').ComputationSignal<V>} */
const signal = {
b: block,
c: null,
d: null,
e: null,
f: flags,
l: 0,
i: null,
r: null,
v: value,
w: 0,
x: null,
y: null
};
if (DEV) {
// @ts-expect-error
signal.inspect = new Set();
}
return signal;
}
/**
* @param {import('../types.js').ComputationSignal} target_signal
* @param {import('../types.js').ComputationSignal} ref_signal
* @returns {void}
*/
export function push_reference(target_signal, ref_signal) {
const references = target_signal.r;
if (references === null) {
target_signal.r = [ref_signal];
} else {
references.push(ref_signal);
}
}
/**
* @param {import('../types.js').EffectType} type
* @param {(() => void | (() => void)) | ((b: import('../types.js').Block) => void | (() => void))} fn
* @param {boolean} sync
* @param {null | import('../types.js').Block} block
* @param {boolean} schedule
* @returns {import('../types.js').EffectSignal}
*/
function internal_create_effect(type, fn, sync, block, schedule) {
const signal = create_computation_signal(type | DIRTY, null, block);
signal.i = fn;
signal.x = current_component_context;
if (current_effect !== null) {
signal.l = current_effect.l + 1;
if ((type & MANAGED) === 0) {
push_reference(current_effect, signal);
}
}
if (schedule) {
schedule_effect(signal, sync);
}
return signal;
}
/**
* @returns {boolean}
*/
export function effect_active() {
return current_effect ? (current_effect.f & MANAGED) === 0 : false;
}
/**
* Internal representation of `$effect(...)`
* @param {() => void | (() => void)} fn
* @returns {import('../types.js').EffectSignal}
*/
export function user_effect(fn) {
if (current_effect === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_EFFECT' +
(DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '')
);
}
const apply_component_effect_heuristics =
current_effect.f & RENDER_EFFECT &&
current_component_context !== null &&
!current_component_context.m;
const effect = internal_create_effect(
EFFECT,
fn,
false,
current_block,
!apply_component_effect_heuristics
);
if (apply_component_effect_heuristics) {
const context = /** @type {import('../types.js').ComponentContext} */ (
current_component_context
);
(context.e ??= []).push(effect);
}
return effect;
}
/**
* Internal representation of `$effect.root(...)`
* @param {() => void | (() => void)} fn
* @returns {() => void}
*/
export function user_root_effect(fn) {
const effect = render_effect(fn, current_block, true);
return () => {
destroy_signal(effect);
};
}
/**
* @param {() => void | (() => void)} fn
* @returns {import('../types.js').EffectSignal}
*/
export function effect(fn) {
return internal_create_effect(EFFECT, fn, false, current_block, true);
}
/**
* @param {() => void | (() => void)} fn
* @returns {import('../types.js').EffectSignal}
*/
export function managed_effect(fn) {
return internal_create_effect(EFFECT | MANAGED, fn, false, current_block, true);
}
/**
* @param {() => void | (() => void)} fn
* @param {boolean} sync
* @returns {import('../types.js').EffectSignal}
*/
export function managed_pre_effect(fn, sync) {
return internal_create_effect(PRE_EFFECT | MANAGED, fn, sync, current_block, true);
}
/**
* Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn
* @returns {import('../types.js').EffectSignal}
*/
export function pre_effect(fn) {
if (current_effect === null) {
throw new Error(
'ERR_SVELTE_ORPHAN_EFFECT' +
(DEV
? ': The Svelte $effect.pre rune can only be used during component initialisation.'
: '')
);
}
const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0;
return internal_create_effect(
PRE_EFFECT,
() => {
const val = fn();
flush_local_render_effects();
return val;
},
sync,
current_block,
true
);
}
/**
* This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the
* bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything.
*
* @param {() => void | (() => void)} fn
* @returns {import('../types.js').EffectSignal}
*/
export function invalidate_effect(fn) {
return internal_create_effect(PRE_EFFECT, fn, true, current_block, true);
}
/**
* @template {import('../types.js').Block} B
* @param {(block: B) => void | (() => void)} fn
* @param {any} block
* @param {any} managed
* @param {any} sync
* @returns {import('../types.js').EffectSignal}
*/
export function render_effect(fn, block = current_block, managed = false, sync = true) {
let flags = RENDER_EFFECT;
if (managed) {
flags |= MANAGED;
}
return internal_create_effect(flags, /** @type {any} */ (fn), sync, block, true);
}
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
const is_unowned = current_effect === null;
const flags = is_unowned ? DERIVED | UNOWNED : DERIVED;
const signal = /** @type {import('../types.js').ComputationSignal<V>} */ (
create_computation_signal(flags | CLEAN, UNINITIALIZED, current_block)
);
signal.i = fn;
signal.e = default_equals;
if (current_consumer !== null) {
push_reference(current_consumer, signal);
}
return signal;
}
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').ComputationSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
const signal = derived(fn);
signal.e = safe_equal;
return signal;
}

@ -0,0 +1,30 @@
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function default_equals(a, b) {
return a === b;
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function safe_not_equal(a, b) {
// eslint-disable-next-line eqeqeq
return a != a
? // eslint-disable-next-line eqeqeq
b == b
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function safe_equal(a, b) {
return !safe_not_equal(a, b);
}

@ -0,0 +1,70 @@
import { DEV } from 'esm-env';
import { current_component_context } from '../runtime.js';
import { default_equals, safe_equal } from './equality.js';
import { CLEAN, SOURCE } from '../constants.js';
/**
* @template V
* @param {V} initial_value
* @returns {import('../types.js').SourceSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function source(initial_value) {
return create_source_signal(SOURCE | CLEAN, initial_value);
}
/**
* @template V
* @param {V} initial_value
* @returns {import('../types.js').SourceSignal<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value) {
const s = source(initial_value);
s.e = safe_equal;
// bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks
if (current_component_context) {
(current_component_context.d ??= []).push(s);
}
return s;
}
/**
* @template V
* @param {import('../types.js').SignalFlags} flags
* @param {V} value
* @returns {import('../types.js').SourceSignal<V> | import('../types.js').SourceSignal<V> & import('../types.js').SourceSignalDebug}
*/
function create_source_signal(flags, value) {
if (DEV) {
return {
// consumers
c: null,
// equals
e: default_equals,
// flags
f: flags,
// value
v: value,
// write version
w: 0,
// this is for DEV only
inspect: new Set()
};
}
return {
// consumers
c: null,
// equals
e: default_equals,
// flags
f: flags,
// value
v: value,
// write version
w: 0
};
}

@ -0,0 +1,153 @@
import { subscribe_to_store } from '../../../store/utils.js';
import { noop } from '../../common.js';
import { UNINITIALIZED } from '../constants.js';
import { get, set, set_ignore_mutation_validation, untrack } from '../runtime.js';
import { user_effect } from './computations.js';
import { mutable_source } from './sources.js';
/**
* Gets the current value of a store. If the store isn't subscribed to yet, it will create a proxy
* signal that will be updated when the store is. The store references container is needed to
* track reassignments to stores and to track the correct component context.
* @template V
* @param {import('../types.js').Store<V> | null | undefined} store
* @param {string} store_name
* @param {import('../types.js').StoreReferencesContainer} stores
* @returns {V}
*/
export function store_get(store, store_name, stores) {
/** @type {import('../types.js').StoreReferencesContainer[''] | undefined} */
let entry = stores[store_name];
const is_new = entry === undefined;
if (is_new) {
entry = {
store: null,
last_value: null,
value: mutable_source(UNINITIALIZED),
unsubscribe: noop
};
// TODO: can we remove this code? it was refactored out when we split up source/comptued signals
// push_destroy_fn(entry.value, () => {
// /** @type {import('../types.js').StoreReferencesContainer['']} */ (entry).last_value =
// /** @type {import('../types.js').StoreReferencesContainer['']} */ (entry).value.value;
// });
stores[store_name] = entry;
}
if (is_new || entry.store !== store) {
entry.unsubscribe();
entry.store = store ?? null;
entry.unsubscribe = connect_store_to_signal(store, entry.value);
}
const value = get(entry.value);
// This could happen if the store was cleaned up because the component was destroyed and there's a leak on the user side.
// In that case we don't want to fail with a cryptic Symbol error, but rather return the last value we got.
return value === UNINITIALIZED ? entry.last_value : value;
}
/**
* @template V
* @param {import('../types.js').Store<V> | null | undefined} store
* @param {import('../types.js').SourceSignal<V>} source
*/
function connect_store_to_signal(store, source) {
if (store == null) {
set(source, undefined);
return noop;
}
/** @param {V} v */
const run = (v) => {
set_ignore_mutation_validation(true);
set(source, v);
set_ignore_mutation_validation(false);
};
return subscribe_to_store(store, run);
}
/**
* Sets the new value of a store and returns that value.
* @template V
* @param {import('../types.js').Store<V>} store
* @param {V} value
* @returns {V}
*/
export function store_set(store, value) {
store.set(value);
return value;
}
/**
* Unsubscribes from all auto-subscribed stores on destroy
* @param {import('../types.js').StoreReferencesContainer} stores
*/
export function unsubscribe_on_destroy(stores) {
on_destroy(() => {
let store_name;
for (store_name in stores) {
const ref = stores[store_name];
ref.unsubscribe();
// TODO: can we remove this code? it was refactored out when we split up source/comptued signals
// destroy_signal(ref.value);
}
});
}
/**
* Updates a store with a new value.
* @param {import('../types.js').Store<V>} store the store to update
* @param {any} expression the expression that mutates the store
* @param {V} new_value the new store value
* @template V
*/
export function mutate_store(store, expression, new_value) {
store.set(new_value);
return expression;
}
/**
* @template V
* @param {unknown} val
* @returns {val is import('../types.js').Store<V>}
*/
export function is_store(val) {
return (
typeof val === 'object' &&
val !== null &&
typeof (/** @type {import('../types.js').Store<V>} */ (val).subscribe) === 'function'
);
}
/**
* @param {import('../types.js').Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_store(store, store_value, d = 1) {
store.set(store_value + d);
return store_value;
}
/**
* @param {import('../types.js').Store<number>} store
* @param {number} store_value
* @param {1 | -1} [d]
* @returns {number}
*/
export function update_pre_store(store, store_value, d = 1) {
const value = store_value + d;
store.set(value);
return value;
}
/**
* Schedules a callback to run immediately before the component is unmounted.
* @param {() => any} fn
* @returns {void}
*/
function on_destroy(fn) {
user_effect(() => () => untrack(fn));
}

@ -103,7 +103,9 @@ export function reconcile_html(target, value, svg) {
if (svg) {
html = `<svg>${html}</svg>`;
}
var content = create_fragment_with_script_from_html(html);
// Don't use create_fragment_with_script_from_html here because that would mean script tags are executed.
// @html is basically `.innerHTML = ...` and that doesn't execute scripts either due to security reasons.
var content = create_fragment_from_html(html);
if (svg) {
content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild));
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -8,19 +8,17 @@ import {
IF_BLOCK,
KEY_BLOCK,
ROOT_BLOCK
} from './block.js';
import { destroy_each_item_block, get_first_element } from './each.js';
} from './constants.js';
import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js';
import { schedule_raf_task } from './dom/task.js';
import { append_child, empty } from './operations.js';
import { effect, managed_effect, managed_pre_effect } from './reactivity/computations.js';
import {
current_block,
current_effect,
destroy_signal,
effect,
execute_effect,
managed_effect,
managed_pre_effect,
mark_subtree_inert,
schedule_raf_task,
untrack
} from './runtime.js';
import { raf } from './timing.js';

@ -1,4 +1,9 @@
import {
DERIVED,
EFFECT,
RENDER_EFFECT,
SOURCE,
PRE_EFFECT,
ROOT_BLOCK,
EACH_BLOCK,
EACH_ITEM_BLOCK,
@ -8,10 +13,9 @@ import {
HEAD_BLOCK,
DYNAMIC_COMPONENT_BLOCK,
DYNAMIC_ELEMENT_BLOCK,
SNIPPET_BLOCK
} from './block.js';
import type { READONLY_SYMBOL, STATE_SYMBOL } from './proxy.js';
import { DERIVED, EFFECT, RENDER_EFFECT, SOURCE, PRE_EFFECT } from './runtime.js';
SNIPPET_BLOCK,
STATE_SYMBOL
} from './constants.js';
// Put all internal types in this file. Once we convert to JSDoc, we can make this a d.ts file
@ -40,8 +44,8 @@ export type ComponentContext = {
d: null | Signal<any>[];
/** props */
s: Record<string, unknown>;
/** accessors */
a: Record<string, any> | null;
/** exports (and props, if `accessors: true`) */
x: Record<string, any> | null;
/** effects */
e: null | Array<EffectSignal>;
/** mounted */
@ -77,6 +81,8 @@ export type SourceSignal<V = unknown> = {
f: SignalFlags;
/** value: The latest value for this signal */
v: V;
// write version
w: number;
};
export type SourceSignalDebug = {
@ -111,6 +117,8 @@ export type ComputationSignal<V = unknown> = {
v: V;
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
/** write version: used for unowned signals to track if their depdendencies are dirty or not **/
w: number;
};
export type Signal<V = unknown> = SourceSignal<V> | ComputationSignal<V>;
@ -403,15 +411,13 @@ export interface ProxyMetadata<T = Record<string | symbol, any>> {
/** Immutable: Whether to use a source or mutable source under the hood */
i: boolean;
/** The associated proxy */
p: ProxyStateObject<T> | ProxyReadonlyObject<T>;
p: ProxyStateObject<T>;
/** The original target this proxy was created for */
t: T;
/** Dev-only — the components that 'own' this state, if any */
o: null | Set<Function>;
}
export type ProxyStateObject<T = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata;
};
export type ProxyReadonlyObject<T = Record<string | symbol, any>> = ProxyStateObject<T> & {
[READONLY_SYMBOL]: ProxyMetadata;
};

@ -1,5 +1,4 @@
import { EACH_INDEX_REACTIVE } from '../../constants.js';
import { source, untrack } from './runtime.js';
import { untrack } from './runtime.js';
import { is_array } from './utils.js';
/** regex of all html void element names */

@ -1,50 +1,38 @@
export {
store_get,
get,
set,
set_sync,
invalidate_inner_signals,
source,
mutable_source,
derived,
derived_safe_equal,
prop,
user_effect,
render_effect,
pre_effect,
invalidate_effect,
flushSync,
bubble_event,
safe_equal,
tick,
untrack,
update,
update_prop,
update_store,
update_pre,
update_pre_prop,
update_pre_store,
mutate,
mutate_store,
value_or_fallback,
exclude_from_object,
store_set,
unsubscribe_on_destroy,
pop,
push,
reactive_import,
effect_active,
user_root_effect,
inspect,
unwrap,
freeze,
init
deep_read
} from './client/runtime.js';
export * from './client/each.js';
export * from './client/dev/ownership.js';
export { await_block as await } from './client/dom/blocks/await.js';
export { if_block as if } from './client/dom/blocks/if.js';
export { key_block as key } from './client/dom/blocks/key.js';
export * from './client/dom/blocks/each.js';
export * from './client/reactivity/computations.js';
export * from './client/reactivity/sources.js';
export * from './client/reactivity/equality.js';
export * from './client/reactivity/store.js';
export * from './client/render.js';
export * from './client/validate.js';
export { raf } from './client/timing.js';
export { proxy, readonly, unstate } from './client/proxy.js';
export { proxy, unstate } from './client/proxy.js';
export { create_custom_element } from './client/custom-element.js';
export {
child,

@ -14,6 +14,7 @@ import * as $ from '../internal/index.js';
* @param {import('../main/public.js').ComponentConstructorOptions<Props> & {
* component: import('../main/public.js').SvelteComponent<Props, Events, Slots>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options
* @returns {import('../main/public.js').SvelteComponent<Props, Events, Slots> & Exports}
@ -53,28 +54,31 @@ class Svelte4Component {
/** @type {any} */
#events = {};
/** @type {ReturnType<typeof $.createRoot>} */
/** @type {Record<string, any>} */
#instance;
/**
* @param {import('../main/public.js').ComponentConstructorOptions & {
* component: any;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: false;
* }} options
*/
constructor(options) {
this.#instance = $.createRoot(options.component, {
// Using proxy state here isn't completely mirroring the Svelte 4 behavior, because mutations to a property
// cause fine-grained updates to only the places where that property is used, and not the entire property.
// Reactive statements and actions (the things where this matters) are handling this properly regardless, so it should be fine in practise.
const props = $.proxy({ ...(options.props || {}), $$events: this.#events }, false);
this.#instance = (options.hydrate ? $.hydrate : $.mount)(options.component, {
target: options.target,
props: { ...options.props, $$events: this.#events },
props,
context: options.context,
intro: options.intro,
recover: options.recover
});
for (const key of Object.keys(this.#instance)) {
if (key === '$set' || key === '$destroy') continue;
define_property(this, key, {
get() {
return this.#instance[key];
@ -86,6 +90,13 @@ class Svelte4Component {
enumerable: true
});
}
this.#instance.$set = /** @param {Record<string, any>} next */ (next) => {
Object.assign(props, next);
};
this.#instance.$destroy = () => {
$.unmount(this.#instance);
};
}
/** @param {Record<string, any>} props */

@ -1,10 +1,10 @@
import {
current_component_context,
get_or_init_context_map,
untrack,
user_effect
untrack
} from '../internal/client/runtime.js';
import { is_array } from '../internal/client/utils.js';
import { user_effect } from '../internal/index.js';
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -164,7 +164,7 @@ export function createEventDispatcher() {
// in a server (non-DOM) environment?
const event = create_custom_event(/** @type {string} */ (type), detail, options);
for (const fn of callbacks) {
fn.call(component_context.a, event);
fn.call(component_context.x, event);
}
return !event.defaultPrevented;
}
@ -235,10 +235,11 @@ function init_update_callbacks(context) {
// (except probably untrack — do we want to expose that, if there's also a rune?)
export {
flushSync,
createRoot,
mount,
hydrate,
tick,
unmount,
untrack,
unstate
unstate,
createRoot
} from '../internal/index.js';

@ -1,7 +1,6 @@
import { on_destroy } from '../internal/server/index.js';
export {
createRoot,
createEventDispatcher,
flushSync,
getAllContexts,
@ -11,7 +10,9 @@ export {
hydrate,
setContext,
tick,
untrack
unmount,
untrack,
createRoot
} from './main-client.js';
/** @returns {void} */

@ -6,5 +6,5 @@
* https://svelte.dev/docs/svelte-compiler#svelte-version
* @type {string}
*/
export const VERSION = '5.0.0-next.56';
export const VERSION = '5.0.0-next.65';
export const PUBLIC_VERSION = '5';

@ -4,6 +4,7 @@ export default test({
error: {
code: 'invalid-derived-export',
message:
'Cannot export derived state. To expose the current derived value, export a function returning its value'
'Cannot export derived state from a module. To expose the current derived value, export a function returning its value',
position: [24, 66]
}
});

@ -1,4 +0,0 @@
<script>
let count = $state(0);
export const double = $derived(count * 2);
</script>

@ -1,10 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
position: [59, 99]
}
});

@ -1,15 +0,0 @@
<script>
export const object = $state({
ok: true
});
export const primitive = $state('nope');
export function update_object() {
object.ok = !object.ok;
}
export function update_primitive() {
primitive = 'yep';
}
</script>

@ -4,7 +4,7 @@ export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
"Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
position: [46, 86]
}
});

@ -4,7 +4,7 @@ export default test({
error: {
code: 'invalid-state-export',
message:
"Cannot export state if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
"Cannot export state from a module if it is reassigned. Either export a function returning the state value or only mutate the state value's properties",
position: [28, 53]
}
});

@ -1,9 +0,0 @@
import { test } from '../../test';
export default test({
error: {
code: 'invalid-prop-export',
message:
'Cannot export properties. To expose the current value of a property, export a function returning its value'
}
});

@ -1,4 +0,0 @@
<script>
let { foo } = $props();
export { foo };
</script>

@ -2,6 +2,6 @@
background-color: red;
}
main.svelte-xyz div:where(.svelte-xyz) > button:where(.svelte-xyz) {
main.svelte-xyz div > button:where(.svelte-xyz) {
background-color: blue;
}

@ -0,0 +1,9 @@
<x>
<z></z>
</x>
<style>
x y z {
color: red;
}
</style>

@ -7,10 +7,10 @@
h2.svelte-xyz span:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz > span:where(.svelte-xyz) > b:where(.svelte-xyz) {
h2.svelte-xyz > span > b:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz span b:where(.svelte-xyz) {
h2.svelte-xyz span:where(.svelte-xyz) b:where(.svelte-xyz) {
color: red;
}
h2.svelte-xyz b:where(.svelte-xyz) {

@ -1,4 +1,4 @@
.match.svelte-xyz > :where(.svelte-xyz) ~ :where(.svelte-xyz) {
.match.svelte-xyz > * ~ :where(.svelte-xyz) {
margin-left: 4px;
}
/* (unused) .not-match > * ~ * {

@ -2,6 +2,6 @@
<div></div>
</div>
<div class="match svelte-xyz">
<div class="svelte-xyz"></div>
<div></div>
<div class="svelte-xyz"></div>
</div>

@ -1,6 +1,6 @@
div.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
span.svelte-xyz ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span:where(.svelte-xyz) ~ b:where(.svelte-xyz) { color: green; }
div.svelte-xyz span ~ b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ article:where(.svelte-xyz) { color: green; }
div.svelte-xyz ~ .b:where(.svelte-xyz) { color: green; }
.a.svelte-xyz ~ .c:where(.svelte-xyz) { color: green; }

@ -1,12 +1,5 @@
import { test } from '../../test';
export default test({
warnings: [
{
code: 'css-unused-selector',
message: 'Unused CSS selector "a:global(.foo) > div"',
start: { character: 91, column: 1, line: 8 },
end: { character: 111, column: 21, line: 8 }
}
]
warnings: []
});

@ -1,3 +1,3 @@
div > div.svelte-xyz {
a > b > div.svelte-xyz {
color: red;
}

@ -1,3 +1,3 @@
<div class="svelte-xyz">
<div class="svelte-xyz"></div>
<div></div>
</div>

@ -1,5 +1,5 @@
<style>
:global(div) > div {
:global(a) > :global(b) > div {
color: red;
}
</style>

@ -1,3 +0,0 @@
<div class="svelte-xyz">
<div class="svelte-xyz"></div>
</div>

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

Loading…
Cancel
Save