From 5a072198362364291e0d5c9a042feeace67b0cc9 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 25 Jul 2024 16:30:52 +0100 Subject: [PATCH 001/196] fix: add missing hydration mismatch call-site (#12604) * fix: add missing hydration mismatch call-site * add changeset * Update playgrounds/demo/index.html Co-authored-by: Conduitry --------- Co-authored-by: Conduitry --- .changeset/hip-stingrays-teach.md | 5 +++++ packages/svelte/src/internal/client/dom/hydration.js | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .changeset/hip-stingrays-teach.md diff --git a/.changeset/hip-stingrays-teach.md b/.changeset/hip-stingrays-teach.md new file mode 100644 index 0000000000..4f82007fbb --- /dev/null +++ b/.changeset/hip-stingrays-teach.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add missing hydration mismatch call-site diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 8b03fe7f9e..5f0880a676 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,6 +1,12 @@ /** @import { TemplateNode } from '#client' */ -import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../constants.js'; +import { + HYDRATION_END, + HYDRATION_ERROR, + HYDRATION_START, + HYDRATION_START_ELSE +} from '../../../constants.js'; +import * as w from '../warnings.js'; /** * Use this variable to guard everything related to hydration code so it can be treeshaken out @@ -28,6 +34,10 @@ export function set_hydrate_node(node) { } export function hydrate_next() { + if (hydrate_node === null) { + w.hydration_mismatch(); + throw HYDRATION_ERROR; + } return (hydrate_node = /** @type {TemplateNode} */ (hydrate_node.nextSibling)); } From db7652116ec1c9c1962861781c6ca978baffef5c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Jul 2024 12:01:46 -0400 Subject: [PATCH 002/196] chore: tweak `$state.is` warning docs a bit more (#12605) --- .../messages/client-warnings/warnings.md | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/svelte/messages/client-warnings/warnings.md b/packages/svelte/messages/client-warnings/warnings.md index 695776dd60..cae88b0bef 100644 --- a/packages/svelte/messages/client-warnings/warnings.md +++ b/packages/svelte/messages/client-warnings/warnings.md @@ -42,22 +42,26 @@ > Reactive `$state(...)` proxies and the values they proxy have different identities. Because of this, comparisons with `%operator%` will produce unexpected results. Consider using `$state.is(a, b)` instead%details% -`$state(...)` does create a proxy of the value it is passed. Therefore, the resulting object has a different identity and equality checks will always return `false`: +`$state(...)` creates a [proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) of the value it is passed. The proxy and the value have different identities, meaning equality checks will always return `false`: ```svelte ``` -Most of the time this will not be a problem in practise because it is very rare to keep the original value around to later compare it against a state value. In case it happens, Svelte will warn you about it in development mode and suggest to use `$state.is` instead, which is able to unwrap the proxy and compare the original values: +In the rare case that you need to compare them, you can use `$state.is`, which unwraps proxies: ```svelte ``` + +During development, Svelte will warn you when comparing values with proxies. From e2f17c8a7b93287f420f5e1af022374aae15c472 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Jul 2024 12:05:06 -0400 Subject: [PATCH 003/196] chore: tweak xmlns docs (#12606) --- documentation/docs/02-template-syntax/09-special-elements.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/docs/02-template-syntax/09-special-elements.md b/documentation/docs/02-template-syntax/09-special-elements.md index 3baa652185..a46779c1cc 100644 --- a/documentation/docs/02-template-syntax/09-special-elements.md +++ b/documentation/docs/02-template-syntax/09-special-elements.md @@ -64,7 +64,7 @@ If `this` is the name of a [void element](https://developer.mozilla.org/en-US/do Foo ``` -Svelte tries its best to infer the correct namespace from its surroundings, but that's not always possible to analyze statically. You can give a hint by adding a `xmlns` attribute: +Svelte tries its best to infer the correct namespace from the element's surroundings, but it's not always possible. You can make it explicit with an `xmlns` attribute: ```svelte From 13d86e901913ba0b953b547be3f06b2c796e57d6 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:04:07 +0200 Subject: [PATCH 004/196] feat: allow `:global` in more places (alternative) (#12560) * `div { :global { &.x { ... } } }` is equivalent to `div:global.x { ... }`, so the latter should be allowed, too * finalize * replace obsolete breaking change (which turned out to be a wrong change and was since reverted), add new breaking change note * changeset * regenerate types * Update sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md Co-authored-by: Rich Harris * always remove descendant selector before global * error on lone `:global` with nested `&`, revert "remove spaces" rule * regenerate types * documentation * oops * switch to removing descendant combinator * fix * revert combinator validation relaxation * error on first global being modified * tweak docs * tweak error messages * Update documentation/docs/02-template-syntax/05-styles-and-classes.md Co-authored-by: Rich Harris * clarify * tweak messages * update tests * tweak docs * tweak `:global(...)` docs * tweak docs --------- Co-authored-by: Rich Harris --- .changeset/tricky-balloons-care.md | 5 + .../05-styles-and-classes.md | 47 +++++--- .../svelte/messages/compile-errors/style.md | 20 ++-- packages/svelte/src/compiler/errors.js | 38 +++--- .../phases/2-analyze/css/css-analyze.js | 109 ++++++++++++------ .../phases/2-analyze/css/css-prune.js | 11 +- .../compiler/phases/3-transform/css/index.js | 42 +++++-- packages/svelte/src/compiler/types/css.d.ts | 31 ++++- .../css-global-block-combinator-2/_config.js | 9 ++ .../css-global-block-combinator-2/main.svelte | 11 ++ .../css-global-block-combinator/_config.js | 4 +- .../css-global-block-combinator/main.svelte | 8 +- .../css-global-block-declaration/_config.js | 4 +- .../css-global-block-declaration/main.svelte | 10 ++ .../_config.js | 10 -- .../main.svelte | 8 -- .../css-global-block-modifier/main.svelte | 3 - .../css-global-block-multiple/_config.js | 2 +- .../css-global-modifier-start-1/_config.js | 9 ++ .../css-global-modifier-start-1/main.svelte | 11 ++ .../css-global-modifier-start-2/_config.js | 9 ++ .../css-global-modifier-start-2/main.svelte | 22 ++++ .../_config.js | 4 +- .../samples/css-global-modifier/main.svelte | 11 ++ .../tests/css/samples/global-block/_config.js | 8 +- .../css/samples/global-block/expected.css | 54 +++++++++ .../css/samples/global-block/input.svelte | 53 +++++++++ .../errors.json | 2 +- .../errors.json | 2 +- .../errors.json | 2 +- .../errors.json | 2 +- .../errors.json | 2 +- .../css-invalid-global-placement/errors.json | 2 +- .../css-invalid-global-selector-2/errors.json | 2 +- .../css-invalid-global-selector-3/errors.json | 2 +- .../css-invalid-global-selector-4/errors.json | 2 +- .../css-invalid-global-selector-5/errors.json | 2 +- .../css-invalid-global-selector-6/errors.json | 2 +- .../errors.json | 2 +- .../css-invalid-global-selector/errors.json | 2 +- .../errors.json | 2 +- packages/svelte/types/index.d.ts | 31 ++++- .../03-appendix/02-breaking-changes.md | 13 ++- 43 files changed, 483 insertions(+), 142 deletions(-) create mode 100644 .changeset/tricky-balloons-care.md create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-combinator-2/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/_config.js delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/main.svelte delete mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/main.svelte rename packages/svelte/tests/compiler-errors/samples/{css-global-block-modifier => css-global-modifier}/_config.js (56%) create mode 100644 packages/svelte/tests/compiler-errors/samples/css-global-modifier/main.svelte diff --git a/.changeset/tricky-balloons-care.md b/.changeset/tricky-balloons-care.md new file mode 100644 index 0000000000..936f3ecddf --- /dev/null +++ b/.changeset/tricky-balloons-care.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: allow `:global` in more places diff --git a/documentation/docs/02-template-syntax/05-styles-and-classes.md b/documentation/docs/02-template-syntax/05-styles-and-classes.md index 48cdeabce5..4a1497f2e6 100644 --- a/documentation/docs/02-template-syntax/05-styles-and-classes.md +++ b/documentation/docs/02-template-syntax/05-styles-and-classes.md @@ -25,31 +25,28 @@ This works by adding a class to affected elements, which is based on a hash of t ``` -## :global +## :global(...) -To apply styles to a selector globally, use the `:global(...)` modifier. +To apply styles to a single selector globally, use the `:global(...)` modifier: ```svelte ``` @@ -66,6 +63,30 @@ The `-global-` part will be removed when compiled, and the keyframe will then be ``` +## :global + +To apply styles to a group of selectors globally, create a `:global {...}` block: + +```svelte + +``` + +> The second example above could also be written as an equivalent `.a :global .b .c .d` selector, where everything after the `:global` is unscoped, though the nested form is preferred. + ## Nested style tags There should only be 1 top-level ` diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/_config.js index 1014cd34c2..3d701e10b7 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ error: { code: 'css_global_block_invalid_combinator', - message: 'A :global {...} block cannot follow a > combinator', - position: [12, 21] + message: 'A `:global` selector cannot follow a `>` combinator', + position: [54, 63] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/main.svelte index b3b6ebe6ac..b3af8b4e21 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/main.svelte +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-combinator/main.svelte @@ -1,3 +1,9 @@ diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/_config.js index 3f7a82b54c..776cb9c7d0 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ error: { code: 'css_global_block_invalid_declaration', - message: 'A :global {...} block can only contain rules, not declarations', - position: [24, 34] + message: 'A top-level `:global {...}` block can only contain rules, not declarations', + position: [109, 119] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/main.svelte index 6a1891ca29..d3374f2aa0 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/main.svelte +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-declaration/main.svelte @@ -1,5 +1,15 @@ diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/_config.js deleted file mode 100644 index 0eb061e41e..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/_config.js +++ /dev/null @@ -1,10 +0,0 @@ -import { test } from '../../test'; - -export default test({ - error: { - code: 'css_global_block_invalid_placement', - message: - 'A :global {...} block can only appear at the end of a selector sequence (did you mean to use :global(...) instead?)', - position: [50, 57] - } -}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/main.svelte deleted file mode 100644 index 15d721f54f..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-invalid-selector/main.svelte +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/main.svelte deleted file mode 100644 index 7d274e3894..0000000000 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/main.svelte +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js index 9f06599f7a..9ae4e758c4 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/css-global-block-multiple/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ error: { code: 'css_global_block_invalid_list', - message: 'A :global {...} block cannot be part of a selector list with more than one item', + message: 'A `:global` selector cannot be part of a selector list with more than one item', position: [9, 31] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/_config.js new file mode 100644 index 0000000000..9f58616ca7 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_modifier_start', + message: 'A `:global` selector can only be modified if it is a descendant of other selectors', + position: [75, 77] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/main.svelte new file mode 100644 index 0000000000..027874c7a7 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-1/main.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/_config.js new file mode 100644 index 0000000000..6d4b02adab --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'css_global_block_invalid_modifier_start', + message: 'A `:global` selector can only be modified if it is a descendant of other selectors', + position: [147, 148] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/main.svelte new file mode 100644 index 0000000000..5e5123a1c9 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier-start-2/main.svelte @@ -0,0 +1,22 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/_config.js b/packages/svelte/tests/compiler-errors/samples/css-global-modifier/_config.js similarity index 56% rename from packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/_config.js rename to packages/svelte/tests/compiler-errors/samples/css-global-modifier/_config.js index 00843fa217..3659ac1d44 100644 --- a/packages/svelte/tests/compiler-errors/samples/css-global-block-modifier/_config.js +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier/_config.js @@ -3,7 +3,7 @@ import { test } from '../../test'; export default test({ error: { code: 'css_global_block_invalid_modifier', - message: 'A :global {...} block cannot modify an existing selector', - position: [14, 21] + message: 'A `:global` selector cannot modify an existing selector', + position: [70, 77] } }); diff --git a/packages/svelte/tests/compiler-errors/samples/css-global-modifier/main.svelte b/packages/svelte/tests/compiler-errors/samples/css-global-modifier/main.svelte new file mode 100644 index 0000000000..0cb7273572 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/css-global-modifier/main.svelte @@ -0,0 +1,11 @@ + diff --git a/packages/svelte/tests/css/samples/global-block/_config.js b/packages/svelte/tests/css/samples/global-block/_config.js index 430b45fe37..bee0d7204d 100644 --- a/packages/svelte/tests/css/samples/global-block/_config.js +++ b/packages/svelte/tests/css/samples/global-block/_config.js @@ -7,14 +7,14 @@ export default test({ code: 'css_unused_selector', message: 'Unused CSS selector ".unused :global"', start: { - line: 16, + line: 69, column: 1, - character: 128 + character: 917 }, end: { - line: 16, + line: 69, column: 16, - character: 143 + character: 932 } } ] diff --git a/packages/svelte/tests/css/samples/global-block/expected.css b/packages/svelte/tests/css/samples/global-block/expected.css index 8bf0f0e596..3acc1b0212 100644 --- a/packages/svelte/tests/css/samples/global-block/expected.css +++ b/packages/svelte/tests/css/samples/global-block/expected.css @@ -1,3 +1,4 @@ + /* :global {*/ .x { color: green; @@ -10,6 +11,59 @@ } } + /* some css preprocessors de-nest :global blocks with a single child + (e.g turn `:global { div { ... } }` into `:global div { ... }`), + so we need to support it, too */ + div { + .y { + color: green; + } + } + + div.svelte-xyz p { + .y { + color: green; + } + } + + /* `div { :global { &.x { ...} } }` is allowed ... */ + div.svelte-xyz { + /* :global {*/ + &.x { + color: green; + } + /*}*/ + } + + /* ...wich is equivalent to `div :global { &.x { ...} }` ... */ + div.svelte-xyz { + &.x { + color: green; + } + } + + /* ...so `div :global.x` must be, too ... */ + div.svelte-xyz.x { + color: green; + } + + /* ...and therefore `div { :global.x { ... }` aswell */ + div.svelte-xyz { + &.x { + color: green; + } + } + + div.svelte-xyz { + &.x { + color: green; + } + } + + div.svelte-xyz:is(html.dark-mode *) { + color: green; + } + /* (unused) .unused :global { .z { color: red; diff --git a/packages/svelte/tests/css/samples/global-block/input.svelte b/packages/svelte/tests/css/samples/global-block/input.svelte index d05a6b7edc..2f2cdfcbd5 100644 --- a/packages/svelte/tests/css/samples/global-block/input.svelte +++ b/packages/svelte/tests/css/samples/global-block/input.svelte @@ -13,6 +13,59 @@ } } + /* some css preprocessors de-nest :global blocks with a single child + (e.g turn `:global { div { ... } }` into `:global div { ... }`), + so we need to support it, too */ + :global div { + .y { + color: green; + } + } + + div :global p { + .y { + color: green; + } + } + + /* `div { :global { &.x { ...} } }` is allowed ... */ + div { + :global { + &.x { + color: green; + } + } + } + + /* ...wich is equivalent to `div :global { &.x { ...} }` ... */ + div :global { + &.x { + color: green; + } + } + + /* ...so `div :global.x` must be, too ... */ + div :global.x { + color: green; + } + + /* ...and therefore `div { :global.x { ... }` aswell */ + div { + :global.x { + color: green; + } + } + + div { + & :global.x { + color: green; + } + } + + div :global:is(html.dark-mode *) { + color: green; + } + .unused :global { .z { color: red; diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json index cf556bf4c4..bab7b8a9e9 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-2/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_placement", - "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", + "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle", "start": { "line": 8, "column": 6 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-3/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-3/errors.json index 164daa5796..8ca06aa625 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-3/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-3/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_placement", - "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", + "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle", "start": { "line": 5, "column": 6 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-4/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-4/errors.json index 7b63fe4d7a..bf3dce8b7f 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-4/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-4/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector_list", - "message": ":global(...) must not contain type or universal selectors when used in a compound selector", + "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector", "start": { "line": 2, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-5/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-5/errors.json index fcc75beb5c..6260b09743 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-5/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-5/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector_list", - "message": ":global(...) must not contain type or universal selectors when used in a compound selector", + "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector", "start": { "line": 2, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement-6/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement-6/errors.json index e1bc38e0f1..1af01bce50 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement-6/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement-6/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector_list", - "message": ":global(...) must not contain type or universal selectors when used in a compound selector", + "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector", "start": { "line": 5, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-placement/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-placement/errors.json index 0b641f2115..484319ef06 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-placement/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-placement/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_placement", - "message": ":global(...) can be at the start or end of a selector sequence, but not in the middle", + "message": "`:global(...)` can be at the start or end of a selector sequence, but not in the middle", "start": { "line": 2, "column": 6 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-2/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-2/errors.json index d0be529197..d61411ee96 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-2/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-2/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 11, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-3/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-3/errors.json index d37ff8d5d1..722e8be23f 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-3/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-3/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 5, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-4/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-4/errors.json index c05bb647ad..9a20737c5d 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-4/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-4/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 2, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-5/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-5/errors.json index b1d79c25b8..5317179d5b 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-5/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-5/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 5, "column": 1 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-6/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-6/errors.json index b1d79c25b8..5317179d5b 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-6/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-6/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 5, "column": 1 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json index e9261e5e1c..406fd6129c 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector-list/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector_list", - "message": ":global(...) must not contain type or universal selectors when used in a compound selector", + "message": "`:global(...)` must not contain type or universal selectors when used in a compound selector", "start": { "line": 20, "column": 6 diff --git a/packages/svelte/tests/validator/samples/css-invalid-global-selector/errors.json b/packages/svelte/tests/validator/samples/css-invalid-global-selector/errors.json index 3d96fbc1ff..3d8ae68ec0 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-global-selector/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-global-selector/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_global_invalid_selector", - "message": ":global(...) must contain exactly one selector", + "message": "`:global(...)` must contain exactly one selector", "start": { "line": 11, "column": 5 diff --git a/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json index e7c461b699..5c75d86774 100644 --- a/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json +++ b/packages/svelte/tests/validator/samples/css-invalid-type-selector-placement/errors.json @@ -1,7 +1,7 @@ [ { "code": "css_type_selector_invalid_placement", - "message": ":global(...) must not be followed with a type selector", + "message": "`:global(...)` must not be followed by a type selector", "start": { "line": 17, "column": 14 diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index b393174e0f..9de214c847 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1322,17 +1322,32 @@ declare module 'svelte/compiler' { metadata: { parent_rule: null | Rule; has_local_selectors: boolean; + /** + * `true` if the rule contains a `:global` selector, and therefore everything inside should be unscoped + */ is_global_block: boolean; }; } + /** + * A list of selectors, e.g. `a, b, c {}` + */ export interface SelectorList extends BaseNode { type: 'SelectorList'; + /** + * The `a`, `b` and `c` in `a, b, c {}` + */ children: ComplexSelector[]; } + /** + * A complex selector, e.g. `a b c {}` + */ export interface ComplexSelector extends BaseNode { type: 'ComplexSelector'; + /** + * The `a`, `b` and `c` in `a b c {}` + */ children: RelativeSelector[]; metadata: { rule: null | Rule; @@ -1340,14 +1355,26 @@ declare module 'svelte/compiler' { }; } + /** + * A relative selector, e.g the `a` and `> b` in `a > b {}` + */ export interface RelativeSelector extends BaseNode { type: 'RelativeSelector'; + /** + * In `a > b`, `> b` forms one relative selector, and `>` is the combinator. `null` for the first selector. + */ combinator: null | Combinator; + /** + * The `b:is(...)` in `> b:is(...)` + */ selectors: SimpleSelector[]; metadata: { - /** :global(..) */ + /** + * `true` if the whole selector is unscoped, e.g. `:global(...)` or `:global` or `:global.x`. + * Selectors like `:global(...).x` are not considered global, because they still need scoping. + */ is_global: boolean; - /** :root, :host, ::view-transition */ + /** `:root`, `:host`, `::view-transition`, or selectors after a `:global` */ is_global_like: boolean; scoped: boolean; }; 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 e85ee2d529..5c475b7ec8 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 @@ -202,9 +202,18 @@ This is because the Svelte compiler treated the assignment to `foo.value` as an Assignments to destructured parts of a `@const` declaration are no longer allowed. It was an oversight that this was ever allowed. -### Stricter CSS `:global` selector validation +### :is(...) and :where(...) are scoped -Previously, a compound selector starting with a global modifier which has universal or type selectors (like `:global(span).foo`) was valid. In Svelte 5, this is a validation error instead. The reason is that in this selector the resulting CSS would be equivalent to one without `:global` - in other words, `:global` is ignored in this case. +Previously, Svelte did not analyse selectors inside `:is(...)` and `:where(...)`, effectively treating them as global. Svelte 5 analyses them in the context of the current component. As such, some selectors may now be treated as unused if they were relying on this treatment. To fix this, use `:global(...)` inside the `:is(...)/:where(...)` selectors. + +When using Tailwind's `@apply` directive, add a `:global` selector to preserve rules that use Tailwind-generated `:is(...)` selectors: + +```diff +- main { ++ main :global { + @apply bg-blue-100 dark:bg-blue-900 +} +``` ### CSS hash position no longer deterministic From ba5f5f38b1e865215b3fb1e9731ce0dd64193c52 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 25 Jul 2024 13:04:39 -0400 Subject: [PATCH 005/196] chore: tweak event delegation docs (#12607) --- .../docs/02-template-syntax/02-basic-markup.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/documentation/docs/02-template-syntax/02-basic-markup.md b/documentation/docs/02-template-syntax/02-basic-markup.md index a97f8b84e6..b8e2632319 100644 --- a/documentation/docs/02-template-syntax/02-basic-markup.md +++ b/documentation/docs/02-template-syntax/02-basic-markup.md @@ -110,18 +110,18 @@ Because events are just attributes, the same rules as for attributes apply: - you can spread them: `` - component events are just (callback) properties and don't need a separate concept -Timing-wise, event attributes always fire after events from bindings (e.g. `oninput` always fires after an update to `bind:value`). Under the hood, events are either listened to directly through `addEventListener`, or the event is delegated. +Timing-wise, event attributes always fire after events from bindings (e.g. `oninput` always fires after an update to `bind:value`). Under the hood, some event handlers are attached directly with `addEventListener`, while others are _delegated_. ### Event delegation -To reduce the memory footprint and increase performance, Svelte uses a technique called event delegation. This means that certain events are only listened to once at the application root, invoking a handler that then traverses the event call path and invokes listeners along the way. +To reduce memory footprint and increase performance, Svelte uses a technique called event delegation. This means that for certain events — see the list below — a single event listener at the application root takes responsibility for running any handlers on the event's path. -There are a few gotchas you need to be aware of when it comes to event delegation: +There are a few gotchas to be aware of: -- when you manually dispatch an event with the same name as one of the delegated ones, make sure to set the `{ bubbles: true }` option -- when listening to events programmatically (i.e. not through `
1
`, - async test({ assert, target, window }) { + async test({ assert, target, logs }) { const button = target.querySelector('button'); - const event = new window.MouseEvent('click'); - /** - * @type {any[]} - */ - const messages = []; - const log = console.log; - console.log = (msg) => messages.push(msg); - flushSync(() => { - // @ts-ignore - button.dispatchEvent(event); - }); + button?.click(); + flushSync(); - console.log = log; - assert.deepEqual(messages, ['afterUpdate', 'onDestroy']); + assert.deepEqual(logs, ['afterUpdate', 'onDestroy']); } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js index 848da78b2d..b6b601a96b 100644 --- a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js @@ -1,9 +1,6 @@ -// @ts-nocheck import { test } from '../../test'; export default test({ - skip: true, // TODO: needs fixing - get props() { return { things: [ @@ -27,7 +24,9 @@ export default test({ test({ assert, component, target, raf }) { let divs = target.querySelectorAll('div'); divs.forEach((div) => { + // @ts-expect-error div.getBoundingClientRect = function () { + // @ts-expect-error const index = [...this.parentNode.children].indexOf(this); const top = index * 30; @@ -49,13 +48,13 @@ export default test({ ]; divs = target.querySelectorAll('div'); - assert.ok(~divs[0].style.animation.indexOf('__svelte')); - assert.equal(divs[1].style.animation, ''); - assert.equal(divs[2].style.animation, ''); - assert.equal(divs[3].style.animation, ''); - assert.ok(~divs[4].style.animation.indexOf('__svelte')); + assert.ok(divs[0].getAnimations().length > 0); + assert.equal(divs[1].getAnimations().length, 0); + assert.equal(divs[2].getAnimations().length, 0); + assert.equal(divs[3].getAnimations().length, 0); + assert.ok(divs[4].getAnimations().length > 0); raf.tick(100); - assert.deepEqual([divs[0].style.animation, divs[4].style.animation], ['', '']); + assert.deepEqual([divs[0].getAnimations().length, divs[4].getAnimations().length], [0, 0]); } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js b/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js index c21b68106d..9364bd9f9e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - skip: true, // TODO: needs fixing + mode: ['client', 'hydrate'], html: ` 3 diff --git a/packages/svelte/tests/runtime-legacy/samples/dev-warning-readonly-computed/_config.js b/packages/svelte/tests/runtime-legacy/samples/dev-warning-readonly-computed/_config.js index 333ad48ba5..8606f012b4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dev-warning-readonly-computed/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/dev-warning-readonly-computed/_config.js @@ -1,8 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true, // TODO: needs fixing, error message is wrong - compileOptions: { dev: true }, @@ -17,7 +15,7 @@ export default test({ throw new Error('Expected an error'); } catch (err) { // @ts-ignore - assert.equal(err.message, "
: Cannot set read-only property 'foo'"); + assert.equal(err.message, 'Cannot set property foo of # which has only a getter'); } } }); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js index 12a0960d87..136dc92f54 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/_config.js @@ -1,11 +1,7 @@ import { test } from '../../test'; export default test({ - // This is skipped for now, because it's not clear how to make this work on client-side initial run: - // The anchor isn't connected to its parent at the time we can do a runtime check for the namespace, and we - // need the parent for this check. (this didn't work in Svelte 4 either) - skip: true, - html: '', + html: '', test({ assert, target }) { const svg = target.querySelector('svg'); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte index 00069f711c..9d190b7b2d 100644 --- a/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-element-svg-implicit-namespace/main.svelte @@ -5,6 +5,6 @@ export let path = 'path'; - + diff --git a/packages/svelte/tests/validator/samples/general-siblings-combinator-in-custom-element-selects-slot-fallback/_config.js b/packages/svelte/tests/validator/samples/general-siblings-combinator-in-custom-element-selects-slot-fallback/_config.js index 6bd925197f..307385cb57 100644 --- a/packages/svelte/tests/validator/samples/general-siblings-combinator-in-custom-element-selects-slot-fallback/_config.js +++ b/packages/svelte/tests/validator/samples/general-siblings-combinator-in-custom-element-selects-slot-fallback/_config.js @@ -1,7 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true, compileOptions: { customElement: true } diff --git a/packages/svelte/tests/validator/samples/siblings-combinator-in-custom-element-selects-slot-fallback/_config.js b/packages/svelte/tests/validator/samples/siblings-combinator-in-custom-element-selects-slot-fallback/_config.js index 6bd925197f..307385cb57 100644 --- a/packages/svelte/tests/validator/samples/siblings-combinator-in-custom-element-selects-slot-fallback/_config.js +++ b/packages/svelte/tests/validator/samples/siblings-combinator-in-custom-element-selects-slot-fallback/_config.js @@ -1,7 +1,6 @@ import { test } from '../../test'; export default test({ - skip: true, compileOptions: { customElement: true } From 32af9434bc588655a4d99bb1d330e8bb6d4fbc2a Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:05:49 +0200 Subject: [PATCH 014/196] fix: never set custom element props as attributes (#12622) * fix: never set custom element props as attributes * changeset --------- Co-authored-by: Rich Harris --- .changeset/fifty-toys-invite.md | 5 +++++ .../compiler/phases/3-transform/client/visitors/template.js | 1 + .../samples/attribute-custom-element-inheritance/_config.js | 3 +-- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/fifty-toys-invite.md diff --git a/.changeset/fifty-toys-invite.md b/.changeset/fifty-toys-invite.md new file mode 100644 index 0000000000..2dd19c3b04 --- /dev/null +++ b/.changeset/fifty-toys-invite.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: never set custom element props as attributes inside templates diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 3e8fff654f..0548d7a143 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2152,6 +2152,7 @@ export const template_visitors = { } if ( + !is_custom_element && attribute.name !== 'autofocus' && (attribute.value === true || is_text_attribute(attribute)) ) { diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/_config.js index 0b5365a9f1..396048c91a 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/_config.js @@ -1,8 +1,7 @@ import { test } from '../../test'; export default test({ - skip: true, // TODO: needs fixing - + mode: ['client'], html: ` Hello World! ` From c9e9e90659375942bbc2b658d257336257682413 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:07:02 +0200 Subject: [PATCH 015/196] fix: always close tags when namespace is foreign (#12623) --- .../phases/3-transform/server/visitors/RegularElement.js | 2 +- .../samples/attribute-casing-foreign-namespace/_config.js | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 66ad5ed72c..975038f3ac 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -95,7 +95,7 @@ export function RegularElement(node, context) { ); } - if (!VoidElements.includes(node.name) && namespace !== 'foreign') { + if (!VoidElements.includes(node.name) || namespace === 'foreign') { state.template.push(b.literal(``)); } diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/_config.js index 786e9308d0..5321756cef 100644 --- a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/_config.js @@ -1,7 +1,9 @@ import { test } from '../../test'; export default test({ - skip: true, // TODO: needs fixing + // TODO: needs fixing. Can only be fixed once we also support document.createElementNS-style creation of elements + // because the $.template('...') approach has no option to preserve attribute name casing + skip: true, html: ` From 1d17677131ceea4cfabce0d067bfc32a91625a60 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:45:08 -0400 Subject: [PATCH 016/196] Version Packages (next) (#12613) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index a21f3fb3e3..945ae7f965 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -180,6 +180,7 @@ "fifty-masks-give", "fifty-rice-wait", "fifty-steaks-float", + "fifty-toys-invite", "five-tigers-search", "flat-feet-visit", "flat-ghosts-fly", @@ -626,6 +627,7 @@ "strong-gifts-smoke", "strong-lemons-provide", "strong-pans-doubt", + "stupid-bottles-lay", "stupid-parents-crash", "sweet-bottles-check", "sweet-mangos-beg", @@ -720,6 +722,7 @@ "unlucky-trees-lick", "violet-mails-trade", "violet-mugs-behave", + "violet-otters-carry", "violet-pigs-jam", "warm-cherries-shake", "warm-waves-reply", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 4114b05835..f924237196 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.0.0-next.200 + +### Patch Changes + +- fix: never set custom element props as attributes inside templates ([#12622](https://github.com/sveltejs/svelte/pull/12622)) + +- feat: better code generation for `let:` directives in SSR mode ([#12611](https://github.com/sveltejs/svelte/pull/12611)) + +- fix: correctly update stores when reassigning with operator other than `=` ([#12614](https://github.com/sveltejs/svelte/pull/12614)) + ## 5.0.0-next.199 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 781e6ca207..698676fddd 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.199", + "version": "5.0.0-next.200", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 07b9d46c23..13072c37af 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.199'; +export const VERSION = '5.0.0-next.200'; export const PUBLIC_VERSION = '5'; From dba4dfaff8aae7602e85f6310764903c2eada345 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 27 Jul 2024 02:22:27 +0200 Subject: [PATCH 017/196] docs: add testing section (#12600) * docs: add testing section - explain component and rune testing using Vitest - explain e2e testing using Playwright closes #10244 closes #10650 * better examples * Update documentation/docs/05-misc/02-testing.md Co-authored-by: Rich Harris * Update documentation/docs/05-misc/02-testing.md Co-authored-by: Rich Harris * Update documentation/docs/05-misc/02-testing.md Co-authored-by: Rich Harris * fix * Update documentation/docs/05-misc/02-testing.md * Update documentation/docs/05-misc/02-testing.md Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * Update documentation/docs/05-misc/02-testing.md * we normally use single quotes * Apply suggestions from code review Co-authored-by: Rich Harris * more details on component testing * extract component testing into its own sub section, reorder a bit * fix code example * Update documentation/docs/05-misc/02-testing.md * Apply suggestions from code review Co-authored-by: Rich Harris --------- Co-authored-by: Rich Harris Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> --- documentation/docs/05-misc/02-testing.md | 218 ++++++++++++++++++++++- 1 file changed, 214 insertions(+), 4 deletions(-) diff --git a/documentation/docs/05-misc/02-testing.md b/documentation/docs/05-misc/02-testing.md index caf0b694ed..c1b9d538a4 100644 --- a/documentation/docs/05-misc/02-testing.md +++ b/documentation/docs/05-misc/02-testing.md @@ -2,7 +2,217 @@ title: Testing --- -- component testing basics -- rune testing basics -- vitest setup -- e2e +Testing helps you write and maintain your code and guard against regressions. Testing frameworks help you with that, allowing you to describe assertions or expectations about how your code should behave. Svelte is unopinionated about which testing framework you use — you can write unit tests, integration tests, and end-to-end tests using solutions like [Vitest](https://vitest.dev/), [Jasmine](https://jasmine.github.io/), [Cypress](https://www.cypress.io/) and [Playwright](https://playwright.dev/). + +## Unit and integration testing using Vitest + +Unit tests allow you to test small isolated parts of your code. Integration tests allow you to test parts of your application to see if they work together. If you're using Vite (including via SvelteKit), we recommend using [Vitest](https://vitest.dev/). + +To get started, install Vitest: + +```bash +npm install -D vitest +``` + +Then adjust your `vite.config.js`: + +```diff +/// file: vite.config.js +- import { defineConfig } from 'vite'; ++ import { defineConfig } from 'vitest/config'; + +export default defineConfig({ /* ... */ }) +``` + +You can now write unit tests for code inside your `.js/.ts` files: + +```js +/// file: multiplier.svelte.test.js +import { flushSync } from 'svelte'; +import { expect, test } from 'vitest'; +import { multiplier } from './multiplier.js'; + +test('Multiplier', () => { + let double = multiplier(0, 2); + + expect(double.value).toEqual(0); + + double.set(5); + + expect(double.value).toEqual(10); +}); +``` + +### Using runes inside your test files + +It is possible to use runes inside your test files. First ensure your bundler knows to route the file through the Svelte compiler before running the test by adding `.svelte` to the filename (e.g `multiplier.svelte.test.js`). After that, you can use runes inside your tests. + +```js +/// file: multiplier.svelte.test.js +import { flushSync } from 'svelte'; +import { expect, test } from 'vitest'; +import { multiplier } from './multiplier.svelte.js'; + +test('Multiplier', () => { + let count = $state(0); + let double = multiplier(() => count, 2); + + expect(double.value).toEqual(0); + + count = 5; + + expect(double.value).toEqual(10); +}); +``` + +If the code being tested uses effects, you need to wrap the test inside `$effect.root`: + +```js +/// file: logger.svelte.test.js +import { flushSync } from 'svelte'; +import { expect, test } from 'vitest'; +import { logger } from './logger.svelte.js'; + +test('Effect', () => { + const cleanup = $effect.root(() => { + let count = $state(0); + + // logger uses an $effect to log updates of its input + let log = logger(() => count); + + // effects normally run after a microtask, + // use flushSync to execute all pending effects synchronously + flushSync(); + expect(log.value).toEqual([0]); + + count = 1; + flushSync(); + + expect(log.value).toEqual([0, 1]); + }); + + cleanup(); +}); +``` + +### Component testing + +It is possible to test your components in isolation using Vitest. + +> Before writing component tests, think about whether you actually need to test the component, or if it's more about the logic _inside_ the component. If so, consider extracting out that logic to test it in isolation, without the overhead of a component + +To get started, install jsdom (a library that shims DOM APIs): + +```bash +npm install -D jsdom +``` + +Then adjust your `vite.config.js`: + +```js +/// file: vite.config.js +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [ + /* ... */ + ], + test: { + // If you are testing components client-side, you need to setup a DOM environment. + // If not all your files should have this environment, you can use a + // `// @vitest-environment jsdom` comment at the top of the test files instead. + environment: 'jsdom' + }, + // Tell Vitest to use the `browser` entry points in `package.json` files, even though it's running in Node + resolve: process.env.VITEST + ? { + conditions: ['browser'] + } + : undefined +}); +``` + +After that, you can create a test file in which you import the component to test, interact with it programmatically and write expectations about the results: + +```js +/// file: component.test.js +import { flushSync, mount, unmount } from 'svelte'; +import { expect, test } from 'vitest'; +import Component from './Component.svelte'; + +test('Component', () => { + // Instantiate the component using Svelte's `mount` API + const component = mount(Component, { + target: document.body, // `document` exists because of jsdom + props: { initial: 0 } + }); + + expect(document.body.innerHTML).toBe(''); + + // Click the button, then flush the changes so you can synchronously write expectations + document.body.querySelector('button').click(); + flushSync(); + + expect(document.body.innerHTML).toBe(''); + + // Remove the component from the DOM + unmount(component); +}); +``` + +While the process is very straightforward, it is also low level and somewhat brittle, as the precise structure of your component may change frequently. Tools like [@testing-library/svelte](https://testing-library.com/docs/svelte-testing-library/intro/) can help streamline your tests. The above test could be rewritten like this: + +```js +/// file: component.test.js +import { render, screen } from '@testing-library/svelte'; +import userEvent from '@testing-library/user-event'; +import { expect, test } from 'vitest'; +import Component from './Component.svelte'; + +test('Component', async () => { + const user = userEvent.setup(); + render(Component); + + const button = screen.getByRole('button'); + expect(button).toHaveTextContent(0); + + await user.click(button); + expect(button).toHaveTextContent(1); +}); +``` + +When writing component tests that involve two-way bindings, context or snippet props, it's best to create a wrapper component for your specific test and interact with that. `@testing-library/svelte` contains some [examples](https://testing-library.com/docs/svelte-testing-library/example). + +## E2E tests using Playwright + +E2E (short for 'end to end') tests allow you to test your full application through the eyes of the user. This section uses [Playwright](https://playwright.dev/) as an example, but you can also use other solutions like [Cypress](https://www.cypress.io/) or [NightwatchJS](https://nightwatchjs.org/). + +To get start with Playwright, either let you guide by [their VS Code extension](https://playwright.dev/docs/getting-started-vscode), or install it from the command line using `npm init playwright`. It is also part of the setup CLI when you run `npm create svelte`. + +After you've done that, you should have a `tests` folder and a Playwright config. You may need to adjust that config to tell Playwright what to do before running the tests - mainly starting your application at a certain port: + +```js +/// file: playwright.config.js +const config = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + }, + testDir: 'tests', + testMatch: /(.+\.)?(test|spec)\.[jt]s/ +}; + +export default config; +``` + +You can now start writing tests. These are totally unaware of Svelte as a framework, so you mainly interact with the DOM and write assertions. + +```js +/// file: tests/hello-world.spec.js +import { expect, test } from '@playwright/test'; + +test('home page has expected h1', async ({ page }) => { + await page.goto('/'); + await expect(page.locator('h1')).toBeVisible(); +}); +``` From 04a211e1e17d3f0a245c796a356aa2d79058223c Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 27 Jul 2024 15:14:26 -0400 Subject: [PATCH 018/196] chore: VSCode -> VS Code (#12631) --- documentation/blog/2019-04-15-setting-up-your-editor.md | 2 +- documentation/blog/2019-04-16-svelte-for-new-developers.md | 2 +- .../blog/2021-01-01-whats-new-in-svelte-january-2021.md | 2 +- .../blog/2021-02-01-whats-new-in-svelte-february-2021.md | 4 ++-- .../blog/2021-03-01-whats-new-in-svelte-march-2021.md | 4 ++-- .../blog/2021-10-01-whats-new-in-svelte-october-2021.md | 2 +- .../blog/2023-02-01-whats-new-in-svelte-february-2023.md | 2 +- documentation/blog/2023-03-09-zero-config-type-safety.md | 2 +- documentation/docs/05-misc/03-typescript.md | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/documentation/blog/2019-04-15-setting-up-your-editor.md b/documentation/blog/2019-04-15-setting-up-your-editor.md index 6456397a41..eaf3fac624 100644 --- a/documentation/blog/2019-04-15-setting-up-your-editor.md +++ b/documentation/blog/2019-04-15-setting-up-your-editor.md @@ -12,7 +12,7 @@ This post will walk you through setting up your editor so that it recognises Sve - eslint-plugin-svelte3 - svelte-vscode -- associating .svelte files with HTML in VSCode, Sublime, etc. +- associating .svelte files with HTML in VS Code, Sublime, etc. ## Atom diff --git a/documentation/blog/2019-04-16-svelte-for-new-developers.md b/documentation/blog/2019-04-16-svelte-for-new-developers.md index c0ad2add2b..e11e180a6c 100644 --- a/documentation/blog/2019-04-16-svelte-for-new-developers.md +++ b/documentation/blog/2019-04-16-svelte-for-new-developers.md @@ -48,7 +48,7 @@ Once installed, you'll have access to three new commands: ## Installing a text editor -To write code, you need a good editor. The most popular choice is [Visual Studio Code](https://code.visualstudio.com/) or VSCode, and justifiably so — it's well-designed and fully-featured, and has a wealth of extensions ([including one for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), which provides syntax highlighting and diagnostic messages when you're writing components). +To write code, you need a good editor. The most popular choice is [Visual Studio Code](https://code.visualstudio.com/) or VS Code, and justifiably so — it's well-designed and fully-featured, and has a wealth of extensions ([including one for Svelte](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode), which provides syntax highlighting and diagnostic messages when you're writing components). ## Creating a project diff --git a/documentation/blog/2021-01-01-whats-new-in-svelte-january-2021.md b/documentation/blog/2021-01-01-whats-new-in-svelte-january-2021.md index 675d809fec..4a5ea4eddc 100644 --- a/documentation/blog/2021-01-01-whats-new-in-svelte-january-2021.md +++ b/documentation/blog/2021-01-01-whats-new-in-svelte-january-2021.md @@ -39,7 +39,7 @@ As cautioned in _[What's the deal with SvelteKit?](https://svelte.dev/blog/whats - [weather-ab](https://github.com/ganochenkodg/weather-ab) compares the archive of weather in different cities of the world. Indispensable for people thinking about migration - [Game Nibs](https://gamenibs.com/) is a platform for gamers to find and share concise bite-sized bits of gaming advice, tips, tricks, screenshots, builds, and much more - [Ora](https://github.com/cupcakearmy/ora) is an open source website tracking and limiting tool for Chrome and Firefox -- [vscode-dms](https://github.com/techsyndicate/vscode-dms) is a group direct messaging chat app for VSCode +- [vscode-dms](https://github.com/techsyndicate/vscode-dms) is a group direct messaging chat app for VS Code - [Zero.2](https://zero.oleksandrdemian.tech/) is a math-based challenge game where you try to get to zero as quickly as possible - [Octave Compass](https://octavecompass.com/2741) is a chord table and scale explorer for many popular musical scales - [Infinite Walking Bass Generator 2](https://github.com/elialbert/infinitewalkingbass2) is an online music player that generates a unique walking bass line diff --git a/documentation/blog/2021-02-01-whats-new-in-svelte-february-2021.md b/documentation/blog/2021-02-01-whats-new-in-svelte-february-2021.md index 9b5a5eec4c..e85b274d96 100644 --- a/documentation/blog/2021-02-01-whats-new-in-svelte-february-2021.md +++ b/documentation/blog/2021-02-01-whats-new-in-svelte-february-2021.md @@ -24,7 +24,7 @@ Have a suggestion for a compiler feature or want to help implement new features/ For the complete list of changes, check out the language-tools [Releases page](https://github.com/sveltejs/language-tools/releases). -A great way to try the language tools is to download the [Svelte Extension for VSCode](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). This extension provides syntax highlighting and rich intellisense for Svelte components in VS Code, using the svelte language server. Check your editor's extension sources to see if there's a Svelte plugin for your IDE or build your own (see [coc-svelte](https://github.com/coc-extensions/coc-svelte) for example)! +A great way to try the language tools is to download the [Svelte Extension for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). This extension provides syntax highlighting and rich intellisense for Svelte components in VS Code, using the svelte language server. Check your editor's extension sources to see if there's a Svelte plugin for your IDE or build your own (see [coc-svelte](https://github.com/coc-extensions/coc-svelte) for example)! ## Big improvements across the Svelte ecosystem @@ -77,7 +77,7 @@ New changes to the Svelte Society website include [a new cheat sheet](https://sv - [lihautan's Svelte Actions Playlist](https://www.youtube.com/watch?v=ciaMT_MswzE&list=PLoKaNN3BjQX3Gl14MBygFf8buPIw9pAeK) teaches how actions work and how they can help solve common problems when developing Svelte applications - [One-click Portfolio/Personal blog generator from dev.to API ](https://dev.to/shriji/one-click-portfolio-personal-blog-generator-from-dev-to-api-3apb) walks through creating a Sapper site that also fetches your articles from DEV.to using the API -- [How to Code a VSCode Extension](https://www.youtube.com/watch?v=a5DX5pQ9p5M) features Svelte as a way to render a custom UI within VSCode +- [How to Code a VS Code Extension](https://www.youtube.com/watch?v=a5DX5pQ9p5M) features Svelte as a way to render a custom UI within VS Code - [This YouTube series on Plenti](https://www.youtube.com/watch?v=wyNC7R_VVyQ&list=PLbWvcwWtuDm12y3Hye6oKDwI2gAS0ccHW) walks through the new static site generator in detail ## See you next month! diff --git a/documentation/blog/2021-03-01-whats-new-in-svelte-march-2021.md b/documentation/blog/2021-03-01-whats-new-in-svelte-march-2021.md index 46e45a1130..dea4d292cd 100644 --- a/documentation/blog/2021-03-01-whats-new-in-svelte-march-2021.md +++ b/documentation/blog/2021-03-01-whats-new-in-svelte-march-2021.md @@ -29,7 +29,7 @@ For a complete list of changes, including bug fixes and links to PRs, check out - HTML tags can now be renamed together - Mustache tags parsing is now more robust and will provide better intellisense in more situations -Haven't tried the language-tools yet? Check out [Svelte Extension for VSCode](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) or find a plugin for your favorite IDE! +Haven't tried the language-tools yet? Check out [Svelte Extension for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) or find a plugin for your favorite IDE! ## Other changes from `sveltejs/*` @@ -73,7 +73,7 @@ Haven't tried the language-tools yet? Check out [Svelte Extension for VSCode](ht - [svelte-virtualized-auto-sizer](https://github.com/micha-lmxt/svelte-virtualized-auto-sizer) is a high-order component that automatically adjusts the width and height of a single child. - [svelte-window](https://github.com/micha-lmxt/svelte-window) are components for efficiently rendering large, scrollable lists and tabular data. - [Svelte Persistent store](https://github.com/MacFJA/svelte-persistent-store) is a Svelte store that keep its value through pages and reloads -- [Svelte Dark](https://marketplace.visualstudio.com/items?itemName=NickScialli.svelte-dark) is a VSCode theme inspired by the svelte.dev REPL +- [Svelte Dark](https://marketplace.visualstudio.com/items?itemName=NickScialli.svelte-dark) is a VS Code theme inspired by the svelte.dev REPL - [Import Cost](https://marketplace.visualstudio.com/items?itemName=wix.vscode-import-cost) has been updated to support Svelte libraries and help developers keep their bundle size under control. - [Tree-sitter-svelte](https://github.com/Himujjal/tree-sitter-svelte) provides tree-sitter grammar for svelte - [Svelte Ripple](https://svelte.dev/repl/b73224a0fd4248178e3eab41943d41a9?version=3.31.2) is a Material Design ripple effect that doesn't depend on `@material/ripple` (made by @karakara in the Svelte Discord) diff --git a/documentation/blog/2021-10-01-whats-new-in-svelte-october-2021.md b/documentation/blog/2021-10-01-whats-new-in-svelte-october-2021.md index c3611c2552..d20cce0cab 100644 --- a/documentation/blog/2021-10-01-whats-new-in-svelte-october-2021.md +++ b/documentation/blog/2021-10-01-whats-new-in-svelte-october-2021.md @@ -79,7 +79,7 @@ To see all updates to SvelteKit, check out the [SvelteKit changelog](https://git - [sveltekit-netlify-cms](https://github.com/buhrmi/sveltekit-netlify-cms) is a SvelteKit skeleton app configured for use with Netlify CMS - [SvelteFireTS](https://github.com/jacobbowdoin/sveltefirets) is a SvelteKit + TypeScript + Firebase library inspired by Fireship.io - [stores-x](https://github.com/Anyass3/stores-x) lets you use Svelte stores just like vueX -- [sveltekit-snippets](https://github.com/stordahl/sveltekit-snippets) is a VSCode extension that provides snippets for common patterns in SvelteKit & Vanilla Svelte +- [sveltekit-snippets](https://github.com/stordahl/sveltekit-snippets) is a VS Code extension that provides snippets for common patterns in SvelteKit & Vanilla Svelte - [svelte-xactor](https://github.com/wobsoriano/svelte-xactor) is a middleware that allows you to easily convert your xactor machines into a global store that implements the store contract - [vite-plugin-pages-svelte](https://github.com/aldy505/vite-plugin-pages-svelte) is a vite plugin for automatic filesystem-based routing - [sveltio](https://www.npmjs.com/package/sveltio) is a Svelte wrapper for valtio - a proxy-state library diff --git a/documentation/blog/2023-02-01-whats-new-in-svelte-february-2023.md b/documentation/blog/2023-02-01-whats-new-in-svelte-february-2023.md index 1a4876c4ac..98be8918ca 100644 --- a/documentation/blog/2023-02-01-whats-new-in-svelte-february-2023.md +++ b/documentation/blog/2023-02-01-whats-new-in-svelte-february-2023.md @@ -65,7 +65,7 @@ _To Watch or Hear_ - [I built a $5 chat app with Pocketbase & Svelte. Will it scale?](https://www.youtube.com/watch?v=gUYBFDPZ5qk) by Fireship - [The Comprehensive Introduction To SvelteKit](https://www.youtube.com/watch?v=obmiLi3bhkQ), [What Svelte UI Library Should You Use?](https://www.youtube.com/watch?v=O0mNU0maItY) and [Learn Everything About SvelteKit Routing (Pages, Layout, Nested Routes)](https://www.youtube.com/watch?v=7hXHbGj6iE0) by Joy of Code -- [Progressive Enhancement in SvelteKit (use:enhance)](https://www.youtube.com/watch?v=jXtzWMhdI2U), [Are Your Routes Actually Protected?](https://www.youtube.com/watch?v=UbhhJWV3bmI) and [10X Your SvelteKit Developer Experience in VSCode](https://www.youtube.com/watch?v=13v50nLh67Q) by Huntabyte +- [Progressive Enhancement in SvelteKit (use:enhance)](https://www.youtube.com/watch?v=jXtzWMhdI2U), [Are Your Routes Actually Protected?](https://www.youtube.com/watch?v=UbhhJWV3bmI) and [10X Your SvelteKit Developer Experience in VS Code](https://www.youtube.com/watch?v=13v50nLh67Q) by Huntabyte - [The easiest realtime app I’ve ever built](https://www.youtube.com/watch?v=UbOaAtHWidc) by Beyond Fireship - [Angular developers can learn Svelte so fast...](https://www.youtube.com/watch?v=lKdw_z9qmPU) by Joshua Morony - [SvelteKit + PocketBase Integration: User Login and Registration](https://www.youtube.com/watch?v=AxPB3e-3yEM) by Jian Yuan Lee diff --git a/documentation/blog/2023-03-09-zero-config-type-safety.md b/documentation/blog/2023-03-09-zero-config-type-safety.md index f7e53d122c..0ebf9e0a28 100644 --- a/documentation/blog/2023-03-09-zero-config-type-safety.md +++ b/documentation/blog/2023-03-09-zero-config-type-safety.md @@ -13,7 +13,7 @@ As of today, yes: it can. -If you're using VSCode, just upgrade the Svelte extension to the latest version, and you'll never have to annotate your `load` functions or `data` props again. Extensions for other editors can also use this feature, as long as they support the Language Server Protocol and TypeScript plugins. It even works with the latest version of our CLI diagnostics tool `svelte-check`! +If you're using VS Code, just upgrade the Svelte extension to the latest version, and you'll never have to annotate your `load` functions or `data` props again. Extensions for other editors can also use this feature, as long as they support the Language Server Protocol and TypeScript plugins. It even works with the latest version of our CLI diagnostics tool `svelte-check`! Before we dive in, let's recap how type safety works in SvelteKit. diff --git a/documentation/docs/05-misc/03-typescript.md b/documentation/docs/05-misc/03-typescript.md index 8dd02ce8be..eb58e89b5c 100644 --- a/documentation/docs/05-misc/03-typescript.md +++ b/documentation/docs/05-misc/03-typescript.md @@ -8,7 +8,7 @@ title: TypeScript - using `Component` and the other helper types - using `svelte-check` -You can use TypeScript within Svelte components. IDE extensions like the [Svelte VSCode extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) will help you catch errors right in your editor, and [`svelte-check`](https://www.npmjs.com/package/svelte-check) does the same on the command line, which you can integrate into your CI. +You can use TypeScript within Svelte components. IDE extensions like the [Svelte VS Code extension](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode) will help you catch errors right in your editor, and [`svelte-check`](https://www.npmjs.com/package/svelte-check) does the same on the command line, which you can integrate into your CI. ## `

- +

+
+ +
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json index 0454282103..3d97865823 100644 --- a/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-2/errors.json @@ -1,7 +1,7 @@ [ { "code": "node_invalid_placement", - "message": "
is invalid inside

", + "message": "`

` is invalid inside `

`", "start": { "line": 4, "column": 3 diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json index 8b2e2e67ba..4d637aed80 100644 --- a/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-4/errors.json @@ -1,7 +1,7 @@ [ { "code": "node_invalid_placement", - "message": "

is invalid inside ", + "message": "`` is invalid inside ``", "start": { "line": 4, "column": 3 diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-5/input.svelte b/packages/svelte/tests/validator/samples/invalid-node-placement-5/input.svelte new file mode 100644 index 0000000000..c50f4e087f --- /dev/null +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-5/input.svelte @@ -0,0 +1,9 @@ +
+ + {#if foo} + + + + {/if} + +
diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json b/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json new file mode 100644 index 0000000000..e85050beb7 --- /dev/null +++ b/packages/svelte/tests/validator/samples/invalid-node-placement-5/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "node_invalid_placement_ssr", + "message": "`
` is invalid inside ``. When rendering this component on the server, the resulting HTML will be modified by the browser, likely resulting in a `hydration_mismatch` warning", + "start": { + "line": 4, + "column": 3 + }, + "end": { + "line": 6, + "column": 10 + } + } +] diff --git a/packages/svelte/tests/validator/samples/invalid-node-placement/errors.json b/packages/svelte/tests/validator/samples/invalid-node-placement/errors.json index 2955a1e522..b9bdad5e72 100644 --- a/packages/svelte/tests/validator/samples/invalid-node-placement/errors.json +++ b/packages/svelte/tests/validator/samples/invalid-node-placement/errors.json @@ -1,7 +1,7 @@ [ { "code": "node_invalid_placement", - "message": " is invalid inside ", + "message": "`` is invalid inside ``", "start": { "line": 4, "column": 6 From 7af0e604d82ca2478c126f6acd319ab98435b4e9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 28 Jul 2024 12:53:26 -0400 Subject: [PATCH 021/196] feat: remove `$.unwrap` calls from each block indexes (#12640) * remove $.unwrap from key functions * feat: remove `$.unwrap` calls from each block indexes * tweak --- .changeset/dry-hotels-matter.md | 5 +++ .../phases/3-transform/client/utils.js | 10 +++--- .../3-transform/client/visitors/template.js | 35 ++++++++++++------- 3 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/dry-hotels-matter.md diff --git a/.changeset/dry-hotels-matter.md b/.changeset/dry-hotels-matter.md new file mode 100644 index 0000000000..927f84908d --- /dev/null +++ b/.changeset/dry-hotels-matter.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: remove $.unwrap calls from each block indexes 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 b9456f8653..529318d4fc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -79,6 +79,11 @@ export function serialize_get_binding(node, state) { return node; } + 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'); @@ -88,11 +93,6 @@ export function serialize_get_binding(node, state) { return b.call(node); } - if (Object.hasOwn(state.getters, node.name)) { - const getter = state.getters[node.name]; - return typeof getter === 'function' ? getter(node) : getter; - } - if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (is_prop_source(binding, state)) { return b.call(node); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 0548d7a143..cf0fd231e5 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2493,6 +2493,12 @@ export const template_visitors = { getters: { ...context.state.getters } }; + /** 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 {Binding['mutation']} @@ -2550,8 +2556,12 @@ export const template_visitors = { if (node.index) { child_state.getters[node.index] = (id) => { const index_with_loc = with_loc(index, id); - return b.call('$.unwrap', index_with_loc); + return (flags & EACH_INDEX_REACTIVE) === 0 + ? index_with_loc + : b.call('$.get', index_with_loc); }; + + key_state.getters[node.index] = b.id(node.index); } /** @type {Statement[]} */ @@ -2565,6 +2575,8 @@ export const template_visitors = { true ) ); + + key_state.getters[node.context.name] = node.context; } else { const unwrapped = getter(binding.node); const paths = extract_paths(node.context); @@ -2592,23 +2604,20 @@ export const template_visitors = { if (context.state.options.dev) { declarations.push(b.stmt(getter)); } + + key_state.getters[name] = path.node; } } const block = /** @type {BlockStatement} */ (context.visit(node.body, child_state)); - const key_function = node.key - ? b.arrow( - [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], - declarations.length > 0 - ? b.block( - declarations.concat( - b.return(/** @type {Expression} */ (context.visit(node.key, child_state))) - ) - ) - : /** @type {Expression} */ (context.visit(node.key, child_state)) - ) - : b.id('$.index'); + /** @type {Expression} */ + let key_function = b.id('$.index'); + + if (node.key) { + const expression = /** @type {Expression} */ (context.visit(node.key, key_state)); + key_function = b.arrow([node.context, index], expression); + } if (node.index && each_node_meta.contains_group_binding) { // We needed to create a unique identifier for the index above, but we want to use the From 219ba6a71ba3a72f1d87b6d2a5c77fad61d5c97e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 28 Jul 2024 15:27:10 -0400 Subject: [PATCH 022/196] fix: error on `bind:this` to each block parameter (#12638) * chore: remove outdated comment * fix: error on `bind:this` to each block parameter --- .changeset/sharp-spies-live.md | 5 ++++ .../compiler/phases/2-analyze/validation.js | 28 +++++++++---------- .../_config.js | 9 ++++++ .../main.svelte | 11 ++++++++ 4 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 .changeset/sharp-spies-live.md create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/main.svelte diff --git a/.changeset/sharp-spies-live.md b/.changeset/sharp-spies-live.md new file mode 100644 index 0000000000..0ddbde97ed --- /dev/null +++ b/.changeset/sharp-spies-live.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: error on `bind:this` to each block parameter diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index eda5ed42b0..296104b050 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -385,33 +385,31 @@ const validation = { const binding = context.state.scope.get(left.name); - if ( - assignee.type === 'Identifier' && - node.name !== 'this' // bind:this also works for regular variables - ) { + if (assignee.type === 'Identifier') { // reassignment if ( - !binding || - (binding.kind !== 'state' && - binding.kind !== 'frozen_state' && - binding.kind !== 'prop' && - binding.kind !== 'bindable_prop' && - binding.kind !== 'each' && - binding.kind !== 'store_sub' && - !binding.mutated) + node.name !== 'this' && // bind:this also works for regular variables + (!binding || + (binding.kind !== 'state' && + binding.kind !== 'frozen_state' && + binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && + binding.kind !== 'each' && + binding.kind !== 'store_sub' && + !binding.mutated)) ) { e.bind_invalid_value(node.expression); } - if (binding.kind === 'derived') { + if (binding?.kind === 'derived') { e.constant_binding(node.expression, 'derived state'); } - if (context.state.analysis.runes && binding.kind === 'each') { + if (context.state.analysis.runes && binding?.kind === 'each') { e.each_item_invalid_assignment(node); } - if (binding.kind === 'snippet') { + if (binding?.kind === 'snippet') { e.snippet_parameter_assignment(node); } } diff --git a/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/_config.js new file mode 100644 index 0000000000..524c2f161c --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'each_item_invalid_assignment', + message: + 'Cannot reassign or bind to each block argument in runes mode. Use the array and index variables instead (e.g. `array[i] = value` instead of `entry = value`)' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/main.svelte new file mode 100644 index 0000000000..f6d887d048 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-invalid-each-binding-this/main.svelte @@ -0,0 +1,11 @@ + + +{#each array as { id, element } (id)} + +{/each} From 5d82cf1f03e5b9c57feaf50a4996238688597588 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 28 Jul 2024 15:27:42 -0400 Subject: [PATCH 023/196] feat: remove `$.unwrap` calls from `bind:group` (#12642) * add each.metadata.keyed * simplify * feat: remove `$.unwrap` from `bind:group` code * changeset * regenerate types --- .changeset/three-donkeys-jump.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 7 ++++++ .../3-transform/client/visitors/template.js | 25 ++++++++++--------- packages/svelte/src/compiler/phases/scope.js | 1 + .../svelte/src/compiler/types/template.d.ts | 1 + .../client/dom/elements/bindings/input.js | 9 ++----- packages/svelte/types/index.d.ts | 1 + 7 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 .changeset/three-donkeys-jump.md diff --git a/.changeset/three-donkeys-jump.md b/.changeset/three-donkeys-jump.md new file mode 100644 index 0000000000..6cdc6c7556 --- /dev/null +++ b/.changeset/three-donkeys-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: remove `$.unwrap` calls from `bind:group` diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index aa1e43196f..aac76faa60 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1547,6 +1547,13 @@ const common_visitors = { }, RenderTag(node, context) { context.next({ ...context.state, render_tag: node }); + }, + EachBlock(node) { + if (node.key) { + // treat `{#each items as item, i (i)}` as a normal indexed block, everything else as keyed + node.metadata.keyed = + node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index; + } } }; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index cf0fd231e5..ca78a33a1c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -2409,10 +2409,7 @@ export const template_visitors = { let flags = 0; - if ( - node.key && - (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index) - ) { + if (node.metadata.keyed) { flags |= EACH_KEYED; if (node.index) { @@ -2421,7 +2418,7 @@ export const template_visitors = { // In runes mode, if key === item, we don't need to wrap the item in a source const key_is_item = - node.key.type === 'Identifier' && + /** @type {Expression} */ (node.key).type === 'Identifier' && node.context.type === 'Identifier' && node.context.name === node.key.name; @@ -2614,8 +2611,11 @@ export const template_visitors = { /** @type {Expression} */ let key_function = b.id('$.index'); - if (node.key) { - const expression = /** @type {Expression} */ (context.visit(node.key, key_state)); + if (node.metadata.keyed) { + const expression = /** @type {Expression} */ ( + context.visit(/** @type {Expression} */ (node.key), key_state) + ); + key_function = b.arrow([node.context, index], expression); } @@ -3049,11 +3049,12 @@ export const template_visitors = { call_expr = b.call(`$.bind_focused`, state.node, setter); break; case 'group': { - /** @type {CallExpression[]} */ - const indexes = []; - for (const parent_each_block of node.metadata.parent_each_blocks) { - indexes.push(b.call('$.unwrap', parent_each_block.metadata.index)); - } + const indexes = node.metadata.parent_each_blocks.map((each) => { + // if we have a keyed block with an index, the index is wrapped in a source + return each.metadata.keyed && each.index + ? b.call('$.get', each.metadata.index) + : each.metadata.index; + }); // We need to additionally invoke the value attribute signal to register it as a dependency, // so that when the value is updated, the group binding is updated diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 1ba9851ee3..167f61e034 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -592,6 +592,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } node.metadata = { + keyed: false, contains_group_binding: false, array_name: needs_array_deduplication ? state.scope.root.unique('$$array') : null, index: scope.root.unique('$$index'), diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index ec35a08227..e5bc072c1a 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -393,6 +393,7 @@ export interface EachBlock extends BaseNode { index?: string; key?: Expression; metadata: { + keyed: boolean; contains_group_binding: boolean; /** Set if something in the array expression is shadowed within the each block */ array_name: Identifier | null; diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/input.js b/packages/svelte/src/internal/client/dom/elements/bindings/input.js index ca83d90f20..35dd229d39 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/input.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/input.js @@ -73,13 +73,8 @@ export function bind_group(inputs, group_index, input, get_value, update) { if (group_index !== null) { for (var index of group_index) { - var group = binding_group; - // @ts-ignore - binding_group = group[index]; - if (binding_group === undefined) { - // @ts-ignore - binding_group = group[index] = []; - } + // @ts-expect-error + binding_group = binding_group[index] ??= []; } } diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9de214c847..d9537235f6 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1845,6 +1845,7 @@ declare module 'svelte/compiler' { index?: string; key?: Expression; metadata: { + keyed: boolean; contains_group_binding: boolean; /** Set if something in the array expression is shadowed within the each block */ array_name: Identifier | null; From 5817767a87e130fc3ae3bd39a00cf3290821b136 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 28 Jul 2024 15:36:31 -0400 Subject: [PATCH 024/196] Version Packages (next) (#12641) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 945ae7f965..1c02935ded 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -122,6 +122,7 @@ "dry-eggs-retire", "dry-fans-march", "dry-ghosts-prove", + "dry-hotels-matter", "dry-parrots-bathe", "dry-pillows-exist", "dull-coins-vanish", @@ -530,6 +531,7 @@ "shaggy-comics-jog", "sharp-gorillas-impress", "sharp-kids-happen", + "sharp-spies-live", "sharp-tomatoes-learn", "shiny-baboons-play", "shiny-kangaroos-move", @@ -682,6 +684,7 @@ "thirty-wombats-relax", "three-buses-sleep", "three-camels-sell", + "three-donkeys-jump", "three-foxes-relax", "three-icons-trade", "three-lions-visit", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f924237196..95f0286f9f 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.0.0-next.201 + +### Patch Changes + +- feat: remove $.unwrap calls from each block indexes ([#12640](https://github.com/sveltejs/svelte/pull/12640)) + +- fix: error on `bind:this` to each block parameter ([#12638](https://github.com/sveltejs/svelte/pull/12638)) + +- feat: remove `$.unwrap` calls from `bind:group` ([#12642](https://github.com/sveltejs/svelte/pull/12642)) + ## 5.0.0-next.200 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 698676fddd..bb90134aa5 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.200", + "version": "5.0.0-next.201", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 13072c37af..4051b6c8d9 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.200'; +export const VERSION = '5.0.0-next.201'; export const PUBLIC_VERSION = '5'; From e417d3a2d281a5ec9b595be5ffbd47efe57b28c3 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 29 Jul 2024 11:14:04 +0100 Subject: [PATCH 025/196] fix: remove implicit passive behavior from OnDirective events (#12645) Fixes #12639 --- .changeset/modern-peaches-cheer.md | 5 +++++ .../compiler/phases/3-transform/client/visitors/template.js | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .changeset/modern-peaches-cheer.md diff --git a/.changeset/modern-peaches-cheer.md b/.changeset/modern-peaches-cheer.md new file mode 100644 index 0000000000..b02545e091 --- /dev/null +++ b/.changeset/modern-peaches-cheer.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove implicit passive behavior from OnDirective events diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index ca78a33a1c..9006d68aca 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1298,7 +1298,11 @@ function serialize_event(node, metadata, context) { args.push(b.literal(true)); } else if (node.modifiers.includes('nonpassive')) { args.push(b.literal(false)); - } else if (PassiveEvents.includes(node.name)) { + } else if ( + PassiveEvents.includes(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)); } From 32c4e4706005bbaec3ff000fb2361b66013d3778 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Mon, 29 Jul 2024 15:32:09 +0200 Subject: [PATCH 026/196] fix: always set draggable through `setAttribute` to avoid weird behavior (#12649) Closes #12643 Very weird behaviour from the draggable setter...if you set element.draggable="false" it will actually set draggable to true (the boolean). --- .changeset/wild-poems-design.md | 5 +++++ .../internal/client/dom/elements/attributes.js | 5 +++-- .../samples/svelte-element-draggable/_config.js | 17 +++++++++++++++++ .../svelte-element-draggable/main.svelte | 7 +++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 .changeset/wild-poems-design.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/main.svelte diff --git a/.changeset/wild-poems-design.md b/.changeset/wild-poems-design.md new file mode 100644 index 0000000000..e973ab9518 --- /dev/null +++ b/.changeset/wild-poems-design.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: always set draggable through `setAttribute` to avoid weird behavior diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 7c8c0d8107..1321470062 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -341,9 +341,10 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) { * because updating them through the property setter doesn't work reliably. * In the example of `width`/`height`, the problem is that the setter only * accepts numeric values, but the attribute can also be set to a string like `50%`. - * If this list becomes too big, rethink this approach. + * In case of draggable trying to set `element.draggable='false'` will actually set + * draggable to `true`. If this list becomes too big, rethink this approach. */ -var always_set_through_set_attribute = ['width', 'height']; +var always_set_through_set_attribute = ['width', 'height', 'draggable']; /** @type {Map} */ var setters_cache = new Map(); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/_config.js new file mode 100644 index 0000000000..589e079b16 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/_config.js @@ -0,0 +1,17 @@ +import { ok, test } from '../../test'; +import { flushSync } from 'svelte'; + +export default test({ + html: `
`, + + async test({ assert, target, logs }) { + const [div, div2] = target.querySelectorAll('div'); + ok(div); + ok(div2); + + assert.deepEqual(div.getAttribute('draggable'), 'false'); + assert.deepEqual(div.draggable, false); + assert.deepEqual(div2.getAttribute('draggable'), 'false'); + assert.deepEqual(div2.draggable, false); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/main.svelte new file mode 100644 index 0000000000..37e655cb15 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-draggable/main.svelte @@ -0,0 +1,7 @@ + + + + +
From 85f2f161ed3ad141ecfdc942719a582dd99172aa Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:32:26 +0200 Subject: [PATCH 027/196] chore: use queue_microtask on `#await` again (#12647) Now that we've made `mount` and `hydrate` not call `flushSync` anymore, we can go back to using `queue_microtask` inside `#await`, which means people who want to synchronously see the pending block can do so using `flushSync` (as validated by our tests). This essentially reverts #12274 --- .../src/internal/client/dom/blocks/await.js | 2 +- .../expected.html | 6 +- .../input.svelte | 11 +-- .../_config.js | 12 ++-- .../expected.css | 6 +- .../expected.html | 4 +- .../input.svelte | 13 ++-- .../expected.html | 6 +- .../input.svelte | 13 ++-- .../siblings-combinator-await/_config.js | 16 ++--- .../siblings-combinator-await/expected.css | 8 +-- .../siblings-combinator-await/expected.html | 4 +- .../siblings-combinator-await/input.svelte | 15 ++-- packages/svelte/tests/css/test.ts | 3 +- .../await-block-func-function/_config.js | 4 ++ .../await-catch-no-expression/_config.js | 6 +- .../await-conservative-update/_config.js | 4 ++ .../samples/await-containing-if/_config.js | 4 ++ .../samples/await-in-each/_config.js | 4 ++ .../_config.js | 1 + .../_config.js | 3 +- .../samples/await-set-simultaneous/_config.js | 1 - .../await-then-blowback-reactive/_config.js | 2 + .../await-then-catch-anchor/_config.js | 7 +- .../samples/await-then-catch-event/_config.js | 4 ++ .../samples/await-then-catch-if/_config.js | 4 ++ .../await-then-catch-in-slot/_config.js | 5 +- .../await-then-catch-multiple/_config.js | 8 ++- .../await-then-catch-no-values/_config.js | 5 +- .../samples/await-then-catch-order/_config.js | 4 ++ .../await-then-catch-static/_config.js | 7 +- .../samples/await-then-catch/_config.js | 7 +- .../_config.js | 4 ++ .../await-then-destruct-array/_config.js | 4 ++ .../_config.js | 4 ++ .../await-then-destruct-object/_config.js | 4 ++ .../samples/await-then-if/_config.js | 4 ++ .../await-then-no-expression/_config.js | 7 +- .../samples/await-then-shorthand/_config.js | 3 +- .../samples/await-with-components/_config.js | 3 - .../samples/await-with-update-2/_config.js | 4 ++ .../samples/await-with-update/_config.js | 4 ++ .../samples/await-without-catch/_config.js | 7 +- .../samples/context-in-await/_config.js | 4 ++ .../_config.js | 1 - .../transition-js-await-block/_config.js | 3 +- .../samples/await-pending-wait/_config.js | 68 +++++++++++++++++++ .../samples/await-pending-wait/await.svelte | 9 +++ .../samples/await-pending-wait/main.svelte | 13 ++++ .../samples/await-resolve-2/_config.js | 7 +- .../samples/await-resolve/_config.js | 9 +-- .../03-appendix/02-breaking-changes.md | 23 +------ 52 files changed, 272 insertions(+), 112 deletions(-) create mode 100644 packages/svelte/tests/runtime-runes/samples/await-pending-wait/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/await-pending-wait/await.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/await-pending-wait/main.svelte diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 49a7bd5892..5477176660 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -139,7 +139,7 @@ export function await_block(node, get_input, pending_fn, then_fn, catch_fn) { } else { // Wait a microtask before checking if we should show the pending state as // the promise might have resolved by the next microtask. - Promise.resolve().then(() => { + queue_micro_task(() => { if (!resolved) update(PENDING, true); }); } diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/expected.html b/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/expected.html index 884480bca1..de97b02a5e 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/expected.html +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/expected.html @@ -1,4 +1,4 @@
-
-
-
+
+
+
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/input.svelte b/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/input.svelte index 5fb2c1e540..a3e4230161 100644 --- a/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/input.svelte +++ b/packages/svelte/tests/css/samples/general-siblings-combinator-await-not-exhaustive/input.svelte @@ -1,3 +1,7 @@ + +
- -{#await true} +{#await promise}
{:then value}
diff --git a/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/expected.html b/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/expected.html index 884480bca1..de97b02a5e 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/expected.html +++ b/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/expected.html @@ -1,4 +1,4 @@
-
-
-
+
+
+
\ No newline at end of file diff --git a/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/input.svelte b/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/input.svelte index 30d48d1a6a..520c6f39a9 100644 --- a/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/input.svelte +++ b/packages/svelte/tests/css/samples/siblings-combinator-await-not-exhaustive/input.svelte @@ -1,3 +1,7 @@ + +
- -{#await true} +{#await promise}
{:then value}
diff --git a/packages/svelte/tests/css/test.ts b/packages/svelte/tests/css/test.ts index b4b25fc479..30ad96369a 100644 --- a/packages/svelte/tests/css/test.ts +++ b/packages/svelte/tests/css/test.ts @@ -4,7 +4,7 @@ import * as fs from 'node:fs'; import { assert } from 'vitest'; import { compile_directory, try_read_file } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; -import { mount, unmount } from 'svelte'; +import { flushSync, mount, unmount } from 'svelte'; import { suite, type BaseTest } from '../suite.js'; import type { CompileOptions, Warning } from '#compiler'; @@ -60,6 +60,7 @@ const { test, run } = suite(async (config, cwd) => { const target = window.document.createElement('main'); const component = mount(ClientComponent, { props: config.props ?? {}, target }); + flushSync(); const html = target.innerHTML; diff --git a/packages/svelte/tests/runtime-legacy/samples/await-block-func-function/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-block-func-function/_config.js index 3b64e74f24..3125eafb53 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-block-func-function/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-block-func-function/_config.js @@ -7,6 +7,10 @@ export default test({ }; }, + html: ` + Waiting... + `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve({ func: 12345 })); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-catch-no-expression/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-catch-no-expression/_config.js index 7ef3df596b..a13e636f3c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-catch-no-expression/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-catch-no-expression/_config.js @@ -13,6 +13,11 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +
+

the promise is pending

+ `, + async test({ assert, component, target }) { deferred.resolve(42); @@ -22,7 +27,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

the promise is pending

'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-conservative-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-conservative-update/_config.js index c7839f689f..c36d75ea1f 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-conservative-update/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-conservative-update/_config.js @@ -2,6 +2,10 @@ import { test } from '../../test'; import { sleep } from './sleep.js'; export default test({ + html: ` +

loading...

+ `, + test({ assert, target }) { return sleep(50).then(() => { assert.htmlEqual( diff --git a/packages/svelte/tests/runtime-legacy/samples/await-containing-if/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-containing-if/_config.js index e46ef352f3..3c9bab013f 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-containing-if/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-containing-if/_config.js @@ -13,6 +13,10 @@ export default test({ return { thePromise: deferred.promise, show: true }; }, + html: ` +

loading...

+ `, + test({ assert, component, target }) { deferred.resolve(42); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-in-each/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-in-each/_config.js index ea2ded02e3..71831276a6 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-in-each/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-in-each/_config.js @@ -19,6 +19,10 @@ export default test({ return { items }; }, + html: ` +

a title: loading...

+ `, + test({ assert, target }) { fulfil(42); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/_config.js index 653378f52b..e207ef31d6 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-mount-and-unmount-immediately/_config.js @@ -1,6 +1,7 @@ import { test } from '../../test'; export default test({ + html: 'Loading...', async test({ assert, component, target }) { await component.test(); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous-reactive/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous-reactive/_config.js index 5770a65a9c..1d775465cd 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous-reactive/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous-reactive/_config.js @@ -1,8 +1,9 @@ import { test } from '../../test'; export default test({ + html: '

wait for it...

', test({ assert, component, target }) { - return component.promise.then(() => { + return component.promise.then(async () => { assert.htmlEqual( target.innerHTML, ` diff --git a/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous/_config.js index d1b93da8eb..9fe8683973 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-set-simultaneous/_config.js @@ -7,7 +7,6 @@ export default test({ }); component.promise = promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

wait for it...

'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-blowback-reactive/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-blowback-reactive/_config.js index 8d45e3a42f..665b8d94b6 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-blowback-reactive/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-blowback-reactive/_config.js @@ -3,6 +3,8 @@ import { ok, test } from '../../test'; export default test({ async test({ assert, component, target }) { + assert.htmlEqual(target.innerHTML, 'Loading...'); + await component.promise; await Promise.resolve(); const span = target.querySelector('span'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-anchor/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-anchor/_config.js index 36b1e836fe..d778d2c8d2 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-anchor/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-anchor/_config.js @@ -13,11 +13,15 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +

loading...

+ `, + test({ assert, component, target }) { deferred.resolve(42); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual( target.innerHTML, ` @@ -28,7 +32,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

loading...

'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js index 598e657f06..26d22b8c1a 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-event/_config.js @@ -15,6 +15,10 @@ export default test({ }; }, + html: ` +

loading...

+ `, + test({ assert, component, target, window }) { deferred.resolve(42); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-if/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-if/_config.js index 104ff82ea8..d68f1597fc 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-if/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-if/_config.js @@ -12,6 +12,10 @@ export default test({ return { show: true, thePromise }; }, + html: ` +

loading...

+ `, + test({ assert, component, target }) { fulfil(42); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-in-slot/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-in-slot/_config.js index 6d6cfa4a28..09ab6e3912 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-in-slot/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-in-slot/_config.js @@ -13,6 +13,10 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +

loading...

+ `, + async test({ assert, component, target }) { deferred.resolve(42); @@ -21,7 +25,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

loading...

'); deferred.reject(new Error('something broke')); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-multiple/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-multiple/_config.js index ff90451519..719484ff0e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-multiple/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-multiple/_config.js @@ -13,11 +13,16 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +

loading...

+

loading...

+ `, + test({ assert, component, target }) { deferred.resolve(42); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual( target.innerHTML, ` @@ -29,7 +34,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-no-values/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-no-values/_config.js index aad71a1df0..4608be3edd 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-no-values/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-no-values/_config.js @@ -13,17 +13,18 @@ export default test({ return { thePromise: deferred.promise }; }, + html: 'waiting', + test({ assert, component, target }) { deferred.resolve(9000); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual(target.innerHTML, 'resolved'); deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, 'waiting'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-order/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-order/_config.js index b12d5bc989..e4ef68a75e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-order/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-order/_config.js @@ -12,6 +12,10 @@ export default test({ return { thePromise }; }, + html: ` +

loading...

true!

+ `, + test({ assert, target }) { fulfil(42); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-static/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-static/_config.js index 1e8ae8cad2..46608c41e8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch-static/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch-static/_config.js @@ -12,11 +12,15 @@ export default test({ return { promise }; }, + html: ` +

loading...

+ `, + test({ assert, component, target }) { fulfil(42); return promise - .then(async () => { + .then(() => { assert.htmlEqual( target.innerHTML, ` @@ -29,7 +33,6 @@ export default test({ }); component.promise = promise; - await Promise.resolve(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-catch/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-catch/_config.js index 7ad918a174..33a8eb2bab 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-catch/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-catch/_config.js @@ -13,11 +13,15 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +

loading...

+ `, + test({ assert, component, target }) { deferred.resolve(42); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual( target.innerHTML, ` @@ -28,7 +32,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

loading...

'); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array-nested-rest/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array-nested-rest/_config.js index e914fba478..ee8b8e4a0e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array-nested-rest/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array-nested-rest/_config.js @@ -7,6 +7,10 @@ export default test({ }; }, + html: ` + loading... + `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve([1, 2, 3, 4, 5, 6, 7, 8])); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array/_config.js index 9ab61331ef..1d830ce582 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-array/_config.js @@ -7,6 +7,10 @@ export default test({ }; }, + html: ` + loading... + `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve([1, 2])); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-number-props/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-number-props/_config.js index 35c72e35b9..a5d1e2195e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-number-props/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-number-props/_config.js @@ -7,6 +7,10 @@ export default test({ }; }, + html: ` + loading... + `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve([10, 11, 12, 13, 14, 15])); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object/_config.js index d098da8370..5c7989c933 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-destruct-object/_config.js @@ -7,6 +7,10 @@ export default test({ }; }, + html: ` + loading... + `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve({ error: 'error message' })); assert.htmlEqual( diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-if/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-if/_config.js index 348075c4c5..27b111d68b 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-if/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-if/_config.js @@ -12,6 +12,10 @@ export default test({ return { thePromise }; }, + html: ` + loading... + `, + async test({ assert, target }) { fulfil([]); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-no-expression/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-no-expression/_config.js index a7b8bac126..7790a06d4d 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-no-expression/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-no-expression/_config.js @@ -13,6 +13,12 @@ export default test({ return { thePromise: deferred.promise }; }, + html: ` +
+
+

the promise is pending

+ `, + expect_unhandled_rejections: true, async test({ assert, component, target }) { deferred.resolve(); @@ -33,7 +39,6 @@ export default test({ const local = (deferred = create_deferred()); component.thePromise = local.promise; - await Promise.resolve(); assert.htmlEqual( target.innerHTML, diff --git a/packages/svelte/tests/runtime-legacy/samples/await-then-shorthand/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-then-shorthand/_config.js index 4c71961420..79e7094c6c 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-then-shorthand/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-then-shorthand/_config.js @@ -19,7 +19,7 @@ export default test({ deferred.resolve(42); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual( target.innerHTML, ` @@ -30,7 +30,6 @@ export default test({ deferred = create_deferred(); component.thePromise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, ''); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-with-components/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-with-components/_config.js index 7198203756..3798e8dc08 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-with-components/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-with-components/_config.js @@ -11,7 +11,6 @@ export default test({ let promise = new Promise((ok) => (resolve = ok)); component.promise = promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, 'Loading...'); resolve(42); @@ -20,7 +19,6 @@ export default test({ promise = new Promise((ok, fail) => (reject = fail)); component.promise = promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, 'Loading...'); reject(99); @@ -29,7 +27,6 @@ export default test({ promise = new Promise((ok) => (resolve = ok)); component.promise = promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, 'Loading...'); resolve(1); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-with-update-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-with-update-2/_config.js index 93d28ebefa..c091d4fbc8 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-with-update-2/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-with-update-2/_config.js @@ -8,6 +8,10 @@ export default test({ }; }, + html: ` +

loading...

+ `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve({ value: 'success', diff --git a/packages/svelte/tests/runtime-legacy/samples/await-with-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-with-update/_config.js index 7f5aa010b4..cea4955911 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-with-update/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-with-update/_config.js @@ -8,6 +8,10 @@ export default test({ }; }, + html: ` +

loading...

+ `, + async test({ assert, component, target }) { await (component.thePromise = Promise.resolve(component.Component)); diff --git a/packages/svelte/tests/runtime-legacy/samples/await-without-catch/_config.js b/packages/svelte/tests/runtime-legacy/samples/await-without-catch/_config.js index e7fcc52efb..a4db703dc4 100644 --- a/packages/svelte/tests/runtime-legacy/samples/await-without-catch/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/await-without-catch/_config.js @@ -13,18 +13,21 @@ export default test({ return { promise: deferred.promise }; }, + html: ` +

loading...

+ `, + expect_unhandled_rejections: true, test({ assert, component, target }) { deferred.resolve(42); return deferred.promise - .then(async () => { + .then(() => { assert.htmlEqual(target.innerHTML, '

loaded

'); deferred = create_deferred(); component.promise = deferred.promise; - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

loading...

'); diff --git a/packages/svelte/tests/runtime-legacy/samples/context-in-await/_config.js b/packages/svelte/tests/runtime-legacy/samples/context-in-await/_config.js index b9e76c33e9..1d131be782 100644 --- a/packages/svelte/tests/runtime-legacy/samples/context-in-await/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/context-in-await/_config.js @@ -1,6 +1,10 @@ import { test } from '../../test'; export default test({ + html: ` +

...waiting

+ `, + async test({ assert, component, target }) { await component.promise; diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js index b4c108afb4..1ce8a85028 100644 --- a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block-outros/_config.js @@ -14,7 +14,6 @@ export default test({ intro: true, async test({ assert, target, component, raf }) { - await Promise.resolve(); assert.htmlEqual(target.innerHTML, '

loading...

'); let time = 0; diff --git a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js index 171ed5c82d..706b306ebb 100644 --- a/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/transition-js-await-block/_config.js @@ -16,8 +16,7 @@ export default test({ intro: true, - async test({ assert, target, raf }) { - await Promise.resolve(); + test({ assert, target, raf }) { const p = /** @type {HTMLParagraphElement & { foo: number }} */ (target.querySelector('p')); raf.tick(0); diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-wait/_config.js b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/_config.js new file mode 100644 index 0000000000..7ad653433c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/_config.js @@ -0,0 +1,68 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + + // not flushing means we wait a tick before showing the pending state ... + b2.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` ` + ); + + // ... and show the then state directly if the promise resolved by then + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` then` + ); + + // reset + b1.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` ` + ); + + // flushing means we show the pending state immediately + b2.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` pending` + ); + + await Promise.resolve(); + b1.click(); + flushSync(); + assert.htmlEqual( + target.innerHTML, + ` ` + ); + + // when not flushing ... + b3.click(); + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` ` + ); + + // ... we show the pending state after a tick when the promise hasn't resolved by then + await Promise.resolve(); + assert.htmlEqual( + target.innerHTML, + ` pending` + ); + + await new Promise((r) => setTimeout(r, 110)); + assert.htmlEqual( + target.innerHTML, + ` then` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-wait/await.svelte b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/await.svelte new file mode 100644 index 0000000000..765b38717c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/await.svelte @@ -0,0 +1,9 @@ + + +{#await promise} + pending +{:then value} + then {value} +{/await} diff --git a/packages/svelte/tests/runtime-runes/samples/await-pending-wait/main.svelte b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/main.svelte new file mode 100644 index 0000000000..2adcc3faf4 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/await-pending-wait/main.svelte @@ -0,0 +1,13 @@ + + + + + + +{#if promise} + +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/await-resolve-2/_config.js b/packages/svelte/tests/runtime-runes/samples/await-resolve-2/_config.js index b72a0f100f..88f1b1e961 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-resolve-2/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-resolve-2/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - async test({ assert, target, logs, variant }) { + async test({ assert, target, logs }) { const [b1, b2, b3, b4] = target.querySelectorAll('button'); b1.click(); await Promise.resolve(); @@ -16,9 +16,6 @@ export default test({ b4.click(); await Promise.resolve(); await Promise.resolve(); - assert.deepEqual( - logs, - variant === 'hydrate' ? ['pending', 'a', 'b', 'c', 'pending'] : ['a', 'b', 'c', 'pending'] - ); + assert.deepEqual(logs, ['pending', 'a', 'b', 'c', 'pending']); } }); diff --git a/packages/svelte/tests/runtime-runes/samples/await-resolve/_config.js b/packages/svelte/tests/runtime-runes/samples/await-resolve/_config.js index 3f718e0c0f..11a645673a 100644 --- a/packages/svelte/tests/runtime-runes/samples/await-resolve/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/await-resolve/_config.js @@ -1,7 +1,7 @@ import { test } from '../../test'; export default test({ - async test({ assert, target, logs, variant }) { + async test({ assert, target, logs }) { const [b1, b2] = target.querySelectorAll('button'); b1.click(); await Promise.resolve(); @@ -22,11 +22,6 @@ export default test({ `

then b

` ); - assert.deepEqual( - logs, - variant === 'hydrate' - ? ['rendering pending block', 'rendering then block'] - : ['rendering then block'] - ); + assert.deepEqual(logs, ['rendering pending block', 'rendering then block']); } }); 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 5c475b7ec8..029b2ba688 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 @@ -83,6 +83,8 @@ export default { }; ``` +Note that `mount` and `hydrate` are _not_ synchronous, so things like `onMount` won't have been called by the time the function returns and the pending block of promises will not have been rendered yet (because `#await` waits a microtask to wait for a potentially immediately-resolved promise). If you need that guarantee, call `flushSync` (import from `'svelte'`) after calling `mount/hydrate`. + ### Server API changes Similarly, components no longer have a `render` method when compiled for server side rendering. Instead, pass the function to `render` from `svelte/server`: @@ -331,24 +333,3 @@ Since these mismatches are extremely rare, Svelte 5 assumes that the values are ### Hydration works differently Svelte 5 makes use of comments during server side rendering which are used for more robust and efficient hydration on the client. As such, you shouldn't remove comments from your HTML output if you intend to hydrate it, and if you manually authored HTML to be hydrated by a Svelte component, you need to adjust that HTML to include said comments at the correct positions. - -### `await` blocks delay render - -In Svelte 4, an `{#await ...}` block immediately renders the pending section. In some cases, this is wasteful, because the promise is already resolved. - -In Svelte 5 the block remains unrendered when mounting or updating the promise, until we know whether it is already resolved or not — if so, we initally render then `{:then ...}` or `{:catch ...}` section instead. - -This does _not_ apply during hydration, since the pending section was already server-rendered. - -To wait until the pending section has been rendered (for example during testing), use `await Promise.resolve()` after mounting or updating the promise: - -```diff -let props = { - promise: getPromiseSomehow() -}; - -mount(App, { target, props }); - -+await Promise.resolve(); -assert.equal(target.innerHTML, '...'); -``` From 363535c849d71ce7fdb43015fe1b7cddc139ee44 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 29 Jul 2024 15:51:41 +0200 Subject: [PATCH 028/196] feat: warn on obvious legacy component instantiation (#12648) Adds a compiler warning that warns about legacy component instantiation (i.e. using `new Component(..)`). This won't catch all cases, but the most obvious ones which probably make up ~80% --- .../messages/compile-warnings/script.md | 4 +++ .../compiler/phases/2-analyze/validation.js | 28 ++++++++++++++++++- packages/svelte/src/compiler/warnings.js | 9 ++++++ .../input.svelte | 13 +++++++++ .../warnings.json | 14 ++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 packages/svelte/tests/validator/samples/component-legacy-instantiation/input.svelte create mode 100644 packages/svelte/tests/validator/samples/component-legacy-instantiation/warnings.json diff --git a/packages/svelte/messages/compile-warnings/script.md b/packages/svelte/messages/compile-warnings/script.md index af8b635bb6..a31655b972 100644 --- a/packages/svelte/messages/compile-warnings/script.md +++ b/packages/svelte/messages/compile-warnings/script.md @@ -6,6 +6,10 @@ > Component has unused export property '%name%'. If it is for external reference only, please consider using `export const %name%` +## legacy_component_creation + +> Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead. + ## non_reactive_update > `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 296104b050..67459fe5a1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, CallExpression, Expression, Identifier, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ +/** @import { AssignmentExpression, CallExpression, Expression, ImportDeclaration, Identifier, Node, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { Attribute, Component, ElementLike, Fragment, RegularElement, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TransitionDirective } from '#compiler' */ /** @import { NodeLike } from '../../errors.js' */ /** @import { AnalysisState, Context, Visitors } from './types.js' */ @@ -362,6 +362,32 @@ function validate_block_not_empty(node, context) { * @type {Visitors} */ const validation = { + ExpressionStatement(node, { state }) { + if ( + node.expression.type === 'NewExpression' && + node.expression.callee.type === 'Identifier' && + node.expression.arguments.length === 1 && + node.expression.arguments[0].type === 'ObjectExpression' && + node.expression.arguments[0].properties.some( + (p) => p.type === 'Property' && p.key.type === 'Identifier' && p.key.name === 'target' + ) + ) { + const binding = state.scope.get(node.expression.callee.name); + if (binding?.kind === 'normal' && binding.declaration_kind === 'import') { + const declaration = /** @type {ImportDeclaration} */ (binding.initial); + + // Theoretically someone could import a class from a `.svelte.js` module, but that's too rare to worry about + if ( + /** @type {string} */ (declaration.source.value)?.endsWith('.svelte') && + declaration.specifiers.find( + (s) => s.local.name === binding.node.name && s.type === 'ImportDefaultSpecifier' + ) + ) { + w.legacy_component_creation(node.expression); + } + } + } + }, MemberExpression(node, context) { if (node.object.type === 'Identifier' && node.property.type === 'Identifier') { const binding = context.state.scope.get(node.object.name); diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index ca3eaf13f6..facd53cdec 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -96,6 +96,7 @@ export const codes = [ "options_renamed_ssr_dom", "derived_iife", "export_let_unused", + "legacy_component_creation", "non_reactive_update", "perf_avoid_inline_class", "perf_avoid_nested_class", @@ -586,6 +587,14 @@ export function export_let_unused(node, name) { w(node, "export_let_unused", `Component has unused export property '${name}'. If it is for external reference only, please consider using \`export const ${name}\``); } +/** + * Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead. + * @param {null | NodeLike} node + */ +export function legacy_component_creation(node) { + w(node, "legacy_component_creation", "Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead."); +} + /** * `%name%` is updated, but is not declared with `$state(...)`. Changing its value will not correctly trigger updates * @param {null | NodeLike} node diff --git a/packages/svelte/tests/validator/samples/component-legacy-instantiation/input.svelte b/packages/svelte/tests/validator/samples/component-legacy-instantiation/input.svelte new file mode 100644 index 0000000000..e0bf24f348 --- /dev/null +++ b/packages/svelte/tests/validator/samples/component-legacy-instantiation/input.svelte @@ -0,0 +1,13 @@ + diff --git a/packages/svelte/tests/validator/samples/component-legacy-instantiation/warnings.json b/packages/svelte/tests/validator/samples/component-legacy-instantiation/warnings.json new file mode 100644 index 0000000000..9ec8f9fa3e --- /dev/null +++ b/packages/svelte/tests/validator/samples/component-legacy-instantiation/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "legacy_component_creation", + "message": "Svelte 5 components are no longer classes. Instantiate them using `mount` or `hydrate` (imported from 'svelte') instead.", + "start": { + "column": 1, + "line": 7 + }, + "end": { + "column": 26, + "line": 7 + } + } +] From 65234b80c04c8915e84a32b34039d3b33190a4fe Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 29 Jul 2024 09:57:18 -0400 Subject: [PATCH 029/196] chore: unified expression metadata (#12644) * add ExpressionMetadata type * use unified reactivity metadata * simplify component * rename * more consistency * store bindings on expression metadata * tidy * regenerate types * remove junk from options node * reuse * tweak jsdoc --- packages/svelte/src/compiler/index.js | 18 +++- .../compiler/phases/1-parse/state/element.js | 28 ++---- .../src/compiler/phases/1-parse/state/tag.js | 4 +- .../src/compiler/phases/2-analyze/index.js | 56 +++++++----- .../src/compiler/phases/2-analyze/types.d.ts | 5 +- .../3-transform/client/visitors/template.js | 87 ++++++++++--------- .../server/visitors/shared/element.js | 16 ++-- packages/svelte/src/compiler/phases/nodes.js | 13 ++- packages/svelte/src/compiler/types/index.d.ts | 9 ++ .../svelte/src/compiler/types/template.d.ts | 21 ++--- .../parser-modern/samples/options/output.json | 20 +---- packages/svelte/types/index.d.ts | 28 +++--- 12 files changed, 160 insertions(+), 145 deletions(-) diff --git a/packages/svelte/src/compiler/index.js b/packages/svelte/src/compiler/index.js index 594e93fc21..e18c3c2ec3 100644 --- a/packages/svelte/src/compiler/index.js +++ b/packages/svelte/src/compiler/index.js @@ -119,13 +119,23 @@ export function parse(source, { filename, rootDir, modern } = {}) { */ function to_public_ast(source, ast, modern) { if (modern) { + const clean = (/** @type {any} */ node) => { + delete node.metadata; + delete node.parent; + }; + + ast.options?.attributes.forEach((attribute) => { + clean(attribute); + clean(attribute.value); + if (Array.isArray(attribute.value)) { + attribute.value.forEach(clean); + } + }); + // remove things that we don't want to treat as public API return zimmerframe_walk(ast, null, { _(node, { next }) { - // @ts-ignore - delete node.parent; - // @ts-ignore - delete node.metadata; + clean(node); next(); } }); 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 11a9de0951..17e999b96c 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/element.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/element.js @@ -9,7 +9,7 @@ import { decode_character_references } from '../utils/html.js'; import * as e from '../../../errors.js'; import * as w from '../../../warnings.js'; import { create_fragment } from '../utils/create.js'; -import { create_attribute } from '../../nodes.js'; +import { create_attribute, create_expression_metadata } from '../../nodes.js'; import { get_attribute_expression, is_expression_attribute } from '../../../utils/ast.js'; import { closing_tag_omitted } from '../../../../html-tree-validation.js'; @@ -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]) || name === 'svelte:self' || name === 'svelte:component' + : regex_capital_letter.test(name[0]) ? 'Component' : name === 'title' && parent_is_head(parser.stack) ? 'TitleElement' @@ -140,7 +140,7 @@ export default function element(parser) { const element = type === 'RegularElement' ? { - type: type, + type, start, end: -1, name, @@ -163,7 +163,7 @@ export default function element(parser) { fragment: create_fragment(true), parent: null, metadata: { - svg: false + // unpopulated at first, differs between types } }); @@ -508,8 +508,7 @@ function read_attribute(parser) { expression, parent: null, metadata: { - contains_call_expression: false, - dynamic: false + expression: create_expression_metadata() } }; @@ -538,8 +537,7 @@ function read_attribute(parser) { }, parent: null, metadata: { - dynamic: false, - contains_call_expression: false + expression: create_expression_metadata() } }; @@ -584,7 +582,7 @@ function read_attribute(parser) { value, parent: null, metadata: { - dynamic: false + expression: create_expression_metadata() } }; } @@ -616,17 +614,10 @@ function read_attribute(parser) { modifiers, expression, metadata: { - dynamic: false, - contains_call_expression: false + expression: create_expression_metadata() } }; - if (directive.type === 'ClassDirective') { - directive.metadata = { - dynamic: false - }; - } - if (directive.type === 'TransitionDirective') { const direction = name.slice(0, colon_index); directive.intro = direction === 'in' || direction === 'transition'; @@ -789,8 +780,7 @@ function read_sequence(parser, done, location) { expression, parent: null, metadata: { - contains_call_expression: false, - dynamic: false + expression: create_expression_metadata() } }; diff --git a/packages/svelte/src/compiler/phases/1-parse/state/tag.js b/packages/svelte/src/compiler/phases/1-parse/state/tag.js index 4ccfe2cb5f..aacaf29440 100644 --- a/packages/svelte/src/compiler/phases/1-parse/state/tag.js +++ b/packages/svelte/src/compiler/phases/1-parse/state/tag.js @@ -7,6 +7,7 @@ import * as e from '../../../errors.js'; import { create_fragment } from '../utils/create.js'; import { walk } from 'zimmerframe'; import { parse_expression_at } from '../acorn.js'; +import { create_expression_metadata } from '../../nodes.js'; const regex_whitespace_with_closing_curly_brace = /^\s*}/; @@ -39,8 +40,7 @@ export default function tag(parser) { end: parser.index, expression, metadata: { - contains_call_expression: false, - dynamic: false + expression: create_expression_metadata() } }); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index aac76faa60..4e5a03089c 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -1178,20 +1178,19 @@ const common_visitors = { context.next(); - node.metadata.dynamic = get_attribute_chunks(node.value).some((chunk) => { - if (chunk.type !== 'ExpressionTag') { - return false; - } + for (const chunk of get_attribute_chunks(node.value)) { + if (chunk.type !== 'ExpressionTag') continue; if ( chunk.expression.type === 'FunctionExpression' || chunk.expression.type === 'ArrowFunctionExpression' ) { - return false; + continue; } - return chunk.metadata.dynamic || chunk.metadata.contains_call_expression; - }); + node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; + node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + } if (is_event_attribute(node)) { const parent = context.path.at(-1); @@ -1211,10 +1210,10 @@ const common_visitors = { } }, ClassDirective(node, context) { - context.next({ ...context.state, expression: node }); + context.next({ ...context.state, expression: node.metadata.expression }); }, SpreadAttribute(node, context) { - context.next({ ...context.state, expression: node }); + context.next({ ...context.state, expression: node.metadata.expression }); }, SlotElement(node, context) { let name = 'default'; @@ -1230,17 +1229,21 @@ const common_visitors = { if (node.value === true) { const binding = context.state.scope.get(node.name); if (binding?.kind !== 'normal') { - node.metadata.dynamic = true; + node.metadata.expression.has_state = true; } } else { context.next(); - node.metadata.dynamic = get_attribute_chunks(node.value).some( - (node) => node.type === 'ExpressionTag' && node.metadata.dynamic - ); + + for (const chunk of get_attribute_chunks(node.value)) { + if (chunk.type !== 'ExpressionTag') continue; + + node.metadata.expression.has_state ||= chunk.metadata.expression.has_state; + node.metadata.expression.has_call ||= chunk.metadata.expression.has_call; + } } }, ExpressionTag(node, context) { - context.next({ ...context.state, expression: node }); + context.next({ ...context.state, expression: node.metadata.expression }); }, Identifier(node, context) { const parent = /** @type {Node} */ (context.path.at(-1)); @@ -1261,10 +1264,18 @@ const common_visitors = { const binding = context.state.scope.get(node.name); + if (binding && context.state.expression) { + context.state.expression.dependencies.add(binding); + + if (binding.kind !== 'normal') { + context.state.expression.has_state = true; + } + } + // if no binding, means some global variable if (binding && binding.kind !== 'normal') { if (context.state.expression) { - context.state.expression.metadata.dynamic = true; + context.state.expression.has_state = true; } // TODO it would be better to just bail out when we hit the ExportSpecifier node but that's @@ -1297,13 +1308,10 @@ const common_visitors = { }, CallExpression(node, context) { const { expression, render_tag } = context.state; - if ( - (expression?.type === 'ExpressionTag' || - expression?.type === 'SpreadAttribute' || - expression?.type === 'OnDirective') && - !is_known_safe_call(node, context) - ) { - expression.metadata.contains_call_expression = true; + + if (expression && !is_known_safe_call(node, context)) { + expression.has_call = true; + expression.has_state = true; } if (render_tag) { @@ -1354,7 +1362,7 @@ const common_visitors = { }, MemberExpression(node, context) { if (context.state.expression) { - context.state.expression.metadata.dynamic = true; + context.state.expression.has_state = true; } if (!is_safe_identifier(node, context.state.scope)) { @@ -1368,7 +1376,7 @@ const common_visitors = { if (parent?.type === 'SvelteElement' || parent?.type === 'RegularElement') { state.analysis.event_directive_node ??= node; } - next({ ...state, expression: node }); + next({ ...state, expression: node.metadata.expression }); }, BindDirective(node, context) { let i = context.path.length; diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index feacbdedcc..e03b9c9cc4 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -2,6 +2,7 @@ import type { Scope } from '../scope.js'; import type { ComponentAnalysis, ReactiveStatement } from '../types.js'; import type { ClassDirective, + ExpressionMetadata, ExpressionTag, OnDirective, RenderTag, @@ -20,8 +21,8 @@ export interface AnalysisState { has_props_rune: boolean; /** Which slots the current parent component has */ component_slots: Set; - /** The current {expression}, if any */ - expression: ExpressionTag | ClassDirective | OnDirective | SpreadAttribute | null; + /** Information about the current expression/directive/block value */ + expression: ExpressionMetadata | null; /** The current {@render ...} tag, if any */ render_tag: null | RenderTag; private_derived_state: string[]; diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 9006d68aca..dcfa23a99d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1,5 +1,5 @@ /** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */ -/** @import { Attribute, BindDirective, Binding, ClassDirective, Component, DelegatedEvent, EachBlock, ExpressionTag, Namespace, OnDirective, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text } from '#compiler' */ +/** @import { Attribute, BindDirective, Binding, ClassDirective, Component, DelegatedEvent, EachBlock, ExpressionMetadata, ExpressionTag, Namespace, OnDirective, RegularElement, SpreadAttribute, StyleDirective, SvelteComponent, SvelteElement, SvelteNode, SvelteSelf, TemplateNode, Text } from '#compiler' */ /** @import { SourceLocation } from '#shared' */ /** @import { Scope } from '../../../scope.js' */ /** @import { ComponentClientTransformState, ComponentContext, ComponentVisitors } from '../types.js' */ @@ -103,13 +103,11 @@ function serialize_style_directives(style_directives, element_id, context, is_at ) ); - const contains_call_expression = get_attribute_chunks(directive.value).some( - (v) => v.type === 'ExpressionTag' && v.metadata.contains_call_expression - ); + const { has_state, has_call } = directive.metadata.expression; - if (!is_attributes_reactive && contains_call_expression) { + if (!is_attributes_reactive && has_call) { state.init.push(serialize_update(update)); - } else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { + } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); } else { state.init.push(update); @@ -151,11 +149,12 @@ function serialize_class_directives(class_directives, element_id, context, is_at for (const directive of class_directives) { const value = /** @type {Expression} */ (context.visit(directive.expression)); const update = b.stmt(b.call('$.toggle_class', element_id, b.literal(directive.name), value)); - const contains_call_expression = directive.expression.type === 'CallExpression'; - if (!is_attributes_reactive && contains_call_expression) { + const { has_state, has_call } = directive.metadata.expression; + + if (!is_attributes_reactive && has_call) { state.init.push(serialize_update(update)); - } else if (is_attributes_reactive || directive.metadata.dynamic || contains_call_expression) { + } else if (is_attributes_reactive || has_state || has_call) { state.update.push(update); } else { state.init.push(update); @@ -277,7 +276,7 @@ function serialize_element_spread_attributes( for (const attribute of attributes) { if (attribute.type === 'Attribute') { const name = get_attribute_name(element, attribute, context); - // TODO: handle contains_call_expression + // TODO: handle has_call const [, value] = serialize_attribute_value(attribute.value, context); if ( @@ -306,7 +305,7 @@ function serialize_element_spread_attributes( } needs_isolation ||= - attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; + attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call; } const lowercase_attributes = @@ -405,11 +404,11 @@ function serialize_dynamic_element_attributes(attributes, context, element_id) { } is_reactive ||= - attribute.metadata.dynamic || + attribute.metadata.expression.has_state || // objects could contain reactive getters -> play it safe and always assume spread attributes are reactive attribute.type === 'SpreadAttribute'; needs_isolation ||= - attribute.type === 'SpreadAttribute' && attribute.metadata.contains_call_expression; + attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call; } if (needs_isolation || is_reactive) { @@ -486,13 +485,13 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu const name = get_attribute_name(element, attribute, context); const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg'; const is_mathml = context.state.metadata.namespace === 'mathml'; - let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); + let [has_call, value] = serialize_attribute_value(attribute.value, context); // The foreign namespace doesn't have any special handling, everything goes through the attr function if (context.state.metadata.namespace === 'foreign') { const statement = b.stmt(b.call('$.set_attribute', node_id, b.literal(name), value)); - if (attribute.metadata.dynamic) { + if (attribute.metadata.expression.has_state) { const id = state.scope.generate(`${node_id.name}_${name}`); serialize_update_assignment(state, id, undefined, value, statement); return true; @@ -529,8 +528,8 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu update = b.stmt(b.call(callee, node_id, b.literal(name), value)); } - if (attribute.metadata.dynamic) { - if (contains_call_expression) { + if (attribute.metadata.expression.has_state) { + if (has_call) { state.init.push(serialize_update(update)); } else { state.update.push(update); @@ -552,12 +551,12 @@ function serialize_element_attribute_update_assignment(element, node_id, attribu function serialize_custom_element_attribute_update_assignment(node_id, attribute, context) { const state = context.state; const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive - let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); + let [has_call, value] = serialize_attribute_value(attribute.value, context); const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); - if (attribute.metadata.dynamic) { - if (contains_call_expression) { + if (attribute.metadata.expression.has_state) { + if (has_call) { state.init.push(serialize_update(update)); } else { state.update.push(update); @@ -592,7 +591,7 @@ function serialize_element_special_value_attribute(element, node_id, attribute, value ) ); - const is_reactive = attribute.metadata.dynamic; + const is_reactive = attribute.metadata.expression.has_state; const is_select_with_value = // attribute.metadata.dynamic would give false negatives because even if the value does not change, // the inner options could still change, so we need to always treat it as reactive @@ -723,10 +722,10 @@ function serialize_inline_component(node, component_name, context, anchor = cont events[attribute.name].push(handler); } else if (attribute.type === 'SpreadAttribute') { const expression = /** @type {Expression} */ (context.visit(attribute)); - if (attribute.metadata.dynamic) { + if (attribute.metadata.expression.has_state) { let value = expression; - if (attribute.metadata.contains_call_expression) { + if (attribute.metadata.expression.has_call) { const id = b.id(context.state.scope.generate('spread_element')); context.state.init.push(b.var(id, b.call('$.derived', b.thunk(value)))); value = b.call('$.get', id); @@ -754,7 +753,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont const [, value] = serialize_attribute_value(attribute.value, context); - if (attribute.metadata.dynamic) { + if (attribute.metadata.expression.has_state) { let arg = value; // When we have a non-simple computation, anything other than an Identifier or Member expression, @@ -1118,7 +1117,7 @@ function serialize_render_stmt(update) { /** * Serializes the event handler function of the `on:` directive * @param {Pick} node - * @param {null | { contains_call_expression: boolean; dynamic: boolean; } | null} metadata + * @param {null | ExpressionMetadata} metadata * @param {ComponentContext} context */ function serialize_event_handler(node, metadata, { state, visit }) { @@ -1145,7 +1144,7 @@ function serialize_event_handler(node, metadata, { state, visit }) { ); if ( - metadata?.contains_call_expression && + metadata?.has_call && !( (handler.type === 'ArrowFunctionExpression' || handler.type === 'FunctionExpression') && handler.metadata.hoistable @@ -1227,7 +1226,7 @@ function serialize_event_handler(node, metadata, { state, visit }) { /** * Serializes an event handler function of the `on:` directive or an attribute starting with `on` * @param {{name: string;modifiers: string[];expression: Expression | null;delegated?: DelegatedEvent | null;}} node - * @param {null | { contains_call_expression: boolean; dynamic: boolean; }} metadata + * @param {null | ExpressionMetadata} metadata * @param {ComponentContext} context */ function serialize_event(node, metadata, context) { @@ -1357,7 +1356,9 @@ function serialize_event_attribute(node, context) { modifiers, delegated: node.metadata.delegated }, - !Array.isArray(node.value) && node.value?.type === 'ExpressionTag' ? node.value.metadata : null, + !Array.isArray(node.value) && node.value?.type === 'ExpressionTag' + ? node.value.metadata.expression + : null, context ); } @@ -1401,9 +1402,9 @@ function process_children(nodes, expression, is_element, { visit, state }) { b.call('$.set_text', text_id, /** @type {Expression} */ (visit(node.expression, state))) ); - if (node.metadata.contains_call_expression && !within_bound_contenteditable) { + if (node.metadata.expression.has_call && !within_bound_contenteditable) { state.init.push(serialize_update(update)); - } else if (node.metadata.dynamic && !within_bound_contenteditable) { + } else if (node.metadata.expression.has_state && !within_bound_contenteditable) { state.update.push(update); } else { state.init.push( @@ -1424,14 +1425,16 @@ function process_children(nodes, expression, is_element, { visit, state }) { state.template.push(' '); - const [contains_call_expression, value] = serialize_template_literal(sequence, visit, state); + const [has_call, value] = serialize_template_literal(sequence, visit, state); const update = b.stmt(b.call('$.set_text', text_id, value)); - if (contains_call_expression && !within_bound_contenteditable) { + if (has_call && !within_bound_contenteditable) { state.init.push(serialize_update(update)); } else if ( - sequence.some((node) => node.type === 'ExpressionTag' && node.metadata.dynamic) && + sequence.some( + (node) => node.type === 'ExpressionTag' && node.metadata.expression.has_state + ) && !within_bound_contenteditable ) { state.update.push(update); @@ -1517,7 +1520,7 @@ function get_node_id(expression, state, name) { /** * @param {Attribute['value']} value * @param {ComponentContext} context - * @returns {[contains_call_expression: boolean, Expression]} + * @returns {[has_call: boolean, Expression]} */ function serialize_attribute_value(value, context) { if (value === true) { @@ -1532,7 +1535,7 @@ function serialize_attribute_value(value, context) { } return [ - chunk.metadata.contains_call_expression, + chunk.metadata.expression.has_call, /** @type {Expression} */ (context.visit(chunk.expression)) ]; } @@ -1552,18 +1555,18 @@ function serialize_template_literal(values, visit, state) { /** @type {Expression[]} */ const expressions = []; - let contains_call_expression = false; + let has_call = false; let contains_multiple_call_expression = false; quasis.push(b.quasi('')); for (let i = 0; i < values.length; i++) { const node = values[i]; - if (node.type === 'ExpressionTag' && node.metadata.contains_call_expression) { - if (contains_call_expression) { + if (node.type === 'ExpressionTag' && node.metadata.expression.has_call) { + if (has_call) { contains_multiple_call_expression = true; } - contains_call_expression = true; + has_call = true; } } @@ -1600,7 +1603,7 @@ function serialize_template_literal(values, visit, state) { } // TODO instead of this tuple, return a `{ dynamic, complex, value }` object. will DRY stuff out - return [contains_call_expression, b.template(quasis, expressions)]; + return [has_call, b.template(quasis, expressions)]; } /** @type {ComponentVisitors} */ @@ -2855,7 +2858,7 @@ export const template_visitors = { context.next({ ...context.state, in_constructor: false }); }, OnDirective(node, context) { - serialize_event(node, node.metadata, context); + serialize_event(node, node.metadata.expression, context); }, UseDirective(node, { state, next, visit }) { const params = [b.id('$$node')]; @@ -3238,7 +3241,7 @@ export const template_visitors = { name = value; is_default = false; } else if (attribute.name !== 'slot') { - if (attribute.metadata.dynamic) { + if (attribute.metadata.expression.has_state) { props.push(b.get(attribute.name, [b.return(value)])); } else { props.push(b.init(attribute.name, value)); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index fbfa4497b0..052b3c5d6f 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -12,7 +12,11 @@ import { LoadErrorElements, WhitespaceInsensitiveAttributes } from '../../../../constants.js'; -import { create_attribute, is_custom_element_node } from '../../../../nodes.js'; +import { + create_attribute, + create_expression_metadata, + is_custom_element_node +} from '../../../../nodes.js'; import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '../../../../../utils/builders.js'; import { @@ -142,8 +146,7 @@ export function serialize_element_attributes(node, context) { serialize_attribute_value(value_attribute.value, context) ), metadata: { - contains_call_expression: false, - dynamic: false + expression: create_expression_metadata() } } ]) @@ -158,8 +161,7 @@ export function serialize_element_attributes(node, context) { parent: attribute, expression: attribute.expression, metadata: { - contains_call_expression: false, - dynamic: false + expression: create_expression_metadata() } } ]) @@ -399,7 +401,9 @@ function serialize_class_directives(class_directives, class_attribute) { ), b.literal(' ') ), - metadata: { contains_call_expression: false, dynamic: false } + metadata: { + expression: create_expression_metadata() + } }); class_attribute.value = chunks; diff --git a/packages/svelte/src/compiler/phases/nodes.js b/packages/svelte/src/compiler/phases/nodes.js index bb69b9749a..6237f22899 100644 --- a/packages/svelte/src/compiler/phases/nodes.js +++ b/packages/svelte/src/compiler/phases/nodes.js @@ -45,8 +45,19 @@ export function create_attribute(name, start, end, value) { value, parent: null, metadata: { - dynamic: false, + expression: create_expression_metadata(), delegated: null } }; } + +/** + * @returns {Compiler.ExpressionMetadata} + */ +export function create_expression_metadata() { + return { + dependencies: new Set(), + has_state: false, + has_call: false + }; +} diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index c0adbdaa7b..c7bf2ff3e1 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -316,6 +316,15 @@ export interface Binding { } | null; } +export interface ExpressionMetadata { + /** All the bindings that are referenced inside this expression */ + dependencies: Set; + /** True if the expression references state directly, or _might_ (via member/call expressions) */ + has_state: boolean; + /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ + has_call: boolean; +} + export * from './template.js'; export { Css }; diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index e5bc072c1a..ee6315a98c 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -1,4 +1,4 @@ -import type { Binding, Css } from '#compiler'; +import type { Binding, Css, ExpressionMetadata } from '#compiler'; import type { ArrayExpression, ArrowFunctionExpression, @@ -111,12 +111,7 @@ export interface ExpressionTag extends BaseNode { type: 'ExpressionTag'; expression: Expression; metadata: { - contains_call_expression: boolean; - /** - * Whether or not the expression contains any dynamic references — - * determines whether it will be updated in a render effect or not - */ - dynamic: boolean; + expression: ExpressionMetadata; }; } @@ -190,7 +185,7 @@ export interface ClassDirective extends BaseNode { /** The 'y' in `class:x={y}`, or the `x` in `class:x` */ expression: Expression; metadata: { - dynamic: false; + expression: ExpressionMetadata; }; } @@ -212,8 +207,7 @@ export interface OnDirective extends BaseNode { expression: null | Expression; modifiers: string[]; // TODO specify metadata: { - contains_call_expression: boolean; - dynamic: boolean; + expression: ExpressionMetadata; }; } @@ -233,7 +227,7 @@ export interface StyleDirective extends BaseNode { value: true | ExpressionTag | Array; modifiers: Array<'important'>; metadata: { - dynamic: boolean; + expression: ExpressionMetadata; }; } @@ -454,7 +448,7 @@ export interface Attribute extends BaseNode { name: string; value: true | ExpressionTag | Array; metadata: { - dynamic: boolean; + expression: ExpressionMetadata; /** May be set if this is an event attribute */ delegated: null | DelegatedEvent; }; @@ -464,8 +458,7 @@ export interface SpreadAttribute extends BaseNode { type: 'SpreadAttribute'; expression: Expression; metadata: { - contains_call_expression: boolean; - dynamic: boolean; + expression: ExpressionMetadata; }; } diff --git a/packages/svelte/tests/parser-modern/samples/options/output.json b/packages/svelte/tests/parser-modern/samples/options/output.json index 708bf0cd1a..23ccf3b2f7 100644 --- a/packages/svelte/tests/parser-modern/samples/options/output.json +++ b/packages/svelte/tests/parser-modern/samples/options/output.json @@ -39,15 +39,9 @@ "end": 48, "type": "Text", "raw": "my-custom-element", - "data": "my-custom-element", - "parent": null + "data": "my-custom-element" } - ], - "parent": null, - "metadata": { - "dynamic": false, - "delegated": null - } + ] }, { "type": "Attribute", @@ -74,17 +68,7 @@ }, "value": true, "raw": "true" - }, - "parent": null, - "metadata": { - "contains_call_expression": false, - "dynamic": false } - }, - "parent": null, - "metadata": { - "dynamic": false, - "delegated": null } } ], diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index d9537235f6..78ffc17748 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -965,6 +965,15 @@ declare module 'svelte/compiler' { inside_rest?: boolean; } | null; } + + interface ExpressionMetadata { + /** All the bindings that are referenced inside this expression */ + dependencies: Set; + /** True if the expression references state directly, or _might_ (via member/call expressions) */ + has_state: boolean; + /** True if the expression involves a call expression (often, it will need to be wrapped in a derived) */ + has_call: boolean; + } /** * The preprocess function provides convenient hooks for arbitrarily transforming component source code. * For example, it can be used to convert a diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_config.js b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_config.js new file mode 100644 index 0000000000..286454e931 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + css: 'external' + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css new file mode 100644 index 0000000000..bddefdd00c --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.css @@ -0,0 +1,4 @@ + +.foo.svelte-sg04hs { + color: red; +} diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html new file mode 100644 index 0000000000..1f0b2b95fe --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected.html @@ -0,0 +1 @@ +
bar
foo
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html new file mode 100644 index 0000000000..806f6c2db2 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/_expected_head.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/main.svelte b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/main.svelte new file mode 100644 index 0000000000..a2b9f38de3 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options-nested/main.svelte @@ -0,0 +1,13 @@ + + + + +
foo
+ + diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_config.js b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_config.js new file mode 100644 index 0000000000..286454e931 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + css: 'external' + } +}); diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css new file mode 100644 index 0000000000..8882c6ec7e --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.css @@ -0,0 +1,4 @@ + + .foo.svelte-sg04hs { + color: red; + } diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html new file mode 100644 index 0000000000..8ebe1ad73e --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected.html @@ -0,0 +1 @@ +
foo
\ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html new file mode 100644 index 0000000000..7281935646 --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/_expected_head.html @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/packages/svelte/tests/server-side-rendering/samples/css-injected-options/main.svelte b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/main.svelte new file mode 100644 index 0000000000..7387067b9b --- /dev/null +++ b/packages/svelte/tests/server-side-rendering/samples/css-injected-options/main.svelte @@ -0,0 +1,9 @@ + + +
foo
+ + diff --git a/packages/svelte/tests/validator/samples/namespace-invalid/errors.json b/packages/svelte/tests/validator/samples/namespace-invalid/errors.json index ae98ee7e25..bcd2dc1d9c 100644 --- a/packages/svelte/tests/validator/samples/namespace-invalid/errors.json +++ b/packages/svelte/tests/validator/samples/namespace-invalid/errors.json @@ -1,7 +1,7 @@ [ { "code": "svelte_options_invalid_attribute_value", - "message": "Valid values are \"html\", \"mathml\", \"svg\" or \"foreign\"", + "message": "Value must be \"html\", \"mathml\", \"svg\" or \"foreign\", if specified", "start": { "line": 1, "column": 16 diff --git a/packages/svelte/tests/validator/samples/namespace-non-literal/errors.json b/packages/svelte/tests/validator/samples/namespace-non-literal/errors.json index 439dbd032b..d7d637fbb1 100644 --- a/packages/svelte/tests/validator/samples/namespace-non-literal/errors.json +++ b/packages/svelte/tests/validator/samples/namespace-non-literal/errors.json @@ -1,7 +1,7 @@ [ { "code": "svelte_options_invalid_attribute_value", - "message": "Valid values are \"html\", \"mathml\", \"svg\" or \"foreign\"", + "message": "Value must be \"html\", \"mathml\", \"svg\" or \"foreign\", if specified", "start": { "line": 1, "column": 16 diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 8ba68e1d2d..8212c52cc0 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1533,6 +1533,7 @@ declare module 'svelte/compiler' { accessors?: boolean; preserveWhitespace?: boolean; namespace?: Namespace; + css?: 'injected'; customElement?: { tag: string; shadow?: 'open' | 'none'; From 3286617e3cc64a4ac44e3262b2c6d70939c4b727 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Fri, 2 Aug 2024 15:25:32 +0200 Subject: [PATCH 058/196] feat: function called as tagged template literal is reactively called (#12692) * feat: function called as tagged template literal is reactively called Co-authored-by: Oscar Dominguez * chore: re-organize import of visitors * simplify --------- Co-authored-by: Oscar Dominguez Co-authored-by: Rich Harris --- .changeset/serious-owls-think.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 2 ++ .../2-analyze/visitors/CallExpression.js | 29 ++----------------- .../visitors/TaggedTemplateExpression.js | 23 +++++++++++++++ .../phases/2-analyze/visitors/shared/utils.js | 25 +++++++++++++++- .../_config.js | 17 +++++++++++ .../main.svelte | 8 +++++ 7 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 .changeset/serious-owls-think.md create mode 100644 packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js create mode 100644 packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/main.svelte diff --git a/.changeset/serious-owls-think.md b/.changeset/serious-owls-think.md new file mode 100644 index 0000000000..7093f5af4c --- /dev/null +++ b/.changeset/serious-owls-think.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: function called as tagged template literal is reactively called diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 6d90ca1617..40e8719a40 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -57,6 +57,7 @@ import { SvelteElement } from './visitors/SvelteElement.js'; import { SvelteFragment } from './visitors/SvelteFragment.js'; import { SvelteHead } from './visitors/SvelteHead.js'; import { SvelteSelf } from './visitors/SvelteSelf.js'; +import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js'; import { Text } from './visitors/Text.js'; import { TitleElement } from './visitors/TitleElement.js'; import { UpdateExpression } from './visitors/UpdateExpression.js'; @@ -160,6 +161,7 @@ const visitors = { SvelteFragment, SvelteComponent, SvelteSelf, + TaggedTemplateExpression, Text, TitleElement, UpdateExpression, diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 9d5a3ef5c1..98b9a3d7ec 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -4,7 +4,7 @@ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; import { get_parent, unwrap_optional } from '../../../utils/ast.js'; -import { is_safe_identifier } from './shared/utils.js'; +import { is_known_safe_call, is_safe_identifier } from './shared/utils.js'; /** * @param {CallExpression} node @@ -150,7 +150,7 @@ export function CallExpression(node, context) { break; } - if (context.state.expression && !is_known_safe_call(node, context)) { + if (context.state.expression && !is_known_safe_call(node.callee, context)) { context.state.expression.has_call = true; context.state.expression.has_state = true; } @@ -182,28 +182,3 @@ export function CallExpression(node, context) { context.next(); } } - -/** - * @param {CallExpression} node - * @param {Context} context - * @returns {boolean} - */ -function is_known_safe_call(node, context) { - const callee = node.callee; - - // String / Number / BigInt / Boolean casting calls - if (callee.type === 'Identifier') { - const name = callee.name; - const binding = context.state.scope.get(name); - if ( - binding === null && - (name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean') - ) { - return true; - } - } - - // TODO add more cases - - return false; -} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js new file mode 100644 index 0000000000..b079b67512 --- /dev/null +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js @@ -0,0 +1,23 @@ +/** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */ +/** @import { Context } from '../types' */ +import { is_known_safe_call } from './shared/utils.js'; + +/** + * @param {TaggedTemplateExpression} node + * @param {Context} context + */ +export function TaggedTemplateExpression(node, context) { + if (context.state.expression && !is_known_safe_call(node.tag, context)) { + context.state.expression.has_call = true; + context.state.expression.has_state = true; + } + + if (node.tag.type === 'Identifier') { + const binding = context.state.scope.get(node.tag.name); + + if (binding !== null) { + binding.is_called = true; + } + } + context.next(); +} diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index dd790665ce..2980ac3cbe 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -1,4 +1,4 @@ -/** @import { AssignmentExpression, Expression, Pattern, PrivateIdentifier, Super, UpdateExpression, VariableDeclarator } from 'estree' */ +/** @import { AssignmentExpression, CallExpression, Expression, Pattern, PrivateIdentifier, Super, TaggedTemplateExpression, UpdateExpression, VariableDeclarator } from 'estree' */ /** @import { Fragment } from '#compiler' */ /** @import { AnalysisState, Context } from '../../types' */ /** @import { Scope } from '../../../scope' */ @@ -165,3 +165,26 @@ export function is_safe_identifier(expression, scope) { binding.kind !== 'rest_prop' ); } + +/** + * @param {Expression | Super} callee + * @param {Context} context + * @returns {boolean} + */ +export function is_known_safe_call(callee, context) { + // String / Number / BigInt / Boolean casting calls + if (callee.type === 'Identifier') { + const name = callee.name; + const binding = context.state.scope.get(name); + if ( + binding === null && + (name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean') + ) { + return true; + } + } + + // TODO add more cases + + return false; +} diff --git a/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/_config.js b/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/_config.js new file mode 100644 index 0000000000..beef0bbff2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/_config.js @@ -0,0 +1,17 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + mode: ['client'], + async test({ assert, target, ok }) { + const button = target.querySelector('button'); + + assert.htmlEqual(target.innerHTML, `0 `); + + flushSync(() => { + button?.click(); + }); + + assert.htmlEqual(target.innerHTML, `1 `); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/main.svelte b/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/main.svelte new file mode 100644 index 0000000000..7b9da329c2 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/tagged-template-literal-reactive/main.svelte @@ -0,0 +1,8 @@ + + +{showCount``} From 8e04a91c6701aef2b41bf99c157107458f82ef23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 12:28:04 -0400 Subject: [PATCH 059/196] Version Packages (next) (#12707) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 2 ++ packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index dd88b32b44..ef858b3956 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -135,6 +135,7 @@ "early-ads-tie", "early-months-whisper", "eight-carrots-hunt", + "eight-comics-tell", "eight-cougars-watch", "eight-hornets-punch", "eight-jeans-compare", @@ -521,6 +522,7 @@ "serious-gorillas-eat", "serious-kids-deliver", "serious-needles-joke", + "serious-owls-think", "serious-poems-brake", "serious-socks-cover", "serious-zebras-scream", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 3fcd9bdc0d..1007ba88b1 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 5.0.0-next.208 + +### Patch Changes + +- feat: add support for `` ([#12660](https://github.com/sveltejs/svelte/pull/12660)) + +- feat: function called as tagged template literal is reactively called ([#12692](https://github.com/sveltejs/svelte/pull/12692)) + ## 5.0.0-next.207 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 40260b3143..1f3cdb86e1 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.207", + "version": "5.0.0-next.208", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 8f1c8a89ea..c70a430439 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.207'; +export const VERSION = '5.0.0-next.208'; export const PUBLIC_VERSION = '5'; From 93cfa6cd6969956f32c6bdcc6153949110432eac Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Sat, 3 Aug 2024 16:47:21 +0200 Subject: [PATCH 060/196] fix: add css hash to custom element rendered with `svelte:element` (#12715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: add css hash to custom element rendered with `svelte:element` * simplify * skip arg where possible * drive-by improvements — remove some unnecessary arguments where possible --------- Co-authored-by: Rich Harris --- .changeset/chilly-carpets-switch.md | 5 +++ .../client/visitors/RegularElement.js | 10 +++--- .../client/visitors/SvelteElement.js | 4 +-- .../client/dom/elements/attributes.js | 35 +++++++++++-------- .../_config.js | 14 ++++++++ .../main.svelte | 8 +++++ 6 files changed, 53 insertions(+), 23 deletions(-) create mode 100644 .changeset/chilly-carpets-switch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/main.svelte diff --git a/.changeset/chilly-carpets-switch.md b/.changeset/chilly-carpets-switch.md new file mode 100644 index 0000000000..ca8660969a --- /dev/null +++ b/.changeset/chilly-carpets-switch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: add css hash to custom element rendered with `svelte:element` 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 b14c9bea9b..13ebb41d5b 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 @@ -470,10 +470,8 @@ function build_element_spread_attributes( attribute.type === 'SpreadAttribute' && attribute.metadata.expression.has_call; } - const lowercase_attributes = - element.metadata.svg || element.metadata.mathml || is_custom_element_node(element) - ? b.false - : b.true; + const preserve_attribute_case = + element.metadata.svg || element.metadata.mathml || is_custom_element_node(element); const id = context.state.scope.generate('attributes'); const update = b.stmt( @@ -485,8 +483,8 @@ function build_element_spread_attributes( element_id, b.id(id), b.object(values), - lowercase_attributes, - b.literal(context.state.analysis.css.hash), + context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), + preserve_attribute_case && b.true, is_ignored(element, 'hydration_attribute_changed') && b.true ) ) 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 23f6ea5b6b..72174d1015 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 @@ -200,7 +200,7 @@ function build_dynamic_element_attributes(attributes, context, element_id) { element_id, b.id(id), b.object(values), - b.literal(context.state.analysis.css.hash) + context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash) ) ) ); @@ -221,7 +221,7 @@ function build_dynamic_element_attributes(attributes, context, element_id) { element_id, b.literal(null), b.object(values), - b.literal(context.state.analysis.css.hash) + context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash) ) ) ); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index d0e2145937..7e42efae11 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -147,13 +147,19 @@ export function set_custom_element_data(node, prop, value) { * @param {Element & ElementCSSInlineStyle} element * @param {Record | undefined} prev * @param {Record} next New attributes - this function mutates this object - * @param {boolean} lowercase_attributes - * @param {string} css_hash + * @param {string} [css_hash] + * @param {boolean} preserve_attribute_case * @param {boolean} [skip_warning] * @returns {Record} */ -export function set_attributes(element, prev, next, lowercase_attributes, css_hash, skip_warning) { - var has_hash = css_hash.length !== 0; +export function set_attributes( + element, + prev, + next, + css_hash, + preserve_attribute_case = false, + skip_warning +) { var current = prev || {}; var is_option_element = element.tagName === 'OPTION'; @@ -163,8 +169,8 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha } } - if (has_hash && !next.class) { - next.class = ''; + if (css_hash !== undefined) { + next.class = next.class ? next.class + ' ' + css_hash : css_hash; } var setters = setters_cache.get(element.nodeName); @@ -267,7 +273,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha element.value = element[key] = element.__value = value; } else { var name = key; - if (lowercase_attributes) { + if (!preserve_attribute_case) { name = normalize_attribute(name); } @@ -279,11 +285,6 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha element[name] = value; } } else if (typeof value !== 'function') { - if (has_hash && name === 'class') { - if (value) value += ' '; - value += css_hash; - } - set_attribute(element, name, value); } } @@ -309,7 +310,7 @@ export function set_attributes(element, prev, next, lowercase_attributes, css_ha * @param {Element} node * @param {Record | undefined} prev * @param {Record} next The new attributes - this function mutates this object - * @param {string} css_hash + * @param {string} [css_hash] */ export function set_dynamic_element_attributes(node, prev, next, css_hash) { if (node.tagName.includes('-')) { @@ -319,6 +320,10 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) { } } + if (css_hash !== undefined) { + next.class = next.class ? next.class + ' ' + css_hash : css_hash; + } + for (key in next) { set_custom_element_data(node, key, next[key]); } @@ -330,8 +335,8 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) { /** @type {Element & ElementCSSInlineStyle} */ (node), prev, next, - node.namespaceURI !== NAMESPACE_SVG, - css_hash + css_hash, + node.namespaceURI !== NAMESPACE_SVG ); } diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/_config.js b/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/_config.js new file mode 100644 index 0000000000..eae8101e6e --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/_config.js @@ -0,0 +1,14 @@ +import { ok, test } from '../../test'; + +export default test({ + html: ``, + + async test({ assert, target }) { + const [el, el2] = target.querySelectorAll('custom-element'); + ok(el); + ok(el2); + + assert.deepEqual(el.className, 'red svelte-p153w3'); + assert.deepEqual(el2.className, 'red svelte-p153w3'); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/main.svelte b/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/main.svelte new file mode 100644 index 0000000000..d01a602293 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/svelte-element-custom-element-css-hash/main.svelte @@ -0,0 +1,8 @@ + + + + From e66416bec779ba4fba58c1bc670e119052489baf Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Aug 2024 22:18:33 -0400 Subject: [PATCH 061/196] feat: more efficient code generation when referencing globals (#12712) * feat: more efficient code generation when referencing globals * update test --- .changeset/red-kings-draw.md | 5 +++ .../2-analyze/visitors/CallExpression.js | 4 +-- .../2-analyze/visitors/MemberExpression.js | 4 +-- .../visitors/TaggedTemplateExpression.js | 4 +-- .../phases/2-analyze/visitors/shared/utils.js | 27 ++++++++-------- .../samples/effect-tracking/main.svelte | 6 ++-- .../purity/_expected/client/index.svelte.js | 32 +++++++++++++++++++ .../purity/_expected/server/index.svelte.js | 12 +++++++ .../snapshot/samples/purity/index.svelte | 12 +++++++ 9 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 .changeset/red-kings-draw.md create mode 100644 packages/svelte/tests/snapshot/samples/purity/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/purity/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/purity/index.svelte diff --git a/.changeset/red-kings-draw.md b/.changeset/red-kings-draw.md new file mode 100644 index 0000000000..56ce4afff3 --- /dev/null +++ b/.changeset/red-kings-draw.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: more efficient code generation when referencing globals diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 98b9a3d7ec..57cdae0c66 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -4,7 +4,7 @@ import { get_rune } from '../../scope.js'; import * as e from '../../../errors.js'; import { get_parent, unwrap_optional } from '../../../utils/ast.js'; -import { is_known_safe_call, is_safe_identifier } from './shared/utils.js'; +import { is_pure, is_safe_identifier } from './shared/utils.js'; /** * @param {CallExpression} node @@ -150,7 +150,7 @@ export function CallExpression(node, context) { break; } - if (context.state.expression && !is_known_safe_call(node.callee, context)) { + if (context.state.expression && !is_pure(node.callee, context)) { context.state.expression.has_call = true; context.state.expression.has_state = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js index 633275b63a..245a164c71 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/MemberExpression.js @@ -1,7 +1,7 @@ /** @import { MemberExpression } from 'estree' */ /** @import { Context } from '../types' */ import * as e from '../../../errors.js'; -import { is_safe_identifier } from './shared/utils.js'; +import { is_pure, is_safe_identifier } from './shared/utils.js'; /** * @param {MemberExpression} node @@ -15,7 +15,7 @@ export function MemberExpression(node, context) { } } - if (context.state.expression) { + if (context.state.expression && !is_pure(node, context)) { context.state.expression.has_state = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js index b079b67512..eacb8a342a 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/TaggedTemplateExpression.js @@ -1,13 +1,13 @@ /** @import { TaggedTemplateExpression, VariableDeclarator } from 'estree' */ /** @import { Context } from '../types' */ -import { is_known_safe_call } from './shared/utils.js'; +import { is_pure } from './shared/utils.js'; /** * @param {TaggedTemplateExpression} node * @param {Context} context */ export function TaggedTemplateExpression(node, context) { - if (context.state.expression && !is_known_safe_call(node.tag, context)) { + if (context.state.expression && !is_pure(node.tag, context)) { context.state.expression.has_call = true; context.state.expression.has_state = true; } diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js index 2980ac3cbe..963882d396 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/utils.js @@ -4,7 +4,7 @@ /** @import { Scope } from '../../../scope' */ /** @import { NodeLike } from '../../../../errors.js' */ import * as e from '../../../../errors.js'; -import { extract_identifiers } from '../../../../utils/ast.js'; +import { extract_identifiers, object } from '../../../../utils/ast.js'; import * as w from '../../../../warnings.js'; /** @@ -167,24 +167,23 @@ export function is_safe_identifier(expression, scope) { } /** - * @param {Expression | Super} callee + * @param {Expression | Super} node * @param {Context} context * @returns {boolean} */ -export function is_known_safe_call(callee, context) { - // String / Number / BigInt / Boolean casting calls - if (callee.type === 'Identifier') { - const name = callee.name; - const binding = context.state.scope.get(name); - if ( - binding === null && - (name === 'BigInt' || name === 'String' || name === 'Number' || name === 'Boolean') - ) { - return true; - } +export function is_pure(node, context) { + if (node.type !== 'Identifier' && node.type !== 'MemberExpression') { + return false; } - // TODO add more cases + const left = object(node); + if (!left) return false; + + if (left.type === 'Identifier') { + const binding = context.state.scope.get(left.name); + if (binding === null) return true; // globals are assumed to be safe + } + // TODO add more cases (safe Svelte imports, etc) return false; } diff --git a/packages/svelte/tests/runtime-runes/samples/effect-tracking/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-tracking/main.svelte index bb48958880..1e2ea446a5 100644 --- a/packages/svelte/tests/runtime-runes/samples/effect-tracking/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/effect-tracking/main.svelte @@ -1,5 +1,5 @@ + +

{Math.max(min, Math.min(max, number))}

+

{location.href}

+ + From 76cb89c75000b384946adfea39cfe83e47d1bd8f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 5 Aug 2024 16:26:33 +0100 Subject: [PATCH 062/196] fix: allow deletion of $$restProps properties (#12736) --- .changeset/loud-news-deliver.md | 5 +++++ .../src/internal/client/reactivity/props.js | 7 +++++++ .../samples/rest-props-delete/App.svelte | 10 ++++++++++ .../samples/rest-props-delete/_config.js | 15 +++++++++++++++ .../samples/rest-props-delete/main.svelte | 5 +++++ 5 files changed, 42 insertions(+) create mode 100644 .changeset/loud-news-deliver.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/rest-props-delete/App.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/rest-props-delete/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/rest-props-delete/main.svelte 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/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/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 @@ + + + From 8bde2d57108a834ed935cf0bce829badfde93930 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 5 Aug 2024 16:27:12 +0100 Subject: [PATCH 063/196] fix: add touch events on microtask to avoid Chromium bug (#12735) --- .changeset/healthy-mangos-brush.md | 5 +++++ .../svelte/src/internal/client/dom/elements/events.js | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/healthy-mangos-brush.md 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/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); }); From e78cfd393e16fb754f7393b535d2120815010381 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Mon, 5 Aug 2024 17:03:27 +0100 Subject: [PATCH 064/196] fix: correctly handle SvelteDate methods with arguments (#12738) * fix: correctly handle SvelteDate methods with arguments * tweak * Update packages/svelte/src/reactivity/date.js * Update packages/svelte/src/reactivity/date.js Co-authored-by: Rich Harris * Update packages/svelte/src/reactivity/date.test.ts --------- Co-authored-by: Rich Harris --- .changeset/dirty-pens-look.md | 5 +++++ packages/svelte/src/reactivity/date.js | 8 +++++++ packages/svelte/src/reactivity/date.test.ts | 24 +++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 .changeset/dirty-pens-look.md 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/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); }); From 59ea0b9e13f3833b74afa1958f92276a38e16806 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 5 Aug 2024 12:25:40 -0400 Subject: [PATCH 065/196] fix: better event handling (#12722) * simplify * fix/simplify * fix/simplify * start getting a grip of this mess * tidy up * more * more * more * tidy up * make things a bit less weird * tweak * more * more * add once once * consolidate event handling code * some progress. man, this stuff is entangled * more * tidy up * simplify * simplify * more * fix * fix test names * fix a bug * tidy up * changeset * simplify * regenerate * tidy up * tidy up * tidy up * simplify * the module declaration case is already accounted for, above * simplify/document * typo * "hoistable" is a misnomer * hoist non_hoistable, rename * more typos * tweak * regenerate --- .changeset/smart-cars-know.md | 5 + .changeset/wild-pumas-count.md | 5 + .../phases/2-analyze/visitors/Attribute.js | 63 ++++---- .../2-analyze/visitors/shared/function.js | 5 +- .../phases/3-transform/client/utils.js | 12 +- .../3-transform/client/visitors/Attribute.js | 4 +- .../client/visitors/FunctionDeclaration.js | 18 +-- .../client/visitors/OnDirective.js | 33 +++- .../client/visitors/RegularElement.js | 11 +- .../3-transform/client/visitors/SvelteBody.js | 7 +- .../client/visitors/SvelteDocument.js | 7 +- .../client/visitors/SvelteElement.js | 3 + .../client/visitors/SvelteWindow.js | 8 +- .../client/visitors/VariableDeclaration.js | 14 +- .../client/visitors/shared/component.js | 18 ++- .../client/visitors/shared/element.js | 151 +----------------- .../client/visitors/shared/events.js | 144 +++++++++++++++++ .../client/visitors/shared/function.js | 6 +- .../client/visitors/shared/special_element.js | 23 +++ .../client/visitors/shared/utils.js | 111 +------------ .../src/compiler/phases/3-transform/utils.js | 4 +- .../svelte/src/compiler/phases/types.d.ts | 12 +- .../svelte/src/compiler/types/template.d.ts | 4 +- .../src/internal/client/reactivity/effects.js | 2 +- .../_config.js | 0 .../main.svelte | 0 .../dynamic-event-handler-3/Button.svelte | 3 + .../dynamic-event-handler-3/_config.js | 34 ++++ .../dynamic-event-handler-3/main.svelte | 27 ++++ .../_config.js | 0 .../main.svelte | 0 .../event-attribute-not-reactive/_config.js | 19 +++ .../event-attribute-not-reactive/main.svelte | 19 +++ packages/svelte/types/index.d.ts | 4 +- 34 files changed, 414 insertions(+), 362 deletions(-) create mode 100644 .changeset/smart-cars-know.md create mode 100644 .changeset/wild-pumas-count.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/events.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/special_element.js rename packages/svelte/tests/runtime-runes/samples/{dynamic-element-event-handler-2 => dynamic-event-handler-2}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{dynamic-element-event-handler-2 => dynamic-event-handler-2}/main.svelte (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/Button.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dynamic-event-handler-3/main.svelte rename packages/svelte/tests/runtime-runes/samples/{dynamic-element-event-handler => dynamic-event-handler}/_config.js (100%) rename packages/svelte/tests/runtime-runes/samples/{dynamic-element-event-handler => dynamic-event-handler}/main.svelte (100%) create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/event-attribute-not-reactive/main.svelte 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/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/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 824f784a6b..85dfc6c67e 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 + if (reference === 'arguments') 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,17 +174,17 @@ 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' || @@ -193,12 +192,12 @@ function get_delegated_event(event_name, handler, context) { binding.kind === 'legacy_reactive_import') && 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/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/utils.js b/packages/svelte/src/compiler/phases/3-transform/client/utils.js index 58fae325ef..57ede67bb3 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -481,7 +481,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[]} */ @@ -549,15 +549,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 +567,7 @@ export function build_hoistable_params(node, context) { } } - params.push(...hoistable_params); + params.push(...hoisted_params); return params; } 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/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/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/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 13ebb41d5b..2063027782 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 @@ -138,6 +138,13 @@ export function RegularElement(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)); + 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 === 'BindDirective') { if (attribute.name === 'group' || attribute.name === 'checked') { @@ -214,7 +221,7 @@ export function RegularElement(node, context) { ) { might_need_event_replaying = true; } - build_event_attribute(attribute, context); + visit_event_attribute(attribute, context); continue; } 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/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 5dfcec4847..1f024ee450 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))); @@ -219,11 +219,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..ab1abc82da 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 @@ -6,8 +6,9 @@ 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 { build_bind_this, build_validate_binding } from '../shared/utils.js'; import { build_attribute_value } from '../shared/element.js'; +import { build_event_handler } from './events.js'; /** * @param {Component | SvelteComponent | SvelteSelf} node @@ -69,12 +70,21 @@ export function build_component(node, component_name, context, anchor = context. if (attribute.type === 'LetDirective') { lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute))); } 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) { 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..28d6684a57 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)` @@ -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..04b7b36186 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 diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 2b35fa67d7..188ae6739f 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; } 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/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 373822a873..86341d07d0 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -214,10 +214,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 { 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/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/types/index.d.ts b/packages/svelte/types/index.d.ts index 8212c52cc0..969f89222d 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1672,10 +1672,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 { From e747a6f75d67a33fc95cb60afbe9f7c024fb65be Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 12:26:03 -0400 Subject: [PATCH 066/196] Version Packages (next) (#12718) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 5 +++++ packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index ef858b3956..1f55abbc7d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -67,6 +67,7 @@ "chilled-ladybugs-invite", "chilled-pumas-invite", "chilled-seas-jog", + "chilly-carpets-switch", "chilly-dolphins-lick", "chilly-laws-juggle", "chilly-pans-raise", @@ -115,6 +116,7 @@ "dirty-donuts-yell", "dirty-eyes-itch", "dirty-garlics-design", + "dirty-pens-look", "dirty-pianos-eat", "dirty-tips-add", "dry-clocks-grow", @@ -264,6 +266,7 @@ "happy-moles-live", "happy-suits-film", "healthy-ants-film", + "healthy-mangos-brush", "healthy-planes-vanish", "healthy-zebras-accept", "heavy-comics-move", @@ -354,6 +357,7 @@ "loud-cheetahs-flow", "loud-insects-arrive", "loud-mugs-smile", + "loud-news-deliver", "loud-numbers-flow", "loud-penguins-unite", "loud-ravens-drop", @@ -495,6 +499,7 @@ "red-cycles-pretend", "red-doors-own", "red-feet-worry", + "red-kings-draw", "red-poets-study", "red-pots-pretend", "rich-cobras-exist", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1007ba88b1..5b31ce4818 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 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/package.json b/packages/svelte/package.json index 1f3cdb86e1..dc5d52bbbf 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.209", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index c70a430439..fee11f47e8 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.209'; export const PUBLIC_VERSION = '5'; From 98ae05b5693ad5722a4fa0de1bcaca329338c949 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:42:05 -0400 Subject: [PATCH 067/196] Version Packages (next) (#12741) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 2 ++ packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 1f55abbc7d..efe9a46a51 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -598,6 +598,7 @@ "small-planets-destroy", "small-sheep-type", "small-spiders-fail", + "smart-cars-know", "smart-cherries-leave", "smart-fans-crash", "smart-grapes-mate", @@ -765,6 +766,7 @@ "wild-foxes-wonder", "wild-moose-compare", "wild-poems-design", + "wild-pumas-count", "wise-apples-care", "wise-dancers-hang", "wise-dodos-tell", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 5b31ce4818..19db51983b 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index dc5d52bbbf..6b5a7c671b 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.209", + "version": "5.0.0-next.210", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index fee11f47e8..cb12a054f7 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.209'; +export const VERSION = '5.0.0-next.210'; export const PUBLIC_VERSION = '5'; From bd9a2d2077592fb44bc4bf474d10f34f900602d7 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 6 Aug 2024 15:12:55 +0100 Subject: [PATCH 068/196] fix: ensure unowned deriveds correctly update (#12747) --- .changeset/olive-forks-grin.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 7 +++++-- packages/svelte/tests/signals/test.ts | 17 +++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 .changeset/olive-forks-grin.md 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/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 779548a65c..d04d66ba8a 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; 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); + }; + }); }); From d06174e4616bd6c461982c3b8dab4a0a2e1def79 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 6 Aug 2024 16:46:36 +0100 Subject: [PATCH 069/196] chore: add error for derived self referencing (#12746) * chore: add warning for derived self referencin * update build * address feedback * address feedback * build * messages shouldn't end with a period * simplify test * regenerate * newlines are free * no need to export this, we can move it closer to where it's used * fix double negative --------- Co-authored-by: Rich Harris --- .changeset/new-cooks-roll.md | 5 +++++ .../svelte/messages/client-errors/errors.md | 4 ++++ packages/svelte/src/internal/client/errors.js | 16 ++++++++++++++ .../internal/client/reactivity/deriveds.js | 21 +++++++++++++++++++ .../samples/derived-fn-recursive/_config.js | 21 +++++++++++++++++++ .../samples/derived-fn-recursive/main.svelte | 11 ++++++++++ 6 files changed, 78 insertions(+) create mode 100644 .changeset/new-cooks-roll.md create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/derived-fn-recursive/main.svelte 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/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/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/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/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} From 1942f87ed9e2d51f5a86a982d1492e55182ba10b Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 6 Aug 2024 16:49:01 +0100 Subject: [PATCH 070/196] fix: improve prop binding warning validation for stores (#12745) * fix: improve prop binding warning validation for stores * ts * address feedback * add comment * failing test * fix/simplify --------- Co-authored-by: Rich Harris --- .changeset/healthy-dancers-play.md | 5 +++ .../client/visitors/BindDirective.js | 12 +++--- .../client/visitors/shared/component.js | 4 +- .../client/visitors/shared/utils.js | 40 ++++++++++--------- .../binding-property-store/Child.svelte | 5 +++ .../samples/binding-property-store/_config.js | 11 +++++ .../binding-property-store/main.svelte | 12 ++++++ 7 files changed, 61 insertions(+), 28 deletions(-) create mode 100644 .changeset/healthy-dancers-play.md create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-store/Child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-store/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/binding-property-store/main.svelte 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/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..8b9064d59a 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 @@ -7,7 +7,7 @@ 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,12 +30,10 @@ 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)) ); } 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 ab1abc82da..6416e26318 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 @@ -6,7 +6,7 @@ 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_validate_binding } from '../shared/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'; @@ -151,7 +151,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') { 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 04b7b36186..874ebe3131 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 @@ -204,28 +204,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/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}

From 60148d335286582192808e5721c60fb5c968be57 Mon Sep 17 00:00:00 2001 From: Paolo Ricciuti Date: Thu, 8 Aug 2024 15:39:18 +0200 Subject: [PATCH 071/196] fix: order of arguments for `push_element` in `svelte:element` (#12763) * fix: order of arguments for `push_element` in `svelte:element` * chore: move test to runtime * fix: add line and column to function call --- .changeset/six-apes-peel.md | 5 +++++ .../3-transform/server/visitors/SvelteElement.js | 16 ++++++++++++++-- .../invalid-nested-svelte-element/_config.js | 7 +++++++ .../invalid-nested-svelte-element/_expected.html | 1 + .../_expected_head.html | 1 + .../invalid-nested-svelte-element/main.svelte | 3 +++ 6 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .changeset/six-apes-peel.md create mode 100644 packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_config.js create mode 100644 packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/_expected_head.html create mode 100644 packages/svelte/tests/server-side-rendering/samples/invalid-nested-svelte-element/main.svelte 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/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/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 @@ + + + From ec8a029db4721ef6839c10b0d69112a4cb04eaa1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 8 Aug 2024 14:23:34 -0400 Subject: [PATCH 072/196] fix: skip `is_standalone` optimisation for dynamic components (#12767) * fix: skip `is_standalone` optimisation for dynamic components * changeset --- .changeset/nice-bottles-greet.md | 5 +++++ .../src/compiler/phases/3-transform/utils.js | 1 + .../member-expression-component/Row.svelte | 5 +++++ .../member-expression-component/_config.js | 16 ++++++++++++++++ .../member-expression-component/main.svelte | 13 +++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 .changeset/nice-bottles-greet.md create mode 100644 packages/svelte/tests/runtime-runes/samples/member-expression-component/Row.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/member-expression-component/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/member-expression-component/main.svelte 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/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 188ae6739f..8a1a00d862 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -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('--') ))), 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} From 37020384cabc05f94c75694d9061873301870f14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 06:32:40 -0400 Subject: [PATCH 073/196] Version Packages (next) (#12750) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 5 +++++ packages/svelte/CHANGELOG.md | 14 ++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index efe9a46a51..5030d31aec 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -266,6 +266,7 @@ "happy-moles-live", "happy-suits-film", "healthy-ants-film", + "healthy-dancers-play", "healthy-mangos-brush", "healthy-planes-vanish", "healthy-zebras-accept", @@ -416,8 +417,10 @@ "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-cooks-join", "ninety-days-visit", @@ -436,6 +439,7 @@ "old-oranges-compete", "olive-apples-lick", "olive-cobras-wonder", + "olive-forks-grin", "olive-kangaroos-brake", "olive-mice-fix", "olive-moons-act", @@ -569,6 +573,7 @@ "silver-mice-double", "silver-points-approve", "silver-sheep-knock", + "six-apes-peel", "six-bears-trade", "six-boats-shave", "six-chicken-kneel", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 19db51983b..ba128ef63a 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,19 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 6b5a7c671b..7eef44f29f 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.210", + "version": "5.0.0-next.211", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index cb12a054f7..afa49ac254 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.210'; +export const VERSION = '5.0.0-next.211'; export const PUBLIC_VERSION = '5'; From 9f28503bef8ed832a05391aece95e6d12e087949 Mon Sep 17 00:00:00 2001 From: Ben McCann <322311+benmccann@users.noreply.github.com> Date: Sat, 10 Aug 2024 03:33:35 -0700 Subject: [PATCH 074/196] chore: publish package provenance info (#12779) --- .changeset/fluffy-kings-complain.md | 5 +++++ .github/workflows/release.yml | 1 + packages/svelte/package.json | 3 +++ 3 files changed, 9 insertions(+) create mode 100644 .changeset/fluffy-kings-complain.md 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/.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/packages/svelte/package.json b/packages/svelte/package.json index 7eef44f29f..c35a6e6af8 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -154,5 +154,8 @@ "project": [ "src/**" ] + }, + "publishConfig": { + "provenance": true } } From d6e26c0953f0dc899e6bd4267f5bded05483d295 Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 10 Aug 2024 04:08:06 -0700 Subject: [PATCH 075/196] chore: speed up exclude_from_object (#12783) * speed up exclude_from_object * changeset * convert prop names to string at compile time * faster still --------- Co-authored-by: Rich Harris --- .changeset/brown-insects-float.md | 5 +++++ packages/svelte/src/compiler/utils/ast.js | 6 +++++- packages/svelte/src/internal/client/runtime.js | 14 +++++++++----- .../_config.js | 13 ++++++++----- .../main.svelte | 4 ++-- 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 .changeset/brown-insects-float.md 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/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index aef6c270a9..1998b0c98e 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)); }; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index d04d66ba8a..f748fa3993 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -991,12 +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]; + /** @type {Record} */ + var result = {}; + + for (var key in obj) { + if (!keys.includes(key)) { + result[key] = obj[key]; + } } - return obj; + + return result; } /** 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} From 7de3e3b70362003a525d9aa841fdcac9605b622d Mon Sep 17 00:00:00 2001 From: ottomated <31470743+ottomated@users.noreply.github.com> Date: Sat, 10 Aug 2024 04:49:24 -0700 Subject: [PATCH 076/196] feat: simplify derived object destructuring (#12781) * simplify derived object destructuring * add test for destructuring an array * add changeset * shorter temp variable name * skip intermediate derived for simple cases --------- Co-authored-by: Rich Harris --- .changeset/wicked-years-drive.md | 5 ++ .../client/visitors/VariableDeclaration.js | 50 ++++++++----------- .../samples/derived-destructured/_config.js | 2 +- .../samples/derived-destructured/main.svelte | 5 +- 4 files changed, 30 insertions(+), 32 deletions(-) create mode 100644 .changeset/wicked-years-drive.md 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/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/VariableDeclaration.js index 1f024ee450..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 @@ -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)))) ); } } 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 From 9ff33f1346ba9f9b9a1bf841bc14f966ccac2a3e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 08:08:48 -0400 Subject: [PATCH 077/196] Version Packages (next) (#12786) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 5030d31aec..62f19ca629 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", @@ -193,6 +194,7 @@ "fluffy-colts-grin", "fluffy-dolls-share", "fluffy-humans-worry", + "fluffy-kings-complain", "fluffy-ravens-juggle", "forty-bikes-buy", "forty-comics-invent", @@ -767,6 +769,7 @@ "wicked-hairs-cheer", "wicked-wasps-allow", "wicked-ways-reply", + "wicked-years-drive", "wild-cows-chew", "wild-foxes-wonder", "wild-moose-compare", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index ba128ef63a..05da28a721 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c35a6e6af8..19eece8252 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.211", + "version": "5.0.0-next.212", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index afa49ac254..84c2f0521c 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.211'; +export const VERSION = '5.0.0-next.212'; export const PUBLIC_VERSION = '5'; From c32a91891fc647998f738371a012fcba97d56e50 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 10 Aug 2024 13:44:06 +0100 Subject: [PATCH 078/196] fix: ensure event handlers referencing $host are not hoisted (#12775) --- .changeset/smooth-windows-explain.md | 5 +++++ .../compiler/phases/2-analyze/visitors/Attribute.js | 4 ++-- .../custom-elements-samples/host-rune/_config.js | 10 ++++++---- .../custom-elements-samples/host-rune/main.svelte | 5 +++++ 4 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 .changeset/smooth-windows-explain.md 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/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 85dfc6c67e..05ba0d486f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -155,8 +155,8 @@ function get_delegated_event(event_name, handler, context) { 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 unhoisted; + // 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; 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' })) + } + From 19819d0477203615bdb037bae3c8666f17837081 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 10 Aug 2024 17:36:30 +0100 Subject: [PATCH 079/196] fix: provide more hydration mismatch coverage (#12755) * fix: provide more hydration mismatch coverage * tweak * add test for safari borking stuff * fix * fix windows test * failing test * oops * revert playground changes * simplify * template content hydration logic should really be separate from reset logic * actually the test is incorrect, and now i cant seem to recreate what i saw before... hmm * update comment to no longer mention templates * failing test * delete test for now --------- Co-authored-by: Rich Harris --- .changeset/stupid-rivers-stare.md | 5 ++++ .../client/visitors/RegularElement.js | 3 +-- .../src/internal/client/dom/hydration.js | 27 +++++++++++++++---- packages/svelte/src/internal/client/index.js | 2 +- .../samples/safari-borking/_config.js | 5 ++++ .../samples/safari-borking/_expected.html | 1 + .../samples/safari-borking/_override.html | 1 + .../samples/safari-borking/main.svelte | 5 ++++ .../surrounding-whitespace/_expected.html | 2 ++ packages/svelte/tests/hydration/test.ts | 13 ++++----- playgrounds/demo/ssr-dev.js | 4 ++- 11 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 .changeset/stupid-rivers-stare.md create mode 100644 packages/svelte/tests/hydration/samples/safari-borking/_config.js create mode 100644 packages/svelte/tests/hydration/samples/safari-borking/_expected.html create mode 100644 packages/svelte/tests/hydration/samples/safari-borking/_override.html create mode 100644 packages/svelte/tests/hydration/samples/safari-borking/main.svelte create mode 100644 packages/svelte/tests/hydration/samples/surrounding-whitespace/_expected.html 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/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 2063027782..94864fecef 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 @@ -327,9 +327,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/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/index.js b/packages/svelte/src/internal/client/index.js index edd50bff32..d1885e7d00 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, 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/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); }) From 057316ccd135263348755396bac207e8785d57d9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 12:52:17 -0400 Subject: [PATCH 080/196] chore: simpler fallback values (#12788) * DRY * value_or_fallback_async doesnt do anything * rename value_or_fallback to fallback * DRY * DRY * make lazy the exception * changeset --- .changeset/witty-frogs-cheat.md | 5 ++++ .../server/visitors/VariableDeclaration.js | 10 +++---- packages/svelte/src/compiler/utils/ast.js | 26 ++++++++++++++----- packages/svelte/src/internal/client/index.js | 4 +-- .../svelte/src/internal/client/runtime.js | 20 -------------- packages/svelte/src/internal/server/index.js | 22 ++-------------- packages/svelte/src/internal/shared/utils.js | 15 +++++++++++ 7 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 .changeset/witty-frogs-cheat.md 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/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/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 1998b0c98e..95b5d2fb2d 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -368,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({ @@ -549,3 +544,22 @@ 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); +} diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index d1885e7d00..dc51d3caae 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -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/runtime.js b/packages/svelte/src/internal/client/runtime.js index f748fa3993..8939244cf8 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1003,26 +1003,6 @@ export function exclude_from_object(obj, keys) { return 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; -} - -/** - * @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 {Record} props * @param {any} runes 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; +} From d8954d7addb2848947e961f40c6fd2c735c420a5 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 10 Aug 2024 18:15:09 +0100 Subject: [PATCH 081/196] fix: ensure custom elements do not sync flush on mount (#12787) * fix: ensure Svelte4Components do not sync flush * fix: ensure Svelte4Components do not sync flush * lint * feedback * simplify test, remove redundant comments * prettier * fix test --------- Co-authored-by: Rich Harris --- .changeset/cool-turtles-travel.md | 5 +++++ packages/svelte/src/legacy/legacy-client.js | 5 ++++- .../effect-sequence/_config.js | 20 +++++++++++++++++++ .../effect-sequence/main.svelte | 7 +++++++ .../escaped-css/_config.js | 1 + 5 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 .changeset/cool-turtles-travel.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/effect-sequence/main.svelte 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/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/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'); From 1a3dbda20ce5a60d373d1c5127fd892bbd4441ba Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 13:20:19 -0400 Subject: [PATCH 082/196] Version Packages (next) (#12789) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 4 ++++ packages/svelte/CHANGELOG.md | 12 ++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 62f19ca629..10c332ff19 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -98,6 +98,7 @@ "cool-poems-watch", "cool-rabbits-tickle", "cool-roses-trade", + "cool-turtles-travel", "cuddly-pianos-drop", "cuddly-points-tickle", "curly-cooks-cheer", @@ -618,6 +619,7 @@ "smooth-pens-exist", "smooth-pens-protect", "smooth-rings-rush", + "smooth-windows-explain", "soft-clocks-remember", "soft-geese-learn", "soft-months-grab", @@ -653,6 +655,7 @@ "strong-pans-doubt", "stupid-bottles-lay", "stupid-parents-crash", + "stupid-rivers-stare", "sweet-bottles-check", "sweet-mangos-beg", "sweet-pens-sniff", @@ -785,6 +788,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/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 05da28a721..f7d6e9257e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 19eece8252..b6d6393487 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.212", + "version": "5.0.0-next.213", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 84c2f0521c..b08ee5d3a0 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.212'; +export const VERSION = '5.0.0-next.213'; export const PUBLIC_VERSION = '5'; From 536be645faefc2a7724fa458ac7eec1c375ae948 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sat, 10 Aug 2024 18:31:41 +0100 Subject: [PATCH 083/196] fix: ensure custom element styles append correctly during prod (#12777) * fix: ensure custom element styles append correctly during prod * fix * fix * fix * feedback * only add third argument for custom elements --------- Co-authored-by: Rich Harris --- .changeset/hip-goats-smoke.md | 5 +++++ .../compiler/phases/3-transform/client/transform-client.js | 4 +++- packages/svelte/src/internal/client/dom/css.js | 5 +++-- 3 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 .changeset/hip-goats-smoke.md 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/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..6ebb77f2e9 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 @@ -380,7 +380,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) + ) ); } 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); } From 8ac13f76f199b32a27913dc06532ca3e7444d2de Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:32:10 -0400 Subject: [PATCH 084/196] chore: print compiler warnings when using sandbox (#12793) --- playgrounds/sandbox/run.js | 7 +++++++ 1 file changed, 7 insertions(+) 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); } From f13c722ce4b7c42b22c1a4bdf9a4721ea0a5b08e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 13:32:18 -0400 Subject: [PATCH 085/196] chore: increase debug stack trace (#12792) --- .vscode/launch.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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": [ From 57a7b0cd4801528ed567788cab9c0de8264050d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Aug 2024 15:19:22 -0400 Subject: [PATCH 086/196] chore: transformers (#12780) * clear out getters on new scope * fix * fix * fix * fix * consolidate legacy_reactive_import logic * unused * $$sanitized_props * use getters mechanism for store_sub * snapshot * fix * tests passing * remove some stuff * more * fix * tidy up * simplify * simplify * getters -> transformers * update * use update transformers * add assign transformer * more * tweak * remove junk * unused * simplify * tidy up * tweak * assign_property * fix * tidy up * tidy up * move store code * this appears to be unused * tidy up * tweak * simplify * move code * move stuff * note to self * move stuff * each blocks * note to self * lengthen stack trace * tweak * more * tidy up * tidy up * remove some junk * tidy up * move stuff * remove stuff * tweak * tweak * fix * tweak * tidy up * tidy up * tidy up * tweak * simplify * tidy up * simplify * tidy up * improve output * delete comments * more * unused * tidy up * tidy up * fix * move some stuff * tweak * tidy up * DRY * synchronise * DRY out * tidy up * tidy up * tidy up * add test that fails on main * snapshot test * changesets * lint * ugh --- .changeset/nine-ants-invite.md | 5 + .changeset/perfect-hairs-matter.md | 5 + .../phases/2-analyze/visitors/Attribute.js | 4 +- .../phases/2-analyze/visitors/EachBlock.js | 11 +- .../phases/2-analyze/visitors/Identifier.js | 12 +- .../2-analyze/visitors/shared/component.js | 49 +- .../3-transform/client/transform-client.js | 50 +- .../phases/3-transform/client/types.d.ts | 32 +- .../phases/3-transform/client/utils.js | 439 +----------------- .../client/visitors/AssignmentExpression.js | 126 ++++- .../3-transform/client/visitors/AwaitBlock.js | 36 +- .../client/visitors/BindDirective.js | 13 +- .../client/visitors/BlockStatement.js | 12 + .../3-transform/client/visitors/ConstTag.js | 13 +- .../3-transform/client/visitors/EachBlock.js | 134 +++--- .../3-transform/client/visitors/Fragment.js | 2 +- .../client/visitors/LabeledStatement.js | 2 +- .../client/visitors/LetDirective.js | 14 +- .../3-transform/client/visitors/Program.js | 123 +++++ .../client/visitors/RegularElement.js | 11 +- .../client/visitors/SnippetBlock.js | 14 +- .../client/visitors/UpdateExpression.js | 103 ++-- .../client/visitors/shared/component.js | 63 +-- .../client/visitors/shared/declarations.js | 53 +++ .../client/visitors/shared/element.js | 2 +- .../client/visitors/shared/utils.js | 20 +- .../server/visitors/AssignmentExpression.js | 64 +-- .../server/visitors/shared/component.js | 21 +- .../phases/3-transform/shared/assignments.js | 77 +++ .../src/compiler/phases/3-transform/utils.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 127 +++-- packages/svelte/src/compiler/types/index.d.ts | 4 +- .../svelte/src/compiler/types/template.d.ts | 8 + packages/svelte/src/compiler/utils/ast.js | 12 + packages/svelte/src/compiler/utils/slot.js | 20 + .../samples/each-blocks-update/_config.js | 27 ++ .../samples/each-blocks-update/main.svelte | 9 + .../_expected/client/index.svelte.js | 12 + .../_expected/server/index.svelte.js | 9 + .../destructured-assignments/index.svelte.js | 6 + packages/svelte/types/index.d.ts | 11 +- 41 files changed, 907 insertions(+), 850 deletions(-) create mode 100644 .changeset/nine-ants-invite.md create mode 100644 .changeset/perfect-hairs-matter.md create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/BlockStatement.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/Program.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/declarations.js create mode 100644 packages/svelte/src/compiler/phases/3-transform/shared/assignments.js create mode 100644 packages/svelte/src/compiler/utils/slot.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/each-blocks-update/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/each-blocks-update/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/client/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/_expected/server/index.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/destructured-assignments/index.svelte.js 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/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/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js index 05ba0d486f..102091c78d 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/Attribute.js @@ -187,9 +187,7 @@ function get_delegated_event(event_name, handler, context) { // 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 unhoisted; 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 347a59488a..e6bb3c067e 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,7 +1,8 @@ -/** @import { Component, SvelteComponent, SvelteSelf } from '#compiler' */ +/** @import { Comment, Component, Fragment, SvelteComponent, SvelteSelf } from '#compiler' */ /** @import { Context } from '../../types' */ import * as e from '../../../../errors.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, @@ -60,9 +61,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/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 6ebb77f2e9..c4af60f24b 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 = []; @@ -623,11 +639,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 57ede67bb3..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 @@ -507,7 +91,7 @@ function get_hoisted_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. @@ -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/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 8b9064d59a..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,7 +5,6 @@ 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, validate_binding } from './shared/utils.js'; @@ -38,18 +37,10 @@ export function BindDirective(node, context) { } 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/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/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 94864fecef..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 @@ -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,8 +143,6 @@ 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 if (attribute.type === 'OnDirective') { const handler = /** @type {Expression} */ (context.visit(attribute)); const has_action_directive = node.attributes.find((a) => a.type === 'UseDirective'); @@ -145,7 +150,7 @@ export function RegularElement(node, context) { context.state.after_update.push( b.stmt(has_action_directive ? b.call('$.effect', b.thunk(handler)) : handler) ); - } else { + } else if (attribute.type !== 'LetDirective') { if (attribute.type === 'BindDirective') { if (attribute.name === 'group' || attribute.name === 'checked') { needs_special_value_handling = 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/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/shared/component.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/component.js index 6416e26318..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,14 +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 { 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 @@ -24,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 = {}; @@ -45,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 @@ -66,9 +75,20 @@ 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') { if (!attribute.expression) { context.state.analysis.needs_props = true; @@ -174,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)))]) ); } } @@ -214,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); } @@ -242,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 28d6684a57..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 @@ -101,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)) }; 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 874ebe3131..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 @@ -145,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 @@ -154,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; @@ -166,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} */ ( 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/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 8a1a00d862..f71c4120f9 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -425,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/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 86341d07d0..20fdb25ee4 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; @@ -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 95b5d2fb2d..47bbdb945f 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -563,3 +563,15 @@ export function build_fallback(expression, 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/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/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/types/index.d.ts b/packages/svelte/types/index.d.ts index 969f89222d..df012254f6 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. @@ -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 { From a0bbf2ace02ad251cfd4720fa269eb00a0b27200 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 16:00:24 -0400 Subject: [PATCH 087/196] Version Packages (next) (#12794) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 10c332ff19..c99ed9e8b6 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -280,6 +280,7 @@ "heavy-feet-attend", "hip-balloons-begin", "hip-garlics-tap", + "hip-goats-smoke", "hip-months-breathe", "hip-pumpkins-boil", "hip-stingrays-teach", @@ -425,6 +426,7 @@ "nice-avocados-move", "nice-bottles-greet", "nice-jobs-breathe", + "nine-ants-invite", "nine-cooks-join", "ninety-days-visit", "ninety-dingos-walk", @@ -456,6 +458,7 @@ "orange-yaks-protect", "orange-zoos-heal", "perfect-actors-bake", + "perfect-hairs-matter", "perfect-hats-dance", "pink-bikes-agree", "pink-goats-promise", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f7d6e9257e..1b9db51621 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index b6d6393487..ba166774fa 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.213", + "version": "5.0.0-next.214", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index b08ee5d3a0..e9dc0a3a31 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.213'; +export const VERSION = '5.0.0-next.214'; export const PUBLIC_VERSION = '5'; From ba116a1b43f66e607a30fa55f9bbfa067b126ca6 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 11 Aug 2024 03:42:32 +0100 Subject: [PATCH 088/196] fix: propagate custom element component prop changes (#12774) * fix: propagate custom element component prop changes * add test --- .changeset/poor-mugs-pay.md | 5 +++ .../client/dom/elements/custom-element.js | 15 +++++++- .../propagate-prop-changes/_config.js | 37 +++++++++++++++++++ .../propagate-prop-changes/main.svelte | 7 ++++ 4 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/poor-mugs-pay.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/propagate-prop-changes/main.svelte 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/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/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 @@ + + + + + From d2efca0b046734fa6ac462dd7a25e4e5f9c0a8f0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 11 Aug 2024 03:46:16 +0100 Subject: [PATCH 089/196] fix: prevent numerous transition/animation memory leaks (#12759) * fix: prevent numerous transition/animation memory leaks * address feedback * tweak --- .changeset/silly-masks-exist.md | 5 +++++ .../src/internal/client/dom/elements/transitions.js | 11 ++++++++++- playgrounds/demo/index.html | 3 --- 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 .changeset/silly-masks-exist.md 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/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 5d0005b2c1..8777ba706b 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) 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); From 4aa60597125a7e15a699240907e294afb75ba86e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 10 Aug 2024 22:47:41 -0400 Subject: [PATCH 090/196] Version Packages (next) (#12795) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 2 ++ packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index c99ed9e8b6..f463c02610 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -477,6 +477,7 @@ "polite-ways-serve", "poor-eggs-enjoy", "poor-hats-design", + "poor-mugs-pay", "poor-seahorses-flash", "popular-apes-bathe", "popular-cameras-tie", @@ -575,6 +576,7 @@ "silly-ducks-mix", "silly-laws-happen", "silly-lies-film", + "silly-masks-exist", "silly-ways-wash", "silver-mice-double", "silver-points-approve", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 1b9db51621..74f6505e47 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # svelte +## 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ba166774fa..d15876afdb 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.214", + "version": "5.0.0-next.215", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index e9dc0a3a31..ea19843673 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.214'; +export const VERSION = '5.0.0-next.215'; export const PUBLIC_VERSION = '5'; From 97c0150b00540796713299f52750b6b48616912f Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Sun, 11 Aug 2024 11:58:42 +0100 Subject: [PATCH 091/196] fix: improved memory profile for transitions/animations (#12796) * fix: improved memory profile for transitions/animations * put back the catch --- .changeset/thin-papayas-tap.md | 5 +++++ .../src/internal/client/dom/elements/transitions.js | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/thin-papayas-tap.md 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/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 8777ba706b..f747e2f402 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -448,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; From 7ae21ea97de84bd42b95cff5b8de77c9ce0830df Mon Sep 17 00:00:00 2001 From: Theodor Steiner <40017636+Theo-Steiner@users.noreply.github.com> Date: Sun, 11 Aug 2024 20:02:33 +0900 Subject: [PATCH 092/196] feat: make customElement configuration's tag property optional (#12751) (#12754) * feat: make svelte:option customElement tag property optional (#12751) * tweak comment * tweak docs * tweak some more wording * Update .changeset/four-kids-flow.md --------- Co-authored-by: Rich Harris Co-authored-by: Rich Harris --- .changeset/four-kids-flow.md | 5 +++++ .../02-template-syntax/09-special-elements.md | 2 +- .../docs/05-misc/04-custom-elements.md | 2 +- .../09-svelte-options/text.md | 2 +- .../svelte/messages/compile-errors/template.md | 2 +- packages/svelte/src/compiler/errors.js | 4 ++-- .../compiler/phases/1-parse/read/options.js | 8 +------- .../3-transform/client/transform-client.js | 5 ++--- .../svelte/src/compiler/types/template.d.ts | 2 +- .../no-tag-ce-options/_config.js | 18 ++++++++++++++++++ .../no-tag-ce-options/main.svelte | 7 +++++++ .../samples/tag-non-string/errors.json | 2 +- packages/svelte/types/index.d.ts | 2 +- 13 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 .changeset/four-kids-flow.md create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/_config.js create mode 100644 packages/svelte/tests/runtime-browser/custom-elements-samples/no-tag-ce-options/main.svelte 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/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/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/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/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index c4af60f24b..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 @@ -611,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)); diff --git a/packages/svelte/src/compiler/types/template.d.ts b/packages/svelte/src/compiler/types/template.d.ts index 20fdb25ee4..7f2e808ade 100644 --- a/packages/svelte/src/compiler/types/template.d.ts +++ b/packages/svelte/src/compiler/types/template.d.ts @@ -78,7 +78,7 @@ export interface SvelteOptions { namespace?: Namespace; css?: 'injected'; customElement?: { - tag: string; + tag?: string; shadow?: 'open' | 'none'; props?: Record< string, 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/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 df012254f6..4307b5c267 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1533,7 +1533,7 @@ declare module 'svelte/compiler' { namespace?: Namespace; css?: 'injected'; customElement?: { - tag: string; + tag?: string; shadow?: 'open' | 'none'; props?: Record< string, From 09db33979d17d176c6b48370e8006e293d577074 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Aug 2024 07:45:07 -0400 Subject: [PATCH 093/196] Version Packages (next) (#12797) * Version Packages (next) * Update CHANGELOG.md --------- Co-authored-by: github-actions[bot] Co-authored-by: Rich Harris --- .changeset/pre.json | 2 ++ packages/svelte/CHANGELOG.md | 8 ++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f463c02610..172a6e9d68 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -205,6 +205,7 @@ "forty-suns-smile", "four-balloons-beam", "four-flies-hammer", + "four-kids-flow", "four-mice-hammer", "four-papayas-turn", "four-peas-tickle", @@ -703,6 +704,7 @@ "thick-swans-type", "thin-colts-yawn", "thin-foxes-lick", + "thin-papayas-tap", "thin-spoons-float", "thin-years-rhyme", "thirty-dogs-whisper", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 74f6505e47..7b1ab89d31 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,13 @@ # 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 diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d15876afdb..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.215", + "version": "5.0.0-next.216", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ea19843673..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.215'; +export const VERSION = '5.0.0-next.216'; export const PUBLIC_VERSION = '5'; From 5094cb9e89f9dfc93c4e8ca2084ae5aaf8d270c4 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 Aug 2024 06:45:30 -0400 Subject: [PATCH 094/196] feat: treat tag with `.` as a component, even if lowercase (#12798) * feat: treat tag with `.` as a component, even if lowercase * changeset * consistency * note breaking change * oops, wrong place --- .changeset/gold-students-jump.md | 5 +++++ .../src/compiler/phases/1-parse/state/element.js | 4 ++-- .../samples/dot-notation-component/_config.js | 5 +++++ .../samples/dot-notation-component/child.svelte | 1 + .../samples/dot-notation-component/main.svelte | 7 +++++++ .../docs/content/03-appendix/02-breaking-changes.md | 10 ++++++++++ 6 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 .changeset/gold-students-jump.md create mode 100644 packages/svelte/tests/runtime-runes/samples/dot-notation-component/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/dot-notation-component/child.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/dot-notation-component/main.svelte 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/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/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/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. From 9a67ab15dac11ae8273cc6213d4684b62e560837 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 12 Aug 2024 08:56:11 -0400 Subject: [PATCH 095/196] docs: fix section on browser requirements (#12804) * docs: fix section on browser requirements * link --- .../docs/content/03-appendix/02-breaking-changes.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 7b589785d7..47b18ee3d9 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 @@ -144,9 +144,15 @@ Previously, Svelte employed a very complicated algorithm to determine if whitesp As before, you can disable whitespace trimming by setting the `preserveWhitespace` option in your compiler settings or on a per-component basis in ``. -## More recent browser required +## Modern browser required -Svelte now use Mutation Observers instead of IFrames to measure dimensions for `bind:clientWidth/clientHeight/offsetWidth/offsetHeight`. It also no longer listens to the `change` event on range inputs. Lastly, the `legacy` option was removed (or rather, replaced with a different set of settings). +Svelte 5 requires a modern browser (in other words, not Internet Explorer) for various reasons: + +- it uses [`Proxies`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) +- elements with `clientWidth`/`clientHeight`/`offsetWidth`/`offsetHeight` bindings use a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) rather than a convoluted `