pull/12694/head
Rich Harris 2 years ago
commit 9c2d613188

@ -0,0 +1,5 @@
---
'svelte': patch
---
perf: speed up $.exclude_from_object

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure custom elements do not sync flush on mount

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: correctly handle SvelteDate methods with arguments

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: publish package provenance info

@ -0,0 +1,5 @@
---
"svelte": minor
---
feat: make custom element `tag` property optional

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: treat tag with `.` as a component, even if lowercase

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improve prop binding warning validation for stores

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: add touch events on microtask to avoid Chromium bug

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure custom element styles append correctly during prod

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: allow deletion of $$restProps properties

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: add error for derived self referencing

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: skip `is_standalone` optimisation for dynamic components

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: invalidate signals following ++/-- inside each block

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure unowned deriveds correctly update

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: better code generation for destructuring assignments

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: propagate custom element component prop changes

@ -53,6 +53,7 @@
"bright-snakes-sing",
"brown-geckos-fry",
"brown-houses-obey",
"brown-insects-float",
"brown-months-fry",
"brown-spoons-boil",
"calm-buses-clap",
@ -67,6 +68,7 @@
"chilled-ladybugs-invite",
"chilled-pumas-invite",
"chilled-seas-jog",
"chilly-carpets-switch",
"chilly-dolphins-lick",
"chilly-laws-juggle",
"chilly-pans-raise",
@ -96,6 +98,7 @@
"cool-poems-watch",
"cool-rabbits-tickle",
"cool-roses-trade",
"cool-turtles-travel",
"cuddly-pianos-drop",
"cuddly-points-tickle",
"curly-cooks-cheer",
@ -115,6 +118,7 @@
"dirty-donuts-yell",
"dirty-eyes-itch",
"dirty-garlics-design",
"dirty-pens-look",
"dirty-pianos-eat",
"dirty-tips-add",
"dry-clocks-grow",
@ -191,6 +195,7 @@
"fluffy-colts-grin",
"fluffy-dolls-share",
"fluffy-humans-worry",
"fluffy-kings-complain",
"fluffy-ravens-juggle",
"forty-bikes-buy",
"forty-comics-invent",
@ -200,6 +205,7 @@
"forty-suns-smile",
"four-balloons-beam",
"four-flies-hammer",
"four-kids-flow",
"four-mice-hammer",
"four-papayas-turn",
"four-peas-tickle",
@ -264,6 +270,8 @@
"happy-moles-live",
"happy-suits-film",
"healthy-ants-film",
"healthy-dancers-play",
"healthy-mangos-brush",
"healthy-planes-vanish",
"healthy-zebras-accept",
"heavy-comics-move",
@ -273,6 +281,7 @@
"heavy-feet-attend",
"hip-balloons-begin",
"hip-garlics-tap",
"hip-goats-smoke",
"hip-months-breathe",
"hip-pumpkins-boil",
"hip-stingrays-teach",
@ -354,6 +363,7 @@
"loud-cheetahs-flow",
"loud-insects-arrive",
"loud-mugs-smile",
"loud-news-deliver",
"loud-numbers-flow",
"loud-penguins-unite",
"loud-ravens-drop",
@ -412,9 +422,12 @@
"nervous-turkeys-end",
"new-boats-wait",
"new-brooms-grin",
"new-cooks-roll",
"new-rabbits-flow",
"nice-avocados-move",
"nice-bottles-greet",
"nice-jobs-breathe",
"nine-ants-invite",
"nine-cooks-join",
"ninety-days-visit",
"ninety-dingos-walk",
@ -432,6 +445,7 @@
"old-oranges-compete",
"olive-apples-lick",
"olive-cobras-wonder",
"olive-forks-grin",
"olive-kangaroos-brake",
"olive-mice-fix",
"olive-moons-act",
@ -445,6 +459,7 @@
"orange-yaks-protect",
"orange-zoos-heal",
"perfect-actors-bake",
"perfect-hairs-matter",
"perfect-hats-dance",
"pink-bikes-agree",
"pink-goats-promise",
@ -463,6 +478,7 @@
"polite-ways-serve",
"poor-eggs-enjoy",
"poor-hats-design",
"poor-mugs-pay",
"poor-seahorses-flash",
"popular-apes-bathe",
"popular-cameras-tie",
@ -495,6 +511,7 @@
"red-cycles-pretend",
"red-doors-own",
"red-feet-worry",
"red-kings-draw",
"red-poets-study",
"red-pots-pretend",
"rich-cobras-exist",
@ -560,10 +577,12 @@
"silly-ducks-mix",
"silly-laws-happen",
"silly-lies-film",
"silly-masks-exist",
"silly-ways-wash",
"silver-mice-double",
"silver-points-approve",
"silver-sheep-knock",
"six-apes-peel",
"six-bears-trade",
"six-boats-shave",
"six-chicken-kneel",
@ -593,6 +612,7 @@
"small-planets-destroy",
"small-sheep-type",
"small-spiders-fail",
"smart-cars-know",
"smart-cherries-leave",
"smart-fans-crash",
"smart-grapes-mate",
@ -605,6 +625,7 @@
"smooth-pens-exist",
"smooth-pens-protect",
"smooth-rings-rush",
"smooth-windows-explain",
"soft-clocks-remember",
"soft-geese-learn",
"soft-months-grab",
@ -640,6 +661,7 @@
"strong-pans-doubt",
"stupid-bottles-lay",
"stupid-parents-crash",
"stupid-rivers-stare",
"sweet-bottles-check",
"sweet-mangos-beg",
"sweet-pens-sniff",
@ -682,6 +704,7 @@
"thick-swans-type",
"thin-colts-yawn",
"thin-foxes-lick",
"thin-papayas-tap",
"thin-spoons-float",
"thin-years-rhyme",
"thirty-dogs-whisper",
@ -756,10 +779,12 @@
"wicked-hairs-cheer",
"wicked-wasps-allow",
"wicked-ways-reply",
"wicked-years-drive",
"wild-cows-chew",
"wild-foxes-wonder",
"wild-moose-compare",
"wild-poems-design",
"wild-pumas-count",
"wise-apples-care",
"wise-dancers-hang",
"wise-dodos-tell",
@ -770,6 +795,7 @@
"wise-ties-clap",
"witty-bikes-shave",
"witty-camels-warn",
"witty-frogs-cheat",
"witty-hornets-think",
"witty-phones-retire",
"witty-readers-provide",

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent numerous transition/animation memory leaks

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: order of arguments for `push_element` in `svelte:element`

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: avoid recreating handlers for component events

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure event handlers referencing $host are not hoisted

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: provide more hydration mismatch coverage

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: improved memory profile for transitions/animations

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: simplify derived object destructuring

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: call correct event handler for properties of non-reactive objects

@ -0,0 +1,5 @@
---
'svelte': patch
---
chore: simpler fallback values

@ -12,6 +12,7 @@ jobs:
if: github.repository == 'sveltejs/svelte'
permissions:
contents: write # to create release (changesets/action)
id-token: write # OpenID Connect token needed for provenance
pull-requests: write # to create pull request (changesets/action)
name: Release
runs-on: ubuntu-latest

@ -21,7 +21,10 @@
"type": "node",
"request": "launch",
"name": "Run sandbox",
"program": "${workspaceFolder}/playgrounds/sandbox/run.js"
"program": "${workspaceFolder}/playgrounds/sandbox/run.js",
"env": {
"NODE_OPTIONS": "--stack-trace-limit=10000"
}
}
],
"compounds": [

@ -185,7 +185,7 @@ The `<svelte:options>` element provides a place to specify per-component compile
- `accessors={true}` — adds getters and setters for the component's props
- `accessors={false}` — the default
- `namespace="..."` — the namespace where this component will be used, most commonly "svg"; use the "foreign" namespace to opt out of case-insensitive attribute names and HTML-specific warnings
- `customElement="..."` — the name to use when compiling this component as a custom element
- `customElement={...}` — the [options](/docs/custom-elements-api#component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
```svelte
<svelte:options customElement="my-custom-element" />

@ -63,7 +63,7 @@ The inner Svelte component is destroyed in the next tick after the `disconnected
When constructing a custom element, you can tailor several aspects by defining `customElement` as an object within `<svelte:options>` since Svelte 4. This object may contain the following properties:
- `tag`: the mandatory `tag` property for the custom element's name
- `tag: string`: an optional `tag` property for the custom element's name. If set, a custom element with this tag name will be defined with the document's `customElements` registry upon importing this component.
- `shadow`: an optional property that can be set to `"none"` to forgo shadow root creation. Note that styles are then no longer encapsulated, and you can't use slots
- `props`: an optional property to modify certain details and behaviors of your component's properties. It offers the following settings:
- `attribute: string`: To update a custom element's prop, you have two alternatives: either set the property on the custom element's reference as illustrated above or use an HTML attribute. For the latter, the default attribute name is the lowercase property name. Modify this by assigning `attribute: "<desired name>"`.

@ -25,6 +25,6 @@ The options that can be set here are:
- `accessors={true}` — adds getters and setters for the component's props
- `accessors={false}` — the default
- `namespace="..."` — the namespace where this component will be used, most commonly `"svg"`
- `customElement="..."` — the name to use when compiling this component as a custom element
- `customElement={...}` — the [options](/docs/custom-elements-api#component-options) to use when compiling this component as a custom element. If a string is passed, it is used as the `tag` option
Consult the [API reference](/docs) for more information on these options.

@ -1,5 +1,89 @@
# svelte
## 5.0.0-next.216
### Patch Changes
- feat: make custom element `tag` property optional ([#12754](https://github.com/sveltejs/svelte/pull/12754))
- fix: improved memory profile for transitions/animations ([#12796](https://github.com/sveltejs/svelte/pull/12796))
## 5.0.0-next.215
### Patch Changes
- fix: propagate custom element component prop changes ([#12774](https://github.com/sveltejs/svelte/pull/12774))
- fix: prevent numerous transition/animation memory leaks ([#12759](https://github.com/sveltejs/svelte/pull/12759))
## 5.0.0-next.214
### Patch Changes
- fix: ensure custom element styles append correctly during prod ([#12777](https://github.com/sveltejs/svelte/pull/12777))
- fix: invalidate signals following ++/-- inside each block ([#12780](https://github.com/sveltejs/svelte/pull/12780))
- feat: better code generation for destructuring assignments ([#12780](https://github.com/sveltejs/svelte/pull/12780))
## 5.0.0-next.213
### Patch Changes
- fix: ensure custom elements do not sync flush on mount ([#12787](https://github.com/sveltejs/svelte/pull/12787))
- fix: ensure event handlers referencing $host are not hoisted ([#12775](https://github.com/sveltejs/svelte/pull/12775))
- fix: provide more hydration mismatch coverage ([#12755](https://github.com/sveltejs/svelte/pull/12755))
- chore: simpler fallback values ([#12788](https://github.com/sveltejs/svelte/pull/12788))
## 5.0.0-next.212
### Patch Changes
- perf: speed up $.exclude_from_object ([#12783](https://github.com/sveltejs/svelte/pull/12783))
- chore: publish package provenance info ([#12779](https://github.com/sveltejs/svelte/pull/12779))
- feat: simplify derived object destructuring ([#12781](https://github.com/sveltejs/svelte/pull/12781))
## 5.0.0-next.211
### Patch Changes
- fix: improve prop binding warning validation for stores ([#12745](https://github.com/sveltejs/svelte/pull/12745))
- chore: add error for derived self referencing ([#12746](https://github.com/sveltejs/svelte/pull/12746))
- fix: skip `is_standalone` optimisation for dynamic components ([#12767](https://github.com/sveltejs/svelte/pull/12767))
- fix: ensure unowned deriveds correctly update ([#12747](https://github.com/sveltejs/svelte/pull/12747))
- fix: order of arguments for `push_element` in `svelte:element` ([#12763](https://github.com/sveltejs/svelte/pull/12763))
## 5.0.0-next.210
### Patch Changes
- fix: avoid recreating handlers for component events ([#12722](https://github.com/sveltejs/svelte/pull/12722))
- fix: call correct event handler for properties of non-reactive objects ([#12722](https://github.com/sveltejs/svelte/pull/12722))
## 5.0.0-next.209
### Patch Changes
- fix: add css hash to custom element rendered with `svelte:element` ([#12715](https://github.com/sveltejs/svelte/pull/12715))
- fix: correctly handle SvelteDate methods with arguments ([#12738](https://github.com/sveltejs/svelte/pull/12738))
- fix: add touch events on microtask to avoid Chromium bug ([#12735](https://github.com/sveltejs/svelte/pull/12735))
- fix: allow deletion of $restProps properties ([#12736](https://github.com/sveltejs/svelte/pull/12736))
- feat: more efficient code generation when referencing globals ([#12712](https://github.com/sveltejs/svelte/pull/12712))
## 5.0.0-next.208
### Patch Changes

@ -18,6 +18,10 @@
> Attempted to instantiate %component% with `new %name%`, which is no longer valid in Svelte 5. If this component is not under your control, set the `compatibility.componentApi` compiler option to `4` to keep it working. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information
## derived_references_self
> A derived value cannot reference itself recursively
## each_key_duplicate
> Keyed each block has duplicate key at indexes %a% and %b%

@ -328,7 +328,7 @@ HTML restricts where certain elements can appear. In case of a violation the bro
## svelte_options_invalid_customelement
> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
> "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
## svelte_options_invalid_customelement_props

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.0.0-next.208",
"version": "5.0.0-next.216",
"type": "module",
"types": "./types/index.d.ts",
"engines": {
@ -154,5 +154,8 @@
"project": [
"src/**"
]
},
"publishConfig": {
"provenance": true
}
}

@ -1285,12 +1285,12 @@ export function svelte_options_invalid_attribute_value(node, list) {
}
/**
* "customElement" must be a string literal defining a valid custom element name or an object of the form { tag: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
* "customElement" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: "open" | "none"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function svelte_options_invalid_customelement(node) {
e(node, "svelte_options_invalid_customelement", "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag: string; shadow?: \"open\" | \"none\"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }");
e(node, "svelte_options_invalid_customelement", "\"customElement\" must be a string literal defining a valid custom element name or an object of the form { tag?: string; shadow?: \"open\" | \"none\"; props?: { [key: string]: { attribute?: string; reflect?: boolean; type: .. } } }");
}
/**

@ -40,7 +40,7 @@ export default function read_options(node) {
}
case 'customElement': {
/** @type {SvelteOptions['customElement']} */
const ce = { tag: '' };
const ce = {};
const { value: v } = attribute;
const value = v === true || Array.isArray(v) ? v : [v];
@ -79,8 +79,6 @@ export default function read_options(node) {
const tag_value = tag[1]?.value;
validate_tag(tag, tag_value);
ce.tag = tag_value;
} else {
e.svelte_options_invalid_customelement(attribute);
}
const props = properties.find(([name]) => name === 'props')?.[1];
@ -251,8 +249,4 @@ function validate_tag(attribute, tag) {
if (tag && !regex_valid_tag_name.test(tag)) {
e.svelte_options_invalid_tagname(attribute);
}
// TODO do we still need this?
// if (tag && !component.compile_options.customElement) {
// component.warn(attribute, compiler_warnings.missing_custom_element_compile_options);
// }
}

@ -74,7 +74,7 @@ function parent_is_shadowroot_template(stack) {
const regex_closing_textarea_tag = /^<\/textarea(\s[^>]*)?>/i;
const regex_closing_comment = /-->/;
const regex_capital_letter = /[A-Z]/;
const regex_component_name = /^(?:[A-Z]|[A-Za-z][A-Za-z0-9_$]*\.)/;
/** @param {Parser} parser */
export default function element(parser) {
@ -127,7 +127,7 @@ export default function element(parser) {
const type = meta_tags.has(name)
? meta_tags.get(name)
: regex_capital_letter.test(name[0])
: regex_component_name.test(name)
? 'Component'
: name === 'title' && parent_is_head(parser.stack)
? 'TitleElement'

@ -40,8 +40,8 @@ export function Attribute(node, context) {
const delegated_event = get_delegated_event(node.name.slice(2), expression, context);
if (delegated_event !== null) {
if (delegated_event.type === 'hoistable') {
delegated_event.function.metadata.hoistable = true;
if (delegated_event.hoisted) {
delegated_event.function.metadata.hoisted = true;
}
node.metadata.delegated = delegated_event;
@ -50,6 +50,9 @@ export function Attribute(node, context) {
}
}
/** @type {DelegatedEvent} */
const unhoisted = { hoisted: false };
/**
* Checks if given event attribute can be delegated/hoisted and returns the corresponding info if so
* @param {string} event_name
@ -58,26 +61,24 @@ export function Attribute(node, context) {
* @returns {null | DelegatedEvent}
*/
function get_delegated_event(event_name, handler, context) {
// Handle delegated event handlers. Bail-out if not a delegated event.
// Handle delegated event handlers. Bail out if not a delegated event.
if (!handler || !is_delegated(event_name)) {
return null;
}
// If we are not working with a RegularElement, then bail-out.
// If we are not working with a RegularElement, then bail out.
const element = context.path.at(-1);
if (element?.type !== 'RegularElement') {
return null;
}
/** @type {DelegatedEvent} */
const non_hoistable = { type: 'non-hoistable' };
/** @type {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression | null} */
let target_function = null;
let binding = null;
if (element.metadata.has_spread) {
// event attribute becomes part of the dynamic spread array
return non_hoistable;
return unhoisted;
}
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
@ -86,14 +87,14 @@ function get_delegated_event(event_name, handler, context) {
binding = context.state.scope.get(handler.name);
if (context.state.analysis.module.scope.references.has(handler.name)) {
// If a binding with the same name is referenced in the module scope (even if not declared there), bail-out
return non_hoistable;
// If a binding with the same name is referenced in the module scope (even if not declared there), bail out
return unhoisted;
}
if (binding != null) {
for (const { path } of binding.references) {
const parent = path.at(-1);
if (parent == null) return non_hoistable;
if (parent === undefined) return unhoisted;
const grandparent = path.at(-2);
@ -120,17 +121,17 @@ function get_delegated_event(event_name, handler, context) {
element.metadata.has_spread ||
!is_delegated(event_name)
) {
return non_hoistable;
return unhoisted;
}
} else if (parent.type !== 'FunctionDeclaration' && parent.type !== 'VariableDeclarator') {
return non_hoistable;
return unhoisted;
}
}
}
// If the binding is exported, bail-out
// If the binding is exported, bail out
if (context.state.analysis.exports.find((node) => node.name === handler.name)) {
return non_hoistable;
return unhoisted;
}
if (binding !== null && binding.initial !== null && !binding.mutated && !binding.is_called) {
@ -146,27 +147,25 @@ function get_delegated_event(event_name, handler, context) {
}
}
// If we can't find a function, bail-out
if (target_function == null) return non_hoistable;
// If the function is marked as non-hoistable, bail-out
if (target_function.metadata.hoistable === 'impossible') return non_hoistable;
// If the function has more than one arg, then bail-out
if (target_function.params.length > 1) return non_hoistable;
// If we can't find a function, or the function has multiple parameters, bail out
if (target_function == null || target_function.params.length > 1) {
return unhoisted;
}
const visited_references = new Set();
const scope = target_function.metadata.scope;
for (const [reference] of scope.references) {
// Bail-out if the arguments keyword is used
if (reference === 'arguments') return non_hoistable;
// Bail-out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return non_hoistable;
// Bail out if the arguments keyword is used or $host is referenced
if (reference === 'arguments' || reference === '$host') return unhoisted;
// Bail out if references a store subscription
if (scope.get(`$${reference}`)?.kind === 'store_sub') return unhoisted;
const binding = scope.get(reference);
const local_binding = context.state.scope.get(reference);
// If we are referencing a binding that is shadowed in another scope then bail out.
if (local_binding !== null && binding !== null && local_binding.node !== binding.node) {
return non_hoistable;
return unhoisted;
}
// If we have multiple references to the same store using $ prefix, bail out.
@ -175,30 +174,28 @@ function get_delegated_event(event_name, handler, context) {
binding.kind === 'store_sub' &&
visited_references.has(reference.slice(1))
) {
return non_hoistable;
return unhoisted;
}
// If we reference the index within an each block, then bail-out.
if (binding !== null && binding.initial?.type === 'EachBlock') return non_hoistable;
// If we reference the index within an each block, then bail out.
if (binding !== null && binding.initial?.type === 'EachBlock') return unhoisted;
if (
binding !== null &&
// Bail-out if the the binding is a rest param
// Bail out if the the binding is a rest param
(binding.declaration_kind === 'rest_param' ||
// Bail-out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
// Bail out if we reference anything from the EachBlock (for now) that mutates in non-runes mode,
(((!context.state.analysis.runes && binding.kind === 'each') ||
// or any normal not reactive bindings that are mutated.
binding.kind === 'normal' ||
// or any reactive imports (those are rewritten) (can only happen in legacy mode)
binding.kind === 'legacy_reactive_import') &&
binding.kind === 'normal') &&
binding.mutated))
) {
return non_hoistable;
return unhoisted;
}
visited_references.add(reference);
}
return { type: 'hoistable', function: target_function };
return { hoisted: true, function: target_function };
}
/**

@ -1,5 +1,6 @@
/** @import { EachBlock } from '#compiler' */
/** @import { Context } from '../types' */
/** @import { Scope } from '../../scope' */
import * as e from '../../../errors.js';
import { validate_block_not_empty, validate_opening_tag } from './shared/utils.js';
@ -25,5 +26,13 @@ export function EachBlock(node, context) {
node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index;
}
context.next();
// evaluate expression in parent scope
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
});
context.visit(node.body);
if (node.key) context.visit(node.key);
if (node.fallback) context.visit(node.fallback);
}

@ -80,16 +80,8 @@ export function Identifier(node, context) {
binding.declaration_kind !== 'function') ||
binding.declaration_kind === 'import')
) {
if (binding.declaration_kind === 'import') {
if (
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template') &&
parent.type === 'MemberExpression'
) {
binding.kind = 'legacy_reactive_import';
}
} else if (
if (
binding.declaration_kind !== 'import' &&
binding.mutated &&
// TODO could be more fine-grained - not every mention in the template implies a state binding
(context.state.reactive_statement || context.state.ast_type === 'template')

@ -1,8 +1,9 @@
/** @import { Attribute, Component, SvelteComponent, SvelteSelf } from '#compiler' */
/** @import { Comment, Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */
/** @import { Context } from '../../types' */
import * as e from '../../../../errors.js';
import * as w from '../../../../warnings.js';
import { get_attribute_expression, is_expression_attribute } from '../../../../utils/ast.js';
import { determine_slot } from '../../../../utils/slot.js';
import {
validate_attribute,
validate_attribute_name,
@ -65,9 +66,45 @@ export function visit_component(node, context) {
}
}
context.next({
...context.state,
parent_element: null,
component_slots: new Set()
});
// If the component has a slot attribute — `<Foo slot="whatever" .../>` —
// then `let:` directives apply to other attributes, instead of just the
// top-level contents of the component. Yes, this is very weird.
const default_state = determine_slot(node)
? context.state
: { ...context.state, scope: node.metadata.scopes.default };
for (const attribute of node.attributes) {
context.visit(attribute, attribute.type === 'LetDirective' ? default_state : context.state);
}
/** @type {Comment[]} */
let comments = [];
/** @type {Record<string, Fragment['nodes']>} */
const nodes = { default: [] };
for (const child of node.fragment.nodes) {
if (child.type === 'Comment') {
comments.push(child);
continue;
}
const slot_name = determine_slot(child) ?? 'default';
(nodes[slot_name] ??= []).push(...comments, child);
if (slot_name !== 'default') comments = [];
}
const component_slots = new Set();
for (const slot_name in nodes) {
const state = {
...context.state,
scope: node.metadata.scopes[slot_name],
parent_element: null,
component_slots
};
context.visit({ ...node.fragment, nodes: nodes[slot_name] }, state);
}
}

@ -8,9 +8,8 @@
export function visit_function(node, context) {
// TODO retire this in favour of a more general solution based on bindings
node.metadata = {
// module context -> already hoisted
hoistable: context.state.ast_type === 'module' ? 'impossible' : false,
hoistable_params: [],
hoisted: false,
hoisted_params: [],
scope: context.state.scope
};

@ -4,7 +4,6 @@
/** @import { Visitors, ComponentClientTransformState, ClientTransformState } from './types' */
import { walk } from 'zimmerframe';
import * as b from '../../../utils/builders.js';
import { set_scope } from '../../scope.js';
import { build_getter } from './utils.js';
import { render_stylesheet } from '../css/index.js';
import { dev, filename } from '../../../state.js';
@ -15,6 +14,7 @@ import { Attribute } from './visitors/Attribute.js';
import { AwaitBlock } from './visitors/AwaitBlock.js';
import { BinaryExpression } from './visitors/BinaryExpression.js';
import { BindDirective } from './visitors/BindDirective.js';
import { BlockStatement } from './visitors/BlockStatement.js';
import { BreakStatement } from './visitors/BreakStatement.js';
import { CallExpression } from './visitors/CallExpression.js';
import { ClassBody } from './visitors/ClassBody.js';
@ -37,6 +37,7 @@ import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { Program } from './visitors/Program.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
import { SlotElement } from './visitors/SlotElement.js';
@ -58,7 +59,23 @@ import { VariableDeclaration } from './visitors/VariableDeclaration.js';
/** @type {Visitors} */
const visitors = {
_: set_scope,
_: function set_scope(node, { next, state }) {
const scope = state.scopes.get(node);
if (scope && scope !== state.scope) {
const transform = { ...state.transform };
for (const [name, binding] of scope.declarations) {
if (binding.kind === 'normal') {
delete transform[name];
}
}
next({ ...state, transform, scope });
} else {
next();
}
},
AnimateDirective,
ArrowFunctionExpression,
AssignmentExpression,
@ -66,6 +83,7 @@ const visitors = {
AwaitBlock,
BinaryExpression,
BindDirective,
BlockStatement,
BreakStatement,
CallExpression,
ClassBody,
@ -88,6 +106,7 @@ const visitors = {
LetDirective,
MemberExpression,
OnDirective,
Program,
RegularElement,
RenderTag,
SlotElement,
@ -123,6 +142,7 @@ export function client_component(analysis, options) {
is_instance: false,
hoisted: [b.import_all('$', 'svelte/internal/client')],
node: /** @type {any} */ (null), // populated by the root node
legacy_reactive_imports: [],
legacy_reactive_statements: new Map(),
metadata: {
context: {
@ -136,8 +156,7 @@ export function client_component(analysis, options) {
preserve_whitespace: options.preserveWhitespace,
public_state: new Map(),
private_state: new Map(),
getters: {},
setters: {},
transform: {},
in_constructor: false,
// these are set inside the `Fragment` visitor, and cannot be used until then
@ -155,6 +174,7 @@ export function client_component(analysis, options) {
const instance_state = {
...state,
transform: { ...state.transform },
scope: analysis.instance.scope,
scopes: analysis.instance.scopes,
is_instance: true
@ -167,21 +187,17 @@ export function client_component(analysis, options) {
const template = /** @type {ESTree.Program} */ (
walk(
/** @type {SvelteNode} */ (analysis.template.ast),
{ ...state, scope: analysis.instance.scope, scopes: analysis.template.scopes },
{
...state,
transform: instance_state.transform,
scope: analysis.instance.scope,
scopes: analysis.template.scopes
},
visitors
)
);
// Very very dirty way of making import statements reactive in legacy mode if needed
if (!analysis.runes) {
for (const [name, binding] of analysis.module.scope.declarations) {
if (binding.kind === 'legacy_reactive_import') {
instance.body.unshift(
b.var('$$_import_' + name, b.call('$.reactive_import', b.thunk(b.id(name))))
);
}
}
}
instance.body.unshift(...state.legacy_reactive_imports);
/** @type {ESTree.Statement[]} */
const store_setup = [];
@ -380,7 +396,9 @@ export function client_component(analysis, options) {
state.hoisted.push(b.const('$$css', b.object([b.init('hash', hash), b.init('code', code)])));
component_block.body.unshift(
b.stmt(b.call('$.append_styles', b.id('$$anchor'), b.id('$$css')))
b.stmt(
b.call('$.append_styles', b.id('$$anchor'), b.id('$$css'), options.customElement && b.true)
)
);
}
@ -593,9 +611,8 @@ export function client_component(analysis, options) {
/** @type {any} */ (typeof ce !== 'boolean' ? ce.extend : undefined)
);
// If customElement option is set, we define the custom element directly. Else we still create
// the custom element class so that the user may instantiate a custom element themselves later.
if (typeof ce !== 'boolean') {
// If a tag name is provided, call `customElements.define`, otherwise leave to the user
if (typeof ce !== 'boolean' && typeof ce.tag === 'string') {
body.push(b.stmt(b.call('customElements.define', b.literal(ce.tag), create_ce)));
} else {
body.push(b.stmt(create_ce));
@ -621,11 +638,9 @@ export function client_module(analysis, options) {
options,
scope: analysis.module.scope,
scopes: analysis.module.scopes,
legacy_reactive_statements: new Map(),
public_state: new Map(),
private_state: new Map(),
getters: {},
setters: {},
transform: {},
in_constructor: false
};

@ -5,7 +5,8 @@ import type {
Identifier,
PrivateIdentifier,
Expression,
AssignmentExpression
AssignmentExpression,
UpdateExpression
} from 'estree';
import type { Namespace, SvelteNode, ValidatedCompileOptions } from '#compiler';
import type { TransformState } from '../types.js';
@ -22,19 +23,18 @@ export interface ClientTransformState extends TransformState {
*/
readonly in_constructor: boolean;
/** 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)>;
/**
* Counterpart to `getters`
*/
readonly setters: Record<
readonly transform: Record<
string,
(assignment: AssignmentExpression, context: Context) => Expression
{
/** turn `foo` into e.g. `$.get(foo)` */
read: (id: Identifier) => Expression;
/** turn `foo = bar` into e.g. `$.set(foo, bar)` */
assign?: (node: Identifier, value: Expression) => Expression;
/** turn `foo.bar = baz` into e.g. `$.mutate(foo, $.get(foo).bar = baz);` */
mutate?: (node: Identifier, mutation: AssignmentExpression) => Expression;
/** turn `foo++` into e.g. `$.update(foo)` */
update?: (node: UpdateExpression) => Expression;
}
>;
}
@ -79,6 +79,12 @@ export interface ComponentClientTransformState extends ClientTransformState {
/** The anchor node for the current context */
readonly node: Identifier;
/** Imports that should be re-evaluated in legacy mode following a mutation */
readonly legacy_reactive_imports: Statement[];
/** The $: calls, which will be ordered in the end */
readonly legacy_reactive_statements: Map<LabeledStatement, Statement>;
}
export interface StateField {

@ -1,59 +1,17 @@
/** @import { ArrowFunctionExpression, AssignmentExpression, BinaryOperator, Expression, FunctionDeclaration, FunctionExpression, Identifier, MemberExpression, Node, Pattern, PrivateIdentifier, Statement } from 'estree' */
/** @import { ArrowFunctionExpression, Expression, FunctionDeclaration, FunctionExpression, Identifier, 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,
extract_paths,
is_expression_async,
is_simple_expression,
object
} from '../../../utils/ast.js';
import { extract_identifiers, is_simple_expression } from '../../../utils/ast.js';
import {
PROPS_IS_LAZY_INITIAL,
PROPS_IS_IMMUTABLE,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
} from '../../../../constants.js';
import { is_ignored, dev } from '../../../state.js';
/**
* @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 {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
build_getter(node.left, state),
/** @type {Expression} */ (visit(node.right))
);
} else if (
node.left.type === 'MemberExpression' &&
node.left.object.type === 'ThisExpression' &&
node.left.property.type === 'PrivateIdentifier' &&
state.private_state.has(node.left.property.name)
) {
const operator = node.operator;
return operator === '='
? /** @type {Expression} */ (visit(node.right))
: // turn something like x += 1 into x = x + 1
b.binary(
/** @type {BinaryOperator} */ (operator.slice(0, -1)),
/** @type {Expression} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
);
} else {
return /** @type {Expression} */ (visit(node.right));
}
}
import { dev } from '../../../state.js';
import { get_value } from './visitors/shared/declarations.js';
/**
* @param {Binding} binding
@ -73,392 +31,18 @@ export function is_state_source(binding, state) {
* @returns {Expression}
*/
export function build_getter(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 (Object.hasOwn(state.transform, node.name)) {
const binding = state.scope.get(node.name);
if (Object.hasOwn(state.getters, node.name)) {
const getter = state.getters[node.name];
return typeof getter === 'function' ? getter(node) : getter;
}
if (binding.node.name === '$$props') {
// Special case for $$props which only exists in the old world
return b.id('$$sanitized_props');
}
if (binding.kind === 'store_sub') {
return b.call(node);
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (is_prop_source(binding, state)) {
return b.call(node);
// don't transform the declaration itself
if (node !== binding?.node) {
return state.transform[node.name].read(node);
}
if (binding.prop_alias) {
const key = b.key(binding.prop_alias);
return b.member(b.id('$$props'), key, key.type === 'Literal');
}
return b.member(b.id('$$props'), node);
}
if (binding.kind === 'legacy_reactive_import') {
return b.call('$$_import_' + node.name);
}
if (
is_state_source(binding, state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
return b.call('$.get', node);
}
return node;
}
/**
* @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 {Expression}
*/
export function build_setter(node, context, fallback, prefix, options) {
const { state, visit } = context;
const assignee = node.left;
if (
assignee.type === 'ArrayPattern' ||
assignee.type === 'ObjectPattern' ||
assignee.type === 'RestElement'
) {
// Turn assignment into an IIFE, so that `$.set` calls etc don't produce invalid code
const tmp_id = context.state.scope.generate('tmp');
/** @type {AssignmentExpression[]} */
const original_assignments = [];
/** @type {Expression[]} */
const assignments = [];
const paths = extract_paths(assignee);
for (const path of paths) {
const value = path.expression?.(b.id(tmp_id));
const assignment = b.assignment('=', path.node, value);
original_assignments.push(assignment);
assignments.push(build_setter(assignment, context, () => assignment, prefix, options));
}
if (assignments.every((assignment, i) => assignment === original_assignments[i])) {
// No change to output -> nothing to transform -> we can keep the original assignment
return fallback();
}
const rhs_expression = /** @type {Expression} */ (visit(node.right));
const iife_is_async =
is_expression_async(rhs_expression) ||
assignments.some((assignment) => is_expression_async(assignment));
const iife = b.arrow(
[],
b.block([
b.const(tmp_id, rhs_expression),
b.stmt(b.sequence(assignments)),
// return because it could be used in a nested expression where the value is needed.
// example: { foo: ({ bar } = { bar: 1 })}
b.return(b.id(tmp_id))
])
);
if (iife_is_async) {
return b.await(b.call(b.async(iife)));
} else {
return b.call(iife);
}
}
if (assignee.type !== 'Identifier' && assignee.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${assignee.type}`);
}
// Handle class private/public state assignment cases
if (assignee.type === 'MemberExpression') {
if (
assignee.object.type === 'ThisExpression' &&
assignee.property.type === 'PrivateIdentifier'
) {
const private_state = context.state.private_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
if (private_state !== undefined) {
if (state.in_constructor) {
// See if we should wrap value in $.proxy
if (
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id);
return assignment;
}
}
} else {
return b.call(
'$.set',
assignee,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id)
: value
);
}
}
} else if (
assignee.object.type === 'ThisExpression' &&
assignee.property.type === 'Identifier' &&
state.in_constructor
) {
const public_state = context.state.public_state.get(assignee.property.name);
const value = get_assignment_value(node, context);
// See if we should wrap value in $.proxy
if (
context.state.analysis.runes &&
public_state !== undefined &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
) {
const assignment = fallback();
if (assignment.type === 'AssignmentExpression') {
assignment.right =
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, public_state.id);
return assignment;
}
}
}
}
const left = object(assignee);
if (left === null) {
return fallback();
}
const binding = state.scope.get(left.name);
if (!binding) return fallback();
if (Object.hasOwn(state.setters, left.name)) {
const setter = state.setters[left.name];
// @ts-expect-error
return setter(node, context);
}
if (binding.kind === 'legacy_reactive_import') {
return b.call(
'$$_import_' + binding.node.name,
b.assignment(
node.operator,
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
/**
* @param {any} serialized
* @returns
*/
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
}
if (binding.kind === 'derived') {
return maybe_skip_ownership_validation(fallback());
}
const is_store = binding.kind === 'store_sub';
const left_name = is_store ? left.name.slice(1) : left.name;
if (
binding.kind !== 'state' &&
binding.kind !== 'frozen_state' &&
binding.kind !== 'prop' &&
binding.kind !== 'bindable_prop' &&
binding.kind !== 'each' &&
binding.kind !== 'legacy_reactive' &&
!is_store
) {
// TODO error if it's a computed (or rest prop)? or does that already happen elsewhere?
return fallback();
}
const value = get_assignment_value(node, context);
const serialize = () => {
if (left === node.left) {
const is_initial_proxy =
binding.initial !== null &&
should_proxy_or_freeze(/**@type {Expression}*/ (binding.initial), context.state.scope);
if ((binding.kind === 'prop' || binding.kind === 'bindable_prop') && !is_initial_proxy) {
return b.call(left, value);
} else if (is_store) {
return b.call('$.store_set', build_getter(b.id(left_name), state), value);
} else {
let call;
if (binding.kind === 'state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? build_proxy_reassignment(value, left_name)
: value
);
} else if (binding.kind === 'frozen_state') {
call = b.call(
'$.set',
b.id(left_name),
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope)
? b.call('$.freeze', value)
: value
);
} else if (
(binding.kind === 'prop' || binding.kind === 'bindable_prop') &&
is_initial_proxy
) {
call = b.call(
left,
context.state.analysis.runes &&
!options?.skip_proxy_and_freeze &&
should_proxy_or_freeze(value, context.state.scope) &&
binding.kind === 'bindable_prop'
? build_proxy_reassignment(value, left_name)
: value
);
} else {
call = b.call('$.set', b.id(left_name), value);
}
if (state.scope.get(`$${left.name}`)?.kind === 'store_sub') {
return b.call('$.store_unsub', call, b.literal(`$${left.name}`), b.id('$$stores'));
} else {
return call;
}
}
} else {
if (is_store) {
// If we are assigning to a store property, we need to ensure we don't
// capture the read for the store as part of the member expression to
// keep consistency with how store $ shorthand reads work in Svelte 4.
/**
*
* @param {Expression | Pattern} node
* @returns {Expression}
*/
function visit_node(node) {
if (node.type === 'MemberExpression') {
return {
...node,
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 {Expression} */ (visit(node))));
}
}
return /** @type {Expression} */ (visit(node));
}
return maybe_skip_ownership_validation(
b.call(
'$.store_mutate',
build_getter(b.id(left_name), state),
b.assignment(node.operator, /** @type {Pattern}} */ (visit_node(node.left)), value),
b.call('$.untrack', b.id('$' + left_name))
)
);
} else if (
!state.analysis.runes ||
// this condition can go away once legacy mode is gone; only necessary for interop with legacy parent bindings
(binding.mutated && binding.kind === 'bindable_prop')
) {
if (binding.kind === 'bindable_prop') {
return maybe_skip_ownership_validation(
b.call(
left,
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value),
b.true
)
);
} else {
return maybe_skip_ownership_validation(
b.call(
'$.mutate',
b.id(left_name),
b.assignment(node.operator, /** @type {Pattern} */ (visit(node.left)), value)
)
);
}
} else if (
node.right.type === 'Literal' &&
prefix != null &&
(node.operator === '+=' || node.operator === '-=')
) {
return maybe_skip_ownership_validation(
b.update(
node.operator === '+=' ? '++' : '--',
/** @type {Expression} */ (visit(node.left)),
prefix
)
);
} else {
return maybe_skip_ownership_validation(
b.assignment(
node.operator,
/** @type {Pattern} */ (visit(node.left)),
/** @type {Expression} */ (visit(node.right))
)
);
}
}
};
if (value.type === 'BinaryExpression' && /** @type {any} */ (value.operator) === '??') {
return b.logical('??', build_getter(b.id(left_name), state), serialize());
}
return serialize();
}
/**
* @param {Expression} value
* @param {PrivateIdentifier | string} proxy_reference
@ -481,7 +65,7 @@ export function build_proxy_reassignment(value, proxy_reference) {
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
function get_hoistable_params(node, context) {
function get_hoisted_params(node, context) {
const scope = context.state.scope;
/** @type {Identifier[]} */
@ -507,7 +91,7 @@ function get_hoistable_params(node, context) {
binding = /** @type {Binding} */ (scope.get(binding.node.name.slice(1)));
}
const expression = context.state.getters[reference];
let expression = context.state.transform[reference]?.read(b.id(binding.node.name));
if (
// If it's a destructured derived binding, then we can extract the derived signal reference and use that.
@ -549,15 +133,15 @@ function get_hoistable_params(node, context) {
* @param {ComponentContext} context
* @returns {Pattern[]}
*/
export function build_hoistable_params(node, context) {
const hoistable_params = get_hoistable_params(node, context);
node.metadata.hoistable_params = hoistable_params;
export function build_hoisted_params(node, context) {
const hoisted_params = get_hoisted_params(node, context);
node.metadata.hoisted_params = hoisted_params;
/** @type {Pattern[]} */
const params = [];
if (node.params.length === 0) {
if (hoistable_params.length > 0) {
if (hoisted_params.length > 0) {
// For the event object
params.push(b.id('_'));
}
@ -567,7 +151,7 @@ export function build_hoistable_params(node, context) {
}
}
params.push(...hoistable_params);
params.push(...hoisted_params);
return params;
}
@ -706,6 +290,7 @@ export function with_loc(target, source) {
*/
export function create_derived_block_argument(node, context) {
if (node.type === 'Identifier') {
context.state.transform[node.name] = { read: get_value };
return { id: node, declarations: null };
}
@ -723,6 +308,8 @@ export function create_derived_block_argument(node, context) {
const declarations = [b.var(value, create_derived(context.state, b.thunk(block)))];
for (const id of identifiers) {
context.state.transform[id.name] = { read: get_value };
declarations.push(
b.var(id, create_derived(context.state, b.thunk(b.member(b.call('$.get', value), id))))
);

@ -1,11 +1,129 @@
/** @import { AssignmentExpression } from 'estree' */
/** @import { Context } from '../types' */
import { build_setter } from '../utils.js';
/** @import { AssignmentExpression, AssignmentOperator, Expression, Pattern } from 'estree' */
/** @import { Context } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { is_ignored } from '../../../../state.js';
import { build_proxy_reassignment, should_proxy_or_freeze } from '../utils.js';
import { visit_assignment_expression } from '../../shared/assignments.js';
/**
* @param {AssignmentExpression} node
* @param {Context} context
*/
export function AssignmentExpression(node, context) {
return build_setter(node, context, context.next);
return visit_assignment_expression(node, context, build_assignment);
}
/**
* @param {AssignmentOperator} operator
* @param {Pattern} left
* @param {Expression} right
* @param {Context} context
* @returns {Expression | null}
*/
export function build_assignment(operator, left, right, context) {
// Handle class private/public state assignment cases
if (
context.state.analysis.runes &&
left.type === 'MemberExpression' &&
left.object.type === 'ThisExpression'
) {
if (left.property.type === 'PrivateIdentifier') {
const private_state = context.state.private_state.get(left.property.name);
if (private_state !== undefined) {
let transformed = false;
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
if (should_proxy_or_freeze(value, context.state.scope)) {
transformed = true;
value =
private_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, private_state.id);
}
if (!context.state.in_constructor) {
return b.call('$.set', left, value);
} else if (transformed) {
return b.assignment(operator, /** @type {Pattern} */ (context.visit(left)), value);
}
}
} else if (left.property.type === 'Identifier' && context.state.in_constructor) {
const public_state = context.state.public_state.get(left.property.name);
if (public_state !== undefined && should_proxy_or_freeze(right, context.state.scope)) {
const value = /** @type {Expression} */ (context.visit(right));
return b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
public_state.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, public_state.id)
);
}
}
}
let object = left;
while (object.type === 'MemberExpression') {
// @ts-expect-error
object = object.object;
}
if (object.type !== 'Identifier') {
return null;
}
const binding = context.state.scope.get(object.name);
if (!binding) return null;
const transform = Object.hasOwn(context.state.transform, object.name)
? context.state.transform[object.name]
: null;
// reassignment
if (object === left && transform?.assign) {
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
// special case — if an element binding, we know it's a primitive
const path = context.path.map((node) => node.type);
const is_primitive = path.at(-1) === 'BindDirective' && path.at(-2) === 'RegularElement';
if (
!is_primitive &&
binding.kind !== 'prop' &&
context.state.analysis.runes &&
should_proxy_or_freeze(value, context.state.scope)
) {
value =
binding.kind === 'frozen_state'
? b.call('$.freeze', value)
: build_proxy_reassignment(value, object.name);
}
return transform.assign(object, value);
}
/** @type {Expression} */
let mutation = b.assignment(
operator,
/** @type {Pattern} */ (context.visit(left)),
/** @type {Expression} */ (context.visit(right))
);
// mutation
if (transform?.mutate) {
mutation = transform.mutate(object, mutation);
}
return is_ignored(left, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(mutation))
: mutation;
}

@ -1,7 +1,7 @@
/** @import { Attribute } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { is_event_attribute } from '../../../../utils/ast.js';
import { build_event_attribute } from './shared/element.js';
import { visit_event_attribute } from './shared/events.js';
/**
* @param {Attribute} node
@ -9,6 +9,6 @@ import { build_event_attribute } from './shared/element.js';
*/
export function Attribute(node, context) {
if (is_event_attribute(node)) {
build_event_attribute(node, context);
visit_event_attribute(node, context);
}
}

@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression, Pattern } from 'estree' */
/** @import { BlockStatement, Expression, Pattern, Statement } from 'estree' */
/** @import { AwaitBlock } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
@ -15,39 +15,29 @@ export function AwaitBlock(node, context) {
let catch_block;
if (node.then) {
const argument = node.value && create_derived_block_argument(node.value, context);
/** @type {Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {BlockStatement} */ (context.visit(node.then));
if (node.value) {
const argument = create_derived_block_argument(node.value, context);
if (argument) args.push(argument.id);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
const declarations = argument?.declarations ?? [];
const block = /** @type {BlockStatement} */ (context.visit(node.then));
then_block = b.arrow(args, block);
then_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
if (node.catch) {
const argument = node.error && create_derived_block_argument(node.error, context);
/** @type {Pattern[]} */
const args = [b.id('$$anchor')];
const block = /** @type {BlockStatement} */ (context.visit(node.catch));
if (node.error) {
const argument = create_derived_block_argument(node.error, context);
if (argument) args.push(argument.id);
args.push(argument.id);
if (argument.declarations !== null) {
block.body.unshift(...argument.declarations);
}
}
const declarations = argument?.declarations ?? [];
const block = /** @type {BlockStatement} */ (context.visit(node.catch));
catch_block = b.arrow(args, block);
catch_block = b.arrow(args, b.block([...declarations, ...block.body]));
}
context.state.init.push(

@ -5,9 +5,8 @@ import { dev, is_ignored } from '../../../../state.js';
import { is_text_attribute } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { binding_properties } from '../../../bindings.js';
import { build_setter } from '../utils.js';
import { build_attribute_value } from './shared/element.js';
import { build_bind_this, build_validate_binding } from './shared/utils.js';
import { build_bind_this, validate_binding } from './shared/utils.js';
/**
* @param {BindDirective} node
@ -30,28 +29,18 @@ export function BindDirective(node, context) {
)) &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(
build_validate_binding(
context.state,
node,
/**@type {MemberExpression} */ (context.visit(expression))
)
validate_binding(
context.state,
node,
/**@type {MemberExpression} */ (context.visit(expression))
);
}
const getter = b.thunk(/** @type {Expression} */ (context.visit(expression)));
const assignment = b.assignment('=', expression, b.id('$$value'));
const setter = b.arrow(
[b.id('$$value')],
build_setter(
assignment,
context,
() => /** @type {Expression} */ (context.visit(assignment)),
null,
{
skip_proxy_and_freeze: true
}
)
/** @type {Expression} */ (context.visit(b.assignment('=', expression, b.id('$$value'))))
);
/** @type {CallExpression} */

@ -0,0 +1,12 @@
/** @import { BlockStatement } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {BlockStatement} node
* @param {ComponentContext} context
*/
export function BlockStatement(node, context) {
add_state_transformers(context);
context.next();
}

@ -5,6 +5,7 @@ import { dev } from '../../../../state.js';
import { extract_identifiers } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { create_derived } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {ConstTag} node
@ -24,7 +25,7 @@ export function ConstTag(node, context) {
)
);
context.state.getters[declaration.id.name] = b.call('$.get', declaration.id);
context.state.transform[declaration.id.name] = { read: get_value };
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
@ -35,15 +36,15 @@ export function ConstTag(node, context) {
const identifiers = extract_identifiers(declaration.id);
const tmp = b.id(context.state.scope.generate('computed_const'));
const getters = { ...context.state.getters };
const transform = { ...context.state.transform };
// 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) {
getters[node.name] = node;
delete transform[node.name];
}
const child_state = { ...context.state, getters };
const child_state = { ...context.state, transform };
// TODO optimise the simple `{ x } = y` case — we can just return `y`
// instead of destructuring it only to return a new object
@ -67,7 +68,9 @@ export function ConstTag(node, context) {
}
for (const node of identifiers) {
context.state.getters[node.name] = b.member(b.call('$.get', tmp), node);
context.state.transform[node.name] = {
read: (node) => b.member(b.call('$.get', tmp), node)
};
}
}
}

@ -1,6 +1,7 @@
/** @import { AssignmentExpression, BlockStatement, Expression, Identifier, MemberExpression, Pattern, Statement } from 'estree' */
/** @import { BlockStatement, Expression, Identifier, Pattern, Statement } from 'estree' */
/** @import { Binding, EachBlock } from '#compiler' */
/** @import { ComponentContext, Context } from '../types' */
/** @import { ComponentContext } from '../types' */
/** @import { Scope } from '../../../scope' */
import {
EACH_INDEX_REACTIVE,
EACH_IS_ANIMATED,
@ -12,7 +13,8 @@ import {
import { dev } from '../../../../state.js';
import { extract_paths, object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_assignment_value, build_getter, build_setter, with_loc } from '../utils.js';
import { build_getter, with_loc } from '../utils.js';
import { get_value } from './shared/declarations.js';
/**
* @param {EachBlock} node
@ -20,7 +22,15 @@ import { get_assignment_value, build_getter, build_setter, with_loc } from '../u
*/
export function EachBlock(node, context) {
const each_node_meta = node.metadata;
const collection = /** @type {Expression} */ (context.visit(node.expression));
// expression should be evaluated in the parent scope, not the scope
// created by the each block itself
const collection = /** @type {Expression} */ (
context.visit(node.expression, {
...context.state,
scope: /** @type {Scope} */ (context.state.scope.parent)
})
);
if (!each_node_meta.is_controlled) {
context.state.template.push('<!>');
@ -110,54 +120,13 @@ export function EachBlock(node, context) {
const child_state = {
...context.state,
getters: { ...context.state.getters },
setters: { ...context.state.setters }
transform: { ...context.state.transform }
};
/** The state used when generating the key function, if necessary */
const key_state = {
...context.state,
getters: { ...context.state.getters }
};
/**
* @param {Pattern} expression_for_id
* @returns {(assignment: AssignmentExpression, context: Context) => Expression}
*/
const create_mutation = (expression_for_id) => {
return (assignment, context) => {
if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') {
// build_setter turns other patterns into IIFEs and separates the assignments
// into separate expressions, at which point this is called again with an identifier or member expression
return build_setter(assignment, context, () => assignment);
}
const left = object(assignment.left);
const value = get_assignment_value(assignment, context);
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (left === assignment.left) {
const assign = b.assignment('=', expression_for_id, value);
sequence.unshift(assign);
return b.sequence(sequence);
} else {
const original_left = /** @type {MemberExpression} */ (assignment.left);
const left = /** @type {Pattern} */ (context.visit(original_left));
const assign = b.assignment(assignment.operator, left, value);
sequence.unshift(assign);
return b.sequence(sequence);
}
};
transform: { ...context.state.transform }
};
// We need to generate a unique identifier in case there's a bind:group below
@ -170,37 +139,59 @@ export function EachBlock(node, context) {
const item_with_loc = with_loc(item, id);
return b.call('$.unwrap', item_with_loc);
};
child_state.getters[item.name] = getter;
if (node.index) {
child_state.getters[node.index] = (id) => {
const index_with_loc = with_loc(index, id);
return (flags & EACH_INDEX_REACTIVE) === 0 ? index_with_loc : b.call('$.get', index_with_loc);
child_state.transform[node.index] = {
read: (id) => {
const index_with_loc = with_loc(index, id);
return (flags & EACH_INDEX_REACTIVE) === 0
? index_with_loc
: b.call('$.get', index_with_loc);
}
};
key_state.getters[node.index] = b.id(node.index);
delete key_state.transform[node.index];
}
/** @type {Statement[]} */
const declarations = [];
const invalidate = b.call(
'$.invalidate_inner_signals',
b.thunk(b.sequence(indirect_dependencies))
);
const invalidate_store = store_to_invalidate
? b.call('$.invalidate_store', b.id('$$stores'), b.literal(store_to_invalidate))
: undefined;
/** @type {Expression[]} */
const sequence = [];
if (!context.state.analysis.runes) sequence.push(invalidate);
if (invalidate_store) sequence.push(invalidate_store);
if (node.context.type === 'Identifier') {
child_state.setters[node.context.name] = create_mutation(
b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
true
)
);
child_state.transform[node.context.name] = {
read: getter,
assign: (_, value) => {
const left = b.member(
each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection,
index,
true
);
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (_, mutation) => b.sequence([mutation, ...sequence])
};
key_state.getters[node.context.name] = node.context;
delete key_state.transform[node.context.name];
} else {
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 {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), child_state))
@ -208,19 +199,26 @@ export function EachBlock(node, context) {
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
const getter = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
child_state.getters[name] = getter;
child_state.setters[name] = create_mutation(
/** @type {Pattern} */ (path.update_expression(unwrapped))
);
const read = needs_derived ? get_value : b.call;
child_state.transform[name] = {
read,
assign: (node, value) => {
const left = /** @type {Pattern} */ (path.update_expression(unwrapped));
return b.sequence([b.assignment('=', left, value), ...sequence]);
},
mutate: (node, mutation) => {
return b.sequence([mutation, ...sequence]);
}
};
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
declarations.push(b.stmt(getter));
declarations.push(b.stmt(read(b.id(name))));
}
key_state.getters[name] = path.node;
delete key_state.transform[name];
}
}

@ -66,7 +66,7 @@ export function Fragment(node, context) {
after_update: [],
template: [],
locations: [],
getters: { ...context.state.getters },
transform: { ...context.state.transform },
metadata: {
context: {
template_needs_import_node: false,

@ -1,6 +1,6 @@
/** @import { FunctionDeclaration } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_hoistable_params } from '../utils.js';
import { build_hoisted_params } from '../utils.js';
import * as b from '../../../../utils/builders.js';
/**
@ -8,21 +8,13 @@ import * as b from '../../../../utils/builders.js';
* @param {ComponentContext} context
*/
export function FunctionDeclaration(node, context) {
const metadata = node.metadata;
const state = { ...context.state, in_constructor: false };
if (metadata?.hoistable === true) {
const params = build_hoistable_params(node, context);
if (node.metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
const body = context.visit(node.body, state);
context.state.hoisted.push(
/** @type {FunctionDeclaration} */ ({
...node,
id: node.id !== null ? context.visit(node.id, state) : null,
params,
body: context.visit(node.body, state)
})
);
context.state.hoisted.push(/** @type {FunctionDeclaration} */ ({ ...node, params, body }));
return b.empty;
}

@ -34,7 +34,7 @@ export function LabeledStatement(node, context) {
const sequence = [];
for (const binding of reactive_statement.dependencies) {
if (binding.kind === 'normal') continue;
if (binding.kind === 'normal' && binding.declaration_kind !== 'import') continue;
const name = binding.node.name;
let serialized = build_getter(b.id(name), context.state);

@ -16,10 +16,9 @@ export function LetDirective(node, context) {
const bindings = context.state.scope.get_bindings(node);
for (const binding of bindings) {
context.state.getters[binding.node.name] = b.member(
b.call('$.get', b.id(name)),
b.id(binding.node.name)
);
context.state.transform[binding.node.name] = {
read: (node) => b.member(b.call('$.get', b.id(name)), node)
};
}
return b.const(
@ -42,8 +41,13 @@ export function LetDirective(node, context) {
)
);
} else {
const name = node.expression === null ? node.name : node.expression.name;
context.state.transform[name] = {
read: (node) => b.call('$.get', node)
};
return b.const(
node.expression === null ? node.name : node.expression.name,
name,
create_derived(context.state, b.thunk(b.member(b.id('$$slotProps'), b.id(node.name))))
);
}

@ -1,11 +1,38 @@
/** @import { OnDirective } from '#compiler' */
/** @import { OnDirective, SvelteNode } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import { build_event } from './shared/element.js';
import * as b from '../../../../utils/builders.js';
import { build_event, build_event_handler } from './shared/events.js';
const modifiers = [
'stopPropagation',
'stopImmediatePropagation',
'preventDefault',
'self',
'trusted',
'once'
];
/**
* @param {OnDirective} node
* @param {ComponentContext} context
*/
export function OnDirective(node, context) {
build_event(node, node.metadata.expression, context);
if (!node.expression) {
context.state.analysis.needs_props = true;
}
let handler = build_event_handler(node.expression, node.metadata.expression, context);
for (const modifier of modifiers) {
if (node.modifiers.includes(modifier)) {
handler = b.call('$.' + modifier, handler);
}
}
const capture = node.modifiers.includes('capture');
const passive =
node.modifiers.includes('passive') ||
(node.modifiers.includes('nonpassive') ? false : undefined);
return build_event(node.name, context.state.node, handler, capture, passive);
}

@ -0,0 +1,123 @@
/** @import { Expression, MemberExpression, Program } from 'estree' */
/** @import { ComponentContext } from '../types' */
import { build_getter, is_prop_source } from '../utils.js';
import * as b from '../../../../utils/builders.js';
import { add_state_transformers } from './shared/declarations.js';
/**
* @param {Program} _
* @param {ComponentContext} context
*/
export function Program(_, context) {
if (!context.state.analysis.runes) {
context.state.transform['$$props'] = {
read: (node) => ({ ...node, name: '$$sanitized_props' })
};
for (const [name, binding] of context.state.scope.declarations) {
if (binding.declaration_kind === 'import' && binding.mutated) {
const id = b.id('$$_import_' + name);
context.state.transform[name] = {
read: (_) => b.call(id),
mutate: (_, mutation) => b.call(id, mutation)
};
context.state.legacy_reactive_imports.push(
b.var(id, b.call('$.reactive_import', b.thunk(b.id(name))))
);
}
}
}
for (const [name, binding] of context.state.scope.declarations) {
if (binding.kind === 'store_sub') {
const store = /** @type {Expression} */ (context.visit(b.id(name.slice(1))));
context.state.transform[name] = {
read: b.call,
assign: (_, value) => b.call('$.store_set', store, value),
mutate: (node, mutation) => {
// We need to untrack the store read, for consistency with Svelte 4
const untracked = b.call('$.untrack', node);
/**
*
* @param {Expression} n
* @returns {Expression}
*/
function replace(n) {
if (n.type === 'MemberExpression') {
return {
...n,
object: replace(/** @type {Expression} */ (n.object)),
property: n.property
};
}
return untracked;
}
return b.call(
'$.store_mutate',
store,
b.assignment(
mutation.operator,
/** @type {MemberExpression} */ (
replace(/** @type {MemberExpression} */ (mutation.left))
),
mutation.right
),
untracked
);
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre_store' : '$.update_store',
build_getter(b.id(name.slice(1)), context.state),
b.call(node.argument),
node.operator === '--' && b.literal(-1)
);
}
};
}
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
if (is_prop_source(binding, context.state)) {
context.state.transform[name] = {
read: b.call,
assign: (node, value) => b.call(node, value),
mutate: (node, value) => {
if (binding.kind === 'bindable_prop') {
// only necessary for interop with legacy parent bindings
return b.call(node, value, b.true);
}
return value;
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre_prop' : '$.update_prop',
node.argument,
node.operator === '--' && b.literal(-1)
);
}
};
} else if (binding.prop_alias) {
const key = b.key(binding.prop_alias);
context.state.transform[name] = {
read: (_) => b.member(b.id('$$props'), key, key.type === 'Literal')
};
} else {
context.state.transform[name] = {
read: (node) => b.member(b.id('$$props'), node)
};
}
}
}
add_state_transformers(context);
context.next();
}

@ -24,11 +24,11 @@ import {
get_attribute_name,
build_attribute_value,
build_class_directives,
build_event_attribute,
build_style_directives
} from './shared/element.js';
import { process_children } from './shared/fragment.js';
import { build_render_statement, build_update, build_update_assignment } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js';
/**
* @param {RegularElement} node
@ -100,6 +100,13 @@ export function RegularElement(node, context) {
metadata.context.template_needs_import_node = true;
}
// visit let directives first, to set state
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'Attribute') {
attributes.push(attribute);
@ -136,9 +143,14 @@ export function RegularElement(node, context) {
class_directives.push(attribute);
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else {
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute));
const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective');
context.state.after_update.push(
b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler)
);
} else if (attribute.type !== 'LetDirective') {
if (attribute.type === 'BindDirective') {
if (attribute.name === 'group' || attribute.name === 'checked') {
needs_special_value_handling = true;
@ -214,7 +226,7 @@ export function RegularElement(node, context) {
) {
might_need_event_replaying = true;
}
build_event_attribute(attribute, context);
visit_event_attribute(attribute, context);
continue;
}
@ -320,9 +332,8 @@ export function RegularElement(node, context) {
// set the value of `hydrate_node` to `node.content`
if (node.name === 'template') {
needs_reset = true;
child_state.init.push(b.stmt(b.call('$.hydrate_template', arg)));
arg = b.member(arg, b.id('content'));
child_state.init.push(b.stmt(b.call('$.reset', arg)));
}
process_children(trimmed, () => b.call('$.child', arg), true, {

@ -4,6 +4,7 @@
import { dev } from '../../../../state.js';
import { extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_value } from './shared/declarations.js';
/**
* @param {SnippetBlock} node
@ -20,8 +21,8 @@ export function SnippetBlock(node, context) {
/** @type {Statement[]} */
const declarations = [];
const getters = { ...context.state.getters };
const child_state = { ...context.state, getters };
const transform = { ...context.state.transform };
const child_state = { ...context.state, transform };
for (let i = 0; i < node.parameters.length; i++) {
const argument = node.parameters[i];
@ -35,7 +36,8 @@ export function SnippetBlock(node, context) {
right: b.id('$.noop')
});
getters[argument.name] = b.call(argument);
transform[argument.name] = { read: b.call };
continue;
}
@ -53,12 +55,14 @@ export function SnippetBlock(node, context) {
declarations.push(b.let(path.node, needs_derived ? b.call('$.derived_safe_equal', fn) : fn));
getters[name] = needs_derived ? b.call('$.get', b.id(name)) : b.call(name);
transform[name] = {
read: needs_derived ? get_value : b.call
};
// we need to eagerly evaluate the expression in order to hit any
// 'Cannot access x before initialization' errors
if (dev) {
declarations.push(b.stmt(getters[name]));
declarations.push(b.stmt(transform[name].read(b.id(name))));
}
}
}

@ -1,14 +1,11 @@
/** @import { SvelteBody } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { visit_special_element } from './shared/special_element.js';
/**
* @param {SvelteBody} node
* @param {ComponentContext} context
*/
export function SvelteBody(node, context) {
context.next({
...context.state,
node: b.id('$.document.body')
});
visit_special_element(node, '$.document.body', context);
}

@ -1,14 +1,11 @@
/** @import { SvelteDocument } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { visit_special_element } from './shared/special_element.js';
/**
* @param {SvelteDocument} node
* @param {ComponentContext} context
*/
export function SvelteDocument(node, context) {
context.next({
...context.state,
node: b.id('$.document')
});
visit_special_element(node, '$.document', context);
}

@ -71,6 +71,9 @@ export function SvelteElement(node, context) {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state));
inner_context.state.after_update.push(b.stmt(handler));
} else {
context.visit(attribute, inner_context.state);
}

@ -1,14 +1,12 @@
/** @import { Expression } from 'estree' */
/** @import { SvelteWindow } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { visit_special_element } from './shared/special_element.js';
/**
* @param {SvelteWindow} node
* @param {ComponentContext} context
*/
export function SvelteWindow(node, context) {
context.next({
...context.state,
node: b.id('$.window')
});
visit_special_element(node, '$.window', context);
}

@ -1,8 +1,8 @@
/** @import { Expression, Pattern, Statement, UpdateExpression } from 'estree' */
/** @import { Expression, Node, Pattern, Statement, UpdateExpression } from 'estree' */
/** @import { Context } from '../types' */
import { is_ignored } from '../../../../state.js';
import { object } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { build_getter, build_setter } from '../utils.js';
/**
* @param {UpdateExpression} node
@ -11,45 +11,6 @@ import { build_getter, build_setter } from '../utils.js';
export function UpdateExpression(node, context) {
const argument = node.argument;
if (argument.type === 'Identifier') {
const binding = context.state.scope.get(argument.name);
const is_store = binding?.kind === 'store_sub';
const name = is_store ? argument.name.slice(1) : argument.name;
// use runtime functions for smaller output
if (
binding?.kind === 'state' ||
binding?.kind === 'frozen_state' ||
binding?.kind === 'each' ||
binding?.kind === 'legacy_reactive' ||
binding?.kind === 'prop' ||
binding?.kind === 'bindable_prop' ||
is_store
) {
/** @type {Expression[]} */
const args = [];
let fn = '$.update';
if (node.prefix) fn += '_pre';
if (is_store) {
fn += '_store';
args.push(build_getter(b.id(name), context.state), b.call('$' + name));
} else {
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop';
args.push(b.id(name));
}
if (node.operator === '--') {
args.push(b.literal(-1));
}
return b.call(fn, ...args);
}
return context.next();
}
if (
argument.type === 'MemberExpression' &&
argument.object.type === 'ThisExpression' &&
@ -68,43 +29,41 @@ export function UpdateExpression(node, context) {
return b.call(fn, ...args);
}
/** @param {any} serialized */
function maybe_skip_ownership_validation(serialized) {
if (is_ignored(node, 'ownership_invalid_mutation')) {
return b.call('$.skip_ownership_validation', b.thunk(serialized));
}
return serialized;
if (argument.type !== 'Identifier' && argument.type !== 'MemberExpression') {
throw new Error('An impossible state was reached');
}
// turn it into an IIFE assignment expression: i++ -> (() => { const $$value = i; i+=1; return $$value; })
const assignment = b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
);
const left = object(argument);
if (left === null) return context.next();
const serialized_assignment = build_setter(assignment, context, () => assignment, node.prefix);
if (left === argument) {
const transform = context.state.transform;
const update = transform[left.name]?.update;
const value = /** @type {Expression} */ (context.visit(argument));
if (serialized_assignment === assignment) {
// No change to output -> nothing to transform -> we can keep the original update expression
return maybe_skip_ownership_validation(context.next());
if (update && Object.hasOwn(transform, left.name)) {
return update(node);
}
}
if (context.state.analysis.runes) {
return maybe_skip_ownership_validation(serialized_assignment);
}
const assignment = /** @type {Expression} */ (
context.visit(
b.assignment(
node.operator === '++' ? '+=' : '-=',
/** @type {Pattern} */ (argument),
b.literal(1)
)
)
);
/** @type {Statement[]} */
let statements;
if (node.prefix) {
statements = [b.stmt(serialized_assignment), b.return(value)];
} else {
const tmp_id = context.state.scope.generate('$$value');
statements = [b.const(tmp_id, value), b.stmt(serialized_assignment), b.return(b.id(tmp_id))];
}
const parent = /** @type {Node} */ (context.path.at(-1));
const is_standalone = parent.type === 'ExpressionStatement'; // TODO and possibly others, but not e.g. the `test` of a WhileStatement
const update =
node.prefix || is_standalone
? assignment
: b.binary(node.operator === '++' ? '-' : '+', assignment, b.literal(1));
return maybe_skip_ownership_validation(b.call(b.thunk(b.block(statements))));
return is_ignored(node, 'ownership_invalid_mutation')
? b.call('$.skip_ownership_validation', b.thunk(update))
: update;
}

@ -13,7 +13,7 @@ import {
is_state_source,
should_proxy_or_freeze
} from '../utils.js';
import { is_hoistable_function } from '../../utils.js';
import { is_hoisted_function } from '../../utils.js';
/**
* @param {VariableDeclaration} node
@ -36,11 +36,11 @@ export function VariableDeclaration(node, context) {
rune === '$state.snapshot' ||
rune === '$state.is'
) {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = context.visit(init);
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
b.declaration('const', declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;
}
declarations.push(/** @type {VariableDeclarator} */ (context.visit(declarator)));
@ -171,39 +171,29 @@ export function VariableDeclaration(node, context) {
)
);
} else {
const bindings = context.state.scope.get_bindings(declarator);
const object_id = context.state.scope.generate('derived_object');
const values_id = context.state.scope.generate('derived_values');
declarations.push(
b.declarator(
b.id(object_id),
b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value))
)
);
declarations.push(
b.declarator(
b.id(values_id),
b.call(
'$.derived',
b.thunk(
b.block([
b.let(declarator.id, b.call('$.get', b.id(object_id))),
b.return(b.array(bindings.map((binding) => binding.node)))
])
)
)
)
);
const bindings = extract_paths(declarator.id);
const init = /** @type {CallExpression} */ (declarator.init);
/** @type {Identifier} */
let id;
let rhs = value;
if (init.arguments[0].type === 'Identifier') {
id = init.arguments[0];
} else {
id = b.id(context.state.scope.generate('$$d'));
rhs = b.call('$.get', id);
declarations.push(
b.declarator(id, b.call('$.derived', rune === '$derived.by' ? value : b.thunk(value)))
);
}
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i];
declarations.push(
b.declarator(
binding.node,
b.call(
'$.derived',
b.thunk(b.member(b.call('$.get', b.id(values_id)), b.literal(i), true))
)
)
b.declarator(binding.node, b.call('$.derived', b.thunk(binding.expression(rhs))))
);
}
}
@ -219,11 +209,9 @@ export function VariableDeclaration(node, context) {
if (!has_state && !has_props) {
const init = declarator.init;
if (init != null && is_hoistable_function(init)) {
const hoistable_function = context.visit(init);
if (init != null && is_hoisted_function(init)) {
context.state.hoisted.push(
b.declaration('const', declarator.id, /** @type {Expression} */ (hoistable_function))
b.declaration('const', declarator.id, /** @type {Expression} */ (context.visit(init)))
);
continue;

@ -1,13 +1,14 @@
/** @import { BlockStatement, Expression, ExpressionStatement, Identifier, MemberExpression, Property, Statement } from 'estree' */
/** @import { Attribute, Component, SvelteComponent, SvelteSelf, TemplateNode, Text } from '#compiler' */
/** @import { Component, SvelteComponent, SvelteSelf, TemplateNode } from '#compiler' */
/** @import { ComponentContext } from '../../types.js' */
import { dev, is_ignored } from '../../../../../state.js';
import { get_attribute_chunks } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
import { is_element_node } from '../../../../nodes.js';
import { create_derived, build_setter } from '../../utils.js';
import { build_bind_this, build_event_handler, build_validate_binding } from '../shared/utils.js';
import { create_derived } from '../../utils.js';
import { build_bind_this, validate_binding } from '../shared/utils.js';
import { build_attribute_value } from '../shared/element.js';
import { build_event_handler } from './events.js';
import { determine_slot } from '../../../../../utils/slot.js';
/**
* @param {Component | SvelteComponent | SvelteSelf} node
@ -23,6 +24,15 @@ export function build_component(node, component_name, context, anchor = context.
/** @type {ExpressionStatement[]} */
const lets = [];
/** @type {Record<string, typeof context.state>} */
const states = {
default: {
...context.state,
scope: node.metadata.scopes.default,
transform: { ...context.state.transform }
}
};
/** @type {Record<string, TemplateNode[]>} */
const children = {};
@ -44,7 +54,7 @@ export function build_component(node, component_name, context, anchor = context.
* 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.
*/
let slot_scope_applies_to_itself = false;
let slot_scope_applies_to_itself = !!determine_slot(node);
/**
* Components may have a children prop and also have child nodes. In this case, we assume
@ -65,16 +75,36 @@ export function build_component(node, component_name, context, anchor = context.
props_and_spreads.push(props);
}
}
if (slot_scope_applies_to_itself) {
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
}
}
}
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
if (!slot_scope_applies_to_itself) {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute, states.default)));
}
} else if (attribute.type === 'OnDirective') {
events[attribute.name] ||= [];
let handler = build_event_handler(attribute, null, context);
if (!attribute.expression) {
context.state.analysis.needs_props = true;
}
let handler = build_event_handler(
attribute.expression,
attribute.metadata.expression,
context
);
if (attribute.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
events[attribute.name].push(handler);
(events[attribute.name] ||= []).push(handler);
} else if (attribute.type === 'SpreadAttribute') {
const expression = /** @type {Expression} */ (context.visit(attribute));
if (attribute.metadata.expression.has_state) {
@ -141,7 +171,7 @@ export function build_component(node, component_name, context, anchor = context.
context.state.analysis.runes &&
!is_ignored(node, 'binding_property_non_reactive')
) {
context.state.init.push(build_validate_binding(context.state, attribute, expression));
validate_binding(context.state, attribute, expression);
}
if (attribute.name === 'this') {
@ -164,9 +194,7 @@ export function build_component(node, component_name, context, anchor = context.
const assignment = b.assignment('=', attribute.expression, b.id('$$value'));
push_prop(
b.set(attribute.name, [
b.stmt(build_setter(assignment, context, () => context.visit(assignment)))
])
b.set(attribute.name, [b.stmt(/** @type {Expression} */ (context.visit(assignment)))])
);
}
}
@ -204,19 +232,7 @@ export function build_component(node, component_name, context, anchor = context.
continue;
}
let slot_name = 'default';
if (is_element_node(child)) {
const attribute = /** @type {Attribute | undefined} */ (
child.attributes.find(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
);
if (attribute !== undefined) {
slot_name = /** @type {Text[]} */ (attribute.value)[0].data;
}
}
let slot_name = determine_slot(child) ?? 'default';
(children[slot_name] ||= []).push(child);
}
@ -232,12 +248,15 @@ export function build_component(node, component_name, context, anchor = context.
// @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
}
slot_name === 'default'
? slot_scope_applies_to_itself
? context.state
: states.default
: {
...context.state,
scope: node.metadata.scopes[slot_name],
transform: { ...context.state.transform }
}
)
);

@ -0,0 +1,53 @@
/** @import { BinaryOperator, Expression, Identifier } from 'estree' */
/** @import { ComponentContext, Context } from '../../types' */
import { build_proxy_reassignment, is_state_source, should_proxy_or_freeze } from '../../utils.js';
import * as b from '../../../../../utils/builders.js';
/**
* Turns `foo` into `$.get(foo)`
* @param {Identifier} node
*/
export function get_value(node) {
return b.call('$.get', node);
}
/**
*
* @param {Context | ComponentContext} context
*/
export function add_state_transformers(context) {
for (const [name, binding] of context.state.scope.declarations) {
if (
is_state_source(binding, context.state) ||
binding.kind === 'derived' ||
binding.kind === 'legacy_reactive'
) {
context.state.transform[name] = {
read: get_value,
assign: (node, value) => {
let call = b.call('$.set', node, value);
if (context.state.scope.get(`$${node.name}`)?.kind === 'store_sub') {
call = b.call('$.store_unsub', call, b.literal(`$${node.name}`), b.id('$$stores'));
}
return call;
},
mutate: (node, mutation) => {
if (context.state.analysis.runes) {
return mutation;
}
return b.call('$.mutate', node, mutation);
},
update: (node) => {
return b.call(
node.prefix ? '$.update_pre' : '$.update',
node.argument,
node.operator === '--' && b.literal(-1)
);
}
};
}
}
}

@ -1,15 +1,10 @@
/** @import { Expression, Identifier } from 'estree' */
/** @import { Attribute, ClassDirective, DelegatedEvent, ExpressionMetadata, ExpressionTag, Namespace, OnDirective, RegularElement, StyleDirective, SvelteElement, SvelteNode } from '#compiler' */
/** @import { Attribute, ClassDirective, ExpressionMetadata, Namespace, RegularElement, StyleDirective, SvelteElement } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import {
is_capture_event,
is_passive_event,
normalize_attribute
} from '../../../../../../utils.js';
import { get_attribute_expression } from '../../../../../utils/ast.js';
import { normalize_attribute } from '../../../../../../utils.js';
import * as b from '../../../../../utils/builders.js';
import { build_getter } from '../../utils.js';
import { build_event_handler, build_template_literal, build_update } from './utils.js';
import { build_template_literal, build_update } from './utils.js';
/**
* Serializes each style directive into something like `$.set_style(element, style_property, value)`
@ -106,7 +101,7 @@ export function build_attribute_value(value, context) {
}
return {
has_state: chunk.metadata.expression.has_call,
has_state: chunk.metadata.expression.has_state,
has_call: chunk.metadata.expression.has_call,
value: /** @type {Expression} */ (context.visit(chunk.expression))
};
@ -131,143 +126,3 @@ export function get_attribute_name(element, attribute, context) {
return attribute.name;
}
/**
* @param {Attribute & { value: ExpressionTag | [ExpressionTag] }} node
* @param {ComponentContext} context
*/
export function build_event_attribute(node, context) {
/** @type {string[]} */
const modifiers = [];
let event_name = node.name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
modifiers.push('capture');
}
build_event(
{
name: event_name,
expression: get_attribute_expression(node),
modifiers,
delegated: node.metadata.delegated
},
!Array.isArray(node.value) && node.value?.type === 'ExpressionTag'
? node.value.metadata.expression
: null,
context
);
}
/**
* Serializes an event handler function of the `on:` directive or an attribute starting with `on`
* @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: DelegatedEvent | null;}} node
* @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
export function build_event(node, metadata, context) {
const state = context.state;
/** @type {Expression} */
let expression;
if (node.expression) {
let handler = build_event_handler(node, metadata, context);
const event_name = node.name;
const delegated = node.delegated;
if (delegated != null) {
let delegated_assignment;
if (!state.events.has(event_name)) {
state.events.add(event_name);
}
// Hoist function if we can, otherwise we leave the function as is
if (delegated.type === 'hoistable') {
if (delegated.function === node.expression) {
const func_name = context.state.scope.root.unique('on_' + event_name);
state.hoisted.push(b.var(func_name, handler));
handler = func_name;
}
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
const hoistable_params = /** @type {Expression[]} */ (
delegated.function.metadata.hoistable_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
const args = [handler, ...hoistable_params];
delegated_assignment = b.array(args);
} else {
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
delegated_assignment = handler;
}
state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name)),
delegated_assignment
)
)
);
return;
}
if (node.modifiers.includes('once')) {
handler = b.call('$.once', handler);
}
const args = [
b.literal(event_name),
context.state.node,
handler,
b.literal(node.modifiers.includes('capture'))
];
if (node.modifiers.includes('passive')) {
args.push(b.literal(true));
} else if (node.modifiers.includes('nonpassive')) {
args.push(b.literal(false));
} else if (
is_passive_event(node.name) &&
/** @type {OnDirective} */ (node).type !== 'OnDirective'
) {
// For on:something events we don't apply passive behaviour to match Svelte 4.
args.push(b.literal(true));
}
// Events need to run in order with bindings/actions
expression = b.call('$.event', ...args);
} else {
expression = b.call(
'$.event',
b.literal(node.name),
state.node,
build_event_handler(node, metadata, context)
);
}
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' ||
parent.type === 'SvelteBody'
) {
// These nodes are above the component tree, and its events should run parent first
state.before_init.push(statement);
} else {
state.after_update.push(statement);
}
}

@ -0,0 +1,144 @@
/** @import { Expression } from 'estree' */
/** @import { Attribute, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_capture_event, is_passive_event } from '../../../../../../utils.js';
import * as b from '../../../../../utils/builders.js';
/**
* @param {Attribute} node
* @param {ComponentContext} context
*/
export function visit_event_attribute(node, context) {
let capture = false;
let event_name = node.name.slice(2);
if (is_capture_event(event_name)) {
event_name = event_name.slice(0, -7);
capture = true;
}
// we still need to support the weird `onclick="{() => {...}}" form
const tag = Array.isArray(node.value)
? /** @type {ExpressionTag} */ (node.value[0])
: /** @type {ExpressionTag} */ (node.value);
let handler = build_event_handler(tag.expression, tag.metadata.expression, context);
if (node.metadata.delegated) {
let delegated_assignment;
if (!context.state.events.has(event_name)) {
context.state.events.add(event_name);
}
// Hoist function if we can, otherwise we leave the function as is
if (node.metadata.delegated.hoisted) {
if (node.metadata.delegated.function === tag.expression) {
const func_name = context.state.scope.root.unique('on_' + event_name);
context.state.hoisted.push(b.var(func_name, handler));
handler = func_name;
}
const hoisted_params = /** @type {Expression[]} */ (
node.metadata.delegated.function.metadata.hoisted_params
);
// When we hoist a function we assign an array with the function and all
// hoisted closure params.
const args = [handler, ...hoisted_params];
delegated_assignment = b.array(args);
} else {
delegated_assignment = handler;
}
context.state.init.push(
b.stmt(
b.assignment(
'=',
b.member(context.state.node, b.id('__' + event_name)),
delegated_assignment
)
)
);
} else {
const statement = b.stmt(
build_event(event_name, context.state.node, handler, capture, undefined)
);
const type = /** @type {SvelteNode} */ (context.path.at(-1)).type;
if (type === 'SvelteDocument' || type === 'SvelteWindow' || type === 'SvelteBody') {
// These nodes are above the component tree, and its events should run parent first
context.state.init.push(statement);
} else {
context.state.after_update.push(statement);
}
}
}
/**
* Creates a `$.event(...)` call for non-delegated event handlers
* @param {string} event_name
* @param {Expression} node
* @param {Expression} handler
* @param {boolean} capture
* @param {boolean | undefined} passive
*/
export function build_event(event_name, node, handler, capture, passive) {
return b.call(
'$.event',
b.literal(event_name),
node,
handler,
capture && b.true,
passive === undefined ? undefined : b.literal(passive)
);
}
/**
* Creates an event handler
* @param {Expression | null} node
* @param {ExpressionMetadata} metadata
* @param {ComponentContext} context
* @returns {Expression}
*/
export function build_event_handler(node, metadata, context) {
if (node === null) {
// bubble event
return b.function(
null,
[b.id('$$arg')],
b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))])
);
}
let handler = /** @type {Expression} */ (context.visit(node));
// inline handler
if (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') {
return handler;
}
// function declared in the script
if (
handler.type === 'Identifier' &&
context.state.scope.get(handler.name)?.declaration_kind !== 'import'
) {
return handler;
}
if (metadata.has_call) {
// memoize where necessary
const id = b.id(context.state.scope.generate('event_handler'));
context.state.init.push(b.var(id, b.call('$.derived', b.thunk(handler))));
handler = b.call('$.get', id);
}
// wrap the handler in a function, so the expression is re-evaluated for each event
return b.function(
null,
[b.rest(b.id('$$args'))],
b.block([b.stmt(b.call(b.member(handler, b.id('apply'), false, true), b.this, b.id('$$args')))])
);
}

@ -1,6 +1,6 @@
/** @import { ArrowFunctionExpression, FunctionExpression, Node } from 'estree' */
/** @import { ComponentContext } from '../../types' */
import { build_hoistable_params } from '../../utils.js';
import { build_hoisted_params } from '../../utils.js';
/**
* @param {ArrowFunctionExpression | FunctionExpression} node
@ -20,8 +20,8 @@ export const visit_function = (node, context) => {
state = { ...context.state, in_constructor: false };
}
if (metadata?.hoistable === true) {
const params = build_hoistable_params(node, context);
if (metadata?.hoisted === true) {
const params = build_hoisted_params(node, context);
return /** @type {FunctionExpression} */ ({
...node,

@ -0,0 +1,23 @@
/** @import { Expression } from 'estree' */
/** @import { SvelteBody, SvelteDocument, SvelteWindow } from '#compiler' */
/** @import { ComponentContext } from '../../types' */
import { is_event_attribute } from '../../../../../utils/ast.js';
import * as b from '../../../../../utils/builders.js';
/**
*
* @param {SvelteBody | SvelteDocument | SvelteWindow} node
* @param {string} id
* @param {ComponentContext} context
*/
export function visit_special_element(node, id, context) {
const state = { ...context.state, node: b.id(id) };
for (const attribute of node.attributes) {
if (attribute.type === 'OnDirective') {
context.state.init.push(b.stmt(/** @type {Expression} */ (context.visit(attribute, state))));
} else {
context.visit(attribute, state);
}
}
}

@ -1,5 +1,5 @@
/** @import { Expression, ExpressionStatement, Identifier, MemberExpression, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { BindDirective, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode, Text } from '#compiler' */
/** @import { BindDirective, DelegatedEvent, ExpressionMetadata, ExpressionTag, OnDirective, SvelteNode, Text } from '#compiler' */
/** @import { ComponentClientTransformState, ComponentContext } from '../../types' */
import { walk } from 'zimmerframe';
import { object } from '../../../../../utils/ast.js';
@ -132,115 +132,6 @@ export function build_update_assignment(state, id, init, value, update) {
);
}
/**
* Serializes the event handler function of the `on:` directive
* @param {Pick<OnDirective, 'name' | 'modifiers' | 'expression'>} node
* @param {null | ExpressionMetadata} metadata
* @param {ComponentContext} context
*/
export function build_event_handler(node, metadata, { state, visit }) {
/** @type {Expression} */
let handler;
if (node.expression) {
handler = node.expression;
// Event handlers can be dynamic (source/store/prop/conditional etc)
const dynamic_handler = () =>
b.function(
null,
[b.rest(b.id('$$args'))],
b.block([
b.return(
b.call(
b.member(/** @type {Expression} */ (visit(handler)), b.id('apply'), false, true),
b.this,
b.id('$$args')
)
)
])
);
if (
metadata?.has_call &&
!(
(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 (
binding !== null &&
(binding.kind === 'state' ||
binding.kind === 'frozen_state' ||
binding.declaration_kind === 'import' ||
binding.kind === 'legacy_reactive' ||
binding.kind === 'derived' ||
binding.kind === 'prop' ||
binding.kind === 'bindable_prop' ||
binding.kind === 'store_sub')
) {
handler = dynamic_handler();
} else {
handler = /** @type {Expression} */ (visit(handler));
}
} else if (handler.type === 'ConditionalExpression' || handler.type === 'LogicalExpression') {
handler = dynamic_handler();
} else {
handler = /** @type {Expression} */ (visit(handler));
}
} else {
state.analysis.needs_props = true;
// Function + .call to preserve "this" context as much as possible
handler = b.function(
null,
[b.id('$$arg')],
b.block([b.stmt(b.call('$.bubble_event.call', b.this, b.id('$$props'), b.id('$$arg')))])
);
}
if (node.modifiers.includes('stopPropagation')) {
handler = b.call('$.stopPropagation', handler);
}
if (node.modifiers.includes('stopImmediatePropagation')) {
handler = b.call('$.stopImmediatePropagation', handler);
}
if (node.modifiers.includes('preventDefault')) {
handler = b.call('$.preventDefault', handler);
}
if (node.modifiers.includes('self')) {
handler = b.call('$.self', handler);
}
if (node.modifiers.includes('trusted')) {
handler = b.call('$.trusted', handler);
}
return handler;
}
/**
* Serializes `bind:this` for components and elements.
* @param {Identifier | MemberExpression} expression
@ -254,8 +145,10 @@ export function build_bind_this(expression, value, { state, visit }) {
/** @type {Expression[]} */
const values = [];
/** @type {typeof state.getters} */
const getters = {};
/** @type {string[]} */
const seen = [];
const transform = { ...state.transform };
// 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
@ -263,7 +156,8 @@ export function build_bind_this(expression, value, { state, visit }) {
// variables, but that was the same case in Svelte 4, too. Once legacy mode is gone completely, we can revisit this.
walk(expression, null, {
Identifier(node, { path }) {
if (Object.hasOwn(getters, node.name)) return;
if (seen.includes(node.name)) return;
seen.push(node.name);
const parent = /** @type {Expression} */ (path.at(-1));
if (!is_reference(node, parent)) return;
@ -275,14 +169,21 @@ export function build_bind_this(expression, value, { state, visit }) {
if (owner.type === 'EachBlock' && scope === binding.scope) {
ids.push(node);
values.push(/** @type {Expression} */ (visit(node)));
getters[node.name] = node;
if (transform[node.name]) {
transform[node.name] = {
...transform[node.name],
read: (node) => node
};
}
break;
}
}
}
});
const child_state = { ...state, getters: { ...state.getters, ...getters } };
const child_state = { ...state, transform };
const get = /** @type {Expression} */ (visit(expression, child_state));
const set = /** @type {Expression} */ (
@ -313,28 +214,30 @@ export function build_bind_this(expression, value, { state, visit }) {
* @param {BindDirective} binding
* @param {MemberExpression} expression
*/
export function build_validate_binding(state, binding, expression) {
const string = state.analysis.source.slice(binding.start, binding.end);
const get_object = b.thunk(/** @type {Expression} */ (expression.object));
const get_property = b.thunk(
/** @type {Expression} */ (
expression.computed
? expression.property
: b.literal(/** @type {Identifier} */ (expression.property).name)
)
);
export function validate_binding(state, binding, expression) {
// If we are referencing a $store.foo then we don't need to add validation
const left = object(binding.expression);
const left_binding = left && state.scope.get(left.name);
if (left_binding?.kind === 'store_sub') return;
const loc = locator(binding.start);
return b.stmt(
b.call(
'$.validate_binding',
b.literal(string),
get_object,
get_property,
loc && b.literal(loc.line),
loc && b.literal(loc.column)
state.init.push(
b.stmt(
b.call(
'$.validate_binding',
b.literal(state.analysis.source.slice(binding.start, binding.end)),
b.thunk(/** @type {Expression} */ (expression.object)),
b.thunk(
/** @type {Expression} */ (
expression.computed
? expression.property
: b.literal(/** @type {Identifier} */ (expression.property).name)
)
),
loc && b.literal(loc.line),
loc && b.literal(loc.column)
)
)
);
}

@ -1,59 +1,16 @@
/** @import { AssignmentExpression, AssignmentOperator, BinaryOperator, Expression, Node, Pattern } from 'estree' */
/** @import { AssignmentExpression, AssignmentOperator, Expression, 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 { build_getter } from './shared/utils.js';
import { build_assignment_value } from '../../../../utils/ast.js';
import { visit_assignment_expression } from '../../shared/assignments.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 = build_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 build_assignment(node.operator, node.left, node.right, context) || context.next();
return visit_assignment_expression(node, context, build_assignment);
}
/**
@ -83,16 +40,9 @@ function build_assignment(operator, left, right, context) {
}
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)),
build_getter(left, context.state),
value
);
}
let value = /** @type {Expression} */ (
context.visit(build_assignment_value(operator, left, right))
);
return b.call('$.store_set', b.id(name), value);
}

@ -1,7 +1,8 @@
/** @import { Location } from 'locate-character' */
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { SvelteElement } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { dev } from '../../../../state.js';
import { dev, locator } from '../../../../state.js';
import * as b from '../../../../utils/builders.js';
import { determine_namespace_for_children } from '../../utils.js';
import { build_element_attributes } from './shared/element.js';
@ -36,7 +37,18 @@ export function SvelteElement(node, context) {
build_element_attributes(node, { ...context, state });
if (dev) {
context.state.template.push(b.stmt(b.call('$.push_element', tag, b.id('$$payload'))));
const location = /** @type {Location} */ (locator(node.start));
context.state.template.push(
b.stmt(
b.call(
'$.push_element',
b.id('$$payload'),
tag,
b.literal(location.line),
b.literal(location.column)
)
)
);
}
const attributes = b.block([...state.init, ...build_template(state.template)]);

@ -2,7 +2,7 @@
/** @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 { build_fallback, extract_paths } from '../../../../utils/ast.js';
import * as b from '../../../../utils/builders.js';
import { get_rune } from '../../../scope.js';
import { walk } from 'zimmerframe';
@ -96,9 +96,7 @@ export function VariableDeclaration(node, context) {
const name = /** @type {Identifier} */ (path.node).name;
const binding = /** @type {Binding} */ (context.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)))
);
declarations.push(b.declarator(path.node, build_fallback(prop, value)));
}
continue;
}
@ -114,9 +112,7 @@ export function VariableDeclaration(node, context) {
let init = prop;
if (declarator.init) {
const default_value = /** @type {Expression} */ (context.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));
init = build_fallback(prop, default_value);
}
declarations.push(b.declarator(declarator.id, init));

@ -20,6 +20,15 @@ export function build_inline_component(node, expression, context) {
/** @type {Record<string, LetDirective[]>} */
const lets = { default: [] };
/**
* Children in the default slot are evaluated in the component scope,
* children in named slots are evaluated in the parent scope
*/
const child_state = {
...context.state,
scope: node.metadata.scopes.default
};
/** @type {Record<string, TemplateNode[]>} */
const children = {};
@ -144,12 +153,12 @@ export function build_inline_component(node, expression, context) {
// @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
}
slot_name === 'default'
? child_state
: {
...context.state,
scope: node.metadata.scopes[slot_name]
}
)
);

@ -0,0 +1,77 @@
/** @import { AssignmentExpression, AssignmentOperator, Expression, Node, Pattern } from 'estree' */
/** @import { Context as ClientContext } from '../client/types.js' */
/** @import { Context as ServerContext } from '../server/types.js' */
import { extract_paths, is_expression_async } from '../../../utils/ast.js';
import * as b from '../../../utils/builders.js';
/**
* @template {ClientContext | ServerContext} Context
* @param {AssignmentExpression} node
* @param {Context} context
* @param {(operator: AssignmentOperator, left: Pattern, right: Expression, context: Context) => Expression | null} build_assignment
* @returns
*/
export function visit_assignment_expression(node, context, build_assignment) {
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 = build_assignment('=', path.node, value, context);
if (assignment !== null) changed = true;
return (
assignment ??
b.assignment(
'=',
/** @type {Pattern} */ (context.visit(path.node)),
/** @type {Expression} */ (context.visit(value))
)
);
});
if (!changed) {
// No change to output -> nothing to transform -> we can keep the original assignment
return context.next();
}
const is_standalone = /** @type {Node} */ (context.path.at(-1)).type.endsWith('Statement');
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
const iife = b.arrow([rhs], sequence);
const iife_is_async =
is_expression_async(value) ||
assignments.some((assignment) => is_expression_async(assignment));
return iife_is_async ? b.await(b.call(b.async(iife), value)) : b.call(iife, value);
}
return sequence;
}
if (node.left.type !== 'Identifier' && node.left.type !== 'MemberExpression') {
throw new Error(`Unexpected assignment type ${node.left.type}`);
}
return (
build_assignment(node.operator, node.left, node.right, context) ??
/** @type {Expression} */ (context.next())
);
}

@ -20,13 +20,13 @@ import { dev } from '../../state.js';
* @param {Node} node
* @returns {boolean}
*/
export function is_hoistable_function(node) {
export function is_hoisted_function(node) {
if (
node.type === 'ArrowFunctionExpression' ||
node.type === 'FunctionExpression' ||
node.type === 'FunctionDeclaration'
) {
return node.metadata?.hoistable === true;
return node.metadata?.hoisted === true;
}
return false;
}
@ -283,6 +283,7 @@ export function clean_nodes(
((first.type === 'RenderTag' && !first.metadata.dynamic) ||
(first.type === 'Component' &&
!state.options.hmr &&
!first.metadata.dynamic &&
!first.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--')
))),
@ -424,7 +425,7 @@ export function transform_inspect_rune(node, context) {
const { state, visit } = context;
const as_fn = state.options.generate === 'client';
if (!dev) return b.unary('void', b.literal(0));
if (!dev) return b.empty;
if (node.callee.type === 'MemberExpression') {
const raw_inspect_args = /** @type {CallExpression} */ (node.callee.object).arguments;

@ -1,9 +1,8 @@
/** @import { ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, Node, Pattern, VariableDeclarator } from 'estree' */
/** @import { Context, Visitor } from 'zimmerframe' */
/** @import { AnimateDirective, Binding, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteNode, TransitionDirective, UseDirective } from '#compiler' */
/** @import { AnimateDirective, Binding, Component, DeclarationKind, EachBlock, ElementLike, LetDirective, SvelteComponent, SvelteNode, SvelteSelf, TransitionDirective, UseDirective } from '#compiler' */
import is_reference from 'is-reference';
import { walk } from 'zimmerframe';
import { is_element_node } from './nodes.js';
import * as b from '../utils/builders.js';
import * as e from '../errors.js';
import {
@ -13,6 +12,7 @@ import {
unwrap_pattern
} from '../utils/ast.js';
import { is_reserved, is_rune } from '../../utils.js';
import { determine_slot } from '../utils/slot.js';
export class Scope {
/** @type {ScopeRoot} */
@ -290,52 +290,47 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
* @type {Visitor<ElementLike, State, SvelteNode>}
*/
const SvelteFragment = (node, { state, next }) => {
const [scope] = analyze_let_directives(node, state.scope);
const scope = state.scope.child();
scopes.set(node, scope);
next({ scope });
};
/**
* @param {ElementLike} node
* @param {Scope} parent
* @type {Visitor<Component | SvelteComponent | SvelteSelf, State, SvelteNode>}
*/
function analyze_let_directives(node, parent) {
const scope = parent.child();
let is_default_slot = true;
const Component = (node, context) => {
node.metadata.scopes = {
default: context.state.scope.child()
};
const default_state = determine_slot(node)
? context.state
: { scope: node.metadata.scopes.default };
for (const attribute of node.attributes) {
if (attribute.type === 'LetDirective') {
/** @type {Binding[]} */
const bindings = [];
scope.declarators.set(attribute, bindings);
context.visit(attribute, default_state);
} else {
context.visit(attribute);
}
}
// attach the scope to the directive itself, as well as the
// contents to which it applies
scopes.set(attribute, scope);
for (const child of node.fragment.nodes) {
let state = default_state;
if (attribute.expression) {
for (const id of extract_identifiers_from_destructuring(attribute.expression)) {
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else {
/** @type {Identifier} */
const id = {
name: attribute.name,
type: 'Identifier',
start: attribute.start,
end: attribute.end
};
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else if (attribute.type === 'Attribute' && attribute.name === 'slot') {
is_default_slot = false;
const slot_name = determine_slot(child);
if (slot_name !== null) {
node.metadata.scopes[slot_name] = context.state.scope.child();
state = {
scope: node.metadata.scopes[slot_name]
};
}
}
return /** @type {const} */ ([scope, is_default_slot]);
}
context.visit(child, state);
}
};
/**
* @type {Visitor<AnimateDirective | TransitionDirective | UseDirective, State, SvelteNode>}
@ -384,48 +379,37 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
SvelteElement: SvelteFragment,
RegularElement: SvelteFragment,
Component(node, { state, visit, path }) {
state.scope.reference(b.id(node.name), path);
LetDirective(node, context) {
const scope = context.state.scope;
// let:x is super weird:
// - for the default slot, its scope only applies to children that are not slots themselves
// - for named slots, its scope applies to the component itself, too
const [scope, is_default_slot] = analyze_let_directives(node, state.scope);
if (is_default_slot) {
for (const attribute of node.attributes) {
visit(attribute);
}
} else {
scopes.set(node, scope);
/** @type {Binding[]} */
const bindings = [];
scope.declarators.set(node, bindings);
for (const attribute of node.attributes) {
visit(attribute, { ...state, scope });
if (node.expression) {
for (const id of extract_identifiers_from_destructuring(node.expression)) {
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
} else {
/** @type {Identifier} */
const id = {
name: node.name,
type: 'Identifier',
start: node.start,
end: node.end
};
const binding = scope.declare(id, 'derived', 'const');
bindings.push(binding);
}
},
for (const child of node.fragment.nodes) {
if (
is_element_node(child) &&
child.attributes.some(
(attribute) => attribute.type === 'Attribute' && attribute.name === 'slot'
)
) {
// <div slot="..."> inherits the scope above the component unless the component is a named slot itself, because slots are hella weird
scopes.set(child, is_default_slot ? state.scope : scope);
visit(child, { scope: is_default_slot ? state.scope : scope });
} else {
if (child.type === 'ExpressionTag') {
// expression tag is a special case — we don't visit it directly, but via process_children,
// so we need to set the scope on the expression rather than the tag itself
scopes.set(child.expression, scope);
} else {
scopes.set(child, scope);
}
visit(child, { scope });
}
}
Component: (node, context) => {
context.state.scope.reference(b.id(node.name), context.path);
Component(node, context);
},
SvelteSelf: Component,
SvelteComponent: Component,
// updates
AssignmentExpression(node, { state, next }) {
@ -532,7 +516,6 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) {
references_within.add(id);
}
}
scopes.set(node.expression, state.scope);
// context and children are a new scope
const scope = state.scope.child();

@ -82,24 +82,24 @@ export interface ComponentAnalysis extends Analysis {
declare module 'estree' {
interface ArrowFunctionExpression {
metadata: {
hoistable: boolean | 'impossible';
hoistable_params: Pattern[];
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionExpression {
metadata: {
hoistable: boolean | 'impossible';
hoistable_params: Pattern[];
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}
interface FunctionDeclaration {
metadata: {
hoistable: boolean | 'impossible';
hoistable_params: Pattern[];
hoisted: boolean;
hoisted_params: Pattern[];
scope: Scope;
};
}

@ -271,7 +271,6 @@ export interface Binding {
* - `snippet`: A snippet parameter
* - `store_sub`: A $store value
* - `legacy_reactive`: A `$:` declaration
* - `legacy_reactive_import`: An imported binding that is mutated inside the component
*/
kind:
| 'normal'
@ -284,8 +283,7 @@ export interface Binding {
| 'each'
| 'snippet'
| 'store_sub'
| 'legacy_reactive'
| 'legacy_reactive_import';
| 'legacy_reactive';
declaration_kind: DeclarationKind;
/**
* What the value was initialized with.

@ -16,6 +16,7 @@ import type {
ChainExpression,
SimpleCallExpression
} from 'estree';
import type { Scope } from '../phases/scope';
export interface BaseNode {
type: string;
@ -77,7 +78,7 @@ export interface SvelteOptions {
namespace?: Namespace;
css?: 'injected';
customElement?: {
tag: string;
tag?: string;
shadow?: 'open' | 'none';
props?: Record<
string,
@ -214,10 +215,10 @@ export interface OnDirective extends BaseNode {
export type DelegatedEvent =
| {
type: 'hoistable';
hoisted: true;
function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration;
}
| { type: 'non-hoistable' };
| { hoisted: false };
/** A `style:` directive */
export interface StyleDirective extends BaseNode {
@ -275,6 +276,7 @@ interface BaseElement extends BaseNode {
export interface Component extends BaseElement {
type: 'Component';
metadata: {
scopes: Record<string, Scope>;
dynamic: boolean;
};
}
@ -311,6 +313,9 @@ export interface SvelteComponent extends BaseElement {
type: 'SvelteComponent';
name: 'svelte:component';
expression: Expression;
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteDocument extends BaseElement {
@ -356,6 +361,9 @@ export interface SvelteOptionsRaw extends BaseElement {
export interface SvelteSelf extends BaseElement {
type: 'SvelteSelf';
name: 'svelte:self';
metadata: {
scopes: Record<string, Scope>;
};
}
interface SvelteWindow extends BaseElement {

@ -276,15 +276,19 @@ function _extract_paths(assignments = [], param, expression, update_expression,
const rest_expression = (object) => {
/** @type {ESTree.Expression[]} */
const props = [];
for (const p of param.properties) {
if (p.type === 'Property' && p.key.type !== 'PrivateIdentifier') {
if (p.key.type === 'Identifier' && !p.computed) {
props.push(b.literal(p.key.name));
} else if (p.key.type === 'Literal') {
props.push(b.literal(String(p.key.value)));
} else {
props.push(p.key);
props.push(b.call('String', p.key));
}
}
}
return b.call('$.exclude_from_object', expression(object), b.array(props));
};
@ -364,12 +368,7 @@ function _extract_paths(assignments = [], param, expression, update_expression,
case 'AssignmentPattern': {
/** @type {DestructuredAssignment['expression']} */
const fallback_expression = (object) =>
is_expression_async(param.right)
? b.await(
b.call('$.value_or_fallback_async', expression(object), b.thunk(param.right, true))
)
: b.call('$.value_or_fallback', expression(object), b.thunk(param.right));
const fallback_expression = (object) => build_fallback(expression(object), param.right);
if (param.left.type === 'Identifier') {
assignments.push({
@ -545,3 +544,34 @@ export function is_expression_async(expression) {
return false;
}
}
/**
*
* @param {ESTree.Expression} expression
* @param {ESTree.Expression} fallback
*/
export function build_fallback(expression, fallback) {
if (is_simple_expression(fallback)) {
return b.call('$.fallback', expression, fallback);
}
if (fallback.type === 'AwaitExpression' && is_simple_expression(fallback.argument)) {
return b.await(b.call('$.fallback', expression, fallback.argument));
}
return is_expression_async(fallback)
? b.await(b.call('$.fallback', expression, b.thunk(fallback, true), b.true))
: b.call('$.fallback', expression, b.thunk(fallback), b.true);
}
/**
* @param {ESTree.AssignmentOperator} operator
* @param {ESTree.Identifier | ESTree.MemberExpression} left
* @param {ESTree.Expression} right
*/
export function build_assignment_value(operator, left, right) {
return operator === '='
? right
: // turn something like x += 1 into x = x + 1
b.binary(/** @type {ESTree.BinaryOperator} */ (operator.slice(0, -1)), left, right);
}

@ -0,0 +1,20 @@
/** @import { SvelteNode } from '#compiler' */
import { is_element_node } from '../phases/nodes.js';
import { is_text_attribute } from './ast.js';
/**
* @param {SvelteNode} node
*/
export function determine_slot(node) {
if (!is_element_node(node)) return null;
for (const attribute of node.attributes) {
if (attribute.type !== 'Attribute') continue;
if (attribute.name !== 'slot') continue;
if (!is_text_attribute(attribute)) continue;
return /** @type {string} */ (attribute.value[0].data);
}
return null;
}

@ -6,10 +6,11 @@ var seen = new Set();
/**
* @param {Node} anchor
* @param {{ hash: string, code: string }} css
* @param {boolean} [is_custom_element]
*/
export function append_styles(anchor, css) {
export function append_styles(anchor, css, is_custom_element = false) {
// in dev, always check the DOM, so that styles can be replaced with HMR
if (!DEV) {
if (!DEV && !is_custom_element) {
if (seen.has(css)) return;
seen.add(css);
}

@ -1,7 +1,7 @@
import { createClassComponent } from '../../../../legacy/legacy-client.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { append } from '../template.js';
import { define_property, object_keys } from '../../../shared/utils.js';
import { define_property, get_descriptor, object_keys } from '../../../shared/utils.js';
/**
* @typedef {Object} CustomElementPropDefinition
@ -305,7 +305,18 @@ export function create_custom_element(
set(value) {
value = get_custom_element_value(prop, value, props_definition);
this.$$d[prop] = value;
this.$$c?.$set({ [prop]: value });
var component = this.$$c;
if (component) {
// // If the instance has an accessor, use that instead
var setter = get_descriptor(component, prop)?.get;
if (setter) {
component[prop] = value;
} else {
component.$set({ [prop]: value });
}
}
}
});
});

@ -59,8 +59,12 @@ export function create_event(event_name, dom, handler, options) {
// Chrome has a bug where pointer events don't work when attached to a DOM element that has been cloned
// with cloneNode() and the DOM element is disconnected from the document. To ensure the event works, we
// defer the attachment till after it's been appended to the document. TODO: remove this once Chrome fixes
// this bug. The same applies to wheel events.
if (event_name.startsWith('pointer') || event_name === 'wheel') {
// this bug. The same applies to wheel events and touch events.
if (
event_name.startsWith('pointer') ||
event_name.startsWith('touch') ||
event_name === 'wheel'
) {
queue_micro_task(() => {
dom.addEventListener(event_name, target_handler, options);
});

@ -222,6 +222,8 @@ export function transition(flags, element, get_fn, get_params) {
1,
() => {
dispatch_event(element, 'introend');
// Ensure we cancel the animation to prevent leaking
intro?.abort();
intro = current_options = undefined;
},
is_both
@ -249,6 +251,8 @@ export function transition(flags, element, get_fn, get_params) {
0,
() => {
dispatch_event(element, 'outroend');
// Ensure we cancel the animation to prevent leaking
outro?.abort();
outro = current_options = undefined;
fn?.();
},
@ -322,8 +326,10 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
// once DOM has been updated...
/** @type {Animation} */
var a;
var aborted = false;
queue_micro_task(() => {
if (aborted) return;
var o = options({ direction: is_intro ? 'in' : 'out' });
a = animate(element, o, counterpart, t2, on_finish, on_abort);
});
@ -331,7 +337,10 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
// ...but we want to do so without using `async`/`await` everywhere, so
// we return a facade that allows everything to remain synchronous
return {
abort: () => a.abort(),
abort: () => {
aborted = true;
a?.abort();
},
deactivate: () => a.deactivate(),
reset: () => a.reset(),
t: (now) => a.t(now)
@ -439,9 +448,15 @@ function animate(element, options, counterpart, t2, on_finish, on_abort) {
return {
abort: () => {
animation?.cancel();
if (animation) {
animation.cancel();
// This prevents memory leaks in Chromium
animation.effect = null;
}
task?.abort();
on_abort?.();
on_finish = undefined;
on_abort = undefined;
},
deactivate: () => {
on_finish = undefined;

@ -30,21 +30,38 @@ export let hydrate_node;
/** @param {TemplateNode} node */
export function set_hydrate_node(node) {
if (node === null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return (hydrate_node = node);
}
export function hydrate_next() {
if (hydrate_node === null) {
return set_hydrate_node(/** @type {TemplateNode} */ (hydrate_node.nextSibling));
}
/** @param {TemplateNode} node */
export function reset(node) {
if (!hydrating) return;
// If the node has remaining siblings, something has gone wrong
if (hydrate_node.nextSibling !== null) {
w.hydration_mismatch();
throw HYDRATION_ERROR;
}
return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling));
hydrate_node = node;
}
/** @param {TemplateNode} node */
export function reset(node) {
/**
* @param {HTMLTemplateElement} template
*/
export function hydrate_template(template) {
if (hydrating) {
hydrate_node = node;
// @ts-expect-error TemplateNode doesn't include DocumentFragment, but it's actually fine
hydrate_node = template.content;
}
}

@ -93,6 +93,22 @@ export function component_api_invalid_new(component, name) {
}
}
/**
* A derived value cannot reference itself recursively
* @returns {never}
*/
export function derived_references_self() {
if (DEV) {
const error = new Error(`derived_references_self\nA derived value cannot reference itself recursively`);
error.name = 'Svelte error';
throw error;
} else {
// TODO print a link to the documentation
throw new Error("derived_references_self");
}
}
/**
* Keyed each block has duplicate key `%value%` at indexes %a% and %b%
* @param {string} a

@ -66,7 +66,7 @@ export {
bind_focused
} from './dom/elements/bindings/universal.js';
export { bind_window_scroll, bind_window_size } from './dom/elements/bindings/window.js';
export { next, reset } from './dom/hydration.js';
export { hydrate_template, next, reset } from './dom/hydration.js';
export {
once,
preventDefault,
@ -134,8 +134,6 @@ export {
untrack,
update,
update_pre,
value_or_fallback,
value_or_fallback_async,
exclude_from_object,
pop,
push,
@ -164,7 +162,7 @@ export {
$document as document
} from './dom/operations.js';
export { snapshot } from '../shared/clone.js';
export { noop } from '../shared/utils.js';
export { noop, fallback } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,

@ -1,4 +1,5 @@
/** @import { Derived } from '#client' */
import { DEV } from 'esm-env';
import { CLEAN, DERIVED, DESTROYED, DIRTY, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
current_reaction,
@ -11,6 +12,7 @@ import {
increment_version
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
import * as e from '../errors.js';
export let updating_derived = false;
@ -79,17 +81,36 @@ function destroy_derived_children(derived) {
}
}
/**
* The currently updating deriveds, used to detect infinite recursion
* in dev mode and provide a nicer error than 'too much recursion'
* @type {Derived[]}
*/
let stack = [];
/**
* @param {Derived} derived
* @returns {void}
*/
export function update_derived(derived) {
if (DEV) {
if (stack.includes(derived)) {
e.derived_references_self();
}
stack.push(derived);
}
var previous_updating_derived = updating_derived;
updating_derived = true;
destroy_derived_children(derived);
var value = update_reaction(derived);
updating_derived = previous_updating_derived;
if (DEV) {
stack.pop();
}
var status =
(current_skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null
? MAYBE_DIRTY

@ -252,7 +252,7 @@ export function legacy_pre_effect(deps, fn) {
deps();
// If this legacy pre effect has already run before the end of the reset, then
// bail-out to emulate the same behavior.
// bail out to emulate the same behavior.
if (token.ran) return;
token.ran = true;

@ -126,6 +126,13 @@ const legacy_rest_props_handler = {
};
}
},
deleteProperty(target, key) {
// Svelte 4 allowed for deletions on $$restProps
if (target.exclude.includes(key)) return false;
target.exclude.push(key);
update(target.version);
return true;
},
has(target, key) {
if (target.exclude.includes(key)) return false;
return key in target.props;

@ -162,9 +162,9 @@ export function check_dirtiness(reaction) {
if ((flags & MAYBE_DIRTY) !== 0) {
var dependencies = reaction.deps;
var is_unowned = (flags & UNOWNED) !== 0;
if (dependencies !== null) {
var is_unowned = (flags & UNOWNED) !== 0;
var i;
if ((flags & DISCONNECTED) !== 0) {
@ -198,7 +198,10 @@ export function check_dirtiness(reaction) {
}
}
set_signal_status(reaction, CLEAN);
// Unowned signals should never be marked as clean.
if (!is_unowned) {
set_signal_status(reaction, CLEAN);
}
}
return false;
@ -988,32 +991,16 @@ export function update_pre(signal, d = 1) {
* @returns {Record<string, unknown>}
*/
export function exclude_from_object(obj, keys) {
obj = { ...obj };
let key;
for (key of keys) {
delete obj[key];
}
return obj;
}
/** @type {Record<string, unknown>} */
var result = {};
/**
* @template V
* @param {V} value
* @param {() => V} fallback lazy because could contain side effects
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback() : value;
}
for (var key in obj) {
if (!keys.includes(key)) {
result[key] = obj[key];
}
}
/**
* @template V
* @param {V} value
* @param {() => Promise<V>} fallback lazy because could contain side effects
* @returns {Promise<V>}
*/
export async function value_or_fallback_async(value, fallback) {
return value === undefined ? fallback() : value;
return result;
}
/**

@ -383,26 +383,6 @@ export function unsubscribe_stores(store_values) {
}
}
/**
* @template V
* @param {V} value
* @param {() => V} fallback lazy because could contain side effects
* @returns {V}
*/
export function value_or_fallback(value, fallback) {
return value === undefined ? fallback() : value;
}
/**
* @template V
* @param {V} value
* @param {() => Promise<V>} fallback lazy because could contain side effects
* @returns {Promise<V>}
*/
export async function value_or_fallback_async(value, fallback) {
return value === undefined ? fallback() : value;
}
/**
* @param {Payload} payload
* @param {void | ((payload: Payload, props: Record<string, unknown>) => void)} slot_fn
@ -536,6 +516,8 @@ export { push_element, pop_element } from './dev.js';
export { snapshot } from '../shared/clone.js';
export { fallback } from '../shared/utils.js';
export {
invalid_default_snippet,
validate_dynamic_element_tag,

@ -46,3 +46,18 @@ export function run_all(arr) {
arr[i]();
}
}
/**
* @template V
* @param {V} value
* @param {V | (() => V)} fallback
* @param {boolean} [lazy]
* @returns {V}
*/
export function fallback(value, fallback, lazy = false) {
return value === undefined
? lazy
? /** @type {() => V} */ (fallback)()
: /** @type {V} */ (fallback)
: value;
}

@ -110,7 +110,10 @@ class Svelte4Component {
recover: options.recover
});
flush_sync();
// We don't flush_sync for custom element wrappers
if (!options?.props?.$$host) {
flush_sync();
}
this.#events = props.$$events;

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

Loading…
Cancel
Save