From 6822decec2afff5fddf354196e96f7fab21a293f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Mar 2024 11:27:14 -0400 Subject: [PATCH 001/102] tidy up (#10874) --- .../svelte/src/internal/client/dom/blocks/html.js | 2 +- .../svelte/src/internal/client/dom/blocks/if.js | 8 ++++---- .../svelte/src/internal/client/dom/reconciler.js | 4 ++-- .../svelte/src/internal/client/dom/template.js | 4 ++-- packages/svelte/src/internal/client/types.d.ts | 15 ++++----------- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 2e03cbc3bb..0a57d46af2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -8,7 +8,7 @@ import { reconcile_html, remove } from '../reconciler.js'; * @returns {void} */ export function html(dom, get_value, svg) { - /** @type {import('#client').TemplateNode | import('#client').TemplateNode[]} */ + /** @type {import('#client').Dom} */ let html_dom; /** @type {string} */ diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index e03c4dc5af..f257a64833 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -17,8 +17,8 @@ import { create_block } from './utils.js'; /** * @param {Comment} anchor * @param {() => boolean} get_condition - * @param {(anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[]} consequent_fn - * @param {null | ((anchor: Node) => import('#client').TemplateNode | import('#client').TemplateNode[])} alternate_fn + * @param {(anchor: Node) => import('#client').Dom} consequent_fn + * @param {null | ((anchor: Node) => import('#client').Dom)} alternate_fn * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' * @returns {void} */ @@ -27,10 +27,10 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els hydrate_block_anchor(anchor); - /** @type {undefined | import('#client').TemplateNode | Array} */ + /** @type {undefined | import('#client').Dom} */ let consequent_dom; - /** @type {undefined | import('#client').TemplateNode | Array} */ + /** @type {undefined | import('#client').Dom} */ let alternate_dom; /** @type {import('#client').Effect | null} */ diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 81b03bbd2f..02be902598 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -29,7 +29,7 @@ export function create_fragment_with_script_from_html(html) { } /** - * @param {Array | import('../types.js').TemplateNode} current + * @param {import('#client').Dom} current * @param {Text | Element | Comment} sibling * @returns {Text | Element | Comment} */ @@ -49,7 +49,7 @@ export function insert(current, sibling) { } /** - * @param {Array | import('../types.js').TemplateNode} current + * @param {import('#client').Dom} current */ export function remove(current) { if (is_array(current)) { diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 97a8652c7a..8fcd271ecd 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -178,10 +178,10 @@ export function comment(anchor) { * @param {Element | Text} dom * @param {boolean} is_fragment * @param {null | Text | Comment | Element} anchor - * @returns {import('#client').TemplateNode | import('#client').TemplateNode[]} + * @returns {import('#client').Dom} */ function close_template(dom, is_fragment, anchor) { - /** @type {import('#client').TemplateNode | Array} */ + /** @type {import('#client').Dom} */ var current = is_fragment ? is_array(dom) ? dom diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 68025ce99a..d2426ee5d3 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -49,9 +49,11 @@ export type Equals = (this: Value, value: unknown) => boolean; export type TemplateNode = Text | Element | Comment; +export type Dom = TemplateNode | TemplateNode[]; + export interface Block { /** dom */ - d: null | TemplateNode | Array; + d: null | Dom; } export type EachState = { @@ -65,7 +67,7 @@ export type EachItem = { /** animation manager */ a: AnimationManager | null; /** dom */ - d: null | TemplateNode | Array; + d: null | Dom; /** effect */ e: Effect; /** item */ @@ -139,15 +141,6 @@ export type StoreReferencesContainer = Record< export type ActionPayload

= { destroy?: () => void; update?: (value: P) => void }; -export type Render = { - /** dom */ - d: null | TemplateNode | Array; - /** effect */ - e: null | Effect; - /** prev */ - p: Render | null; -}; - export type Raf = { /** Alias for `requestAnimationFrame`, exposed in such a way that we can override in tests */ tick: (callback: (time: DOMHighResTimeStamp) => void) => any; From 2cabc884ca8e9169c6cdc1491e0dfb32604dcda1 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 22 Mar 2024 15:41:53 +0000 Subject: [PATCH 002/102] add some missing legacy tests (#10875) --- .../src/compiler/phases/2-analyze/index.js | 9 +++-- .../samples/prop-const/Nested.svelte | 7 ++++ .../samples/prop-const/_config.js | 27 ++++++++++++++ .../samples/prop-const/main.svelte | 8 ++++ .../samples/props-reactive-b/_config.js | 37 +++++++++++++++++++ .../samples/props-reactive-b/main.svelte | 8 ++++ .../samples/spread-from-import/_config.js | 13 +++++++ .../samples/spread-from-import/main.svelte | 14 +++++++ .../samples/spread-from-import/spread.js | 6 +++ .../samples/target-dom-detached/App.svelte | 11 ++++++ .../samples/target-dom-detached/_config.js | 15 ++++++++ .../samples/target-dom-detached/main.svelte | 18 +++++++++ .../samples/target-dom/App.svelte | 11 ++++++ .../samples/target-dom/_config.js | 15 ++++++++ .../samples/target-dom/main.svelte | 18 +++++++++ .../samples/target-shadow-dom/App.svelte | 11 ++++++ .../samples/target-shadow-dom/_config.js | 18 +++++++++ .../samples/target-shadow-dom/main.svelte | 19 ++++++++++ 18 files changed, 261 insertions(+), 4 deletions(-) create mode 100644 packages/svelte/tests/runtime-legacy/samples/prop-const/Nested.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/prop-const/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/props-reactive-b/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/spread-from-import/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/spread-from-import/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/spread-from-import/spread.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom-detached/App.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom-detached/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom-detached/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom/App.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-dom/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/App.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/main.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 20d4cc4644..8140439145 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -753,10 +753,11 @@ const legacy_scope_tweaker = { state.scope.get(specifier.local.name) ); if ( - binding.kind === 'state' || - binding.kind === 'frozen_state' || - (binding.kind === 'normal' && - (binding.declaration_kind === 'let' || binding.declaration_kind === 'var')) + binding !== null && + (binding.kind === 'state' || + binding.kind === 'frozen_state' || + (binding.kind === 'normal' && + (binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))) ) { binding.kind = 'prop'; if (specifier.exported.name !== specifier.local.name) { diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-const/Nested.svelte b/packages/svelte/tests/runtime-legacy/samples/prop-const/Nested.svelte new file mode 100644 index 0000000000..392bc7555d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/prop-const/Nested.svelte @@ -0,0 +1,7 @@ + + +

a: {a}

+

b: {b}

diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js b/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js new file mode 100644 index 0000000000..040b911cc6 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/prop-const/_config.js @@ -0,0 +1,27 @@ +import { test } from '../../test'; + +export default test({ + get props() { + return { a: 3, b: 4 }; + }, + + html: ` +

a: 3

+

b: 2

+ `, + + async test({ assert, component, target }) { + await component.$set({ + a: 5, + b: 6 + }); + + assert.htmlEqual( + target.innerHTML, + ` +

a: 5

+

b: 2

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/prop-const/main.svelte b/packages/svelte/tests/runtime-legacy/samples/prop-const/main.svelte new file mode 100644 index 0000000000..e9184c8da4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/prop-const/main.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js new file mode 100644 index 0000000000..b92d81bd53 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/_config.js @@ -0,0 +1,37 @@ +import { test } from '../../test'; + +export default test({ + get props() { + return { a: 1, b: 2 }; + }, + + html: ` +

a: 1

+

b: 2

+

c: 3

+ `, + + async test({ assert, component, target }) { + await component.$set({ a: 4 }); + + assert.htmlEqual( + target.innerHTML, + ` +

a: 4

+

b: 2

+

c: 6

+ ` + ); + + await component.$set({ b: 5 }); + + assert.htmlEqual( + target.innerHTML, + ` +

a: 4

+

b: 5

+

c: 9

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/main.svelte b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/main.svelte new file mode 100644 index 0000000000..1c40f4a344 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/props-reactive-b/main.svelte @@ -0,0 +1,8 @@ + + +

a: {a}

+

b: {$$props.b}

+

c: {c}

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

static stuff

+
+
+

dynamic stuff

+
+ + ` +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/spread-from-import/main.svelte b/packages/svelte/tests/runtime-legacy/samples/spread-from-import/main.svelte new file mode 100644 index 0000000000..5cec0ebb08 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/spread-from-import/main.svelte @@ -0,0 +1,14 @@ + + +
+

static stuff

+
+ +
+

{dynamic} stuff

+
+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/spread-from-import/spread.js b/packages/svelte/tests/runtime-legacy/samples/spread-from-import/spread.js new file mode 100644 index 0000000000..8ecf7628f3 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/spread-from-import/spread.js @@ -0,0 +1,6 @@ +export function spread() { + return { + class: 'tooltip', + id: null + }; +} diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/App.svelte b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/App.svelte new file mode 100644 index 0000000000..4ec51de6d5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/App.svelte @@ -0,0 +1,11 @@ + + +
Hello {name}
+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/_config.js b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/_config.js new file mode 100644 index 0000000000..a8330e86b0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + skip_if_ssr: true, + compileOptions: { + cssHash: () => 'svelte-xyz' + }, + async test({ assert, component, window }) { + assert.htmlEqual( + window.document.head.innerHTML, + '' + ); + assert.htmlEqual(component.div.innerHTML, '
Hello World
'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/main.svelte b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/main.svelte new file mode 100644 index 0000000000..eb2d6c0ee6 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom-detached/main.svelte @@ -0,0 +1,18 @@ + diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom/App.svelte b/packages/svelte/tests/runtime-legacy/samples/target-dom/App.svelte new file mode 100644 index 0000000000..4ec51de6d5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom/App.svelte @@ -0,0 +1,11 @@ + + +
Hello {name}
+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom/_config.js b/packages/svelte/tests/runtime-legacy/samples/target-dom/_config.js new file mode 100644 index 0000000000..a8330e86b0 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + skip_if_ssr: true, + compileOptions: { + cssHash: () => 'svelte-xyz' + }, + async test({ assert, component, window }) { + assert.htmlEqual( + window.document.head.innerHTML, + '' + ); + assert.htmlEqual(component.div.innerHTML, '
Hello World
'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/target-dom/main.svelte b/packages/svelte/tests/runtime-legacy/samples/target-dom/main.svelte new file mode 100644 index 0000000000..4d5a825ad3 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-dom/main.svelte @@ -0,0 +1,18 @@ + + +
diff --git a/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/App.svelte b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/App.svelte new file mode 100644 index 0000000000..4ec51de6d5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/App.svelte @@ -0,0 +1,11 @@ + + +
Hello {name}
+ + diff --git a/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/_config.js b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/_config.js new file mode 100644 index 0000000000..34f10f4652 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/_config.js @@ -0,0 +1,18 @@ +import { test } from '../../test'; + +export default test({ + skip_if_ssr: true, + compileOptions: { + cssHash: () => 'svelte-xyz' + }, + async test({ assert, component, window }) { + assert.htmlEqual( + window.document.head.innerHTML, + '' + ); + assert.htmlEqual( + component.div.shadowRoot.innerHTML, + '
Hello World
' + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/main.svelte b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/main.svelte new file mode 100644 index 0000000000..8674130bb8 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/target-shadow-dom/main.svelte @@ -0,0 +1,19 @@ + + +
From 416bc85d9c089aa797cc1587f90b34462f291cc8 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:35:41 +0100 Subject: [PATCH 003/102] breaking: add $bindable() rune to denote bindable props (#10851) Alternative to / closes #10804 closes #10768 closes #10711 --- packages/svelte/src/compiler/errors.js | 1 + .../src/compiler/phases/2-analyze/index.js | 28 ++++++++++--- .../compiler/phases/2-analyze/validation.js | 33 ++++++++++++--- .../3-transform/client/transform-client.js | 40 +++++++++++++------ .../phases/3-transform/client/utils.js | 9 +++-- .../3-transform/client/visitors/global.js | 3 +- .../client/visitors/javascript-legacy.js | 6 +-- .../client/visitors/javascript-runes.js | 35 ++++++++-------- .../3-transform/client/visitors/template.js | 1 + .../3-transform/server/transform-server.js | 28 ++++++++++--- .../svelte/src/compiler/phases/constants.js | 1 + packages/svelte/src/compiler/types/index.d.ts | 6 ++- .../svelte/src/compiler/utils/builders.js | 9 +++++ .../src/internal/client/reactivity/props.js | 18 +++++++-- .../svelte/src/internal/client/runtime.js | 26 ++++++++---- .../svelte/src/internal/client/validate.js | 21 +++++++++- packages/svelte/src/internal/server/index.js | 4 +- packages/svelte/src/main/ambient.d.ts | 13 +++++- .../runes-wrong-bindable-args/_config.js | 8 ++++ .../runes-wrong-bindable-args/main.svelte | 3 ++ .../runes-wrong-bindable-placement/_config.js | 8 ++++ .../main.svelte | 3 ++ .../main.svelte.js | 1 + .../samples/bind-and-spread/button.svelte | 5 +-- .../bind-state-property/CheckBox.svelte | 2 +- .../samples/each-bind-this-member/main.svelte | 2 +- .../Counter.svelte | 2 +- .../sub.svelte | 2 +- .../Counter.svelte | 2 +- .../samples/props-alias/Counter.svelte | 2 +- .../props-bound-fallback/Counter.svelte | 2 +- .../props-bound-to-normal/Inner.svelte | 2 +- .../samples/props-bound/Counter.svelte | 2 +- .../props-default-reactivity/Counter.svelte | 2 +- .../props-default-value-behavior/inner.svelte | 2 +- .../props-not-bindable-spread/Counter.svelte | 5 +++ .../props-not-bindable-spread/_config.js | 11 +++++ .../props-not-bindable-spread/main.svelte | 7 ++++ .../samples/props-not-bindable/Counter.svelte | 5 +++ .../samples/props-not-bindable/_config.js | 11 +++++ .../samples/props-not-bindable/main.svelte | 7 ++++ .../samples/proxy-prop-bound/Counter.svelte | 2 +- .../_expected/server/index.svelte.js | 1 - packages/svelte/types/index.d.ts | 19 +++++++-- .../src/lib/CodeMirror.svelte | 1 + .../routes/docs/content/01-api/02-runes.md | 22 +++++++++- 46 files changed, 330 insertions(+), 93 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte create mode 100644 packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/Counter.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable-spread/main.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable/Counter.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/props-not-bindable/main.svelte diff --git a/packages/svelte/src/compiler/errors.js b/packages/svelte/src/compiler/errors.js index 72397e1f6e..ade5e580ae 100644 --- a/packages/svelte/src/compiler/errors.js +++ b/packages/svelte/src/compiler/errors.js @@ -182,6 +182,7 @@ const runes = { `$props() assignment must not contain nested properties or computed keys`, 'invalid-props-location': () => `$props() can only be used at the top level of components as a variable declaration initializer`, + 'invalid-bindable-location': () => `$bindable() can only be used inside a $props() declaration`, /** @param {string} rune */ 'invalid-state-location': (rune) => `${rune}(...) can only be used as a variable declaration initializer or a class field`, diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 8140439145..da3886cc53 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -436,7 +436,7 @@ export function analyze_component(root, options) { ); } } else { - instance.scope.declare(b.id('$$props'), 'prop', 'synthetic'); + instance.scope.declare(b.id('$$props'), 'bindable_prop', 'synthetic'); instance.scope.declare(b.id('$$restProps'), 'rest_prop', 'synthetic'); for (const { ast, scope, scopes } of [module, instance, template]) { @@ -466,7 +466,10 @@ export function analyze_component(root, options) { } for (const [name, binding] of instance.scope.declarations) { - if (binding.kind === 'prop' && binding.node.name !== '$$props') { + if ( + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && + binding.node.name !== '$$props' + ) { const references = binding.references.filter( (r) => r.node !== binding.node && r.path.at(-1)?.type !== 'ExportSpecifier' ); @@ -759,7 +762,7 @@ const legacy_scope_tweaker = { (binding.kind === 'normal' && (binding.declaration_kind === 'let' || binding.declaration_kind === 'var'))) ) { - binding.kind = 'prop'; + binding.kind = 'bindable_prop'; if (specifier.exported.name !== specifier.local.name) { binding.prop_alias = specifier.exported.name; } @@ -797,7 +800,7 @@ const legacy_scope_tweaker = { for (const declarator of node.declaration.declarations) { for (const id of extract_identifiers(declarator.id)) { const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); - binding.kind = 'prop'; + binding.kind = 'bindable_prop'; } } } @@ -886,11 +889,24 @@ const runes_scope_tweaker = { property.key.type === 'Identifier' ? property.key.name : /** @type {string} */ (/** @type {import('estree').Literal} */ (property.key).value); - const initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; + let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null; const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name)); binding.prop_alias = alias; - binding.initial = initial; // rewire initial from $props() to the actual initial value + + // rewire initial from $props() to the actual initial value, stripping $bindable() if necessary + if ( + initial?.type === 'CallExpression' && + initial.callee.type === 'Identifier' && + initial.callee.name === '$bindable' + ) { + binding.initial = /** @type {import('estree').Expression | null} */ ( + initial.arguments[0] ?? null + ); + binding.kind = 'bindable_prop'; + } else { + binding.initial = initial; + } } } }, diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index cf6f00637f..74ed1b1005 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -299,17 +299,19 @@ const validation = { error(node, 'invalid-binding-expression'); } + const binding = context.state.scope.get(left.name); + if ( assignee.type === 'Identifier' && node.name !== 'this' // bind:this also works for regular variables ) { - const binding = context.state.scope.get(left.name); // 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) @@ -328,8 +330,6 @@ const validation = { // TODO handle mutations of non-state/props in runes mode } - const binding = context.state.scope.get(left.name); - if (node.name === 'group') { if (!binding) { error(node, 'INTERNAL', 'Cannot find declaration for bind:group'); @@ -780,7 +780,25 @@ function validate_call_expression(node, scope, path) { error(node, 'invalid-props-location'); } - if (rune === '$state' || rune === '$derived' || rune === '$derived.by') { + if (rune === '$bindable') { + if (parent.type === 'AssignmentPattern' && path.at(-3)?.type === 'ObjectPattern') { + const declarator = path.at(-4); + if ( + declarator?.type === 'VariableDeclarator' && + get_rune(declarator.init, scope) === '$props' + ) { + return; + } + } + error(node, 'invalid-bindable-location'); + } + + if ( + rune === '$state' || + rune === '$state.frozen' || + rune === '$derived' || + rune === '$derived.by' + ) { if (parent.type === 'VariableDeclarator') return; if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; error(node, 'invalid-state-location', rune); @@ -873,6 +891,8 @@ export const validation_runes_js = { error(node, 'invalid-rune-args-length', rune, [0, 1]); } else if (rune === '$props') { error(node, 'invalid-props-location'); + } else if (rune === '$bindable') { + error(node, 'invalid-bindable-location'); } }, AssignmentExpression(node, { state }) { @@ -1022,6 +1042,9 @@ export const validation_runes = merge(validation, a11y_validators, { } }, CallExpression(node, { state, path }) { + if (get_rune(node, state.scope) === '$bindable' && node.arguments.length > 1) { + error(node, 'invalid-rune-args-length', '$bindable', [0, 1]); + } validate_call_expression(node, state.scope, path); }, EachBlock(node, { next, state }) { @@ -1062,7 +1085,7 @@ export const validation_runes = merge(validation, a11y_validators, { state.has_props_rune = true; if (args.length > 0) { - error(node, 'invalid-rune-args-length', '$props', [0]); + error(node, 'invalid-rune-args-length', rune, [0]); } if (node.id.type !== 'ObjectPattern') { 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 b13ec8e627..1b384798b7 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 @@ -239,7 +239,7 @@ export function client_component(source, analysis, options) { ); }); - const properties = analysis.exports.map(({ name, alias }) => { + const component_returned_object = analysis.exports.map(({ name, alias }) => { const expression = serialize_get_binding(b.id(name), instance_state); if (expression.type === 'Identifier' && !options.dev) { @@ -249,10 +249,26 @@ export function client_component(source, analysis, options) { return b.get(alias ?? name, [b.return(expression)]); }); - if (analysis.accessors) { - for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind !== 'prop' || name.startsWith('$$')) continue; + const properties = [...analysis.instance.scope.declarations].filter( + ([name, binding]) => + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !name.startsWith('$$') + ); + if (analysis.runes && options.dev) { + /** @type {import('estree').Literal[]} */ + const bindable = []; + for (const [name, binding] of properties) { + if (binding.kind === 'bindable_prop') { + bindable.push(b.literal(binding.prop_alias ?? name)); + } + } + instance.body.unshift( + b.stmt(b.call('$.validate_prop_bindings', b.id('$$props'), b.array(bindable))) + ); + } + + if (analysis.accessors) { + for (const [name, binding] of properties) { const key = binding.prop_alias ?? name; const getter = b.get(key, [b.return(b.call(b.id(name)))]); @@ -271,12 +287,12 @@ export function client_component(source, analysis, options) { }; } - properties.push(getter, setter); + component_returned_object.push(getter, setter); } } if (options.legacy.componentApi) { - properties.push( + component_returned_object.push( b.init('$set', b.id('$.update_legacy_props')), b.init( '$on', @@ -292,7 +308,7 @@ export function client_component(source, analysis, options) { ) ); } else if (options.dev) { - properties.push( + component_returned_object.push( b.init( '$set', b.thunk( @@ -360,8 +376,8 @@ export function client_component(source, analysis, options) { append_styles(); component_block.body.push( - properties.length > 0 - ? b.return(b.call('$.pop', b.object(properties))) + component_returned_object.length > 0 + ? b.return(b.call('$.pop', b.object(component_returned_object))) : b.stmt(b.call('$.pop')) ); @@ -369,7 +385,7 @@ export function client_component(source, analysis, options) { /** @type {string[]} */ const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); + if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); } component_block.body.unshift( @@ -476,9 +492,7 @@ export function client_component(source, analysis, options) { /** @type {import('estree').Property[]} */ const props_str = []; - for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind !== 'prop' || name.startsWith('$$')) continue; - + for (const [name, binding] of properties) { const key = binding.prop_alias ?? name; const prop_def = typeof ce === 'boolean' ? {} : ce.props?.[key] || {}; if ( 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 d93f8f5b92..cd274eef23 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/utils.js @@ -78,7 +78,7 @@ export function serialize_get_binding(node, state) { return typeof binding.expression === 'function' ? binding.expression(node) : binding.expression; } - if (binding.kind === 'prop') { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { if (binding.node.name === '$$props') { // Special case for $$props which only exists in the old world // TODO this probably shouldn't have a 'prop' binding kind @@ -377,6 +377,7 @@ export function serialize_set_binding(node, context, fallback, options) { binding.kind !== 'state' && binding.kind !== 'frozen_state' && binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && !is_store @@ -389,7 +390,7 @@ export function serialize_set_binding(node, context, fallback, options) { const serialize = () => { if (left === node.left) { - if (binding.kind === 'prop') { + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') { return b.call(left, value); } else if (is_store) { return b.call('$.store_set', serialize_get_binding(b.id(left_name), state), value); @@ -467,7 +468,7 @@ export function serialize_set_binding(node, context, fallback, options) { b.call('$.untrack', b.id('$' + left_name)) ); } else if (!state.analysis.runes) { - if (binding.kind === 'prop') { + if (binding.kind === 'bindable_prop') { return b.call( left, b.sequence([ @@ -571,7 +572,7 @@ function get_hoistable_params(node, context) { params.push(b.id(binding.expression.object.arguments[0].name)); } else if ( // If we are referencing a simple $$props value, then we need to reference the object property instead - binding.kind === 'prop' && + (binding.kind === 'prop' || binding.kind === 'bindable_prop') && !binding.reassigned && binding.initial === null && !context.state.analysis.accessors diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js index 5d1689cadc..c299dd99ef 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/global.js @@ -52,6 +52,7 @@ export const global_visitors = { binding?.kind === 'each' || binding?.kind === 'legacy_reactive' || binding?.kind === 'prop' || + binding?.kind === 'bindable_prop' || is_store ) { /** @type {import('estree').Expression[]} */ @@ -64,7 +65,7 @@ export const global_visitors = { fn += '_store'; args.push(serialize_get_binding(b.id(name), state), b.call('$' + name)); } else { - if (binding.kind === 'prop') fn += '_prop'; + if (binding.kind === 'prop' || binding.kind === 'bindable_prop') fn += '_prop'; args.push(b.id(name)); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js index ed4c6e8474..ffb089bf83 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js @@ -40,7 +40,7 @@ export const javascript_visitors_legacy = { state.scope.get_bindings(declarator) ); const has_state = bindings.some((binding) => binding.kind === 'state'); - const has_props = bindings.some((binding) => binding.kind === 'prop'); + const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { const init = declarator.init; @@ -80,7 +80,7 @@ export const javascript_visitors_legacy = { declarations.push( b.declarator( path.node, - binding.kind === 'prop' + binding.kind === 'bindable_prop' ? get_prop_source(binding, state, binding.prop_alias ?? name, value) : value ) @@ -168,7 +168,7 @@ export const javascript_visitors_legacy = { // If the binding is a prop, we need to deep read it because it could be fine-grained $state // from a runes-component, where mutations don't trigger an update on the prop as a whole. - if (name === '$$props' || name === '$$restProps' || binding.kind === 'prop') { + if (name === '$$props' || name === '$$restProps' || binding.kind === 'bindable_prop') { serialized = b.call('$.deep_read_state', serialized); } diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index 3eb43322b7..a89ae00f70 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -207,33 +207,30 @@ export const javascript_visitors_runes = { seen.push(name); - let id = property.value; - let initial = undefined; - - if (property.value.type === 'AssignmentPattern') { - id = property.value.left; - initial = /** @type {import('estree').Expression} */ (visit(property.value.right)); - } - + let id = + property.value.type === 'AssignmentPattern' ? property.value.left : property.value; assert.equal(id.type, 'Identifier'); - const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name)); + const initial = + binding.initial && + /** @type {import('estree').Expression} */ (visit(binding.initial)); if (binding.reassigned || state.analysis.accessors || initial) { declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial))); } } else { // RestElement - declarations.push( - b.declarator( - property.argument, - b.call( - '$.rest_props', - b.id('$$props'), - b.array(seen.map((name) => b.literal(name))) - ) - ) - ); + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))]; + + if (state.options.dev) { + // include rest name, so we can provide informative error messages + args.push( + b.literal(/** @type {import('estree').Identifier} */ (property.argument).name) + ); + } + + declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args))); } } 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 265b09591c..7e4a82bef9 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 @@ -1382,6 +1382,7 @@ function serialize_event_handler(node, { state, visit }) { binding.kind === 'legacy_reactive' || binding.kind === 'derived' || binding.kind === 'prop' || + binding.kind === 'bindable_prop' || binding.kind === 'store_sub') ) { handler = dynamic_handler(); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 8ff7d2c316..fc5ef8e57e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -446,6 +446,7 @@ function serialize_set_binding(node, context, fallback) { binding.kind !== 'state' && binding.kind !== 'frozen_state' && binding.kind !== 'prop' && + binding.kind !== 'bindable_prop' && binding.kind !== 'each' && binding.kind !== 'legacy_reactive' && !is_store @@ -690,7 +691,21 @@ const javascript_visitors_runes = { } if (rune === '$props') { - declarations.push(b.declarator(declarator.id, b.id('$$props'))); + // remove $bindable() from props declaration + const id = walk(declarator.id, null, { + AssignmentPattern(node) { + if ( + node.right.type === 'CallExpression' && + get_rune(node.right, state.scope) === '$bindable' + ) { + const right = node.right.arguments.length + ? /** @type {import('estree').Expression} */ (visit(node.right.arguments[0])) + : b.id('undefined'); + return b.assignment_pattern(node.left, right); + } + } + }); + declarations.push(b.declarator(id, b.id('$$props'))); continue; } @@ -1131,7 +1146,7 @@ const javascript_visitors_legacy = { state.scope.get_bindings(declarator) ); const has_state = bindings.some((binding) => binding.kind === 'state'); - const has_props = bindings.some((binding) => binding.kind === 'prop'); + const has_props = bindings.some((binding) => binding.kind === 'bindable_prop'); if (!has_state && !has_props) { declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); @@ -2217,7 +2232,8 @@ export function server_component(analysis, options) { }); } - // If the component binds to a child, we need to put the template in a loop and repeat until bindings are stable + // If the component binds to a child, we need to put the template in a loop and repeat until legacy bindings are stable. + // We can remove this once the legacy syntax is gone. if (analysis.uses_component_bindings) { template.body = [ b.let('$$settled', b.true), @@ -2258,7 +2274,7 @@ export function server_component(analysis, options) { /** @type {import('estree').Property[]} */ const props = []; for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop' && !name.startsWith('$$')) { + if (binding.kind === 'bindable_prop' && !name.startsWith('$$')) { props.push(b.init(binding.prop_alias ?? name, b.id(name))); } } @@ -2266,6 +2282,8 @@ export function server_component(analysis, options) { props.push(b.init(alias ?? name, b.id(name))); } if (props.length > 0) { + // This has no effect in runes mode other than throwing an error when someone passes + // undefined to a binding that has a default value. template.body.push(b.stmt(b.call('$.bind_props', b.id('$$props'), b.object(props)))); } @@ -2280,7 +2298,7 @@ export function server_component(analysis, options) { /** @type {string[]} */ const named_props = analysis.exports.map(({ name, alias }) => alias ?? name); for (const [name, binding] of analysis.instance.scope.declarations) { - if (binding.kind === 'prop') named_props.push(binding.prop_alias ?? name); + if (binding.kind === 'bindable_prop') named_props.push(binding.prop_alias ?? name); } component_block.body.unshift( diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index eaf01b7f34..822174d4dd 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -32,6 +32,7 @@ export const Runes = /** @type {const} */ ([ '$state', '$state.frozen', '$props', + '$bindable', '$derived', '$derived.by', '$effect', diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index 6d09effe93..ba7fe22b0f 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -241,7 +241,8 @@ export interface Binding { node: Identifier; /** * - `normal`: A variable that is not in any way special - * - `prop`: A normal prop (possibly mutated) + * - `prop`: A normal prop (possibly reassigned or mutated) + * - `bindable_prop`: A prop one can `bind:` to (possibly reassigned or mutated) * - `rest_prop`: A rest prop * - `state`: A state variable * - `derived`: A derived variable @@ -253,6 +254,7 @@ export interface Binding { kind: | 'normal' | 'prop' + | 'bindable_prop' | 'rest_prop' | 'state' | 'frozen_state' @@ -280,7 +282,7 @@ export interface Binding { scope: Scope; /** For `legacy_reactive`: its reactive dependencies */ legacy_dependencies: Binding[]; - /** Legacy props: the `class` in `{ export klass as class}` */ + /** Legacy props: the `class` in `{ export klass as class}`. $props(): The `class` in { class: klass } = $props() */ prop_alias: string | null; /** * If this is set, all references should use this expression instead of the identifier name. diff --git a/packages/svelte/src/compiler/utils/builders.js b/packages/svelte/src/compiler/utils/builders.js index 0b837d5daf..a5776a46de 100644 --- a/packages/svelte/src/compiler/utils/builders.js +++ b/packages/svelte/src/compiler/utils/builders.js @@ -17,6 +17,15 @@ export function array_pattern(elements) { return { type: 'ArrayPattern', elements }; } +/** + * @param {import('estree').Pattern} left + * @param {import('estree').Expression} right + * @returns {import('estree').AssignmentPattern} + */ +export function assignment_pattern(left, right) { + return { type: 'AssignmentPattern', left, right }; +} + /** * @param {Array} params * @param {import('estree').BlockStatement | import('estree').Expression} body diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 8cfd5c266a..20d4870825 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -36,13 +36,22 @@ export function update_pre_prop(fn, d = 1) { /** * The proxy handler for rest props (i.e. `const { x, ...rest } = $props()`). * Is passed the full `$$props` object and excludes the named props. - * @type {ProxyHandler<{ props: Record, exclude: Array }>}} + * @type {ProxyHandler<{ props: Record, exclude: Array, name?: string }>}} */ const rest_props_handler = { get(target, key) { if (target.exclude.includes(key)) return; return target.props[key]; }, + set(target, key) { + if (DEV) { + throw new Error( + `Rest element properties of $props() such as ${target.name}.${String(key)} are readonly` + ); + } + + return false; + }, getOwnPropertyDescriptor(target, key) { if (target.exclude.includes(key)) return; if (key in target.props) { @@ -64,11 +73,12 @@ const rest_props_handler = { /** * @param {Record} props - * @param {string[]} rest + * @param {string[]} exclude + * @param {string} [name] * @returns {Record} */ -export function rest_props(props, rest) { - return new Proxy({ props, exclude: rest }, rest_props_handler); +export function rest_props(props, exclude, name) { + return new Proxy(DEV ? { props, exclude, name } : { props, exclude }, rest_props_handler); } /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index fe2c323c32..c3af8c992e 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1258,22 +1258,34 @@ export function unwrap(value) { } if (DEV) { - /** @param {string} rune */ - function throw_rune_error(rune) { + /** + * @param {string} rune + * @param {string[]} [variants] + */ + function throw_rune_error(rune, variants = []) { if (!(rune in globalThis)) { + // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message // @ts-ignore globalThis[rune] = () => { - // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message - throw new Error(`${rune} is only available inside .svelte and .svelte.js/ts files`); + throw new Error(`${rune}() is only available inside .svelte and .svelte.js/ts files`); }; + for (const variant of variants) { + // @ts-ignore + globalThis[rune][variant] = () => { + throw new Error( + `${rune}.${variant}() is only available inside .svelte and .svelte.js/ts files` + ); + }; + } } } - throw_rune_error('$state'); - throw_rune_error('$effect'); - throw_rune_error('$derived'); + throw_rune_error('$state', ['frozen']); + throw_rune_error('$effect', ['pre', 'root', 'active']); + throw_rune_error('$derived', ['by']); throw_rune_error('$inspect'); throw_rune_error('$props'); + throw_rune_error('$bindable'); } /** diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 442d91190b..befd68b027 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,5 +1,5 @@ import { untrack } from './runtime.js'; -import { is_array } from './utils.js'; +import { get_descriptor, is_array } from './utils.js'; /** regex of all html void element names */ const void_element_names = @@ -137,3 +137,22 @@ export function validate_component(component_fn) { } return component_fn; } + +/** + * @param {Record} $$props + * @param {string[]} bindable + */ +export function validate_prop_bindings($$props, bindable) { + for (const key in $$props) { + if (!bindable.includes(key)) { + var setter = get_descriptor($$props, key)?.set; + + if (setter) { + throw new Error( + `Cannot use bind:${key} on this component because the property was not declared as bindable. ` + + `To mark a property as bindable, use the $bindable() rune like this: \`let { ${key} = $bindable() } = $props()\`` + ); + } + } + } +} diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index a6b0001d33..bc1caf2d36 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -588,8 +588,8 @@ export function sanitize_slots(props) { } /** - * If the prop has a fallback and is bound in the parent component, - * propagate the fallback value upwards. + * Legacy mode: If the prop has a fallback and is bound in the + * parent component, propagate the fallback value upwards. * @param {Record} props_parent * @param {Record} props_now */ diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index 1cb02f9f4d..a2ad6d63af 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -172,13 +172,24 @@ declare namespace $effect { * Declares the props that a component accepts. Example: * * ```ts - * let { optionalProp = 42, requiredProp }: { optionalProp?: number; requiredProps: string } = $props(); + * let { optionalProp = 42, requiredProp, bindableProp = $bindable() }: { optionalProp?: number; requiredProps: string; bindableProp: boolean } = $props(); * ``` * * https://svelte-5-preview.vercel.app/docs/runes#$props */ declare function $props(): any; +/** + * Declares a prop as bindable, meaning the parent component can use `bind:propName={value}` to bind to it. + * + * ```ts + * let { propName = $bindable() }: { propName: boolean } = $props(); + * ``` + * + * https://svelte-5-preview.vercel.app/docs/runes#$bindable + */ +declare function $bindable(t?: T): T; + /** * Inspects one or more values whenever they, or the properties they contain, change. Example: * diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/_config.js new file mode 100644 index 0000000000..0d1d0e0709 --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-rune-args-length', + message: '$bindable can only be called with 0 or 1 arguments' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/main.svelte new file mode 100644 index 0000000000..01d49aa5cd --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-args/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/_config.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/_config.js new file mode 100644 index 0000000000..bc92ee9ced --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/_config.js @@ -0,0 +1,8 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-bindable-location', + message: '$bindable() can only be used inside a $props() declaration' + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte new file mode 100644 index 0000000000..5af7f0171d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte @@ -0,0 +1,3 @@ + diff --git a/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte.js b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte.js new file mode 100644 index 0000000000..344995a4bb --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/runes-wrong-bindable-placement/main.svelte.js @@ -0,0 +1 @@ +const { a = $bindable() } = $state(); diff --git a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte index 45c1abced8..7bb18ec5f7 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-and-spread/button.svelte @@ -1,6 +1,5 @@ - + diff --git a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte index 172b699205..22c46a363d 100644 --- a/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte +++ b/packages/svelte/tests/runtime-runes/samples/bind-state-property/CheckBox.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte index a685cc9c84..7d46ea90f0 100644 --- a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte @@ -1,5 +1,5 @@ {#each items as item, i} diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte index d1be326830..57cbebde12 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-discouraged/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte index d1be326830..57cbebde12 100644 --- a/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/non-local-mutation-with-binding/Counter.svelte @@ -1,6 +1,6 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte index 6b9240c70e..a2bda4c70b 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-fallback/Counter.svelte @@ -1,5 +1,5 @@ {count} diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte index 82b2f0648a..3d1261071e 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound-to-normal/Inner.svelte @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte index e9bcb945b2..67b08a561f 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-bound/Counter.svelte @@ -1,5 +1,5 @@ diff --git a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte index 2ba2ed9100..077eda5709 100644 --- a/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte +++ b/packages/svelte/tests/runtime-runes/samples/props-default-reactivity/Counter.svelte @@ -1,6 +1,6 @@ + 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 205bdd2bf2..d88a749bc8 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 @@ -179,3 +179,7 @@ In Svelte 4, `null` and `undefined` were printed as the corresponding string. In ### `bind:files` values can only be `null`, `undefined` or `FileList` `bind:files` is now a two-way binding. As such, when setting a value, it needs to be either falsy (`null` or `undefined`) or of type `FileList`. + +### Bindings now react to form resets + +Previously, bindings did not take into account `reset` event of forms, and therefore values could get out of sync with the DOM. Svelte 5 fixes this by placing a `reset` listener on the document and invoking bindings where necessary. From b468978e4dd13baf74323f318bcd030cbe5f5176 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Mar 2024 18:52:24 +0100 Subject: [PATCH 005/102] fix: handle multiple snippet parameters with one or more being optional (#10833) fixes #10825 Co-authored-by: Dominic Gannaway --- .changeset/slow-plums-chew.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/read/context.js | 5 ++++- .../runtime-runes/samples/snippet-typescript/_config.js | 2 +- .../runtime-runes/samples/snippet-typescript/main.svelte | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 .changeset/slow-plums-chew.md diff --git a/.changeset/slow-plums-chew.md b/.changeset/slow-plums-chew.md new file mode 100644 index 0000000000..94908bb9b9 --- /dev/null +++ b/.changeset/slow-plums-chew.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: handle multiple snippet parameters with one or more being optional diff --git a/packages/svelte/src/compiler/phases/1-parse/read/context.js b/packages/svelte/src/compiler/phases/1-parse/read/context.js index 054d60cb0d..b69c16d320 100644 --- a/packages/svelte/src/compiler/phases/1-parse/read/context.js +++ b/packages/svelte/src/compiler/phases/1-parse/read/context.js @@ -121,7 +121,10 @@ function read_type_annotation(parser, optional_allowed = false) { const template = parser.template.slice(0, a).replace(/[^\n]/g, ' ') + insert + - parser.template.slice(parser.index); + // If this is a type annotation for a function parameter, Acorn-TS will treat subsequent + // parameters as part of a sequence expression instead, and will then error on optional + // parameters (`?:`). Therefore replace that sequence with something that will not error. + parser.template.slice(parser.index).replace(/\?\s*:/g, ':'); let expression = parse_expression_at(template, parser.ts, a); // `foo: bar = baz` gets mangled — fix it diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-typescript/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-typescript/_config.js index 094be77b80..06a7242aab 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-typescript/_config.js +++ b/packages/svelte/tests/runtime-runes/samples/snippet-typescript/_config.js @@ -1,5 +1,5 @@ import { test } from '../../test'; export default test({ - html: '1 2 3 4 5' + html: '1 2 3 4 5 6a' }); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-typescript/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-typescript/main.svelte index 9923b1d854..ce6aa5fd84 100644 --- a/packages/svelte/tests/runtime-runes/samples/snippet-typescript/main.svelte +++ b/packages/svelte/tests/runtime-runes/samples/snippet-typescript/main.svelte @@ -16,9 +16,13 @@ {#snippet counter5(c?: number = 5)} {c} {/snippet} +{#snippet counter6(c?: number, d?: string)} + {c}{d} +{/snippet} {@render counter1(1)} {@render counter2({ c: 2 })} {@render counter3(3)} {@render counter4()} {@render counter5()} +{@render counter6(6, 'a')} From 852eca4ee61586d6bc9c7e9c87ff963a6f1463e8 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Fri, 22 Mar 2024 19:19:22 +0100 Subject: [PATCH 006/102] chore: error on accessing global that is a rune (#10877) ...rather than only when the function is invoked --- .../svelte/src/internal/client/runtime.js | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index c3af8c992e..4d89430650 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1260,29 +1260,32 @@ export function unwrap(value) { if (DEV) { /** * @param {string} rune - * @param {string[]} [variants] */ - function throw_rune_error(rune, variants = []) { + function throw_rune_error(rune) { if (!(rune in globalThis)) { // TODO if people start adjusting the "this can contain runes" config through v-p-s more, adjust this message - // @ts-ignore - globalThis[rune] = () => { - throw new Error(`${rune}() is only available inside .svelte and .svelte.js/ts files`); - }; - for (const variant of variants) { - // @ts-ignore - globalThis[rune][variant] = () => { + /** @type {any} */ + let value; // let's hope noone modifies this global, but belts and braces + Object.defineProperty(globalThis, rune, { + configurable: true, + get: () => { + if (value !== undefined) { + return value; + } throw new Error( - `${rune}.${variant}() is only available inside .svelte and .svelte.js/ts files` + `The ${rune} rune is only available inside .svelte and .svelte.js/ts files` ); - }; - } + }, + set: (v) => { + value = v; + } + }); } } - throw_rune_error('$state', ['frozen']); - throw_rune_error('$effect', ['pre', 'root', 'active']); - throw_rune_error('$derived', ['by']); + throw_rune_error('$state'); + throw_rune_error('$effect'); + throw_rune_error('$derived'); throw_rune_error('$inspect'); throw_rune_error('$props'); throw_rune_error('$bindable'); From 83f30bf0e8eeb46252cd326fa0fadbe6e926dbf8 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 22 Mar 2024 18:36:24 +0000 Subject: [PATCH 007/102] fix: allow runes for variable declarations in the template (#10879) --- .changeset/red-poets-study.md | 5 +++++ .../3-transform/client/visitors/template.js | 3 ++- .../samples/state-in-template/_config.js | 19 +++++++++++++++++++ .../samples/state-in-template/main.svelte | 19 +++++++++++++++++++ 4 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 .changeset/red-poets-study.md create mode 100644 packages/svelte/tests/runtime-runes/samples/state-in-template/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/state-in-template/main.svelte diff --git a/.changeset/red-poets-study.md b/.changeset/red-poets-study.md new file mode 100644 index 0000000000..3a9338f32a --- /dev/null +++ b/.changeset/red-poets-study.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: allow runes for variable declarations in the template 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 01daedb26c..cc018e91cf 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 @@ -3223,5 +3223,6 @@ export const template_visitors = { node: b.id('$.document') }); }, - CallExpression: javascript_visitors_runes.CallExpression + CallExpression: javascript_visitors_runes.CallExpression, + VariableDeclaration: javascript_visitors_runes.VariableDeclaration }; diff --git a/packages/svelte/tests/runtime-runes/samples/state-in-template/_config.js b/packages/svelte/tests/runtime-runes/samples/state-in-template/_config.js new file mode 100644 index 0000000000..093c240424 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-in-template/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
`, + + async test({ assert, target }) { + const btn = target.querySelector('button'); + + flushSync(() => { + btn?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/state-in-template/main.svelte b/packages/svelte/tests/runtime-runes/samples/state-in-template/main.svelte new file mode 100644 index 0000000000..b0eb8f8c6d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/state-in-template/main.svelte @@ -0,0 +1,19 @@ + + +
{ + e.preventDefault(); + const data = new FormData(e.target); + const state = $state({ name: data.get('name') }); + set.add(state); + e.target.reset(); +}}> + + +
+ +{#each set as item} +
{item.name}
+{/each} From c47c5713e2e0a15a3f550ca02d40a7196cc79fea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:22:32 -0400 Subject: [PATCH 008/102] Version Packages (next) (#10869) 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 0921244615..e57048e4f3 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -225,6 +225,7 @@ "real-pandas-brush", "red-doors-own", "red-feet-worry", + "red-poets-study", "rich-cobras-exist", "rich-olives-yell", "rich-sheep-burn", @@ -258,6 +259,7 @@ "short-countries-rush", "silent-apes-report", "silly-laws-happen", + "silly-ways-wash", "silver-points-approve", "sixty-items-crash", "slimy-clouds-talk", @@ -266,9 +268,11 @@ "slow-beds-shave", "slow-chefs-dream", "slow-kids-sparkle", + "slow-plums-chew", "slow-wombats-reply", "small-papayas-laugh", "small-sheep-type", + "small-spiders-fail", "smart-parents-swim", "smart-turkeys-tell", "smart-zebras-pay", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 56ef4d3af2..47fdbd7db3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,17 @@ # svelte +## 5.0.0-next.82 + +### Patch Changes + +- fix: allow runes for variable declarations in the template ([#10879](https://github.com/sveltejs/svelte/pull/10879)) + +- feat: take form resets into account for two way bindings ([#10617](https://github.com/sveltejs/svelte/pull/10617)) + +- fix: handle multiple snippet parameters with one or more being optional ([#10833](https://github.com/sveltejs/svelte/pull/10833)) + +- breaking: apply fallback value every time in runes mode ([#10797](https://github.com/sveltejs/svelte/pull/10797)) + ## 5.0.0-next.81 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index c7c2f1af04..0332f923c6 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.81", + "version": "5.0.0-next.82", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 89fd02424a..69694b6b29 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.81'; +export const VERSION = '5.0.0-next.82'; export const PUBLIC_VERSION = '5'; From b6c7956b2676f171fcae3079ce87c54782466a70 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 22 Mar 2024 16:10:43 -0400 Subject: [PATCH 009/102] chore: remove blocks (#10880) * attach DOM to effects * null out effect.dom * remove some block.d references * another * drive-by fix * better comment * unused arg * another * another * another * more * finish renaming stuff * more * remove item.d * remove block.d * remove effect.block * remove current_block * delete delete delete * rename * remove some stuff we dont need * simplify --- .../src/internal/client/dom/blocks/await.js | 47 +- .../src/internal/client/dom/blocks/each.js | 429 ++++++++---------- .../src/internal/client/dom/blocks/if.js | 58 +-- .../src/internal/client/dom/blocks/key.js | 68 ++- .../src/internal/client/dom/blocks/snippet.js | 27 +- .../client/dom/blocks/svelte-component.js | 63 +-- .../client/dom/blocks/svelte-element.js | 82 ++-- .../internal/client/dom/blocks/svelte-head.js | 44 +- .../src/internal/client/dom/blocks/utils.js | 7 - .../src/internal/client/dom/elements/misc.js | 2 - .../client/dom/elements/transitions.js | 8 +- .../src/internal/client/dom/template.js | 4 +- .../src/internal/client/reactivity/effects.js | 81 ++-- .../src/internal/client/reactivity/types.d.ts | 5 +- packages/svelte/src/internal/client/render.js | 52 +-- .../svelte/src/internal/client/runtime.js | 10 +- .../svelte/src/internal/client/types.d.ts | 7 +- packages/svelte/tests/signals/test.ts | 1 - 18 files changed, 421 insertions(+), 574 deletions(-) delete mode 100644 packages/svelte/src/internal/client/dom/blocks/utils.js diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 5dcf52d914..b437c17389 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -9,8 +9,7 @@ import { set_current_reaction } from '../../runtime.js'; import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js'; -import { DESTROYED, INERT } from '../../constants.js'; -import { create_block } from './utils.js'; +import { INERT } from '../../constants.js'; /** * @template V @@ -22,8 +21,6 @@ import { create_block } from './utils.js'; * @returns {void} */ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { - const block = create_block(); - const component_context = current_component_context; hydrate_block_anchor(anchor); @@ -48,7 +45,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { set_current_effect(branch); set_current_reaction(branch); // TODO do we need both? set_current_component_context(component_context); - var effect = render_effect(() => fn(anchor, value), {}, true); + var effect = render_effect(() => fn(anchor, value), true); set_current_component_context(null); set_current_reaction(null); set_current_effect(null); @@ -60,18 +57,6 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { return effect; } - /** @param {import('#client').Effect} effect */ - function pause(effect) { - if ((effect.f & DESTROYED) !== 0) return; - const block = effect.block; - - pause_effect(effect, () => { - // TODO make this unnecessary - const dom = block?.d; - if (dom) remove(dom); - }); - } - const branch = render_effect(() => { if (input === (input = get_input())) return; @@ -80,20 +65,20 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { if (pending_fn) { if (pending_effect && (pending_effect.f & INERT) === 0) { - if (pending_effect.block?.d) remove(pending_effect.block.d); + if (pending_effect.dom) remove(pending_effect.dom); destroy_effect(pending_effect); } - pending_effect = render_effect(() => pending_fn(anchor), {}, true); + pending_effect = render_effect(() => pending_fn(anchor), true); } - if (then_effect) pause(then_effect); - if (catch_effect) pause(catch_effect); + if (then_effect) pause_effect(then_effect); + if (catch_effect) pause_effect(catch_effect); promise.then( (value) => { if (promise !== input) return; - if (pending_effect) pause(pending_effect); + if (pending_effect) pause_effect(pending_effect); if (then_fn) { then_effect = create_effect(then_fn, value); @@ -101,7 +86,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { }, (error) => { if (promise !== input) return; - if (pending_effect) pause(pending_effect); + if (pending_effect) pause_effect(pending_effect); if (catch_fn) { catch_effect = create_effect(catch_fn, error); @@ -109,24 +94,24 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { } ); } else { - if (pending_effect) pause(pending_effect); - if (catch_effect) pause(catch_effect); + if (pending_effect) pause_effect(pending_effect); + if (catch_effect) pause_effect(catch_effect); if (then_fn) { if (then_effect) { - if (then_effect.block?.d) remove(then_effect.block.d); + if (then_effect.dom) remove(then_effect.dom); destroy_effect(then_effect); } - then_effect = render_effect(() => then_fn(anchor, input), {}, true); + then_effect = render_effect(() => then_fn(anchor, input), true); } } - }, block); + }); branch.ondestroy = () => { // TODO this sucks, tidy it up - if (pending_effect?.block?.d) remove(pending_effect.block.d); - if (then_effect?.block?.d) remove(then_effect.block.d); - if (catch_effect?.block?.d) remove(catch_effect.block.d); + if (pending_effect?.dom) remove(pending_effect.dom); + if (then_effect?.dom) remove(then_effect.dom); + if (catch_effect?.dom) remove(catch_effect.dom); }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index bc0f035378..ce17cced94 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -19,6 +19,7 @@ import { untrack } from '../../runtime.js'; import { destroy_effect, pause_effect, + pause_effects, render_effect, resume_effect, user_effect @@ -26,21 +27,20 @@ import { import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen, map_get, map_set } from '../../utils.js'; import { STATE_SYMBOL } from '../../constants.js'; -import { create_block } from './utils.js'; -var NEW_BLOCK = -1; -var LIS_BLOCK = -2; +var NEW_ITEM = -1; +var LIS_ITEM = -2; /** * The row of a keyed each block that is currently updating. We track this * so that `animate:` directives have something to attach themselves to * @type {import('#client').EachItem | null} */ -export let current_each_item_block = null; +export let current_each_item = null; -/** @param {import('#client').EachItem | null} block */ -export function set_current_each_item_block(block) { - current_each_item_block = block; +/** @param {import('#client').EachItem | null} item */ +export function set_current_each_item(item) { + current_each_item = item; } /** @@ -55,8 +55,6 @@ export function set_current_each_item_block(block) { * @returns {void} */ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_fn) { - var block = create_block(); - /** @type {import('#client').EachState} */ var state = { flags, items: [] }; @@ -71,132 +69,122 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re /** @type {import('#client').Effect | null} */ var fallback = null; - var effect = render_effect( - () => { - var collection = get_collection(); + var effect = render_effect(() => { + var collection = get_collection(); - var array = is_array(collection) - ? collection - : collection == null - ? [] - : Array.from(collection); + var array = is_array(collection) + ? collection + : collection == null + ? [] + : Array.from(collection); - var keys = get_key === null ? array : array.map(get_key); + var keys = get_key === null ? array : array.map(get_key); - var length = array.length; + var length = array.length; - // If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items - // are treated as reactive, so they get wrapped in a signal. - var flags = state.flags; - if ((flags & EACH_IS_STRICT_EQUALS) !== 0 && !is_frozen(array) && !(STATE_SYMBOL in array)) { - flags ^= EACH_IS_STRICT_EQUALS; + // If we are working with an array that isn't proxied or frozen, then remove strict equality and ensure the items + // are treated as reactive, so they get wrapped in a signal. + var flags = state.flags; + if ((flags & EACH_IS_STRICT_EQUALS) !== 0 && !is_frozen(array) && !(STATE_SYMBOL in array)) { + flags ^= EACH_IS_STRICT_EQUALS; - // Additionally if we're in an keyed each block, we'll need ensure the items are all wrapped in signals. - if ((flags & EACH_KEYED) !== 0 && (flags & EACH_ITEM_REACTIVE) === 0) { - flags ^= EACH_ITEM_REACTIVE; - } + // Additionally if we're in an keyed each block, we'll need ensure the items are all wrapped in signals. + if ((flags & EACH_KEYED) !== 0 && (flags & EACH_ITEM_REACTIVE) === 0) { + flags ^= EACH_ITEM_REACTIVE; } + } - /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ - let mismatch = false; - - if (hydrating) { - var is_else = - /** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else'; + /** `true` if there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ + let mismatch = false; + + if (hydrating) { + var is_else = + /** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else'; + + if (is_else !== (length === 0)) { + // hydration mismatch — remove the server-rendered DOM and start over + remove(current_hydration_fragment); + set_current_hydration_fragment(null); + mismatch = true; + } else if (is_else) { + // Remove the each_else comment node or else it will confuse the subsequent hydration algorithm + /** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift(); + } + } - if (is_else !== (length === 0)) { - // hydration mismatch — remove the server-rendered DOM and start over - remove(current_hydration_fragment); - set_current_hydration_fragment(null); + // this is separate to the previous block because `hydrating` might change + if (hydrating) { + var b_items = []; + + // Hydrate block + var hydration_list = /** @type {import('#client').TemplateNode[]} */ ( + current_hydration_fragment + ); + var hydrating_node = hydration_list[0]; + + for (var i = 0; i < length; i++) { + var fragment = get_hydration_fragment(hydrating_node); + set_current_hydration_fragment(fragment); + if (!fragment) { + // If fragment is null, then that means that the server rendered less items than what + // the client code specifies -> break out and continue with client-side node creation mismatch = true; - } else if (is_else) { - // Remove the each_else comment node or else it will confuse the subsequent hydration algorithm - /** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift(); + break; } - } - // this is separate to the previous block because `hydrating` might change - if (hydrating) { - var b_blocks = []; + b_items[i] = create_item(array[i], keys?.[i], i, render_fn, flags); - // Hydrate block - var hydration_list = /** @type {import('#client').TemplateNode[]} */ ( - current_hydration_fragment + // TODO helperise this + hydrating_node = /** @type {import('#client').TemplateNode} */ ( + /** @type {Node} */ ( + /** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling + ).nextSibling ); - var hydrating_node = hydration_list[0]; - - for (var i = 0; i < length; i++) { - var fragment = get_hydration_fragment(hydrating_node); - set_current_hydration_fragment(fragment); - if (!fragment) { - // If fragment is null, then that means that the server rendered less items than what - // the client code specifies -> break out and continue with client-side node creation - mismatch = true; - break; - } - - b_blocks[i] = create_item(array[i], keys?.[i], i, render_fn, flags); - - // TODO helperise this - hydrating_node = /** @type {import('#client').TemplateNode} */ ( - /** @type {Node} */ ( - /** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling - ).nextSibling - ); - } + } - remove_excess_hydration_nodes(hydration_list, hydrating_node); + remove_excess_hydration_nodes(hydration_list, hydrating_node); - state.items = b_blocks; - } + state.items = b_items; + } - if (!hydrating) { - // TODO add 'empty controlled block' optimisation here - reconcile_fn(array, state, anchor, render_fn, flags, keys); - } + if (!hydrating) { + // TODO add 'empty controlled block' optimisation here + reconcile_fn(array, state, anchor, render_fn, flags, keys); + } - if (fallback_fn !== null) { - if (length === 0) { - if (fallback) { - resume_effect(fallback); - } else { - fallback = render_effect( - () => { - fallback_fn(anchor); - var dom = block.d; // TODO would be nice if this was just returned from the managed effect function... - - return () => { - if (dom !== null) { - remove(dom); - dom = null; - } - }; - }, - block, - true - ); - } - } else if (fallback !== null) { - pause_effect(fallback, () => { - fallback = null; - }); + if (fallback_fn !== null) { + if (length === 0) { + if (fallback) { + resume_effect(fallback); + } else { + fallback = render_effect(() => { + var dom = fallback_fn(anchor); + + return () => { + if (dom !== undefined) { + remove(dom); + } + }; + }, true); } + } else if (fallback !== null) { + pause_effect(fallback, () => { + fallback = null; + }); } + } - if (mismatch) { - // Set a fragment so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); - } - }, - block, - false - ); + if (mismatch) { + // Set a fragment so that Svelte continues to operate in hydration mode + set_current_hydration_fragment([]); + } + }); effect.ondestroy = () => { for (var item of state.items) { - if (item.d !== null) { + if (item.e.dom !== null) { + remove(item.e.dom); destroy_effect(item.e); - remove(item.d); } } @@ -274,26 +262,14 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) { state.items = b_items; } else if (a > b) { // remove items - var remaining = a - b; - - var clear = () => { - for (var i = b; i < a; i += 1) { - var block = a_items[i]; - if (block.d) remove(block.d); - } + var effects = []; + for (i = b; i < a; i += 1) { + effects.push(a_items[i].e); + } + pause_effects(effects, () => { state.items.length = b; - }; - - var check = () => { - if (--remaining === 0) { - clear(); - } - }; - - for (; i < a; i += 1) { - pause_effect(a_items[i].e, check); - } + }); } } @@ -311,42 +287,42 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) { * @returns {void} */ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { - var a_blocks = state.items; + var a_items = state.items; - var a = a_blocks.length; + var a = a_items.length; var b = array.length; /** @type {Array} */ - var b_blocks = Array(b); + var b_items = Array(b); var is_animated = (flags & EACH_IS_ANIMATED) !== 0; var should_update = (flags & (EACH_ITEM_REACTIVE | EACH_INDEX_REACTIVE)) !== 0; var start = 0; - var block; + var item; - /** @type {Array} */ + /** @type {import('#client').Effect[]} */ var to_destroy = []; // Step 1 — trim common suffix - while (a > 0 && b > 0 && a_blocks[a - 1].k === keys[b - 1]) { - block = b_blocks[--b] = a_blocks[--a]; - anchor = get_first_child(block); + while (a > 0 && b > 0 && a_items[a - 1].k === keys[b - 1]) { + item = b_items[--b] = a_items[--a]; + anchor = get_first_child(item); - resume_effect(block.e); + resume_effect(item.e); if (should_update) { - update_item(block, array[b], b, flags); + update_item(item, array[b], b, flags); } } // Step 2 — trim common prefix - while (start < a && start < b && a_blocks[start].k === keys[start]) { - block = b_blocks[start] = a_blocks[start]; + while (start < a && start < b && a_items[start].k === keys[start]) { + item = b_items[start] = a_items[start]; - resume_effect(block.e); + resume_effect(item.e); if (should_update) { - update_item(block, array[start], start, flags); + update_item(item, array[start], start, flags); } start += 1; @@ -356,14 +332,14 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { if (start === a) { // add only while (start < b) { - block = create_item(array[start], keys[start], start, render_fn, flags); - b_blocks[start++] = block; - insert_item(block, anchor); + item = create_item(array[start], keys[start], start, render_fn, flags); + b_items[start++] = item; + insert_item(item, anchor); } } else if (start === b) { // remove only while (start < a) { - to_destroy.push(a_blocks[start++]); + to_destroy.push(a_items[start++].e); } } else { // reconcile @@ -372,78 +348,78 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { var indexes = new Map(); var i; var index; - var last_block; + var last_item; var last_sibling; - // store the indexes of each block in the new world + // store the indexes of each item in the new world for (i = start; i < b; i += 1) { - sources[i - start] = NEW_BLOCK; + sources[i - start] = NEW_ITEM; map_set(indexes, keys[i], i); } /** @type {Array} */ var to_animate = []; if (is_animated) { - // for all blocks that were in both the old and the new list, + // for all items that were in both the old and the new list, // measure them and store them in `to_animate` so we can // apply animations once the DOM has been updated - for (i = 0; i < a_blocks.length; i += 1) { - block = a_blocks[i]; - if (indexes.has(block.k)) { - block.a?.measure(); - to_animate.push(block); + for (i = 0; i < a_items.length; i += 1) { + item = a_items[i]; + if (indexes.has(item.k)) { + item.a?.measure(); + to_animate.push(item); } } } - // populate the `sources` array for each old block with + // populate the `sources` array for each old item with // its new index, so that we can calculate moves for (i = start; i < a; i += 1) { - block = a_blocks[i]; - index = map_get(indexes, block.k); + item = a_items[i]; + index = map_get(indexes, item.k); - resume_effect(block.e); + resume_effect(item.e); if (index === undefined) { - to_destroy.push(block); + to_destroy.push(item.e); } else { moved = true; sources[index - start] = i; - b_blocks[index] = block; + b_items[index] = item; if (is_animated) { - to_animate.push(block); + to_animate.push(item); } } } - // if we need to move blocks (as opposed to just adding/removing), + // if we need to move items (as opposed to just adding/removing), // figure out how to do so efficiently (I would be lying if I said // I fully understand this part) if (moved) { mark_lis(sources); } - // working from the back, insert new or moved blocks + // working from the back, insert new or moved items while (b-- > start) { index = sources[b - start]; - var insert = index === NEW_BLOCK; + var insert = index === NEW_ITEM; if (insert) { - block = create_item(array[b], keys[b], b, render_fn, flags); + item = create_item(array[b], keys[b], b, render_fn, flags); } else { - block = b_blocks[b]; + item = b_items[b]; if (should_update) { - update_item(block, array[b], b, flags); + update_item(item, array[b], b, flags); } } - if (insert || (moved && index !== LIS_BLOCK)) { - last_sibling = last_block === undefined ? anchor : get_first_child(last_block); - anchor = insert_item(block, last_sibling); + if (insert || (moved && index !== LIS_ITEM)) { + last_sibling = last_item === undefined ? anchor : get_first_child(last_item); + anchor = insert_item(item, last_sibling); } - last_block = b_blocks[b] = block; + last_item = b_items[b] = item; } if (to_animate.length > 0) { @@ -453,36 +429,17 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { // - https://svelte.dev/repl/6e891305e9644a7ca7065fa95c79d2d2?version=4.2.9 user_effect(() => { untrack(() => { - for (block of to_animate) { - block.a?.apply(); + for (item of to_animate) { + item.a?.apply(); } }); }); } } - var remaining = to_destroy.length; - if (remaining > 0) { - var clear = () => { - for (block of to_destroy) { - if (block.d) remove(block.d); - } - - state.items = b_blocks; - }; - - var check = () => { - if (--remaining === 0) { - clear(); - } - }; - - for (block of to_destroy) { - pause_effect(block.e, check); - } - } else { - state.items = b_blocks; - } + pause_effects(to_destroy, () => { + state.items = b_items; + }); } /** @@ -524,7 +481,7 @@ function mark_lis(a) { var hi; // Skip -1 values at the start of the input array `a`. - for (; a[i] === NEW_BLOCK; ++i) { + for (; a[i] === NEW_ITEM; ++i) { /**/ } @@ -533,7 +490,7 @@ function mark_lis(a) { for (; i < length; ++i) { k = a[i]; - if (k !== NEW_BLOCK) { + if (k !== NEW_ITEM) { // Ignore -1 values. j = index[index_length]; @@ -567,27 +524,27 @@ function mark_lis(a) { j = index[index_length]; while (index_length-- >= 0) { - a[j] = LIS_BLOCK; + a[j] = LIS_ITEM; j = parent[j]; } } /** - * @param {import('#client').EachItem} block - * @param {Text | Element | Comment} sibling + * @param {import('#client').EachItem} item + * @param {Text | Element | Comment} anchor * @returns {Text | Element | Comment} */ -function insert_item(block, sibling) { - var current = /** @type {import('#client').TemplateNode} */ (block.d); - return insert(current, sibling); +function insert_item(item, anchor) { + var current = /** @type {import('#client').Dom} */ (item.e.dom); + return insert(current, anchor); } /** - * @param {import('#client').EachItem} block + * @param {import('#client').EachItem} item * @returns {Text | Element | Comment} */ -function get_first_child(block) { - var current = block.d; +function get_first_child(item) { + var current = item.e.dom; if (is_array(current)) { return /** @type {Text | Element | Comment} */ (current[0]); @@ -597,48 +554,40 @@ function get_first_child(block) { } /** - * @param {import('#client').EachItem} block - * @param {any} item + * @param {import('#client').EachItem} item + * @param {any} value * @param {number} index * @param {number} type * @returns {void} */ -function update_item(block, item, index, type) { +function update_item(item, value, index, type) { if ((type & EACH_ITEM_REACTIVE) !== 0) { - set(block.v, item); + set(item.v, value); } if ((type & EACH_INDEX_REACTIVE) !== 0) { - set(/** @type {import('#client').Value} */ (block.i), index); + set(/** @type {import('#client').Value} */ (item.i), index); } else { - block.i = index; + item.i = index; } } /** * @template V - * @param {V} item + * @param {V} value * @param {unknown} key * @param {number} index * @param {(anchor: null, item: V, index: number | import('#client').Value) => void} render_fn * @param {number} flags * @returns {import('#client').EachItem} */ -function create_item(item, key, index, render_fn, flags) { +function create_item(value, key, index, render_fn, flags) { var each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; - var item_value = each_item_not_reactive - ? item - : (flags & EACH_IS_STRICT_EQUALS) !== 0 - ? source(item) - : mutable_source(item); - /** @type {import('#client').EachItem} */ - var block = { + var item = { a: null, // dom - d: null, - // effect // @ts-expect-error e: null, // index @@ -646,24 +595,30 @@ function create_item(item, key, index, render_fn, flags) { // key k: key, // item - v: item_value + v: each_item_not_reactive + ? value + : (flags & EACH_IS_STRICT_EQUALS) !== 0 + ? source(value) + : mutable_source(value) }; - var previous_each_item_block = current_each_item_block; + var previous_each_item = current_each_item; try { - current_each_item_block = block; + current_each_item = item; + + item.e = render_effect(() => { + var dom = render_fn(null, item.v, item.i); - block.e = render_effect( - () => { - render_fn(null, block.v, block.i); - }, - block, - true - ); + return () => { + if (dom !== undefined) { + remove(dom); + } + }; + }, true); - return block; + return item; } finally { - current_each_item_block = previous_each_item_block; + current_each_item = previous_each_item; } } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index f257a64833..217ccac00d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -12,7 +12,6 @@ import { render_effect, resume_effect } from '../../reactivity/effects.js'; -import { create_block } from './utils.js'; /** * @param {Comment} anchor @@ -23,16 +22,8 @@ import { create_block } from './utils.js'; * @returns {void} */ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, elseif = false) { - const block = create_block(); - hydrate_block_anchor(anchor); - /** @type {undefined | import('#client').Dom} */ - let consequent_dom; - - /** @type {undefined | import('#client').Dom} */ - let alternate_dom; - /** @type {import('#client').Effect | null} */ let consequent_effect = null; @@ -71,56 +62,24 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els if (consequent_effect) { resume_effect(consequent_effect); } else { - consequent_effect = render_effect( - () => { - consequent_dom = consequent_fn(anchor); - - return () => { - // TODO make this unnecessary by linking the dom to the effect, - // and removing automatically on teardown - if (consequent_dom !== undefined) { - remove(consequent_dom); - consequent_dom = undefined; - } - }; - }, - block, - true - ); + consequent_effect = render_effect(() => consequent_fn(anchor), true); } if (alternate_effect) { pause_effect(alternate_effect, () => { alternate_effect = null; - if (alternate_dom) remove(alternate_dom); }); } } else { if (alternate_effect) { resume_effect(alternate_effect); } else if (alternate_fn) { - alternate_effect = render_effect( - () => { - alternate_dom = alternate_fn(anchor); - - return () => { - // TODO make this unnecessary by linking the dom to the effect, - // and removing automatically on teardown - if (alternate_dom !== undefined) { - remove(alternate_dom); - alternate_dom = undefined; - } - }; - }, - block, - true - ); + alternate_effect = render_effect(() => alternate_fn(anchor), true); } if (consequent_effect) { pause_effect(consequent_effect, () => { consequent_effect = null; - if (consequent_dom) remove(consequent_dom); }); } } @@ -129,23 +88,14 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els // Set fragment so that Svelte continues to operate in hydration mode set_current_hydration_fragment([]); } - }, block); + }); if (elseif) { if_effect.f |= IS_ELSEIF; } if_effect.ondestroy = () => { - // TODO make this unnecessary by linking the dom to the effect, - // and removing automatically on teardown - if (consequent_dom !== undefined) { - remove(consequent_dom); - } - - if (alternate_dom !== undefined) { - remove(alternate_dom); - } - + // TODO why is this not automatic? this should be children of `if_effect` if (consequent_effect) { destroy_effect(consequent_effect); } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index f7cb9c6dee..2c8d8980b2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -3,18 +3,15 @@ import { hydrate_block_anchor } from '../hydration.js'; import { remove } from '../reconciler.js'; import { pause_effect, render_effect } from '../../reactivity/effects.js'; import { safe_not_equal } from '../../reactivity/equality.js'; -import { create_block } from './utils.js'; /** * @template V * @param {Comment} anchor * @param {() => V} get_key - * @param {(anchor: Node) => void} render_fn + * @param {(anchor: Node) => import('#client').Dom | void} render_fn * @returns {void} */ export function key_block(anchor, get_key, render_fn) { - const block = create_block(); - hydrate_block_anchor(anchor); /** @type {V | typeof UNINITIALIZED} */ @@ -30,47 +27,36 @@ export function key_block(anchor, get_key, render_fn) { */ let effects = new Set(); - const key_effect = render_effect( - () => { - if (safe_not_equal(key, (key = get_key()))) { - if (effect) { - var e = effect; - pause_effect(e, () => { - effects.delete(e); - }); - } - - effect = render_effect( - () => { - render_fn(anchor); - - const dom = block.d; - - return () => { - if (dom !== null) { - remove(dom); - } - }; - }, - block, - true, - true - ); - - // @ts-expect-error TODO tidy up - effect.d = block.d; - - effects.add(effect); + const key_effect = render_effect(() => { + if (safe_not_equal(key, (key = get_key()))) { + if (effect) { + var e = effect; + pause_effect(e, () => { + effects.delete(e); + }); } - }, - block, - false - ); + + effect = render_effect( + () => { + const dom = render_fn(anchor); + + return () => { + if (dom !== undefined) { + remove(dom); + } + }; + }, + true, + true + ); + + effects.add(effect); + } + }); key_effect.ondestroy = () => { for (const e of effects) { - // @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary - if (e.d) remove(e.d); + if (e.dom) remove(e.dom); } }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index ac45cb6981..62ad05c573 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,28 +1,31 @@ import { render_effect } from '../../reactivity/effects.js'; import { remove } from '../reconciler.js'; import { untrack } from '../../runtime.js'; -import { create_block } from './utils.js'; /** - * @param {() => Function | null | undefined} get_snippet - * @param {Node} node + * @template {(node: import('#client').TemplateNode, ...args: any[]) => import('#client').Dom} SnippetFn + * @param {() => SnippetFn | null | undefined} get_snippet + * @param {import('#client').TemplateNode} node * @param {(() => any)[]} args * @returns {void} */ export function snippet(get_snippet, node, ...args) { - const block = create_block(); + /** @type {SnippetFn | null | undefined} */ + var snippet_fn; render_effect(() => { - // Only rerender when the snippet function itself changes, - // not when an eagerly-read prop inside the snippet function changes - const snippet = get_snippet(); - if (snippet) { - untrack(() => snippet(node, ...args)); + if (snippet_fn === (snippet_fn = get_snippet())) return; + + if (snippet_fn) { + // Untrack so we only rerender when the snippet function itself changes, + // not when an eagerly-read prop inside the snippet function changes + var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args)); } + return () => { - if (block.d !== null) { - remove(block.d); + if (dom !== undefined) { + remove(dom); } }; - }, block); + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index 86fc548f04..7ef6f3d66f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,7 +1,7 @@ import { hydrate_block_anchor } from '../hydration.js'; import { pause_effect, render_effect } from '../../reactivity/effects.js'; import { remove } from '../reconciler.js'; -import { create_block } from './utils.js'; +import { current_effect } from '../../runtime.js'; // TODO this is very similar to `key`, can we deduplicate? @@ -10,12 +10,10 @@ import { create_block } from './utils.js'; * @template {(props: P) => void} C * @param {Comment} anchor * @param {() => C} get_component - * @param {(component: C) => void} render_fn + * @param {(component: C) => import('#client').Dom | void} render_fn * @returns {void} */ export function component(anchor, get_component, render_fn) { - const block = create_block(); - hydrate_block_anchor(anchor); /** @type {C} */ @@ -31,49 +29,36 @@ export function component(anchor, get_component, render_fn) { */ let effects = new Set(); - const component_effect = render_effect( - () => { - if (component === (component = get_component())) return; - - if (effect) { - var e = effect; - pause_effect(e, () => { - effects.delete(e); - }); - } + const component_effect = render_effect(() => { + if (component === (component = get_component())) return; - if (component) { - effect = render_effect( - () => { - render_fn(component); + if (effect) { + var e = effect; + pause_effect(e, () => { + effects.delete(e); + }); + } - const dom = block.d; + if (component) { + effect = render_effect(() => { + render_fn(component); - return () => { - if (dom !== null) { - remove(dom); - } - }; - }, - block, - true, - true - ); + // `render_fn` doesn't return anything, and we can't reference `effect` + // yet, so we reference it indirectly as `current_effect` + const dom = /** @type {import('#client').Effect} */ (current_effect).dom; - // @ts-expect-error TODO tidy up - effect.d = block.d; + return () => { + if (dom !== null) remove(dom); + }; + }, true); - effects.add(effect); - } - }, - block, - false - ); + effects.add(effect); + } + }); component_effect.ondestroy = () => { for (const e of effects) { - // @ts-expect-error TODO tidy up. ondestroy should be totally unnecessary - if (e.d) remove(e.d); + if (e.dom) remove(e.dom); } }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index bfaa47e844..bd099a1985 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -10,18 +10,18 @@ import { import { remove } from '../reconciler.js'; import { is_array } from '../../utils.js'; import { set_should_intro } from '../../render.js'; -import { current_each_item_block, set_current_each_item_block } from './each.js'; -import { create_block } from './utils.js'; -import { current_block } from '../../runtime.js'; +import { current_each_item, set_current_each_item } from './each.js'; +import { current_effect } from '../../runtime.js'; /** - * @param {import('#client').Block} block + * @param {import('#client').Effect} effect * @param {Element} from * @param {Element} to * @returns {void} */ -function swap_block_dom(block, from, to) { - const dom = block.d; +function swap_block_dom(effect, from, to) { + const dom = effect.dom; + if (is_array(dom)) { for (let i = 0; i < dom.length; i++) { if (dom[i] === from) { @@ -30,7 +30,7 @@ function swap_block_dom(block, from, to) { } } } else if (dom === from) { - block.d = to; + effect.dom = to; } } @@ -42,8 +42,7 @@ function swap_block_dom(block, from, to) { * @returns {void} */ export function element(anchor, get_tag, is_svg, render_fn) { - const parent_block = /** @type {import('#client').Block} */ (current_block); - const block = create_block(); + const parent_effect = /** @type {import('#client').Effect} */ (current_effect); hydrate_block_anchor(anchor); @@ -64,15 +63,15 @@ export function element(anchor, get_tag, is_svg, render_fn) { * We track this so we can set it when changing the element, allowing any * `animate:` directive to bind itself to the correct block */ - let each_item_block = current_each_item_block; + let each_item_block = current_each_item; const wrapper = render_effect(() => { const next_tag = get_tag() || null; if (next_tag === tag) return; // See explanation of `each_item_block` above - var previous_each_item_block = current_each_item_block; - set_current_each_item_block(each_item_block); + var previous_each_item = current_each_item; + set_current_each_item(each_item_block); // We try our best infering the namespace in case it's not possible to determine statically, // but on the first render on the client (without hydration) the parent will be undefined, @@ -103,51 +102,46 @@ export function element(anchor, get_tag, is_svg, render_fn) { } if (next_tag && next_tag !== current_tag) { - effect = render_effect( - () => { - const prev_element = element; - element = hydrating - ? /** @type {Element} */ (current_hydration_fragment[0]) - : ns - ? document.createElementNS(ns, next_tag) - : document.createElement(next_tag); - - if (render_fn) { - let anchor; - if (hydrating) { - // Use the existing ssr comment as the anchor so that the inner open and close - // methods can pick up the existing nodes correctly - anchor = /** @type {Comment} */ (element.firstChild); - } else { - anchor = empty(); - element.appendChild(anchor); - } - render_fn(element, anchor); + effect = render_effect(() => { + const prev_element = element; + element = hydrating + ? /** @type {Element} */ (current_hydration_fragment[0]) + : ns + ? document.createElementNS(ns, next_tag) + : document.createElement(next_tag); + + if (render_fn) { + let anchor; + if (hydrating) { + // Use the existing ssr comment as the anchor so that the inner open and close + // methods can pick up the existing nodes correctly + anchor = /** @type {Comment} */ (element.firstChild); + } else { + anchor = empty(); + element.appendChild(anchor); } + render_fn(element, anchor); + } - anchor.before(element); + anchor.before(element); - if (prev_element) { - swap_block_dom(parent_block, prev_element, element); - prev_element.remove(); - } - }, - block, - true - ); + if (prev_element) { + swap_block_dom(parent_effect, prev_element, element); + prev_element.remove(); + } + }, true); } tag = next_tag; if (tag) current_tag = tag; set_should_intro(true); - set_current_each_item_block(previous_each_item_block); - }, block); + set_current_each_item(previous_each_item); + }); wrapper.ondestroy = () => { if (element !== null) { remove(element); - block.d = null; element = null; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index bf70cd3e98..3c6bfbcf68 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -7,15 +7,12 @@ import { import { empty } from '../operations.js'; import { render_effect } from '../../reactivity/effects.js'; import { remove } from '../reconciler.js'; -import { create_block } from './utils.js'; /** - * @param {(anchor: Node | null) => void} render_fn + * @param {(anchor: Node | null) => import('#client').Dom | void} render_fn * @returns {void} */ export function head(render_fn) { - const block = create_block(); - // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. let hydration_fragment = null; @@ -29,28 +26,27 @@ export function head(render_fn) { } try { - const head_effect = render_effect( - () => { - const current = block.d; - if (current !== null) { - remove(current); - block.d = null; - } - let anchor = null; - if (!hydrating) { - anchor = empty(); - document.head.appendChild(anchor); - } - render_fn(anchor); - }, - block, - false - ); + /** @type {import('#client').Dom | null} */ + var dom = null; + + const head_effect = render_effect(() => { + if (dom !== null) { + remove(dom); + head_effect.dom = dom = null; + } + + let anchor = null; + if (!hydrating) { + anchor = empty(); + document.head.appendChild(anchor); + } + + dom = render_fn(anchor) ?? null; + }); head_effect.ondestroy = () => { - const current = block.d; - if (current !== null) { - remove(current); + if (dom !== null) { + remove(dom); } }; } finally { diff --git a/packages/svelte/src/internal/client/dom/blocks/utils.js b/packages/svelte/src/internal/client/dom/blocks/utils.js deleted file mode 100644 index f1214ab68c..0000000000 --- a/packages/svelte/src/internal/client/dom/blocks/utils.js +++ /dev/null @@ -1,7 +0,0 @@ -/** @returns {import('#client').Block} */ -export function create_block() { - return { - // dom - d: null - }; -} diff --git a/packages/svelte/src/internal/client/dom/elements/misc.js b/packages/svelte/src/internal/client/dom/elements/misc.js index e0fc8d2a19..0a894ba353 100644 --- a/packages/svelte/src/internal/client/dom/elements/misc.js +++ b/packages/svelte/src/internal/client/dom/elements/misc.js @@ -1,6 +1,5 @@ import { hydrating } from '../hydration.js'; import { render_effect } from '../../reactivity/effects.js'; -import { current_block } from '../../runtime.js'; /** * @param {HTMLElement} dom @@ -17,7 +16,6 @@ export function autofocus(dom, value) { dom.focus(); } }, - current_block, true, false ); diff --git a/packages/svelte/src/internal/client/dom/elements/transitions.js b/packages/svelte/src/internal/client/dom/elements/transitions.js index 8461122a69..210e526d30 100644 --- a/packages/svelte/src/internal/client/dom/elements/transitions.js +++ b/packages/svelte/src/internal/client/dom/elements/transitions.js @@ -5,7 +5,7 @@ import { raf } from '../../timing.js'; import { loop } from '../../loop.js'; import { should_intro } from '../../render.js'; import { is_function } from '../../utils.js'; -import { current_each_item_block } from '../blocks/each.js'; +import { current_each_item } from '../blocks/each.js'; import { TRANSITION_GLOBAL, TRANSITION_IN, TRANSITION_OUT } from '../../../../constants.js'; import { EFFECT_RAN } from '../../constants.js'; @@ -77,7 +77,7 @@ const linear = (t) => t; * @param {(() => P) | null} get_params */ export function animation(element, get_fn, get_params) { - var block = /** @type {import('#client').EachItem} */ (current_each_item_block); + var item = /** @type {import('#client').EachItem} */ (current_each_item); /** @type {DOMRect} */ var from; @@ -88,7 +88,7 @@ export function animation(element, get_fn, get_params) { /** @type {import('#client').Animation | undefined} */ var animation; - block.a ??= { + item.a ??= { element, measure() { from = this.element.getBoundingClientRect(); @@ -118,7 +118,7 @@ export function animation(element, get_fn, get_params) { // when an animation manager already exists, if the tag changes. in that case, we need to // swap out the element rather than creating a new manager, in case it happened at the same // moment as a reconciliation - block.a.element = element; + item.a.element = element; } /** diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 8fcd271ecd..76fb609b77 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -5,7 +5,7 @@ import { create_fragment_with_script_from_html, insert } from './reconciler.js'; -import { current_block } from '../runtime.js'; +import { current_effect } from '../runtime.js'; import { is_array } from '../utils.js'; /** @@ -192,7 +192,7 @@ function close_template(dom, is_fragment, anchor) { insert(current, anchor); } - /** @type {import('#client').Block} */ (current_block).d = current; + /** @type {import('#client').Effect} */ (current_effect).dom = current; return current; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index ebe1a422cb..d0a09a201b 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -1,7 +1,6 @@ import { DEV } from 'esm-env'; import { check_dirtiness, - current_block, current_component_context, current_effect, current_reaction, @@ -25,20 +24,20 @@ import { } from '../constants.js'; import { set } from './sources.js'; import { noop } from '../../common.js'; +import { remove } from '../dom/reconciler.js'; /** * @param {import('./types.js').EffectType} type * @param {(() => void | (() => void))} fn * @param {boolean} sync - * @param {null | import('#client').Block} block * @param {boolean} init * @returns {import('#client').Effect} */ -function create_effect(type, fn, sync, block = current_block, init = true) { +function create_effect(type, fn, sync, init = true) { /** @type {import('#client').Effect} */ const signal = { parent: current_effect, - block, + dom: null, deps: null, f: type | DIRTY, l: 0, @@ -99,7 +98,7 @@ export function user_effect(fn) { current_component_context !== null && !current_component_context.m; - const effect = create_effect(EFFECT, fn, false, current_block, !defer); + const effect = create_effect(EFFECT, fn, false, !defer); if (defer) { const context = /** @type {import('#client').ComponentContext} */ (current_component_context); @@ -115,7 +114,7 @@ export function user_effect(fn) { * @returns {() => void} */ export function user_root_effect(fn) { - const effect = render_effect(fn, current_block, true); + const effect = render_effect(fn, true); return () => { destroy_effect(effect); }; @@ -216,17 +215,15 @@ export function invalidate_effect(fn) { /** * @param {(() => void)} fn - * @param {any} block - * @param {any} managed - * @param {any} sync + * @param {boolean} managed + * @param {boolean} sync * @returns {import('#client').Effect} */ -export function render_effect(fn, block = current_block, managed = false, sync = true) { +export function render_effect(fn, managed = false, sync = true) { let flags = RENDER_EFFECT; - if (managed) { - flags |= MANAGED; - } - return create_effect(flags, /** @type {any} */ (fn), sync, block); + if (managed) flags |= MANAGED; + + return create_effect(flags, /** @type {any} */ (fn), sync); } /** @@ -245,6 +242,11 @@ export function destroy_effect(effect) { } effect.teardown?.(); + + if (effect.dom !== null) { + remove(effect.dom); + } + effect.ondestroy?.(); // @ts-expect-error @@ -253,7 +255,7 @@ export function destroy_effect(effect) { effect.teardown = effect.ondestroy = effect.ctx = - effect.block = + effect.dom = effect.deps = null; } @@ -269,26 +271,51 @@ export function destroy_effect(effect) { */ export function pause_effect(effect, callback = noop) { /** @type {import('#client').TransitionManager[]} */ - const transitions = []; + var transitions = []; pause_children(effect, transitions, true); - let remaining = transitions.length; + out(transitions, () => { + destroy_effect(effect); + callback(); + }); +} - if (remaining > 0) { - const check = () => { - if (!--remaining) { - destroy_effect(effect); - callback(); - } - }; +/** + * Pause multiple effects simultaneously, and coordinate their + * subsequent destruction. Used in each blocks + * @param {import('#client').Effect[]} effects + * @param {() => void} callback + */ +export function pause_effects(effects, callback = noop) { + /** @type {import('#client').TransitionManager[]} */ + var transitions = []; + + for (var effect of effects) { + pause_children(effect, transitions, true); + } + + out(transitions, () => { + for (var effect of effects) { + destroy_effect(effect); + } + callback(); + }); +} - for (const transition of transitions) { +/** + * @param {import('#client').TransitionManager[]} transitions + * @param {() => void} fn + */ +function out(transitions, fn) { + var remaining = transitions.length; + if (remaining > 0) { + var check = () => --remaining || fn(); + for (var transition of transitions) { transition.out(check); } } else { - destroy_effect(effect); - callback(); + fn(); } } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index c0aceff874..a28b0fefbc 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,4 +1,4 @@ -import type { Block, ComponentContext, Equals, TransitionManager } from '#client'; +import type { Block, ComponentContext, Dom, Equals, TransitionManager } from '#client'; import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants'; export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT; @@ -37,8 +37,7 @@ export interface Derived extends Value, Reaction { export interface Effect extends Reaction { parent: Effect | null; - /** The block associated with this effect */ - block: null | Block; + dom: Dom | null; /** The associated component context */ ctx: null | ComponentContext; /** Stuff to do when the effect is destroyed */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 7b7f939154..7c3151e773 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -211,40 +211,30 @@ function _mount(Component, options) { should_intro = options.intro ?? false; - /** @type {import('#client').Block} */ - const block = { - // dom - d: null - }; - /** @type {Exports} */ // @ts-expect-error will be defined because the render effect runs synchronously let component = undefined; - const effect = render_effect( - () => { - if (options.context) { - push({}); - /** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c = - options.context; - } - if (!options.props) { - options.props = /** @type {Props} */ ({}); - } - if (options.events) { - // We can't spread the object or else we'd lose the state proxy stuff, if it is one - /** @type {any} */ (options.props).$$events = options.events; - } - component = - // @ts-expect-error the public typings are not what the actual function looks like - Component(options.anchor, options.props) || {}; - if (options.context) { - pop(); - } - }, - block, - true - ); + const effect = render_effect(() => { + if (options.context) { + push({}); + /** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c = + options.context; + } + if (!options.props) { + options.props = /** @type {Props} */ ({}); + } + if (options.events) { + // We can't spread the object or else we'd lose the state proxy stuff, if it is one + /** @type {any} */ (options.props).$$events = options.events; + } + component = + // @ts-expect-error the public typings are not what the actual function looks like + Component(options.anchor, options.props) || {}; + if (options.context) { + pop(); + } + }, true); const bound_event_listener = handle_event_propagation.bind(null, container); const bound_document_event_listener = handle_event_propagation.bind(null, document); @@ -291,7 +281,7 @@ function _mount(Component, options) { container.removeEventListener(event_name, bound_event_listener); } root_event_handles.delete(event_handle); - const dom = block.d; + const dom = effect.dom; if (dom !== null) { remove(dom); } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 4d89430650..2febac4861 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -107,11 +107,7 @@ export let inspect_fn = null; /** @type {Array} */ let inspect_captured_signals = []; -// Handle rendering tree blocks and anchors -/** @type {null | import('./types.js').Block} */ -export let current_block = null; // Handling runtime component context - /** @type {import('./types.js').ComponentContext | null} */ export let current_component_context = null; @@ -387,13 +383,11 @@ export function execute_effect(signal) { const previous_effect = current_effect; const previous_component_context = current_component_context; - const previous_block = current_block; const component_context = signal.ctx; current_effect = signal; current_component_context = component_context; - current_block = signal.block; try { destroy_children(signal); @@ -403,7 +397,6 @@ export function execute_effect(signal) { } finally { current_effect = previous_effect; current_component_context = previous_component_context; - current_block = previous_block; } if ((signal.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0) { @@ -514,7 +507,6 @@ export function schedule_effect(signal, sync) { if (!should_append) { const target_level = signal.l; - const target_block = signal.block; const is_pre_effect = (flags & PRE_EFFECT) !== 0; let target_signal; let target_signal_level; @@ -530,7 +522,7 @@ export function schedule_effect(signal, sync) { is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0; if ( target_signal_level < target_level || - target_signal.block !== target_block || + target_signal !== signal || (is_target_pre_effect && !is_pre_effect) ) { i++; diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index d2426ee5d3..0a89b0045a 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -51,10 +51,7 @@ export type TemplateNode = Text | Element | Comment; export type Dom = TemplateNode | TemplateNode[]; -export interface Block { - /** dom */ - d: null | Dom; -} +export interface Block {} export type EachState = { /** flags */ @@ -66,8 +63,6 @@ export type EachState = { export type EachItem = { /** animation manager */ a: AnimationManager | null; - /** dom */ - d: null | Dom; /** effect */ e: Effect; /** item */ diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 248046073c..91c0e2eb5f 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -26,7 +26,6 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { () => { execute = fn(runes); }, - null, true, true ); From d6f10c54211ad74909c2951eb8bd76a3ee5ff40a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 22 Mar 2024 20:48:59 +0000 Subject: [PATCH 010/102] add more legacy tests (#10881) * add more legacy tests * add more tests * ts --- .../Component.svelte | 27 ++++++ .../action-update-before-destroy/_config.js | 29 ++++++ .../action-update-before-destroy/main.svelte | 10 ++ .../after-render-prevents-loop/_config.js | 25 +++++ .../after-render-prevents-loop/main.svelte | 13 +++ .../after-render-triggers-update/_config.js | 25 +++++ .../after-render-triggers-update/main.svelte | 15 +++ .../samples/animation-css/_config.js | 61 +++++++++++++ .../samples/animation-css/main.svelte | 17 ++++ .../samples/animation-js-delay/_config.js | 91 +++++++++++++++++++ .../samples/animation-js-delay/main.svelte | 21 +++++ .../samples/animation-js-easing/_config.js | 61 +++++++++++++ .../samples/animation-js-easing/main.svelte | 25 +++++ .../samples/animation-js/_config.js | 82 +++++++++++++++++ .../samples/animation-js/main.svelte | 20 ++++ .../_config.js | 24 +++++ .../main.svelte | 4 + .../_config.js | 19 ++++ .../main.svelte | 4 + .../_config.js | 11 +++ .../main.svelte | 33 +++++++ .../samples/before-render-chain/Item.svelte | 12 +++ .../samples/before-render-chain/List.svelte | 13 +++ .../samples/before-render-chain/_config.js | 28 ++++++ .../samples/before-render-chain/main.svelte | 7 ++ .../before-render-prevents-loop/_config.js | 25 +++++ .../before-render-prevents-loop/main.svelte | 13 +++ .../binding-select-from-let-2/Parent.svelte | 6 ++ .../binding-select-from-let-2/_config.js | 28 ++++++ .../binding-select-from-let-2/main.svelte | 19 ++++ .../binding-select-from-let/Parent.svelte | 6 ++ .../binding-select-from-let/_config.js | 24 +++++ .../binding-select-from-let/main.svelte | 12 +++ 33 files changed, 810 insertions(+) create mode 100644 packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/Component.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-css/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js-delay/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js-delay/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js-easing/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/animation-js/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/attribute-custom-element-inheritance/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-chain/Item.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-chain/List.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-chain/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-chain/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/before-render-prevents-loop/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let-2/Parent.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let-2/main.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let/Parent.svelte create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-from-let/main.svelte diff --git a/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/Component.svelte b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/Component.svelte new file mode 100644 index 0000000000..e38a0fff64 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/Component.svelte @@ -0,0 +1,27 @@ + + + +{#if selected} +
{item.id}
+{/if} diff --git a/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/_config.js b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/_config.js new file mode 100644 index 0000000000..328d67cb2c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/_config.js @@ -0,0 +1,29 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + skip: true, // TODO: needs fixing + + html: ` + +
1
+ `, + async test({ assert, target, window }) { + 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); + }); + + console.log = log; + assert.deepEqual(messages, ['afterUpdate', 'onDestroy']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/main.svelte b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/main.svelte new file mode 100644 index 0000000000..7f99ce0f38 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/action-update-before-destroy/main.svelte @@ -0,0 +1,10 @@ + + +{#each Object.values($items) as item (item.id)} + +{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js new file mode 100644 index 0000000000..d562d943b9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/_config.js @@ -0,0 +1,25 @@ +import { test } from '../../test'; + +export default test({ + skip_if_ssr: true, + + get props() { + return { value: 'hello!' }; + }, + + html: ` +

hello!

+

hello!

+ `, + + test({ assert, component, target }) { + component.value = 'goodbye!'; + assert.htmlEqual( + target.innerHTML, + ` +

goodbye!

+

goodbye!

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/main.svelte b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/main.svelte new file mode 100644 index 0000000000..688a25b124 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-prevents-loop/main.svelte @@ -0,0 +1,13 @@ + + +

{value}

+

{mirror}

diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js new file mode 100644 index 0000000000..d562d943b9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/_config.js @@ -0,0 +1,25 @@ +import { test } from '../../test'; + +export default test({ + skip_if_ssr: true, + + get props() { + return { value: 'hello!' }; + }, + + html: ` +

hello!

+

hello!

+ `, + + test({ assert, component, target }) { + component.value = 'goodbye!'; + assert.htmlEqual( + target.innerHTML, + ` +

goodbye!

+

goodbye!

+ ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/main.svelte b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/main.svelte new file mode 100644 index 0000000000..3f4c15cd96 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/after-render-triggers-update/main.svelte @@ -0,0 +1,15 @@ + + +

{value}

+

diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js new file mode 100644 index 0000000000..848da78b2d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-css/_config.js @@ -0,0 +1,61 @@ +// @ts-nocheck +import { test } from '../../test'; + +export default test({ + skip: true, // TODO: needs fixing + + get props() { + return { + things: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ] + }; + }, + + html: ` +
a
+
b
+
c
+
d
+
e
+ `, + + test({ assert, component, target, raf }) { + let divs = target.querySelectorAll('div'); + divs.forEach((div) => { + div.getBoundingClientRect = function () { + const index = [...this.parentNode.children].indexOf(this); + const top = index * 30; + + return { + left: 0, + right: 100, + top, + bottom: top + 20 + }; + }; + }); + + component.things = [ + { id: 5, name: 'e' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 1, name: 'a' } + ]; + + 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')); + + raf.tick(100); + assert.deepEqual([divs[0].style.animation, divs[4].style.animation], ['', '']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-css/main.svelte b/packages/svelte/tests/runtime-legacy/samples/animation-css/main.svelte new file mode 100644 index 0000000000..bd3647d73c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-css/main.svelte @@ -0,0 +1,17 @@ + + +{#each things as thing (thing.id)} +
{thing.name}
+{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/_config.js new file mode 100644 index 0000000000..a72aed883b --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/_config.js @@ -0,0 +1,91 @@ +// @ts-nocheck +import { ok, test } from '../../test'; + +export default test({ + skip: true, // TODO: needs fixing + + get props() { + return { + things: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ] + }; + }, + + html: ` +
a
+
b
+
c
+
d
+
e
+ `, + + test({ assert, component, window, raf }) { + let divs = window.document.querySelectorAll('div'); + divs.forEach((div) => { + div.getBoundingClientRect = function () { + const index = [...this.parentNode.children].indexOf(this); + const top = index * 30; + + return { + left: 0, + right: 100, + top, + bottom: top + 20 + }; + }; + }); + + component.things = [ + { id: 5, name: 'e' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 1, name: 'a' } + ]; + + divs = window.document.querySelectorAll('div'); + assert.equal(divs[0].dy, 120); + assert.equal(divs[4].dy, -120); + + raf.tick(50); + assert.equal(divs[0].dy, 108); + assert.equal(divs[4].dy, -60); + + raf.tick(100); + assert.equal(divs[0].dy, 48); + assert.equal(divs[4].dy, 0); + + raf.tick(150); + assert.equal(divs[0].dy, 0); + assert.equal(divs[4].dy, 0); + + component.things = [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ]; + + divs = document.querySelectorAll('div'); + assert.equal(divs[0].dy, 120); + assert.equal(divs[4].dy, -120); + + raf.tick(200); + assert.equal(divs[0].dy, 108); + assert.equal(divs[4].dy, -60); + + raf.tick(250); + assert.equal(divs[0].dy, 48); + assert.equal(divs[4].dy, 0); + + raf.tick(300); + assert.equal(divs[0].dy, 0); + assert.equal(divs[4].dy, 0); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/main.svelte b/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/main.svelte new file mode 100644 index 0000000000..c29394fa67 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-delay/main.svelte @@ -0,0 +1,21 @@ + + +{#each things as thing, i (thing.id)} +
{thing.name}
+{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js new file mode 100644 index 0000000000..5b7ed1c732 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/_config.js @@ -0,0 +1,61 @@ +// @ts-nocheck +import { ok, test } from '../../test'; + +export default test({ + get props() { + return { + things: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ] + }; + }, + + html: ` +
a
+
b
+
c
+
d
+
e
+ `, + + test({ assert, component, raf }) { + let divs = document.querySelectorAll('div'); + divs.forEach((div) => { + div.getBoundingClientRect = function () { + const index = [...this.parentNode.children].indexOf(this); + const top = index * 30; + + return { + left: 0, + right: 100, + top, + bottom: top + 20 + }; + }; + }); + + component.things = [ + { id: 5, name: 'e' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 1, name: 'a' } + ]; + + divs = document.querySelectorAll('div'); + assert.equal(divs[0].dy, 120); + assert.equal(divs[4].dy, -120); + + raf.tick(50); + assert.equal(divs[0].dy, 60); + assert.equal(divs[4].dy, -60); + + raf.tick(100); + assert.equal(divs[0].dy, 0); + assert.equal(divs[4].dy, 0); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/main.svelte b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/main.svelte new file mode 100644 index 0000000000..b376452e15 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js-easing/main.svelte @@ -0,0 +1,25 @@ + + +{#each things as thing (thing.id)} +
{thing.name}
+{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js new file mode 100644 index 0000000000..3606f7d17b --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js/_config.js @@ -0,0 +1,82 @@ +// @ts-nocheck +import { test } from '../../test'; + +export default test({ + get props() { + return { + things: [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ] + }; + }, + + html: ` +
a
+
b
+
c
+
d
+
e
+ `, + + test({ assert, component, raf }) { + let divs = document.querySelectorAll('div'); + divs.forEach((div) => { + div.getBoundingClientRect = function () { + const index = [...this.parentNode.children].indexOf(this); + const top = index * 30; + + return { + left: 0, + right: 100, + top, + bottom: top + 20 + }; + }; + }); + + component.things = [ + { id: 5, name: 'e' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 1, name: 'a' } + ]; + + divs = document.querySelectorAll('div'); + assert.equal(divs[0].dy, 120); + assert.equal(divs[4].dy, -120); + + raf.tick(50); + assert.equal(divs[0].dy, 60); + assert.equal(divs[4].dy, -60); + + raf.tick(100); + assert.equal(divs[0].dy, 0); + assert.equal(divs[4].dy, 0); + + component.things = [ + { id: 1, name: 'a' }, + { id: 2, name: 'b' }, + { id: 3, name: 'c' }, + { id: 4, name: 'd' }, + { id: 5, name: 'e' } + ]; + + divs = document.querySelectorAll('div'); + + assert.equal(divs[0].dy, 120); + assert.equal(divs[4].dy, -120); + + raf.tick(150); + assert.equal(divs[0].dy, 60); + assert.equal(divs[4].dy, -60); + + raf.tick(200); + assert.equal(divs[0].dy, 0); + assert.equal(divs[4].dy, 0); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/animation-js/main.svelte b/packages/svelte/tests/runtime-legacy/samples/animation-js/main.svelte new file mode 100644 index 0000000000..4d060dd140 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/animation-js/main.svelte @@ -0,0 +1,20 @@ + + +{#each things as thing (thing.id)} +
{thing.name}
+{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/_config.js b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/_config.js new file mode 100644 index 0000000000..5672a7d75a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/_config.js @@ -0,0 +1,24 @@ +import { test } from '../../test'; + +export default test({ + skip: true, // TODO: needs fixing + + html: ` + + + + + `, + skip_if_hydrate: true, + + compileOptions: { + namespace: 'foreign' + }, + test({ assert, target }) { + // @ts-ignore + const attr = (/** @type {string} */ sel) => target.querySelector(sel).attributes[0].name; + assert.equal(attr('page'), 'horizontalAlignment'); + assert.equal(attr('button'), 'textWrap'); + assert.equal(attr('text'), 'wordWrap'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/main.svelte b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/main.svelte new file mode 100644 index 0000000000..52b3e646e1 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/attribute-casing-foreign-namespace-compiler-option/main.svelte @@ -0,0 +1,4 @@ + + + `, + + async test({ assert, component, window, target }) { + const button = target.querySelector('button'); + // @ts-ignore + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(2) = 2

+ + ` + ); + assert.equal(component.n, 2); + // @ts-ignore + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(1) = 3

+ + ` + ); + assert.equal(component.n, 3); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-evals-props-once/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-evals-props-once/main.svelte new file mode 100644 index 0000000000..3a5805b3e5 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-evals-props-once/main.svelte @@ -0,0 +1,12 @@ + + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp1.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp1.svelte new file mode 100644 index 0000000000..e87e7ec699 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp1.svelte @@ -0,0 +1,9 @@ + + +

value(1) = {value}

+

foo={foo}

+

typeof cb={typeof cb}

diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp2.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp2.svelte new file mode 100644 index 0000000000..06c4c7acfe --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/Comp2.svelte @@ -0,0 +1,9 @@ + + +

value(2) = {value}

+

foo={foo}

+

typeof cb={typeof cb}

diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/_config.js b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/_config.js new file mode 100644 index 0000000000..8e224b3eb3 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/_config.js @@ -0,0 +1,36 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

value(1) = 1

+

foo=bar

+

typeof cb=function

+ + `, + + async test({ assert, window, target }) { + const button = target.querySelector('button'); + // @ts-ignore + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(2) = 2

+

foo=bar

+

typeof cb=function

+ + ` + ); + // @ts-ignore + await button.dispatchEvent(new window.Event('click')); + assert.htmlEqual( + target.innerHTML, + ` +

value(1) = 1

+

foo=bar

+

typeof cb=function

+ + ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/main.svelte b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/main.svelte new file mode 100644 index 0000000000..082ebbcda2 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/dynamic-component-spread-props/main.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/Empty.svelte b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/Empty.svelte new file mode 100644 index 0000000000..616839ee0e --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/Empty.svelte @@ -0,0 +1,8 @@ + + diff --git a/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/_config.js b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/_config.js new file mode 100644 index 0000000000..a47e7976eb --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/_config.js @@ -0,0 +1,38 @@ +import { test } from '../../test'; + +/** + * @type {{ (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; (...data: any[]): void; (message?: any, ...optionalParams: any[]): void; }} + */ +let log; + +export default test({ + html: ` + + `, + + before_test() { + log = console.log; + }, + after_test() { + console.log = log; + }, + + async test({ assert, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + /** + * @type {any[]} + */ + const messages = []; + console.log = (msg) => messages.push(msg); + // @ts-ignore + await button.dispatchEvent(event); + assert.htmlEqual( + target.innerHTML, + ` + + ` + ); + assert.deepEqual(messages, ['destroy']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/main.svelte b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/main.svelte new file mode 100644 index 0000000000..0ee5ef4a04 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/empty-component-destroy/main.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/_config.js new file mode 100644 index 0000000000..fdd881ddd9 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/_config.js @@ -0,0 +1,33 @@ +import { test } from '../../test'; + +export default test({ + get props() { + return { items: ['foo', 'bar', 'baz'] }; + }, + + html: ` + + + + `, + + test({ assert, component, target, window }) { + const buttons = target.querySelectorAll('button'); + const event = new window.MouseEvent('click'); + + /** + * @type {any[]} + */ + const clicked = []; + + component.$on('clicked', (/** @type {{ detail: { node: any; }; }} */ event) => { + clicked.push(event.detail.node); + }); + + buttons[1].dispatchEvent(event); + + assert.equal(clicked.length, 1); + assert.equal(clicked[0].nodeName, 'BUTTON'); + assert.equal(clicked[0].textContent, 'bar'); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/main.svelte new file mode 100644 index 0000000000..56ae6b2de2 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-each-this/main.svelte @@ -0,0 +1,11 @@ + + +{#each items as item} + +{/each} diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/Widget.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/Widget.svelte new file mode 100644 index 0000000000..45f55baae1 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/Widget.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/_config.js new file mode 100644 index 0000000000..0201f3da3d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/_config.js @@ -0,0 +1,21 @@ +import { ok, test } from '../../test'; + +export default test({ + html: ` + + `, + + test({ assert, component, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + + let answer; + component.$on('foo', (/** @type {{ detail: { answer: any; }; }} */ event) => { + answer = event.detail.answer; + }); + + // @ts-ignore + button.dispatchEvent(event); + assert.equal(answer, 42); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/main.svelte new file mode 100644 index 0000000000..d70ec7290e --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-component/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/Widget.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/Widget.svelte new file mode 100644 index 0000000000..45f55baae1 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/Widget.svelte @@ -0,0 +1,7 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/_config.js new file mode 100644 index 0000000000..0201f3da3d --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/_config.js @@ -0,0 +1,21 @@ +import { ok, test } from '../../test'; + +export default test({ + html: ` + + `, + + test({ assert, component, target, window }) { + const button = target.querySelector('button'); + const event = new window.MouseEvent('click'); + + let answer; + component.$on('foo', (/** @type {{ detail: { answer: any; }; }} */ event) => { + answer = event.detail.answer; + }); + + // @ts-ignore + button.dispatchEvent(event); + assert.equal(answer, 42); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/main.svelte new file mode 100644 index 0000000000..f6c5494ce4 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-dynamic-component/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/_config.js new file mode 100644 index 0000000000..47dac62117 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/_config.js @@ -0,0 +1,21 @@ +import { ok, test } from '../../test'; + +export default test({ + html: ` + + `, + + test({ assert, component, target, window }) { + const button = target.querySelector('button'); + const event = new window.Event('click-now'); + + let clicked; + component.$on('click-now', () => { + clicked = true; + }); + + // @ts-ignore + button.dispatchEvent(event); + assert.ok(clicked); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/main.svelte new file mode 100644 index 0000000000..8241f05659 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-shorthand-sanitized/main.svelte @@ -0,0 +1 @@ + From 7f10642add4612563c5283384004c8baa5824161 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 12:10:12 -0400 Subject: [PATCH 012/102] chore: improve hydration tests (#10887) * use server-rendered HTML as hydration test starting point * update tests * remove _before.html files * remove _before_head.html files * override output with _expected.html * expected output for binding-input case * remove unused files * fix * changeset --- .changeset/serious-gorillas-eat.md | 5 ++ .../src/internal/client/dom/blocks/each.js | 8 +-- .../hydration/samples/basic/_before.html | 1 - .../samples/binding-input/_before.html | 2 - .../samples/binding-input/_expected.html | 1 + .../samples/claim-comment/_before.html | 3 - .../samples/claim-static/_before.html | 8 --- .../hydration/samples/claim-text/_before.html | 5 -- .../samples/component-in-element/_before.html | 3 - .../hydration/samples/component/_before.html | 3 - .../samples/dynamic-text-changed/_before.html | 1 - .../samples/dynamic-text-changed/_config.js | 4 ++ .../{_after.html => _expected.html} | 0 .../samples/dynamic-text-nil/_before.html | 1 - .../samples/dynamic-text/_before.html | 1 - .../samples/each-block-arg-clash/_before.html | 5 -- .../each-block-fallback-mismatch/_before.html | 2 - .../each-block-fallback-mismatch/_config.js | 12 +++- .../{_after.html => _expected.html} | 0 .../each-block-fallback-mismatch/main.svelte | 3 +- .../_before.html | 12 ---- .../_config.js | 8 +++ .../{_after.html => _expected.html} | 0 .../main.svelte | 2 +- .../_before.html | 9 --- .../_config.js | 8 +++ .../{_after.html => _expected.html} | 0 .../main.svelte | 2 +- .../hydration/samples/each-block/_before.html | 5 -- .../hydration/samples/each-else/_before.html | 3 - .../element-attribute-added/_before.html | 1 - .../element-attribute-added/_config.js | 2 + .../{_after.html => _expected.html} | 0 .../element-attribute-changed/_before.html | 1 - .../element-attribute-changed/_config.js | 4 ++ .../{_after.html => _expected.html} | 0 .../element-attribute-removed/_before.html | 1 - .../element-attribute-removed/_config.js | 4 ++ .../{_after.html => _expected.html} | 0 .../_before.html | 1 - .../element-attribute-unchanged/_before.html | 1 - .../element-nested-sibling/_before.html | 3 - .../samples/element-nested/_before.html | 3 - .../samples/element-ref/_before.html | 1 - .../samples/event-handler/_before.html | 5 -- .../samples/expression-sibling/_before.html | 1 - .../head-html-and-component/_before.html | 1 - .../head-html-and-component/_before_head.html | 6 -- .../head-meta-hydrate-duplicate/_before.html | 1 - .../_before_head.html | 4 -- .../samples/html-tag-hydration/_after.html | 1 - .../samples/html-tag-hydration/_before.html | 1 - .../samples/if-block-anchor/_before.html | 2 - .../samples/if-block-empty/_after.html | 4 -- .../samples/if-block-empty/_before.html | 4 -- .../samples/if-block-empty/_config.js | 10 +++- .../samples/if-block-false/_before.html | 5 -- .../samples/if-block-mismatch/_after.html | 1 - .../samples/if-block-mismatch/_before.html | 1 - .../samples/if-block-mismatch/_config.js | 8 +++ .../samples/if-block-mismatch/main.svelte | 4 ++ .../samples/if-block-update/_before.html | 1 - .../hydration/samples/if-block/_before.html | 4 -- .../ignore-mismatched-href/_before.html | 1 - .../samples/ignore-mismatched-href/_config.js | 8 +++ .../ignore-mismatched-href/main.svelte | 2 +- .../hydration/samples/noscript/_before.html | 2 - .../noscript/{_after.html => _expected.html} | 0 .../samples/raw-mismatch/_after.html | 1 - .../samples/raw-mismatch/_before.html | 1 - .../hydration/samples/raw-repair/_after.html | 2 - .../hydration/samples/raw-repair/_before.html | 6 -- .../hydration/samples/raw-svg/_before.html | 1 - .../raw-with-empty-line-at-top/_before.html | 10 ---- .../tests/hydration/samples/raw/_before.html | 4 -- .../_after.html | 1 - .../_before.html | 1 - .../hydration/samples/text-empty/_before.html | 1 - .../hydration/samples/text-empty/_config.js | 10 +++- .../{_after.html => _expected.html} | 0 .../hydration/samples/text-empty/main.svelte | 2 +- .../samples/text-fallback/_before.html | 1 - .../samples/top-level-text/_before.html | 1 - packages/svelte/tests/hydration/test.ts | 55 ++++++++----------- 84 files changed, 116 insertions(+), 197 deletions(-) create mode 100644 .changeset/serious-gorillas-eat.md delete mode 100644 packages/svelte/tests/hydration/samples/basic/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/binding-input/_before.html create mode 100644 packages/svelte/tests/hydration/samples/binding-input/_expected.html delete mode 100644 packages/svelte/tests/hydration/samples/claim-comment/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/claim-static/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/claim-text/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/component-in-element/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/component/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/dynamic-text-changed/_before.html rename packages/svelte/tests/hydration/samples/dynamic-text-changed/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/dynamic-text-nil/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/dynamic-text/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/each-block-arg-clash/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_before.html rename packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_before.html rename packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_before.html rename packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/each-block/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/each-else/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/element-attribute-added/_before.html rename packages/svelte/tests/hydration/samples/element-attribute-added/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/element-attribute-changed/_before.html rename packages/svelte/tests/hydration/samples/element-attribute-changed/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/element-attribute-removed/_before.html rename packages/svelte/tests/hydration/samples/element-attribute-removed/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/element-attribute-unchanged-2/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/element-attribute-unchanged/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/element-nested-sibling/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/element-nested/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/element-ref/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/event-handler/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/expression-sibling/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/head-html-and-component/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/head-html-and-component/_before_head.html delete mode 100644 packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before_head.html delete mode 100644 packages/svelte/tests/hydration/samples/html-tag-hydration/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/html-tag-hydration/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-anchor/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-empty/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-false/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-mismatch/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block-update/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/if-block/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/ignore-mismatched-href/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/noscript/_before.html rename packages/svelte/tests/hydration/samples/noscript/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/raw-mismatch/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/raw-mismatch/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/raw-repair/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/raw-repair/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/raw-svg/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/raw-with-empty-line-at-top/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/raw/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_after.html delete mode 100644 packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/text-empty/_before.html rename packages/svelte/tests/hydration/samples/text-empty/{_after.html => _expected.html} (100%) delete mode 100644 packages/svelte/tests/hydration/samples/text-fallback/_before.html delete mode 100644 packages/svelte/tests/hydration/samples/top-level-text/_before.html diff --git a/.changeset/serious-gorillas-eat.md b/.changeset/serious-gorillas-eat.md new file mode 100644 index 0000000000..407f2e6db8 --- /dev/null +++ b/.changeset/serious-gorillas-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correctly hydrate controlled each-else block diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index ce17cced94..bf4d0b5a17 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -50,7 +50,7 @@ export function set_current_each_item(item) { * @param {number} flags * @param {null | ((item: V) => string)} get_key * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn + * @param {null | ((anchor: Node | null) => void)} fallback_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @returns {void} */ @@ -158,7 +158,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re resume_effect(fallback); } else { fallback = render_effect(() => { - var dom = fallback_fn(anchor); + var dom = fallback_fn(hydrating ? null : anchor); return () => { if (dom !== undefined) { @@ -199,7 +199,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re * @param {number} flags * @param {null | ((item: V) => string)} get_key * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn + * @param {null | ((anchor: Node | null) => void)} fallback_fn * @returns {void} */ export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fallback_fn) { @@ -212,7 +212,7 @@ export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fa * @param {() => V[]} get_collection * @param {number} flags * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn - * @param {null | ((anchor: Node) => void)} fallback_fn + * @param {null | ((anchor: Node | null) => void)} fallback_fn * @returns {void} */ export function each_indexed(anchor, get_collection, flags, render_fn, fallback_fn) { diff --git a/packages/svelte/tests/hydration/samples/basic/_before.html b/packages/svelte/tests/hydration/samples/basic/_before.html deleted file mode 100644 index f97ad9726b..0000000000 --- a/packages/svelte/tests/hydration/samples/basic/_before.html +++ /dev/null @@ -1 +0,0 @@ -

Hello world!

diff --git a/packages/svelte/tests/hydration/samples/binding-input/_before.html b/packages/svelte/tests/hydration/samples/binding-input/_before.html deleted file mode 100644 index c6b1da4217..0000000000 --- a/packages/svelte/tests/hydration/samples/binding-input/_before.html +++ /dev/null @@ -1,2 +0,0 @@ - -

Hello world!

diff --git a/packages/svelte/tests/hydration/samples/binding-input/_expected.html b/packages/svelte/tests/hydration/samples/binding-input/_expected.html new file mode 100644 index 0000000000..b5bc6af161 --- /dev/null +++ b/packages/svelte/tests/hydration/samples/binding-input/_expected.html @@ -0,0 +1 @@ +

Hello world!

diff --git a/packages/svelte/tests/hydration/samples/claim-comment/_before.html b/packages/svelte/tests/hydration/samples/claim-comment/_before.html deleted file mode 100644 index e1bab99a66..0000000000 --- a/packages/svelte/tests/hydration/samples/claim-comment/_before.html +++ /dev/null @@ -1,3 +0,0 @@ -
-p -
diff --git a/packages/svelte/tests/hydration/samples/claim-static/_before.html b/packages/svelte/tests/hydration/samples/claim-static/_before.html deleted file mode 100644 index bfbe85f59c..0000000000 --- a/packages/svelte/tests/hydration/samples/claim-static/_before.html +++ /dev/null @@ -1,8 +0,0 @@ -
hello
- -
bye
- -
-
aaa
-
bbb
-
diff --git a/packages/svelte/tests/hydration/samples/claim-text/_before.html b/packages/svelte/tests/hydration/samples/claim-text/_before.html deleted file mode 100644 index 98702233ff..0000000000 --- a/packages/svelte/tests/hydration/samples/claim-text/_before.html +++ /dev/null @@ -1,5 +0,0 @@ - -

This p and the slot below are direct children of the root.

-
There should be one
- - diff --git a/packages/svelte/tests/hydration/samples/component-in-element/_before.html b/packages/svelte/tests/hydration/samples/component-in-element/_before.html deleted file mode 100644 index c95522231e..0000000000 --- a/packages/svelte/tests/hydration/samples/component-in-element/_before.html +++ /dev/null @@ -1,3 +0,0 @@ -
-

nested

-
diff --git a/packages/svelte/tests/hydration/samples/component/_before.html b/packages/svelte/tests/hydration/samples/component/_before.html deleted file mode 100644 index bc73093efd..0000000000 --- a/packages/svelte/tests/hydration/samples/component/_before.html +++ /dev/null @@ -1,3 +0,0 @@ - -

nested

- diff --git a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_before.html b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_before.html deleted file mode 100644 index f97ad9726b..0000000000 --- a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_before.html +++ /dev/null @@ -1 +0,0 @@ -

Hello world!

diff --git a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_config.js b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_config.js index 4c00df87e5..9852c25463 100644 --- a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_config.js +++ b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_config.js @@ -1,6 +1,10 @@ import { test } from '../../test'; export default test({ + server_props: { + name: 'world' + }, + props: { name: 'everybody' }, diff --git a/packages/svelte/tests/hydration/samples/dynamic-text-changed/_after.html b/packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/dynamic-text-changed/_after.html rename to packages/svelte/tests/hydration/samples/dynamic-text-changed/_expected.html diff --git a/packages/svelte/tests/hydration/samples/dynamic-text-nil/_before.html b/packages/svelte/tests/hydration/samples/dynamic-text-nil/_before.html deleted file mode 100644 index 7508e31db6..0000000000 --- a/packages/svelte/tests/hydration/samples/dynamic-text-nil/_before.html +++ /dev/null @@ -1 +0,0 @@ -

diff --git a/packages/svelte/tests/hydration/samples/dynamic-text/_before.html b/packages/svelte/tests/hydration/samples/dynamic-text/_before.html deleted file mode 100644 index f97ad9726b..0000000000 --- a/packages/svelte/tests/hydration/samples/dynamic-text/_before.html +++ /dev/null @@ -1 +0,0 @@ -

Hello world!

diff --git a/packages/svelte/tests/hydration/samples/each-block-arg-clash/_before.html b/packages/svelte/tests/hydration/samples/each-block-arg-clash/_before.html deleted file mode 100644 index 184a66ac72..0000000000 --- a/packages/svelte/tests/hydration/samples/each-block-arg-clash/_before.html +++ /dev/null @@ -1,5 +0,0 @@ -
    -
  • animal
  • -
  • vegetable
  • -
  • mineral
  • -
diff --git a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_before.html b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_before.html deleted file mode 100644 index 215e1e3c8e..0000000000 --- a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_before.html +++ /dev/null @@ -1,2 +0,0 @@ -

empty

-

a

diff --git a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_config.js b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_config.js index f47bee71df..0b7783bd1c 100644 --- a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_config.js +++ b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_config.js @@ -1,3 +1,13 @@ import { test } from '../../test'; -export default test({}); +export default test({ + server_props: { + items1: [], + items2: [{ name: 'a' }] + }, + + props: { + items1: [{ name: 'a' }], + items2: [] + } +}); diff --git a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_after.html b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_after.html rename to packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/_expected.html diff --git a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/main.svelte index 835f726435..c130034bbe 100644 --- a/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/main.svelte +++ b/packages/svelte/tests/hydration/samples/each-block-fallback-mismatch/main.svelte @@ -1,6 +1,5 @@ {#each items1 as item} diff --git a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_before.html b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_before.html deleted file mode 100644 index f480ea096d..0000000000 --- a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_before.html +++ /dev/null @@ -1,12 +0,0 @@ -
  • a
  • b
-
  • a
  • b
-
  • a
  • b
-
  • a
  • -
  • a
  • b
  • -
  • b
  • -
  • a
  • -
  • a
  • b
  • -
  • b
  • -
  • a
  • -
  • a
  • b
  • -
  • b
  • diff --git a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_config.js b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_config.js index 5b0adcfd86..fa28dbe009 100644 --- a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_config.js +++ b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_config.js @@ -1,6 +1,14 @@ import { assert_ok, test } from '../../test'; export default test({ + server_props: { + items: [{ name: 'a' }, { name: 'b' }] + }, + + props: { + items: [{ name: 'a' }] + }, + snapshot(target) { const ul = target.querySelector('ul'); assert_ok(ul); diff --git a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_after.html b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_after.html rename to packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/_expected.html diff --git a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/main.svelte b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/main.svelte index e2ff0750e3..093e59cef9 100644 --- a/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/main.svelte +++ b/packages/svelte/tests/hydration/samples/each-block-less-nodes-on-client/main.svelte @@ -1,5 +1,5 @@
      diff --git a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_before.html b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_before.html deleted file mode 100644 index 7c3233df17..0000000000 --- a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_before.html +++ /dev/null @@ -1,9 +0,0 @@ -
      • a
      -
      • a
      -
      • a
      -
    • a
    • -
    • a
    • -
    • a
    • -
    • a
    • -
    • a
    • -
    • a
    diff --git a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_config.js b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_config.js index 5b0adcfd86..baffef3b8b 100644 --- a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_config.js +++ b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_config.js @@ -1,6 +1,14 @@ import { assert_ok, test } from '../../test'; export default test({ + server_props: { + items: [{ name: 'x' }] + }, + + props: { + items: [{ name: 'a' }, { name: 'b' }] + }, + snapshot(target) { const ul = target.querySelector('ul'); assert_ok(ul); diff --git a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_after.html b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_after.html rename to packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/_expected.html diff --git a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/main.svelte b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/main.svelte index 7df2b52e2e..093e59cef9 100644 --- a/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/main.svelte +++ b/packages/svelte/tests/hydration/samples/each-block-more-nodes-on-client/main.svelte @@ -1,5 +1,5 @@
      diff --git a/packages/svelte/tests/hydration/samples/each-block/_before.html b/packages/svelte/tests/hydration/samples/each-block/_before.html deleted file mode 100644 index 184a66ac72..0000000000 --- a/packages/svelte/tests/hydration/samples/each-block/_before.html +++ /dev/null @@ -1,5 +0,0 @@ -
        -
      • animal
      • -
      • vegetable
      • -
      • mineral
      • -
      diff --git a/packages/svelte/tests/hydration/samples/each-else/_before.html b/packages/svelte/tests/hydration/samples/each-else/_before.html deleted file mode 100644 index 3335979fb8..0000000000 --- a/packages/svelte/tests/hydration/samples/each-else/_before.html +++ /dev/null @@ -1,3 +0,0 @@ -

      Hello, world

      -

      foo

      -

      foo

      diff --git a/packages/svelte/tests/hydration/samples/element-attribute-added/_before.html b/packages/svelte/tests/hydration/samples/element-attribute-added/_before.html deleted file mode 100644 index fec75ee427..0000000000 --- a/packages/svelte/tests/hydration/samples/element-attribute-added/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      diff --git a/packages/svelte/tests/hydration/samples/element-attribute-added/_config.js b/packages/svelte/tests/hydration/samples/element-attribute-added/_config.js index 3972adadfe..e432ceb468 100644 --- a/packages/svelte/tests/hydration/samples/element-attribute-added/_config.js +++ b/packages/svelte/tests/hydration/samples/element-attribute-added/_config.js @@ -1,6 +1,8 @@ import { test } from '../../test'; export default test({ + server_props: {}, + props: { className: 'bar' }, diff --git a/packages/svelte/tests/hydration/samples/element-attribute-added/_after.html b/packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/element-attribute-added/_after.html rename to packages/svelte/tests/hydration/samples/element-attribute-added/_expected.html diff --git a/packages/svelte/tests/hydration/samples/element-attribute-changed/_before.html b/packages/svelte/tests/hydration/samples/element-attribute-changed/_before.html deleted file mode 100644 index fec75ee427..0000000000 --- a/packages/svelte/tests/hydration/samples/element-attribute-changed/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      diff --git a/packages/svelte/tests/hydration/samples/element-attribute-changed/_config.js b/packages/svelte/tests/hydration/samples/element-attribute-changed/_config.js index 3972adadfe..fe02d429b3 100644 --- a/packages/svelte/tests/hydration/samples/element-attribute-changed/_config.js +++ b/packages/svelte/tests/hydration/samples/element-attribute-changed/_config.js @@ -1,6 +1,10 @@ import { test } from '../../test'; export default test({ + server_props: { + className: 'foo' + }, + props: { className: 'bar' }, diff --git a/packages/svelte/tests/hydration/samples/element-attribute-changed/_after.html b/packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/element-attribute-changed/_after.html rename to packages/svelte/tests/hydration/samples/element-attribute-changed/_expected.html diff --git a/packages/svelte/tests/hydration/samples/element-attribute-removed/_before.html b/packages/svelte/tests/hydration/samples/element-attribute-removed/_before.html deleted file mode 100644 index 4bb2d47ff3..0000000000 --- a/packages/svelte/tests/hydration/samples/element-attribute-removed/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      diff --git a/packages/svelte/tests/hydration/samples/element-attribute-removed/_config.js b/packages/svelte/tests/hydration/samples/element-attribute-removed/_config.js index a6b6b845f7..62c72f3f7c 100644 --- a/packages/svelte/tests/hydration/samples/element-attribute-removed/_config.js +++ b/packages/svelte/tests/hydration/samples/element-attribute-removed/_config.js @@ -1,6 +1,10 @@ import { test } from '../../test'; export default test({ + server_props: { + id: 'foo' + }, + snapshot(target) { const div = target.querySelector('div'); diff --git a/packages/svelte/tests/hydration/samples/element-attribute-removed/_after.html b/packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/element-attribute-removed/_after.html rename to packages/svelte/tests/hydration/samples/element-attribute-removed/_expected.html diff --git a/packages/svelte/tests/hydration/samples/element-attribute-unchanged-2/_before.html b/packages/svelte/tests/hydration/samples/element-attribute-unchanged-2/_before.html deleted file mode 100644 index 433df7f9d1..0000000000 --- a/packages/svelte/tests/hydration/samples/element-attribute-unchanged-2/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      diff --git a/packages/svelte/tests/hydration/samples/element-attribute-unchanged/_before.html b/packages/svelte/tests/hydration/samples/element-attribute-unchanged/_before.html deleted file mode 100644 index fec75ee427..0000000000 --- a/packages/svelte/tests/hydration/samples/element-attribute-unchanged/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      diff --git a/packages/svelte/tests/hydration/samples/element-nested-sibling/_before.html b/packages/svelte/tests/hydration/samples/element-nested-sibling/_before.html deleted file mode 100644 index 78323f1806..0000000000 --- a/packages/svelte/tests/hydration/samples/element-nested-sibling/_before.html +++ /dev/null @@ -1,3 +0,0 @@ -

      1 - - 2

      diff --git a/packages/svelte/tests/hydration/samples/element-nested/_before.html b/packages/svelte/tests/hydration/samples/element-nested/_before.html deleted file mode 100644 index a3bf64142d..0000000000 --- a/packages/svelte/tests/hydration/samples/element-nested/_before.html +++ /dev/null @@ -1,3 +0,0 @@ -
      -

      nested

      -
      diff --git a/packages/svelte/tests/hydration/samples/element-ref/_before.html b/packages/svelte/tests/hydration/samples/element-ref/_before.html deleted file mode 100644 index f97ad9726b..0000000000 --- a/packages/svelte/tests/hydration/samples/element-ref/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      Hello world!

      diff --git a/packages/svelte/tests/hydration/samples/event-handler/_before.html b/packages/svelte/tests/hydration/samples/event-handler/_before.html deleted file mode 100644 index 833699217c..0000000000 --- a/packages/svelte/tests/hydration/samples/event-handler/_before.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/svelte/tests/hydration/samples/expression-sibling/_before.html b/packages/svelte/tests/hydration/samples/expression-sibling/_before.html deleted file mode 100644 index 0ce5257133..0000000000 --- a/packages/svelte/tests/hydration/samples/expression-sibling/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      1 2 3

      diff --git a/packages/svelte/tests/hydration/samples/head-html-and-component/_before.html b/packages/svelte/tests/hydration/samples/head-html-and-component/_before.html deleted file mode 100644 index bb7c602edf..0000000000 --- a/packages/svelte/tests/hydration/samples/head-html-and-component/_before.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/svelte/tests/hydration/samples/head-html-and-component/_before_head.html b/packages/svelte/tests/hydration/samples/head-html-and-component/_before_head.html deleted file mode 100644 index cc1d406e3d..0000000000 --- a/packages/svelte/tests/hydration/samples/head-html-and-component/_before_head.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before.html b/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before.html deleted file mode 100644 index 7217379ee6..0000000000 --- a/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      Just a dummy page.
      diff --git a/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before_head.html b/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before_head.html deleted file mode 100644 index 040469d618..0000000000 --- a/packages/svelte/tests/hydration/samples/head-meta-hydrate-duplicate/_before_head.html +++ /dev/null @@ -1,4 +0,0 @@ -Some Title - - - \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration/_after.html b/packages/svelte/tests/hydration/samples/html-tag-hydration/_after.html deleted file mode 100644 index b85905ec0b..0000000000 --- a/packages/svelte/tests/hydration/samples/html-tag-hydration/_after.html +++ /dev/null @@ -1 +0,0 @@ -1 2 3 diff --git a/packages/svelte/tests/hydration/samples/html-tag-hydration/_before.html b/packages/svelte/tests/hydration/samples/html-tag-hydration/_before.html deleted file mode 100644 index 2d1b0bd6c3..0000000000 --- a/packages/svelte/tests/hydration/samples/html-tag-hydration/_before.html +++ /dev/null @@ -1 +0,0 @@ - 1 2 3 diff --git a/packages/svelte/tests/hydration/samples/if-block-anchor/_before.html b/packages/svelte/tests/hydration/samples/if-block-anchor/_before.html deleted file mode 100644 index 163050a1a6..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-anchor/_before.html +++ /dev/null @@ -1,2 +0,0 @@ -

      foo!

      -

      bar!

      diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_after.html b/packages/svelte/tests/hydration/samples/if-block-empty/_after.html deleted file mode 100644 index 9c71a6ffee..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-empty/_after.html +++ /dev/null @@ -1,4 +0,0 @@ - - -x - diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_before.html b/packages/svelte/tests/hydration/samples/if-block-empty/_before.html deleted file mode 100644 index 88c34c8f29..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-empty/_before.html +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/packages/svelte/tests/hydration/samples/if-block-empty/_config.js b/packages/svelte/tests/hydration/samples/if-block-empty/_config.js index f47bee71df..78f00a1b51 100644 --- a/packages/svelte/tests/hydration/samples/if-block-empty/_config.js +++ b/packages/svelte/tests/hydration/samples/if-block-empty/_config.js @@ -1,3 +1,11 @@ import { test } from '../../test'; -export default test({}); +export default test({ + server_props: { + foo: '' + }, + + props: { + foo: 'x' + } +}); diff --git a/packages/svelte/tests/hydration/samples/if-block-false/_before.html b/packages/svelte/tests/hydration/samples/if-block-false/_before.html deleted file mode 100644 index 7911a5ef45..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-false/_before.html +++ /dev/null @@ -1,5 +0,0 @@ -

      before

      - - - -

      after

      diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/_after.html b/packages/svelte/tests/hydration/samples/if-block-mismatch/_after.html deleted file mode 100644 index b98a7cb541..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/_after.html +++ /dev/null @@ -1 +0,0 @@ -

      foo

      diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/_before.html b/packages/svelte/tests/hydration/samples/if-block-mismatch/_before.html deleted file mode 100644 index fe8876acb7..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      bar

      diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/_config.js b/packages/svelte/tests/hydration/samples/if-block-mismatch/_config.js index 9bf1e9b276..f93877415f 100644 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/_config.js +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/_config.js @@ -3,5 +3,13 @@ import { test } from '../../test'; // even {#if true} or {#if false} should be kept as an if block, because it could be {#if browser} originally, // which is then different between client and server. export default test({ + server_props: { + condition: false + }, + + props: { + condition: true + }, + trim_whitespace: false }); diff --git a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte index 4059553992..c6799c5f95 100644 --- a/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte +++ b/packages/svelte/tests/hydration/samples/if-block-mismatch/main.svelte @@ -1,3 +1,7 @@ + + {#if true}

      foo

      {:else} diff --git a/packages/svelte/tests/hydration/samples/if-block-update/_before.html b/packages/svelte/tests/hydration/samples/if-block-update/_before.html deleted file mode 100644 index 5821b859a7..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block-update/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      foo!

      diff --git a/packages/svelte/tests/hydration/samples/if-block/_before.html b/packages/svelte/tests/hydration/samples/if-block/_before.html deleted file mode 100644 index ebd6a90aad..0000000000 --- a/packages/svelte/tests/hydration/samples/if-block/_before.html +++ /dev/null @@ -1,4 +0,0 @@ - - -

      foo!

      - diff --git a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_before.html b/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_before.html deleted file mode 100644 index afeffd5eb6..0000000000 --- a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_before.html +++ /dev/null @@ -1 +0,0 @@ -foo diff --git a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_config.js b/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_config.js index cc24163f2c..ce81251e5e 100644 --- a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_config.js +++ b/packages/svelte/tests/hydration/samples/ignore-mismatched-href/_config.js @@ -1,6 +1,14 @@ import { test } from '../../test'; export default test({ + server_props: { + browser: false + }, + + props: { + browser: true + }, + test(assert, target) { assert.equal(target.querySelector('a')?.getAttribute('href'), '/bar'); } diff --git a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/main.svelte b/packages/svelte/tests/hydration/samples/ignore-mismatched-href/main.svelte index 5dcd4d4002..de78109c00 100644 --- a/packages/svelte/tests/hydration/samples/ignore-mismatched-href/main.svelte +++ b/packages/svelte/tests/hydration/samples/ignore-mismatched-href/main.svelte @@ -1,5 +1,5 @@ foo diff --git a/packages/svelte/tests/hydration/samples/noscript/_before.html b/packages/svelte/tests/hydration/samples/noscript/_before.html deleted file mode 100644 index 34ac9d9bc3..0000000000 --- a/packages/svelte/tests/hydration/samples/noscript/_before.html +++ /dev/null @@ -1,2 +0,0 @@ - -

      Hello!

      Count: 0

      diff --git a/packages/svelte/tests/hydration/samples/noscript/_after.html b/packages/svelte/tests/hydration/samples/noscript/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/noscript/_after.html rename to packages/svelte/tests/hydration/samples/noscript/_expected.html diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch/_after.html b/packages/svelte/tests/hydration/samples/raw-mismatch/_after.html deleted file mode 100644 index d28ae350fb..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-mismatch/_after.html +++ /dev/null @@ -1 +0,0 @@ -

      foo

      \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/raw-mismatch/_before.html b/packages/svelte/tests/hydration/samples/raw-mismatch/_before.html deleted file mode 100644 index 4c04a81f66..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-mismatch/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      bar

      \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/raw-repair/_after.html b/packages/svelte/tests/hydration/samples/raw-repair/_after.html deleted file mode 100644 index 31cdc12015..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-repair/_after.html +++ /dev/null @@ -1,2 +0,0 @@ -

      invalid

      -

      invalid

      \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/raw-repair/_before.html b/packages/svelte/tests/hydration/samples/raw-repair/_before.html deleted file mode 100644 index 3e2942c4be..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-repair/_before.html +++ /dev/null @@ -1,6 +0,0 @@ -

      -

      invalid

      -

      -

      invalid

      - -

      \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/raw-svg/_before.html b/packages/svelte/tests/hydration/samples/raw-svg/_before.html deleted file mode 100644 index 7ec08fa13f..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-svg/_before.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/svelte/tests/hydration/samples/raw-with-empty-line-at-top/_before.html b/packages/svelte/tests/hydration/samples/raw-with-empty-line-at-top/_before.html deleted file mode 100644 index 6aea13491a..0000000000 --- a/packages/svelte/tests/hydration/samples/raw-with-empty-line-at-top/_before.html +++ /dev/null @@ -1,10 +0,0 @@ -
      before
      -
      - - -a -b -c - - -
      after
      diff --git a/packages/svelte/tests/hydration/samples/raw/_before.html b/packages/svelte/tests/hydration/samples/raw/_before.html deleted file mode 100644 index be89227a9b..0000000000 --- a/packages/svelte/tests/hydration/samples/raw/_before.html +++ /dev/null @@ -1,4 +0,0 @@ - -

      this is some html

      -

      and so is this

      - diff --git a/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_after.html b/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_after.html deleted file mode 100644 index d20b3ed496..0000000000 --- a/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_after.html +++ /dev/null @@ -1 +0,0 @@ -

      Hello client!

      diff --git a/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_before.html b/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_before.html deleted file mode 100644 index f97ad9726b..0000000000 --- a/packages/svelte/tests/hydration/samples/repairs-apparent-static-content/_before.html +++ /dev/null @@ -1 +0,0 @@ -

      Hello world!

      diff --git a/packages/svelte/tests/hydration/samples/text-empty/_before.html b/packages/svelte/tests/hydration/samples/text-empty/_before.html deleted file mode 100644 index a8cad39ae7..0000000000 --- a/packages/svelte/tests/hydration/samples/text-empty/_before.html +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/packages/svelte/tests/hydration/samples/text-empty/_config.js b/packages/svelte/tests/hydration/samples/text-empty/_config.js index f47bee71df..4067b68b04 100644 --- a/packages/svelte/tests/hydration/samples/text-empty/_config.js +++ b/packages/svelte/tests/hydration/samples/text-empty/_config.js @@ -1,3 +1,11 @@ import { test } from '../../test'; -export default test({}); +export default test({ + server_props: { + x: '' + }, + + props: { + x: 'x' + } +}); diff --git a/packages/svelte/tests/hydration/samples/text-empty/_after.html b/packages/svelte/tests/hydration/samples/text-empty/_expected.html similarity index 100% rename from packages/svelte/tests/hydration/samples/text-empty/_after.html rename to packages/svelte/tests/hydration/samples/text-empty/_expected.html diff --git a/packages/svelte/tests/hydration/samples/text-empty/main.svelte b/packages/svelte/tests/hydration/samples/text-empty/main.svelte index d88ec4833c..90451138d3 100644 --- a/packages/svelte/tests/hydration/samples/text-empty/main.svelte +++ b/packages/svelte/tests/hydration/samples/text-empty/main.svelte @@ -1,5 +1,5 @@ {x} diff --git a/packages/svelte/tests/hydration/samples/text-fallback/_before.html b/packages/svelte/tests/hydration/samples/text-fallback/_before.html deleted file mode 100644 index d5d3925b3b..0000000000 --- a/packages/svelte/tests/hydration/samples/text-fallback/_before.html +++ /dev/null @@ -1 +0,0 @@ -
      foo override
      default
      diff --git a/packages/svelte/tests/hydration/samples/top-level-text/_before.html b/packages/svelte/tests/hydration/samples/top-level-text/_before.html deleted file mode 100644 index 8f4d4886d4..0000000000 --- a/packages/svelte/tests/hydration/samples/top-level-text/_before.html +++ /dev/null @@ -1 +0,0 @@ -Text diff --git a/packages/svelte/tests/hydration/test.ts b/packages/svelte/tests/hydration/test.ts index c9db5d7e6f..460d589695 100644 --- a/packages/svelte/tests/hydration/test.ts +++ b/packages/svelte/tests/hydration/test.ts @@ -4,14 +4,14 @@ import * as fs from 'node:fs'; import { assert } from 'vitest'; import { compile_directory, should_update_expected } from '../helpers.js'; import { assert_html_equal } from '../html_equal.js'; -import { suite, assert_ok } from '../suite.js'; +import { suite, assert_ok, type BaseTest } from '../suite.js'; import { createClassComponent } from 'svelte/legacy'; +import { render } from 'svelte/server'; import type { CompileOptions } from '#compiler'; -interface HydrationTest { - solo?: boolean; - skip?: boolean; +interface HydrationTest extends BaseTest { load_compiled?: boolean; + server_props?: Record; props?: Record; compileOptions?: Partial; /** @@ -53,14 +53,16 @@ const { test, run } = suite(async (config, cwd) => { const target = window.document.body; const head = window.document.head; - target.innerHTML = read_html(`${cwd}/_before.html`); + const rendered = render((await import(`${cwd}/_output/server/main.svelte.js`)).default, { + props: config.server_props ?? config.props ?? {} + }); - let before_head; - try { - before_head = read_html(`${cwd}/_before_head.html`); - head.innerHTML = before_head; - } catch (err) { - // continue regardless of error + fs.writeFileSync(`${cwd}/_output/body.html`, rendered.html + '\n'); + target.innerHTML = rendered.html; + + if (rendered.head) { + fs.writeFileSync(`${cwd}/_output/head.html`, rendered.head + '\n'); + head.innerHTML = rendered.head; } config.before_test?.(); @@ -95,29 +97,16 @@ const { test, run } = suite(async (config, cwd) => { assert.ok(!got_hydration_error, 'Unexpected hydration error'); } - try { - assert_html_equal(target.innerHTML, read_html(`${cwd}/_after.html`, `${cwd}/_before.html`)); - } catch (error) { - if (should_update_expected()) { - fs.writeFileSync(`${cwd}/_after.html`, target.innerHTML); - console.log(`Updated ${cwd}/_after.html.`); - } else { - throw error; - } - } + const expected = fs.existsSync(`${cwd}/_expected.html`) + ? read_html(`${cwd}/_expected.html`) + : rendered.html; + assert_html_equal(target.innerHTML, expected); - if (before_head) { - try { - const after_head = read_html(`${cwd}/_after_head.html`, `${cwd}/_before_head.html`); - assert_html_equal(head.innerHTML, after_head); - } catch (error) { - if (should_update_expected()) { - fs.writeFileSync(`${cwd}/_after_head.html`, head.innerHTML); - console.log(`Updated ${cwd}/_after_head.html.`); - } else { - throw error; - } - } + if (rendered.head) { + const expected = fs.existsSync(`${cwd}/_expected_head.html`) + ? read_html(`${cwd}/_expected_head.html`) + : rendered.head; + assert_html_equal(head.innerHTML, expected); } if (config.snapshot) { From dfd1819559eaa32d8db10c50d1b8b4d4743b586a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 13:05:12 -0400 Subject: [PATCH 013/102] chore: tidy up hydration code (#10891) * remove some indirection * tidy up * tidy * tidy up * simplify * fix * don't attempt to hydrate children of void dynamic element * simplify * tighten up * fix * add note, simplify * tidy up * changeset * revert this change, save for a separate PR --- .../internal/client/dom/blocks/css-props.js | 4 +- .../src/internal/client/dom/blocks/each.js | 36 +++--- .../src/internal/client/dom/blocks/if.js | 19 ++- .../client/dom/blocks/svelte-element.js | 25 ++-- .../internal/client/dom/blocks/svelte-head.js | 23 ++-- .../src/internal/client/dom/hydration.js | 108 ++++++++++++------ .../src/internal/client/dom/operations.js | 84 +++++--------- .../src/internal/client/dom/reconciler.js | 4 +- .../src/internal/client/dom/template.js | 8 +- packages/svelte/src/internal/client/render.js | 37 +++--- 10 files changed, 174 insertions(+), 174 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index 5bfb41dd91..a8e1ff57b2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -1,5 +1,5 @@ import { namespace_svg } from '../../../../constants.js'; -import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js'; +import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js'; import { empty } from '../operations.js'; import { render_effect } from '../../reactivity/effects.js'; import { remove } from '../reconciler.js'; @@ -22,7 +22,7 @@ export function css_props(anchor, is_html, props, component) { if (hydrating) { // Hydration: css props element is surrounded by a ssr comment ... - tag = /** @type {HTMLElement | SVGElement} */ (current_hydration_fragment[0]); + tag = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]); // ... and the child(ren) of the css props element is also surround by a ssr comment component_anchor = /** @type {Comment} */ (tag.firstChild); } else { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index bf4d0b5a17..e3a88dafc7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -7,11 +7,11 @@ import { EACH_KEYED } from '../../../../constants.js'; import { - current_hydration_fragment, - get_hydration_fragment, + hydrate_nodes, hydrate_block_anchor, hydrating, - set_current_hydration_fragment + set_hydrating, + update_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { insert, remove } from '../reconciler.js'; @@ -98,17 +98,16 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re let mismatch = false; if (hydrating) { - var is_else = - /** @type {Comment} */ (current_hydration_fragment?.[0])?.data === 'ssr:each_else'; + var is_else = /** @type {Comment} */ (hydrate_nodes?.[0])?.data === 'ssr:each_else'; if (is_else !== (length === 0)) { // hydration mismatch — remove the server-rendered DOM and start over - remove(current_hydration_fragment); - set_current_hydration_fragment(null); + remove(hydrate_nodes); + set_hydrating(false); mismatch = true; } else if (is_else) { // Remove the each_else comment node or else it will confuse the subsequent hydration algorithm - /** @type {import('#client').TemplateNode[]} */ (current_hydration_fragment).shift(); + /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes).shift(); } } @@ -117,18 +116,17 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re var b_items = []; // Hydrate block - var hydration_list = /** @type {import('#client').TemplateNode[]} */ ( - current_hydration_fragment - ); + var hydration_list = /** @type {import('#client').TemplateNode[]} */ (hydrate_nodes); var hydrating_node = hydration_list[0]; for (var i = 0; i < length; i++) { - var fragment = get_hydration_fragment(hydrating_node); - set_current_hydration_fragment(fragment); - if (!fragment) { - // If fragment is null, then that means that the server rendered less items than what - // the client code specifies -> break out and continue with client-side node creation + var nodes = update_hydrate_nodes(hydrating_node); + + if (nodes === null) { + // If `nodes` is null, then that means that the server rendered fewer items than what + // expected, so break out and continue appending non-hydrated items mismatch = true; + set_hydrating(false); break; } @@ -137,7 +135,7 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re // TODO helperise this hydrating_node = /** @type {import('#client').TemplateNode} */ ( /** @type {Node} */ ( - /** @type {Node} */ (fragment[fragment.length - 1] || hydrating_node).nextSibling + /** @type {Node} */ (nodes[nodes.length - 1] || hydrating_node).nextSibling ).nextSibling ); } @@ -175,8 +173,8 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re } if (mismatch) { - // Set a fragment so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); + // continue in hydration mode + set_hydrating(true); } }); diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 217ccac00d..84b294ccbb 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,10 +1,5 @@ import { IS_ELSEIF } from '../../constants.js'; -import { - current_hydration_fragment, - hydrate_block_anchor, - hydrating, - set_current_hydration_fragment -} from '../hydration.js'; +import { hydrate_nodes, hydrate_block_anchor, hydrating, set_hydrating } from '../hydration.js'; import { remove } from '../reconciler.js'; import { destroy_effect, @@ -40,7 +35,7 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els let mismatch = false; if (hydrating) { - const comment_text = /** @type {Comment} */ (current_hydration_fragment?.[0])?.data; + const comment_text = /** @type {Comment} */ (hydrate_nodes?.[0])?.data; if ( !comment_text || @@ -49,12 +44,12 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els ) { // Hydration mismatch: remove everything inside the anchor and start fresh. // This could happen using when `{#if browser} .. {/if}` in SvelteKit. - remove(current_hydration_fragment); - set_current_hydration_fragment(null); + remove(hydrate_nodes); + set_hydrating(false); mismatch = true; } else { // Remove the ssr:if comment node or else it will confuse the subsequent hydration algorithm - current_hydration_fragment.shift(); + hydrate_nodes.shift(); } } @@ -85,8 +80,8 @@ export function if_block(anchor, get_condition, consequent_fn, alternate_fn, els } if (mismatch) { - // Set fragment so that Svelte continues to operate in hydration mode - set_current_hydration_fragment([]); + // continue in hydration mode + set_hydrating(true); } }); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index bd099a1985..225721e0ff 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -1,5 +1,5 @@ import { namespace_svg } from '../../../../constants.js'; -import { current_hydration_fragment, hydrate_block_anchor, hydrating } from '../hydration.js'; +import { hydrate_nodes, hydrate_block_anchor, hydrating } from '../hydration.js'; import { empty } from '../operations.js'; import { destroy_effect, @@ -105,22 +105,25 @@ export function element(anchor, get_tag, is_svg, render_fn) { effect = render_effect(() => { const prev_element = element; element = hydrating - ? /** @type {Element} */ (current_hydration_fragment[0]) + ? /** @type {Element} */ (hydrate_nodes[0]) : ns ? document.createElementNS(ns, next_tag) : document.createElement(next_tag); if (render_fn) { - let anchor; - if (hydrating) { - // Use the existing ssr comment as the anchor so that the inner open and close - // methods can pick up the existing nodes correctly - anchor = /** @type {Comment} */ (element.firstChild); - } else { - anchor = empty(); - element.appendChild(anchor); + // If hydrating, use the existing ssr comment as the anchor so that the + // inner open and close methods can pick up the existing nodes correctly + var child_anchor = hydrating + ? /** @type {Comment} */ (element.firstChild) + : element.appendChild(empty()); + + if (child_anchor) { + // `child_anchor` can be undefined if this is a void element with children, + // i.e. `...`. This is + // user error, but we warn on it elsewhere (in dev) so here we just + // silently ignore it + render_fn(element, child_anchor); } - render_fn(element, anchor); } anchor.before(element); diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index 3c6bfbcf68..cd7ef3b318 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,9 +1,4 @@ -import { - current_hydration_fragment, - get_hydration_fragment, - hydrating, - set_current_hydration_fragment -} from '../hydration.js'; +import { hydrate_nodes, hydrating, set_hydrate_nodes, update_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { render_effect } from '../../reactivity/effects.js'; import { remove } from '../reconciler.js'; @@ -15,14 +10,12 @@ import { remove } from '../reconciler.js'; export function head(render_fn) { // The head function may be called after the first hydration pass and ssr comment nodes may still be present, // therefore we need to skip that when we detect that we're not in hydration mode. - let hydration_fragment = null; - let previous_hydration_fragment = null; + let previous_hydrate_nodes = null; + let was_hydrating = hydrating; - let is_hydrating = hydrating; - if (is_hydrating) { - hydration_fragment = get_hydration_fragment(document.head.firstChild); - previous_hydration_fragment = current_hydration_fragment; - set_current_hydration_fragment(hydration_fragment); + if (hydrating) { + previous_hydrate_nodes = hydrate_nodes; + update_hydrate_nodes(document.head.firstChild); } try { @@ -50,8 +43,8 @@ export function head(render_fn) { } }; } finally { - if (is_hydrating) { - set_current_hydration_fragment(previous_hydration_fragment); + if (was_hydrating) { + set_hydrate_nodes(previous_hydrate_nodes); } } } diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index c896454659..efb8543589 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -1,5 +1,3 @@ -// Handle hydration - import { schedule_task } from './task.js'; import { empty } from './operations.js'; @@ -9,66 +7,79 @@ import { empty } from './operations.js'; */ export let hydrating = false; +/** @param {boolean} value */ +export function set_hydrating(value) { + hydrating = value; +} + /** * Array of nodes to traverse for hydration. This will be null if we're not hydrating, but for * the sake of simplicity we're not going to use `null` checks everywhere and instead rely on * the `hydrating` flag to tell whether or not we're in hydration mode at which point this is set. - * @type {import('../types.js').TemplateNode[]} + * @type {import('#client').TemplateNode[]} */ -export let current_hydration_fragment = /** @type {any} */ (null); +export let hydrate_nodes = /** @type {any} */ (null); /** - * @param {null | import('../types.js').TemplateNode[]} fragment + * @param {null | import('#client').TemplateNode[]} nodes * @returns {void} */ -export function set_current_hydration_fragment(fragment) { - hydrating = fragment !== null; - current_hydration_fragment = /** @type {import('../types.js').TemplateNode[]} */ (fragment); +export function set_hydrate_nodes(nodes) { + hydrate_nodes = /** @type {import('#client').TemplateNode[]} */ (nodes); +} + +/** + * @param {Node | null} first + * @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty + */ +export function update_hydrate_nodes(first, insert_text) { + const nodes = get_hydrate_nodes(first, insert_text); + set_hydrate_nodes(nodes); + return nodes; } /** * Returns all nodes between the first `` comment tag pair encountered. * @param {Node | null} node - * @param {boolean} [insert_text] Whether to insert an empty text node if the fragment is empty - * @returns {import('../types.js').TemplateNode[] | null} + * @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty + * @returns {import('#client').TemplateNode[] | null} */ -export function get_hydration_fragment(node, insert_text = false) { - /** @type {import('../types.js').TemplateNode[]} */ - const fragment = []; +function get_hydrate_nodes(node, insert_text = false) { + /** @type {import('#client').TemplateNode[]} */ + var nodes = []; - /** @type {null | Node} */ - let current_node = node; + var current_node = /** @type {null | import('#client').TemplateNode} */ (node); /** @type {null | string} */ - let target_depth = null; + var target_depth = null; + while (current_node !== null) { - const node_type = current_node.nodeType; - const next_sibling = current_node.nextSibling; - if (node_type === 8) { - const data = /** @type {Comment} */ (current_node).data; + if (current_node.nodeType === 8) { + var data = /** @type {Comment} */ (current_node).data; + if (data.startsWith('ssr:')) { - const depth = data.slice(4); + var depth = data.slice(4); + if (target_depth === null) { target_depth = depth; } else if (depth === target_depth) { - if (insert_text && fragment.length === 0) { - const text = empty(); - fragment.push(text); - /** @type {Node} */ (current_node.parentNode).insertBefore(text, current_node); + if (insert_text && nodes.length === 0) { + var text = empty(); + nodes.push(text); + current_node.before(text); } - return fragment; + return nodes; } else { - fragment.push(/** @type {Text | Comment | Element} */ (current_node)); + nodes.push(current_node); } - current_node = next_sibling; - continue; } + } else if (target_depth !== null) { + nodes.push(current_node); } - if (target_depth !== null) { - fragment.push(/** @type {Text | Comment | Element} */ (current_node)); - } - current_node = next_sibling; + + current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling); } + return null; } @@ -81,18 +92,39 @@ export function hydrate_block_anchor(node) { if (node.nodeType === 8) { // @ts-ignore - let fragment = node.$$fragment; - if (fragment === undefined) { - fragment = get_hydration_fragment(node); + let nodes = node.$$fragment; + if (nodes === undefined) { + nodes = get_hydrate_nodes(node); } else { schedule_task(() => { // @ts-expect-error clean up memory node.$$fragment = undefined; }); } - set_current_hydration_fragment(fragment); + set_hydrate_nodes(nodes); } else { const first_child = /** @type {Element | null} */ (node.firstChild); - set_current_hydration_fragment(first_child === null ? [] : [first_child]); + set_hydrate_nodes(first_child === null ? [] : [first_child]); + } +} + +/** + * Expects to only be called in hydration mode + * @param {Node} node + * @returns {Node} + */ +export function capture_fragment_from_node(node) { + if ( + node.nodeType === 8 && + /** @type {Comment} */ (node).data.startsWith('ssr:') && + hydrate_nodes[hydrate_nodes.length - 1] !== node + ) { + const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node)); + const last_child = nodes[nodes.length - 1] || node; + const target = /** @type {Node} */ (last_child.nextSibling); + // @ts-ignore + target.$$fragment = nodes; + return target; } + return node; } diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 8b8746cb3f..0ef60f52c8 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -1,4 +1,4 @@ -import { current_hydration_fragment, get_hydration_fragment, hydrating } from './hydration.js'; +import { capture_fragment_from_node, hydrate_nodes, hydrating } from './hydration.js'; import { get_descriptor } from '../utils.js'; // We cache the Node and Element prototype methods, so that we can avoid doing @@ -123,17 +123,14 @@ export function empty() { /*#__NO_SIDE_EFFECTS__*/ export function child(node) { const child = first_child_get.call(node); - if (hydrating) { - // Child can be null if we have an element with a single child, like `

      {text}

      `, where `text` is empty - if (child === null) { - const text = empty(); - node.appendChild(text); - return text; - } else { - return capture_fragment_from_node(child); - } + if (!hydrating) return child; + + // Child can be null if we have an element with a single child, like `

      {text}

      `, where `text` is empty + if (child === null) { + return node.appendChild(empty()); } - return child; + + return capture_fragment_from_node(child); } /** @@ -144,28 +141,26 @@ export function child(node) { */ /*#__NO_SIDE_EFFECTS__*/ export function child_frag(node, is_text) { - if (hydrating) { - const first_node = /** @type {Node[]} */ (node)[0]; + if (!hydrating) { + return first_child_get.call(/** @type {Node} */ (node)); + } - // if an {expression} is empty during SSR, there might be no - // text node to hydrate — we must therefore create one - if (is_text && first_node?.nodeType !== 3) { - const text = empty(); - current_hydration_fragment.unshift(text); - if (first_node) { - /** @type {DocumentFragment} */ (first_node.parentNode).insertBefore(text, first_node); - } - return text; - } + const first_node = /** @type {import('#client').TemplateNode[]} */ (node)[0]; - if (first_node !== null) { - return capture_fragment_from_node(first_node); - } + // if an {expression} is empty during SSR, there might be no + // text node to hydrate — we must therefore create one + if (is_text && first_node?.nodeType !== 3) { + const text = empty(); + hydrate_nodes.unshift(text); + first_node?.before(text); + return text; + } - return first_node; + if (first_node !== null) { + return capture_fragment_from_node(first_node); } - return first_child_get.call(/** @type {Node} */ (node)); + return first_node; } /** @@ -177,19 +172,18 @@ export function child_frag(node, is_text) { /*#__NO_SIDE_EFFECTS__*/ export function sibling(node, is_text = false) { const next_sibling = next_sibling_get.call(node); + if (hydrating) { // if a sibling {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one if (is_text && next_sibling?.nodeType !== 3) { const text = empty(); if (next_sibling) { - const index = current_hydration_fragment.indexOf( - /** @type {Text | Comment | Element} */ (next_sibling) - ); - current_hydration_fragment.splice(index, 0, text); - /** @type {DocumentFragment} */ (next_sibling.parentNode).insertBefore(text, next_sibling); + const index = hydrate_nodes.indexOf(/** @type {Text | Comment | Element} */ (next_sibling)); + hydrate_nodes.splice(index, 0, text); + next_sibling.before(text); } else { - current_hydration_fragment.push(text); + hydrate_nodes.push(text); } return text; @@ -199,6 +193,7 @@ export function sibling(node, is_text = false) { return capture_fragment_from_node(next_sibling); } } + return next_sibling; } @@ -226,24 +221,3 @@ export function clear_text_content(node) { export function create_element(name) { return document.createElement(name); } - -/** - * Expects to only be called in hydration mode - * @param {Node} node - * @returns {Node} - */ -function capture_fragment_from_node(node) { - if ( - node.nodeType === 8 && - /** @type {Comment} */ (node).data.startsWith('ssr:') && - current_hydration_fragment[current_hydration_fragment.length - 1] !== node - ) { - const fragment = /** @type {Array} */ (get_hydration_fragment(node)); - const last_child = fragment[fragment.length - 1] || node; - const target = /** @type {Node} */ (last_child.nextSibling); - // @ts-ignore - target.$$fragment = fragment; - return target; - } - return node; -} diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 02be902598..78ae58c80c 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -1,5 +1,5 @@ import { append_child } from './operations.js'; -import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js'; +import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js'; import { is_array } from '../utils.js'; /** @param {string} html */ @@ -76,7 +76,7 @@ export function remove(current) { export function reconcile_html(target, value, svg) { hydrate_block_anchor(target); if (hydrating) { - return current_hydration_fragment; + return hydrate_nodes; } var html = value + ''; // Even if html is the empty string we need to continue to insert something or diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 76fb609b77..dc10c05dd9 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -1,4 +1,4 @@ -import { current_hydration_fragment, hydrate_block_anchor, hydrating } from './hydration.js'; +import { hydrate_nodes, hydrate_block_anchor, hydrating } from './hydration.js'; import { child, clone_node, empty } from './operations.js'; import { create_fragment_from_html, @@ -94,9 +94,9 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) } // In ssr+hydration optimization mode, we might remove the template_element, // so we need to is_fragment flag to properly handle hydrated content accordingly. - const fragment = current_hydration_fragment; - if (fragment !== null) { - return is_fragment ? fragment : /** @type {Element} */ (fragment[0]); + const nodes = hydrate_nodes; + if (nodes !== null) { + return is_fragment ? nodes : /** @type {Element} */ (nodes[0]); } } return use_clone_node diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 7c3151e773..d8948cfe08 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -5,11 +5,12 @@ import { remove } from './dom/reconciler.js'; import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { render_effect, destroy_effect } from './reactivity/effects.js'; import { - current_hydration_fragment, - get_hydration_fragment, + hydrate_nodes, hydrate_block_anchor, hydrating, - set_current_hydration_fragment + set_hydrate_nodes, + set_hydrating, + update_hydrate_nodes } from './dom/hydration.js'; import { array_from } from './utils.js'; import { handle_event_propagation } from './dom/elements/events.js'; @@ -113,7 +114,6 @@ export function createRoot() { * @returns {Exports} */ export function mount(component, options) { - init_operations(); const anchor = empty(); options.target.appendChild(anchor); // Don't flush previous effects to ensure order of outer effects stays consistent @@ -138,18 +138,20 @@ export function mount(component, options) { * @returns {Exports} */ export function hydrate(component, options) { - init_operations(); const container = options.target; const first_child = /** @type {ChildNode} */ (container.firstChild); + const previous_hydrate_nodes = hydrate_nodes; + // Call with insert_text == true to prevent empty {expressions} resulting in an empty - // fragment array, resulting in a hydration error down the line - const hydration_fragment = get_hydration_fragment(first_child, true); - const previous_hydration_fragment = current_hydration_fragment; - set_current_hydration_fragment(hydration_fragment); + // `nodes` array, resulting in a hydration error down the line + // TODO is both this and the `container.appendChild(anchor)` below necessary? + const nodes = update_hydrate_nodes(first_child, true); + set_hydrating(true); /** @type {null | Text} */ let anchor = null; - if (hydration_fragment === null) { + + if (nodes === null) { anchor = empty(); container.appendChild(anchor); } @@ -162,12 +164,12 @@ export function hydrate(component, options) { const instance = _mount(component, { ...options, anchor }); // flush_sync will run this callback and then synchronously run any pending effects, // which don't belong to the hydration phase anymore - therefore reset it here - set_current_hydration_fragment(null); + set_hydrating(false); finished_hydrating = true; return instance; }, false); } catch (error) { - if (!finished_hydrating && options.recover !== false && hydration_fragment !== null) { + if (!finished_hydrating && options.recover !== false && nodes !== null) { // eslint-disable-next-line no-console console.error( 'ERR_SVELTE_HYDRATION_MISMATCH' + @@ -176,16 +178,17 @@ export function hydrate(component, options) { : ''), error ); - remove(hydration_fragment); + remove(nodes); first_child.remove(); - hydration_fragment[hydration_fragment.length - 1]?.nextSibling?.remove(); - set_current_hydration_fragment(null); + nodes[nodes.length - 1]?.nextSibling?.remove(); + set_hydrating(false); return mount(component, options); } else { throw error; } } finally { - set_current_hydration_fragment(previous_hydration_fragment); + set_hydrating(!!previous_hydrate_nodes); + set_hydrate_nodes(previous_hydrate_nodes); } } @@ -206,6 +209,8 @@ export function hydrate(component, options) { * @returns {Exports} */ function _mount(Component, options) { + init_operations(); + const registered_events = new Set(); const container = options.target; From 1fc5f8b9c061da4c4fa26c16b82816fd89b22e47 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 13:27:40 -0400 Subject: [PATCH 014/102] chore: code-golf a bit (#10893) --- .../svelte/scripts/check-treeshakeability.js | 2 +- .../internal/client/dom/blocks/css-props.js | 23 +++++++++---------- .../internal/client/dom/blocks/svelte-head.js | 10 +++----- packages/svelte/src/internal/client/render.js | 18 ++++----------- 4 files changed, 20 insertions(+), 33 deletions(-) diff --git a/packages/svelte/scripts/check-treeshakeability.js b/packages/svelte/scripts/check-treeshakeability.js index 9ded247b2b..66129b8657 100644 --- a/packages/svelte/scripts/check-treeshakeability.js +++ b/packages/svelte/scripts/check-treeshakeability.js @@ -109,7 +109,7 @@ const bundle = await bundle_code( ).js.code ); -if (!bundle.includes('current_hydration_fragment')) { +if (!bundle.includes('hydrate_nodes')) { // eslint-disable-next-line no-console console.error(`✅ Hydration code treeshakeable`); } else { diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index a8e1ff57b2..68efe677e7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -15,27 +15,26 @@ export function css_props(anchor, is_html, props, component) { hydrate_block_anchor(anchor); /** @type {HTMLElement | SVGElement} */ - let tag; + let element; /** @type {Text | Comment} */ let component_anchor; if (hydrating) { // Hydration: css props element is surrounded by a ssr comment ... - tag = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]); + element = /** @type {HTMLElement | SVGElement} */ (hydrate_nodes[0]); // ... and the child(ren) of the css props element is also surround by a ssr comment - component_anchor = /** @type {Comment} */ (tag.firstChild); + component_anchor = /** @type {Comment} */ (element.firstChild); } else { if (is_html) { - tag = document.createElement('div'); - tag.style.display = 'contents'; + element = document.createElement('div'); + element.style.display = 'contents'; } else { - tag = document.createElementNS(namespace_svg, 'g'); + element = document.createElementNS(namespace_svg, 'g'); } - anchor.before(tag); - component_anchor = empty(); - tag.appendChild(component_anchor); + anchor.before(element); + component_anchor = element.appendChild(empty()); } component(component_anchor); @@ -48,18 +47,18 @@ export function css_props(anchor, is_html, props, component) { for (const key in current_props) { if (!(key in next_props)) { - tag.style.removeProperty(key); + element.style.removeProperty(key); } } for (const key in next_props) { - tag.style.setProperty(key, next_props[key]); + element.style.setProperty(key, next_props[key]); } current_props = next_props; }); effect.ondestroy = () => { - remove(tag); + remove(element); }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index cd7ef3b318..5ef9c53ea8 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -18,6 +18,8 @@ export function head(render_fn) { update_hydrate_nodes(document.head.firstChild); } + var anchor = document.head.appendChild(empty()); + try { /** @type {import('#client').Dom | null} */ var dom = null; @@ -28,13 +30,7 @@ export function head(render_fn) { head_effect.dom = dom = null; } - let anchor = null; - if (!hydrating) { - anchor = empty(); - document.head.appendChild(anchor); - } - - dom = render_fn(anchor) ?? null; + dom = render_fn(hydrating ? null : anchor) ?? null; }); head_effect.ondestroy = () => { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index d8948cfe08..5807f7dcc8 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -114,8 +114,7 @@ export function createRoot() { * @returns {Exports} */ export function mount(component, options) { - const anchor = empty(); - options.target.appendChild(anchor); + const anchor = options.target.appendChild(empty()); // Don't flush previous effects to ensure order of outer effects stays consistent return flush_sync(() => _mount(component, { ...options, anchor }), false); } @@ -148,28 +147,21 @@ export function hydrate(component, options) { const nodes = update_hydrate_nodes(first_child, true); set_hydrating(true); - /** @type {null | Text} */ - let anchor = null; - - if (nodes === null) { - anchor = empty(); - container.appendChild(anchor); - } - - let finished_hydrating = false; + let hydrated = false; try { // Don't flush previous effects to ensure order of outer effects stays consistent return flush_sync(() => { + const anchor = nodes === null ? container.appendChild(empty()) : null; const instance = _mount(component, { ...options, anchor }); // flush_sync will run this callback and then synchronously run any pending effects, // which don't belong to the hydration phase anymore - therefore reset it here set_hydrating(false); - finished_hydrating = true; + hydrated = true; return instance; }, false); } catch (error) { - if (!finished_hydrating && options.recover !== false && nodes !== null) { + if (!hydrated && options.recover !== false && nodes !== null) { // eslint-disable-next-line no-console console.error( 'ERR_SVELTE_HYDRATION_MISMATCH' + From 82140752856f7da5497fb43fbaa314bdbcc5e54f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 15:11:14 -0400 Subject: [PATCH 015/102] fix: update type of `options.target` (#10892) --- .changeset/metal-lobsters-burn.md | 5 +++++ packages/svelte/src/internal/client/render.js | 6 +++--- packages/svelte/tests/types/component.ts | 4 ++-- packages/svelte/types/index.d.ts | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 .changeset/metal-lobsters-burn.md diff --git a/.changeset/metal-lobsters-burn.md b/.changeset/metal-lobsters-burn.md new file mode 100644 index 0000000000..b1e241554e --- /dev/null +++ b/.changeset/metal-lobsters-burn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: update type of `options.target` diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 5807f7dcc8..2046cdda92 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -105,7 +105,7 @@ export function createRoot() { * @template {Record} Events * @param {import('../../main/public.js').ComponentType>} component * @param {{ - * target: Node; + * target: Document | Element | ShadowRoot; * props?: Props; * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * context?: Map; @@ -127,7 +127,7 @@ export function mount(component, options) { * @template {Record} Events * @param {import('../../main/public.js').ComponentType>} component * @param {{ - * target: Node; + * target: Document | Element | ShadowRoot; * props?: Props; * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * context?: Map; @@ -190,7 +190,7 @@ export function hydrate(component, options) { * @template {Record} Events * @param {import('../../main/public.js').ComponentType>} Component * @param {{ - * target: Node; + * target: Document | Element | ShadowRoot; * anchor: null | Text; * props?: Props; * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 8ef5274016..d4ed55121f 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -105,7 +105,7 @@ const newComponentEvents2: ComponentEvents = { }; mount(NewComponent, { - target: null as any as Document | Element | ShadowRoot | Text | Comment, + target: null as any as Document | Element | ShadowRoot, props: { prop: 'foo', // @ts-expect-error @@ -120,7 +120,7 @@ mount(NewComponent, { }); hydrate(NewComponent, { - target: null as any as Document | Element | ShadowRoot | Text | Comment, + target: null as any as Document | Element | ShadowRoot, props: { prop: 'foo', // @ts-expect-error diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 44d680c9e9..58ca185d85 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -300,7 +300,7 @@ declare module 'svelte' { * * */ export function mount, Exports extends Record, Events extends Record>(component: ComponentType>, options: { - target: Node; + target: Document | Element | ShadowRoot; props?: Props | undefined; events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined; context?: Map | undefined; @@ -311,7 +311,7 @@ declare module 'svelte' { * * */ export function hydrate, Exports extends Record, Events extends Record>(component: ComponentType>, options: { - target: Node; + target: Document | Element | ShadowRoot; props?: Props | undefined; events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined; context?: Map | undefined; From 89f4e8d53a5273cdf54ea11c44535da7befd8eec Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 15:31:07 -0400 Subject: [PATCH 016/102] chore: more hydration stuff (#10894) * simplify * put memory cleanup where it belongs --- .../src/internal/client/dom/hydration.js | 23 ++++++------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index efb8543589..702ad7a2c3 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -90,22 +90,9 @@ function get_hydrate_nodes(node, insert_text = false) { export function hydrate_block_anchor(node) { if (!hydrating) return; - if (node.nodeType === 8) { - // @ts-ignore - let nodes = node.$$fragment; - if (nodes === undefined) { - nodes = get_hydrate_nodes(node); - } else { - schedule_task(() => { - // @ts-expect-error clean up memory - node.$$fragment = undefined; - }); - } - set_hydrate_nodes(nodes); - } else { - const first_child = /** @type {Element | null} */ (node.firstChild); - set_hydrate_nodes(first_child === null ? [] : [first_child]); - } + // @ts-ignore + var nodes = node.$$fragment ?? get_hydrate_nodes(node); + set_hydrate_nodes(nodes); } /** @@ -124,6 +111,10 @@ export function capture_fragment_from_node(node) { const target = /** @type {Node} */ (last_child.nextSibling); // @ts-ignore target.$$fragment = nodes; + schedule_task(() => { + // @ts-expect-error clean up memory + target.$$fragment = undefined; + }); return target; } return node; From 9b7331c04c05a536e2dd1531f188774d7641bef0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 23 Mar 2024 16:30:45 -0400 Subject: [PATCH 017/102] chore: only specify use_clone_node when necessary (#10895) --- .../3-transform/client/visitors/template.js | 39 ++++++++----------- .../client/dom/elements/custom-element.js | 2 +- .../src/internal/client/dom/template.js | 16 ++++---- .../_expected/client/main.svelte.js | 4 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/index.svelte.js | 4 +- 6 files changed, 31 insertions(+), 38 deletions(-) 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 cc018e91cf..0a03a562ee 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 @@ -1169,18 +1169,14 @@ function create_block(parent, name, nodes, context) { ) ); - body.push( - b.var( - id, - b.call( - '$.open', - b.id('$$anchor'), - b.literal(!state.metadata.context.template_needs_import_node), - template_name - ) - ), - ...state.init - ); + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$anchor'), template_name]; + + if (state.metadata.context.template_needs_import_node) { + args.push(b.false); + } + + body.push(b.var(id, b.call('$.open', ...args)), ...state.init); close = b.stmt(b.call('$.close', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); @@ -1227,17 +1223,14 @@ function create_block(parent, name, nodes, context) { ) ); - body.push( - b.var( - id, - b.call( - '$.open_frag', - b.id('$$anchor'), - b.literal(!state.metadata.context.template_needs_import_node), - template_name - ) - ) - ); + /** @type {import('estree').Expression[]} */ + const args = [b.id('$$anchor'), template_name]; + + if (state.metadata.context.template_needs_import_node) { + args.push(b.false); + } + + body.push(b.var(id, b.call('$.open_frag', ...args))); } body.push(...state.init); 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 cd2d2dfc60..c53447913d 100644 --- a/packages/svelte/src/internal/client/dom/elements/custom-element.js +++ b/packages/svelte/src/internal/client/dom/elements/custom-element.js @@ -98,7 +98,7 @@ if (typeof HTMLElement === 'function') { * @param {Element} anchor */ return (anchor) => { - const node = open(anchor, true, () => { + const node = open(anchor, () => { const slot = document.createElement('slot'); if (name !== 'default') { slot.name = name; diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index dc10c05dd9..8c2bae828a 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -106,23 +106,23 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) /** * @param {null | Text | Comment | Element} anchor - * @param {boolean} use_clone_node - * @param {() => Node} [template_element_fn] + * @param {() => Node} template_element_fn + * @param {boolean} [use_clone_node] * @returns {Element | DocumentFragment | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function open(anchor, use_clone_node, template_element_fn) { +export function open(anchor, template_element_fn, use_clone_node = true) { return open_template(false, use_clone_node, anchor, template_element_fn); } /** * @param {null | Text | Comment | Element} anchor - * @param {boolean} use_clone_node - * @param {() => Node} [template_element_fn] + * @param {() => Node} template_element_fn + * @param {boolean} [use_clone_node] * @returns {Element | DocumentFragment | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ -export function open_frag(anchor, use_clone_node, template_element_fn) { +export function open_frag(anchor, template_element_fn, use_clone_node = true) { return open_template(true, use_clone_node, anchor, template_element_fn); } @@ -135,7 +135,7 @@ const comment_template = template('', true); /*#__NO_SIDE_EFFECTS__*/ export function space_frag(anchor) { /** @type {Node | null} */ - var node = /** @type {any} */ (open(anchor, true, space_template)); + var node = /** @type {any} */ (open(anchor, space_template)); // if an {expression} is empty during SSR, there might be no // text node to hydrate (or an anchor comment is falsely detected instead) // — we must therefore create one @@ -169,7 +169,7 @@ export function space(anchor) { */ /*#__NO_SIDE_EFFECTS__*/ export function comment(anchor) { - return open_frag(anchor, true, comment_template); + return open_frag(anchor, comment_template); } /** diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index 6494d2436a..d75f77be1e 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -12,7 +12,7 @@ export default function Main($$anchor, $$props) { let x = 'test'; let y = () => 'test'; /* Init */ - var fragment = $.open_frag($$anchor, false, frag); + var fragment = $.open_frag($$anchor, frag, false); var div = $.child_frag(fragment); var svg = $.sibling($.sibling(div, true)); var custom_element = $.sibling($.sibling(svg, true)); @@ -45,4 +45,4 @@ export default function Main($$anchor, $$props) { $.close_frag($$anchor, fragment); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index f57dd8a9fd..ca941ce026 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -10,8 +10,8 @@ export default function Hello_world($$anchor, $$props) { $.init(); /* Init */ - var h1 = $.open($$anchor, true, frag); + var h1 = $.open($$anchor, frag); $.close($$anchor, h1); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index 3da681ea53..dbb06e8d27 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -18,7 +18,7 @@ export default function State_proxy_literal($$anchor, $$props) { let str = $.source(''); let tpl = $.source(``); /* Init */ - var fragment = $.open_frag($$anchor, true, frag); + var fragment = $.open_frag($$anchor, frag); var input = $.child_frag(fragment); $.remove_input_attr_defaults(input); @@ -36,4 +36,4 @@ export default function State_proxy_literal($$anchor, $$props) { $.pop(); } -$.delegate(["click"]); \ No newline at end of file +$.delegate(["click"]); From 32b18241980f933b14805fb90c0d6cecd040e12a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 24 Mar 2024 11:03:38 -0400 Subject: [PATCH 018/102] chore: more hydration stuff (#10896) * this condition is always true * rename child_frag to first_child * no need to use is_array, it is always an array when hydrating * simplify close_template * spread is faster than Array.from * avoid reassigning argument --- .../3-transform/client/visitors/template.js | 2 +- .../src/internal/client/dom/operations.js | 11 ++--- .../src/internal/client/dom/reconciler.js | 2 +- .../src/internal/client/dom/template.js | 41 ++++++++++--------- packages/svelte/src/internal/index.js | 2 +- .../_expected/client/index.svelte.js | 4 +- .../_expected/client/main.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 4 +- 11 files changed, 38 insertions(+), 36 deletions(-) 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 0a03a562ee..7c783c6f9e 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 @@ -1202,7 +1202,7 @@ function create_block(parent, name, nodes, context) { } else { /** @type {(is_text: boolean) => import('estree').Expression} */ const expression = (is_text) => - is_text ? b.call('$.child_frag', id, b.true) : b.call('$.child_frag', id); + is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id); process_children(trimmed, expression, false, { ...context, state }); diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 0ef60f52c8..067885b473 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -134,18 +134,19 @@ export function child(node) { } /** - * @template {Node | Node[]} N - * @param {N} node + * @param {DocumentFragment | import('#client').TemplateNode[]} fragment * @param {boolean} is_text * @returns {Node | null} */ /*#__NO_SIDE_EFFECTS__*/ -export function child_frag(node, is_text) { +export function first_child(fragment, is_text) { if (!hydrating) { - return first_child_get.call(/** @type {Node} */ (node)); + // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) + return first_child_get.call(/** @type {DocumentFragment} */ (fragment)); } - const first_node = /** @type {import('#client').TemplateNode[]} */ (node)[0]; + // when we _are_ hydrating, `fragment` is an array of nodes + const first_node = /** @type {import('#client').TemplateNode[]} */ (fragment)[0]; // if an {expression} is empty during SSR, there might be no // text node to hydrate — we must therefore create one diff --git a/packages/svelte/src/internal/client/dom/reconciler.js b/packages/svelte/src/internal/client/dom/reconciler.js index 78ae58c80c..ca12e72e5b 100644 --- a/packages/svelte/src/internal/client/dom/reconciler.js +++ b/packages/svelte/src/internal/client/dom/reconciler.js @@ -93,7 +93,7 @@ export function reconcile_html(target, value, svg) { content = /** @type {DocumentFragment} */ (/** @type {unknown} */ (content.firstChild)); } var clone = content.cloneNode(true); - frag_nodes = Array.from(clone.childNodes); + frag_nodes = [...clone.childNodes]; frag_nodes.forEach((node) => { target.before(node); }); diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 8c2bae828a..4b438a6baa 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -87,18 +87,17 @@ export function svg_template_with_script(svg, return_fragment) { * @param {() => Node} [template_element_fn] * @returns {Element | DocumentFragment | Node[]} */ +/*#__NO_SIDE_EFFECTS__*/ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) { if (hydrating) { if (anchor !== null) { + // TODO why is this sometimes null and sometimes not? needs clear documentation hydrate_block_anchor(anchor); } - // In ssr+hydration optimization mode, we might remove the template_element, - // so we need to is_fragment flag to properly handle hydrated content accordingly. - const nodes = hydrate_nodes; - if (nodes !== null) { - return is_fragment ? nodes : /** @type {Element} */ (nodes[0]); - } + + return is_fragment ? hydrate_nodes : /** @type {Element} */ (hydrate_nodes[0]); } + return use_clone_node ? clone_node(/** @type {() => Element} */ (template_element_fn)(), true) : document.importNode(/** @type {() => Element} */ (template_element_fn)(), true); @@ -108,11 +107,10 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) * @param {null | Text | Comment | Element} anchor * @param {() => Node} template_element_fn * @param {boolean} [use_clone_node] - * @returns {Element | DocumentFragment | Node[]} + * @returns {Element} */ -/*#__NO_SIDE_EFFECTS__*/ export function open(anchor, template_element_fn, use_clone_node = true) { - return open_template(false, use_clone_node, anchor, template_element_fn); + return /** @type {Element} */ (open_template(false, use_clone_node, anchor, template_element_fn)); } /** @@ -121,7 +119,6 @@ export function open(anchor, template_element_fn, use_clone_node = true) { * @param {boolean} [use_clone_node] * @returns {Element | DocumentFragment | Node[]} */ -/*#__NO_SIDE_EFFECTS__*/ export function open_frag(anchor, template_element_fn, use_clone_node = true) { return open_template(true, use_clone_node, anchor, template_element_fn); } @@ -175,21 +172,25 @@ export function comment(anchor) { /** * Assign the created (or in hydration mode, traversed) dom elements to the current block * and insert the elements into the dom (in client mode). - * @param {Element | Text} dom + * @param {import('#client').Dom} dom * @param {boolean} is_fragment * @param {null | Text | Comment | Element} anchor * @returns {import('#client').Dom} */ function close_template(dom, is_fragment, anchor) { - /** @type {import('#client').Dom} */ - var current = is_fragment - ? is_array(dom) - ? dom - : /** @type {import('#client').TemplateNode[]} */ (Array.from(dom.childNodes)) - : dom; - - if (!hydrating && anchor !== null) { - insert(current, anchor); + var current = dom; + + if (!hydrating) { + if (is_fragment) { + // if hydrating, `dom` is already an array of nodes, but if not then + // we need to create an array to store it on the current effect + current = /** @type {import('#client').Dom} */ ([.../** @type {Node} */ (dom).childNodes]); + } + + if (anchor !== null) { + // TODO as with `open_template — why is this sometimes null and sometimes not? + insert(current, anchor); + } } /** @type {import('#client').Effect} */ (current_effect).dom = current; diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 71dd6e0f84..a8286ea16d 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -64,7 +64,7 @@ export { proxy, unstate } from './client/proxy.js'; export { create_custom_element } from './client/dom/elements/custom-element.js'; export { child, - child_frag, + first_child, sibling, $window as window, $document as document diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index 78ae1ffc52..f0ec694005 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -9,9 +9,9 @@ export default function Bind_this($$anchor, $$props) { /* Init */ var fragment = $.comment($$anchor); - var node = $.child_frag(fragment); + var node = $.first_child(fragment); $.bind_this(Foo(node, {}), ($$value) => foo = $$value, () => foo); $.close_frag($$anchor, fragment); $.pop(); -} \ No newline at end of file +} diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index d75f77be1e..28e6805179 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -13,7 +13,7 @@ export default function Main($$anchor, $$props) { let y = () => 'test'; /* Init */ var fragment = $.open_frag($$anchor, frag, false); - var div = $.child_frag(fragment); + var div = $.first_child(fragment); var svg = $.sibling($.sibling(div, true)); var custom_element = $.sibling($.sibling(svg, true)); var div_1 = $.sibling($.sibling(custom_element, true)); diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 3a88121a38..7fd397b25c 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Each_string_template($$anchor, $$props) { /* Init */ var fragment = $.comment($$anchor); - var node = $.child_frag(fragment); + var node = $.first_child(fragment); $.each_indexed( node, diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index cbe02d18ad..aeeb403491 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -15,7 +15,7 @@ export default function Function_prop_no_getter($$anchor, $$props) { const plusOne = (num) => num + 1; /* Init */ var fragment = $.comment($$anchor); - var node = $.child_frag(fragment); + var node = $.first_child(fragment); Button(node, { onmousedown: () => $.set(count, $.get(count) + 1), diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index dbb06e8d27..c3d983f6a0 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -19,7 +19,7 @@ export default function State_proxy_literal($$anchor, $$props) { let tpl = $.source(``); /* Init */ var fragment = $.open_frag($$anchor, frag); - var input = $.child_frag(fragment); + var input = $.first_child(fragment); $.remove_input_attr_defaults(input); diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js index b2734b8c46..4b6952d1dc 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -9,9 +9,9 @@ export default function Svelte_element($$anchor, $$props) { let tag = $.prop($$props, "tag", 3, 'hr'); /* Init */ var fragment = $.comment($$anchor); - var node = $.child_frag(fragment); + var node = $.first_child(fragment); $.element(node, tag, false); $.close_frag($$anchor, fragment); $.pop(); -} \ No newline at end of file +} From 7e5e4621948a2d930c855196f53fd56ab4b7cf79 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 24 Mar 2024 11:25:18 -0400 Subject: [PATCH 019/102] feat: use short comments (#10899) * use short comments * fix --- .../phases/3-transform/server/transform-server.js | 6 +++--- packages/svelte/src/internal/server/index.js | 2 +- packages/svelte/tests/html_equal.js | 14 +++++++------- .../_expected-head.html | 4 ++-- .../head-meta-hydrate-duplicate/_expected.html | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index fc5ef8e57e..35b8ef28a8 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1521,7 +1521,7 @@ const template_visitors = { if (node.fallback) { const fallback_stmts = create_block(node, node.fallback.nodes, context); fallback_stmts.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) ); state.template.push( t_statement( @@ -1549,14 +1549,14 @@ const template_visitors = { const consequent = create_block(node, node.consequent.nodes, context); consequent.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) ); const alternate = node.alternate ? /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) : b.block([]); alternate.body.unshift( - b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) + b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(''))) ); state.template.push( diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index bc1caf2d36..8e0651ff93 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -637,7 +637,7 @@ export function ensure_array_like(array_like_or_iterator) { /** @param {{ anchor: number }} payload */ export function create_anchor(payload) { const depth = payload.anchor++; - return ``; + return ``; } /** @returns {[() => false, (value: boolean) => void]} */ diff --git a/packages/svelte/tests/html_equal.js b/packages/svelte/tests/html_equal.js index 9303fea184..a53f3ef35e 100644 --- a/packages/svelte/tests/html_equal.js +++ b/packages/svelte/tests/html_equal.js @@ -77,7 +77,7 @@ export function normalize_html( try { const node = window.document.createElement('div'); node.innerHTML = html - .replace(/()/g, preserveComments ? '$1' : '') + .replace(/()/g, preserveComments ? '$1' : '') .replace(/(data-svelte-h="[^"]+")/g, removeDataSvelte ? '' : '$1') .replace(/>[ \t\n\r\f]+<') .trim(); @@ -135,15 +135,15 @@ export function setup_html_equal(options = {}) { try { assert.deepEqual( withoutNormalizeHtml - ? normalize_new_line(actual) + ? normalize_new_line(actual.trim()) .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, actual, { ...options, preserveComments }), + .replace(/()/g, preserveComments !== false ? '$1' : '') + : normalize_html(window, actual.trim(), { ...options, preserveComments }), withoutNormalizeHtml - ? normalize_new_line(expected) + ? normalize_new_line(expected.trim()) .replace(/(\sdata-svelte-h="[^"]+")/g, options.removeDataSvelte ? '' : '$1') - .replace(/()/g, preserveComments !== false ? '$1' : '') - : normalize_html(window, expected, { ...options, preserveComments }), + .replace(/()/g, preserveComments !== false ? '$1' : '') + : normalize_html(window, expected.trim(), { ...options, preserveComments }), message ); } catch (e) { diff --git a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html index 74821a4d73..0e12a4a9de 100644 --- a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html +++ b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected-head.html @@ -1,6 +1,6 @@ - + Some Title - + diff --git a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html index b5fd3947d5..b43330ef0a 100644 --- a/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html +++ b/packages/svelte/tests/server-side-rendering/samples/head-meta-hydrate-duplicate/_expected.html @@ -1 +1 @@ -
      Just a dummy page.
      \ No newline at end of file +
      Just a dummy page.
      From 302b0ec973c2f9116fe316fad329146259aaca4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 24 Mar 2024 14:25:24 -0400 Subject: [PATCH 020/102] chore: remove unused selector() function (#10903) --- .../src/compiler/phases/2-analyze/index.js | 19 ------------------- packages/svelte/src/internal/server/index.js | 6 ------ 2 files changed, 25 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index da3886cc53..e8246db02e 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -952,25 +952,6 @@ const runes_scope_tweaker = { function is_known_safe_call(node, context) { const callee = node.callee; - // Check for selector() API calls - if (callee.type === 'MemberExpression' && callee.object.type === 'Identifier') { - const binding = context.state.scope.get(callee.object.name); - const selector_binding = context.state.scope.get('selector'); - if ( - selector_binding !== null && - selector_binding.declaration_kind === 'import' && - selector_binding.initial !== null && - selector_binding.initial.type === 'ImportDeclaration' && - selector_binding.initial.source.value === 'svelte' && - binding !== null && - binding.initial !== null && - binding.initial.type === 'CallExpression' && - binding.initial.callee.type === 'Identifier' && - binding.initial.callee.name === 'selector' - ) { - return true; - } - } // String / Number / BigInt / Boolean casting calls if (callee.type === 'Identifier') { const name = callee.name; diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 8e0651ff93..3dffbbb598 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -640,12 +640,6 @@ export function create_anchor(payload) { return ``; } -/** @returns {[() => false, (value: boolean) => void]} */ -export function selector() { - // Add SSR stubs - return [() => false, noop]; -} - /** * @param {number} timeout * @returns {() => void} From f1d9afe32f84ffd138ab2a67906b30305cd3e4e7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 24 Mar 2024 18:03:56 -0400 Subject: [PATCH 021/102] changeset (#10906) --- .changeset/many-rockets-give.md | 5 +++++ .../3-transform/client/visitors/template.js | 19 ++++++++++++------- .../src/internal/client/dom/blocks/if.js | 10 ++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) create mode 100644 .changeset/many-rockets-give.md diff --git a/.changeset/many-rockets-give.md b/.changeset/many-rockets-give.md new file mode 100644 index 0000000000..b26d8ef64f --- /dev/null +++ b/.changeset/many-rockets-give.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: more efficient if block compiler output 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 7c783c6f9e..873fa44a70 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 @@ -2573,15 +2573,20 @@ export const template_visitors = { const args = [ context.state.node, b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.test))), - b.arrow([b.id('$$anchor')], consequent), - node.alternate - ? b.arrow( - [b.id('$$anchor')], - /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) - ) - : b.literal(null) + b.arrow([b.id('$$anchor')], consequent) ]; + if (node.alternate || node.elseif) { + args.push( + node.alternate + ? b.arrow( + [b.id('$$anchor')], + /** @type {import('estree').BlockStatement} */ (context.visit(node.alternate)) + ) + : b.literal(null) + ); + } + if (node.elseif) { // We treat this... // diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 84b294ccbb..dde4de4dbc 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -12,11 +12,17 @@ import { * @param {Comment} anchor * @param {() => boolean} get_condition * @param {(anchor: Node) => import('#client').Dom} consequent_fn - * @param {null | ((anchor: Node) => import('#client').Dom)} alternate_fn + * @param {null | ((anchor: Node) => import('#client').Dom)} [alternate_fn] * @param {boolean} [elseif] True if this is an `{:else if ...}` block rather than an `{#if ...}`, as that affects which transitions are considered 'local' * @returns {void} */ -export function if_block(anchor, get_condition, consequent_fn, alternate_fn, elseif = false) { +export function if_block( + anchor, + get_condition, + consequent_fn, + alternate_fn = null, + elseif = false +) { hydrate_block_anchor(anchor); /** @type {import('#client').Effect | null} */ From 8685d497e5484c6f2bc3f0648be518800f81a5b1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 25 Mar 2024 09:04:38 -0400 Subject: [PATCH 022/102] feat: use bracket matching instead of `ssr:n` comments (#10904) * use short comments * use bracket matching * fix * update snapshots * update tests * fix --- .../3-transform/server/transform-server.js | 93 ++++++------------- .../src/internal/client/dom/hydration.js | 33 ++++--- packages/svelte/src/internal/client/render.js | 14 ++- packages/svelte/src/internal/server/index.js | 32 +++---- .../runtime-legacy/samples/pre-tag/_config.js | 4 +- .../samples/raw-anchor-first-child/_config.js | 2 +- .../samples/textarea-content/_config.js | 6 +- .../samples/comment-preserve/_expected.html | 6 +- .../_expected.html | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 7 +- .../_expected/client/main.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 10 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 5 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/server/index.svelte.js | 5 +- 20 files changed, 98 insertions(+), 135 deletions(-) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 35b8ef28a8..5b40e32606 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -27,6 +27,9 @@ import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patte import { DOMBooleanAttributes } from '../../../../constants.js'; import { sanitize_template_string } from '../../../utils/sanitize_template_string.js'; +const block_open = t_string(''); +const block_close = t_string(''); + /** * @param {string} value * @returns {import('./types').TemplateString} @@ -52,15 +55,6 @@ function t_statement(value) { return { type: 'statement', value }; } -/** - * @param {import('./types').ServerTransformState} state - * @returns {[import('estree').VariableDeclaration, import('estree').Identifier]} - */ -function serialize_anchor(state) { - const id = state.scope.root.unique('anchor'); - return [b.const(id, b.call('$.create_anchor', b.id('$$payload'))), id]; -} - /** * @param {import('./types').Template[]} template * @param {import('estree').Identifier} out @@ -1237,12 +1231,10 @@ const template_visitors = { }, HtmlTag(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const raw = /** @type {import('estree').Expression} */ (context.visit(node.expression)); context.state.template.push(t_expression(raw)); - state.template.push(t_expression(id)); + state.template.push(block_close); }, ConstTag(node, { state, visit }) { const declaration = node.declaration.declarations[0]; @@ -1273,10 +1265,8 @@ const template_visitors = { }, RenderTag(node, context) { const state = context.state; - const [anchor, anchor_id] = serialize_anchor(state); - state.init.push(anchor); - state.template.push(t_expression(anchor_id)); + state.template.push(block_open); const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; @@ -1302,7 +1292,7 @@ const template_visitors = { ) ); - state.template.push(t_expression(anchor_id)); + state.template.push(block_close); }, ClassDirective(node) { error(node, 'INTERNAL', 'Node should have been handled elsewhere'); @@ -1427,9 +1417,7 @@ const template_visitors = { } }; - const [el_anchor, anchor_id] = serialize_anchor(context.state); - context.state.init.push(el_anchor); - context.state.template.push(t_expression(anchor_id)); + context.state.template.push(block_open); const main = create_block(node, node.fragment.nodes, { ...context, @@ -1465,7 +1453,7 @@ const template_visitors = { ) ) ), - t_expression(anchor_id) + block_close ); if (context.state.options.dev) { context.state.template.push(t_statement(b.stmt(b.call('$.pop_element')))); @@ -1473,9 +1461,7 @@ const template_visitors = { }, EachBlock(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const each_node_meta = node.metadata; const collection = /** @type {import('estree').Expression} */ (context.visit(node.expression)); @@ -1486,14 +1472,6 @@ const template_visitors = { : b.id(node.index); const children = node.body.nodes; - const [each_dec, each_id] = serialize_anchor(state); - - /** @type {import('./types').Anchor} */ - const anchor = { - type: 'Anchor', - id: each_id - }; - const array_id = state.scope.root.unique('each_array'); state.init.push(b.const(array_id, b.call('$.ensure_array_like', collection))); @@ -1507,11 +1485,14 @@ const template_visitors = { each.push(b.let(node.index, index)); } + each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_open.value)))); + each.push( - each_dec, - .../** @type {import('estree').Statement[]} */ (create_block(node, children, context, anchor)) + .../** @type {import('estree').Statement[]} */ (create_block(node, children, context)) ); + each.push(b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(block_close.value)))); + const for_loop = b.for( b.let(index, b.literal(0)), b.binary('<', index, b.member(array_id, b.id('length'))), @@ -1535,13 +1516,11 @@ const template_visitors = { } else { state.template.push(t_statement(for_loop)); } - state.template.push(t_expression(id)); + state.template.push(block_close); }, IfBlock(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); // Insert ssr:if:true/false anchors in addition to the other anchors so that // the if block can catch hydration mismatches (false on the server, true on the client and vice versa) @@ -1568,13 +1547,11 @@ const template_visitors = { ) ) ); - state.template.push(t_expression(id)); + state.template.push(block_close); }, AwaitBlock(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); state.template.push( t_statement( @@ -1608,16 +1585,14 @@ const template_visitors = { ) ); - state.template.push(t_expression(id)); + state.template.push(block_close); }, KeyBlock(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const body = create_block(node, node.fragment.nodes, context); state.template.push(t_statement(b.block(body))); - state.template.push(t_expression(id)); + state.template.push(block_close); }, SnippetBlock(node, context) { // TODO hoist where possible @@ -1635,34 +1610,28 @@ const template_visitors = { }, Component(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const call = serialize_inline_component(node, node.name, context); state.template.push(t_statement(call)); - state.template.push(t_expression(id)); + state.template.push(block_close); }, SvelteSelf(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const call = serialize_inline_component(node, context.state.analysis.name, context); state.template.push(t_statement(call)); - state.template.push(t_expression(id)); + state.template.push(block_close); }, SvelteComponent(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); const call = serialize_inline_component( node, /** @type {import('estree').Expression} */ (context.visit(node.expression)), context ); state.template.push(t_statement(call)); - state.template.push(t_expression(id)); + state.template.push(block_close); }, LetDirective(node, { state }) { if (node.expression && node.expression.type !== 'Identifier') { @@ -1745,9 +1714,7 @@ const template_visitors = { }, SlotElement(node, context) { const state = context.state; - const [dec, id] = serialize_anchor(state); - state.init.push(dec); - state.template.push(t_expression(id)); + state.template.push(block_open); /** @type {import('estree').Property[]} */ const props = []; @@ -1794,7 +1761,7 @@ const template_visitors = { const slot = b.call('$.slot', b.id('$$payload'), expression, props_expression, fallback); state.template.push(t_statement(b.stmt(slot))); - state.template.push(t_expression(id)); + state.template.push(block_close); }, SvelteHead(node, context) { const state = context.state; diff --git a/packages/svelte/src/internal/client/dom/hydration.js b/packages/svelte/src/internal/client/dom/hydration.js index 702ad7a2c3..3850862f1e 100644 --- a/packages/svelte/src/internal/client/dom/hydration.js +++ b/packages/svelte/src/internal/client/dom/hydration.js @@ -39,7 +39,7 @@ export function update_hydrate_nodes(first, insert_text) { } /** - * Returns all nodes between the first `` comment tag pair encountered. + * Returns all nodes between the first `...` comment tag pair encountered. * @param {Node | null} node * @param {boolean} [insert_text] Whether to insert an empty text node if `nodes` is empty * @returns {import('#client').TemplateNode[] | null} @@ -50,34 +50,43 @@ function get_hydrate_nodes(node, insert_text = false) { var current_node = /** @type {null | import('#client').TemplateNode} */ (node); - /** @type {null | string} */ - var target_depth = null; + var depth = 0; + + var will_start = false; + var started = false; while (current_node !== null) { if (current_node.nodeType === 8) { var data = /** @type {Comment} */ (current_node).data; - if (data.startsWith('ssr:')) { - var depth = data.slice(4); + if (data === '[') { + depth += 1; + will_start = true; + } else if (data === ']') { + if (!started) { + // TODO get rid of this — it exists because each blocks are doubly wrapped + return null; + } - if (target_depth === null) { - target_depth = depth; - } else if (depth === target_depth) { + if (--depth === 0) { if (insert_text && nodes.length === 0) { var text = empty(); nodes.push(text); current_node.before(text); } + return nodes; - } else { - nodes.push(current_node); } } - } else if (target_depth !== null) { + } + + if (started) { nodes.push(current_node); } current_node = /** @type {null | import('#client').TemplateNode} */ (current_node.nextSibling); + + started = will_start; } return null; @@ -103,7 +112,7 @@ export function hydrate_block_anchor(node) { export function capture_fragment_from_node(node) { if ( node.nodeType === 8 && - /** @type {Comment} */ (node).data.startsWith('ssr:') && + /** @type {Comment} */ (node).data === '[' && hydrate_nodes[hydrate_nodes.length - 1] !== node ) { const nodes = /** @type {Node[]} */ (get_hydrate_nodes(node)); diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 2046cdda92..a8683be79f 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1,5 +1,11 @@ import { DEV } from 'esm-env'; -import { append_child, create_element, empty, init_operations } from './dom/operations.js'; +import { + append_child, + clear_text_content, + create_element, + empty, + init_operations +} from './dom/operations.js'; import { PassiveDelegatedEvents } from '../../constants.js'; import { remove } from './dom/reconciler.js'; import { flush_sync, push, pop, current_component_context } from './runtime.js'; @@ -170,9 +176,9 @@ export function hydrate(component, options) { : ''), error ); - remove(nodes); - first_child.remove(); - nodes[nodes.length - 1]?.nextSibling?.remove(); + + clear_text_content(container); + set_hydrating(false); return mount(component, options); } else { diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 3dffbbb598..313ed39099 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -162,13 +162,12 @@ export function element(payload, tag, attributes_fn, children_fn) { payload.out += `>`; if (!VoidElements.has(tag)) { - const anchor = tag !== 'textarea' ? create_anchor(payload) : null; - if (anchor !== null) { - payload.out += anchor; + if (tag !== 'textarea') { + payload.out += ''; } children_fn(); - if (anchor !== null) { - payload.out += anchor; + if (tag !== 'textarea') { + payload.out += ''; } payload.out += ``; } @@ -187,12 +186,10 @@ export let on_destroy = []; */ export function render(component, options) { const payload = create_payload(); - const root_anchor = create_anchor(payload); - const root_head_anchor = create_anchor(payload.head); const prev_on_destroy = on_destroy; on_destroy = []; - payload.out += root_anchor; + payload.out += ''; if (options.context) { $.push({}); @@ -203,14 +200,14 @@ export function render(component, options) { if (options.context) { $.pop(); } - payload.out += root_anchor; + payload.out += ''; for (const cleanup of on_destroy) cleanup(); on_destroy = prev_on_destroy; return { head: payload.head.out || payload.head.title - ? payload.head.title + root_head_anchor + payload.head.out + root_head_anchor + ? payload.head.title + '' + payload.head.out + '' : '', html: payload.out }; @@ -284,17 +281,16 @@ export function attr(name, value, boolean) { */ export function css_props(payload, is_html, props, component) { const styles = style_object_to_string(props); - const anchor = create_anchor(payload); if (is_html) { - payload.out += `
      ${anchor}`; + payload.out += `
      `; } else { - payload.out += `${anchor}`; + payload.out += ``; } component(); if (is_html) { - payload.out += `${anchor}
      `; + payload.out += `
      `; } else { - payload.out += `${anchor}`; + payload.out += ``; } } @@ -634,12 +630,6 @@ export function ensure_array_like(array_like_or_iterator) { : Array.from(array_like_or_iterator); } -/** @param {{ anchor: number }} payload */ -export function create_anchor(payload) { - const depth = payload.anchor++; - return ``; -} - /** * @param {number} timeout * @returns {() => void} diff --git a/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js b/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js index d5a18488e2..3f28031a05 100644 --- a/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/pre-tag/_config.js @@ -12,7 +12,7 @@ function get_html(ssr) { // ssr rendered HTML has an extra newline prefixed within `
      ` tag,
       	// if the 
       tag starts with `\n`
       	// because when browser parses the SSR rendered HTML, it will ignore the 1st '\n' character
      -	return `${ssr ? '' : ''}
        A
      +	return `${ssr ? '' : ''}
        A
         B
         
           C
      @@ -35,5 +35,5 @@ function get_html(ssr) {
       leading newlines
      without spaces
        with spaces  
      ${' '}
       newline after leading space
       
      -multiple leading newlines
      ${ssr ? '' : ''}`; +multiple leading newlines
      ${ssr ? '' : ''}`; } diff --git a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js index c9137c3ca9..f575b3274e 100644 --- a/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/raw-anchor-first-child/_config.js @@ -11,7 +11,7 @@ export default test({ if (variant === 'dom') { assert.ok(!span.previousSibling); } else { - assert.ok(span.previousSibling?.textContent?.startsWith('ssr:')); // ssr commment node + assert.ok(span.previousSibling?.textContent === '['); // ssr commment node } component.raw = 'bar'; diff --git a/packages/svelte/tests/runtime-legacy/samples/textarea-content/_config.js b/packages/svelte/tests/runtime-legacy/samples/textarea-content/_config.js index 080976c518..7cd7c553e0 100644 --- a/packages/svelte/tests/runtime-legacy/samples/textarea-content/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/textarea-content/_config.js @@ -4,17 +4,17 @@ export default test({ withoutNormalizeHtml: true, // Unable to test `html` with `
      `, +multiple leading newlines`, test({ assert, target }) { // Test for