diff --git a/.changeset/brown-insects-float.md b/.changeset/brown-insects-float.md new file mode 100644 index 0000000000..58d397b427 --- /dev/null +++ b/.changeset/brown-insects-float.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: speed up $.exclude_from_object diff --git a/.changeset/cool-turtles-travel.md b/.changeset/cool-turtles-travel.md new file mode 100644 index 0000000000..31145e6e76 --- /dev/null +++ b/.changeset/cool-turtles-travel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure custom elements do not sync flush on mount diff --git a/.changeset/dirty-pens-look.md b/.changeset/dirty-pens-look.md new file mode 100644 index 0000000000..16536edce7 --- /dev/null +++ b/.changeset/dirty-pens-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly handle SvelteDate methods with arguments diff --git a/.changeset/fluffy-kings-complain.md b/.changeset/fluffy-kings-complain.md new file mode 100644 index 0000000000..9eed7768d7 --- /dev/null +++ b/.changeset/fluffy-kings-complain.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: publish package provenance info diff --git a/.changeset/four-kids-flow.md b/.changeset/four-kids-flow.md new file mode 100644 index 0000000000..894816dfb6 --- /dev/null +++ b/.changeset/four-kids-flow.md @@ -0,0 +1,5 @@ +--- +"svelte": minor +--- + +feat: make custom element `tag` property optional diff --git a/.changeset/gold-students-jump.md b/.changeset/gold-students-jump.md new file mode 100644 index 0000000000..d38393554a --- /dev/null +++ b/.changeset/gold-students-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: treat tag with `.` as a component, even if lowercase diff --git a/.changeset/healthy-dancers-play.md b/.changeset/healthy-dancers-play.md new file mode 100644 index 0000000000..6fe572bb67 --- /dev/null +++ b/.changeset/healthy-dancers-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve prop binding warning validation for stores diff --git a/.changeset/healthy-mangos-brush.md b/.changeset/healthy-mangos-brush.md new file mode 100644 index 0000000000..3eeada7ad4 --- /dev/null +++ b/.changeset/healthy-mangos-brush.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add touch events on microtask to avoid Chromium bug diff --git a/.changeset/hip-goats-smoke.md b/.changeset/hip-goats-smoke.md new file mode 100644 index 0000000000..ce023460b7 --- /dev/null +++ b/.changeset/hip-goats-smoke.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure custom element styles append correctly during prod diff --git a/.changeset/loud-news-deliver.md b/.changeset/loud-news-deliver.md new file mode 100644 index 0000000000..f562caff8f --- /dev/null +++ b/.changeset/loud-news-deliver.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: allow deletion of $$restProps properties diff --git a/.changeset/new-cooks-roll.md b/.changeset/new-cooks-roll.md new file mode 100644 index 0000000000..d13c52a799 --- /dev/null +++ b/.changeset/new-cooks-roll.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: add error for derived self referencing diff --git a/.changeset/nice-bottles-greet.md b/.changeset/nice-bottles-greet.md new file mode 100644 index 0000000000..88864809fe --- /dev/null +++ b/.changeset/nice-bottles-greet.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: skip `is_standalone` optimisation for dynamic components diff --git a/.changeset/nine-ants-invite.md b/.changeset/nine-ants-invite.md new file mode 100644 index 0000000000..fdf33c27a9 --- /dev/null +++ b/.changeset/nine-ants-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: invalidate signals following ++/-- inside each block diff --git a/.changeset/olive-forks-grin.md b/.changeset/olive-forks-grin.md new file mode 100644 index 0000000000..6b66827c86 --- /dev/null +++ b/.changeset/olive-forks-grin.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure unowned deriveds correctly update diff --git a/.changeset/perfect-hairs-matter.md b/.changeset/perfect-hairs-matter.md new file mode 100644 index 0000000000..8ec02f7507 --- /dev/null +++ b/.changeset/perfect-hairs-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: better code generation for destructuring assignments diff --git a/.changeset/poor-mugs-pay.md b/.changeset/poor-mugs-pay.md new file mode 100644 index 0000000000..01f54e4604 --- /dev/null +++ b/.changeset/poor-mugs-pay.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: propagate custom element component prop changes diff --git a/.changeset/pre.json b/.changeset/pre.json index ef858b3956..172a6e9d68 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -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", diff --git a/.changeset/silly-masks-exist.md b/.changeset/silly-masks-exist.md new file mode 100644 index 0000000000..e1b161719b --- /dev/null +++ b/.changeset/silly-masks-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent numerous transition/animation memory leaks diff --git a/.changeset/six-apes-peel.md b/.changeset/six-apes-peel.md new file mode 100644 index 0000000000..e642a1f507 --- /dev/null +++ b/.changeset/six-apes-peel.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: order of arguments for `push_element` in `svelte:element` diff --git a/.changeset/smart-cars-know.md b/.changeset/smart-cars-know.md new file mode 100644 index 0000000000..5b87239e41 --- /dev/null +++ b/.changeset/smart-cars-know.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: avoid recreating handlers for component events diff --git a/.changeset/smooth-windows-explain.md b/.changeset/smooth-windows-explain.md new file mode 100644 index 0000000000..82f86945ea --- /dev/null +++ b/.changeset/smooth-windows-explain.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure event handlers referencing $host are not hoisted diff --git a/.changeset/stupid-rivers-stare.md b/.changeset/stupid-rivers-stare.md new file mode 100644 index 0000000000..d1d730cd21 --- /dev/null +++ b/.changeset/stupid-rivers-stare.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: provide more hydration mismatch coverage diff --git a/.changeset/thin-papayas-tap.md b/.changeset/thin-papayas-tap.md new file mode 100644 index 0000000000..abdd314005 --- /dev/null +++ b/.changeset/thin-papayas-tap.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improved memory profile for transitions/animations diff --git a/.changeset/wicked-years-drive.md b/.changeset/wicked-years-drive.md new file mode 100644 index 0000000000..27610ab7c2 --- /dev/null +++ b/.changeset/wicked-years-drive.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: simplify derived object destructuring diff --git a/.changeset/wild-pumas-count.md b/.changeset/wild-pumas-count.md new file mode 100644 index 0000000000..5d34731ef2 --- /dev/null +++ b/.changeset/wild-pumas-count.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: call correct event handler for properties of non-reactive objects diff --git a/.changeset/witty-frogs-cheat.md b/.changeset/witty-frogs-cheat.md new file mode 100644 index 0000000000..2b5b81006a --- /dev/null +++ b/.changeset/witty-frogs-cheat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simpler fallback values diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d97642601f..e474e60e2b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 41d8017ce2..fc593c0d06 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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": [ diff --git a/documentation/docs/02-template-syntax/09-special-elements.md b/documentation/docs/02-template-syntax/09-special-elements.md index a46779c1cc..b2f6e1e134 100644 --- a/documentation/docs/02-template-syntax/09-special-elements.md +++ b/documentation/docs/02-template-syntax/09-special-elements.md @@ -185,7 +185,7 @@ The `` 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 diff --git a/documentation/docs/05-misc/04-custom-elements.md b/documentation/docs/05-misc/04-custom-elements.md index 46c1bb6f13..5cb3bbfbb1 100644 --- a/documentation/docs/05-misc/04-custom-elements.md +++ b/documentation/docs/05-misc/04-custom-elements.md @@ -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 `` 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: ""`. diff --git a/documentation/tutorial/16-special-elements/09-svelte-options/text.md b/documentation/tutorial/16-special-elements/09-svelte-options/text.md index 4c78d4d13e..2049365efc 100644 --- a/documentation/tutorial/16-special-elements/09-svelte-options/text.md +++ b/documentation/tutorial/16-special-elements/09-svelte-options/text.md @@ -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. diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1007ba88b1..7b1ab89d31 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -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 diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index 442a9e7684..f0ca476ff1 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -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% diff --git a/packages/svelte/messages/compile-errors/template.md b/packages/svelte/messages/compile-errors/template.md index 7920329c8b..affd44e943 100644 --- a/packages/svelte/messages/compile-errors/template.md +++ b/packages/svelte/messages/compile-errors/template.md @@ -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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 1f3cdb86e1..884145f783 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -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 } } diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 958c86a817..8b650ebf3c 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -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: .. } } }"); } /** diff --git a/packages/svelte/src/compiler/phases/1-parse/read/options.js b/packages/svelte/src/compiler/phases/1-parse/read/options.js index 7c83946336..a8ba7e216c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/options.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/options.js @@ -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); - // } } diff --git a/packages/svelte/src/compiler/phases/1-parse/state/element.js b/packages/svelte/src/compiler/phases/1-parse/state/element.js index bdadf41347..12e75400be 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -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' diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 824f784a6b..102091c78d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -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 }; } /** diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js index 2e09c756e3..ebf20d5aa6 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/EachBlock.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js index c00d521610..2e4595ec08 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Identifier.js @@ -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') diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js index d407d5afde..4d6cadebff 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/component.js @@ -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 — `` — + // 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} */ + 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); + } } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js index da1f06d7f6..c6151992bf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/function.js @@ -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 }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 82d49f0ad7..15c0de5df8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -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 }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts index 0284c82d1e..cae1a10940 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/client/types.d.ts @@ -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; - /** - * A map of `[name, node]` pairs, where `Identifier` nodes matching `name` - * will be replaced with `node` (e.g. `x` -> `$.get(x)`) - */ - readonly getters: Record 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; } export interface StateField { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 58fae325ef..d5e6420d5d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -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} 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} 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)))) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js index fefc11bf1f..afd2d50ba4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AssignmentExpression.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js index 1fcf110216..3cab0d7eec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Attribute.js @@ -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); } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js index 21f16f3449..f956a5cf30 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/AwaitBlock.js @@ -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( diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js index 611e5e570d..81bafcc18a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BindDirective.js @@ -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} */ diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js new file mode 100644 index 0000000000..502fbd471e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js @@ -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(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js index 10623ce3f5..bfff38338b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/ConstTag.js @@ -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) + }; } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js index a82677817a..fa19509c06 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/EachBlock.js @@ -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]; } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js index 1696d9213e..e7312d15aa 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Fragment.js @@ -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, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js index f223d748ca..ed8fefc6ba 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/FunctionDeclaration.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js index 262e44dfcb..87f56262a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LabeledStatement.js @@ -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); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js index 2423c6e50e..c455ea3219 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/LetDirective.js @@ -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)))) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js index dc7da67e16..9bd22b7eb4 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/OnDirective.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js new file mode 100644 index 0000000000..c222d74d6e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js @@ -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(); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 13ebb41d5b..890713f22a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -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, { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js index cdb7455447..b62a58c8d7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SnippetBlock.js @@ -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)))); } } } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBody.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBody.js index 6c7637adee..c046b21534 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBody.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteBody.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteDocument.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteDocument.js index e11c53fffb..bff32fa570 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteDocument.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteDocument.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js index 72174d1015..90e74259f1 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteElement.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteWindow.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteWindow.js index 57ac3ee47b..a29fec60bf 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteWindow.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SvelteWindow.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js index 5f8685eb3b..91383a5679 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/UpdateExpression.js @@ -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; } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 5dfcec4847..914db108e3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index d498ef10ea..44f943ddec 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js @@ -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} */ + const states = { + default: { + ...context.state, + scope: node.metadata.scopes.default, + transform: { ...context.state.transform } + } + }; + /** @type {Record} */ 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 } + } ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js new file mode 100644 index 0000000000..5a03fd9be4 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js @@ -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) + ); + } + }; + } + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 06563cd033..7d5c61c056 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -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); - } -} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js new file mode 100644 index 0000000000..9510715a91 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js @@ -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')))]) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js index 9939621890..e66cdcb091 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/function.js @@ -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, diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js new file mode 100644 index 0000000000..6a0be55949 --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js @@ -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); + } + } +} diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js index b6b11c6909..b11c2ed224 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/utils.js @@ -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} 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) + ) ) ); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js index c349f64b3d..e92d54c3b7 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/AssignmentExpression.js @@ -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); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js index acd9330698..390fde6811 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/SvelteElement.js @@ -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)]); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js index f7809af43a..bd8a8f435e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/VariableDeclaration.js @@ -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)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js index 60b3498510..941088228a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/component.js @@ -20,6 +20,15 @@ export function build_inline_component(node, expression, context) { /** @type {Record} */ 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} */ 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] + } ) ); diff --git a/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js new file mode 100644 index 0000000000..ce7595a06e --- /dev/null +++ b/packages/svelte/src/compiler/phases/3-transform/shared/assignments.js @@ -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()) + ); +} diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 2b35fa67d7..f71c4120f9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -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; diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5eba0d3cf2..633f326080 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -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} */ 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} */ - 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} @@ -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' - ) - ) { - //
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(); diff --git a/packages/svelte/src/compiler/phases/types.d.ts b/packages/svelte/src/compiler/phases/types.d.ts index a223834d06..96c0f65eb7 100644 --- a/packages/svelte/src/compiler/phases/types.d.ts +++ b/packages/svelte/src/compiler/phases/types.d.ts @@ -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; }; } diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 47dc9d7eb6..f7d111a267 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -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. diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 373822a873..7f2e808ade 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -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; dynamic: boolean; }; } @@ -311,6 +313,9 @@ export interface SvelteComponent extends BaseElement { type: 'SvelteComponent'; name: 'svelte:component'; expression: Expression; + metadata: { + scopes: Record; + }; } 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; + }; } interface SvelteWindow extends BaseElement { diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index aef6c270a9..47bbdb945f 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -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); +} diff --git a/packages/svelte/src/compiler/utils/slot.js b/packages/svelte/src/compiler/utils/slot.js new file mode 100644 index 0000000000..1c05ec8ee6 --- /dev/null +++ b/packages/svelte/src/compiler/utils/slot.js @@ -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; +} diff --git a/packages/svelte/src/internal/client/dom/css.js b/packages/svelte/src/internal/client/dom/css.js index 1cccb42b3e..dbf862543f 100644 --- a/packages/svelte/src/internal/client/dom/css.js +++ b/packages/svelte/src/internal/client/dom/css.js @@ -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); } diff --git a/packages/svelte/src/internal/client/dom/elements/custom-element.js b/packages/svelte/src/internal/client/dom/elements/custom-element.js index 1c31d0a394..a0483e9ea5 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -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 }); + } + } } }); }); diff --git a/packages/svelte/src/internal/client/dom/elements/events.js b/packages/svelte/src/internal/client/dom/elements/events.js index c01055d016..2c9a63436d 100644 --- a/packages/svelte/src/internal/client/dom/elements/events.js +++ b/packages/svelte/src/internal/client/dom/elements/events.js @@ -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); }); diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 5d0005b2c1..f747e2f402 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -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; diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 5f0880a676..82ad0df613 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -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; } } diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index e0d6b73c39..20a338fd5b 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -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 diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index edd50bff32..dc51d3caae 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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, diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index 2b9f87dfbf..2063b96cb0 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -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 diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index cf2e7cc11d..9fd238c866 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -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; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index b822f8d4ec..81c3e4d743 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -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; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 779548a65c..8939244cf8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -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} */ export function exclude_from_object(obj, keys) { - obj = { ...obj }; - let key; - for (key of keys) { - delete obj[key]; - } - return obj; -} + /** @type {Record} */ + 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} fallback lazy because could contain side effects - * @returns {Promise} - */ -export async function value_or_fallback_async(value, fallback) { - return value === undefined ? fallback() : value; + return result; } /** diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 8ab7e52162..422784d765 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -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} fallback lazy because could contain side effects - * @returns {Promise} - */ -export async function value_or_fallback_async(value, fallback) { - return value === undefined ? fallback() : value; -} - /** * @param {Payload} payload * @param {void | ((payload: Payload, props: Record) => 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, diff --git a/packages/svelte/src/internal/shared/utils.js b/packages/svelte/src/internal/shared/utils.js index 5285b2c2e1..f32af747a4 100644 --- a/packages/svelte/src/internal/shared/utils.js +++ b/packages/svelte/src/internal/shared/utils.js @@ -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; +} diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index a11f3f9cee..ff145f3fb2 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -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; diff --git a/packages/svelte/src/reactivity/date.js b/packages/svelte/src/reactivity/date.js index 1b64097e73..a1fb73c5f5 100644 --- a/packages/svelte/src/reactivity/date.js +++ b/packages/svelte/src/reactivity/date.js @@ -32,6 +32,14 @@ export class SvelteDate extends Date { if (method.startsWith('get') || method.startsWith('to')) { // @ts-ignore proto[method] = function (...args) { + // don't memoize if there are arguments + // @ts-ignore + if (args.length > 0) { + get(this.#time); + // @ts-ignore + return date_proto[method].apply(this, args); + } + var d = this.#deriveds.get(method); if (d === undefined) { diff --git a/packages/svelte/src/reactivity/date.test.ts b/packages/svelte/src/reactivity/date.test.ts index d1732d26b1..e2702d9ff6 100644 --- a/packages/svelte/src/reactivity/date.test.ts +++ b/packages/svelte/src/reactivity/date.test.ts @@ -555,6 +555,30 @@ test('Date fine grained tests', () => { cleanup(); }); +test('Date.toLocaleString', () => { + const date = new SvelteDate(initial_date); + + const log: any = []; + + const cleanup = effect_root(() => { + render_effect(() => { + log.push(date.toLocaleString(undefined, { month: 'long', year: 'numeric' })); + }); + render_effect(() => { + log.push(date.toLocaleString(undefined, { month: 'long' })); + }); + }); + + flushSync(); + + assert.deepEqual(log, [ + initial_date.toLocaleString(undefined, { month: 'long', year: 'numeric' }), + initial_date.toLocaleString(undefined, { month: 'long' }) + ]); + + cleanup(); +}); + test('Date.instanceOf', () => { assert.equal(new SvelteDate() instanceof Date, true); }); diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c70a430439..af0b9af8a7 100644 --- a/packages/svelte/src/version.js +++ b/packages/svelte/src/version.js @@ -6,5 +6,5 @@ * https://svelte.dev/docs/svelte-compiler#svelte-version * @type {string} */ -export const VERSION = '5.0.0-next.208'; +export const VERSION = '5.0.0-next.216'; export const PUBLIC_VERSION = '5'; diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_config.js b/packages/svelte/tests/hydration/samples/safari-borking/_config.js new file mode 100644 index 0000000000..cf22ff2c85 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + expect_hydration_error: true +}); diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_expected.html b/packages/svelte/tests/hydration/samples/safari-borking/_expected.html new file mode 100644 index 0000000000..3f23afe0bf --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_expected.html @@ -0,0 +1 @@ +

call +636-555-3226 now

diff --git a/packages/svelte/tests/hydration/samples/safari-borking/_override.html b/packages/svelte/tests/hydration/samples/safari-borking/_override.html new file mode 100644 index 0000000000..fcbb5c1830 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/_override.html @@ -0,0 +1 @@ +

call +636-555-3226 now

diff --git a/packages/svelte/tests/hydration/samples/safari-borking/main.svelte b/packages/svelte/tests/hydration/samples/safari-borking/main.svelte new file mode 100644 index 0000000000..1385fbb2a4 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/safari-borking/main.svelte @@ -0,0 +1,5 @@ + + +

{message}

diff --git a/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html new file mode 100644 index 0000000000..e728b682d0 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html @@ -0,0 +1,2 @@ + + hello diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index d592a65de3..7247778d0f 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -113,15 +113,16 @@ const { test, run } = suite(async (config, cwd) => { throw new Error(`Unexpected errors: ${errors.join('\n')}`); } - if (!override) { - const expected = read(`${cwd}/_expected.html`) ?? rendered.html; - flushSync(); - assert.equal(target.innerHTML.trim(), expected.trim()); - } + flushSync(); + + const normalize = (string: string) => string.trim().replace(/\r\n/g, '\n'); + + const expected = read(`${cwd}/_expected.html`) ?? rendered.html; + assert.equal(normalize(target.innerHTML), normalize(expected)); if (rendered.head) { const expected = read(`${cwd}/_expected_head.html`) ?? rendered.head; - assert.equal(head.innerHTML.trim(), expected.trim()); + assert.equal(normalize(head.innerHTML), normalize(expected)); } if (config.snapshot) { diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/_config.js new file mode 100644 index 0000000000..84eb9bc13a --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/_config.js @@ -0,0 +1,20 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + let changed = false; + + target.innerHTML = ''; + + await tick(); // wait for element to upgrade + + target.addEventListener('change', () => { + changed = true; + }); + + await tick(); // wait for effect + + assert.equal(changed, true); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/main.svelte new file mode 100644 index 0000000000..0f639d6e57 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/escaped-css/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/escaped-css/_config.js index 0cbfd63821..892da67df7 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/escaped-css/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/escaped-css/_config.js @@ -5,6 +5,7 @@ export default test({ async test({ assert, target }) { target.innerHTML = ''; await tick(); + await tick(); /** @type {any} */ const ce = target.querySelector('custom-element'); const icon = ce.shadowRoot.querySelector('.icon'); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js index 0c2625dc20..bf09e864fa 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/_config.js @@ -14,11 +14,13 @@ export default test({ await tick(); - el.shadowRoot.querySelector('button').click(); - assert.deepEqual(events, ['greeting', 'hello']); + el.shadowRoot.querySelectorAll('button')[0].click(); + el.shadowRoot.querySelectorAll('button')[1].click(); + assert.deepEqual(events, ['greeting', 'hello', 'greeting', 'welcome']); el.removeEventListener('greeting', handle_evt); - el.shadowRoot.querySelector('button').click(); - assert.deepEqual(events, ['greeting', 'hello']); + el.shadowRoot.querySelectorAll('button')[0].click(); + el.shadowRoot.querySelectorAll('button')[1].click(); + assert.deepEqual(events, ['greeting', 'hello', 'greeting', 'welcome']); } }); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte index 080e7a2740..2eddf77e9b 100644 --- a/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/host-rune/main.svelte @@ -4,6 +4,11 @@ function greet(greeting) { $host().dispatchEvent(new CustomEvent('greeting', { detail: greeting })) } + + function welcome() { + $host().dispatchEvent(new CustomEvent('greeting', { detail: 'welcome' })) + } + diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/_config.js new file mode 100644 index 0000000000..050ae77520 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + warnings: [], + async test({ assert, target, componentCtor }) { + customElements.define('no-tag', componentCtor.element); + target.innerHTML = ''; + await tick(); + + /** @type {any} */ + const el = target.querySelector('no-tag'); + const h1 = el.querySelector('h1'); + + assert.equal(el.shadowRoot, null); + assert.equal(h1.textContent, 'Hello world!'); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/main.svelte new file mode 100644 index 0000000000..054efab1dc --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/main.svelte @@ -0,0 +1,7 @@ + + + + +

Hello {name}!

diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/_config.js b/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/_config.js new file mode 100644 index 0000000000..da451f6230 --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/_config.js @@ -0,0 +1,37 @@ +import { flushSync } from 'svelte'; +import { test } from '../../assert'; +const tick = () => Promise.resolve(); + +export default test({ + async test({ assert, target }) { + target.innerHTML = ''; + await tick(); + await tick(); + + /** @type {any} */ + const el = target.querySelector('custom-element'); + const button = el.shadowRoot.querySelector('button'); + + assert.equal(button.textContent, '0'); + assert.equal(el.count, 0); + + button.click(); + + flushSync(); + + assert.equal(button.textContent, '1'); + assert.equal(el.count, 1); + + el.count = 0; + + assert.equal(button.textContent, '0'); + assert.equal(el.count, 0); + + button.click(); + + flushSync(); + + assert.equal(button.textContent, '1'); + assert.equal(el.count, 1); + } +}); diff --git a/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/main.svelte b/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/main.svelte new file mode 100644 index 0000000000..5aafc8eecf --- /dev/null +++ b/packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/main.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/_config.js b/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/_config.js index fc91ce3f6d..3915e2eda3 100644 --- a/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/_config.js @@ -9,18 +9,21 @@ export default test({ quote: 'q1', 'wrong-quote': 'wq1', 16: '16', + 17: '17', class: 'class' }, { quote: 'q2', 'wrong-quote': 'wq2', 16: 'sixteen', + 17: 'seventeen', class: 'glass' }, { quote: 'q3', 'wrong-quote': 'wq3', 16: 'seize', + 17: 'dix-sept', class: 'mass' } ] @@ -28,19 +31,19 @@ export default test({ }, html: ` -

Quote: q1, Wrong Quote: wq1, 16: 16

-

Quote: q2, Wrong Quote: wq2, 16: sixteen

-

Quote: q3, Wrong Quote: wq3, 16: seize

+

Quote: q1, Wrong Quote: wq1, 16: 16, 17: 17

+

Quote: q2, Wrong Quote: wq2, 16: sixteen, 17: seventeen

+

Quote: q3, Wrong Quote: wq3, 16: seize, 17: dix-sept

`, test({ assert, component, target }) { component.objectsArray = [ - { quote: 'new-quote', 'wrong-quote': 'wq4', 16: 'ten+six', role: 'role' } + { quote: 'new-quote', 'wrong-quote': 'wq4', 16: 'ten+six', 17: 'ten+seven', role: 'role' } ]; assert.htmlEqual( target.innerHTML, ` -

Quote: new-quote, Wrong Quote: wq4, 16: ten+six

+

Quote: new-quote, Wrong Quote: wq4, 16: ten+six, 17: ten+seven

` ); } diff --git a/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/main.svelte b/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/main.svelte index 943b3b7940..84a4f1b4fe 100644 --- a/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/each-block-destructured-object-literal-rest/main.svelte @@ -2,6 +2,6 @@ export let objectsArray; -{#each objectsArray as { "quote": quotedProp, "wrong-quote": wrongQuote, 16: sixteen, ...props } } -

Quote: {quotedProp}, Wrong Quote: {wrongQuote}, 16: {sixteen}

+{#each objectsArray as { "quote": quotedProp, "wrong-quote": wrongQuote, 16: sixteen, [10 + 7]: seventeen, ...props }} +

Quote: {quotedProp}, Wrong Quote: {wrongQuote}, 16: {sixteen}, 17: {seventeen}

{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js new file mode 100644 index 0000000000..a00605a634 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` + + + +

1, 2, 3

+ `, + + test({ assert, target }) { + let buttons = target.querySelectorAll('button'); + + flushSync(() => buttons[2].click()); + + assert.htmlEqual( + target.innerHTML, + ` + + + +

1, 2, 4

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte new file mode 100644 index 0000000000..0abded02ff --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte @@ -0,0 +1,9 @@ + + +{#each arr as n} + +{/each} + +

{arr.join(', ')}

diff --git a/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/App.svelte b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/App.svelte new file mode 100644 index 0000000000..37825576e5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/App.svelte @@ -0,0 +1,10 @@ + + + + +{JSON.stringify($$restProps)} diff --git a/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/_config.js b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/_config.js new file mode 100644 index 0000000000..853af731f9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/_config.js @@ -0,0 +1,15 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `\n{"a":1,"b":2}`, + + test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + btn1.click(); + flushSync(); + + assert.htmlEqual(target.innerHTML, `\n{"b":2}`); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/main.svelte b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/main.svelte new file mode 100644 index 0000000000..461848a3b3 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/rest-props-delete/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-store/Child.svelte b/packages/svelte/tests/runtime-runes/samples/binding-property-store/Child.svelte new file mode 100644 index 0000000000..c1bd5ff3ed --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-store/Child.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-store/_config.js b/packages/svelte/tests/runtime-runes/samples/binding-property-store/_config.js new file mode 100644 index 0000000000..e93067eb9d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-store/_config.js @@ -0,0 +1,11 @@ +import { test } from '../../test'; + +export default test({ + mode: ['client'], + compileOptions: { + dev: true + }, + async test({ warnings, assert }) { + assert.deepEqual(warnings, []); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/binding-property-store/main.svelte b/packages/svelte/tests/runtime-runes/samples/binding-property-store/main.svelte new file mode 100644 index 0000000000..206d0ceb44 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/binding-property-store/main.svelte @@ -0,0 +1,12 @@ + + + + +

{$a.value}

+

{$b.nested.value}

diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-destructured/_config.js index 059c693802..24f9163fd2 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-destructured/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: `true 1 2 baz` + html: `true 1 2 baz 1 2 3` }); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-destructured/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-destructured/main.svelte index 99892cd748..73d2cca23f 100644 --- a/packages/svelte/tests/runtime-runes/samples/derived-destructured/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/derived-destructured/main.svelte @@ -1,6 +1,9 @@ -{foo} {a} {b} {baz} +{foo} {a} {b} {baz} {d} {e} {f} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/_config.js b/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/_config.js new file mode 100644 index 0000000000..ae38cafd69 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `\n0`, + + mode: ['client'], + + test({ assert, target }) { + const btn = target.querySelector('button'); + + btn?.click(); + + assert.throws( + flushSync, + 'derived_references_self\nA derived value cannot reference itself recursively' + ); + + assert.htmlEqual(target.innerHTML, `\n0`); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/main.svelte b/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/main.svelte new file mode 100644 index 0000000000..4ccb5e2d47 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/main.svelte @@ -0,0 +1,11 @@ + + + + +{even} diff --git a/packages/svelte/tests/runtime-runes/samples/dot-notation-component/_config.js b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/_config.js new file mode 100644 index 0000000000..3b44dd63ca --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '

hello

' +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dot-notation-component/child.svelte b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/child.svelte new file mode 100644 index 0000000000..6e7c1e6ac8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/child.svelte @@ -0,0 +1 @@ +

hello

diff --git a/packages/svelte/tests/runtime-runes/samples/dot-notation-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/main.svelte new file mode 100644 index 0000000000..12e9d22b36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dot-notation-component/main.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler-2/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-2/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler-2/_config.js rename to packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-2/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-2/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler-2/main.svelte rename to packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-2/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/Button.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/Button.svelte new file mode 100644 index 0000000000..8fcd7c705d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/Button.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/_config.js new file mode 100644 index 0000000000..b87e8c4ce1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/_config.js @@ -0,0 +1,34 @@ +import { test } from '../../test'; + +export default test({ + test({ assert, logs, target }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + + b2?.click(); + b2?.click(); + b3?.click(); + b3?.click(); + + b1?.click(); + + b2?.click(); + b2?.click(); + b3?.click(); + b3?.click(); + + assert.deepEqual(logs, [ + 'creating handler (1)', + 1, + 2, + 'creating handler (1)', + 3, + 4, + 'creating handler (2)', + 6, + 8, + 'creating handler (2)', + 10, + 12 + ]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/main.svelte new file mode 100644 index 0000000000..2f7bb13043 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/main.svelte @@ -0,0 +1,27 @@ + + + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler/_config.js b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler/_config.js similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler/_config.js rename to packages/svelte/tests/runtime-runes/samples/dynamic-event-handler/_config.js diff --git a/packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler/main.svelte b/packages/svelte/tests/runtime-runes/samples/dynamic-event-handler/main.svelte similarity index 100% rename from packages/svelte/tests/runtime-runes/samples/dynamic-element-event-handler/main.svelte rename to packages/svelte/tests/runtime-runes/samples/dynamic-event-handler/main.svelte diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/_config.js new file mode 100644 index 0000000000..3dfeaa2952 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, target }) { + const [btn1, btn2, btn3] = target.querySelectorAll('button'); + + flushSync(() => btn3.click()); + assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 1'); + + flushSync(() => btn2.click()); + flushSync(() => btn3.click()); + assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 0'); + + flushSync(() => btn1.click()); + flushSync(() => btn3.click()); + assert.htmlEqual(/** @type {string} */ (btn3.textContent), 'clicks: 1'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/main.svelte new file mode 100644 index 0000000000..9de44f23d9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/main.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/packages/svelte/tests/runtime-runes/samples/member-expression-component/Row.svelte b/packages/svelte/tests/runtime-runes/samples/member-expression-component/Row.svelte new file mode 100644 index 0000000000..9169cbb6da --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/member-expression-component/Row.svelte @@ -0,0 +1,5 @@ + + +{id} diff --git a/packages/svelte/tests/runtime-runes/samples/member-expression-component/_config.js b/packages/svelte/tests/runtime-runes/samples/member-expression-component/_config.js new file mode 100644 index 0000000000..e15157dbcc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/member-expression-component/_config.js @@ -0,0 +1,16 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: ` 012`, + + async test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => button?.click()); + assert.htmlEqual( + target.innerHTML, + ` 210` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/member-expression-component/main.svelte b/packages/svelte/tests/runtime-runes/samples/member-expression-component/main.svelte new file mode 100644 index 0000000000..647fda6bad --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/member-expression-component/main.svelte @@ -0,0 +1,13 @@ + + + + +{#each items as item (item.id)} + +{/each} diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js new file mode 100644 index 0000000000..ed0ead960b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + dev: true + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected.html b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected.html new file mode 100644 index 0000000000..70b23b87bc --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected.html @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html new file mode 100644 index 0000000000..27c37f693b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte new file mode 100644 index 0000000000..a152dd2b79 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte @@ -0,0 +1,3 @@ + + + diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 9e537b7659..d14d5fe964 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -676,4 +676,21 @@ describe('signals', () => { assert.equal(d.deps?.length, 1); }; }); + + test('unowned deriveds correctly update', () => { + return () => { + const arr1 = proxy<{ a: number }[]>([]); + const arr2 = proxy([]); + const combined = derived(() => [...arr1, ...arr2]); + const derived_length = derived(() => $.get(combined).length); + + assert.deepEqual($.get(combined), []); + assert.equal($.get(derived_length), 0); + + arr1.push({ a: 1 }); + + assert.deepEqual($.get(combined), [{ a: 1 }]); + assert.equal($.get(derived_length), 1); + }; + }); }); diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js new file mode 100644 index 0000000000..9400b52718 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js @@ -0,0 +1,12 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal/client"; + +let a = $.source(1); +let b = $.source(2); + +export function update(array) { + ( + $.set(a, $.proxy(array[0])), + $.set(b, $.proxy(array[1])) + ); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js new file mode 100644 index 0000000000..846ed48458 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js @@ -0,0 +1,9 @@ +/* index.svelte.js generated by Svelte VERSION */ +import * as $ from "svelte/internal/server"; + +let a = 1; +let b = 2; + +export function update(array) { + [a, b] = array; +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js new file mode 100644 index 0000000000..9c0da7558a --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js @@ -0,0 +1,6 @@ +let a = $state(1); +let b = $state(2); + +export function update(array) { + [a, b] = array; +} diff --git a/packages/svelte/tests/validator/samples/tag-non-string/errors.json b/packages/svelte/tests/validator/samples/tag-non-string/errors.json index b74358966c..71f8df4d00 100644 --- a/packages/svelte/tests/validator/samples/tag-non-string/errors.json +++ b/packages/svelte/tests/validator/samples/tag-non-string/errors.json @@ -1,7 +1,7 @@ [ { "code": "svelte_options_invalid_customelement", - "message": "\"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: .. } } }", + "message": "\"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: .. } } }", "start": { "line": 1, "column": 16 diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 8212c52cc0..4307b5c267 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -920,7 +920,6 @@ declare module 'svelte/compiler' { * - `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' @@ -933,8 +932,7 @@ declare module 'svelte/compiler' { | 'each' | 'snippet' | 'store_sub' - | 'legacy_reactive' - | 'legacy_reactive_import'; + | 'legacy_reactive'; declaration_kind: DeclarationKind; /** * What the value was initialized with. @@ -1535,7 +1533,7 @@ declare module 'svelte/compiler' { namespace?: Namespace; css?: 'injected'; customElement?: { - tag: string; + tag?: string; shadow?: 'open' | 'none'; props?: Record< string, @@ -1672,10 +1670,10 @@ declare module 'svelte/compiler' { type DelegatedEvent = | { - type: 'hoistable'; + hoisted: true; function: ArrowFunctionExpression | FunctionExpression | FunctionDeclaration; } - | { type: 'non-hoistable' }; + | { hoisted: false }; /** A `style:` directive */ interface StyleDirective extends BaseNode { @@ -1733,6 +1731,7 @@ declare module 'svelte/compiler' { interface Component extends BaseElement { type: 'Component'; metadata: { + scopes: Record; dynamic: boolean; }; } @@ -1769,6 +1768,9 @@ declare module 'svelte/compiler' { type: 'SvelteComponent'; name: 'svelte:component'; expression: Expression; + metadata: { + scopes: Record; + }; } interface SvelteDocument extends BaseElement { @@ -1814,6 +1816,9 @@ declare module 'svelte/compiler' { interface SvelteSelf extends BaseElement { type: 'SvelteSelf'; name: 'svelte:self'; + metadata: { + scopes: Record; + }; } interface SvelteWindow extends BaseElement { diff --git a/playgrounds/demo/index.html b/playgrounds/demo/index.html index fae74ccb1e..512b5426a9 100644 --- a/playgrounds/demo/index.html +++ b/playgrounds/demo/index.html @@ -20,9 +20,6 @@ const component = render(App, { target: document.getElementById('root') }); - - // @ts-ignore - window.unmount = () => unmount(component); diff --git a/playgrounds/demo/ssr-dev.js b/playgrounds/demo/ssr-dev.js index 617b49e652..65390b70ca 100644 --- a/playgrounds/demo/ssr-dev.js +++ b/playgrounds/demo/ssr-dev.js @@ -27,7 +27,9 @@ polka() const html = transformed_template .replace(``, head) - .replace(``, body); + .replace(``, body) + // check that Safari doesn't break hydration + .replaceAll('+636-555-3226', '+636-555-3226'); res.writeHead(200, { 'Content-Type': 'text/html' }).end(html); }) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index 0b72c18f46..9c48a6c97b 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -63,11 +63,18 @@ for (const generate of /** @type {const} */ (['client', 'server'])) { runes: argv.values.runes }); + for (const warning of compiled.warnings) { + console.warn(warning.code); + console.warn(warning.frame); + } + fs.writeFileSync( output_js, compiled.js.code + '\n//# sourceMappingURL=' + path.basename(output_map) ); + fs.writeFileSync(output_map, compiled.js.map.toString()); + if (compiled.css) { fs.writeFileSync(output_css, compiled.css.code); } diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md index 95469ccbaf..7b589785d7 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md @@ -161,6 +161,16 @@ Svelte now use Mutation Observers instead of IFrames to measure dimensions for ` Content inside component tags becomes a [snippet prop](/docs/snippets) called `children`. You cannot have a separate prop by that name. +## Dot notation indicates a component + +In Svelte 4, `` would create an element with a tag name of `"foo.bar"`. In Svelte 5, `foo.bar` is treated as a component instead. This is particularly useful inside `each` blocks: + +```svelte +{#each items as item} + +{/each} +``` + ## Breaking changes in runes mode Some breaking changes only apply once your component is in runes mode.