pull/12545/head
Dominic Gannaway 2 years ago
commit b5b989ba47

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure dynamic event handlers are wrapped in a derived

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: tidy up dynamic event handler generated code

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: dynamic event delegation for stateful call expressions

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure $state.snapshot correctly clones Date objects

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: remove internal `binding.expression` mechanism

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add missing hydration mismatch call-site

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: apply dynamic event fixes to OnDirective

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly set anchor inside HMR block

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove runtime validation of components/snippets, rely on types instead

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure directives run in sequential order

@ -46,6 +46,7 @@
"breezy-rules-beg",
"breezy-waves-camp",
"bright-berries-smell",
"bright-colts-play",
"bright-falcons-float",
"bright-needles-pretend",
"bright-peas-juggle",
@ -55,6 +56,7 @@
"brown-months-fry",
"brown-spoons-boil",
"calm-buses-clap",
"calm-cameras-hide",
"calm-pandas-vanish",
"calm-ravens-sneeze",
"chatty-beans-divide",
@ -62,6 +64,7 @@
"chatty-ghosts-unite",
"chatty-sloths-allow",
"chatty-taxis-juggle",
"chilled-ladybugs-invite",
"chilled-pumas-invite",
"chilled-seas-jog",
"chilly-dolphins-lick",
@ -112,6 +115,7 @@
"dirty-donuts-yell",
"dirty-eyes-itch",
"dirty-garlics-design",
"dirty-pianos-eat",
"dirty-tips-add",
"dry-clocks-grow",
"dry-eggs-play",
@ -195,6 +199,7 @@
"four-flies-hammer",
"four-mice-hammer",
"four-papayas-turn",
"four-peas-tickle",
"four-pugs-listen",
"fresh-beds-wash",
"fresh-dots-destroy",
@ -267,11 +272,13 @@
"hip-garlics-tap",
"hip-months-breathe",
"hip-pumpkins-boil",
"hip-stingrays-teach",
"honest-buses-add",
"honest-dragons-turn",
"honest-icons-change",
"honest-nails-share",
"honest-pans-kick",
"honest-phones-flash",
"hot-cooks-repair",
"hot-jobs-tap",
"hot-rivers-punch",
@ -336,6 +343,7 @@
"little-seals-reflect",
"long-buckets-lay",
"long-carrots-sneeze",
"long-coats-fry",
"long-crews-return",
"long-humans-repair",
"long-lobsters-mate",
@ -376,6 +384,7 @@
"moody-frogs-exist",
"moody-ghosts-buy",
"moody-houses-argue",
"moody-lions-watch",
"moody-owls-cry",
"moody-sheep-type",
"moody-toys-relax",
@ -437,6 +446,7 @@
"plenty-elephants-fry",
"plenty-items-build",
"plenty-starfishes-dress",
"plenty-turkeys-raise",
"plenty-zoos-fix",
"polite-dolphins-care",
"polite-peas-mate",
@ -460,6 +470,7 @@
"proud-pets-hang",
"proud-queens-sniff",
"purple-dragons-peel",
"quick-pumpkins-study",
"quiet-apricots-dream",
"quiet-berries-end",
"quiet-berries-explode",
@ -535,10 +546,12 @@
"silent-apes-report",
"silent-hats-stare",
"silent-rabbits-join",
"silent-rocks-yell",
"silly-ducks-mix",
"silly-laws-happen",
"silly-lies-film",
"silly-ways-wash",
"silver-mice-double",
"silver-points-approve",
"silver-sheep-knock",
"six-bears-trade",
@ -558,6 +571,7 @@
"slimy-walls-draw",
"slow-beds-shave",
"slow-chefs-dream",
"slow-gorillas-yawn",
"slow-kids-sparkle",
"slow-ligers-enjoy",
"slow-plums-chew",
@ -566,6 +580,7 @@
"small-chefs-sing",
"small-owls-remain",
"small-papayas-laugh",
"small-planets-destroy",
"small-sheep-type",
"small-spiders-fail",
"smart-cherries-leave",
@ -685,6 +700,7 @@
"tough-radios-punch",
"tough-tomatoes-explain",
"tricky-avocados-play",
"tricky-balloons-care",
"tricky-ears-shout",
"tricky-laws-bathe",
"twelve-beans-drive",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: tweak element_invalid_self_closing_tag to exclude namespace

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: exclude `bind:this` from reactive state validation

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: properly update store values

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: avoid flushing queued updates on mount/hydrate

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: better code generation for `let:` directives in SSR mode

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `:global` in more places

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly update stores when reassigning with operator other than `=`

@ -110,16 +110,18 @@ Because events are just attributes, the same rules as for attributes apply:
- you can spread them: `<button {...thisSpreadContainsEventAttributes}>click me</button>`
- component events are just (callback) properties and don't need a separate concept
Timing-wise, event attributes always fire after events from bindings (e.g. `oninput` always fires after an update to `bind:value`). Under the hood, some event handlers are attached directly with `addEventListener`, while others are _delegated_.
### Event delegation
To reduce the memory footprint and increase performance, Svelte uses a technique called event delegation. This means that certain events are only listened to once at the application root, invoking a handler that then traverses the event call path and invokes listeners along the way.
To reduce memory footprint and increase performance, Svelte uses a technique called event delegation. This means that for certain events — see the list below — a single event listener at the application root takes responsibility for running any handlers on the event's path.
There are a few gotchas you need to be aware of when it comes to event delegation:
There are a few gotchas to be aware of:
- when you dispatch events manually, make sure to set the `{ bubbles: true }` option
- when listening to events programmatically (i.e. not through `<button onclick={...}>` but through `node.addEventListener`), be careful to not call `stopPropagation` or else the delegated event handler won't be reached and handlers won't be invoked. For this reaon it's best to use `on` (which properly handles `stopPropagation`) from `svelte/events` instead of `addEventListener` to make sure the chain of events is preserved
- when you manually dispatch an event with a delegated listener, make sure to set the `{ bubbles: true }` option or it won't reach the application root
- when using `addEventListener` directly, avoid calling `stopPropagation` or the event won't reach the application root and handlers won't be invoked. Similarly, handlers added manually inside the application root will run _before_ handlers added declaratively deeper in the DOM (with e.g. `onclick={...}`), in both capturing and bubbling phases. For these reasons it's better to use the `on` function imported from `svelte/events` rather than `addEventListener`, as it will ensure that order is preserved and `stopPropagation` is handled correctly.
The following events are delegated:
The following event handlers are delegated:
- `beforeinput`
- `click`

@ -25,31 +25,28 @@ This works by adding a class to affected elements, which is based on a hash of t
</style>
```
## :global
## :global(...)
To apply styles to a selector globally, use the `:global(...)` modifier.
To apply styles to a single selector globally, use the `:global(...)` modifier:
```svelte
<style>
:global(body) {
/* this will apply to <body> */
/* applies to <body> */
margin: 0;
}
div :global(strong) {
/* this will apply to all <strong> elements, in any
component, that are inside <div> elements belonging
to this component */
/* applies to all <strong> elements, in any component,
that are inside <div> elements belonging
to this component */
color: goldenrod;
}
p:global(.red) {
/* this will apply to all <p> elements belonging to this
component with a class of red, even if class="red" does
not initially appear in the markup, and is instead
added at runtime. This is useful when the class
of the element is dynamically applied, for instance
when updating the element's classList property directly. */
p:global(.big.red) {
/* applies to all <p> elements belonging to this component
with `class="big red"`, even if it is applied
programmatically (for example by a library) */
}
</style>
```
@ -66,6 +63,30 @@ The `-global-` part will be removed when compiled, and the keyframe will then be
</style>
```
## :global
To apply styles to a group of selectors globally, create a `:global {...}` block:
```svelte
<style>
:global {
/* applies to every <div> in your application */
div { ... }
/* applies to every <p> in your application */
p { ... }
}
.a :global {
/* applies to every `.b .c .d` element, in any component,
that is inside an `.a` element in this component */
.b .c .d {...}
}
</style>
```
> The second example above could also be written as an equivalent `.a :global .b .c .d` selector, where everything after the `:global` is unscoped, though the nested form is preferred.
## Nested style tags
There should only be 1 top-level `<style>` tag per component.

@ -64,6 +64,12 @@ If `this` is the name of a [void element](https://developer.mozilla.org/en-US/do
<svelte:element this={tag} on:click={handler}>Foo</svelte:element>
```
Svelte tries its best to infer the correct namespace from the element's surroundings, but it's not always possible. You can make it explicit with an `xmlns` attribute:
```svelte
<svelte:element this={tag} xmlns="http://www.w3.org/2000/svg" />
```
## `<svelte:window>`
```svelte

@ -29,6 +29,8 @@ const app = mount(App, {
You can mount multiple components per page, and you can also mount from within your application, for example when creating a tooltip component and attaching it to the hovered element.
Note that unlike calling `new App(...)` in Svelte 4, things like effects (including `onMount` callbacks, and action functions) will not run during `mount`. If you need to force pending effects to run (in the context of a test, for example) you can do so with `flushSync()`.
## `unmount`
Unmounts a component created with [`mount`](#mount) or [`hydrate`](#hydrate):
@ -74,3 +76,5 @@ const app = hydrate(App, {
props: { some: 'property' }
});
```
As with `mount`, effects will not run during `hydrate` — use `flushSync()` immediately afterwards if you need them to.

@ -1,5 +1,57 @@
# svelte
## 5.0.0-next.199
### Patch Changes
- fix: add missing hydration mismatch call-site ([#12604](https://github.com/sveltejs/svelte/pull/12604))
- fix: apply dynamic event fixes to OnDirective ([#12582](https://github.com/sveltejs/svelte/pull/12582))
- fix: ensure directives run in sequential order ([#12591](https://github.com/sveltejs/svelte/pull/12591))
- fix: tweak element_invalid_self_closing_tag to exclude namespace ([#12585](https://github.com/sveltejs/svelte/pull/12585))
- breaking: avoid flushing queued updates on mount/hydrate ([#12602](https://github.com/sveltejs/svelte/pull/12602))
- feat: allow `:global` in more places ([#12560](https://github.com/sveltejs/svelte/pull/12560))
## 5.0.0-next.198
### Patch Changes
- chore: remove internal `binding.expression` mechanism ([#12530](https://github.com/sveltejs/svelte/pull/12530))
- fix: exclude `bind:this` from reactive state validation ([#12566](https://github.com/sveltejs/svelte/pull/12566))
## 5.0.0-next.197
### Patch Changes
- fix: correctly set anchor inside HMR block ([#12575](https://github.com/sveltejs/svelte/pull/12575))
## 5.0.0-next.196
### Patch Changes
- fix: ensure dynamic event handlers are wrapped in a derived ([#12563](https://github.com/sveltejs/svelte/pull/12563))
- chore: tidy up dynamic event handler generated code ([#12553](https://github.com/sveltejs/svelte/pull/12553))
- fix: dynamic event delegation for stateful call expressions ([#12549](https://github.com/sveltejs/svelte/pull/12549))
- fix: ensure $state.snapshot correctly clones Date objects ([#12564](https://github.com/sveltejs/svelte/pull/12564))
- fix: remove runtime validation of components/snippets, rely on types instead ([#12507](https://github.com/sveltejs/svelte/pull/12507))
- fix: properly update store values ([#12562](https://github.com/sveltejs/svelte/pull/12562))
## 5.0.0-next.195
### Patch Changes
- fix: update original source in HMR update ([#12547](https://github.com/sveltejs/svelte/pull/12547))
## 5.0.0-next.194
### Patch Changes

@ -40,4 +40,28 @@
## state_proxy_equality_mismatch
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results. Consider using `$state.is(a, b)` instead
> Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results. Consider using `$state.is(a, b)` instead%details%
`$state(...)` creates a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) of the value it is passed. The proxy and the value have different identities, meaning equality checks will always return `false`:
```svelte
<script>
let value = { foo: 'bar' };
let proxy = $state(value);
value === proxy; // always false
</script>
```
In the rare case that you need to compare them, you can use `$state.is`, which unwraps proxies:
```svelte
<script>
let value = { foo: 'bar' };
let proxy = $state(value);
$state.is(value, proxy); // true
</script>
```
During development, Svelte will warn you when comparing values with proxies.

@ -8,35 +8,35 @@
## css_global_block_invalid_combinator
> A :global {...} block cannot follow a %name% combinator
> A `:global` selector cannot follow a `%name%` combinator
## css_global_block_invalid_declaration
> A :global {...} block can only contain rules, not declarations
> A top-level `:global {...}` block can only contain rules, not declarations
## css_global_block_invalid_list
> A :global {...} block cannot be part of a selector list with more than one item
> A `:global` selector cannot be part of a selector list with more than one item
## css_global_block_invalid_modifier
> A :global {...} block cannot modify an existing selector
> A `:global` selector cannot modify an existing selector
## css_global_block_invalid_placement
## css_global_block_invalid_modifier_start
> A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)
> A `:global` selector can only be modified if it is a descendant of other selectors
## css_global_invalid_placement
> :global(...) can be at the start or end of a selector sequence, but not in the middle
> `:global(...)` can be at the start or end of a selector sequence, but not in the middle
## css_global_invalid_selector
> :global(...) must contain exactly one selector
> `:global(...)` must contain exactly one selector
## css_global_invalid_selector_list
> :global(...) must not contain type or universal selectors when used in a compound selector
> `:global(...)` must not contain type or universal selectors when used in a compound selector
## css_nesting_selector_invalid_placement
@ -48,4 +48,4 @@
## css_type_selector_invalid_placement
> :global(...) must not be followed with a type selector
> `:global(...)` must not be followed by a type selector

@ -6,14 +6,6 @@
> `%name%(...)` can only be used during component initialisation
## render_tag_invalid_argument
> The argument to `{@render ...}` must be a snippet function, not a component or a slot with a `let:` directive or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}`
## snippet_used_as_component
> A snippet must be rendered with `{@render ...}`
## store_invalid_shape
> `%name%` is not a store with a `subscribe` method

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

@ -33,7 +33,7 @@ for (const category of fs.readdirSync('messages')) {
const sections = text.trim().split('\n\n');
let details = null;
if (!sections[sections.length - 1].startsWith('> ')) {
while (!sections[sections.length - 1].startsWith('> ')) {
details = /** @type {string} */ (sections.pop());
}

@ -434,76 +434,76 @@ export function css_expected_identifier(node) {
}
/**
* A :global {...} block cannot follow a %name% combinator
* A `:global` selector cannot follow a `%name%` combinator
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function css_global_block_invalid_combinator(node, name) {
e(node, "css_global_block_invalid_combinator", `A :global {...} block cannot follow a ${name} combinator`);
e(node, "css_global_block_invalid_combinator", `A \`:global\` selector cannot follow a \`${name}\` combinator`);
}
/**
* A :global {...} block can only contain rules, not declarations
* A top-level `:global {...}` block can only contain rules, not declarations
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_declaration(node) {
e(node, "css_global_block_invalid_declaration", "A :global {...} block can only contain rules, not declarations");
e(node, "css_global_block_invalid_declaration", "A top-level `:global {...}` block can only contain rules, not declarations");
}
/**
* A :global {...} block cannot be part of a selector list with more than one item
* A `:global` selector cannot be part of a selector list with more than one item
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_list(node) {
e(node, "css_global_block_invalid_list", "A :global {...} block cannot be part of a selector list with more than one item");
e(node, "css_global_block_invalid_list", "A `:global` selector cannot be part of a selector list with more than one item");
}
/**
* A :global {...} block cannot modify an existing selector
* A `:global` selector cannot modify an existing selector
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_modifier(node) {
e(node, "css_global_block_invalid_modifier", "A :global {...} block cannot modify an existing selector");
e(node, "css_global_block_invalid_modifier", "A `:global` selector cannot modify an existing selector");
}
/**
* A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)
* A `:global` selector can only be modified if it is a descendant of other selectors
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_block_invalid_placement(node) {
e(node, "css_global_block_invalid_placement", "A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)");
export function css_global_block_invalid_modifier_start(node) {
e(node, "css_global_block_invalid_modifier_start", "A `:global` selector can only be modified if it is a descendant of other selectors");
}
/**
* :global(...) can be at the start or end of a selector sequence, but not in the middle
* `:global(...)` can be at the start or end of a selector sequence, but not in the middle
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_invalid_placement(node) {
e(node, "css_global_invalid_placement", ":global(...) can be at the start or end of a selector sequence, but not in the middle");
e(node, "css_global_invalid_placement", "`:global(...)` can be at the start or end of a selector sequence, but not in the middle");
}
/**
* :global(...) must contain exactly one selector
* `:global(...)` must contain exactly one selector
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_invalid_selector(node) {
e(node, "css_global_invalid_selector", ":global(...) must contain exactly one selector");
e(node, "css_global_invalid_selector", "`:global(...)` must contain exactly one selector");
}
/**
* :global(...) must not contain type or universal selectors when used in a compound selector
* `:global(...)` must not contain type or universal selectors when used in a compound selector
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_global_invalid_selector_list(node) {
e(node, "css_global_invalid_selector_list", ":global(...) must not contain type or universal selectors when used in a compound selector");
e(node, "css_global_invalid_selector_list", "`:global(...)` must not contain type or universal selectors when used in a compound selector");
}
/**
@ -525,12 +525,12 @@ export function css_selector_invalid(node) {
}
/**
* :global(...) must not be followed with a type selector
* `:global(...)` must not be followed by a type selector
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function css_type_selector_invalid_placement(node) {
e(node, "css_type_selector_invalid_placement", ":global(...) must not be followed with a type selector");
e(node, "css_type_selector_invalid_placement", "`:global(...)` must not be followed by a type selector");
}
/**

@ -1,3 +1,5 @@
/** @import { Comment, Program } from 'estree' */
/** @import { Node } from 'acorn' */
import * as acorn from 'acorn';
import { walk } from 'zimmerframe';
import { tsPlugin } from 'acorn-typescript';
@ -23,7 +25,7 @@ export function parse(source, typescript) {
if (typescript) amend(source, ast);
add_comments(ast);
return /** @type {import('estree').Program} */ (ast);
return /** @type {Program} */ (ast);
}
/**
@ -57,7 +59,7 @@ export function parse_expression_at(source, typescript, index) {
*/
function get_comment_handlers(source) {
/**
* @typedef {import('estree').Comment & {
* @typedef {Comment & {
* start: number;
* end: number;
* }} CommentWithLocation
@ -149,7 +151,7 @@ function get_comment_handlers(source) {
/**
* Tidy up some stuff left behind by acorn-typescript
* @param {string} source
* @param {import('acorn').Node} node
* @param {Node} node
*/
function amend(source, node) {
return walk(node, null, {

@ -1,10 +1,12 @@
/** @import { Expression } from 'estree' */
/** @import { Parser } from '../index.js' */
import { parse_expression_at } from '../acorn.js';
import { regex_whitespace } from '../../patterns.js';
import * as e from '../../../errors.js';
/**
* @param {import('../index.js').Parser} parser
* @returns {import('estree').Expression}
* @param {Parser} parser
* @returns {Expression}
*/
export default function read_expression(parser) {
try {
@ -35,7 +37,7 @@ export default function read_expression(parser) {
parser.index = index;
return /** @type {import('estree').Expression} */ (node);
return /** @type {Expression} */ (node);
} catch (err) {
parser.acorn_error(err);
}

@ -1,5 +1,6 @@
/** @import { Parser } from '../index.js' */
/** @import { Expression } from 'estree' */
/** @import * as Compiler from '#compiler' */
/** @import { Parser } from '../index.js' */
import { is_void } from '../../../../constants.js';
import read_expression from '../read/expression.js';
import { read_script } from '../read/script.js';
@ -589,7 +590,7 @@ function read_attribute(parser) {
const first_value = value === true ? undefined : Array.isArray(value) ? value[0] : value;
/** @type {import('estree').Expression | null} */
/** @type {Expression | null} */
let expression = null;
if (first_value) {
@ -612,7 +613,11 @@ function read_attribute(parser) {
type,
name: directive_name,
modifiers,
expression
expression,
metadata: {
dynamic: false,
contains_call_expression: false
}
};
if (directive.type === 'ClassDirective') {

@ -1,3 +1,6 @@
/** @import { ArrowFunctionExpression, Expression, Identifier } from 'estree' */
/** @import { AwaitBlock, ConstTag, DebugTag, EachBlock, ExpressionTag, HtmlTag, IfBlock, KeyBlock, RenderTag, SnippetBlock } from '#compiler' */
/** @import { Parser } from '../index.js' */
import read_pattern from '../read/context.js';
import read_expression from '../read/expression.js';
import * as e from '../../../errors.js';
@ -7,7 +10,7 @@ import { parse_expression_at } from '../acorn.js';
const regex_whitespace_with_closing_curly_brace = /^\s*}/;
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
export default function tag(parser) {
const start = parser.index;
parser.index += 1;
@ -29,7 +32,7 @@ export default function tag(parser) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').ExpressionTag>>} */
/** @type {ReturnType<typeof parser.append<ExpressionTag>>} */
parser.append({
type: 'ExpressionTag',
start,
@ -42,7 +45,7 @@ export default function tag(parser) {
});
}
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function open(parser) {
let start = parser.index - 2;
while (parser.template[start] !== '{') start -= 1;
@ -50,7 +53,7 @@ function open(parser) {
if (parser.eat('if')) {
parser.require_whitespace();
/** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */
/** @type {ReturnType<typeof parser.append<IfBlock>>} */
const block = parser.append({
type: 'IfBlock',
elseif: false,
@ -76,7 +79,7 @@ function open(parser) {
const template = parser.template;
let end = parser.template.length;
/** @type {import('estree').Expression | undefined} */
/** @type {Expression | undefined} */
let expression;
// we have to do this loop because `{#each x as { y = z }}` fails to parse —
@ -119,7 +122,7 @@ function open(parser) {
expression = walk(expression, null, {
// @ts-expect-error
TSAsExpression(node, context) {
if (node.end === /** @type {import('estree').Expression} */ (expression).end) {
if (node.end === /** @type {Expression} */ (expression).end) {
assertion = node;
end = node.expression.end;
return node.expression;
@ -171,7 +174,7 @@ function open(parser) {
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').EachBlock>>} */
/** @type {ReturnType<typeof parser.append<EachBlock>>} */
const block = parser.append({
type: 'EachBlock',
start,
@ -195,7 +198,7 @@ function open(parser) {
const expression = read_expression(parser);
parser.allow_whitespace();
/** @type {ReturnType<typeof parser.append<import('#compiler').AwaitBlock>>} */
/** @type {ReturnType<typeof parser.append<AwaitBlock>>} */
const block = parser.append({
type: 'AwaitBlock',
start,
@ -249,7 +252,7 @@ function open(parser) {
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').KeyBlock>>} */
/** @type {ReturnType<typeof parser.append<KeyBlock>>} */
const block = parser.append({
type: 'KeyBlock',
start,
@ -293,14 +296,14 @@ function open(parser) {
const prelude = parser.template.slice(0, params_start).replace(/\S/g, ' ');
const params = parser.template.slice(params_start, parser.index);
let function_expression = /** @type {import('estree').ArrowFunctionExpression} */ (
let function_expression = /** @type {ArrowFunctionExpression} */ (
parse_expression_at(prelude + `${params} => {}`, parser.ts, params_start)
);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').SnippetBlock>>} */
/** @type {ReturnType<typeof parser.append<SnippetBlock>>} */
const block = parser.append({
type: 'SnippetBlock',
start,
@ -323,7 +326,7 @@ function open(parser) {
e.expected_block_type(parser.index);
}
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function next(parser) {
const start = parser.index - 1;
@ -352,7 +355,7 @@ function next(parser) {
let elseif_start = start - 1;
while (parser.template[elseif_start] !== '{') elseif_start -= 1;
/** @type {ReturnType<typeof parser.append<import('#compiler').IfBlock>>} */
/** @type {ReturnType<typeof parser.append<IfBlock>>} */
const child = parser.append({
start: elseif_start,
end: -1,
@ -434,7 +437,7 @@ function next(parser) {
e.block_invalid_continuation_placement(start);
}
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function close(parser) {
const start = parser.index - 1;
@ -448,7 +451,7 @@ function close(parser) {
while (block.elseif) {
block.end = parser.index;
parser.stack.pop();
block = /** @type {import('#compiler').IfBlock} */ (parser.current());
block = /** @type {IfBlock} */ (parser.current());
}
block.end = parser.index;
parser.pop();
@ -482,7 +485,7 @@ function close(parser) {
parser.pop();
}
/** @param {import('../index.js').Parser} parser */
/** @param {Parser} parser */
function special(parser) {
let start = parser.index;
while (parser.template[start] !== '{') start -= 1;
@ -496,7 +499,7 @@ function special(parser) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').HtmlTag>>} */
/** @type {ReturnType<typeof parser.append<HtmlTag>>} */
parser.append({
type: 'HtmlTag',
start,
@ -508,7 +511,7 @@ function special(parser) {
}
if (parser.eat('debug')) {
/** @type {import('estree').Identifier[]} */
/** @type {Identifier[]} */
let identifiers;
// Implies {@debug} which indicates "debug all"
@ -519,8 +522,8 @@ function special(parser) {
identifiers =
expression.type === 'SequenceExpression'
? /** @type {import('estree').Identifier[]} */ (expression.expressions)
: [/** @type {import('estree').Identifier} */ (expression)];
? /** @type {Identifier[]} */ (expression.expressions)
: [/** @type {Identifier} */ (expression)];
identifiers.forEach(
/** @param {any} node */ (node) => {
@ -534,7 +537,7 @@ function special(parser) {
parser.eat('}', true);
}
/** @type {ReturnType<typeof parser.append<import('#compiler').DebugTag>>} */
/** @type {ReturnType<typeof parser.append<DebugTag>>} */
parser.append({
type: 'DebugTag',
start,
@ -567,7 +570,7 @@ function special(parser) {
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').ConstTag>>} */
/** @type {ReturnType<typeof parser.append<ConstTag>>} */
parser.append({
type: 'ConstTag',
start,
@ -598,7 +601,7 @@ function special(parser) {
parser.allow_whitespace();
parser.eat('}', true);
/** @type {ReturnType<typeof parser.append<import('#compiler').RenderTag>>} */
/** @type {ReturnType<typeof parser.append<RenderTag>>} */
parser.append({
type: 'RenderTag',
start,

@ -27,10 +27,25 @@ function is_global(relative_selector) {
return (
first.type === 'PseudoClassSelector' &&
first.name === 'global' &&
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
)
(first.args === null ||
// Only these two selector types keep the whole selector global, because e.g.
// :global(button).x means that the selector is still scoped because of the .x
relative_selector.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
))
);
}
/**
* True if is `:global`
* @param {Css.SimpleSelector} simple_selector
*/
function is_global_block_selector(simple_selector) {
return (
simple_selector.type === 'PseudoClassSelector' &&
simple_selector.name === 'global' &&
simple_selector.args === null
);
}
@ -48,19 +63,12 @@ const analysis_visitors = {
node.metadata.rule = context.state.rule;
node.metadata.used = node.children.every(
node.metadata.used ||= node.children.every(
({ metadata }) => metadata.is_global || metadata.is_global_like
);
},
RelativeSelector(node, context) {
node.metadata.is_global =
node.selectors.length >= 1 &&
node.selectors[0].type === 'PseudoClassSelector' &&
node.selectors[0].name === 'global' &&
node.selectors.every(
(selector) =>
selector.type === 'PseudoClassSelector' || selector.type === 'PseudoElementSelector'
);
node.metadata.is_global = node.selectors.length >= 1 && is_global(node);
if (node.selectors.length === 1) {
const first = node.selectors[0];
@ -85,15 +93,30 @@ const analysis_visitors = {
Rule(node, context) {
node.metadata.parent_rule = context.state.rule;
// `:global {...}` or `div :global {...}`
node.metadata.is_global_block = node.prelude.children.some((selector) => {
const last = selector.children[selector.children.length - 1];
let is_global_block = false;
const s = last.selectors[last.selectors.length - 1];
for (const child of selector.children) {
const idx = child.selectors.findIndex(is_global_block_selector);
if (s.type === 'PseudoClassSelector' && s.name === 'global' && s.args === null) {
return true;
if (is_global_block) {
// All selectors after :global are unscoped
child.metadata.is_global_like = true;
}
if (idx !== -1) {
is_global_block = true;
for (let i = idx + 1; i < child.selectors.length; i++) {
walk(/** @type {Css.Node} */ (child.selectors[i]), null, {
ComplexSelector(node) {
node.metadata.used = true;
}
});
}
}
}
return is_global_block;
});
context.next({
@ -118,21 +141,35 @@ const validation_visitors = {
}
const complex_selector = node.prelude.children[0];
const relative_selector = complex_selector.children[complex_selector.children.length - 1];
const global_selector = complex_selector.children.find((r, selector_idx) => {
const idx = r.selectors.findIndex(is_global_block_selector);
if (idx === 0) {
if (r.selectors.length > 1 && selector_idx === 0 && node.metadata.parent_rule === null) {
e.css_global_block_invalid_modifier_start(r.selectors[1]);
}
return true;
} else if (idx !== -1) {
e.css_global_block_invalid_modifier(r.selectors[idx]);
}
});
if (relative_selector.selectors.length > 1) {
e.css_global_block_invalid_modifier(
relative_selector.selectors[relative_selector.selectors.length - 1]
);
if (!global_selector) {
throw new Error('Internal error: global block without :global selector');
}
if (relative_selector.combinator && relative_selector.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(relative_selector, relative_selector.combinator.name);
if (global_selector.combinator && global_selector.combinator.name !== ' ') {
e.css_global_block_invalid_combinator(global_selector, global_selector.combinator.name);
}
const declaration = node.block.children.find((child) => child.type === 'Declaration');
if (declaration) {
if (
declaration &&
// :global { color: red; } is invalid, but foo :global { color: red; } is valid
node.prelude.children.length === 1 &&
node.prelude.children[0].children.length === 1 &&
node.prelude.children[0].children[0].selectors.length === 1
) {
e.css_global_block_invalid_declaration(declaration);
}
}
@ -146,14 +183,7 @@ const validation_visitors = {
if (global) {
const idx = node.children.indexOf(global);
if (global.selectors[0].args === null && idx !== node.children.length - 1) {
// ensure `:global` is only at the end of a selector
e.css_global_block_invalid_placement(global.selectors[0]);
} else if (
global.selectors[0].args !== null &&
idx !== 0 &&
idx !== node.children.length - 1
) {
if (global.selectors[0].args !== null && idx !== 0 && idx !== node.children.length - 1) {
// ensure `:global(...)` is not used in the middle of a selector (but multiple `global(...)` in sequence are ok)
for (let i = idx + 1; i < node.children.length; i++) {
if (!is_global(node.children[i])) {
@ -196,8 +226,17 @@ const validation_visitors = {
},
NestingSelector(node, context) {
const rule = /** @type {Css.Rule} */ (context.state.rule);
if (!rule.metadata.parent_rule) {
const parent_rule = rule.metadata.parent_rule;
if (!parent_rule) {
e.css_nesting_selector_invalid_placement(node);
} else if (
// :global { &.foo { ... } } is invalid
parent_rule.metadata.is_global_block &&
!parent_rule.metadata.parent_rule &&
parent_rule.prelude.children[0].children.length === 1 &&
parent_rule.prelude.children[0].children[0].selectors.length === 1
) {
e.css_global_block_invalid_modifier_start(node);
}
}
};

@ -295,12 +295,19 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element,
return false;
}
if (name === 'global' && relative_selector.selectors.length === 1) {
const args = /** @type {Compiler.Css.SelectorList} */ (selector.args);
if (
name === 'global' &&
selector.args !== null &&
relative_selector.selectors.length === 1
) {
const args = selector.args;
const complex_selector = args.children[0];
return apply_selector(complex_selector.children, rule, element, stylesheet);
}
// We came across a :global, everything beyond it is global and therefore a potential match
if (name === 'global' && selector.args === null) return true;
if ((name === 'is' || name === 'where') && selector.args) {
let matched = false;

@ -1,3 +1,7 @@
/** @import { ArrowFunctionExpression, CallExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, LabeledStatement, Literal, Node, Program, Super } from 'estree' */
/** @import { Attribute, BindDirective, Binding, DelegatedEvent, RegularElement, Root, Script, SvelteNode, Text, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */
/** @import { AnalysisState, Context, LegacyAnalysisState, Visitors } from './types' */
/** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import * as e from '../../errors.js';
@ -37,14 +41,14 @@ import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.j
import { equal } from '../../utils/assert.js';
/**
* @param {import('#compiler').Script | null} script
* @param {Script | null} script
* @param {ScopeRoot} root
* @param {boolean} allow_reactive_declarations
* @param {Scope | null} parent
* @returns {import('../types.js').Js}
* @returns {Js}
*/
function js(script, root, allow_reactive_declarations, parent) {
/** @type {import('estree').Program} */
/** @type {Program} */
const ast = script?.content ?? {
type: 'Program',
sourceType: 'module',
@ -75,9 +79,9 @@ function get_component_name(filename) {
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
* @param {import('estree').Expression | null} handler
* @param {import('./types').Context} context
* @returns {null | import('#compiler').DelegatedEvent}
* @param {Expression | null} handler
* @param {Context} context
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail-out if not a delegated event.
@ -91,9 +95,9 @@ function get_delegated_event(event_name, handler, context) {
return null;
}
/** @type {import('#compiler').DelegatedEvent} */
/** @type {DelegatedEvent} */
const non_hoistable = { type: 'non-hoistable' };
/** @type {import('estree').FunctionExpression | import('estree').FunctionDeclaration | import('estree').ArrowFunctionExpression | null} */
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
@ -119,20 +123,20 @@ function get_delegated_event(event_name, handler, context) {
const grandparent = path.at(-2);
/** @type {import('#compiler').RegularElement | null} */
/** @type {RegularElement | null} */
let element = null;
/** @type {string | null} */
let event_name = null;
if (parent.type === 'OnDirective') {
element = /** @type {import('#compiler').RegularElement} */ (grandparent);
element = /** @type {RegularElement} */ (grandparent);
event_name = parent.name;
} else if (
parent.type === 'ExpressionTag' &&
grandparent?.type === 'Attribute' &&
is_event_attribute(grandparent)
) {
element = /** @type {import('#compiler').RegularElement} */ (path.at(-3));
const attribute = /** @type {import('#compiler').Attribute} */ (grandparent);
element = /** @type {RegularElement} */ (path.at(-3));
const attribute = /** @type {Attribute} */ (grandparent);
event_name = get_attribute_event_name(attribute.name);
}
@ -224,9 +228,9 @@ function get_delegated_event(event_name, handler, context) {
}
/**
* @param {import('estree').Program} ast
* @param {import('#compiler').ValidatedModuleCompileOptions} options
* @returns {import('../types.js').Analysis}
* @param {Program} ast
* @param {ValidatedModuleCompileOptions} options
* @returns {Analysis}
*/
export function analyze_module(ast, options) {
const { scope, scopes } = create_scopes(ast, new ScopeRoot(), false, null);
@ -239,7 +243,7 @@ export function analyze_module(ast, options) {
}
walk(
/** @type {import('estree').Node} */ (ast),
/** @type {Node} */ (ast),
{ scope, analysis: { runes: true } },
// @ts-expect-error TODO clean this mess up
merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker)
@ -255,10 +259,10 @@ export function analyze_module(ast, options) {
}
/**
* @param {import('#compiler').Root} root
* @param {Root} root
* @param {string} source
* @param {import('#compiler').ValidatedCompileOptions} options
* @returns {import('../types.js').ComponentAnalysis}
* @param {ValidatedCompileOptions} options
* @returns {ComponentAnalysis}
*/
export function analyze_component(root, source, options) {
const scope_root = new ScopeRoot();
@ -268,7 +272,7 @@ export function analyze_component(root, source, options) {
const { scope, scopes } = create_scopes(root.fragment, scope_root, false, instance.scope);
/** @type {import('../types.js').Template} */
/** @type {Template} */
const template = { ast: root.fragment, scope, scopes };
// create synthetic bindings for store subscriptions
@ -339,7 +343,7 @@ export function analyze_component(root, source, options) {
/** @type {number} */ (node.start) > /** @type {number} */ (module.ast.start) &&
/** @type {number} */ (node.end) < /** @type {number} */ (module.ast.end) &&
// const state = $state(0) is valid
get_rune(/** @type {import('estree').Node} */ (path.at(-1)), module.scope) === null
get_rune(/** @type {Node} */ (path.at(-1)), module.scope) === null
) {
e.store_invalid_subscription(node);
}
@ -360,7 +364,7 @@ export function analyze_component(root, source, options) {
Array.from(module.scope.references).some(([name]) => Runes.includes(/** @type {any} */ (name)));
// TODO remove all the ?? stuff, we don't need it now that we're validating the config
/** @type {import('../types.js').ComponentAnalysis} */
/** @type {ComponentAnalysis} */
const analysis = {
name: module.scope.generate(options.name ?? component_name),
root: scope_root,
@ -434,7 +438,7 @@ export function analyze_component(root, source, options) {
}
for (const { ast, scope, scopes } of [module, instance, template]) {
/** @type {import('./types').AnalysisState} */
/** @type {AnalysisState} */
const state = {
scope,
analysis,
@ -450,7 +454,7 @@ export function analyze_component(root, source, options) {
};
walk(
/** @type {import('#compiler').SvelteNode} */ (ast),
/** @type {SvelteNode} */ (ast),
state,
merge(set_scope(scopes), validation_runes, runes_scope_tweaker, common_visitors)
);
@ -474,7 +478,7 @@ export function analyze_component(root, source, options) {
// bind:this doesn't need to be a state reference if it will never change
if (
type === 'BindDirective' &&
/** @type {import('#compiler').BindDirective} */ (path[i]).name === 'this'
/** @type {BindDirective} */ (path[i]).name === 'this'
) {
for (let j = i - 1; j >= 0; j -= 1) {
const type = path[j].type;
@ -503,7 +507,7 @@ export function analyze_component(root, source, options) {
instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic');
for (const { ast, scope, scopes } of [module, instance, template]) {
/** @type {import('./types').LegacyAnalysisState} */
/** @type {LegacyAnalysisState} */
const state = {
scope,
analysis,
@ -522,7 +526,7 @@ export function analyze_component(root, source, options) {
};
walk(
/** @type {import('#compiler').SvelteNode} */ (ast),
/** @type {SvelteNode} */ (ast),
state,
// @ts-expect-error TODO
merge(set_scope(scopes), validation_legacy, legacy_scope_tweaker, common_visitors)
@ -583,7 +587,7 @@ export function analyze_component(root, source, options) {
// TODO this happens during the analysis phase, which shouldn't know anything about client vs server
if (element.type === 'SvelteElement' && options.generate === 'client') continue;
/** @type {import('#compiler').Attribute | undefined} */
/** @type {Attribute | undefined} */
let class_attribute = undefined;
for (const attribute of element.attributes) {
@ -602,7 +606,7 @@ export function analyze_component(root, source, options) {
if (is_text_attribute(class_attribute)) {
class_attribute.value[0].data += ` ${analysis.css.hash}`;
} else {
/** @type {import('#compiler').Text} */
/** @type {Text} */
const css_text = {
type: 'Text',
data: ` ${analysis.css.hash}`,
@ -642,19 +646,19 @@ export function analyze_component(root, source, options) {
return analysis;
}
/** @type {import('./types').Visitors<import('./types').LegacyAnalysisState>} */
/** @type {Visitors<LegacyAnalysisState>} */
const legacy_scope_tweaker = {
LabeledStatement(node, { next, path, state }) {
if (
state.ast_type !== 'instance' ||
node.label.name !== '$' ||
/** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program'
/** @type {SvelteNode} */ (path.at(-1)).type !== 'Program'
) {
return next();
}
// Find all dependencies of this `$: {...}` statement
/** @type {import('../types.js').ReactiveStatement} */
/** @type {ReactiveStatement} */
const reactive_statement = {
assignments: new Set(),
dependencies: []
@ -669,14 +673,14 @@ const legacy_scope_tweaker = {
if (binding === null) continue;
for (const { node, path } of nodes) {
/** @type {import('estree').Expression} */
/** @type {Expression} */
let left = node;
let i = path.length - 1;
let parent = /** @type {import('estree').Expression} */ (path.at(i));
let parent = /** @type {Expression} */ (path.at(i));
while (parent.type === 'MemberExpression') {
left = parent;
parent = /** @type {import('estree').Expression} */ (path.at(--i));
parent = /** @type {Expression} */ (path.at(--i));
}
if (
@ -757,7 +761,7 @@ const legacy_scope_tweaker = {
next();
},
Identifier(node, { state, path }) {
const parent = /** @type {import('estree').Node} */ (path.at(-1));
const parent = /** @type {Node} */ (path.at(-1));
if (is_reference(node, parent)) {
if (node.name === '$$props') {
state.analysis.uses_props = true;
@ -834,9 +838,7 @@ const legacy_scope_tweaker = {
if (!node.declaration) {
for (const specifier of node.specifiers) {
const binding = /** @type {import('#compiler').Binding} */ (
state.scope.get(specifier.local.name)
);
const binding = /** @type {Binding} */ (state.scope.get(specifier.local.name));
if (
binding !== null &&
(binding.kind === 'state' ||
@ -863,7 +865,7 @@ const legacy_scope_tweaker = {
node.declaration.type === 'ClassDeclaration'
) {
state.analysis.exports.push({
name: /** @type {import('estree').Identifier} */ (node.declaration.id).name,
name: /** @type {Identifier} */ (node.declaration.id).name,
alias: null
});
return next();
@ -881,7 +883,7 @@ const legacy_scope_tweaker = {
for (const declarator of node.declaration.declarations) {
for (const id of extract_identifiers(declarator.id)) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
const binding = /** @type {Binding} */ (state.scope.get(id.name));
binding.kind = 'bindable_prop';
}
}
@ -899,7 +901,7 @@ const legacy_scope_tweaker = {
}
};
/** @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, { scope: Scope, analysis: { runes: true } }>} */
/** @type {Visitors} */
const runes_scope_js_tweaker = {
VariableDeclarator(node, { state }) {
if (node.init?.type !== 'CallExpression') return;
@ -920,7 +922,7 @@ const runes_scope_js_tweaker = {
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
const binding = /** @type {Binding} */ (state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
@ -933,7 +935,7 @@ const runes_scope_js_tweaker = {
}
};
/** @type {import('./types').Visitors} */
/** @type {Visitors} */
const runes_scope_tweaker = {
CallExpression(node, { state, next }) {
const rune = get_rune(node, state.scope);
@ -964,7 +966,7 @@ const runes_scope_tweaker = {
for (const path of extract_paths(node.id)) {
// @ts-ignore this fails in CI for some insane reason
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(path.node.name));
const binding = /** @type {Binding} */ (state.scope.get(path.node.name));
binding.kind =
rune === '$state'
? 'state'
@ -983,7 +985,7 @@ const runes_scope_tweaker = {
state.analysis.needs_props = true;
if (node.id.type === 'Identifier') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
const binding = /** @type {Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
@ -994,15 +996,15 @@ const runes_scope_tweaker = {
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
? /** @type {Identifier} */ (property.value.left).name
: /** @type {Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
: String(/** @type {Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
const binding = /** @type {Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
@ -1011,9 +1013,7 @@ const runes_scope_tweaker = {
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.initial = /** @type {Expression | null} */ (initial.arguments[0] ?? null);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
@ -1043,7 +1043,7 @@ const runes_scope_tweaker = {
node.declaration.type === 'ClassDeclaration'
) {
state.analysis.exports.push({
name: /** @type {import('estree').Identifier} */ (node.declaration.id).name,
name: /** @type {Identifier} */ (node.declaration.id).name,
alias: null
});
return next();
@ -1060,8 +1060,8 @@ const runes_scope_tweaker = {
};
/**
* @param {import('estree').CallExpression} node
* @param {import('./types').Context} context
* @param {CallExpression} node
* @param {Context} context
* @returns {boolean}
*/
function is_known_safe_call(node, context) {
@ -1085,8 +1085,8 @@ function is_known_safe_call(node, context) {
}
/**
* @param {import('estree').ArrowFunctionExpression | import('estree').FunctionExpression | import('estree').FunctionDeclaration} node
* @param {import('./types').Context} context
* @param {ArrowFunctionExpression | FunctionExpression | FunctionDeclaration} node
* @param {Context} context
*/
const function_visitor = (node, context) => {
// TODO retire this in favour of a more general solution based on bindings
@ -1106,7 +1106,7 @@ const function_visitor = (node, context) => {
/**
* A 'safe' identifier means that the `foo` in `foo.bar` or `foo()` will not
* call functions that require component context to exist
* @param {import('estree').Expression | import('estree').Super} expression
* @param {Expression | Super} expression
* @param {Scope} scope
*/
function is_safe_identifier(expression, scope) {
@ -1130,7 +1130,7 @@ function is_safe_identifier(expression, scope) {
);
}
/** @type {import('./types').Visitors} */
/** @type {Visitors} */
const common_visitors = {
_(node, { state, next, path }) {
ignore_map.set(node, structuredClone(ignore_stack));
@ -1253,7 +1253,7 @@ const common_visitors = {
context.next({ ...context.state, expression: node });
},
Identifier(node, context) {
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
const parent = /** @type {Node} */ (context.path.at(-1));
if (!is_reference(node, parent)) return;
if (node.name === '$$slots') {
@ -1280,8 +1280,7 @@ const common_visitors = {
// TODO it would be better to just bail out when we hit the ExportSpecifier node but that's
// not currently possibly because of our visitor merging, which I desperately want to nuke
const is_export_specifier =
/** @type {import('#compiler').SvelteNode} */ (context.path.at(-1)).type ===
'ExportSpecifier';
/** @type {SvelteNode} */ (context.path.at(-1)).type === 'ExportSpecifier';
if (
context.state.analysis.runes &&
@ -1309,7 +1308,9 @@ const common_visitors = {
CallExpression(node, context) {
const { expression, render_tag } = context.state;
if (
(expression?.type === 'ExpressionTag' || expression?.type === 'SpreadAttribute') &&
(expression?.type === 'ExpressionTag' ||
expression?.type === 'SpreadAttribute' ||
expression?.type === 'OnDirective') &&
!is_known_safe_call(node, context)
) {
expression.metadata.contains_call_expression = true;
@ -1387,7 +1388,7 @@ const common_visitors = {
if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') {
state.analysis.event_directive_node ??= node;
}
next();
next({ ...state, expression: node });
},
BindDirective(node, context) {
let i = context.path.length;
@ -1490,8 +1491,8 @@ const common_visitors = {
node.attributes.push(
create_attribute(
'value',
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(0)).start,
/** @type {import('#compiler').Text} */ (node.fragment.nodes.at(-1)).end,
/** @type {Text} */ (node.fragment.nodes.at(0)).start,
/** @type {Text} */ (node.fragment.nodes.at(-1)).end,
// @ts-ignore
node.fragment.nodes
)
@ -1570,7 +1571,7 @@ const common_visitors = {
};
/**
* @param {import('#compiler').RegularElement} node
* @param {RegularElement} node
*/
function determine_element_spread(node) {
let has_spread = false;
@ -1596,10 +1597,10 @@ function get_attribute_event_name(event_name) {
}
/**
* @param {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement>} unsorted_reactive_declarations
* @param {Map<LabeledStatement, ReactiveStatement>} unsorted_reactive_declarations
*/
function order_reactive_statements(unsorted_reactive_declarations) {
/** @typedef {[import('estree').LabeledStatement, import('../types.js').ReactiveStatement]} Tuple */
/** @typedef {[LabeledStatement, ReactiveStatement]} Tuple */
/** @type {Map<string, Array<Tuple>>} */
const lookup = new Map();
@ -1632,13 +1633,13 @@ function order_reactive_statements(unsorted_reactive_declarations) {
}
// We use a map and take advantage of the fact that the spec says insertion order is preserved when iterating
/** @type {Map<import('estree').LabeledStatement, import('../types.js').ReactiveStatement>} */
/** @type {Map<LabeledStatement, ReactiveStatement>} */
const reactive_declarations = new Map();
/**
*
* @param {import('estree').LabeledStatement} node
* @param {import('../types.js').ReactiveStatement} declaration
* @param {LabeledStatement} node
* @param {ReactiveStatement} declaration
* @returns
*/
const add_declaration = (node, declaration) => {

@ -3,6 +3,7 @@ import type { ComponentAnalysis, ReactiveStatement } from '../types.js';
import type {
ClassDirective,
ExpressionTag,
OnDirective,
RenderTag,
SpreadAttribute,
SvelteNode,
@ -20,7 +21,7 @@ export interface AnalysisState {
/** Which slots the current parent component has */
component_slots: Set<string>;
/** The current {expression}, if any */
expression: ExpressionTag | ClassDirective | SpreadAttribute | null;
expression: ExpressionTag | ClassDirective | OnDirective | SpreadAttribute | null;
/** The current {@render ...} tag, if any */
render_tag: null | RenderTag;
private_derived_state: string[];

@ -1,3 +1,7 @@
/** @import { AssignmentExpression, CallExpression, Expression, Identifier, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */
/** @import { Attribute, Component, ElementLike, Fragment, RegularElement, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TransitionDirective } from '#compiler' */
/** @import { NodeLike } from '../../errors.js' */
/** @import { AnalysisState, Context, Visitors } from './types.js' */
import is_reference from 'is-reference';
import {
disallowed_paragraph_contents,
@ -35,8 +39,8 @@ import { merge } from '../visitors.js';
import { a11y_validators } from './a11y.js';
/**
* @param {import('#compiler').Attribute} attribute
* @param {import('#compiler').ElementLike} parent
* @param {Attribute} attribute
* @param {ElementLike} parent
*/
function validate_attribute(attribute, parent) {
if (
@ -63,8 +67,8 @@ function validate_attribute(attribute, parent) {
}
/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {Context} context
*/
function validate_component(node, context) {
for (const attribute of node.attributes) {
@ -123,16 +127,16 @@ const react_attributes = new Map([
]);
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {RegularElement | SvelteElement} node
* @param {Context} context
*/
function validate_element(node, context) {
let has_animate_directive = false;
/** @type {import('#compiler').TransitionDirective | null} */
/** @type {TransitionDirective | null} */
let in_transition = null;
/** @type {import('#compiler').TransitionDirective | null} */
/** @type {TransitionDirective | null} */
let out_transition = null;
for (const attribute of node.attributes) {
@ -175,7 +179,7 @@ function validate_element(node, context) {
}
if (attribute.name === 'slot') {
/** @type {import('#compiler').RegularElement | import('#compiler').SvelteElement | import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf | undefined} */
/** @type {RegularElement | SvelteElement | Component | SvelteComponent | SvelteSelf | undefined} */
validate_slot_attribute(context, attribute);
}
@ -212,7 +216,7 @@ function validate_element(node, context) {
has_animate_directive = true;
}
} else if (attribute.type === 'TransitionDirective') {
const existing = /** @type {import('#compiler').TransitionDirective | null} */ (
const existing = /** @type {TransitionDirective | null} */ (
(attribute.intro && in_transition) || (attribute.outro && out_transition)
);
@ -255,7 +259,7 @@ function validate_element(node, context) {
}
/**
* @param {import('#compiler').Attribute} attribute
* @param {Attribute} attribute
*/
function validate_attribute_name(attribute) {
if (
@ -269,8 +273,8 @@ function validate_attribute_name(attribute) {
}
/**
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {import('#compiler').Attribute} attribute
* @param {Context} context
* @param {Attribute} attribute
* @param {boolean} is_component
*/
function validate_slot_attribute(context, attribute, is_component = false) {
@ -343,8 +347,8 @@ function validate_slot_attribute(context, attribute, is_component = false) {
}
/**
* @param {import('#compiler').Fragment | null | undefined} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('./types.js').AnalysisState>} context
* @param {Fragment | null | undefined} node
* @param {Context} context
*/
function validate_block_not_empty(node, context) {
if (!node) return;
@ -356,7 +360,7 @@ function validate_block_not_empty(node, context) {
}
/**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
* @type {Visitors}
*/
const validation = {
MemberExpression(node, context) {
@ -466,7 +470,7 @@ const validation = {
}
if (parent.name === 'input' && node.name !== 'this') {
const type = /** @type {import('#compiler').Attribute | undefined} */ (
const type = /** @type {Attribute | undefined} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'type')
);
if (type && !is_text_attribute(type)) {
@ -507,7 +511,7 @@ const validation = {
}
if (ContentEditableBindings.includes(node.name)) {
const contenteditable = /** @type {import('#compiler').Attribute} */ (
const contenteditable = /** @type {Attribute} */ (
parent.attributes.find((a) => a.type === 'Attribute' && a.name === 'contenteditable')
);
if (!contenteditable) {
@ -637,11 +641,14 @@ const validation = {
}
}
// Strip off any namespace from the beginning of the node name.
const node_name = node.name.replace(/[a-zA-Z-]*:/g, '');
if (
context.state.analysis.source[node.end - 2] === '/' &&
context.state.options.namespace !== 'foreign' &&
!VoidElements.includes(node.name) &&
!SVGElements.includes(node.name)
!VoidElements.includes(node_name) &&
!SVGElements.includes(node_name)
) {
w.element_invalid_self_closing_tag(node, node.name);
}
@ -844,8 +851,7 @@ export const validation_legacy = merge(validation, a11y_validators, {
LabeledStatement(node, { path, state }) {
if (
node.label.name === '$' &&
(state.ast_type !== 'instance' ||
/** @type {import('#compiler').SvelteNode} */ (path.at(-1)).type !== 'Program')
(state.ast_type !== 'instance' || /** @type {SvelteNode} */ (path.at(-1)).type !== 'Program')
) {
w.reactive_declaration_invalid_placement(node);
}
@ -857,8 +863,8 @@ export const validation_legacy = merge(validation, a11y_validators, {
/**
*
* @param {import('estree').Node} node
* @param {import('../scope').Scope} scope
* @param {Node} node
* @param {Scope} scope
* @param {string} name
*/
function validate_export(node, scope, name) {
@ -880,16 +886,16 @@ function validate_export(node, scope, name) {
}
/**
* @param {import('estree').CallExpression} node
* @param {CallExpression} node
* @param {Scope} scope
* @param {import('#compiler').SvelteNode[]} path
* @param {SvelteNode[]} path
* @returns
*/
function validate_call_expression(node, scope, path) {
const rune = get_rune(node, scope);
if (rune === null) return;
const parent = /** @type {import('#compiler').SvelteNode} */ (get_parent(path, -1));
const parent = /** @type {SvelteNode} */ (get_parent(path, -1));
if (rune === '$props') {
if (parent.type === 'VariableDeclarator') return;
@ -968,8 +974,8 @@ function validate_call_expression(node, scope, path) {
}
/**
* @param {import('estree').VariableDeclarator} node
* @param {import('./types.js').AnalysisState} state
* @param {VariableDeclarator} node
* @param {AnalysisState} state
*/
function ensure_no_module_import_conflict(node, state) {
const ids = extract_identifiers(node.id);
@ -984,7 +990,7 @@ function ensure_no_module_import_conflict(node, state) {
}
/**
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
* @type {Visitors}
*/
export const validation_runes_js = {
ImportDeclaration(node) {
@ -1019,7 +1025,7 @@ export const validation_runes_js = {
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const args = /** @type {CallExpression} */ (init).arguments;
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {
e.rune_invalid_arguments_length(node, rune, 'exactly one argument');
@ -1076,7 +1082,7 @@ export const validation_runes_js = {
},
Identifier(node, { path, state }) {
let i = path.length;
let parent = /** @type {import('estree').Expression} */ (path[--i]);
let parent = /** @type {Expression} */ (path[--i]);
if (
Runes.includes(/** @type {Runes[number]} */ (node.name)) &&
@ -1084,16 +1090,16 @@ export const validation_runes_js = {
state.scope.get(node.name) === null &&
state.scope.get(node.name.slice(1)) === null
) {
/** @type {import('estree').Expression} */
/** @type {Expression} */
let current = node;
let name = node.name;
while (parent.type === 'MemberExpression') {
if (parent.computed) e.rune_invalid_computed_property(parent);
name += `.${/** @type {import('estree').Identifier} */ (parent.property).name}`;
name += `.${/** @type {Identifier} */ (parent.property).name}`;
current = parent;
parent = /** @type {import('estree').Expression} */ (path[--i]);
parent = /** @type {Expression} */ (path[--i]);
if (!Runes.includes(/** @type {Runes[number]} */ (name))) {
if (name === '$effect.active') {
@ -1112,8 +1118,8 @@ export const validation_runes_js = {
};
/**
* @param {import('../../errors.js').NodeLike} node
* @param {import('estree').Pattern | import('estree').Expression} argument
* @param {NodeLike} node
* @param {Pattern | Expression} argument
* @param {Scope} scope
* @param {boolean} is_binding
*/
@ -1159,7 +1165,7 @@ function validate_no_const_assignment(node, argument, scope, is_binding) {
* Validates that the opening of a control flow block is `{` immediately followed by the expected character.
* In legacy mode whitespace is allowed inbetween. TODO remove once legacy mode is gone and move this into parser instead.
* @param {{start: number; end: number}} node
* @param {import('./types.js').AnalysisState} state
* @param {AnalysisState} state
* @param {string} expected
*/
function validate_opening_tag(node, state, expected) {
@ -1170,9 +1176,9 @@ function validate_opening_tag(node, state, expected) {
}
/**
* @param {import('estree').AssignmentExpression | import('estree').UpdateExpression} node
* @param {import('estree').Pattern | import('estree').Expression} argument
* @param {import('./types.js').AnalysisState} state
* @param {AssignmentExpression | UpdateExpression} node
* @param {Pattern | Expression} argument
* @param {AnalysisState} state
*/
function validate_assignment(node, argument, state) {
validate_no_const_assignment(node, argument, state.scope, false);
@ -1195,9 +1201,9 @@ function validate_assignment(node, argument, state) {
}
}
let object = /** @type {import('estree').Expression | import('estree').Super} */ (argument);
let object = /** @type {Expression | Super} */ (argument);
/** @type {import('estree').Expression | import('estree').PrivateIdentifier | null} */
/** @type {Expression | PrivateIdentifier | null} */
let property = null;
while (object.type === 'MemberExpression') {
@ -1325,7 +1331,7 @@ export const validation_runes = merge(validation, a11y_validators, {
if (rune === null) return;
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const args = /** @type {CallExpression} */ (init).arguments;
// TODO some of this is duplicated with above, seems off
if ((rune === '$derived' || rune === '$derived.by') && args.length !== 1) {

@ -65,6 +65,7 @@ export function client_component(source, analysis, options) {
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
getters: {},
in_constructor: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
@ -583,6 +584,7 @@ export function client_module(analysis, options) {
legacy_reactive_statements: new Map(),
public_state: new Map(),
private_state: new Map(),
getters: {},
in_constructor: false
};

@ -3,7 +3,8 @@ import type {
Statement,
LabeledStatement,
Identifier,
PrivateIdentifier
PrivateIdentifier,
Expression
} from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -22,6 +23,11 @@ export interface ClientTransformState extends TransformState {
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
/**
* A map of `[name, node]` pairs, where `Identifier` nodes matching `name`
* will be replaced with `node` (e.g. `x` -> `$.get(x)`)
*/
readonly getters: Record<string, Expression | ((id: Identifier) => Expression)>;
}
export interface ComponentClientTransformState extends ClientTransformState {

@ -1,3 +1,7 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BinaryOperator, Expression, FunctionDeclaration, FunctionExpression, Identifier, MemberExpression, Node, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { Binding, SvelteNode } from '#compiler' */
/** @import { ClientTransformState, ComponentClientTransformState, ComponentContext } from './types.js' */
/** @import { Scope } from '../../scope.js' */
import * as b from '../../../utils/builders.js';
import {
extract_identifiers,
@ -14,21 +18,21 @@ import {
} from '../../../../constants.js';
/**
* @template {import('./types').ClientTransformState} State
* @param {import('estree').AssignmentExpression} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
* @template {ClientTransformState} State
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, State>} context
* @returns
*/
export function get_assignment_value(node, { state, visit }) {
if (node.left.type === 'Identifier') {
const operator = node.operator;
return operator === '='
? /** @type {import('estree').Expression} */ (visit(node.right))
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {import('estree').BinaryOperator} */ (operator.slice(0, -1)),
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
serialize_get_binding(node.left, state),
/** @type {import('estree').Expression} */ (visit(node.right))
/** @type {Expression} */ (visit(node.right))
);
} else if (
node.left.type === 'MemberExpression' &&
@ -38,21 +42,21 @@ export function get_assignment_value(node, { state, visit }) {
) {
const operator = node.operator;
return operator === '='
? /** @type {import('estree').Expression} */ (visit(node.right))
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {import('estree').BinaryOperator} */ (operator.slice(0, -1)),
/** @type {import('estree').Expression} */ (visit(node.left)),
/** @type {import('estree').Expression} */ (visit(node.right))
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
);
} else {
return /** @type {import('estree').Expression} */ (visit(node.right));
return /** @type {Expression} */ (visit(node.right));
}
}
/**
* @param {import('#compiler').Binding} binding
* @param {import('./types').ClientTransformState} state
* @param {Binding} binding
* @param {ClientTransformState} state
* @returns {boolean}
*/
export function is_state_source(binding, state) {
@ -63,9 +67,9 @@ export function is_state_source(binding, state) {
}
/**
* @param {import('estree').Identifier} node
* @param {import('./types').ClientTransformState} state
* @returns {import('estree').Expression}
* @param {Identifier} node
* @param {ClientTransformState} state
* @returns {Expression}
*/
export function serialize_get_binding(node, state) {
const binding = state.scope.get(node.name);
@ -84,8 +88,9 @@ export function serialize_get_binding(node, state) {
return b.call(node);
}
if (binding.expression) {
return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression;
if (Object.hasOwn(state.getters, node.name)) {
const getter = state.getters[node.name];
return typeof getter === 'function' ? getter(node) : getter;
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
@ -120,13 +125,13 @@ export function serialize_get_binding(node, state) {
}
/**
* @template {import('./types').ClientTransformState} State
* @param {import('estree').AssignmentExpression} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
* @template {ClientTransformState} State
* @param {AssignmentExpression} node
* @param {import('zimmerframe').Context<SvelteNode, State>} context
* @param {() => any} fallback
* @param {boolean | null} [prefix] - If the assignment is a transformed update expression, set this. Else `null`
* @param {{skip_proxy_and_freeze?: boolean}} [options]
* @returns {import('estree').Expression}
* @returns {Expression}
*/
export function serialize_set_binding(node, context, fallback, prefix, options) {
const { state, visit } = context;
@ -140,10 +145,10 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
const tmp_id = context.state.scope.generate('tmp');
/** @type {import('estree').AssignmentExpression[]} */
/** @type {AssignmentExpression[]} */
const original_assignments = [];
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const assignments = [];
const paths = extract_paths(assignee);
@ -162,7 +167,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
return fallback();
}
const rhs_expression = /** @type {import('estree').Expression} */ (visit(node.right));
const rhs_expression = /** @type {Expression} */ (visit(node.right));
const iife_is_async =
is_expression_async(rhs_expression) ||
@ -274,8 +279,8 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
'$$_import_' + binding.node.name,
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
/** @type {import('estree').Expression} */ (visit(node.right))
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
@ -303,10 +308,7 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
if (left === node.left) {
const is_initial_proxy =
binding.initial !== null &&
should_proxy_or_freeze(
/**@type {import("estree").Expression}*/ (binding.initial),
context.state.scope
);
should_proxy_or_freeze(/**@type {Expression}*/ (binding.initial), context.state.scope);
if (
(binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
@ -370,38 +372,31 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
// keep consistency with how store $ shorthand reads work in Svelte 4.
/**
*
* @param {import("estree").Expression | import("estree").Pattern} node
* @returns {import("estree").Expression}
* @param {Expression | Pattern} node
* @returns {Expression}
*/
function visit_node(node) {
if (node.type === 'MemberExpression') {
return {
...node,
object: visit_node(/** @type {import("estree").Expression} */ (node.object)),
property: /** @type {import("estree").MemberExpression} */ (visit(node)).property
object: visit_node(/** @type {Expression} */ (node.object)),
property: /** @type {MemberExpression} */ (visit(node)).property
};
}
if (node.type === 'Identifier') {
const binding = state.scope.get(node.name);
if (binding !== null && binding.kind === 'store_sub') {
return b.call(
'$.untrack',
b.thunk(/** @type {import('estree').Expression} */ (visit(node)))
);
return b.call('$.untrack', b.thunk(/** @type {Expression} */ (visit(node))));
}
}
return /** @type {import("estree").Expression} */ (visit(node));
return /** @type {Expression} */ (visit(node));
}
return b.call(
'$.mutate_store',
'$.store_mutate',
serialize_get_binding(b.id(left_name), state),
b.assignment(
node.operator,
/** @type {import("estree").Pattern}} */ (visit_node(node.left)),
value
),
b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value),
b.call('$.untrack', b.id('$' + left_name))
);
} else if (
@ -412,22 +407,14 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
if (binding.kind === 'bindable_prop') {
return b.call(
left,
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
value
),
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value),
b.true
);
} else {
return b.call(
'$.mutate',
b.id(left_name),
b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
value
)
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
);
}
} else if (
@ -437,14 +424,14 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
) {
return b.update(
node.operator === '+=' ? '++' : '--',
/** @type {import('estree').Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.left)),
prefix
);
} else {
return b.assignment(
node.operator,
/** @type {import('estree').Pattern} */ (visit(node.left)),
/** @type {import('estree').Expression} */ (visit(node.right))
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
);
}
}
@ -458,9 +445,9 @@ export function serialize_set_binding(node, context, fallback, prefix, options)
}
/**
* @param {import('estree').Expression} value
* @param {import('estree').PrivateIdentifier | string} proxy_reference
* @param {import('./types').ClientTransformState} state
* @param {Expression} value
* @param {PrivateIdentifier | string} proxy_reference
* @param {ClientTransformState} state
*/
export function serialize_proxy_reassignment(value, proxy_reference, state) {
return state.options.dev
@ -476,8 +463,8 @@ export function serialize_proxy_reassignment(value, proxy_reference, state) {
}
/**
* @param {import('estree').ArrowFunctionExpression | import('estree').FunctionExpression} node
* @param {import('./types').ComponentContext} context
* @param {ArrowFunctionExpression | FunctionExpression} node
* @param {ComponentContext} context
*/
export const function_visitor = (node, context) => {
const metadata = node.metadata;
@ -485,7 +472,7 @@ export const function_visitor = (node, context) => {
let state = context.state;
if (node.type === 'FunctionExpression') {
const parent = /** @type {import('estree').Node} */ (context.path.at(-1));
const parent = /** @type {Node} */ (context.path.at(-1));
const in_constructor = parent.type === 'MethodDefinition' && parent.kind === 'constructor';
state = { ...context.state, in_constructor };
@ -496,7 +483,7 @@ export const function_visitor = (node, context) => {
if (metadata?.hoistable === true) {
const params = serialize_hoistable_params(node, context);
return /** @type {import('estree').FunctionExpression} */ ({
return /** @type {FunctionExpression} */ ({
...node,
params,
body: context.visit(node.body, state)
@ -507,19 +494,19 @@ export const function_visitor = (node, context) => {
};
/**
* @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression | import('estree').ArrowFunctionExpression} node
* @param {import('./types').ComponentContext} context
* @returns {import('estree').Pattern[]}
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
function get_hoistable_params(node, context) {
const scope = context.state.scope;
/** @type {import('estree').Identifier[]} */
/** @type {Identifier[]} */
const params = [];
/**
* We only want to push if it's not already present to avoid name clashing
* @param {import('estree').Identifier} id
* @param {Identifier} id
*/
function push_unique(id) {
if (!params.find((param) => param.name === id.name)) {
@ -534,22 +521,23 @@ function get_hoistable_params(node, context) {
if (binding.kind === 'store_sub') {
// We need both the subscription for getting the value and the store for updating
push_unique(b.id(binding.node.name));
binding = /** @type {import('#compiler').Binding} */ (
scope.get(binding.node.name.slice(1))
);
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
const expression = context.state.getters[reference];
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
binding.expression !== null &&
typeof binding.expression !== 'function' &&
binding.expression.type === 'MemberExpression' &&
binding.expression.object.type === 'CallExpression' &&
binding.expression.object.callee.type === 'Identifier' &&
binding.expression.object.callee.name === '$.get' &&
binding.expression.object.arguments[0].type === 'Identifier'
// TODO this code is bad, we need to kill it
expression != null &&
typeof expression !== 'function' &&
expression.type === 'MemberExpression' &&
expression.object.type === 'CallExpression' &&
expression.object.callee.type === 'Identifier' &&
expression.object.callee.name === '$.get' &&
expression.object.arguments[0].type === 'Identifier'
) {
push_unique(b.id(binding.expression.object.arguments[0].name));
push_unique(b.id(expression.object.arguments[0].name));
} else if (
// If we are referencing a simple $$props value, then we need to reference the object property instead
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
@ -574,15 +562,15 @@ function get_hoistable_params(node, context) {
}
/**
* @param {import('estree').FunctionDeclaration | import('estree').FunctionExpression | import('estree').ArrowFunctionExpression} node
* @param {import('./types').ComponentContext} context
* @returns {import('estree').Pattern[]}
* @param {FunctionDeclaration | FunctionExpression | ArrowFunctionExpression} node
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
export function serialize_hoistable_params(node, context) {
const hoistable_params = get_hoistable_params(node, context);
node.metadata.hoistable_params = hoistable_params;
/** @type {import('estree').Pattern[]} */
/** @type {Pattern[]} */
const params = [];
if (node.params.length === 0) {
@ -592,7 +580,7 @@ export function serialize_hoistable_params(node, context) {
}
} else {
for (const param of node.params) {
params.push(/** @type {import('estree').Pattern} */ (context.visit(param)));
params.push(/** @type {Pattern} */ (context.visit(param)));
}
}
@ -601,14 +589,14 @@ export function serialize_hoistable_params(node, context) {
}
/**
* @param {import('#compiler').Binding} binding
* @param {import('./types').ComponentClientTransformState} state
* @param {Binding} binding
* @param {ComponentClientTransformState} state
* @param {string} name
* @param {import('estree').Expression | null} [initial]
* @param {Expression | null} [initial]
* @returns
*/
export function get_prop_source(binding, state, name, initial) {
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const args = [b.id('$$props'), b.literal(name)];
let flags = 0;
@ -630,7 +618,7 @@ export function get_prop_source(binding, state, name, initial) {
flags |= PROPS_IS_UPDATED;
}
/** @type {import('estree').Expression | undefined} */
/** @type {Expression | undefined} */
let arg;
if (initial) {
@ -662,8 +650,8 @@ export function get_prop_source(binding, state, name, initial) {
/**
*
* @param {import('#compiler').Binding} binding
* @param {import('./types').ClientTransformState} state
* @param {Binding} binding
* @param {ClientTransformState} state
* @returns
*/
export function is_prop_source(binding, state) {
@ -680,8 +668,8 @@ export function is_prop_source(binding, state) {
}
/**
* @param {import('estree').Expression} node
* @param {import("../../scope.js").Scope | null} scope
* @param {Expression} node
* @param {Scope | null} scope
*/
export function should_proxy_or_freeze(node, scope) {
if (
@ -718,8 +706,8 @@ export function should_proxy_or_freeze(node, scope) {
* Port over the location information from the source to the target identifier.
* but keep the target as-is (i.e. a new id is created).
* This ensures esrap can generate accurate source maps.
* @param {import('estree').Identifier} target
* @param {import('estree').Identifier} source
* @param {Identifier} target
* @param {Identifier} source
*/
export function with_loc(target, source) {
if (source.loc) {
@ -729,16 +717,16 @@ export function with_loc(target, source) {
}
/**
* @param {import("estree").Pattern} node
* @param {import("zimmerframe").Context<import("#compiler").SvelteNode, import("./types").ComponentClientTransformState>} context
* @returns {{ id: import("estree").Pattern, declarations: null | import("estree").Statement[] }}
* @param {Pattern} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
* @returns {{ id: Pattern, declarations: null | Statement[] }}
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
return { id: node, declarations: null };
}
const pattern = /** @type {import('estree').Pattern} */ (context.visit(node));
const pattern = /** @type {Pattern} */ (context.visit(node));
const identifiers = extract_identifiers(node);
const id = b.id('$$source');
@ -762,8 +750,8 @@ export function create_derived_block_argument(node, context) {
/**
* Svelte legacy mode should use safe equals in most places, runes mode shouldn't
* @param {import('./types.js').ComponentClientTransformState} state
* @param {import('estree').Expression} arg
* @param {ComponentClientTransformState} state
* @param {Expression} arg
*/
export function create_derived(state, arg) {
return b.call(state.analysis.runes ? '$.derived' : '$.derived_safe_equal', arg);

@ -1,11 +1,13 @@
/** @import { Expression, Node, Pattern, Statement } from 'estree' */
/** @import { Visitors } from '../types' */
import is_reference from 'is-reference';
import { serialize_get_binding, serialize_set_binding } from '../utils.js';
import * as b from '../../../../utils/builders.js';
/** @type {import('../types').Visitors} */
/** @type {Visitors} */
export const global_visitors = {
Identifier(node, { path, state }) {
if (is_reference(node, /** @type {import('estree').Node} */ (path.at(-1)))) {
if (is_reference(node, /** @type {Node} */ (path.at(-1)))) {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
@ -75,7 +77,7 @@ export const global_visitors = {
binding?.kind === 'bindable_prop' ||
is_store
) {
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const args = [];
let fn = '$.update';
@ -107,7 +109,7 @@ export const global_visitors = {
let fn = '$.update';
if (node.prefix) fn += '_pre';
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const args = [argument];
if (node.operator === '--') {
args.push(b.literal(-1));
@ -118,7 +120,7 @@ export const global_visitors = {
// turn it into an IIFEE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; })
const assignment = b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {import('estree').Pattern} */ (argument),
/** @type {Pattern} */ (argument),
b.literal(1)
);
const serialized_assignment = serialize_set_binding(
@ -127,14 +129,14 @@ export const global_visitors = {
() => assignment,
node.prefix
);
const value = /** @type {import('estree').Expression} */ (visit(argument));
const value = /** @type {Expression} */ (visit(argument));
if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression
return next();
} else if (context.state.analysis.runes) {
return serialized_assignment;
} else {
/** @type {import('estree').Statement[]} */
/** @type {Statement[]} */
let statements;
if (node.prefix) {
statements = [b.stmt(serialized_assignment), b.return(value)];

@ -1,3 +1,6 @@
/** @import { CallExpression, Expression, Identifier, Literal, MethodDefinition, PrivateIdentifier, PropertyDefinition, VariableDeclarator } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { ComponentVisitors, StateField } from '../types.js' */
import { get_rune } from '../../../scope.js';
import { is_hoistable_function, transform_inspect_rune } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
@ -12,13 +15,13 @@ import {
import { extract_paths } from '../../../../utils/ast.js';
import { regex_invalid_identifier_chars } from '../../../patterns.js';
/** @type {import('../types.js').ComponentVisitors} */
/** @type {ComponentVisitors} */
export const javascript_visitors_runes = {
ClassBody(node, { state, visit }) {
/** @type {Map<string, import('../types.js').StateField>} */
/** @type {Map<string, StateField>} */
const public_state = new Map();
/** @type {Map<string, import('../types.js').StateField>} */
/** @type {Map<string, StateField>} */
const private_state = new Map();
/** @type {string[]} */
@ -47,7 +50,7 @@ export const javascript_visitors_runes = {
rune === '$derived' ||
rune === '$derived.by'
) {
/** @type {import('../types.js').StateField} */
/** @type {StateField} */
const field = {
kind:
rune === '$state'
@ -84,7 +87,7 @@ export const javascript_visitors_runes = {
field.id = b.private_id(deconflicted);
}
/** @type {Array<import('estree').MethodDefinition | import('estree').PropertyDefinition>} */
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
const child_state = { ...state, public_state, private_state };
@ -107,7 +110,7 @@ export const javascript_visitors_runes = {
let value = null;
if (definition.value.arguments.length > 0) {
const init = /** @type {import('estree').Expression} **/ (
const init = /** @type {Expression} **/ (
visit(definition.value.arguments[0], child_state)
);
@ -208,7 +211,7 @@ export const javascript_visitors_runes = {
}
}
body.push(/** @type {import('estree').MethodDefinition} **/ (visit(definition, child_state)));
body.push(/** @type {MethodDefinition} **/ (visit(definition, child_state)));
}
if (state.options.dev && public_state.size > 0) {
@ -251,15 +254,11 @@ export const javascript_visitors_runes = {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
b.declaration(
'const',
declarator.id,
/** @type {import('estree').Expression} */ (hoistable_function)
)
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
);
continue;
}
declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator)));
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
}
@ -272,7 +271,7 @@ export const javascript_visitors_runes = {
}
if (declarator.id.type === 'Identifier') {
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
@ -286,9 +285,7 @@ export const javascript_visitors_runes = {
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const key = /** @type {Identifier | Literal} */ (property.key);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
@ -296,10 +293,8 @@ export const javascript_visitors_runes = {
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
const binding = /** @type {Binding} */ (state.scope.get(id.name));
let initial = binding.initial && /** @type {Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (
@ -315,14 +310,12 @@ export const javascript_visitors_runes = {
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
/** @type {Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
);
args.push(b.literal(/** @type {Identifier} */ (property.argument).name));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
@ -334,19 +327,17 @@ export const javascript_visitors_runes = {
continue;
}
const args = /** @type {import('estree').CallExpression} */ (init).arguments;
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0
? b.id('undefined')
: /** @type {import('estree').Expression} */ (visit(args[0]));
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (visit(args[0]));
if (rune === '$state' || rune === '$state.frozen' || rune === '$state.link') {
/**
* @param {import('estree').Identifier} id
* @param {import('estree').Expression} value
* @param {Identifier} id
* @param {Expression} value
*/
const create_state_declarator = (id, value) => {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
const binding = /** @type {Binding} */ (state.scope.get(id.name));
if (should_proxy_or_freeze(value, state.scope)) {
value = b.call(
rune === '$state' || rune === '$state.link' ? '$.proxy' : '$.freeze',
@ -373,9 +364,7 @@ export const javascript_visitors_runes = {
b.declarator(b.id(tmp), value),
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
const binding = state.scope.get(
/** @type {import('estree').Identifier} */ (path.node).name
);
const binding = state.scope.get(/** @type {Identifier} */ (path.node).name);
return b.declarator(
path.node,
binding?.kind === 'state' || binding?.kind === 'frozen_state'
@ -458,7 +447,7 @@ export const javascript_visitors_runes = {
const func = context.visit(node.expression.arguments[0]);
return {
...node,
expression: b.call('$.user_effect', /** @type {import('estree').Expression} */ (func))
expression: b.call('$.user_effect', /** @type {Expression} */ (func))
};
}
@ -473,7 +462,7 @@ export const javascript_visitors_runes = {
const func = context.visit(node.expression.arguments[0]);
return {
...node,
expression: b.call('$.user_pre_effect', /** @type {import('estree').Expression} */ (func))
expression: b.call('$.user_pre_effect', /** @type {Expression} */ (func))
};
}
}
@ -492,24 +481,19 @@ export const javascript_visitors_runes = {
}
if (rune === '$state.snapshot') {
return b.call(
'$.snapshot',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0]))
);
return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0])));
}
if (rune === '$state.is') {
return b.call(
'$.is',
/** @type {import('estree').Expression} */ (context.visit(node.arguments[0])),
/** @type {import('estree').Expression} */ (context.visit(node.arguments[1]))
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
}
if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg))
);
const args = /** @type {Expression[]} */ (node.arguments.map((arg) => context.visit(arg)));
return b.call('$.effect_root', ...args);
}
@ -526,8 +510,8 @@ export const javascript_visitors_runes = {
if (operator === '===' || operator === '!==') {
return b.call(
'$.strict_equals',
/** @type {import('estree').Expression} */ (visit(node.left)),
/** @type {import('estree').Expression} */ (visit(node.right)),
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right)),
operator === '!==' && b.literal(false)
);
}
@ -535,8 +519,8 @@ export const javascript_visitors_runes = {
if (operator === '==' || operator === '!=') {
return b.call(
'$.equals',
/** @type {import('estree').Expression} */ (visit(node.left)),
/** @type {import('estree').Expression} */ (visit(node.right)),
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right)),
operator === '!=' && b.literal(false)
);
}
@ -547,7 +531,7 @@ export const javascript_visitors_runes = {
};
/**
* @param {import('estree').Identifier | import('estree').PrivateIdentifier | import('estree').Literal} node
* @param {Identifier | PrivateIdentifier | Literal} node
*/
function get_name(node) {
if (node.type === 'Literal') {

@ -1,6 +1,8 @@
/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { BindDirective } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types' */
/** @import { Attribute, BindDirective, Binding, ClassDirective, Component, DelegatedEvent, EachBlock, ExpressionTag, Namespace, OnDirective, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text } from '#compiler' */
/** @import { SourceLocation } from '#shared' */
/** @import { Scope } from '../../../scope.js' */
/** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */
import {
extract_identifiers,
extract_paths,
@ -52,11 +54,12 @@ import { javascript_visitors_runes } from './javascript-runes.js';
import { sanitize_template_string } from '../../../../utils/sanitize_template_string.js';
import { walk } from 'zimmerframe';
import { locator } from '../../../../state.js';
import is_reference from 'is-reference';
/**
* @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element
* @param {import('#compiler').Attribute} attribute
* @param {{ state: { metadata: { namespace: import('#compiler').Namespace }}}} context
* @param {RegularElement | SvelteElement} element
* @param {Attribute} attribute
* @param {{ state: { metadata: { namespace: Namespace }}}} context
*/
function get_attribute_name(element, attribute, context) {
let name = attribute.name;
@ -76,9 +79,9 @@ function get_attribute_name(element, attribute, context) {
/**
* Serializes each style directive into something like `$.set_style(element, style_property, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {import('#compiler').StyleDirective[]} style_directives
* @param {StyleDirective[]} style_directives
* @param {Identifier} element_id
* @param {import('../types.js').ComponentContext} context
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
function serialize_style_directives(style_directives, element_id, context, is_attributes_reactive) {
@ -138,9 +141,9 @@ function parse_directive_name(name) {
/**
* Serializes each class directive into something like `$.class_toogle(element, class_name, value)`
* and adds it either to init or update, depending on whether or not the value or the attributes are dynamic.
* @param {import('#compiler').ClassDirective[]} class_directives
* @param {ClassDirective[]} class_directives
* @param {Identifier} element_id
* @param {import('../types.js').ComponentContext} context
* @param {ComponentContext} context
* @param {boolean} is_attributes_reactive
*/
function serialize_class_directives(class_directives, element_id, context, is_attributes_reactive) {
@ -161,11 +164,11 @@ function serialize_class_directives(class_directives, element_id, context, is_at
}
/**
* @param {import('#compiler').Binding[]} references
* @param {import('../types.js').ComponentContext} context
* @param {Binding[]} references
* @param {ComponentContext} context
*/
function serialize_transitive_dependencies(references, context) {
/** @type {Set<import('#compiler').Binding>} */
/** @type {Set<Binding>} */
const dependencies = new Set();
for (const ref of references) {
@ -179,9 +182,9 @@ function serialize_transitive_dependencies(references, context) {
}
/**
* @param {import('#compiler').Binding} binding
* @param {Set<import('#compiler').Binding>} seen
* @returns {import('#compiler').Binding[]}
* @param {Binding} binding
* @param {Set<Binding>} seen
* @returns {Binding[]}
*/
function collect_transitive_dependencies(binding, seen = new Set()) {
if (binding.kind !== 'legacy_reactive') return [];
@ -201,8 +204,8 @@ function collect_transitive_dependencies(binding, seen = new Set()) {
/**
* Special case: if we have a value binding on a select element, we need to set up synchronization
* between the value binding and inner signals, for indirect updates
* @param {import('#compiler').BindDirective} value_binding
* @param {import('../types.js').ComponentContext} context
* @param {BindDirective} value_binding
* @param {ComponentContext} context
*/
function setup_select_synchronization(value_binding, context) {
if (context.state.analysis.runes) return;
@ -253,9 +256,9 @@ function setup_select_synchronization(value_binding, context) {
}
/**
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context
* @param {import('#compiler').RegularElement} element
* @param {Array<Attribute | SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {RegularElement} element
* @param {Identifier} element_id
* @param {boolean} needs_select_handling
*/
@ -358,8 +361,8 @@ function serialize_element_spread_attributes(
/**
* Serializes dynamic element attribute assignments.
* Returns the `true` if spread is deemed reactive.
* @param {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} attributes
* @param {import('../types.js').ComponentContext} context
* @param {Array<Attribute | SpreadAttribute>} attributes
* @param {ComponentContext} context
* @param {Identifier} element_id
* @returns {boolean}
*/
@ -472,10 +475,10 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) {
* });
* ```
* Returns true if attribute is deemed reactive, false otherwise.
* @param {import('#compiler').RegularElement} element
* @param {RegularElement} element
* @param {Identifier} node_id
* @param {import('#compiler').Attribute} attribute
* @param {import('../types.js').ComponentContext} context
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_element_attribute_update_assignment(element, node_id, attribute, context) {
@ -542,8 +545,8 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu
/**
* Like `serialize_element_attribute_update_assignment` but without any special attribute treatment.
* @param {Identifier} node_id
* @param {import('#compiler').Attribute} attribute
* @param {import('../types.js').ComponentContext} context
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_custom_element_attribute_update_assignment(node_id, attribute, context) {
@ -572,8 +575,8 @@ function serialize_custom_element_attribute_update_assignment(node_id, attribute
* Returns true if attribute is deemed reactive, false otherwise.
* @param {string} element
* @param {Identifier} node_id
* @param {import('#compiler').Attribute} attribute
* @param {import('../types.js').ComponentContext} context
* @param {Attribute} attribute
* @param {ComponentContext} context
* @returns {boolean}
*/
function serialize_element_special_value_attribute(element, node_id, attribute, context) {
@ -632,7 +635,7 @@ function serialize_element_special_value_attribute(element, node_id, attribute,
}
/**
* @param {import('../types.js').ComponentClientTransformState} state
* @param {ComponentClientTransformState} state
* @param {string} id
* @param {Expression | undefined} init
* @param {Expression} value
@ -646,18 +649,16 @@ function serialize_update_assignment(state, id, init, value, update) {
}
/**
* @param {import('../types.js').ComponentContext} context
* @param {ComponentContext} context
*/
function collect_parent_each_blocks(context) {
return /** @type {import('#compiler').EachBlock[]} */ (
context.path.filter((node) => node.type === 'EachBlock')
);
return /** @type {EachBlock[]} */ (context.path.filter((node) => node.type === 'EachBlock'));
}
/**
* @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {string} component_name
* @param {import('../types.js').ComponentContext} context
* @param {ComponentContext} context
* @param {Expression} anchor
* @returns {Statement}
*/
@ -668,7 +669,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/** @type {ExpressionStatement[]} */
const lets = [];
/** @type {Record<string, import('#compiler').TemplateNode[]>} */
/** @type {Record<string, TemplateNode[]>} */
const children = {};
/** @type {Record<string, Expression[]>} */
@ -681,7 +682,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
let bind_this = null;
/**
* @type {import("estree").ExpressionStatement[]}
* @type {ExpressionStatement[]}
*/
const binding_initializers = [];
@ -715,7 +716,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
events[attribute.name] ||= [];
let handler = serialize_event_handler(attribute, context);
let handler = serialize_event_handler(attribute, null, context);
if (attribute.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
@ -844,14 +845,14 @@ function serialize_inline_component(node, component_name, context, anchor = cont
let slot_name = 'default';
if (is_element_node(child)) {
const attribute = /** @type {import('#compiler').Attribute | undefined} */ (
const attribute = /** @type {Attribute | undefined} */ (
child.attributes.find(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
);
if (attribute !== undefined) {
slot_name = /** @type {import('#compiler').Text[]} */ (attribute.value)[0].data;
slot_name = /** @type {Text[]} */ (attribute.value)[0].data;
}
}
@ -933,24 +934,14 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/** @param {Expression} node_id */
let fn = (node_id) => {
return b.call(
context.state.options.dev
? b.call('$.validate_component', b.id(component_name))
: component_name,
node_id,
props_expression
);
return b.call(component_name, node_id, props_expression);
};
if (bind_this !== null) {
const prev = fn;
fn = (node_id) => {
return serialize_bind_this(
/** @type {Identifier | MemberExpression} */ (bind_this),
context,
prev(node_id)
);
return serialize_bind_this(bind_this, prev(node_id), context);
};
}
@ -1003,75 +994,73 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} bind_this
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, import('../types.js').ComponentClientTransformState>} context
* @param {Expression} node
* @returns
* @param {Identifier | MemberExpression} expression
* @param {Expression} value
* @param {import('zimmerframe').Context<SvelteNode, ComponentClientTransformState>} context
*/
function serialize_bind_this(bind_this, context, node) {
let i = 0;
/** @type {Map<import('#compiler').Binding, [arg_idx: number, transformed: 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.
function serialize_bind_this(expression, value, { state, visit }) {
/** @type {Identifier[]} */
const ids = [];
/** @type {Expression[]} */
const values = [];
/** @type {typeof state.getters} */
const getters = {};
// Pass in each context variables to the get/set functions, so that we can null out old values on teardown.
// 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 {Expression} */ (context.visit(node)),
binding.expression
]);
binding.expression = b.id('$$value_' + i);
i++;
walk(expression, null, {
Identifier(node, { path }) {
if (Object.hasOwn(getters, node.name)) return;
const parent = /** @type {Expression} */ (path.at(-1));
if (!is_reference(node, parent)) return;
const binding = state.scope.get(node.name);
if (!binding) return;
for (const [owner, scope] of state.scopes) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
values.push(/** @type {Expression} */ (visit(node)));
getters[node.name] = node;
break;
}
}
}
);
});
const bind_this_id = /** @type {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));
const child_state = { ...state, getters: { ...state.getters, ...getters } };
for (const [binding, [, , expression]] of each_ids) {
// reset expressions to what they were before
binding.expression = expression;
}
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (
visit(b.assignment('=', expression, b.id('$$value')), child_state)
);
/** @type {Expression[]} */
const args = [node, b.arrow([b.id('$$value'), ...ids], update), b.arrow([...ids], bind_this_id)];
// If we're mutating a property, then it might already be non-existent.
// If we make all the object nodes optional, then it avoids any runtime exceptions.
/** @type {Expression | Super} */
let bind_node = bind_this_id;
let node = get;
while (bind_node?.type === 'MemberExpression') {
bind_node.optional = true;
bind_node = bind_node.object;
}
if (each_ids.size) {
args.push(b.thunk(b.array(Array.from(each_ids.values()).map((id) => id[1]))));
while (node.type === 'MemberExpression') {
node.optional = true;
node = node.object;
}
return b.call('$.bind_this', ...args);
return b.call(
'$.bind_this',
value,
b.arrow([b.id('$$value'), ...ids], set),
b.arrow([...ids], get),
values.length > 0 && b.thunk(b.array(values))
);
}
/**
* @param {import('#shared').SourceLocation[]} locations
* @param {SourceLocation[]} locations
*/
function serialize_locations(locations) {
return b.array(
@ -1089,8 +1078,8 @@ function serialize_locations(locations) {
/**
*
* @param {import('#compiler').Namespace} namespace
* @param {import('../types.js').ComponentClientTransformState} state
* @param {Namespace} namespace
* @param {ComponentClientTransformState} state
* @returns
*/
function get_template_function(namespace, state) {
@ -1128,10 +1117,11 @@ function serialize_render_stmt(update) {
/**
* Serializes the event handler function of the `on:` directive
* @param {Pick<import('#compiler').OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {import('../types.js').ComponentContext} context
* @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {null | { contains_call_expression: boolean; dynamic: boolean; } | null} metadata
* @param {ComponentContext} context
*/
function serialize_event_handler(node, { state, visit }) {
function serialize_event_handler(node, metadata, { state, visit }) {
/** @type {Expression} */
let handler;
@ -1144,14 +1134,44 @@ function serialize_event_handler(node, { state, visit }) {
null,
[b.rest(b.id('$$args'))],
b.block([
b.const('$$callback', /** @type {Expression} */ (visit(handler))),
b.return(
b.call(b.member(b.id('$$callback'), b.id('apply'), false, true), b.this, b.id('$$args'))
b.call(
b.member(/** @type {Expression} */ (visit(handler)), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
if (handler.type === 'Identifier' || handler.type === 'MemberExpression') {
if (
metadata?.contains_call_expression &&
!(
(handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') &&
handler.metadata.hoistable
)
) {
// Create a derived dynamic event handler
const id = b.id(state.scope.generate('event_handler'));
state.init.push(
b.var(id, b.call('$.derived', b.thunk(/** @type {Expression} */ (visit(handler)))))
);
handler = b.function(
null,
[b.rest(b.id('$$args'))],
b.block([
b.return(
b.call(
b.member(b.call('$.get', id), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
} else if (handler.type === 'Identifier' || handler.type === 'MemberExpression') {
const id = object(handler);
const binding = id === null ? null : state.scope.get(id.name);
if (
@ -1206,17 +1226,18 @@ function serialize_event_handler(node, { state, visit }) {
/**
* Serializes an event handler function of the `on:` directive or an attribute starting with `on`
* @param {{name: string; modifiers: string[]; expression: Expression | null; delegated?: import('#compiler').DelegatedEvent | null; }} node
* @param {import('../types.js').ComponentContext} context
* @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: DelegatedEvent | null;}} node
* @param {null | { contains_call_expression: boolean; dynamic: boolean; }} metadata
* @param {ComponentContext} context
*/
function serialize_event(node, context) {
function serialize_event(node, metadata, context) {
const state = context.state;
/** @type {Statement} */
let statement;
/** @type {Expression} */
let expression;
if (node.expression) {
let handler = serialize_event_handler(node, context);
let handler = serialize_event_handler(node, metadata, context);
const event_name = node.name;
const delegated = node.delegated;
@ -1282,14 +1303,23 @@ function serialize_event(node, context) {
}
// Events need to run in order with bindings/actions
statement = b.stmt(b.call('$.event', ...args));
expression = b.call('$.event', ...args);
} else {
statement = b.stmt(
b.call('$.event', b.literal(node.name), state.node, serialize_event_handler(node, context))
expression = b.call(
'$.event',
b.literal(node.name),
state.node,
serialize_event_handler(node, metadata, context)
);
}
const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1));
const parent = /** @type {SvelteNode} */ (context.path.at(-1));
const has_action_directive =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
const statement = b.stmt(
has_action_directive ? b.call('$.effect', b.thunk(expression)) : expression
);
if (
parent.type === 'SvelteDocument' ||
parent.type === 'SvelteWindow' ||
@ -1303,8 +1333,8 @@ function serialize_event(node, context) {
}
/**
* @param {import('#compiler').Attribute & { value: import('#compiler').ExpressionTag | [import('#compiler').ExpressionTag] }} node
* @param {import('../types').ComponentContext} context
* @param {Attribute & { value: ExpressionTag | [ExpressionTag] }} node
* @param {ComponentContext} context
*/
function serialize_event_attribute(node, context) {
/** @type {string[]} */
@ -1323,6 +1353,7 @@ function serialize_event_attribute(node, context) {
modifiers,
delegated: node.metadata.delegated
},
!Array.isArray(node.value) && node.value?.type === 'ExpressionTag' ? node.value.metadata : null,
context
);
}
@ -1331,15 +1362,15 @@ function serialize_event_attribute(node, context) {
* Processes an array of template nodes, joining sibling text/expression nodes
* (e.g. `{a} b {c}`) into a single update function. Along the way it creates
* corresponding template node references these updates are applied to.
* @param {import('#compiler').SvelteNode[]} nodes
* @param {SvelteNode[]} nodes
* @param {(is_text: boolean) => Expression} expression
* @param {boolean} is_element
* @param {import('../types.js').ComponentContext} context
* @param {ComponentContext} context
*/
function process_children(nodes, expression, is_element, { visit, state }) {
const within_bound_contenteditable = state.metadata.bound_contenteditable;
/** @typedef {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} Sequence */
/** @typedef {Array<Text | ExpressionTag>} Sequence */
/** @type {Sequence} */
let sequence = [];
@ -1363,7 +1394,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
const text_id = get_node_id(expression(true), state, 'text');
const update = b.stmt(
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression)))
b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state)))
);
if (node.metadata.contains_call_expression && !within_bound_contenteditable) {
@ -1465,7 +1496,7 @@ function process_children(nodes, expression, is_element, { visit, state }) {
/**
* @param {Expression} expression
* @param {import('../types.js').ComponentClientTransformState} state
* @param {ComponentClientTransformState} state
* @param {string} name
*/
function get_node_id(expression, state, name) {
@ -1480,8 +1511,8 @@ function get_node_id(expression, state, name) {
}
/**
* @param {import('#compiler').Attribute['value']} value
* @param {import('../types').ComponentContext} context
* @param {Attribute['value']} value
* @param {ComponentContext} context
* @returns {[contains_call_expression: boolean, Expression]}
*/
function serialize_attribute_value(value, context) {
@ -1506,9 +1537,9 @@ function serialize_attribute_value(value, context) {
}
/**
* @param {Array<import('#compiler').Text | import('#compiler').ExpressionTag>} values
* @param {(node: import('#compiler').SvelteNode, state: any) => any} visit
* @param {import("../types.js").ComponentClientTransformState} state
* @param {Array<Text | ExpressionTag>} values
* @param {(node: SvelteNode, state: any) => any} visit
* @param {ComponentClientTransformState} state
* @returns {[boolean, TemplateLiteral]}
*/
function serialize_template_literal(values, visit, state) {
@ -1568,7 +1599,7 @@ function serialize_template_literal(values, visit, state) {
return [contains_call_expression, b.template(quasis, expressions)];
}
/** @type {import('../types').ComponentVisitors} */
/** @type {ComponentVisitors} */
export const template_visitors = {
Fragment(node, context) {
// Creates a new block which looks roughly like this:
@ -1614,7 +1645,7 @@ export const template_visitors = {
/** @type {Statement | undefined} */
let close = undefined;
/** @type {import('../types').ComponentClientTransformState} */
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
before_init: [],
@ -1623,6 +1654,7 @@ export const template_visitors = {
after_update: [],
template: [],
locations: [],
getters: { ...context.state.getters },
metadata: {
context: {
template_needs_import_node: false,
@ -1661,7 +1693,7 @@ export const template_visitors = {
};
if (is_single_element) {
const element = /** @type {import('#compiler').RegularElement} */ (trimmed[0]);
const element = /** @type {RegularElement} */ (trimmed[0]);
const id = b.id(context.state.scope.generate(element.name));
@ -1785,6 +1817,8 @@ export const template_visitors = {
)
);
state.getters[declaration.id.name] = b.call('$.get', declaration.id);
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (state.options.dev) {
@ -1794,21 +1828,24 @@ export const template_visitors = {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(state.scope.generate('computed_const'));
const getters = { ...state.getters };
// Make all identifiers that are declared within the following computed regular
// variables, as they are not signals in that context yet
for (const node of identifiers) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name));
binding.expression = node;
getters[node.name] = node;
}
const child_state = { ...state, getters };
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
const fn = b.arrow(
[],
b.block([
b.const(
/** @type {Pattern} */ (visit(declaration.id)),
/** @type {Expression} */ (visit(declaration.init))
/** @type {Pattern} */ (visit(declaration.id, child_state)),
/** @type {Expression} */ (visit(declaration.init, child_state))
),
b.return(b.object(identifiers.map((node) => b.prop('init', node, node))))
])
@ -1823,8 +1860,7 @@ export const template_visitors = {
}
for (const node of identifiers) {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.name));
binding.expression = b.member(b.call('$.get', tmp), node);
state.getters[node.name] = b.member(b.call('$.get', tmp), node);
}
}
},
@ -1872,9 +1908,6 @@ export const template_visitors = {
}
let snippet_function = /** @type {Expression} */ (context.visit(callee));
if (context.state.options.dev) {
snippet_function = b.call('$.validate_snippet', snippet_function);
}
if (node.metadata.dynamic) {
context.state.init.push(
@ -1935,7 +1968,7 @@ export const template_visitors = {
state.after_update.push(b.stmt(b.call('$.transition', ...args)));
},
RegularElement(node, context) {
/** @type {import('#shared').SourceLocation} */
/** @type {SourceLocation} */
let location = [-1, -1];
if (context.state.options.dev) {
@ -1963,13 +1996,13 @@ export const template_visitors = {
context.state.template.push(`<${node.name}`);
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
/** @type {Array<Attribute | SpreadAttribute>} */
const attributes = [];
/** @type {import('#compiler').ClassDirective[]} */
/** @type {ClassDirective[]} */
const class_directives = [];
/** @type {import('#compiler').StyleDirective[]} */
/** @type {StyleDirective[]} */
const style_directives = [];
/** @type {ExpressionStatement[]} */
@ -1979,7 +2012,7 @@ export const template_visitors = {
let needs_input_reset = false;
let needs_content_reset = false;
/** @type {import('#compiler').BindDirective | null} */
/** @type {BindDirective | null} */
let value_binding = null;
/** If true, needs `__value` for inputs */
@ -2101,7 +2134,7 @@ export const template_visitors = {
);
is_attributes_reactive = true;
} else {
for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) {
for (const attribute of /** @type {Attribute[]} */ (attributes)) {
if (is_event_attribute(attribute)) {
if (
(attribute.name === 'onload' || attribute.name === 'onerror') &&
@ -2163,17 +2196,15 @@ export const template_visitors = {
context.state.template.push('>');
/** @type {import('#shared').SourceLocation[]} */
/** @type {SourceLocation[]} */
const child_locations = [];
/** @type {import('../types').ComponentClientTransformState} */
/** @type {ComponentClientTransformState} */
const state = {
...context.state,
metadata: child_metadata,
locations: child_locations,
scope: /** @type {import('../../../scope').Scope} */ (
context.state.scopes.get(node.fragment)
),
scope: /** @type {Scope} */ (context.state.scopes.get(node.fragment)),
preserve_whitespace:
context.state.preserve_whitespace ||
((node.name === 'pre' || node.name === 'textarea') &&
@ -2256,16 +2287,16 @@ export const template_visitors = {
SvelteElement(node, context) {
context.state.template.push(`<!>`);
/** @type {Array<import('#compiler').Attribute | import('#compiler').SpreadAttribute>} */
/** @type {Array<Attribute | SpreadAttribute>} */
const attributes = [];
/** @type {import('#compiler').Attribute['value'] | undefined} */
/** @type {Attribute['value'] | undefined} */
let dynamic_namespace = undefined;
/** @type {import('#compiler').ClassDirective[]} */
/** @type {ClassDirective[]} */
const class_directives = [];
/** @type {import('#compiler').StyleDirective[]} */
/** @type {StyleDirective[]} */
const style_directives = [];
/** @type {ExpressionStatement[]} */
@ -2275,7 +2306,7 @@ export const template_visitors = {
// They'll then be added to the function parameter of $.element
const element_id = b.id(context.state.scope.generate('$$element'));
/** @type {import('../types').ComponentContext} */
/** @type {ComponentContext} */
const inner_context = {
...context,
state: {
@ -2456,9 +2487,14 @@ export const template_visitors = {
indirect_dependencies.push(...transitive_dependencies);
}
const child_state = {
...context.state,
getters: { ...context.state.getters }
};
/**
* @param {Pattern} expression_for_id
* @returns {import('#compiler').Binding['mutation']}
* @returns {Binding['mutation']}
*/
const create_mutation = (expression_for_id) => {
return (assignment, context) => {
@ -2503,18 +2539,15 @@ export const template_visitors = {
? each_node_meta.index
: b.id(node.index);
const item = each_node_meta.item;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(item.name));
binding.expression = (/** @type {import("estree").Identifier} */ id) => {
const binding = /** @type {Binding} */ (context.state.scope.get(item.name));
const getter = (/** @type {Identifier} */ id) => {
const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc);
};
child_state.getters[item.name] = getter;
if (node.index) {
const index_binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(node.index)
);
index_binding.expression = (id) => {
child_state.getters[node.index] = (id) => {
const index_with_loc = with_loc(index, id);
return b.call('$.unwrap', index_with_loc);
};
@ -2532,19 +2565,23 @@ export const template_visitors = {
)
);
} else {
const unwrapped = binding.expression(binding.node);
const unwrapped = getter(binding.node);
const paths = extract_paths(node.context);
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name));
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(/** @type {Expression} */ (context.visit(path.expression?.(unwrapped))));
const fn = b.thunk(
/** @type {Expression} */ (context.visit(path.expression?.(unwrapped), child_state))
);
declarations.push(
b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)
);
binding.expression = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
const getter = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
child_state.getters[name] = getter;
binding.mutation = create_mutation(
/** @type {Pattern} */ (path.update_expression(unwrapped))
);
@ -2552,21 +2589,23 @@ export const template_visitors = {
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (context.state.options.dev) {
declarations.push(b.stmt(binding.expression));
declarations.push(b.stmt(getter));
}
}
}
const block = /** @type {BlockStatement} */ (context.visit(node.body));
const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state));
const key_function = node.key
? b.arrow(
[node.context.type === 'Identifier' ? node.context : b.id('$$item'), index],
declarations.length > 0
? b.block(
declarations.concat(b.return(/** @type {Expression} */ (context.visit(node.key))))
declarations.concat(
b.return(/** @type {Expression} */ (context.visit(node.key, child_state)))
)
)
: /** @type {Expression} */ (context.visit(node.key))
: /** @type {Expression} */ (context.visit(node.key, child_state))
)
: b.id('$.index');
@ -2727,6 +2766,9 @@ export const template_visitors = {
/** @type {Statement[]} */
const declarations = [];
const getters = { ...context.state.getters };
const child_state = { ...context.state, getters };
for (let i = 0; i < node.parameters.length; i++) {
const argument = node.parameters[i];
@ -2738,10 +2780,8 @@ export const template_visitors = {
left: argument,
right: b.id('$.noop')
});
const binding = /** @type {import('#compiler').Binding} */ (
context.state.scope.get(argument.name)
);
binding.expression = b.call(argument);
getters[argument.name] = b.call(argument);
continue;
}
@ -2752,7 +2792,7 @@ export const template_visitors = {
for (const path of paths) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {import('#compiler').Binding} */ (context.state.scope.get(name));
const binding = /** @type {Binding} */ (context.state.scope.get(name));
const needs_derived = path.has_default_value; // to ensure that default value is only called once
const fn = b.thunk(
/** @type {Expression} */ (
@ -2763,19 +2803,20 @@ export const template_visitors = {
declarations.push(
b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn)
);
binding.expression = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
getters[name] = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (context.state.options.dev) {
declarations.push(b.stmt(binding.expression));
declarations.push(b.stmt(getters[name]));
}
}
}
body = b.block([
...declarations,
.../** @type {BlockStatement} */ (context.visit(node.body)).body
.../** @type {BlockStatement} */ (context.visit(node.body, child_state)).body
]);
/** @type {Expression} */
@ -2800,7 +2841,7 @@ export const template_visitors = {
context.next({ ...context.state, in_constructor: false });
},
OnDirective(node, context) {
serialize_event(node, context);
serialize_event(node, node.metadata, context);
},
UseDirective(node, { state, next, visit }) {
const params = [b.id('$$node')];
@ -2829,9 +2870,18 @@ export const template_visitors = {
BindDirective(node, context) {
const { state, path, visit } = context;
const expression = node.expression;
const property = binding_properties[node.name];
if (
expression.type === 'MemberExpression' &&
(node.name !== 'this' ||
path.some(
({ type }) =>
type === 'IfBlock' ||
type === 'EachBlock' ||
type === 'AwaitBlock' ||
type === 'KeyBlock'
)) &&
context.state.options.dev &&
context.state.analysis.runes
) {
@ -2862,8 +2912,7 @@ export const template_visitors = {
/** @type {CallExpression} */
let call_expr;
const property = binding_properties[node.name];
if (property && property.event) {
if (property?.event) {
call_expr = b.call(
'$.bind_property',
b.literal(node.name),
@ -2968,7 +3017,7 @@ export const template_visitors = {
break;
case 'this':
call_expr = serialize_bind_this(node.expression, context, state.node);
call_expr = serialize_bind_this(node.expression, state.node, context);
break;
case 'textContent':
case 'innerHTML':
@ -3002,7 +3051,7 @@ export const template_visitors = {
const parent = path.at(-1);
if (parent?.type === 'RegularElement') {
const value = /** @type {any[]} */ (
/** @type {import('#compiler').Attribute} */ (
/** @type {Attribute} */ (
parent.attributes.find(
(a) =>
a.type === 'Attribute' &&
@ -3038,12 +3087,20 @@ export const template_visitors = {
}
}
const parent = /** @type {import('#compiler').SvelteNode} */ (context.path.at(-1));
const has_action_directive =
parent.type === 'RegularElement' && parent.attributes.find((a) => a.type === 'UseDirective');
// Bindings need to happen after attribute updates, therefore after the render effect, and in order with events/actions.
// bind:this is a special case as it's one-way and could influence the render effect.
if (node.name === 'this') {
state.init.push(b.stmt(call_expr));
state.init.push(
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(call_expr)) : call_expr)
);
} else {
state.after_update.push(b.stmt(call_expr));
state.after_update.push(
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(call_expr)) : call_expr)
);
}
},
Component(node, context) {
@ -3090,7 +3147,10 @@ export const template_visitors = {
const bindings = state.scope.get_bindings(node);
for (const binding of bindings) {
binding.expression = b.member(b.call('$.get', b.id(name)), b.id(binding.node.name));
state.getters[binding.node.name] = b.member(
b.call('$.get', b.id(name)),
b.id(binding.node.name)
);
}
return b.const(
@ -3213,7 +3273,7 @@ export const template_visitors = {
b.assignment(
'=',
b.member(b.id('$.document'), b.id('title')),
b.literal(/** @type {import('#compiler').Text} */ (node.fragment.nodes[0]).data)
b.literal(/** @type {Text} */ (node.fragment.nodes[0]).data)
)
)
);
@ -3252,7 +3312,7 @@ export const template_visitors = {
};
/**
* @param {import('../types.js').ComponentClientTransformState} state
* @param {ComponentClientTransformState} state
* @param {BindDirective} binding
* @param {MemberExpression} expression
*/

@ -92,7 +92,7 @@ const visitors = {
next();
},
Declaration(node, { state, next }) {
Declaration(node, { state }) {
const property = node.property && remove_css_prefix(node.property.toLowerCase());
if (property === 'animation' || property === 'animation-name') {
let index = node.start + node.property.length + 1;
@ -140,7 +140,7 @@ const visitors = {
if (node.metadata.is_global_block) {
const selector = node.prelude.children[0];
if (selector.children.length === 1) {
if (selector.children.length === 1 && selector.children[0].selectors.length === 1) {
// `:global {...}`
state.code.prependRight(node.start, '/* ');
state.code.appendLeft(node.block.start + 1, '*/');
@ -216,16 +216,38 @@ const visitors = {
ComplexSelector(node, context) {
const before_bumped = context.state.specificity.bumped;
/** @param {Css.SimpleSelector} selector */
function remove_global_pseudo_class(selector) {
context.state.code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
/**
* @param {Css.PseudoClassSelector} selector
* @param {Css.Combinator | null} combinator
*/
function remove_global_pseudo_class(selector, combinator) {
if (selector.args === null) {
let start = selector.start;
if (combinator?.name === ' ') {
// div :global.x becomes div.x
while (/\s/.test(context.state.code.original[start - 1])) start--;
}
context.state.code.remove(start, selector.start + ':global'.length);
} else {
context.state.code
.remove(selector.start, selector.start + ':global('.length)
.remove(selector.end - 1, selector.end);
}
}
for (const relative_selector of node.children) {
if (relative_selector.metadata.is_global) {
remove_global_pseudo_class(relative_selector.selectors[0]);
const global = /** @type {Css.PseudoClassSelector} */ (relative_selector.selectors[0]);
remove_global_pseudo_class(global, relative_selector.combinator);
if (
node.metadata.rule?.metadata.parent_rule &&
global.args === null &&
relative_selector.combinator === null
) {
// div { :global.x { ... } } becomes div { &.x { ... } }
context.state.code.prependRight(global.start, '&');
}
continue;
}
@ -241,10 +263,10 @@ const visitors = {
}
}
// for any :global() at the middle of compound selector
// for any :global() or :global at the middle of compound selector
for (const selector of relative_selector.selectors) {
if (selector.type === 'PseudoClassSelector' && selector.name === 'global') {
remove_global_pseudo_class(selector);
remove_global_pseudo_class(selector, null);
}
}

@ -0,0 +1,118 @@
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */
/** @import { SvelteNode } from '#compiler' */
/** @import { Context, ServerTransformState } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { extract_paths } from '../../../../utils/ast.js';
import { serialize_get_binding } from './shared/utils.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type.endsWith('Statement');
if (
node.left.type === 'ArrayPattern' ||
node.left.type === 'ObjectPattern' ||
node.left.type === 'RestElement'
) {
const value = /** @type {Expression} */ (context.visit(node.right));
const should_cache = value.type !== 'Identifier';
const rhs = should_cache ? b.id('$$value') : value;
let changed = false;
const assignments = extract_paths(node.left).map((path) => {
const value = path.expression?.(rhs);
let assignment = serialize_assignment('=', path.node, value, context);
if (assignment !== null) changed = true;
return assignment ?? b.assignment('=', path.node, value);
});
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
return context.next();
}
const sequence = b.sequence(assignments);
if (!is_standalone) {
// this is part of an expression, we need the sequence to end with the value
sequence.expressions.push(rhs);
}
if (should_cache) {
// the right hand side is a complex expression, wrap in an IIFE to cache it
return b.call(b.arrow([rhs], sequence), value);
}
return sequence;
}
return serialize_assignment(node.operator, node.left, node.right, context) || context.next();
}
/**
* Only returns an expression if this is not a `$store` assignment, as others can be kept as-is
* @param {AssignmentOperator} operator
* @param {Pattern} left
* @param {Expression} right
* @param {import('zimmerframe').Context<SvelteNode, ServerTransformState>} context
* @returns {Expression | null}
*/
function serialize_assignment(operator, left, right, context) {
let object = left;
while (object.type === 'MemberExpression') {
// @ts-expect-error
object = object.object;
}
if (object.type !== 'Identifier' || !is_store_name(object.name)) {
return null;
}
const name = object.name.slice(1);
if (!context.state.scope.get(name)) {
return null;
}
if (object === left) {
let value = /** @type {Expression} */ (context.visit(right));
if (operator !== '=') {
// turn `x += 1` into `x = x + 1`
value = b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
serialize_get_binding(left, context.state),
value
);
}
return b.call('$.store_set', b.id(name), value);
}
return b.call(
'$.store_mutate',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(object.name),
b.id(name),
b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
/** @type {Expression} */ (context.visit(right))
)
);
}
/**
* @param {string} name
*/
function is_store_name(name) {
return name[0] === '$' && /[A-Za-z_]/.test(name[1]);
}

@ -0,0 +1,33 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { AwaitBlock } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { empty_comment } from './shared/utils.js';
/**
* @param {AwaitBlock} node
* @param {ComponentContext} context
*/
export function AwaitBlock(node, context) {
context.state.template.push(
empty_comment,
b.stmt(
b.call(
'$.await',
/** @type {Expression} */ (context.visit(node.expression)),
b.thunk(
node.pending ? /** @type {BlockStatement} */ (context.visit(node.pending)) : b.block([])
),
b.arrow(
node.value ? [/** @type {Pattern} */ (context.visit(node.value))] : [],
node.then ? /** @type {BlockStatement} */ (context.visit(node.then)) : b.block([])
),
b.arrow(
node.error ? [/** @type {Pattern} */ (context.visit(node.error))] : [],
node.catch ? /** @type {BlockStatement} */ (context.visit(node.catch)) : b.block([])
)
)
),
empty_comment
);
}

@ -0,0 +1,44 @@
/** @import { CallExpression, Expression } from 'estree' */
/** @import { Context } from '../types.js' */
import { get_rune } from '../../../scope.js';
import * as b from '../../../../utils/builders.js';
import { transform_inspect_rune } from '../../utils.js';
/**
* @param {CallExpression} node
* @param {Context} context
*/
export function CallExpression(node, context) {
const rune = get_rune(node, context.state.scope);
if (rune === '$host') {
return b.id('undefined');
}
if (rune === '$effect.tracking') {
return b.literal(false);
}
if (rune === '$effect.root') {
// ignore $effect.root() calls, just return a noop which mimics the cleanup function
return b.arrow([], b.block([]));
}
if (rune === '$state.snapshot') {
return b.call('$.snapshot', /** @type {Expression} */ (context.visit(node.arguments[0])));
}
if (rune === '$state.is') {
return b.call(
'Object.is',
/** @type {Expression} */ (context.visit(node.arguments[0])),
/** @type {Expression} */ (context.visit(node.arguments[1]))
);
}
if (rune === '$inspect' || rune === '$inspect().with') {
return transform_inspect_rune(node, context);
}
context.next();
}

@ -0,0 +1,118 @@
/** @import { ClassBody, Expression, MethodDefinition, PropertyDefinition } from 'estree' */
/** @import { Context } from '../types.js' */
/** @import { StateField } from '../../client/types.js' */
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {ClassBody} node
* @param {Context} context
*/
export function ClassBodyRunes(node, context) {
/** @type {Map<string, StateField>} */
const public_derived = new Map();
/** @type {Map<string, StateField>} */
const private_derived = new Map();
/** @type {string[]} */
const private_ids = [];
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const { type, name } = definition.key;
const is_private = type === 'PrivateIdentifier';
if (is_private) private_ids.push(name);
if (definition.value?.type === 'CallExpression') {
const rune = get_rune(definition.value, context.state.scope);
if (rune === '$derived' || rune === '$derived.by') {
/** @type {StateField} */
const field = {
kind: rune === '$derived.by' ? 'derived_call' : 'derived',
// @ts-expect-error this is set in the next pass
id: is_private ? definition.key : null
};
if (is_private) {
private_derived.set(name, field);
} else {
public_derived.set(name, field);
}
}
}
}
}
// each `foo = $derived()` needs a backing `#foo` field
for (const [name, field] of public_derived) {
let deconflicted = name;
while (private_ids.includes(deconflicted)) {
deconflicted = '_' + deconflicted;
}
private_ids.push(deconflicted);
field.id = b.private_id(deconflicted);
}
/** @type {Array<MethodDefinition | PropertyDefinition>} */
const body = [];
const child_state = { ...context.state, private_derived };
// Replace parts of the class body
for (const definition of node.body) {
if (
definition.type === 'PropertyDefinition' &&
(definition.key.type === 'Identifier' || definition.key.type === 'PrivateIdentifier')
) {
const name = definition.key.name;
const is_private = definition.key.type === 'PrivateIdentifier';
const field = (is_private ? private_derived : public_derived).get(name);
if (definition.value?.type === 'CallExpression' && field !== undefined) {
const init = /** @type {Expression} **/ (
context.visit(definition.value.arguments[0], child_state)
);
const value =
field.kind === 'derived_call' ? b.call('$.once', init) : b.call('$.once', b.thunk(init));
if (is_private) {
body.push(b.prop_def(field.id, value));
} else {
// #foo;
const member = b.member(b.this, field.id);
body.push(b.prop_def(field.id, value));
// get foo() { return this.#foo; }
body.push(b.method('get', definition.key, [], [b.return(b.call(member))]));
if (
(field.kind === 'derived' || field.kind === 'derived_call') &&
context.state.options.dev
) {
body.push(
b.method(
'set',
definition.key,
[b.id('_')],
[b.throw_error(`Cannot update a derived property ('${name}')`)]
)
);
}
}
continue;
}
}
body.push(/** @type {MethodDefinition} **/ (context.visit(definition, child_state)));
}
return { ...node, body };
}

@ -0,0 +1,12 @@
/** @import { Component } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { serialize_inline_component } from './shared/component.js';
/**
* @param {Component} node
* @param {ComponentContext} context
*/
export function Component(node, context) {
serialize_inline_component(node, b.id(node.name), context);
}

@ -0,0 +1,16 @@
/** @import { Expression, Pattern } from 'estree' */
/** @import { ConstTag } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {ConstTag} node
* @param {ComponentContext} context
*/
export function ConstTag(node, context) {
const declaration = node.declaration.declarations[0];
const id = /** @type {Pattern} */ (context.visit(declaration.id));
const init = /** @type {Expression} */ (context.visit(declaration.init));
context.state.init.push(b.const(id, init));
}

@ -0,0 +1,24 @@
/** @import { Expression } from 'estree' */
/** @import { DebugTag } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {DebugTag} node
* @param {ComponentContext} context
*/
export function DebugTag(node, context) {
context.state.template.push(
b.stmt(
b.call(
'console.log',
b.object(
node.identifiers.map((identifier) =>
b.prop('init', identifier, /** @type {Expression} */ (context.visit(identifier)))
)
)
)
),
b.debugger
);
}

@ -0,0 +1,63 @@
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { EachBlock } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '../../../../utils/builders.js';
import { block_close, block_open } from './shared/utils.js';
/**
* @param {EachBlock} node
* @param {ComponentContext} context
*/
export function EachBlock(node, context) {
const state = context.state;
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
const item = each_node_meta.item;
const index =
each_node_meta.contains_group_binding || !node.index ? each_node_meta.index : b.id(node.index);
const array_id = state.scope.root.unique('each_array');
state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection)));
/** @type {Statement[]} */
const each = [b.const(item, b.member(array_id, index, true))];
if (node.context.type !== 'Identifier') {
each.push(b.const(/** @type {Pattern} */ (node.context), item));
}
if (index.name !== node.index && node.index != null) {
each.push(b.let(node.index, index));
}
each.push(.../** @type {BlockStatement} */ (context.visit(node.body)).body);
const for_loop = b.for(
b.let(index, b.literal(0)),
b.binary('<', index, b.member(array_id, b.id('length'))),
b.update('++', index, false),
b.block(each)
);
if (node.fallback) {
const open = b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open));
const fallback = /** @type {BlockStatement} */ (context.visit(node.fallback));
fallback.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
state.template.push(
b.if(
b.binary('!==', b.member(array_id, b.id('length')), b.literal(0)),
b.block([open, for_loop]),
fallback
),
block_close
);
} else {
state.template.push(block_open, for_loop, block_close);
}
}

@ -0,0 +1,29 @@
/** @import { ExpressionStatement } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {ExpressionStatement} node
* @param {Context} context
*/
export function ExpressionStatementRunes(node, context) {
const expression = node.expression;
if (expression.type === 'CallExpression') {
const callee = expression.callee;
if (callee.type === 'Identifier' && callee.name === '$effect') {
return b.empty;
}
if (
callee.type === 'MemberExpression' &&
callee.object.type === 'Identifier' &&
callee.object.name === '$effect'
) {
return b.empty;
}
}
context.next();
}

@ -0,0 +1,46 @@
/** @import { Fragment } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
import { clean_nodes, infer_namespace } from '../../utils.js';
import * as b from '../../../../utils/builders.js';
import { empty_comment, process_children, serialize_template } from './shared/utils.js';
/**
* @param {Fragment} node
* @param {ComponentContext} context
*/
export function Fragment(node, context) {
const parent = context.path.at(-1) ?? node;
const namespace = infer_namespace(context.state.namespace, parent, node.nodes);
const { hoisted, trimmed, is_standalone, is_text_first } = clean_nodes(
parent,
node.nodes,
context.path,
namespace,
context.state,
context.state.preserve_whitespace,
context.state.options.preserveComments
);
/** @type {ComponentServerTransformState} */
const state = {
...context.state,
init: [],
template: [],
namespace,
skip_hydration_boundaries: is_standalone
};
for (const node of hoisted) {
context.visit(node, state);
}
if (is_text_first) {
// insert `<!---->` to prevent this from being glued to the previous fragment
state.template.push(empty_comment);
}
process_children(trimmed, { ...context, state });
return b.block([...state.init, ...serialize_template(state.template)]);
}

@ -0,0 +1,13 @@
/** @import { Expression } from 'estree' */
/** @import { HtmlTag } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {HtmlTag} node
* @param {ComponentContext} context
*/
export function HtmlTag(node, context) {
const expression = /** @type {Expression} */ (context.visit(node.expression));
context.state.template.push(b.call('$.html', expression));
}

@ -0,0 +1,19 @@
/** @import { Identifier, Node } from 'estree' */
/** @import { Context } from '../types.js' */
import is_reference from 'is-reference';
import * as b from '../../../../utils/builders.js';
import { serialize_get_binding } from './shared/utils.js';
/**
* @param {Identifier} node
* @param {Context} context
*/
export function Identifier(node, context) {
if (is_reference(node, /** @type {Node} */ (context.path.at(-1)))) {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
return serialize_get_binding(node, context.state);
}
}

@ -0,0 +1,28 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { IfBlock } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
import * as b from '../../../../utils/builders.js';
import { block_close, block_open } from './shared/utils.js';
/**
* @param {IfBlock} node
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
const test = /** @type {Expression} */ (context.visit(node.test));
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
context.state.template.push(b.if(test, consequent, alternate), block_close);
}

@ -0,0 +1,16 @@
/** @import { BlockStatement } from 'estree' */
/** @import { KeyBlock } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { empty_comment } from './shared/utils.js';
/**
* @param {KeyBlock} node
* @param {ComponentContext} context
*/
export function KeyBlock(node, context) {
context.state.template.push(
empty_comment,
/** @type {BlockStatement} */ (context.visit(node.fragment)),
empty_comment
);
}

@ -0,0 +1,23 @@
/** @import { ExpressionStatement, LabeledStatement } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {LabeledStatement} node
* @param {Context} context
*/
export function LabeledStatementLegacy(node, context) {
if (context.path.length > 1) return;
if (node.label.name !== '$') return;
// TODO bail out if we're in module context
// these statements will be topologically ordered later
context.state.legacy_reactive_statements.set(
node,
// people could do "break $" inside, so we need to keep the label
b.labeled('$', /** @type {ExpressionStatement} */ (context.visit(node.body)))
);
return b.empty;
}

@ -0,0 +1,19 @@
/** @import { MemberExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {MemberExpression} node
* @param {Context} context
*/
export function MemberExpressionRunes(node, context) {
if (node.object.type === 'ThisExpression' && node.property.type === 'PrivateIdentifier') {
const field = context.state.private_derived.get(node.property.name);
if (field) {
return b.call(node);
}
}
context.next();
}

@ -0,0 +1,41 @@
/** @import { Expression, PropertyDefinition } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
/**
* @param {PropertyDefinition} node
* @param {Context} context
*/
export function PropertyDefinitionRunes(node, context) {
if (node.value != null && node.value.type === 'CallExpression') {
const rune = get_rune(node.value, context.state.scope);
if (
rune === '$state' ||
rune === '$state.link' ||
rune === '$state.frozen' ||
rune === '$derived'
) {
return {
...node,
value:
node.value.arguments.length === 0
? null
: /** @type {Expression} */ (context.visit(node.value.arguments[0]))
};
}
if (rune === '$derived.by') {
return {
...node,
value:
node.value.arguments.length === 0
? null
: b.call(/** @type {Expression} */ (context.visit(node.value.arguments[0])))
};
}
}
context.next();
}

@ -0,0 +1,105 @@
/** @import { Location } from 'locate-character' */
/** @import { RegularElement, Text } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { VoidElements } from '../../../constants.js';
import { clean_nodes, determine_namespace_for_children } from '../../utils.js';
import { serialize_element_attributes } from './shared/element.js';
import { process_children, serialize_template } from './shared/utils.js';
/**
* @param {RegularElement} node
* @param {ComponentContext} context
*/
export function RegularElement(node, context) {
const namespace = determine_namespace_for_children(node, context.state.namespace);
/** @type {ComponentServerTransformState} */
const state = {
...context.state,
namespace,
preserve_whitespace:
context.state.preserve_whitespace ||
((node.name === 'pre' || node.name === 'textarea') && namespace !== 'foreign')
};
context.state.template.push(b.literal(`<${node.name}`));
const body = serialize_element_attributes(node, { ...context, state });
context.state.template.push(b.literal('>'));
if ((node.name === 'script' || node.name === 'style') && node.fragment.nodes.length === 1) {
context.state.template.push(
b.literal(/** @type {Text} */ (node.fragment.nodes[0]).data),
b.literal(`</${node.name}>`)
);
return;
}
const { hoisted, trimmed } = clean_nodes(
node,
node.fragment.nodes,
context.path,
namespace,
{
...state,
scope: /** @type {Scope} */ (state.scopes.get(node.fragment))
},
state.preserve_whitespace,
state.options.preserveComments
);
for (const node of hoisted) {
context.visit(node, state);
}
if (state.options.dev) {
const location = /** @type {Location} */ (locator(node.start));
state.template.push(
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
b.literal(node.name),
b.literal(location.line),
b.literal(location.column)
)
)
);
}
if (body === null) {
process_children(trimmed, { ...context, state });
} else {
let id = body;
if (body.type !== 'Identifier') {
id = b.id(state.scope.generate('$$body'));
state.template.push(b.const(id, body));
}
// if this is a `<textarea>` value or a contenteditable binding, we only add
// the body if the attribute/binding is falsy
const inner_state = { ...state, template: [], init: [] };
process_children(trimmed, { ...context, state: inner_state });
// Use the body expression as the body if it's truthy, otherwise use the inner template
state.template.push(
b.if(
id,
b.block(serialize_template([id])),
b.block([...inner_state.init, ...serialize_template(inner_state.template)])
)
);
}
if (!VoidElements.includes(node.name) && namespace !== 'foreign') {
state.template.push(b.literal(`</${node.name}>`));
}
if (state.options.dev) {
state.template.push(b.stmt(b.call('$.pop_element')));
}
}

@ -0,0 +1,35 @@
/** @import { Expression } from 'estree' */
/** @import { RenderTag } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { unwrap_optional } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { empty_comment } from './shared/utils.js';
/**
* @param {RenderTag} node
* @param {ComponentContext} context
*/
export function RenderTag(node, context) {
const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments;
const snippet_function = /** @type {Expression} */ (context.visit(callee));
const snippet_args = raw_args.map((arg) => {
return /** @type {Expression} */ (context.visit(arg));
});
context.state.template.push(
b.stmt(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(
snippet_function,
b.id('$$payload'),
...snippet_args
)
)
);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}
}

@ -0,0 +1,48 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Property } from 'estree' */
/** @import { SlotElement } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { empty_comment, serialize_attribute_value } from './shared/utils.js';
/**
* @param {SlotElement} node
* @param {ComponentContext} context
*/
export function SlotElement(node, context) {
/** @type {Property[]} */
const props = [];
/** @type {Expression[]} */
const spreads = [];
/** @type {Expression} */
let expression = b.call('$.default_slot', b.id('$$props'));
for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(/** @type {Expression} */ (context.visit(attribute)));
} else if (attribute.type === 'Attribute') {
const value = serialize_attribute_value(attribute.value, context, false, true);
if (attribute.name === 'name') {
expression = b.member(b.member_id('$$props.$$slots'), value, true, true);
} else if (attribute.name !== 'slot') {
props.push(b.init(attribute.name, value));
}
}
}
const props_expression =
spreads.length === 0
? b.object(props)
: b.call('$.spread_props', b.array([b.object(props), ...spreads]));
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
: b.thunk(/** @type {BlockStatement} */ (context.visit(node.fragment)));
const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback);
context.state.template.push(empty_comment, b.stmt(slot), empty_comment);
}

@ -0,0 +1,22 @@
/** @import { BlockStatement } from 'estree' */
/** @import { SnippetBlock } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SnippetBlock} node
* @param {ComponentContext} context
*/
export function SnippetBlock(node, context) {
const fn = b.function_declaration(
node.expression,
[b.id('$$payload'), ...node.parameters],
/** @type {BlockStatement} */ (context.visit(node.body))
);
// @ts-expect-error - TODO remove this hack once $$render_inner for legacy bindings is gone
fn.___snippet = true;
// TODO hoist where possible
context.state.init.push(fn);
}

@ -0,0 +1,10 @@
/** @import { SpreadAttribute } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/**
* @param {SpreadAttribute} node
* @param {ComponentContext} context
*/
export function SpreadAttribute(node, context) {
return context.visit(node.expression);
}

@ -0,0 +1,16 @@
/** @import { Expression } from 'estree' */
/** @import { SvelteComponent } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { serialize_inline_component } from './shared/component.js';
/**
* @param {SvelteComponent} node
* @param {ComponentContext} context
*/
export function SvelteComponent(node, context) {
serialize_inline_component(
node,
/** @type {Expression} */ (context.visit(node.expression)),
context
);
}

@ -0,0 +1,59 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { SvelteElement } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import { serialize_element_attributes } from './shared/element.js';
import { serialize_template } from './shared/utils.js';
/**
* @param {SvelteElement} node
* @param {ComponentContext} context
*/
export function SvelteElement(node, context) {
let tag = /** @type {Expression} */ (context.visit(node.tag));
if (tag.type !== 'Identifier') {
const tag_id = context.state.scope.generate('$$tag');
context.state.init.push(b.const(tag_id, tag));
tag = b.id(tag_id);
}
if (context.state.options.dev) {
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', b.thunk(tag))));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', b.thunk(tag))));
}
const state = {
...context.state,
namespace: determine_namespace_for_children(node, context.state.namespace),
template: [],
init: []
};
serialize_element_attributes(node, { ...context, state });
if (context.state.options.dev) {
context.state.template.push(b.stmt(b.call('$.push_element', tag, b.id('$$payload'))));
}
const attributes = b.block([...state.init, ...serialize_template(state.template)]);
const children = /** @type {BlockStatement} */ (context.visit(node.fragment, state));
context.state.template.push(
b.stmt(
b.call(
'$.element',
b.id('$$payload'),
tag,
attributes.body.length > 0 && b.thunk(attributes),
children.body.length > 0 && b.thunk(children)
)
)
);
if (context.state.options.dev) {
context.state.template.push(b.stmt(b.call('$.pop_element')));
}
}

@ -0,0 +1,11 @@
/** @import { BlockStatement } from 'estree' */
/** @import { SvelteFragment } from '#compiler' */
/** @import { ComponentContext } from '../types' */
/**
* @param {SvelteFragment} node
* @param {ComponentContext} context
*/
export function SvelteFragment(node, context) {
context.state.template.push(/** @type {BlockStatement} */ (context.visit(node.fragment)));
}

@ -0,0 +1,16 @@
/** @import { BlockStatement } from 'estree' */
/** @import { SvelteHead } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {SvelteHead} node
* @param {ComponentContext} context
*/
export function SvelteHead(node, context) {
const block = /** @type {BlockStatement} */ (context.visit(node.fragment));
context.state.template.push(
b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], block)))
);
}

@ -0,0 +1,12 @@
/** @import { SvelteSelf } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { serialize_inline_component } from './shared/component.js';
/**
* @param {SvelteSelf} node
* @param {ComponentContext} context
*/
export function SvelteSelf(node, context) {
serialize_inline_component(node, b.id(context.state.analysis.name), context);
}

@ -0,0 +1,17 @@
/** @import { TitleElement } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { process_children, serialize_template } from './shared/utils.js';
/**
* @param {TitleElement} node
* @param {ComponentContext} context
*/
export function TitleElement(node, context) {
// title is guaranteed to contain only text/expression tag children
const template = [b.literal('<title>')];
process_children(node.fragment.nodes, { ...context, state: { ...context.state, template } });
template.push(b.literal('</title>'));
context.state.init.push(...serialize_template(template, b.id('$$payload.title'), '='));
}

@ -0,0 +1,26 @@
/** @import { UpdateExpression } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
/**
* @param {UpdateExpression} node
* @param {Context} context
*/
export function UpdateExpression(node, context) {
const argument = node.argument;
if (
argument.type === 'Identifier' &&
context.state.scope.get(argument.name)?.kind === 'store_sub'
) {
return b.call(
node.prefix ? '$.update_store_pre' : '$.update_store',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(argument.name),
b.id(argument.name.slice(1)),
node.operator === '--' && b.literal(-1)
);
}
return context.next();
}

@ -0,0 +1,172 @@
/** @import { VariableDeclaration, VariableDeclarator, Expression, CallExpression, Pattern, Identifier } from 'estree' */
/** @import { Binding } from '#compiler' */
/** @import { Context } from '../types.js' */
/** @import { Scope } from '../../../scope.js' */
import { extract_paths, is_expression_async } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
/**
* @param {VariableDeclaration} node
* @param {Context} context
*/
export function VariableDeclarationRunes(node, context) {
const declarations = [];
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, context.state.scope);
if (!rune || rune === '$effect.tracking' || rune === '$inspect' || rune === '$effect.root') {
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
continue;
}
if (rune === '$props') {
// remove $bindable() from props declaration
const id = walk(declarator.id, null, {
AssignmentPattern(node) {
if (
node.right.type === 'CallExpression' &&
get_rune(node.right, context.state.scope) === '$bindable'
) {
const right = node.right.arguments.length
? /** @type {Expression} */ (context.visit(node.right.arguments[0]))
: b.id('undefined');
return b.assignment_pattern(node.left, right);
}
}
});
declarations.push(b.declarator(id, b.id('$$props')));
continue;
}
const args = /** @type {CallExpression} */ (init).arguments;
const value =
args.length === 0 ? b.id('undefined') : /** @type {Expression} */ (context.visit(args[0]));
if (rune === '$derived.by') {
declarations.push(
b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), b.call(value))
);
continue;
}
if (declarator.id.type === 'Identifier') {
declarations.push(b.declarator(declarator.id, value));
continue;
}
if (rune === '$derived') {
declarations.push(b.declarator(/** @type {Pattern} */ (context.visit(declarator.id)), value));
continue;
}
declarations.push(...create_state_declarators(declarator, context.state.scope, value));
}
return {
...node,
declarations
};
}
/**
* @param {VariableDeclaration} node
* @param {Context} context
*/
export function VariableDeclarationLegacy(node, { state, visit }) {
/** @type {VariableDeclarator[]} */
const declarations = [];
for (const declarator of node.declarations) {
const bindings = /** @type {Binding[]} */ (state.scope.get_bindings(declarator));
const has_state = bindings.some((binding) => binding.kind === 'state');
const has_props = bindings.some((binding) => binding.kind === 'bindable_prop');
if (!has_state && !has_props) {
declarations.push(/** @type {VariableDeclarator} */ (visit(declarator)));
continue;
}
if (has_props) {
if (declarator.id.type !== 'Identifier') {
// Turn export let into props. It's really really weird because export let { x: foo, z: [bar]} = ..
// means that foo and bar are the props (i.e. the leafs are the prop names), not x and z.
const tmp = state.scope.generate('tmp');
const paths = extract_paths(declarator.id);
declarations.push(
b.declarator(
b.id(tmp),
/** @type {Expression} */ (visit(/** @type {Expression} */ (declarator.init)))
)
);
for (const path of paths) {
const value = path.expression?.(b.id(tmp));
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (state.scope.get(name));
const prop = b.member(b.id('$$props'), b.literal(binding.prop_alias ?? name), true);
declarations.push(
b.declarator(path.node, b.call('$.value_or_fallback', prop, b.thunk(value)))
);
}
continue;
}
const binding = /** @type {Binding} */ (state.scope.get(declarator.id.name));
const prop = b.member(
b.id('$$props'),
b.literal(binding.prop_alias ?? declarator.id.name),
true
);
/** @type {Expression} */
let init = prop;
if (declarator.init) {
const default_value = /** @type {Expression} */ (visit(declarator.init));
init = is_expression_async(default_value)
? b.await(b.call('$.value_or_fallback_async', prop, b.thunk(default_value, true)))
: b.call('$.value_or_fallback', prop, b.thunk(default_value));
}
declarations.push(b.declarator(declarator.id, init));
continue;
}
declarations.push(
...create_state_declarators(
declarator,
state.scope,
/** @type {Expression} */ (declarator.init && visit(declarator.init))
)
);
}
return {
...node,
declarations
};
}
/**
* @param {VariableDeclarator} declarator
* @param {Scope} scope
* @param {Expression} value
* @returns {VariableDeclarator[]}
*/
function create_state_declarators(declarator, scope, value) {
if (declarator.id.type === 'Identifier') {
return [b.declarator(declarator.id, value)];
}
const tmp = scope.generate('tmp');
const paths = extract_paths(declarator.id);
return [
b.declarator(b.id(tmp), value), // TODO inject declarator for opts, so we can use it below
...paths.map((path) => {
const value = path.expression?.(b.id(tmp));
return b.declarator(path.node, value);
})
];
}

@ -0,0 +1,262 @@
/** @import { BlockStatement, Expression, Pattern, Property, Statement } from 'estree' */
/** @import { Attribute, Component, LetDirective, SvelteComponent, SvelteSelf, TemplateNode, Text } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { empty_comment, serialize_attribute_value } from './utils.js';
import * as b from '../../../../../utils/builders.js';
import { is_element_node } from '../../../../nodes.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
* @param {Expression} expression
* @param {ComponentContext} context
*/
export function serialize_inline_component(node, expression, context) {
/** @type {Array<Property[] | Expression>} */
const props_and_spreads = [];
/** @type {Property[]} */
const custom_css_props = [];
/** @type {Record<string, LetDirective[]>} */
const lets = { default: [] };
/** @type {Record<string, TemplateNode[]>} */
const children = {};
/**
* 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.
*/
const slot_scope_applies_to_itself = node.attributes.some(
(node) => node.type === 'Attribute' && node.name === 'slot'
);
/**
* Components may have a children prop and also have child nodes. In this case, we assume
* that the child component isn't using render tags yet and pass the slot as $$slots.default.
* We're not doing it for spread attributes, as this would result in too many false positives.
*/
let has_children_prop = false;
/**
* @param {Property} prop
*/
function push_prop(prop) {
const current = props_and_spreads.at(-1);
const current_is_props = Array.isArray(current);
const props = current_is_props ? current : [];
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
if (!slot_scope_applies_to_itself) {
lets.default.push(attribute);
}
} else if (attribute.type === 'SpreadAttribute') {
props_and_spreads.push(/** @type {Expression} */ (context.visit(attribute)));
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
const value = serialize_attribute_value(attribute.value, context, false, true);
custom_css_props.push(b.init(attribute.name, value));
continue;
}
if (attribute.name === 'children') {
has_children_prop = true;
}
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' && 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, [
b.return(/** @type {Expression} */ (context.visit(attribute.expression)))
])
);
push_prop(
b.set(attribute.name, [
b.stmt(
/** @type {Expression} */ (
context.visit(b.assignment('=', attribute.expression, b.id('$$value')))
)
),
b.stmt(b.assignment('=', b.id('$$settled'), b.false))
])
);
}
}
/** @type {Statement[]} */
const snippet_declarations = [];
// Group children by slot
for (const child of node.fragment.nodes) {
if (child.type === 'SnippetBlock') {
// the SnippetBlock visitor adds a declaration to `init`, but if it's directly
// inside a component then we want to hoist them into a block so that they
// can be used as props without creating conflicts
context.visit(child, {
...context.state,
init: snippet_declarations
});
push_prop(b.prop('init', child.expression, child.expression));
continue;
}
let slot_name = 'default';
if (is_element_node(child)) {
const slot = /** @type {Attribute | undefined} */ (
child.attributes.find(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
);
if (slot !== undefined) {
slot_name = /** @type {Text[]} */ (slot.value)[0].data;
lets[slot_name] = child.attributes.filter((attribute) => attribute.type === 'LetDirective');
} else if (child.type === 'SvelteFragment') {
lets.default.push(
...child.attributes.filter((attribute) => attribute.type === 'LetDirective')
);
}
}
children[slot_name] = children[slot_name] || [];
children[slot_name].push(child);
}
// Serialize each slot
/** @type {Property[]} */
const serialized_slots = [];
for (const slot_name of Object.keys(children)) {
const block = /** @type {BlockStatement} */ (
context.visit(
{
...node.fragment,
// @ts-expect-error
nodes: children[slot_name]
},
{
...context.state,
scope:
context.state.scopes.get(slot_name === 'default' ? children[slot_name][0] : node) ??
context.state.scope
}
)
);
if (block.body.length === 0) continue;
/** @type {Pattern[]} */
const params = [b.id('$$payload')];
if (lets[slot_name].length > 0) {
const pattern = b.object_pattern(
lets[slot_name].map((node) => {
if (node.expression === null) {
return b.init(node.name, b.id(node.name));
}
if (node.expression.type === 'ObjectExpression') {
// @ts-expect-error it gets parsed as an `ObjectExpression` but is really an `ObjectPattern`
return b.init(node.name, b.object_pattern(node.expression.properties));
}
if (node.expression.type === 'ArrayExpression') {
// @ts-expect-error it gets parsed as an `ArrayExpression` but is really an `ArrayPattern`
return b.init(node.name, b.array_pattern(node.expression.elements));
}
return b.init(node.name, node.expression);
})
);
params.push(pattern);
}
const slot_fn = b.arrow(params, b.block(block.body));
if (slot_name === 'default' && !has_children_prop) {
if (
lets.default.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
// create `children` prop...
push_prop(b.prop('init', b.id('children'), slot_fn));
// and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true));
} else {
// create `$$slots.default`...
serialized_slots.push(b.init(slot_name, slot_fn));
// and a `children` prop that errors
push_prop(b.init('children', b.id('$.invalid_default_snippet')));
}
} else {
serialized_slots.push(b.init(slot_name, slot_fn));
}
}
if (serialized_slots.length > 0) {
push_prop(b.prop('init', b.id('$$slots'), b.object(serialized_slots)));
}
const props_expression =
props_and_spreads.length === 0 ||
(props_and_spreads.length === 1 && Array.isArray(props_and_spreads[0]))
? b.object(/** @type {Property[]} */ (props_and_spreads[0] || []))
: b.call(
'$.spread_props',
b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p)))
);
/** @type {Statement} */
let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
expression,
b.id('$$payload'),
props_expression
)
);
if (snippet_declarations.length > 0) {
statement = b.block([...snippet_declarations, statement]);
}
const dynamic =
node.type === 'SvelteComponent' || (node.type === 'Component' && node.metadata.dynamic);
if (custom_css_props.length > 0) {
context.state.template.push(
b.stmt(
b.call(
'$.css_props',
b.id('$$payload'),
b.literal(context.state.namespace === 'svg' ? false : true),
b.object(custom_css_props),
b.thunk(b.block([statement])),
dynamic && b.true
)
)
);
} else {
if (dynamic) {
context.state.template.push(empty_comment);
}
context.state.template.push(statement);
if (!context.state.skip_hydration_boundaries) {
context.state.template.push(empty_comment);
}
}
}

@ -0,0 +1,436 @@
/** @import { Expression, ExpressionStatement, Literal } from 'estree' */
/** @import { Attribute, ClassDirective, Namespace, RegularElement, SpreadAttribute, StyleDirective, SvelteElement, SvelteNode } from '#compiler' */
/** @import { ComponentContext, ComponentServerTransformState } from '../../types.js' */
import {
get_attribute_chunks,
is_event_attribute,
is_text_attribute
} from '../../../../../utils/ast.js';
import { binding_properties } from '../../../../bindings.js';
import {
ContentEditableBindings,
LoadErrorElements,
WhitespaceInsensitiveAttributes
} from '../../../../constants.js';
import { create_attribute, is_custom_element_node } from '../../../../nodes.js';
import { regex_starts_with_newline } from '../../../../patterns.js';
import * as b from '../../../../../utils/builders.js';
import {
DOMBooleanAttributes,
ELEMENT_IS_NAMESPACED,
ELEMENT_PRESERVE_ATTRIBUTE_CASE
} from '../../../../../../constants.js';
import { serialize_attribute_value } from './utils.js';
/**
* Writes the output to the template output. Some elements may have attributes on them that require the
* their output to be the child content instead. In this case, an object is returned.
* @param {RegularElement | SvelteElement} node
* @param {import('zimmerframe').Context<SvelteNode, ComponentServerTransformState>} context
*/
export function serialize_element_attributes(node, context) {
/** @type {Array<Attribute | SpreadAttribute>} */
const attributes = [];
/** @type {ClassDirective[]} */
const class_directives = [];
/** @type {StyleDirective[]} */
const style_directives = [];
/** @type {Expression | null} */
let content = null;
let has_spread = false;
// Use the index to keep the attributes order which is important for spreading
let class_index = -1;
let style_index = -1;
let events_to_capture = new Set();
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
if (attribute.name === 'value') {
if (node.name === 'textarea') {
if (
attribute.value !== true &&
Array.isArray(attribute.value) &&
attribute.value[0].type === 'Text' &&
regex_starts_with_newline.test(attribute.value[0].data)
) {
// Two or more leading newlines are required to restore the leading newline immediately after `<textarea>`.
// see https://html.spec.whatwg.org/multipage/syntax.html#element-restrictions
// also see related code in analysis phase
attribute.value[0].data = '\n' + attribute.value[0].data;
}
content = b.call('$.escape', serialize_attribute_value(attribute.value, context));
} else if (node.name !== 'select') {
// omit value attribute for select elements, it's irrelevant for the initially selected value and has no
// effect on the selected value after the user interacts with the select element (the value _property_ does, but not the attribute)
attributes.push(attribute);
}
// omit event handlers except for special cases
} else if (is_event_attribute(attribute)) {
if (
(attribute.name === 'onload' || attribute.name === 'onerror') &&
LoadErrorElements.includes(node.name)
) {
events_to_capture.add(attribute.name);
}
} else {
if (attribute.name === 'class') {
class_index = attributes.length;
} else if (attribute.name === 'style') {
style_index = attributes.length;
}
attributes.push(attribute);
}
} else if (attribute.type === 'BindDirective') {
if (attribute.name === 'value' && node.name === 'select') continue;
if (
attribute.name === 'value' &&
attributes.some(
(attr) =>
attr.type === 'Attribute' &&
attr.name === 'type' &&
is_text_attribute(attr) &&
attr.value[0].data === 'file'
)
) {
continue;
}
if (attribute.name === 'this') continue;
const binding = binding_properties[attribute.name];
if (binding?.omit_in_ssr) continue;
if (ContentEditableBindings.includes(attribute.name)) {
content = /** @type {Expression} */ (context.visit(attribute.expression));
} else if (attribute.name === 'value' && node.name === 'textarea') {
content = b.call(
'$.escape',
/** @type {Expression} */ (context.visit(attribute.expression))
);
} else if (attribute.name === 'group') {
const value_attribute = /** @type {Attribute | undefined} */ (
node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'value')
);
if (!value_attribute) continue;
const is_checkbox = node.attributes.some(
(attr) =>
attr.type === 'Attribute' &&
attr.name === 'type' &&
is_text_attribute(attr) &&
attr.value[0].data === 'checkbox'
);
attributes.push(
create_attribute('checked', -1, -1, [
{
type: 'ExpressionTag',
start: -1,
end: -1,
parent: attribute,
expression: is_checkbox
? b.call(
b.member(attribute.expression, b.id('includes')),
serialize_attribute_value(value_attribute.value, context)
)
: b.binary(
'===',
attribute.expression,
serialize_attribute_value(value_attribute.value, context)
),
metadata: {
contains_call_expression: false,
dynamic: false
}
}
])
);
} else {
attributes.push(
create_attribute(attribute.name, -1, -1, [
{
type: 'ExpressionTag',
start: -1,
end: -1,
parent: attribute,
expression: attribute.expression,
metadata: {
contains_call_expression: false,
dynamic: false
}
}
])
);
}
} else if (attribute.type === 'SpreadAttribute') {
attributes.push(attribute);
has_spread = true;
if (LoadErrorElements.includes(node.name)) {
events_to_capture.add('onload');
events_to_capture.add('onerror');
}
} else if (attribute.type === 'UseDirective') {
if (LoadErrorElements.includes(node.name)) {
events_to_capture.add('onload');
events_to_capture.add('onerror');
}
} else if (attribute.type === 'ClassDirective') {
class_directives.push(attribute);
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
// do nothing, these are handled inside `serialize_inline_component`
} else {
context.visit(attribute);
}
}
if (class_directives.length > 0 && !has_spread) {
const class_attribute = serialize_class_directives(
class_directives,
/** @type {Attribute | null} */ (attributes[class_index] ?? null)
);
if (class_index === -1) {
attributes.push(class_attribute);
}
}
if (style_directives.length > 0 && !has_spread) {
serialize_style_directives(
style_directives,
/** @type {Attribute | null} */ (attributes[style_index] ?? null),
context
);
if (style_index > -1) {
attributes.splice(style_index, 1);
}
}
if (has_spread) {
serialize_element_spread_attributes(
node,
attributes,
style_directives,
class_directives,
context
);
} else {
for (const attribute of /** @type {Attribute[]} */ (attributes)) {
if (attribute.value === true || is_text_attribute(attribute)) {
const name = get_attribute_name(node, attribute, context);
const literal_value = /** @type {Literal} */ (
serialize_attribute_value(
attribute.value,
context,
WhitespaceInsensitiveAttributes.includes(name)
)
).value;
if (name !== 'class' || literal_value) {
context.state.template.push(
b.literal(
` ${attribute.name}${
DOMBooleanAttributes.includes(name) && literal_value === true
? ''
: `="${literal_value === true ? '' : String(literal_value)}"`
}`
)
);
}
continue;
}
const name = get_attribute_name(node, attribute, context);
const is_boolean = DOMBooleanAttributes.includes(name);
const value = serialize_attribute_value(
attribute.value,
context,
WhitespaceInsensitiveAttributes.includes(name)
);
context.state.template.push(
b.call('$.attr', b.literal(name), value, is_boolean && b.literal(is_boolean))
);
}
}
if (events_to_capture.size !== 0) {
for (const event of events_to_capture) {
context.state.template.push(b.literal(` ${event}="this.__e=event"`));
}
}
return content;
}
/**
* @param {RegularElement | SvelteElement} element
* @param {Attribute} attribute
* @param {{ state: { namespace: Namespace }}} context
*/
function get_attribute_name(element, attribute, context) {
let name = attribute.name;
if (!element.metadata.svg && !element.metadata.mathml && context.state.namespace !== 'foreign') {
name = name.toLowerCase();
// don't lookup boolean aliases here, the server runtime function does only
// check for the lowercase variants of boolean attributes
}
return name;
}
/**
*
* @param {RegularElement | SvelteElement} element
* @param {Array<Attribute | SpreadAttribute>} attributes
* @param {StyleDirective[]} style_directives
* @param {ClassDirective[]} class_directives
* @param {ComponentContext} context
*/
function serialize_element_spread_attributes(
element,
attributes,
style_directives,
class_directives,
context
) {
let classes;
let styles;
let flags = 0;
if (class_directives.length > 0 || context.state.analysis.css.hash) {
const properties = class_directives.map((directive) =>
b.init(
directive.name,
directive.expression.type === 'Identifier' && directive.expression.name === directive.name
? b.id(directive.name)
: /** @type {Expression} */ (context.visit(directive.expression))
)
);
if (context.state.analysis.css.hash) {
properties.unshift(b.init(context.state.analysis.css.hash, b.literal(true)));
}
classes = b.object(properties);
}
if (style_directives.length > 0) {
const properties = style_directives.map((directive) =>
b.init(
directive.name,
directive.value === true
? b.id(directive.name)
: serialize_attribute_value(directive.value, context, true)
)
);
styles = b.object(properties);
}
if (element.metadata.svg || element.metadata.mathml) {
flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE;
} else if (is_custom_element_node(element)) {
flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE;
}
const object = b.object(
attributes.map((attribute) => {
if (attribute.type === 'Attribute') {
const name = get_attribute_name(element, attribute, context);
const value = serialize_attribute_value(
attribute.value,
context,
WhitespaceInsensitiveAttributes.includes(name)
);
return b.prop('init', b.key(name), value);
}
return b.spread(/** @type {Expression} */ (context.visit(attribute)));
})
);
const args = [object, classes, styles, flags ? b.literal(flags) : undefined];
context.state.template.push(b.call('$.spread_attributes', ...args));
}
/**
*
* @param {ClassDirective[]} class_directives
* @param {Attribute | null} class_attribute
* @returns
*/
function serialize_class_directives(class_directives, class_attribute) {
const expressions = class_directives.map((directive) =>
b.conditional(directive.expression, b.literal(directive.name), b.literal(''))
);
if (class_attribute === null) {
class_attribute = create_attribute('class', -1, -1, []);
}
const chunks = get_attribute_chunks(class_attribute.value);
const last = chunks.at(-1);
if (last?.type === 'Text') {
last.data += ' ';
last.raw += ' ';
} else if (last) {
chunks.push({
type: 'Text',
start: -1,
end: -1,
parent: class_attribute,
data: ' ',
raw: ' '
});
}
chunks.push({
type: 'ExpressionTag',
start: -1,
end: -1,
parent: class_attribute,
expression: b.call(
b.member(
b.call(b.member(b.array(expressions), b.id('filter')), b.id('Boolean')),
b.id('join')
),
b.literal(' ')
),
metadata: { contains_call_expression: false, dynamic: false }
});
class_attribute.value = chunks;
return class_attribute;
}
/**
* @param {StyleDirective[]} style_directives
* @param {Attribute | null} style_attribute
* @param {ComponentContext} context
*/
function serialize_style_directives(style_directives, style_attribute, context) {
const styles = style_directives.map((directive) => {
let value =
directive.value === true
? b.id(directive.name)
: serialize_attribute_value(directive.value, context, true);
if (directive.modifiers.includes('important')) {
value = b.binary('+', value, b.literal(' !important'));
}
return b.init(directive.name, value);
});
const arg =
style_attribute === null
? b.object(styles)
: b.call(
'$.merge_styles',
serialize_attribute_value(style_attribute.value, context, true),
b.object(styles)
);
context.state.template.push(b.call('$.add_styles', arg));
}

@ -0,0 +1,229 @@
/** @import { AssignmentOperator, Expression, Identifier, Node, Statement, TemplateElement } from 'estree' */
/** @import { Attribute, Comment, ExpressionTag, SvelteNode, Text } from '#compiler' */
/** @import { ComponentContext, ServerTransformState } from '../../types.js' */
import { escape_html } from '../../../../../../escaping.js';
import {
BLOCK_CLOSE,
BLOCK_OPEN,
EMPTY_COMMENT
} from '../../../../../../internal/server/hydration.js';
import * as b from '../../../../../utils/builders.js';
import { sanitize_template_string } from '../../../../../utils/sanitize_template_string.js';
import { regex_whitespaces_strict } from '../../../../patterns.js';
/** Opens an if/each block, so that we can remove nodes in the case of a mismatch */
export const block_open = b.literal(BLOCK_OPEN);
/** Closes an if/each block, so that we can remove nodes in the case of a mismatch. Also serves as an anchor for these blocks */
export const block_close = b.literal(BLOCK_CLOSE);
/** Empty comment to keep text nodes separate, or provide an anchor node for blocks */
export const empty_comment = b.literal(EMPTY_COMMENT);
/**
* Processes an array of template nodes, joining sibling text/expression nodes and
* recursing into child nodes.
* @param {Array<SvelteNode>} nodes
* @param {ComponentContext} context
*/
export function process_children(nodes, { visit, state }) {
/** @type {Array<Text | Comment | ExpressionTag>} */
let sequence = [];
function flush() {
let quasi = b.quasi('', false);
const quasis = [quasi];
/** @type {Expression[]} */
const expressions = [];
for (let i = 0; i < sequence.length; i++) {
const node = sequence[i];
if (node.type === 'Text' || node.type === 'Comment') {
quasi.value.raw += sanitize_template_string(
node.type === 'Comment' ? `<!--${node.data}-->` : escape_html(node.data)
);
} else if (node.type === 'ExpressionTag' && node.expression.type === 'Literal') {
if (node.expression.value != null) {
quasi.value.raw += sanitize_template_string(escape_html(node.expression.value + ''));
}
} else {
expressions.push(b.call('$.escape', /** @type {Expression} */ (visit(node.expression))));
quasi = b.quasi('', i + 1 === sequence.length);
quasis.push(quasi);
}
}
state.template.push(b.template(quasis, expressions));
}
for (let i = 0; i < nodes.length; i += 1) {
const node = nodes[i];
if (node.type === 'Text' || node.type === 'Comment' || node.type === 'ExpressionTag') {
sequence.push(node);
} else {
if (sequence.length > 0) {
flush();
sequence = [];
}
visit(node, { ...state });
}
}
if (sequence.length > 0) {
flush();
}
}
/**
* @param {Node} node
* @returns {node is Statement}
*/
function is_statement(node) {
return node.type.endsWith('Statement') || node.type.endsWith('Declaration');
}
/**
* @param {Array<Statement | Expression>} template
* @param {Identifier} out
* @param {AssignmentOperator} operator
* @returns {Statement[]}
*/
export function serialize_template(template, out = b.id('$$payload.out'), operator = '+=') {
/** @type {TemplateElement[]} */
let quasis = [];
/** @type {Expression[]} */
let expressions = [];
/** @type {Statement[]} */
const statements = [];
const flush = () => {
statements.push(b.stmt(b.assignment(operator, out, b.template(quasis, expressions))));
quasis = [];
expressions = [];
};
for (let i = 0; i < template.length; i++) {
const node = template[i];
if (is_statement(node)) {
if (quasis.length !== 0) {
flush();
}
statements.push(node);
} else {
let last = quasis.at(-1);
if (!last) quasis.push((last = b.quasi('', false)));
if (node.type === 'Literal') {
last.value.raw +=
typeof node.value === 'string' ? sanitize_template_string(node.value) : node.value;
} else if (node.type === 'TemplateLiteral') {
last.value.raw += node.quasis[0].value.raw;
quasis.push(...node.quasis.slice(1));
expressions.push(...node.expressions);
} else {
expressions.push(node);
quasis.push(b.quasi('', i + 1 === template.length || is_statement(template[i + 1])));
}
}
}
if (quasis.length !== 0) {
flush();
}
return statements;
}
/**
*
* @param {Attribute['value']} value
* @param {ComponentContext} context
* @param {boolean} trim_whitespace
* @param {boolean} is_component
* @returns {Expression}
*/
export function serialize_attribute_value(
value,
context,
trim_whitespace = false,
is_component = false
) {
if (value === true) {
return b.true;
}
if (!Array.isArray(value) || value.length === 1) {
const chunk = Array.isArray(value) ? value[0] : value;
if (chunk.type === 'Text') {
const data = trim_whitespace
? chunk.data.replace(regex_whitespaces_strict, ' ').trim()
: chunk.data;
return b.literal(is_component ? data : escape_html(data, true));
}
return /** @type {Expression} */ (context.visit(chunk.expression));
}
let quasi = b.quasi('', false);
const quasis = [quasi];
/** @type {Expression[]} */
const expressions = [];
for (let i = 0; i < value.length; i++) {
const node = value[i];
if (node.type === 'Text') {
quasi.value.raw += trim_whitespace
? node.data.replace(regex_whitespaces_strict, ' ')
: node.data;
} else {
expressions.push(
b.call('$.stringify', /** @type {Expression} */ (context.visit(node.expression)))
);
quasi = b.quasi('', i + 1 === value.length);
quasis.push(quasi);
}
}
return b.template(quasis, expressions);
}
/**
* @param {Identifier} node
* @param {ServerTransformState} state
* @returns {Expression}
*/
export function serialize_get_binding(node, state) {
const binding = state.scope.get(node.name);
if (binding === null || node === binding.node) {
// No associated binding or the declaration itself which shouldn't be transformed
return node;
}
if (binding.kind === 'store_sub') {
const store_id = b.id(node.name.slice(1));
return b.call(
'$.store_get',
b.assignment('??=', b.id('$$store_subs'), b.object([])),
b.literal(node.name),
serialize_get_binding(store_id, state)
);
}
return node;
}

@ -15,7 +15,8 @@ export const binding_properties = {
// media
currentTime: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
duration: {
valid_elements: ['audio', 'video'],
@ -25,7 +26,8 @@ export const binding_properties = {
focused: {},
paused: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
buffered: {
valid_elements: ['audio', 'video'],
@ -41,15 +43,18 @@ export const binding_properties = {
},
volume: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
muted: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
playbackRate: {
valid_elements: ['audio', 'video'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
seeking: {
valid_elements: ['audio', 'video'],
@ -124,11 +129,13 @@ export const binding_properties = {
},
scrollX: {
valid_elements: ['svelte:window'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
scrollY: {
valid_elements: ['svelte:window'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
},
online: {
valid_elements: ['svelte:window'],
@ -180,23 +187,28 @@ export const binding_properties = {
omit_in_ssr: true // no corresponding attribute
},
checked: {
valid_elements: ['input']
valid_elements: ['input'],
bidirectional: true
},
group: {
valid_elements: ['input']
valid_elements: ['input'],
bidirectional: true
},
// various
this: {
omit_in_ssr: true
},
innerText: {
invalid_elements: ['svelte:window', 'svelte:document']
invalid_elements: ['svelte:window', 'svelte:document'],
bidirectional: true
},
innerHTML: {
invalid_elements: ['svelte:window', 'svelte:document']
invalid_elements: ['svelte:window', 'svelte:document'],
bidirectional: true
},
textContent: {
invalid_elements: ['svelte:window', 'svelte:document']
invalid_elements: ['svelte:window', 'svelte:document'],
bidirectional: true
},
open: {
event: 'toggle',
@ -204,10 +216,12 @@ export const binding_properties = {
valid_elements: ['details']
},
value: {
valid_elements: ['input', 'textarea', 'select']
valid_elements: ['input', 'textarea', 'select'],
bidirectional: true
},
files: {
valid_elements: ['input'],
omit_in_ssr: true
omit_in_ssr: true,
bidirectional: true
}
};

@ -1,3 +1,6 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor, Visitors } from 'zimmerframe' */
/** @import { AnimateDirective, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { is_element_node } from './nodes.js';
@ -30,20 +33,20 @@ export class Scope {
/**
* A map of every identifier declared by this scope, and all the
* identifiers that reference it
* @type {Map<string, import('#compiler').Binding>}
* @type {Map<string, Binding>}
*/
declarations = new Map();
/**
* A map of declarators to the bindings they declare
* @type {Map<import('estree').VariableDeclarator | import('#compiler').LetDirective, import('#compiler').Binding[]>}
* @type {Map<VariableDeclarator | LetDirective, Binding[]>}
*/
declarators = new Map();
/**
* A set of all the names referenced with this scope
* useful for generating unique names
* @type {Map<string, { node: import('estree').Identifier; path: import('#compiler').SvelteNode[] }[]>}
* @type {Map<string, { node: Identifier; path: SvelteNode[] }[]>}
*/
references = new Map();
@ -67,11 +70,11 @@ export class Scope {
}
/**
* @param {import('estree').Identifier} node
* @param {import('#compiler').Binding['kind']} kind
* @param {import('#compiler').DeclarationKind} declaration_kind
* @param {null | import('estree').Expression | import('estree').FunctionDeclaration | import('estree').ClassDeclaration | import('estree').ImportDeclaration | import('../types/template.js').EachBlock} initial
* @returns {import('#compiler').Binding}
* @param {Identifier} node
* @param {Binding['kind']} kind
* @param {DeclarationKind} declaration_kind
* @param {null | Expression | FunctionDeclaration | ClassDeclaration | ImportDeclaration | EachBlock} initial
* @returns {Binding}
*/
declare(node, kind, declaration_kind, initial = null) {
if (node.name === '$') {
@ -103,7 +106,7 @@ export class Scope {
e.declaration_duplicate(node, node.name);
}
/** @type {import('#compiler').Binding} */
/** @type {Binding} */
const binding = {
node,
references: [],
@ -115,7 +118,6 @@ export class Scope {
declaration_kind,
is_called: false,
prop_alias: null,
expression: null,
mutation: null,
reassigned: false,
metadata: null
@ -158,15 +160,15 @@ export class Scope {
/**
* @param {string} name
* @returns {import('#compiler').Binding | null}
* @returns {Binding | null}
*/
get(name) {
return this.declarations.get(name) ?? this.parent?.get(name) ?? null;
}
/**
* @param {import('estree').VariableDeclarator | import('#compiler').LetDirective} node
* @returns {import('#compiler').Binding[]}
* @param {VariableDeclarator | LetDirective} node
* @returns {Binding[]}
*/
get_bindings(node) {
const bindings = this.declarators.get(node);
@ -185,8 +187,8 @@ export class Scope {
}
/**
* @param {import('estree').Identifier} node
* @param {import('#compiler').SvelteNode[]} path
* @param {Identifier} node
* @param {SvelteNode[]} path
*/
reference(node, path) {
path = [...path]; // ensure that mutations to path afterwards don't affect this reference
@ -232,7 +234,7 @@ export class ScopeRoot {
}
/**
* @param {import('#compiler').SvelteNode} ast
* @param {SvelteNode} ast
* @param {ScopeRoot} root
* @param {boolean} allow_reactive_declarations
* @param {Scope | null} parent
@ -242,7 +244,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/**
* A map of node->associated scope. A node appearing in this map does not necessarily mean that it created a scope
* @type {Map<import('#compiler').SvelteNode, Scope>}
* @type {Map<SvelteNode, Scope>}
*/
const scopes = new Map();
const scope = new Scope(root, parent, false);
@ -251,21 +253,21 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/** @type {State} */
const state = { scope };
/** @type {[Scope, { node: import('estree').Identifier; path: import('#compiler').SvelteNode[] }][]} */
/** @type {[Scope, { node: Identifier; path: SvelteNode[] }][]} */
const references = [];
/** @type {[Scope, import('estree').Pattern | import('estree').MemberExpression][]} */
/** @type {[Scope, Pattern | MemberExpression][]} */
const updates = [];
/**
* An array of reactive declarations, i.e. the `a` in `$: a = b * 2`
* @type {import('estree').Identifier[]}
* @type {Identifier[]}
*/
const possible_implicit_declarations = [];
/**
* @param {Scope} scope
* @param {import('estree').Pattern[]} params
* @param {Pattern[]} params
*/
function add_params(scope, params) {
for (const param of params) {
@ -276,7 +278,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @type {import('zimmerframe').Visitor<import('estree').Node, State, import('#compiler').SvelteNode>}
* @type {Visitor<Node, State, SvelteNode>}
*/
const create_block_scope = (node, { state, next }) => {
const scope = state.scope.child(true);
@ -286,7 +288,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
};
/**
* @type {import('zimmerframe').Visitor<import('#compiler').ElementLike, State, import('#compiler').SvelteNode>}
* @type {Visitor<ElementLike, State, SvelteNode>}
*/
const SvelteFragment = (node, { state, next }) => {
const [scope] = analyze_let_directives(node, state.scope);
@ -295,7 +297,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
};
/**
* @param {import('#compiler').ElementLike} node
* @param {ElementLike} node
* @param {Scope} parent
*/
function analyze_let_directives(node, parent) {
@ -304,7 +306,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
/** @type {import('#compiler').Binding[]} */
/** @type {Binding[]} */
const bindings = [];
scope.declarators.set(attribute, bindings);
@ -318,7 +320,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
bindings.push(binding);
}
} else {
/** @type {import('estree').Identifier} */
/** @type {Identifier} */
const id = {
name: attribute.name,
type: 'Identifier',
@ -337,7 +339,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
}
/**
* @type {import('zimmerframe').Visitor<import('#compiler').AnimateDirective | import('#compiler').TransitionDirective | import('#compiler').UseDirective, State, import('#compiler').SvelteNode>}
* @type {Visitor<AnimateDirective | TransitionDirective | UseDirective, State, SvelteNode>}
*/
const SvelteDirective = (node, { state, path, visit }) => {
state.scope.reference(b.id(node.name.split('.')[0]), path);
@ -351,7 +353,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
// references
Identifier(node, { path, state }) {
const parent = path.at(-1);
if (parent && is_reference(node, /** @type {import('estree').Node} */ (parent))) {
if (parent && is_reference(node, /** @type {Node} */ (parent))) {
references.push([state.scope, { node, path: path.slice() }]);
}
},
@ -433,12 +435,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
},
UpdateExpression(node, { state, next }) {
updates.push([
state.scope,
/** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
node.argument
)
]);
updates.push([state.scope, /** @type {Identifier | MemberExpression} */ (node.argument)]);
next();
},
@ -490,7 +487,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
VariableDeclaration(node, { state, path, next }) {
const is_parent_const_tag = path.at(-1)?.type === 'ConstTag';
for (const declarator of node.declarations) {
/** @type {import('#compiler').Binding[]} */
/** @type {Binding[]} */
const bindings = [];
state.scope.declarators.set(declarator, bindings);
@ -526,7 +523,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
EachBlock(node, { state, visit }) {
// Array part is still from the scope above
/** @type {Set<import('estree').Identifier>} */
/** @type {Set<Identifier>} */
const references_within = new Set();
const idx = references.length;
visit(node.expression);
@ -601,7 +598,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
item: node.context.type === 'Identifier' ? node.context : b.id('$$item'),
declarations: scope.declarations,
references: [...references_within]
.map((id) => /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)))
.map((id) => /** @type {Binding} */ (state.scope.get(id.name)))
.filter(Boolean),
is_controlled: false
};
@ -674,9 +671,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
BindDirective(node, context) {
updates.push([
context.state.scope,
/** @type {import('estree').Identifier | import('estree').MemberExpression} */ (
node.expression
)
/** @type {Identifier | MemberExpression} */ (node.expression)
]);
context.next();
},
@ -719,7 +714,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
object = object.object;
}
const binding = scope.get(/** @type {import('estree').Identifier} */ (object).name);
const binding = scope.get(/** @type {Identifier} */ (object).name);
if (binding) binding.mutated = true;
} else {
unwrap_pattern(node).forEach((node) => {
@ -743,15 +738,15 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
/**
* @template {{ scope: Scope }} State
* @param {Map<import('#compiler').SvelteNode, Scope>} scopes
* @returns {import('zimmerframe').Visitors<import('#compiler').SvelteNode, State>}
* @param {Map<SvelteNode, Scope>} scopes
* @returns {Visitors<SvelteNode, State>}
*/
export function set_scope(scopes) {
return {
/**
*
* @param {import('#compiler').SvelteNode} node
* @param {import('zimmerframe').Context<import('#compiler').SvelteNode, State>} context
* @param {SvelteNode} node
* @param {Context<SvelteNode, State>} context
*/
_(node, { next, state }) {
const scope = scopes.get(node);
@ -762,7 +757,7 @@ export function set_scope(scopes) {
/**
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {import('estree').Node | import('../types/template.js').EachBlock | null | undefined} node
* @param {Node | EachBlock | null | undefined} node
* @param {Scope} scope
* @returns {Runes[number] | null}
*/

@ -33,17 +33,32 @@ export namespace Css {
metadata: {
parent_rule: null | Rule;
has_local_selectors: boolean;
/**
* `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped
*/
is_global_block: boolean;
};
}
/**
* A list of selectors, e.g. `a, b, c {}`
*/
export interface SelectorList extends BaseNode {
type: 'SelectorList';
/**
* The `a`, `b` and `c` in `a, b, c {}`
*/
children: ComplexSelector[];
}
/**
* A complex selector, e.g. `a b c {}`
*/
export interface ComplexSelector extends BaseNode {
type: 'ComplexSelector';
/**
* The `a`, `b` and `c` in `a b c {}`
*/
children: RelativeSelector[];
metadata: {
rule: null | Rule;
@ -51,14 +66,26 @@ export namespace Css {
};
}
/**
* A relative selector, e.g the `a` and `> b` in `a > b {}`
*/
export interface RelativeSelector extends BaseNode {
type: 'RelativeSelector';
/**
* In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector.
*/
combinator: null | Combinator;
/**
* The `b:is(...)` in `> b:is(...)`
*/
selectors: SimpleSelector[];
metadata: {
/** :global(..) */
/**
* `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`.
* Selectors like `:global(...).x` are not considered global, because they still need scoping.
*/
is_global: boolean;
/** :root, :host, ::view-transition */
/** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */
is_global_like: boolean;
scoped: boolean;
};

@ -129,6 +129,8 @@ export interface CompileOptions extends ModuleCompileOptions {
* Set to `undefined` (the default) to infer runes mode from the component code.
* Is always `true` for JS/TS modules compiled with Svelte.
* Will be `true` by default in Svelte 6.
* Note that setting this to `true` in your `svelte.config.js` will force runes mode for your entire project, including components in `node_modules`,
* which is likely not what you want. If you're using Vite, consider using [dynamicCompileOptions](https://github.com/sveltejs/vite-plugin-svelte/blob/main/docs/config.md#dynamiccompileoptions) instead.
* @default undefined
*/
runes?: boolean | undefined;
@ -306,11 +308,6 @@ export interface Binding {
legacy_dependencies: Binding[];
/** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */
prop_alias: string | null;
/**
* If this is set, all references should use this expression instead of the identifier name.
* If a function is given, it will be called with the identifier at that location and should return the new expression.
*/
expression: Expression | ((id: Identifier) => Expression) | null;
/** If this is set, all mutations should use this expression */
mutation: ((assignment: AssignmentExpression, context: Context<any, any>) => Expression) | null;
/** Additional metadata, varies per binding type */

@ -211,6 +211,10 @@ export interface OnDirective extends BaseNode {
/** The 'y' in `on:x={y}` */
expression: null | Expression;
modifiers: string[]; // TODO specify
metadata: {
contains_call_expression: boolean;
dynamic: boolean;
};
}
export type DelegatedEvent =

@ -320,10 +320,11 @@ export function object(properties) {
}
/**
* @param {Array<ESTree.RestElement | ESTree.AssignmentProperty>} properties
* @param {Array<ESTree.RestElement | ESTree.AssignmentProperty | ESTree.Property>} properties
* @returns {ESTree.ObjectPattern}
*/
export function object_pattern(properties) {
// @ts-expect-error the types appear to be wrong
return { type: 'ObjectPattern', properties };
}

@ -70,7 +70,7 @@ function merge_tables(this_table, other_table) {
}
return [new_table, idx_map, val_changed, idx_changed];
}
const regex_line_token = /([^\d\w\s]|\s+)/g;
const regex_line_token = /([^\w\s]|\s+)/g;
/** */
export class MappedCode {
/**

@ -1,3 +1,6 @@
/** @import { ComponentContext, ComponentContextLegacy } from '#client' */
/** @import { EventDispatcher } from './index.js' */
/** @import { NotFunction } from './internal/types.js' */
import { current_component_context, flush_sync, untrack } from './internal/client/runtime.js';
import { is_array } from './internal/shared/utils.js';
import { user_effect } from './internal/client/index.js';
@ -15,7 +18,7 @@ import { lifecycle_outside_component } from './internal/shared/errors.js';
*
* https://svelte.dev/docs/svelte#onmount
* @template T
* @param {() => import('./internal/types').NotFunction<T> | Promise<import('./internal/types').NotFunction<T>> | (() => any)} fn
* @param {() => NotFunction<T> | Promise<NotFunction<T>> | (() => any)} fn
* @returns {void}
*/
export function onMount(fn) {
@ -84,7 +87,7 @@ function create_custom_event(type, detail, { bubbles = false, cancelable = false
* https://svelte.dev/docs/svelte#createeventdispatcher
* @deprecated Use callback props and/or the `$host()` rune instead see https://svelte-5-preview.vercel.app/docs/deprecations#createeventdispatcher
* @template {Record<string, any>} [EventMap = any]
* @returns {import('./index.js').EventDispatcher<EventMap>}
* @returns {EventDispatcher<EventMap>}
*/
export function createEventDispatcher() {
const component_context = current_component_context;
@ -164,10 +167,10 @@ export function afterUpdate(fn) {
/**
* Legacy-mode: Init callbacks object for onMount/beforeUpdate/afterUpdate
* @param {import('#client').ComponentContext} context
* @param {ComponentContext} context
*/
function init_update_callbacks(context) {
var l = /** @type {import('#client').ComponentContextLegacy} */ (context).l;
var l = /** @type {ComponentContextLegacy} */ (context).l;
return (l.u ??= { a: [], b: [], m: [] });
}

@ -1,10 +1,11 @@
/** @import { Component } from '#server' */
import { current_component } from './internal/server/context.js';
import { noop } from './internal/shared/utils.js';
import * as e from './internal/server/errors.js';
/** @param {() => void} fn */
export function onDestroy(fn) {
var context = /** @type {import('#server').Component} */ (current_component);
var context = /** @type {Component} */ (current_component);
(context.d ??= []).push(fn);
}

@ -104,6 +104,15 @@ export class SvelteComponent<
$set(props: Partial<Props>): void;
}
declare const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Internal implementation details that vary between environments
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
/**
* Can be used to create strongly typed Svelte components.
*
@ -136,7 +145,8 @@ export interface Component<
* @param props The props passed to the component.
*/
(
internal: unknown,
this: void,
internals: ComponentInternals,
props: Props
): {
/**

@ -20,7 +20,10 @@ export function init_array_prototype_warnings() {
const test = indexOf.call(get_proxied_value(this), get_proxied_value(item), from_index);
if (test !== -1) {
w.state_proxy_equality_mismatch('array.indexOf(...)');
w.state_proxy_equality_mismatch(
'array.indexOf(...)',
': `array.findIndex(entry => $state.is(entry, item))`'
);
}
}
@ -42,7 +45,10 @@ export function init_array_prototype_warnings() {
);
if (test !== -1) {
w.state_proxy_equality_mismatch('array.lastIndexOf(...)');
w.state_proxy_equality_mismatch(
'array.lastIndexOf(...)',
': `array.findLastIndex(entry => $state.is(entry, item))`'
);
}
}
@ -56,7 +62,10 @@ export function init_array_prototype_warnings() {
const test = includes.call(get_proxied_value(this), get_proxied_value(item), from_index);
if (test) {
w.state_proxy_equality_mismatch('array.includes(...)');
w.state_proxy_equality_mismatch(
'array.includes(...)',
': `array.some(entry => $state.is(entry, item))`'
);
}
}
@ -79,7 +88,7 @@ export function init_array_prototype_warnings() {
*/
export function strict_equals(a, b, equal = true) {
if ((a === b) !== (get_proxied_value(a) === get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '===' : '!==');
w.state_proxy_equality_mismatch(equal ? '===' : '!==', '');
}
return (a === b) === equal;
@ -93,7 +102,7 @@ export function strict_equals(a, b, equal = true) {
*/
export function equals(a, b, equal = true) {
if ((a == b) !== (get_proxied_value(a) == get_proxied_value(b))) {
w.state_proxy_equality_mismatch(equal ? '==' : '!=');
w.state_proxy_equality_mismatch(equal ? '==' : '!=', '');
}
return (a == b) === equal;

@ -1,6 +1,7 @@
/** @import { Source, Effect } from '#client' */
/** @import { Source, Effect, TemplateNode } from '#client' */
import { FILENAME, HMR } from '../../../constants.js';
import { EFFECT_TRANSPARENT } from '../constants.js';
import { hydrate_node, hydrating } from '../dom/hydration.js';
import { block, branch, destroy_effect } from '../reactivity/effects.js';
import { source } from '../reactivity/sources.js';
import { set_should_intro } from '../render.js';
@ -13,7 +14,7 @@ import { get } from '../runtime.js';
*/
export function hmr(original, get_source) {
/**
* @param {Comment} anchor
* @param {TemplateNode} anchor
* @param {any} props
*/
function wrapper(anchor, props) {
@ -53,6 +54,10 @@ export function hmr(original, get_source) {
ran = true;
if (hydrating) {
anchor = hydrate_node;
}
return instance;
}

@ -1,4 +1,4 @@
/** @import { TemplateNode } from '#client' */
/** @import { EachItem, EachState, Effect, EffectNodes, MaybeSource, Source, TemplateNode, TransitionManager, Value } from '#client' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@ -36,11 +36,11 @@ import { current_effect } from '../../runtime.js';
/**
* The row of a keyed each block that is currently updating. We track this
* so that `animate:` directives have something to attach themselves to
* @type {import('#client').EachItem | null}
* @type {EachItem | null}
*/
export let current_each_item = null;
/** @param {import('#client').EachItem | null} item */
/** @param {EachItem | null} item */
export function set_current_each_item(item) {
current_each_item = item;
}
@ -56,13 +56,13 @@ export function index(_, i) {
/**
* Pause multiple effects simultaneously, and coordinate their
* subsequent destruction. Used in each blocks
* @param {import('#client').EachState} state
* @param {import('#client').EachItem[]} items
* @param {EachState} state
* @param {EachItem[]} items
* @param {null | Node} controlled_anchor
* @param {Map<any, import("#client").EachItem>} items_map
* @param {Map<any, EachItem>} items_map
*/
function pause_effects(state, items, controlled_anchor, items_map) {
/** @type {import('#client').TransitionManager[]} */
/** @type {TransitionManager[]} */
var transitions = [];
var length = items.length;
@ -101,14 +101,14 @@ function pause_effects(state, items, controlled_anchor, items_map) {
* @param {number} flags
* @param {() => V[]} get_collection
* @param {(value: V, index: number) => any} get_key
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {(anchor: Node, item: MaybeSource<V>, index: MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
export function each(node, flags, get_collection, get_key, render_fn, fallback_fn = null) {
var anchor = node;
/** @type {import('#client').EachState} */
/** @type {EachState} */
var state = { flags, items: new Map(), first: null };
var is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -125,7 +125,7 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
hydrate_next();
}
/** @type {import('#client').Effect | null} */
/** @type {Effect | null} */
var fallback = null;
block(() => {
@ -174,10 +174,10 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
// this is separate to the previous block because `hydrating` might change
if (hydrating) {
/** @type {import('#client').EachItem | null} */
/** @type {EachItem | null} */
var prev = null;
/** @type {import('#client').EachItem} */
/** @type {EachItem} */
var item;
for (var i = 0; i < length; i++) {
@ -239,9 +239,9 @@ export function each(node, flags, get_collection, get_key, render_fn, fallback_f
/**
* @template V
* @param {Array<V>} array
* @param {import('#client').EachState} state
* @param {EachState} state
* @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {(anchor: Node, item: MaybeSource<V>, index: number | Source<number>) => void} render_fn
* @param {number} flags
* @param {(value: V, index: number) => any} get_key
* @returns {void}
@ -255,19 +255,19 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
var first = state.first;
var current = first;
/** @type {Set<import('#client').EachItem>} */
/** @type {Set<EachItem>} */
var seen = new Set();
/** @type {import('#client').EachItem | null} */
/** @type {EachItem | null} */
var prev = null;
/** @type {Set<import('#client').EachItem>} */
/** @type {Set<EachItem>} */
var to_animate = new Set();
/** @type {import('#client').EachItem[]} */
/** @type {EachItem[]} */
var matched = [];
/** @type {import('#client').EachItem[]} */
/** @type {EachItem[]} */
var stashed = [];
/** @type {V} */
@ -276,7 +276,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
/** @type {any} */
var key;
/** @type {import('#client').EachItem | undefined} */
/** @type {EachItem | undefined} */
var item;
/** @type {number} */
@ -301,9 +301,7 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
item = items.get(key);
if (item === undefined) {
var child_anchor = current
? /** @type {import('#client').EffectNodes} */ (current.e.nodes).start
: anchor;
var child_anchor = current ? /** @type {EffectNodes} */ (current.e.nodes).start : anchor;
prev = create_item(
child_anchor,
@ -436,12 +434,12 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) {
});
}
/** @type {import('#client').Effect} */ (current_effect).first = state.first && state.first.e;
/** @type {import('#client').Effect} */ (current_effect).last = prev && prev.e;
/** @type {Effect} */ (current_effect).first = state.first && state.first.e;
/** @type {Effect} */ (current_effect).last = prev && prev.e;
}
/**
* @param {import('#client').EachItem} item
* @param {EachItem} item
* @param {any} value
* @param {number} index
* @param {number} type
@ -453,7 +451,7 @@ function update_item(item, value, index, type) {
}
if ((type & EACH_INDEX_REACTIVE) !== 0) {
set(/** @type {import('#client').Value<number>} */ (item.i), index);
set(/** @type {Value<number>} */ (item.i), index);
} else {
item.i = index;
}
@ -462,15 +460,15 @@ function update_item(item, value, index, type) {
/**
* @template V
* @param {Node} anchor
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
* @param {EachState} state
* @param {EachItem | null} prev
* @param {EachItem | null} next
* @param {V} value
* @param {unknown} key
* @param {number} index
* @param {(anchor: Node, item: V | import('#client').Source<V>, index: number | import('#client').Value<number>) => void} render_fn
* @param {(anchor: Node, item: V | Source<V>, index: number | Value<number>) => void} render_fn
* @param {number} flags
* @returns {import('#client').EachItem}
* @returns {EachItem}
*/
function create_item(anchor, state, prev, next, value, key, index, render_fn, flags) {
var previous_each_item = current_each_item;
@ -482,7 +480,7 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
/** @type {import('#client').EachItem} */
/** @type {EachItem} */
var item = {
i,
v,
@ -519,29 +517,27 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl
}
/**
* @param {import('#client').EachItem} item
* @param {import('#client').EachItem | null} next
* @param {EachItem} item
* @param {EachItem | null} next
* @param {Text | Element | Comment} anchor
*/
function move(item, next, anchor) {
var end = item.next
? /** @type {import('#client').EffectNodes} */ (item.next.e.nodes).start
: anchor;
var end = item.next ? /** @type {EffectNodes} */ (item.next.e.nodes).start : anchor;
var dest = next ? /** @type {import('#client').EffectNodes} */ (next.e.nodes).start : anchor;
var node = /** @type {import('#client').EffectNodes} */ (item.e.nodes).start;
var dest = next ? /** @type {EffectNodes} */ (next.e.nodes).start : anchor;
var node = /** @type {EffectNodes} */ (item.e.nodes).start;
while (node !== end) {
var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling);
var next_node = /** @type {TemplateNode} */ (node.nextSibling);
dest.before(node);
node = next_node;
}
}
/**
* @param {import('#client').EachState} state
* @param {import('#client').EachItem | null} prev
* @param {import('#client').EachItem | null} next
* @param {EachState} state
* @param {EachItem | null} prev
* @param {EachItem | null} next
*/
function link(state, prev, next) {
if (prev === null) {

@ -1,4 +1,4 @@
/** @import { TemplateNode } from '#client' */
/** @import { Effect, TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '../../constants.js';
import {
hydrate_next,
@ -14,8 +14,8 @@ import { HYDRATION_START_ELSE } from '../../../../constants.js';
/**
* @param {TemplateNode} node
* @param {() => boolean} get_condition
* @param {(anchor: Node) => import('#client').Dom} consequent_fn
* @param {null | ((anchor: Node) => import('#client').Dom)} [alternate_fn]
* @param {(anchor: Node) => void} consequent_fn
* @param {null | ((anchor: Node) => void)} [alternate_fn]
* @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local'
* @returns {void}
*/
@ -26,10 +26,10 @@ export function if_block(node, get_condition, consequent_fn, alternate_fn = null
var anchor = node;
/** @type {import('#client').Effect | null} */
/** @type {Effect | null} */
var consequent_effect = null;
/** @type {import('#client').Effect | null} */
/** @type {Effect | null} */
var alternate_effect = null;
/** @type {boolean | null} */

@ -1,7 +1,6 @@
/** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import {
@ -55,7 +54,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn
*/
export function wrap_snippet(component, fn) {
return add_snippet_symbol((/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@ -64,7 +63,7 @@ export function wrap_snippet(component, fn) {
} finally {
set_dev_current_component_function(previous_component_function);
}
});
};
}
/**
@ -77,32 +76,33 @@ export function wrap_snippet(component, fn) {
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol(
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
if (DEV && (element.nextSibling !== null || element.nodeType !== 1)) {
w.invalid_raw_snippet_render();
}
anchor.before(element);
}
// @ts-expect-error the types are a lie
return (/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (hydrating) {
element = /** @type {Element} */ (hydrate_node);
hydrate_next();
} else {
var html = snippet.render().trim();
var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild);
if (typeof result === 'function') {
teardown(result);
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
w.invalid_raw_snippet_render();
}
anchor.before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
);
};
}

@ -1,3 +1,4 @@
/** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, Task, TransitionFn, TransitionManager } from '#client' */
import { noop, is_function } from '../../../shared/utils.js';
import { effect } from '../../reactivity/effects.js';
import { current_effect, untrack } from '../../runtime.js';
@ -60,11 +61,11 @@ const linear = (t) => t;
* and attaches it to the block, so that moves can be animated following reconciliation.
* @template P
* @param {Element} element
* @param {() => import('#client').AnimateFn<P | undefined>} get_fn
* @param {() => AnimateFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
*/
export function animation(element, get_fn, get_params) {
var item = /** @type {import('#client').EachItem} */ (current_each_item);
var item = /** @type {EachItem} */ (current_each_item);
/** @type {DOMRect} */
var from;
@ -72,7 +73,7 @@ export function animation(element, get_fn, get_params) {
/** @type {DOMRect} */
var to;
/** @type {import('#client').Animation | undefined} */
/** @type {Animation | undefined} */
var animation;
/** @type {null | { position: string, width: string, height: string, transform: string }} */
@ -167,7 +168,7 @@ export function animation(element, get_fn, get_params) {
* @template P
* @param {number} flags
* @param {HTMLElement} element
* @param {() => import('#client').TransitionFn<P | undefined>} get_fn
* @param {() => TransitionFn<P | undefined>} get_fn
* @param {(() => P) | null} get_params
* @returns {void}
*/
@ -180,15 +181,15 @@ export function transition(flags, element, get_fn, get_params) {
/** @type {'in' | 'out' | 'both'} */
var direction = is_both ? 'both' : is_intro ? 'in' : 'out';
/** @type {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig) | undefined} */
/** @type {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig) | undefined} */
var current_options;
var inert = element.inert;
/** @type {import('#client').Animation | undefined} */
/** @type {Animation | undefined} */
var intro;
/** @type {import('#client').Animation | undefined} */
/** @type {Animation | undefined} */
var outro;
/** @type {(() => void) | undefined} */
@ -201,7 +202,7 @@ export function transition(flags, element, get_fn, get_params) {
return (current_options ??= get_fn()(element, get_params?.(), { direction }));
}
/** @type {import('#client').TransitionManager} */
/** @type {TransitionManager} */
var transition = {
is_global,
in() {
@ -271,7 +272,7 @@ export function transition(flags, element, get_fn, get_params) {
}
};
var e = /** @type {import('#client').Effect} */ (current_effect);
var e = /** @type {Effect} */ (current_effect);
(e.transitions ??= []).push(transition);
@ -282,7 +283,7 @@ export function transition(flags, element, get_fn, get_params) {
let run = is_global;
if (!run) {
var block = /** @type {import('#client').Effect | null} */ (e.parent);
var block = /** @type {Effect | null} */ (e.parent);
// skip over transparent blocks (e.g. snippets, else-if blocks)
while (block && (block.f & EFFECT_TRANSPARENT) !== 0) {
@ -305,12 +306,12 @@ export function transition(flags, element, get_fn, get_params) {
/**
* Animates an element, according to the provided configuration
* @param {Element} element
* @param {import('#client').AnimationConfig | ((opts: { direction: 'in' | 'out' }) => import('#client').AnimationConfig)} options
* @param {import('#client').Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {AnimationConfig | ((opts: { direction: 'in' | 'out' }) => AnimationConfig)} options
* @param {Animation | undefined} counterpart The corresponding intro/outro to this outro/intro
* @param {number} t2 The target `t` value `1` for intro, `0` for outro
* @param {(() => void) | undefined} on_finish Called after successfully completing the animation
* @param {(() => void) | undefined} on_abort Called if the animation is aborted
* @returns {import('#client').Animation}
* @returns {Animation}
*/
function animate(element, options, counterpart, t2, on_finish, on_abort) {
var is_intro = t2 === 1;
@ -319,7 +320,7 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
// In the case of a deferred transition (such as `crossfade`), `option` will be
// a function rather than an `AnimationConfig`. We need to call this function
// once DOM has been updated...
/** @type {import('#client').Animation} */
/** @type {Animation} */
var a;
queue_micro_task(() => {
@ -358,10 +359,10 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
var duration = options.duration * Math.abs(delta);
var end = start + duration;
/** @type {Animation} */
/** @type {globalThis.Animation} */
var animation;
/** @type {import('#client').Task} */
/** @type {Task} */
var task;
if (css) {

@ -1,6 +1,12 @@
/** @import { TemplateNode } from '#client' */
import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js';
import {
HYDRATION_END,
HYDRATION_ERROR,
HYDRATION_START,
HYDRATION_START_ELSE
} from '../../../constants.js';
import * as w from '../warnings.js';
/**
* Use this variable to guard everything related to hydration code so it can be treeshaken out
@ -28,6 +34,10 @@ export function set_hydrate_node(node) {
}
export function hydrate_next() {
if (hydrate_node === null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling));
}

@ -101,6 +101,7 @@ export {
legacy_pre_effect_reset,
render_effect,
template_effect,
effect,
user_effect,
user_pre_effect
} from './reactivity/effects.js';
@ -122,7 +123,7 @@ export {
} from './reactivity/props.js';
export {
invalidate_store,
mutate_store,
store_mutate,
setup_stores,
store_get,
store_set,
@ -172,9 +173,7 @@ export { snapshot } from '../shared/clone.js';
export { noop } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_component,
validate_dynamic_element_tag,
validate_snippet,
validate_store,
validate_void_dynamic_element
} from '../shared/validate.js';

@ -1,3 +1,4 @@
/** @import { ProxyMetadata, ProxyStateObject, Source } from '#client' */
import { DEV } from 'esm-env';
import { get, current_component_context, untrack, current_effect } from './runtime.js';
import {
@ -18,9 +19,9 @@ import * as e from './errors.js';
/**
* @template T
* @param {T} value
* @param {import('#client').ProxyMetadata | null} [parent]
* @param {import('#client').Source<T>} [prev] dev mode only
* @returns {import('#client').ProxyStateObject<T> | T}
* @param {ProxyMetadata | null} [parent]
* @param {Source<T>} [prev] dev mode only
* @returns {ProxyStateObject<T> | T}
*/
export function proxy(value, parent = null, prev) {
if (
@ -31,7 +32,7 @@ export function proxy(value, parent = null, prev) {
) {
// If we have an existing proxy, return it...
if (STATE_SYMBOL in value) {
const metadata = /** @type {import('#client').ProxyMetadata<T>} */ (value[STATE_SYMBOL]);
const metadata = /** @type {ProxyMetadata<T>} */ (value[STATE_SYMBOL]);
// ...unless the proxy belonged to a different object, because
// someone copied the state symbol using `Reflect.ownKeys(...)`
@ -53,7 +54,7 @@ export function proxy(value, parent = null, prev) {
const proxy = new Proxy(value, state_proxy_handler);
define_property(value, STATE_SYMBOL, {
value: /** @type {import('#client').ProxyMetadata} */ ({
value: /** @type {ProxyMetadata} */ ({
s: new Map(),
v: source(0),
a: is_array(value),
@ -94,18 +95,18 @@ export function proxy(value, parent = null, prev) {
}
/**
* @param {import('#client').Source<number>} signal
* @param {Source<number>} signal
* @param {1 | -1} [d]
*/
function update_version(signal, d = 1) {
set(signal, signal.v + d);
}
/** @type {ProxyHandler<import('#client').ProxyStateObject<any>>} */
/** @type {ProxyHandler<ProxyStateObject<any>>} */
const state_proxy_handler = {
defineProperty(target, prop, descriptor) {
if (descriptor.value) {
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
@ -116,7 +117,7 @@ const state_proxy_handler = {
},
deleteProperty(target, prop) {
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
const is_array = metadata.a;
@ -149,7 +150,7 @@ const state_proxy_handler = {
return Reflect.get(target, STATE_SYMBOL);
}
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
@ -170,7 +171,7 @@ const state_proxy_handler = {
getOwnPropertyDescriptor(target, prop) {
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
if (descriptor && 'value' in descriptor) {
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const s = metadata.s.get(prop);
@ -186,7 +187,7 @@ const state_proxy_handler = {
if (prop === STATE_SYMBOL) {
return true;
}
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
const has = Reflect.has(target, prop);
@ -208,7 +209,7 @@ const state_proxy_handler = {
},
set(target, prop, value, receiver) {
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
let s = metadata.s.get(prop);
// If we haven't yet created a source for this property, we need to ensure
@ -227,7 +228,7 @@ const state_proxy_handler = {
const not_has = !(prop in target);
if (DEV) {
/** @type {import('#client').ProxyMetadata | undefined} */
/** @type {ProxyMetadata | undefined} */
const prop_metadata = value?.[STATE_SYMBOL];
if (prop_metadata && prop_metadata?.parent !== metadata) {
widen_ownership(metadata, prop_metadata);
@ -271,7 +272,7 @@ const state_proxy_handler = {
},
ownKeys(target) {
/** @type {import('#client').ProxyMetadata} */
/** @type {ProxyMetadata} */
const metadata = target[STATE_SYMBOL];
get(metadata.v);

@ -1,3 +1,4 @@
/** @import { Derived } from '#client' */
import { CLEAN, DERIVED, DESTROYED, DIRTY, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
current_reaction,
@ -16,14 +17,14 @@ export let updating_derived = false;
/**
* @template V
* @param {() => V} fn
* @returns {import('#client').Derived<V>}
* @returns {Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
let flags = DERIVED | DIRTY;
if (current_effect === null) flags |= UNOWNED;
/** @type {import('#client').Derived<V>} */
/** @type {Derived<V>} */
const signal = {
deps: null,
deriveds: null,
@ -38,7 +39,7 @@ export function derived(fn) {
};
if (current_reaction !== null && (current_reaction.f & DERIVED) !== 0) {
var current_derived = /** @type {import('#client').Derived} */ (current_reaction);
var current_derived = /** @type {Derived} */ (current_reaction);
if (current_derived.deriveds === null) {
current_derived.deriveds = [signal];
} else {
@ -52,7 +53,7 @@ export function derived(fn) {
/**
* @template V
* @param {() => V} fn
* @returns {import('#client').Derived<V>}
* @returns {Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
@ -62,7 +63,7 @@ export function derived_safe_equal(fn) {
}
/**
* @param {import('#client').Derived} derived
* @param {Derived} derived
* @returns {void}
*/
function destroy_derived_children(derived) {
@ -79,7 +80,7 @@ function destroy_derived_children(derived) {
}
/**
* @param {import('#client').Derived} derived
* @param {Derived} derived
* @returns {void}
*/
export function update_derived(derived) {
@ -103,7 +104,7 @@ export function update_derived(derived) {
}
/**
* @param {import('#client').Derived} signal
* @param {Derived} signal
* @returns {void}
*/
export function destroy_derived(signal) {

@ -1,3 +1,4 @@
/** @import { ComponentContext, ComponentContextLegacy, Effect, Reaction, TemplateNode, TransitionManager } from '#client' */
import {
check_dirtiness,
current_component_context,
@ -57,8 +58,8 @@ export function validate_effect(rune) {
}
/**
* @param {import("#client").Effect} effect
* @param {import("#client").Reaction} parent_effect
* @param {Effect} effect
* @param {Reaction} parent_effect
*/
export function push_effect(effect, parent_effect) {
var parent_last = parent_effect.last;
@ -76,12 +77,12 @@ export function push_effect(effect, parent_effect) {
* @param {null | (() => void | (() => void))} fn
* @param {boolean} sync
* @param {boolean} push
* @returns {import('#client').Effect}
* @returns {Effect}
*/
function create_effect(type, fn, sync, push = true) {
var is_root = (type & ROOT_EFFECT) !== 0;
/** @type {import('#client').Effect} */
/** @type {Effect} */
var effect = {
ctx: current_component_context,
deps: null,
@ -187,7 +188,7 @@ export function user_effect(fn) {
}
if (defer) {
var context = /** @type {import('#client').ComponentContext} */ (current_component_context);
var context = /** @type {ComponentContext} */ (current_component_context);
(context.e ??= []).push(fn);
} else {
var signal = effect(fn);
@ -198,7 +199,7 @@ export function user_effect(fn) {
/**
* Internal representation of `$effect.pre(...)`
* @param {() => void | (() => void)} fn
* @returns {import('#client').Effect}
* @returns {Effect}
*/
export function user_pre_effect(fn) {
validate_effect('$effect.pre');
@ -229,7 +230,7 @@ export function effect_root(fn) {
/**
* @param {() => void | (() => void)} fn
* @returns {import('#client').Effect}
* @returns {Effect}
*/
export function effect(fn) {
return create_effect(EFFECT, fn, false);
@ -241,9 +242,9 @@ export function effect(fn) {
* @param {() => void | (() => void)} fn
*/
export function legacy_pre_effect(deps, fn) {
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
var context = /** @type {ComponentContextLegacy} */ (current_component_context);
/** @type {{ effect: null | import('#client').Effect, ran: boolean }} */
/** @type {{ effect: null | Effect, ran: boolean }} */
var token = { effect: null, ran: false };
context.l.r1.push(token);
@ -261,7 +262,7 @@ export function legacy_pre_effect(deps, fn) {
}
export function legacy_pre_effect_reset() {
var context = /** @type {import('#client').ComponentContextLegacy} */ (current_component_context);
var context = /** @type {ComponentContextLegacy} */ (current_component_context);
render_effect(() => {
if (!get(context.l.r2)) return;
@ -283,7 +284,7 @@ export function legacy_pre_effect_reset() {
/**
* @param {() => void | (() => void)} fn
* @returns {import('#client').Effect}
* @returns {Effect}
*/
export function render_effect(fn) {
return create_effect(RENDER_EFFECT, fn, true);
@ -291,7 +292,7 @@ export function render_effect(fn) {
/**
* @param {() => void | (() => void)} fn
* @returns {import('#client').Effect}
* @returns {Effect}
*/
export function template_effect(fn) {
if (DEV) {
@ -319,7 +320,7 @@ export function branch(fn, push = true) {
}
/**
* @param {import("#client").Effect} effect
* @param {Effect} effect
*/
export function execute_effect_teardown(effect) {
var teardown = effect.teardown;
@ -338,7 +339,7 @@ export function execute_effect_teardown(effect) {
}
/**
* @param {import('#client').Effect} effect
* @param {Effect} effect
* @param {boolean} [remove_dom]
* @returns {void}
*/
@ -346,14 +347,13 @@ export function destroy_effect(effect, remove_dom = true) {
var removed = false;
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes !== null) {
/** @type {import('#client').TemplateNode | null} */
/** @type {TemplateNode | null} */
var node = effect.nodes.start;
var end = effect.nodes.end;
while (node !== null) {
/** @type {import('#client').TemplateNode | null} */
var next =
node === end ? null : /** @type {import('#client').TemplateNode} */ (node.nextSibling);
/** @type {TemplateNode | null} */
var next = node === end ? null : /** @type {TemplateNode} */ (node.nextSibling);
node.remove();
node = next;
@ -396,7 +396,7 @@ export function destroy_effect(effect, remove_dom = true) {
/**
* Detach an effect from the effect tree, freeing up memory and
* reducing the amount of work that happens on subsequent traversals
* @param {import('#client').Effect} effect
* @param {Effect} effect
*/
export function unlink_effect(effect) {
var parent = effect.parent;
@ -418,11 +418,11 @@ export function unlink_effect(effect) {
* It stays around (in memory, and in the DOM) until outro transitions have
* completed, and if the state change is reversed then we _resume_ it.
* A paused effect does not update, and the DOM subtree becomes inert.
* @param {import('#client').Effect} effect
* @param {Effect} effect
* @param {() => void} [callback]
*/
export function pause_effect(effect, callback) {
/** @type {import('#client').TransitionManager[]} */
/** @type {TransitionManager[]} */
var transitions = [];
pause_children(effect, transitions, true);
@ -434,7 +434,7 @@ export function pause_effect(effect, callback) {
}
/**
* @param {import('#client').TransitionManager[]} transitions
* @param {TransitionManager[]} transitions
* @param {() => void} fn
*/
export function run_out_transitions(transitions, fn) {
@ -450,8 +450,8 @@ export function run_out_transitions(transitions, fn) {
}
/**
* @param {import('#client').Effect} effect
* @param {import('#client').TransitionManager[]} transitions
* @param {Effect} effect
* @param {TransitionManager[]} transitions
* @param {boolean} local
*/
export function pause_children(effect, transitions, local) {
@ -482,14 +482,14 @@ export function pause_children(effect, transitions, local) {
/**
* The opposite of `pause_effect`. We call this if (for example)
* `x` becomes falsy then truthy: `{#if x}...{/if}`
* @param {import('#client').Effect} effect
* @param {Effect} effect
*/
export function resume_effect(effect) {
resume_children(effect, true);
}
/**
* @param {import('#client').Effect} effect
* @param {Effect} effect
* @param {boolean} local
*/
function resume_children(effect, local) {

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

Loading…
Cancel
Save