From 617df1f3c31d6baa03745cc8ab2266983fcd0d5e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 21 Nov 2023 04:27:01 +0100 Subject: [PATCH 01/46] fix: adjust regex (#9572) would error on non-quoted attributes with more than one character --- .changeset/wicked-clouds-exercise.md | 5 +++++ packages/svelte/src/compiler/phases/1-parse/index.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/wicked-clouds-exercise.md diff --git a/.changeset/wicked-clouds-exercise.md b/.changeset/wicked-clouds-exercise.md new file mode 100644 index 0000000000..64c1c0c9b2 --- /dev/null +++ b/.changeset/wicked-clouds-exercise.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: adjust regex diff --git a/packages/svelte/src/compiler/phases/1-parse/index.js b/packages/svelte/src/compiler/phases/1-parse/index.js index eef5178fb1..8c1bb174f8 100644 --- a/packages/svelte/src/compiler/phases/1-parse/index.js +++ b/packages/svelte/src/compiler/phases/1-parse/index.js @@ -11,7 +11,7 @@ import read_options from './read/options.js'; const regex_position_indicator = / \(\d+:\d+\)$/; const regex_lang_attribute = - /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s])\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; + /|]*|(?:[^=>'"/]+=(?:"[^"]*"|'[^']*'|[^>\s]+)\s+)*)lang=(["'])?([^"' >]+)\1[^>]*>/; export class Parser { /** From 945a90b56cbe6961b73e736ee4bf5b4401037ff3 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Nov 2023 03:39:44 -0500 Subject: [PATCH 02/46] fix: disallow static state fields (#9577) closes #9547 --- .../compiler/phases/2-analyze/validation.js | 25 ++++++++++++------- .../class-state-field-static/_config.js | 9 +++++++ .../class-state-field-static/main.svelte | 5 ++++ 3 files changed, 30 insertions(+), 9 deletions(-) create mode 100644 packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js create mode 100644 packages/svelte/tests/compiler-errors/samples/class-state-field-static/main.svelte diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 8c6c3365b0..2cf0129c53 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -490,18 +490,25 @@ function validate_call_expression(node, scope, path) { const rune = get_rune(node, scope); if (rune === null) return; - if (rune === '$props' && path.at(-1)?.type !== 'VariableDeclarator') { + const parent = /** @type {import('#compiler').SvelteNode} */ (path.at(-1)); + + if (rune === '$props') { + if (parent.type === 'VariableDeclarator') return; error(node, 'invalid-props-location'); - } else if ( - (rune === '$state' || rune === '$derived') && - path.at(-1)?.type !== 'VariableDeclarator' && - path.at(-1)?.type !== 'PropertyDefinition' - ) { + } + + if (rune === '$state' || rune === '$derived') { + if (parent.type === 'VariableDeclarator') return; + if (parent.type === 'PropertyDefinition' && !parent.static && !parent.computed) return; error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'); - } else if (rune === '$effect') { - if (path.at(-1)?.type !== 'ExpressionStatement') { + } + + if (rune === '$effect') { + if (parent.type !== 'ExpressionStatement') { error(node, 'invalid-effect-location'); - } else if (node.arguments.length !== 1) { + } + + if (node.arguments.length !== 1) { error(node, 'invalid-rune-args-length', '$effect', [1]); } } diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js new file mode 100644 index 0000000000..b6dca4e40d --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/_config.js @@ -0,0 +1,9 @@ +import { test } from '../../test'; + +export default test({ + error: { + code: 'invalid-state-location', + message: '$state() can only be used as a variable declaration initializer or a class field', + position: process.platform === 'win32' ? [35, 43] : [33, 41] + } +}); diff --git a/packages/svelte/tests/compiler-errors/samples/class-state-field-static/main.svelte b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/main.svelte new file mode 100644 index 0000000000..8eb861366b --- /dev/null +++ b/packages/svelte/tests/compiler-errors/samples/class-state-field-static/main.svelte @@ -0,0 +1,5 @@ + From 1bc89b5eb6e745f05fd534b45dc62b2d269f05f2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Nov 2023 03:49:36 -0500 Subject: [PATCH 03/46] fix: deconflict generated names against globals (#9570) fixes #9559 --- .changeset/green-hounds-play.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 4 ++++ .../samples/globals-deconflicted/_config.js | 15 +++++++++++++++ .../samples/globals-deconflicted/main.svelte | 1 + 4 files changed, 25 insertions(+) create mode 100644 .changeset/green-hounds-play.md create mode 100644 packages/svelte/tests/runtime-runes/samples/globals-deconflicted/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/globals-deconflicted/main.svelte diff --git a/.changeset/green-hounds-play.md b/.changeset/green-hounds-play.md new file mode 100644 index 0000000000..6a2922d0d5 --- /dev/null +++ b/.changeset/green-hounds-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: deconflict generated names against globals diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 5c18e78f7c..22e5d3eaec 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -184,6 +184,10 @@ export class Scope { declaration.references.push({ node, path }); } else if (this.#parent) { this.#parent.reference(node, path); + } else { + // no declaration was found, and this is the top level scope, + // which means this is a global + this.root.conflicts.add(node.name); } } } diff --git a/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/_config.js b/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/_config.js new file mode 100644 index 0000000000..3d1ab275b9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + html: `

hello

`, + + before_test: () => { + // @ts-expect-error + globalThis.frag = 'hello'; + }, + + after_test: () => { + // @ts-expect-error + delete globalThis.frag; + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/main.svelte b/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/main.svelte new file mode 100644 index 0000000000..25a651859c --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/globals-deconflicted/main.svelte @@ -0,0 +1 @@ +

{frag}

From c011db178bdda07bc089b85fa3efb9ebd4880639 Mon Sep 17 00:00:00 2001 From: Theodore Brown Date: Tue, 21 Nov 2023 03:03:14 -0600 Subject: [PATCH 04/46] fix: select enabled option with null value when it matches bound value (#9550) Fix select binding when matching enabled option has null value Fix null option being selected when it doesn't match the bound value Fixes #9545 --- .../3-transform/client/visitors/template.js | 27 ++++--- packages/svelte/src/internal/client/render.js | 74 ++++++------------- .../_config.js | 2 +- .../_config.js | 34 +++++++++ .../main.svelte | 11 +++ 5 files changed, 85 insertions(+), 63 deletions(-) create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/main.svelte 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 615fafd880..5d6d9a02b2 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 @@ -255,7 +255,7 @@ function setup_select_synchronization(value_binding, context) { * value = $.spread_attributes(element, value, [...]) * }); * ``` - * Returns the id of the spread_attribute varialbe if spread is deemed reactive, `null` otherwise. + * Returns the id of the spread_attribute variable if spread is deemed reactive, `null` otherwise. * @param {Array} attributes * @param {import('../types.js').ComponentContext} context * @param {import('estree').Identifier} element_id @@ -376,7 +376,7 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen } /** - * Serializes an assigment to an element property by adding relevant statements to either only + * Serializes an assignment to an element property by adding relevant statements to either only * the init or the the init and update arrays, depending on whether or not the value is dynamic. * Resulting code for static looks something like this: * ```js @@ -551,7 +551,7 @@ function serialize_custom_element_attribute_update_assignment(node_id, attribute } /** - * Serializes an assigment to the value property of a `` element + * Serializes an assignment to the value property of a `` element * that needs the hidden `__value` property. * Returns true if attribute is deemed reactive, false otherwise. * @param {string} element @@ -567,13 +567,17 @@ function serialize_element_special_value_attribute(element, node_id, attribute, const inner_assignment = b.assignment( '=', b.member(node_id, b.id('value')), - b.assignment('=', b.member(node_id, b.id('__value')), value) + b.conditional( + b.binary('==', b.literal(null), b.assignment('=', b.member(node_id, b.id('__value')), value)), + b.literal(''), // render null/undefined values as empty string to support placeholder options + value + ) ); const is_reactive = attribute.metadata.dynamic; const needs_selected_call = element === 'option' && (is_reactive || collect_parent_each_blocks(context).length > 0); const needs_option_call = element === 'select' && is_reactive; - const assigment = b.stmt( + const assignment = b.stmt( needs_selected_call ? b.sequence([ inner_assignment, @@ -582,9 +586,12 @@ function serialize_element_special_value_attribute(element, node_id, attribute, b.call('$.selected', node_id) ]) : needs_option_call - ? // This ensures a one-way street to the DOM in case it's - b.call('$.select_option', node_id, inner_assignment) + ? b.sequence([ + inner_assignment, + // This ensures a one-way street to the DOM in case it's + b.call('$.select_option', node_id, value) + ]) : inner_assignment ); @@ -595,12 +602,12 @@ function serialize_element_special_value_attribute(element, node_id, attribute, id, undefined, value, - { grouped: assigment }, + { grouped: assignment }, contains_call_expression ); return true; } else { - state.init.push(assigment); + state.init.push(assignment); return false; } } diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index d992199d8d..804840d6c9 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -444,20 +444,22 @@ export function class_toggle(dom, class_name, value) { * @template V * @param {HTMLSelectElement} select * @param {V} value + * @param {boolean} [mounting] */ -export function select_option(select, value) { +export function select_option(select, value, mounting) { if (select.multiple) { return select_options(select, value); } - for (let i = 0; i < select.options.length; i += 1) { - const option = select.options[i]; + for (const option of select.options) { const option_value = get_option_value(option); if (option_value === value) { option.selected = true; return; } } - select.value = ''; + if (!mounting || value !== undefined) { + select.selectedIndex = -1; // no option should be selected + } } /** @@ -466,8 +468,7 @@ export function select_option(select, value) { * @param {V} value */ function select_options(select, value) { - for (let i = 0; i < select.options.length; i += 1) { - const option = select.options[i]; + for (const option of select.options) { // @ts-ignore option.selected = ~value.indexOf(get_option_value(option)); } @@ -897,20 +898,10 @@ export function selected(dom) { } select = select.parentNode; } - if (select != null) { - // @ts-ignore - const select_value = select.__value; - // @ts-ignore - const option_value = dom.__value; - const selected = select_value === option_value; - dom.selected = selected; - dom.value = option_value; - // Handle the edge case of new options being added to a select when its state is "nothing selected" - // and keeping the selection state in sync (the DOM auto-selects the first option on insert) - // @ts-ignore - if (select.__value === null) { - /** @type {HTMLSelectElement} */ (select).value = ''; - } + // @ts-ignore + if (select != null && dom.__value === select.__value) { + // never set to false, since this causes browser to select default option + dom.selected = true; } }); } @@ -949,7 +940,7 @@ export function bind_value(dom, get_value, update) { * @returns {void} */ export function bind_select_value(dom, get_value, update) { - let mounted = false; + let mounting = true; dom.addEventListener('change', () => { /** @type {unknown} */ let value; @@ -964,40 +955,19 @@ export function bind_select_value(dom, get_value, update) { }); // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated effect(() => { - const value = get_value(); - if (value == null && !mounted) { + let value = get_value(); + select_option(dom, value, mounting); + if (mounting && value === undefined) { /** @type {HTMLOptionElement | null} */ - let selected_option = value === undefined ? dom.querySelector(':checked') : null; - if (selected_option === null) { - dom.value = ''; - // @ts-ignore - dom.__value = null; + let selected_option = dom.querySelector(':checked'); + if (selected_option !== null) { + value = get_option_value(selected_option); + update(value); } - const options = dom.querySelectorAll('option'); - for (const option of options) { - if (get_option_value(option) === value || option.hasAttribute('selected')) { - if (option.disabled) { - option.value = ''; - } - option.selected = true; - selected_option = option; - break; - } - } - if (selected_option != null) { - const non_null_value = get_option_value(selected_option); - update(non_null_value); - if (selected_option.hasAttribute('selected')) { - selected_option.removeAttribute('selected'); - selected_option.selected = true; - } - } - } else { - select_option(dom, value); - // @ts-ignore - dom.__value = value; } - mounted = true; + // @ts-ignore + dom.__value = value; + mounting = false; }); } diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-2/_config.js index b7ddbb7d3c..0590c6eef1 100644 --- a/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-2/_config.js +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-initial-value-undefined-2/_config.js @@ -8,7 +8,7 @@ export default test({ diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/_config.js new file mode 100644 index 0000000000..497364f1be --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/_config.js @@ -0,0 +1,34 @@ +import { ok, test } from '../../test'; + +const items = [{ id: 'a' }, { id: 'b' }]; + +export default test({ + get props() { + return { + /** @type {{ id: string } | null} */ + foo: null, + items + }; + }, + + test({ assert, component, target }) { + const select = target.querySelector('select'); + ok(select); + + const options = target.querySelectorAll('option'); + + assert.equal(options[0].selected, true); + assert.equal(options[1].selected, false); + assert.equal(options[0].value, ''); + + component.foo = items[0]; + assert.equal(options[0].selected, false); + assert.equal(options[1].selected, true); + + component.foo = { id: 'c' }; // doesn't match an option + assert.equal(select.value, ''); + assert.equal(select.selectedIndex, -1); + assert.equal(options[0].selected, false); + assert.equal(options[1].selected, false); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/main.svelte new file mode 100644 index 0000000000..56219ef284 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-select-null-placeholder-2/main.svelte @@ -0,0 +1,11 @@ + + + From 46c572a14da0b6ebfe76dcac8e759d54a6027356 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Tue, 21 Nov 2023 10:54:47 +0100 Subject: [PATCH 05/46] chore: deduplicate --- .../3-transform/client/visitors/template.js | 72 ++++++++----------- 1 file changed, 31 insertions(+), 41 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 5d6d9a02b2..09adde9d27 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 @@ -1076,26 +1076,7 @@ function create_block(parent, name, nodes, context) { } } if (state.update.length > 0) { - let render; - if (state.update.length === 1 && state.update[0].singular) { - render = state.update[0].singular; - } else { - render = b.stmt( - b.call( - '$.render_effect', - b.thunk( - b.block( - state.update.map((n) => { - if (n.init) { - body.push(n.init); - } - return n.grouped; - }) - ) - ) - ) - ); - } + const render = serialize_render_stmt(state, body); if (!update) { update = render; } @@ -1135,6 +1116,35 @@ function create_block(parent, name, nodes, context) { return body; } +/** + * + * @param {import('../types.js').ComponentClientTransformState} state + * @param {import('estree').Statement[]} body + */ +function serialize_render_stmt(state, body) { + let render; + if (state.update.length === 1 && state.update[0].singular) { + render = state.update[0].singular; + } else { + render = b.stmt( + b.call( + '$.render_effect', + b.thunk( + b.block( + state.update.map((n) => { + if (n.init) { + body.push(n.init); + } + return n.grouped; + }) + ) + ) + ) + ); + } + return render; +} + /** * Serializes the event handler function of the `on:` directive * @param {Pick} node @@ -2041,27 +2051,7 @@ export const template_visitors = { } } if (inner_context.state.update.length > 0) { - let render; - if (inner_context.state.update.length === 1 && inner_context.state.update[0].singular) { - render = inner_context.state.update[0].singular; - } else { - render = b.stmt( - b.call( - '$.render_effect', - b.thunk( - b.block( - inner_context.state.update.map((n) => { - if (n.init) { - inner.push(n.init); - } - return n.grouped; - }) - ) - ) - ) - ); - } - inner.push(render); + inner.push(serialize_render_stmt(inner_context.state, inner)); } } inner.push(...inner_context.state.after_update); From da15806136f6124745ebcb8bba119948e5cfa278 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Tue, 21 Nov 2023 11:25:49 +0000 Subject: [PATCH 06/46] fix: adjust event delegation heuristics (#9581) Fixes #9561 --- .changeset/thirty-impalas-repair.md | 5 +++++ .../src/compiler/phases/2-analyze/index.js | 2 +- .../event-handler-mutation-scope/_config.js | 21 +++++++++++++++++++ .../event-handler-mutation-scope/main.svelte | 16 ++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .changeset/thirty-impalas-repair.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/main.svelte diff --git a/.changeset/thirty-impalas-repair.md b/.changeset/thirty-impalas-repair.md new file mode 100644 index 0000000000..9e909e9ed2 --- /dev/null +++ b/.changeset/thirty-impalas-repair.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: adjust event delegation heuristics diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 9878561349..5e8ba056ba 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -175,7 +175,7 @@ function get_delegated_event(node, context) { // Bail-out if we reference anything from the EachBlock (for now) that mutates in non-runes mode, ((!context.state.analysis.runes && binding.kind === 'each') || // or any normal not reactive bindings that are mutated. - (binding.kind === 'normal' && context.state.analysis.runes) || + binding.kind === 'normal' || // or any reactive imports (those are rewritten) (can only happen in legacy mode) (binding.kind === 'state' && binding.declaration_kind === 'import')) && binding.mutated diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/_config.js new file mode 100644 index 0000000000..96e5edba1f --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/_config.js @@ -0,0 +1,21 @@ +import { flushSync } from 'svelte'; +import { ok, test } from '../../test'; + +export default test({ + test({ assert, component, target, window }) { + const button = target.querySelector('button'); + ok(button); + + flushSync(() => { + button.click(); + }); + + assert.deepEqual(component.log, ['1 - 1']); + + flushSync(() => { + button.click(); + }); + + assert.deepEqual(component.log, ['1 - 1', '2 - 2']); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/main.svelte new file mode 100644 index 0000000000..b7b9f3da0c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-mutation-scope/main.svelte @@ -0,0 +1,16 @@ + + + +{referenced_directly} From 1003acccc4a7446dc84d287038fe9c8d0bc12f40 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Tue, 21 Nov 2023 16:52:54 +0100 Subject: [PATCH 07/46] feat: add Snippet type (#9584) * feat: add Snippet type related to #9447 * one more test --- .changeset/shiny-baboons-play.md | 5 +++ .../src/compiler/phases/1-parse/acorn.js | 1 - .../src/compiler/phases/1-parse/ambient.d.ts | 3 ++ packages/svelte/src/main/public.d.ts | 13 ++++++ packages/svelte/tests/types/snippet.ts | 40 +++++++++++++++++++ packages/svelte/tsconfig.json | 2 +- 6 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 .changeset/shiny-baboons-play.md create mode 100644 packages/svelte/src/compiler/phases/1-parse/ambient.d.ts create mode 100644 packages/svelte/tests/types/snippet.ts diff --git a/.changeset/shiny-baboons-play.md b/.changeset/shiny-baboons-play.md new file mode 100644 index 0000000000..dad945f3db --- /dev/null +++ b/.changeset/shiny-baboons-play.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add Snippet type diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index 0db95bdee0..dfc87d90af 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -2,7 +2,6 @@ import * as acorn from 'acorn'; import { walk } from 'zimmerframe'; import { tsPlugin } from 'acorn-typescript'; -// @ts-expect-error const ParserWithTS = acorn.Parser.extend(tsPlugin()); /** diff --git a/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts b/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts new file mode 100644 index 0000000000..8243dd0a42 --- /dev/null +++ b/packages/svelte/src/compiler/phases/1-parse/ambient.d.ts @@ -0,0 +1,3 @@ +// Silence the acorn typescript errors through this ambient type definition + tsconfig.json path alias +// That way we can omit `"skipLibCheck": true` and catch other errors in our d.ts files +declare module 'acorn-typescript'; diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index f6d1ab7571..e98a627342 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -185,6 +185,19 @@ export type ComponentType = (new ( element?: typeof HTMLElement; }; +declare const SnippetReturn: unique symbol; + +/** + * The type of a `#snippet` block. You can use it to (for example) express that your component expects a snippet of a certain type: + * ```ts + * let { banner } = $props<{ banner: Snippet<{ text: string }> }>(); + * ``` + * You can only call a snippet through the `{@render ...}` tag. + */ +export interface Snippet { + (arg: T): typeof SnippetReturn; +} + interface DispatchOptions { cancelable?: boolean; } diff --git a/packages/svelte/tests/types/snippet.ts b/packages/svelte/tests/types/snippet.ts new file mode 100644 index 0000000000..5a1e46c241 --- /dev/null +++ b/packages/svelte/tests/types/snippet.ts @@ -0,0 +1,40 @@ +import type { Snippet } from 'svelte'; + +const return_type: ReturnType = null as any; + +// @ts-expect-error +const a: Snippet<{ text: string }> = () => {}; +// @ts-expect-error +const b: Snippet = (a, b) => { + return return_type; +}; +// @ts-expect-error +const c: Snippet = (a: string) => { + return return_type; +}; +// @ts-expect-error +const d: Snippet = (a: string, b: number) => { + return return_type; +}; +// @ts-expect-error +const e: Snippet = (a: string) => { + return return_type; +}; +const f: Snippet = (a) => { + // @ts-expect-error + a?.x; + return return_type; +}; +const g: Snippet = (a) => { + // @ts-expect-error + a === ''; + a === true; + return return_type; +}; +const h: Snippet<{ a: true }> = (a) => { + a.a === true; + return return_type; +}; +const i: Snippet = () => { + return return_type; +}; diff --git a/packages/svelte/tsconfig.json b/packages/svelte/tsconfig.json index dc8c2d134b..97f8819b28 100644 --- a/packages/svelte/tsconfig.json +++ b/packages/svelte/tsconfig.json @@ -10,12 +10,12 @@ "noErrorTruncation": true, "allowSyntheticDefaultImports": true, "verbatimModuleSyntax": true, - "skipLibCheck": true, "types": ["node"], "strict": true, "allowJs": true, "checkJs": true, "paths": { + "acorn-typescript": ["./src/compiler/phases/1-parse/ambient.d.ts"], "svelte": ["./src/main/public.d.ts"], "svelte/action": ["./src/action/public.d.ts"], "svelte/compiler": ["./src/compiler/public.d.ts"], From d57eff76ed24ae2330f11f3d3938761ae4e14b4b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 21 Nov 2023 11:42:22 -0500 Subject: [PATCH 08/46] generate AST first (#9587) Co-authored-by: Rich Harris --- playgrounds/sandbox/run.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/playgrounds/sandbox/run.js b/playgrounds/sandbox/run.js index be6e7d05bd..e387bd44e7 100644 --- a/playgrounds/sandbox/run.js +++ b/playgrounds/sandbox/run.js @@ -34,22 +34,11 @@ for (const generate of ['client', 'server']) { const input = `${cwd}/input/${file}`; const source = fs.readFileSync(input, 'utf-8'); - const compiled = compile(source, { - filename: input, - generate, - runes: argv.runes - }); - const output_js = `${cwd}/output/${generate}/${file}.js`; const output_css = `${cwd}/output/${generate}/${file}.css`; mkdirp(path.dirname(output_js)); - fs.writeFileSync(output_js, compiled.js.code); - if (compiled.css) { - fs.writeFileSync(output_css, compiled.css.code); - } - if (generate === 'client') { const ast = parse(source, { modern: true @@ -57,6 +46,17 @@ for (const generate of ['client', 'server']) { fs.writeFileSync(`${cwd}/output/${file}.json`, JSON.stringify(ast, null, '\t')); } + + const compiled = compile(source, { + filename: input, + generate, + runes: argv.runes + }); + + fs.writeFileSync(output_js, compiled.js.code); + if (compiled.css) { + fs.writeFileSync(output_css, compiled.css.code); + } } for (const file of js_modules) { From eb0b4dc6e5dab222e907a3c380ca330ebd961ca4 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 22 Nov 2023 15:51:08 +0000 Subject: [PATCH 09/46] chore: add inline new class warning (#9583) * chore: add inline new class warning * Address feedback * address feedback * more tests --- .changeset/cold-birds-own.md | 5 ++++ .../src/compiler/phases/2-analyze/index.js | 14 +++++++---- .../compiler/phases/2-analyze/validation.js | 23 ++++++++++++++++++- packages/svelte/src/compiler/phases/scope.js | 2 +- packages/svelte/src/compiler/warnings.js | 8 +++++++ .../samples/inline-new-class-2/input.svelte | 8 +++++++ .../samples/inline-new-class-2/warnings.json | 14 +++++++++++ .../samples/inline-new-class-3/input.svelte | 8 +++++++ .../samples/inline-new-class-3/warnings.json | 1 + .../samples/inline-new-class-4/input.svelte | 7 ++++++ .../samples/inline-new-class-4/warnings.json | 14 +++++++++++ .../samples/inline-new-class-5/input.svelte | 5 ++++ .../samples/inline-new-class-5/warnings.json | 1 + .../samples/inline-new-class/input.svelte | 5 ++++ .../samples/inline-new-class/warnings.json | 14 +++++++++++ 15 files changed, 122 insertions(+), 7 deletions(-) create mode 100644 .changeset/cold-birds-own.md create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-2/input.svelte create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-3/input.svelte create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-3/warnings.json create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-4/input.svelte create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-5/input.svelte create mode 100644 packages/svelte/tests/validator/samples/inline-new-class-5/warnings.json create mode 100644 packages/svelte/tests/validator/samples/inline-new-class/input.svelte create mode 100644 packages/svelte/tests/validator/samples/inline-new-class/warnings.json diff --git a/.changeset/cold-birds-own.md b/.changeset/cold-birds-own.md new file mode 100644 index 0000000000..043f64b028 --- /dev/null +++ b/.changeset/cold-birds-own.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: add inline new class warning diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 5e8ba056ba..c0c8906acc 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -201,16 +201,20 @@ export function analyze_module(ast, options) { } } + /** @type {import('../types').RawWarning[]} */ + const warnings = []; + + const analysis = { + warnings + }; + walk( /** @type {import('estree').Node} */ (ast), - { scope }, + { scope, analysis }, // @ts-expect-error TODO clean this mess up merge(set_scope(scopes), validation_runes_js, runes_scope_js_tweaker) ); - /** @type {import('../types').RawWarning[]} */ - const warnings = []; - // If we are in runes mode, then check for possible misuses of state runes for (const [, scope] of scopes) { for (const [name, binding] of scope.declarations) { @@ -608,7 +612,7 @@ const legacy_scope_tweaker = { } }; -/** @type {import('zimmerframe').Visitors} */ +/** @type {import('zimmerframe').Visitors} */ const runes_scope_js_tweaker = { VariableDeclarator(node, { state }) { if (node.init?.type !== 'CallExpression') return; diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 2cf0129c53..8f8b8670b5 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -579,6 +579,26 @@ export const validation_runes_js = { ...context.state, private_derived_state }); + }, + NewExpression(node, context) { + const callee = node.callee; + + const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null; + const is_module = context.state.ast_type === 'module'; + // In modules, we allow top-level module scope only, in components, we allow the component scope, + // which is function_depth of 1. With the exception of `new class` which is also not allowed at + // component scope level either. + const allowed_depth = is_module ? 0 : 1; + + if ( + (callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) || + (binding !== null && + binding.initial !== null && + binding.initial.type === 'ClassDeclaration' && + binding.scope.function_depth > allowed_depth) + ) { + warn(context.state.analysis.warnings, node, context.path, 'inline-new-class'); + } } }; @@ -721,5 +741,6 @@ export const validation_runes = merge(validation, a11y_validators, { } } }, - ClassBody: validation_runes_js.ClassBody + ClassBody: validation_runes_js.ClassBody, + NewExpression: validation_runes_js.NewExpression }); diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 22e5d3eaec..8d7cb526d4 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -437,7 +437,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { SwitchStatement: create_block_scope, ClassDeclaration(node, { state, next }) { - if (node.id) state.scope.declare(node.id, 'normal', 'const'); + if (node.id) state.scope.declare(node.id, 'normal', 'const', node); next(); }, diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 0d0f0dd487..f0981281fe 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -191,12 +191,20 @@ const state = { `State referenced in its own scope will never update. Did you mean to reference it inside a closure?` }; +/** @satisfies {Warnings} */ +const performance = { + 'inline-new-class': () => + `Creating inline classes will likely cause performance issues. ` + + `Instead, declare the class at the module-level and create new instances from the class reference.` +}; + /** @satisfies {Warnings} */ const warnings = { ...css, ...attributes, ...runes, ...a11y, + ...performance, ...state }; diff --git a/packages/svelte/tests/validator/samples/inline-new-class-2/input.svelte b/packages/svelte/tests/validator/samples/inline-new-class-2/input.svelte new file mode 100644 index 0000000000..34d31cd74b --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-2/input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json new file mode 100644 index 0000000000..6ae8014ab5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "inline-new-class", + "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "start": { + "column": 12, + "line": 6 + }, + "end": { + "column": 21, + "line": 6 + } + } +] diff --git a/packages/svelte/tests/validator/samples/inline-new-class-3/input.svelte b/packages/svelte/tests/validator/samples/inline-new-class-3/input.svelte new file mode 100644 index 0000000000..1dd632df0e --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-3/input.svelte @@ -0,0 +1,8 @@ + diff --git a/packages/svelte/tests/validator/samples/inline-new-class-3/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-3/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-3/warnings.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/inline-new-class-4/input.svelte b/packages/svelte/tests/validator/samples/inline-new-class-4/input.svelte new file mode 100644 index 0000000000..1f8a6f5ba5 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-4/input.svelte @@ -0,0 +1,7 @@ + diff --git a/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json new file mode 100644 index 0000000000..7a3637a4a3 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "inline-new-class", + "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "start": { + "column": 12, + "line": 3 + }, + "end": { + "column": 3, + "line": 5 + } + } +] diff --git a/packages/svelte/tests/validator/samples/inline-new-class-5/input.svelte b/packages/svelte/tests/validator/samples/inline-new-class-5/input.svelte new file mode 100644 index 0000000000..817bb29310 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-5/input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/validator/samples/inline-new-class-5/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-5/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class-5/warnings.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/inline-new-class/input.svelte b/packages/svelte/tests/validator/samples/inline-new-class/input.svelte new file mode 100644 index 0000000000..10a29724ff --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class/input.svelte @@ -0,0 +1,5 @@ + diff --git a/packages/svelte/tests/validator/samples/inline-new-class/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class/warnings.json new file mode 100644 index 0000000000..d57fc6ca15 --- /dev/null +++ b/packages/svelte/tests/validator/samples/inline-new-class/warnings.json @@ -0,0 +1,14 @@ +[ + { + "code": "inline-new-class", + "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "start": { + "column": 11, + "line": 2 + }, + "end": { + "column": 2, + "line": 4 + } + } +] From fe9c0bc19d43b6cb88833baaf19af65b933be185 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 12:54:34 -0500 Subject: [PATCH 10/46] chore: bump esrap (#9590) --- .changeset/new-boats-wait.md | 5 +++++ packages/svelte/package.json | 2 +- pnpm-lock.yaml | 22 +++++++++++----------- sites/svelte-5-preview/package.json | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) create mode 100644 .changeset/new-boats-wait.md diff --git a/.changeset/new-boats-wait.md b/.changeset/new-boats-wait.md new file mode 100644 index 0000000000..b7f09bd0a6 --- /dev/null +++ b/.changeset/new-boats-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: bump esrap diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ab15840702..51a8071895 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -120,7 +120,7 @@ "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", - "esrap": "^1.1.1", + "esrap": "^1.2.0", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1f8c91fd0..625aaeaaf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,7 +13,7 @@ importers: version: 2.26.2 '@sveltejs/eslint-config': specifier: ^6.0.4 - version: 6.0.4(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.35.0)(eslint-plugin-unicorn@49.0.0)(eslint@8.53.0)(typescript@5.2.2) + version: 6.0.4(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.35.1)(eslint-plugin-unicorn@49.0.0)(eslint@8.53.0)(typescript@5.2.2) '@svitejs/changesets-changelog-github-compact': specifier: ^1.1.0 version: 1.1.0 @@ -81,8 +81,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 esrap: - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.2.0 + version: 1.2.0 is-reference: specifier: ^3.0.1 version: 3.0.2 @@ -261,8 +261,8 @@ importers: specifier: ^5.0.1 version: 5.0.2 esrap: - specifier: ^1.1.1 - version: 1.1.1 + specifier: ^1.2.0 + version: 1.2.0 publint: specifier: ^0.2.0 version: 0.2.5 @@ -2188,7 +2188,7 @@ packages: - supports-color dev: true - /@sveltejs/eslint-config@6.0.4(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.35.0)(eslint-plugin-unicorn@49.0.0)(eslint@8.53.0)(typescript@5.2.2): + /@sveltejs/eslint-config@6.0.4(@typescript-eslint/eslint-plugin@5.62.0)(@typescript-eslint/parser@5.62.0)(eslint-config-prettier@9.0.0)(eslint-plugin-svelte@2.35.1)(eslint-plugin-unicorn@49.0.0)(eslint@8.53.0)(typescript@5.2.2): resolution: {integrity: sha512-U9pwmDs+DbmsnCgTfu6Bacdwqn0DuI1IQNSiQqTgzVyYfaaj+zy9ZoQCiJfxFBGXHkklyXuRHp0KMx346N0lcQ==} peerDependencies: '@typescript-eslint/eslint-plugin': '>= 5' @@ -2203,7 +2203,7 @@ packages: '@typescript-eslint/parser': 5.62.0(eslint@8.53.0)(typescript@5.2.2) eslint: 8.53.0 eslint-config-prettier: 9.0.0(eslint@8.53.0) - eslint-plugin-svelte: 2.35.0(eslint@8.53.0)(svelte@packages+svelte) + eslint-plugin-svelte: 2.35.1(eslint@8.53.0)(svelte@packages+svelte) eslint-plugin-unicorn: 49.0.0(eslint@8.53.0) typescript: 5.2.2 dev: true @@ -3911,8 +3911,8 @@ packages: '@types/estree': 1.0.5 dev: true - /eslint-plugin-svelte@2.35.0(eslint@8.53.0)(svelte@packages+svelte): - resolution: {integrity: sha512-3WDFxNrkXaMlpqoNo3M1ZOQuoFLMO9+bdnN6oVVXaydXC7nzCJuGy9a0zqoNDHMSRPYt0Rqo6hIdHMEaI5sQnw==} + /eslint-plugin-svelte@2.35.1(eslint@8.53.0)(svelte@packages+svelte): + resolution: {integrity: sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0-0 @@ -4056,8 +4056,8 @@ packages: estraverse: 5.3.0 dev: true - /esrap@1.1.1: - resolution: {integrity: sha512-PIgHGLP8VAG4Iao4CbOc+/5tgn+TpzGhyAuVCR5qgcFgPOUk9Ds61bH7hD2lbjDuu86lagofx3lVrRFWcIF+Gg==} + /esrap@1.2.0: + resolution: {integrity: sha512-ZWd00MnkN45Hcj+nIV5FPiZk6Nx7RDUL8G0KGp1cg1xSKXOEac0IhMQdsrGswXB1O0fMzH+MXq288Lt0Abpg/Q==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 '@types/estree': 1.0.5 diff --git a/sites/svelte-5-preview/package.json b/sites/svelte-5-preview/package.json index 0a50a57768..eaf3aecbb1 100644 --- a/sites/svelte-5-preview/package.json +++ b/sites/svelte-5-preview/package.json @@ -18,7 +18,7 @@ "@sveltejs/kit": "^1.22.5", "@sveltejs/site-kit": "6.0.0-next.51", "@types/marked": "^5.0.1", - "esrap": "^1.1.1", + "esrap": "^1.2.0", "marked": "^9.0.0", "publint": "^0.2.0", "shiki": "^0.14.4", From 13aef5245b1d7d79d64c4a2995f3549757143281 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 13:17:01 -0500 Subject: [PATCH 11/46] put nested class warning on the declaration, not the usage site (#9592) Co-authored-by: Rich Harris --- .../compiler/phases/2-analyze/validation.js | 25 ++++++++----------- packages/svelte/src/compiler/warnings.js | 6 ++--- .../samples/inline-new-class-2/warnings.json | 12 ++++----- .../samples/inline-new-class-4/warnings.json | 4 +-- .../samples/inline-new-class/warnings.json | 4 +-- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 8f8b8670b5..d87f4219a3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -580,24 +580,19 @@ export const validation_runes_js = { private_derived_state }); }, - NewExpression(node, context) { - const callee = node.callee; - - const binding = callee.type === 'Identifier' ? context.state.scope.get(callee.name) : null; - const is_module = context.state.ast_type === 'module'; + ClassDeclaration(node, context) { // In modules, we allow top-level module scope only, in components, we allow the component scope, // which is function_depth of 1. With the exception of `new class` which is also not allowed at // component scope level either. - const allowed_depth = is_module ? 0 : 1; + const allowed_depth = context.state.ast_type === 'module' ? 0 : 1; - if ( - (callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) || - (binding !== null && - binding.initial !== null && - binding.initial.type === 'ClassDeclaration' && - binding.scope.function_depth > allowed_depth) - ) { - warn(context.state.analysis.warnings, node, context.path, 'inline-new-class'); + if (context.state.scope.function_depth > allowed_depth) { + warn(context.state.analysis.warnings, node, context.path, 'avoid-nested-class'); + } + }, + NewExpression(node, context) { + if (node.callee.type === 'ClassExpression' && context.state.scope.function_depth > 0) { + warn(context.state.analysis.warnings, node, context.path, 'avoid-inline-class'); } } }; @@ -741,6 +736,8 @@ export const validation_runes = merge(validation, a11y_validators, { } } }, + // TODO this is a code smell. need to refactor this stuff ClassBody: validation_runes_js.ClassBody, + ClassDeclaration: validation_runes_js.ClassDeclaration, NewExpression: validation_runes_js.NewExpression }); diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index f0981281fe..7103f4162c 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -193,9 +193,9 @@ const state = { /** @satisfies {Warnings} */ const performance = { - 'inline-new-class': () => - `Creating inline classes will likely cause performance issues. ` + - `Instead, declare the class at the module-level and create new instances from the class reference.` + 'avoid-inline-class': () => + `Avoid 'new class' — instead, declare the class at the top level scope`, + 'avoid-nested-class': () => `Avoid declaring classes below the top level scope` }; /** @satisfies {Warnings} */ diff --git a/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json index 6ae8014ab5..6956314377 100644 --- a/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json +++ b/packages/svelte/tests/validator/samples/inline-new-class-2/warnings.json @@ -1,14 +1,14 @@ [ { - "code": "inline-new-class", - "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "code": "avoid-nested-class", + "message": "Avoid declaring classes below the top level scope", "start": { - "column": 12, - "line": 6 + "column": 2, + "line": 3 }, "end": { - "column": 21, - "line": 6 + "column": 3, + "line": 5 } } ] diff --git a/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json index 7a3637a4a3..71beba537f 100644 --- a/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json +++ b/packages/svelte/tests/validator/samples/inline-new-class-4/warnings.json @@ -1,7 +1,7 @@ [ { - "code": "inline-new-class", - "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "code": "avoid-inline-class", + "message": "Avoid 'new class' — instead, declare the class at the top level scope", "start": { "column": 12, "line": 3 diff --git a/packages/svelte/tests/validator/samples/inline-new-class/warnings.json b/packages/svelte/tests/validator/samples/inline-new-class/warnings.json index d57fc6ca15..6995428bee 100644 --- a/packages/svelte/tests/validator/samples/inline-new-class/warnings.json +++ b/packages/svelte/tests/validator/samples/inline-new-class/warnings.json @@ -1,7 +1,7 @@ [ { - "code": "inline-new-class", - "message": "Creating inline classes will likely cause performance issues. Instead, declare the class at the module-level and create new instances from the class reference.", + "code": "avoid-inline-class", + "message": "Avoid 'new class' — instead, declare the class at the top level scope", "start": { "column": 11, "line": 2 From cf9130613d8c77ad00899102fa778f6cee20c80a Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Wed, 22 Nov 2023 18:56:49 +0000 Subject: [PATCH 12/46] feat: add $effect.active rune (#9591) * chore: add $effect.active rune * chore: add $effect.active rune --- .changeset/popular-mangos-rest.md | 5 +++++ .../compiler/phases/2-analyze/validation.js | 11 ++++++++++ .../client/visitors/javascript-runes.js | 16 ++++++++++++++- .../3-transform/client/visitors/template.js | 4 +++- .../3-transform/server/transform-server.js | 20 +++++++++++++++++-- .../svelte/src/internal/client/runtime.js | 7 +++++++ packages/svelte/src/internal/index.js | 3 ++- .../samples/effect-active/_config.js | 15 ++++++++++++++ .../samples/effect-active/main.svelte | 11 ++++++++++ 9 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 .changeset/popular-mangos-rest.md create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-active/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/effect-active/main.svelte diff --git a/.changeset/popular-mangos-rest.md b/.changeset/popular-mangos-rest.md new file mode 100644 index 0000000000..4293bf3ba0 --- /dev/null +++ b/.changeset/popular-mangos-rest.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add $effect.active rune diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index d87f4219a3..4147afa697 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -504,6 +504,17 @@ function validate_call_expression(node, scope, path) { } if (rune === '$effect') { + const callee = node.callee; + if ( + callee.type === 'MemberExpression' && + callee.property.type === 'Identifier' && + callee.property.name === 'active' + ) { + if (node.arguments.length !== 0) { + error(node, 'invalid-rune-args-length', '$effect.active', [0]); + } + return; + } if (parent.type !== 'ExpressionStatement') { error(node, 'invalid-effect-location'); } 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 b59526c948..4182d400eb 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 @@ -135,7 +135,7 @@ export const javascript_visitors_runes = { for (const declarator of node.declarations) { const init = declarator.init; const rune = get_rune(init, state.scope); - if (!rune) { + if (!rune || rune === '$effect') { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); state.hoisted.push( @@ -291,5 +291,19 @@ export const javascript_visitors_runes = { } context.next(); + }, + CallExpression(node, { state, next }) { + const rune = get_rune(node, state.scope); + const callee = node.callee; + + if ( + rune === '$effect' && + callee.type === 'MemberExpression' && + callee.property.type === 'Identifier' && + callee.property.name === 'active' + ) { + return b.call('$.effect_active'); + } + next(); } }; 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 09adde9d27..8bea4b1d24 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 @@ -35,6 +35,7 @@ import { EACH_KEYED } from '../../../../../constants.js'; import { regex_is_valid_identifier } from '../../../patterns.js'; +import { javascript_visitors_runes } from './javascript-runes.js'; /** * Serializes each style directive into something like `$.style(element, style_property, value)` @@ -2921,5 +2922,6 @@ export const template_visitors = { ...context.state, node: b.id('$.document') }); - } + }, + CallExpression: javascript_visitors_runes.CallExpression }; 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 e008f7c4c9..cba81fa1ae 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 @@ -550,7 +550,7 @@ const javascript_visitors_runes = { for (const declarator of node.declarations) { const rune = get_rune(declarator.init, state.scope); - if (!rune) { + if (!rune || rune === '$effect') { declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); continue; } @@ -604,6 +604,20 @@ const javascript_visitors_runes = { } } context.next(); + }, + CallExpression(node, { state, next }) { + const rune = get_rune(node, state.scope); + const callee = node.callee; + + if ( + rune === '$effect' && + callee.type === 'MemberExpression' && + callee.property.type === 'Identifier' && + callee.property.name === 'active' + ) { + return b.literal(false); + } + next(); } }; @@ -1579,7 +1593,9 @@ const template_visitors = { b.stmt(b.call('$.head', b.id('$$payload'), b.arrow([b.id('$$payload')], b.block(body)))) ) ); - } + }, + // @ts-ignore: need to extract this out somehow + CallExpression: javascript_visitors_runes.CallExpression }; /** diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index a082adda2e..57ee1ac24f 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1143,6 +1143,13 @@ function internal_create_effect(type, init, sync, block, schedule) { return signal; } +/** + * @returns {boolean} + */ +export function effect_active() { + return current_effect ? (current_effect.f & MANAGED) === 0 : false; +} + /** * @param {() => void | (() => void)} init * @returns {import('./types.js').EffectSignal} diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index a0796cd37d..be6459b7df 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -35,7 +35,8 @@ export { onDestroy, pop, push, - reactive_import + reactive_import, + effect_active } from './client/runtime.js'; export * from './client/validate.js'; diff --git a/packages/svelte/tests/runtime-runes/samples/effect-active/_config.js b/packages/svelte/tests/runtime-runes/samples/effect-active/_config.js new file mode 100644 index 0000000000..69ba5793b8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-active/_config.js @@ -0,0 +1,15 @@ +import { test } from '../../test'; + +export default test({ + ssrHtml: ` +

false

+

false

+

false

+ `, + + html: ` +

false

+

true

+

true

+ ` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/effect-active/main.svelte b/packages/svelte/tests/runtime-runes/samples/effect-active/main.svelte new file mode 100644 index 0000000000..a65a355eef --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/effect-active/main.svelte @@ -0,0 +1,11 @@ + + +

{foo}

+

{bar}

+

{$effect.active()}

From 1f4ae5ef451fc369a80dc784ea62ced0a83c34ce Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 14:10:12 -0500 Subject: [PATCH 13/46] chore: remove some unused code (#9593) Co-authored-by: Rich Harris --- .changeset/two-falcons-buy.md | 5 +++ .../src/compiler/phases/1-parse/acorn.js | 36 +++++++------------ .../3-transform/client/visitors/template.js | 8 ++--- .../src/compiler/phases/3-transform/index.js | 12 ++----- .../samples/javascript-comments/output.json | 12 +++---- 5 files changed, 27 insertions(+), 46 deletions(-) create mode 100644 .changeset/two-falcons-buy.md diff --git a/.changeset/two-falcons-buy.md b/.changeset/two-falcons-buy.md new file mode 100644 index 0000000000..c1a8e069f6 --- /dev/null +++ b/.changeset/two-falcons-buy.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: remove unused code diff --git a/packages/svelte/src/compiler/phases/1-parse/acorn.js b/packages/svelte/src/compiler/phases/1-parse/acorn.js index dfc87d90af..ad23d47d6b 100644 --- a/packages/svelte/src/compiler/phases/1-parse/acorn.js +++ b/packages/svelte/src/compiler/phases/1-parse/acorn.js @@ -58,7 +58,6 @@ export function get_comment_handlers(source) { * @typedef {import('estree').Comment & { * start: number; * end: number; - * has_trailing_newline?: boolean * }} CommentWithLocation */ @@ -91,35 +90,26 @@ export function get_comment_handlers(source) { add_comments(ast) { if (comments.length === 0) return; - walk( - ast, - {}, - { - _(node, { next }) { - let comment; + walk(ast, null, { + _(node, { next }) { + let comment; - while (comments[0] && comments[0].start < node.start) { - comment = /** @type {CommentWithLocation} */ (comments.shift()); - - const next = comments[0] || node; - comment.has_trailing_newline = - comment.type === 'Line' || /\n/.test(source.slice(comment.end, next.start)); - - (node.leadingComments ||= []).push(comment); - } + while (comments[0] && comments[0].start < node.start) { + comment = /** @type {CommentWithLocation} */ (comments.shift()); + (node.leadingComments ||= []).push(comment); + } - next(); + next(); - if (comments[0]) { - const slice = source.slice(node.end, comments[0].start); + if (comments[0]) { + const slice = source.slice(node.end, comments[0].start); - if (/^[,) \t]*$/.test(slice)) { - node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())]; - } + if (/^[,) \t]*$/.test(slice)) { + node.trailingComments = [/** @type {CommentWithLocation} */ (comments.shift())]; } } } - ); + }); } }; } 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 8bea4b1d24..207bc6faf0 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 @@ -1087,9 +1087,7 @@ function create_block(parent, name, nodes, context) { /** @type {import('estree').Statement} */ (update).leadingComments = [ { type: 'Block', - value: ` Update `, - // @ts-expect-error - has_trailing_newline: true + value: ` Update ` } ]; } @@ -1107,9 +1105,7 @@ function create_block(parent, name, nodes, context) { body[0].leadingComments = [ { type: 'Block', - value: ` Init `, - // @ts-expect-error - has_trailing_newline: true + value: ` Init ` } ]; } diff --git a/packages/svelte/src/compiler/phases/3-transform/index.js b/packages/svelte/src/compiler/phases/3-transform/index.js index 66459d60d6..af8de96d70 100644 --- a/packages/svelte/src/compiler/phases/3-transform/index.js +++ b/packages/svelte/src/compiler/phases/3-transform/index.js @@ -32,15 +32,11 @@ export function transform_component(analysis, source, options) { program.body[0].leadingComments = [ { type: 'Line', - value: ` ${basename} (Svelte v${VERSION})`, - // @ts-ignore - has_trailing_newline: true + value: ` ${basename} (Svelte v${VERSION})` }, { type: 'Line', - value: ' Note: compiler output will change before 5.0 is released!', - // @ts-ignore - has_trailing_newline: true + value: ' Note: compiler output will change before 5.0 is released!' } ]; } @@ -86,9 +82,7 @@ export function transform_module(analysis, source, options) { program.body[0].leadingComments = [ { type: 'Block', - value: ` ${basename} generated by Svelte v${VERSION} `, - // @ts-ignore - has_trailing_newline: true + value: ` ${basename} generated by Svelte v${VERSION} ` } ]; } diff --git a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json index 9ec95cfaae..951cf05ff8 100644 --- a/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json +++ b/packages/svelte/tests/parser-legacy/samples/javascript-comments/output.json @@ -109,8 +109,7 @@ "type": "Block", "value": " another comment ", "start": 163, - "end": 184, - "has_trailing_newline": true + "end": 184 } ] } @@ -121,8 +120,7 @@ "type": "Line", "value": " comment", "start": 141, - "end": 151, - "has_trailing_newline": true + "end": 151 } ] } @@ -222,8 +220,7 @@ "type": "Line", "value": " a leading comment", "start": 10, - "end": 30, - "has_trailing_newline": true + "end": 30 } ], "trailingComments": [ @@ -290,8 +287,7 @@ "type": "Block", "value": "* a comment ", "start": 72, - "end": 88, - "has_trailing_newline": true + "end": 88 } ] } From c837771ca64ff7ec56107404e473a69fb1b48545 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 14:28:19 -0500 Subject: [PATCH 14/46] chore: better `get_rune` helper (#9594) * chore: add $effect.active rune * chore: add $effect.active rune * WIP * allow get_rune to return a keypath * oops --------- Co-authored-by: Dominic Gannaway Co-authored-by: Rich Harris --- .../compiler/phases/2-analyze/validation.js | 21 ++++++--------- .../client/visitors/javascript-runes.js | 13 +++------- .../3-transform/server/transform-server.js | 11 +++----- .../svelte/src/compiler/phases/constants.js | 2 +- packages/svelte/src/compiler/phases/scope.js | 26 +++++++++++++++---- 5 files changed, 37 insertions(+), 36 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 4147afa697..805f673974 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -503,24 +503,19 @@ function validate_call_expression(node, scope, path) { error(node, rune === '$derived' ? 'invalid-derived-location' : 'invalid-state-location'); } - if (rune === '$effect') { - const callee = node.callee; - if ( - callee.type === 'MemberExpression' && - callee.property.type === 'Identifier' && - callee.property.name === 'active' - ) { - if (node.arguments.length !== 0) { - error(node, 'invalid-rune-args-length', '$effect.active', [0]); - } - return; - } + if (rune === '$effect' || rune === '$effect.pre') { if (parent.type !== 'ExpressionStatement') { error(node, 'invalid-effect-location'); } if (node.arguments.length !== 1) { - error(node, 'invalid-rune-args-length', '$effect', [1]); + error(node, 'invalid-rune-args-length', rune, [1]); + } + } + + if (rune === '$effect.active') { + if (node.arguments.length !== 0) { + error(node, 'invalid-rune-args-length', '$effect.active', [0]); } } } 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 4182d400eb..1aa78b14af 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 @@ -135,7 +135,7 @@ export const javascript_visitors_runes = { for (const declarator of node.declarations) { const init = declarator.init; const rune = get_rune(init, state.scope); - if (!rune || rune === '$effect') { + if (!rune || rune === '$effect.active') { if (init != null && is_hoistable_function(init)) { const hoistable_function = visit(init); state.hoisted.push( @@ -294,16 +294,11 @@ export const javascript_visitors_runes = { }, CallExpression(node, { state, next }) { const rune = get_rune(node, state.scope); - const callee = node.callee; - - if ( - rune === '$effect' && - callee.type === 'MemberExpression' && - callee.property.type === 'Identifier' && - callee.property.name === 'active' - ) { + + if (rune === '$effect.active') { return b.call('$.effect_active'); } + next(); } }; 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 cba81fa1ae..78356d8f5f 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 @@ -550,7 +550,7 @@ const javascript_visitors_runes = { for (const declarator of node.declarations) { const rune = get_rune(declarator.init, state.scope); - if (!rune || rune === '$effect') { + if (!rune || rune === '$effect.active') { declarations.push(/** @type {import('estree').VariableDeclarator} */ (visit(declarator))); continue; } @@ -607,16 +607,11 @@ const javascript_visitors_runes = { }, CallExpression(node, { state, next }) { const rune = get_rune(node, state.scope); - const callee = node.callee; - if ( - rune === '$effect' && - callee.type === 'MemberExpression' && - callee.property.type === 'Identifier' && - callee.property.name === 'active' - ) { + if (rune === '$effect.active') { return b.literal(false); } + next(); } }; diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index b0fbe2ecd8..394168ea53 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -110,7 +110,7 @@ export const ElementBindings = [ 'indeterminate' ]; -export const Runes = ['$state', '$props', '$derived', '$effect']; +export const Runes = ['$state', '$props', '$derived', '$effect', '$effect.pre', '$effect.active']; /** * Whitespace inside one of these elements will not result in diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 8d7cb526d4..302e182c95 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -668,15 +668,31 @@ export function set_scope(scopes) { /** * Returns the name of the rune if the given expression is a `CallExpression` using a rune. - * @param {import('estree').Expression | null | undefined} node + * @param {import('estree').Node | null | undefined} node * @param {Scope} scope */ export function get_rune(node, scope) { - const callee = get_callee_name(node); - if (callee === null || !Runes.includes(callee)) return null; + if (!node) return null; + if (node.type !== 'CallExpression') return null; - const binding = scope.get(callee); + let n = node.callee; + + let joined = ''; + + while (n.type === 'MemberExpression') { + if (n.computed) return null; + if (n.property.type !== 'Identifier') return null; + joined = '.' + n.property.name + joined; + n = n.object; + } + + if (n.type !== 'Identifier') return null; + + joined = n.name + joined; + if (!Runes.includes(joined)) return null; + + const binding = scope.get(n.name); if (binding !== null) return null; // rune name, but references a variable or store - return callee; + return joined; } From f40efb2027fca454aff409466433f25d1bffbf02 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 15:00:24 -0500 Subject: [PATCH 15/46] `$effect.active` docs (#9598) * docs: $effect.active * link --------- Co-authored-by: Rich Harris --- .../src/routes/docs/content/01-api/02-runes.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index d8da797c7e..47017d1a09 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -168,6 +168,24 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca Previously, you would have used `beforeUpdate`, which — like `afterUpdate` — is deprecated in Svelte 5. +## `$effect.active` + +The `$effect.active` rune is an advanced feature that tells you whether or not the code is running inside an effect or inside your template ([demo](/#H4sIAAAAAAAAE3XP0QrCMAwF0F-JRXAD595rLfgdzodRUyl0bVgzQcb-3VYFQfExl5tDMgvrPCYhT7MI_YBCiiOR2Aq-UxnSDT1jnlOcRlMSlczoiHUXOjYxpOhx5-O12rgAJg4UAwaGhDyR3Gxhjdai4V1v2N2wqus9tC3Y3ifMQjbehaqq4aBhLtEv_Or893icCsdLve-Caj8nBkU67zMO5HtGCfM3sKiWNKhV0zwVaBqd3x3ixVmHFyFLuJyXB-moOe8pAQAA)): + +```svelte + + +

in template: {$effect.active()}

+``` + +This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. + ## `$props` To declare component props, use the `$props` rune: From 13c6c273c60b5aa7dca89d22d8152eb9ee3d2eaf Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 22 Nov 2023 21:56:47 +0100 Subject: [PATCH 16/46] fix: prevent false positives when detecting runes mode (#9599) Move references from module scope to instance scope if we determined that these references are store subscriptions fixes #9580 --- .changeset/fair-crabs-check.md | 5 +++++ packages/svelte/src/compiler/phases/2-analyze/index.js | 8 +++++++- .../samples/store-looking-like-rune/input.svelte | 8 ++++++++ .../samples/store-looking-like-rune/warnings.json | 1 + .../validator/samples/store-runes-conflict/warnings.json | 8 ++++---- 5 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 .changeset/fair-crabs-check.md create mode 100644 packages/svelte/tests/validator/samples/store-looking-like-rune/input.svelte create mode 100644 packages/svelte/tests/validator/samples/store-looking-like-rune/warnings.json diff --git a/.changeset/fair-crabs-check.md b/.changeset/fair-crabs-check.md new file mode 100644 index 0000000000..fa51ebf2d5 --- /dev/null +++ b/.changeset/fair-crabs-check.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent false positives when detecting runes mode diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index c0c8906acc..2920a92610 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -284,7 +284,11 @@ export function analyze_component(root, options) { if (declaration === null && /[a-z]/.test(store_name[0])) { error(references[0].node, 'illegal-global', name); } else if (declaration !== null && Runes.includes(name)) { - warn(warnings, declaration.node, [], 'store-with-rune-name', store_name); + for (const { node, path } of references) { + if (path.at(-1)?.type === 'CallExpression') { + warn(warnings, node, [], 'store-with-rune-name', store_name); + } + } } } @@ -302,6 +306,8 @@ export function analyze_component(root, options) { const binding = instance.scope.declare(b.id(name), 'store_sub', 'synthetic'); binding.references = references; + instance.scope.references.set(name, references); + module.scope.references.delete(name); } } diff --git a/packages/svelte/tests/validator/samples/store-looking-like-rune/input.svelte b/packages/svelte/tests/validator/samples/store-looking-like-rune/input.svelte new file mode 100644 index 0000000000..d5d0f76942 --- /dev/null +++ b/packages/svelte/tests/validator/samples/store-looking-like-rune/input.svelte @@ -0,0 +1,8 @@ + + +
{$state}
diff --git a/packages/svelte/tests/validator/samples/store-looking-like-rune/warnings.json b/packages/svelte/tests/validator/samples/store-looking-like-rune/warnings.json new file mode 100644 index 0000000000..fe51488c70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/store-looking-like-rune/warnings.json @@ -0,0 +1 @@ +[] diff --git a/packages/svelte/tests/validator/samples/store-runes-conflict/warnings.json b/packages/svelte/tests/validator/samples/store-runes-conflict/warnings.json index 534a5e1983..58236d814b 100644 --- a/packages/svelte/tests/validator/samples/store-runes-conflict/warnings.json +++ b/packages/svelte/tests/validator/samples/store-runes-conflict/warnings.json @@ -3,12 +3,12 @@ "code": "store-with-rune-name", "message": "It looks like you're using the $state rune, but there is a local binding called state. Referencing a local variable with a $ prefix will create a store subscription. Please rename state to avoid the ambiguity.", "start": { - "column": 7, - "line": 2 + "column": 1, + "line": 3 }, "end": { - "column": 12, - "line": 2 + "column": 7, + "line": 3 } } ] From 5f6c5ca8aef3c9f963a3b8c435c2121a8c8f40d0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Wed, 22 Nov 2023 17:34:31 -0500 Subject: [PATCH 17/46] chore: remove get_callee_name (#9595) no longer necessary with the enhanced get_rune function --- .../svelte/src/compiler/phases/2-analyze/index.js | 7 +------ packages/svelte/src/compiler/phases/scope.js | 6 +----- packages/svelte/src/compiler/utils/ast.js | 13 ------------- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 2920a92610..68f3ac599f 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -5,7 +5,6 @@ import * as assert from '../../utils/assert.js'; import { extract_identifiers, extract_paths, - get_callee_name, is_event_attribute, is_text_attribute, object @@ -268,11 +267,7 @@ export function analyze_component(root, options) { !Runes.includes(name) || (declaration !== null && // const state = $state(0) is valid - !Runes.includes( - /** @type {string} */ ( - get_callee_name(/** @type {import('estree').Expression} */ (declaration.initial)) - ) - ) && + get_rune(declaration.initial, instance.scope) === null && // allow `import { derived } from 'svelte/store'` in the same file as `const x = $derived(..)` because one is not a subscription to the other !( name === '$derived' && diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 302e182c95..95c192d4d4 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -3,11 +3,7 @@ import { walk } from 'zimmerframe'; import { is_element_node } from './nodes.js'; import * as b from '../utils/builders.js'; import { error } from '../errors.js'; -import { - extract_identifiers, - extract_identifiers_from_expression, - get_callee_name -} from '../utils/ast.js'; +import { extract_identifiers, extract_identifiers_from_expression } from '../utils/ast.js'; import { Runes } from './constants.js'; export class Scope { diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 416c223584..cf8b859bfc 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -20,19 +20,6 @@ export function object(expression) { return expression; } -/** - * Returns the name of callee if the given expression is a call expression. - * @param {import('estree').Expression | null | undefined} node - */ -export function get_callee_name(node) { - if (!node) return null; - if (node.type !== 'CallExpression') return null; - if (node.callee.type !== 'Identifier' && node.callee.type !== 'MemberExpression') return null; - - const id = object(node.callee); - return id === null ? null : id.name; -} - /** * Returns true if the attribute contains a single static text node. * @param {import('#compiler').Attribute} attribute From 88f1a8f62e25a889c3985ae8e733801cc647ae4e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Wed, 22 Nov 2023 23:43:17 +0100 Subject: [PATCH 18/46] chore: better error message for not using snippet type (#9602) language tools checks that a function passed to `{@render ..}` is returning a type that adheres to the `Snippet` return type. When it does not, the error message is pretty useless without this additional info text. --- packages/svelte/src/main/public.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index e98a627342..6851d3f1d7 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -195,7 +195,9 @@ declare const SnippetReturn: unique symbol; * You can only call a snippet through the `{@render ...}` tag. */ export interface Snippet { - (arg: T): typeof SnippetReturn; + (arg: T): typeof SnippetReturn & { + _: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"'; + }; } interface DispatchOptions { From d83bd7f7c18b2cf5b3652bab74f7527d21602f9b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 22 Nov 2023 17:44:32 -0500 Subject: [PATCH 19/46] Version Packages (next) (#9576) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 9 +++++++++ packages/svelte/CHANGELOG.md | 22 ++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 70c2299955..0a230cc4ba 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -14,15 +14,18 @@ "brown-spoons-boil", "chilly-dolphins-lick", "clean-eels-beg", + "cold-birds-own", "cuddly-pianos-drop", "curly-lizards-dream", "early-ads-tie", "eight-steaks-shout", "eighty-bikes-camp", + "fair-crabs-check", "fifty-steaks-float", "fresh-weeks-trade", "funny-wombats-argue", "good-pianos-jump", + "green-hounds-play", "honest-icons-change", "hungry-dots-fry", "itchy-lions-wash", @@ -32,12 +35,15 @@ "long-crews-return", "lucky-schools-hang", "moody-owls-cry", + "new-boats-wait", "odd-needles-joke", "poor-eggs-enjoy", + "popular-mangos-rest", "quiet-camels-mate", "rich-sheep-burn", "rotten-buckets-develop", "sharp-gorillas-impress", + "shiny-baboons-play", "shiny-shrimps-march", "small-papayas-laugh", "soft-geese-learn", @@ -46,7 +52,10 @@ "tall-shrimps-worry", "thirty-flowers-sit", "thirty-ghosts-fix", + "thirty-impalas-repair", "tiny-kings-whisper", + "two-falcons-buy", + "wicked-clouds-exercise", "wicked-doors-train" ] } diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index d883faef69..03f8c7b584 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,27 @@ # svelte +## 5.0.0-next.10 + +### Patch Changes + +- chore: add inline new class warning ([#9583](https://github.com/sveltejs/svelte/pull/9583)) + +- fix: prevent false positives when detecting runes mode ([#9599](https://github.com/sveltejs/svelte/pull/9599)) + +- fix: deconflict generated names against globals ([#9570](https://github.com/sveltejs/svelte/pull/9570)) + +- chore: bump esrap ([#9590](https://github.com/sveltejs/svelte/pull/9590)) + +- feat: add $effect.active rune ([#9591](https://github.com/sveltejs/svelte/pull/9591)) + +- feat: add Snippet type ([#9584](https://github.com/sveltejs/svelte/pull/9584)) + +- fix: adjust event delegation heuristics ([#9581](https://github.com/sveltejs/svelte/pull/9581)) + +- chore: remove unused code ([#9593](https://github.com/sveltejs/svelte/pull/9593)) + +- fix: adjust regex ([#9572](https://github.com/sveltejs/svelte/pull/9572)) + ## 5.0.0-next.9 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 51a8071895..2f577c7ea4 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.9", + "version": "5.0.0-next.10", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index a081a8b896..7c1b4c063b 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.9'; +export const VERSION = '5.0.0-next.10'; export const PUBLIC_VERSION = '5'; From 0283e50070f3fc551fe7c764efba3a4eae691e96 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 23 Nov 2023 03:47:19 -0500 Subject: [PATCH 20/46] chore: reuse common templates (#9601) #9589 - add comment and space as reusable templates to save a few bytes. We can definitely take this idea further, but this is a base to iterate from. --------- Co-authored-by: Rich Harris Co-authored-by: Simon Holthausen --- .changeset/few-mugs-fail.md | 5 ++ .../3-transform/client/visitors/template.js | 85 +++++++++++++------ packages/svelte/src/internal/client/render.js | 26 +++++- .../_expected/client/index.svelte.js | 12 +-- 4 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 .changeset/few-mugs-fail.md diff --git a/.changeset/few-mugs-fail.md b/.changeset/few-mugs-fail.md new file mode 100644 index 0000000000..b3a4a611e0 --- /dev/null +++ b/.changeset/few-mugs-fail.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: reuse common templates diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 207bc6faf0..3108216242 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 @@ -974,9 +974,6 @@ function create_block(parent, name, nodes, context) { /** @type {import('estree').Statement | undefined} */ let close = undefined; - /** @type {import('estree').Identifier | undefined} */ - let id = undefined; - /** @type {import('../types').ComponentClientTransformState} */ const state = { ...context.state, @@ -999,7 +996,7 @@ function create_block(parent, name, nodes, context) { if (is_single_element) { const element = /** @type {import('#compiler').RegularElement} */ (trimmed[0]); - id = b.id(context.state.scope.generate(element.name)); + const id = b.id(context.state.scope.generate(element.name)); context.visit(element, { ...state, @@ -1014,7 +1011,7 @@ function create_block(parent, name, nodes, context) { body.push( b.var( - id.name, + id, b.call( '$.open', b.id('$$anchor'), @@ -1028,15 +1025,30 @@ function create_block(parent, name, nodes, context) { } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); body.push(...state.init); - } else { - id = b.id(context.state.scope.generate('fragment')); + } else if (trimmed.length > 0) { + const id = b.id(context.state.scope.generate('fragment')); + const node_id = b.id(context.state.scope.generate('node')); - process_children(trimmed, b.call('$.child_frag', id), { + process_children(trimmed, node_id, { ...context, state }); - if (state.template.length > 0) { + const template = state.template[0]; + + if (state.template.length === 1 && (template === ' ' || template === '')) { + if (template === ' ') { + body.push(b.var(node_id, b.call('$.space', b.id('$$anchor'))), ...state.init); + close = b.stmt(b.call('$.close', b.id('$$anchor'), node_id)); + } else { + body.push( + b.var(id, b.call('$.comment', b.id('$$anchor'))), + b.var(node_id, b.call('$.child_frag', id)), + ...state.init + ); + close = b.stmt(b.call('$.close_frag', b.id('$$anchor'), id)); + } + } else { const callee = namespace === 'svg' ? '$.svg_template' : '$.template'; state.hoisted.push( @@ -1048,7 +1060,7 @@ function create_block(parent, name, nodes, context) { body.push( b.var( - id.name, + id, b.call( '$.open_frag', b.id('$$anchor'), @@ -1056,12 +1068,14 @@ function create_block(parent, name, nodes, context) { template_name ) ), + b.var(node_id, b.call('$.child_frag', id)), ...state.init ); + close = b.stmt(b.call('$.close_frag', b.id('$$anchor'), id)); - } else { - body.push(...state.init); } + } else { + body.push(...state.init); } if (state.update.length > 0 || state.update_effects.length > 0) { @@ -1359,13 +1373,11 @@ function process_children(nodes, parent, { visit, state }) { state.template.push(' '); - const name = state.scope.generate('text'); - state.init.push(b.var(name, expression)); - + const text_id = get_node_id(expression, state, 'text'); const singular = b.stmt( b.call( '$.text_effect', - b.id(name), + text_id, b.thunk(/** @type {import('estree').Expression} */ (visit(node.expression))) ) ); @@ -1378,7 +1390,7 @@ function process_children(nodes, parent, { visit, state }) { grouped: b.stmt( b.call( '$.text', - b.id(name), + text_id, /** @type {import('estree').Expression} */ (visit(node.expression)) ) ) @@ -1388,7 +1400,7 @@ function process_children(nodes, parent, { visit, state }) { b.stmt( b.assignment( '=', - b.id(`${name}.nodeValue`), + b.member(text_id, b.id('nodeValue')), b.call( '$.stringify', /** @type {import('estree').Expression} */ (visit(node.expression)) @@ -1403,17 +1415,16 @@ function process_children(nodes, parent, { visit, state }) { state.template.push(' '); - const name = state.scope.generate('text'); + const text_id = get_node_id(expression, state, 'text'); const contains_call_expression = sequence.some( (n) => n.type === 'ExpressionTag' && n.metadata.contains_call_expression ); - state.init.push(b.var(name, expression)); const assignment = serialize_template_literal(sequence, visit, state)[1]; - const init = b.stmt(b.assignment('=', b.id(`${name}.nodeValue`), assignment)); + const init = b.stmt(b.assignment('=', b.member(text_id, b.id('nodeValue')), assignment)); const singular = b.stmt( b.call( '$.text_effect', - b.id(name), + text_id, b.thunk(serialize_template_literal(sequence, visit, state)[1]) ) ); @@ -1426,13 +1437,13 @@ function process_children(nodes, parent, { visit, state }) { ) { state.update.push({ singular, - grouped: b.stmt(b.call('$.text', b.id(name), assignment)) + grouped: b.stmt(b.call('$.text', text_id, assignment)) }); } else { state.init.push(init); } - expression = b.call('$.sibling', b.id(name)); + expression = b.call('$.sibling', text_id); } for (let i = 0; i < nodes.length; i += 1) { @@ -1456,9 +1467,6 @@ function process_children(nodes, parent, { visit, state }) { // get hoisted inside clean_nodes? visit(node, state); } else { - const name = state.scope.generate(node.type === 'RegularElement' ? node.name : 'node'); - const id = b.id(name); - // Optimization path for each blocks. If the parent isn't a fragment and it only has // a single child, then we can classify the block as being "controlled". if ( @@ -1471,7 +1479,12 @@ function process_children(nodes, parent, { visit, state }) { node.metadata.is_controlled = true; visit(node, state); } else { - state.init.push(b.var(name, expression)); + const id = get_node_id( + expression, + state, + node.type === 'RegularElement' ? node.name : 'node' + ); + expression = b.call('$.sibling', id); visit(node, { @@ -1488,6 +1501,22 @@ function process_children(nodes, parent, { visit, state }) { } } +/** + * @param {import('estree').Expression} expression + * @param {import('../types.js').ComponentClientTransformState} state + * @param {string} name + */ +function get_node_id(expression, state, name) { + let id = expression; + + if (id.type !== 'Identifier') { + id = b.id(state.scope.generate(name)); + + state.init.push(b.var(id, expression)); + } + return id; +} + /** * @param {true | Array} attribute_value * @param {import('../types').ComponentContext} context diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 804840d6c9..f32e83ed1d 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2,6 +2,7 @@ import { DEV } from 'esm-env'; import { append_child, child, + child_frag, clone_node, create_element, init_operations, @@ -133,7 +134,7 @@ export function svg_replace(node) { * @param {boolean} is_fragment * @param {boolean} use_clone_node * @param {null | Text | Comment | Element} anchor - * @param {() => Element} [template_element_fn] + * @param {() => Node} [template_element_fn] * @returns {Element | DocumentFragment | Node[]} */ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) { @@ -156,7 +157,7 @@ function open_template(is_fragment, use_clone_node, anchor, template_element_fn) /** * @param {null | Text | Comment | Element} anchor * @param {boolean} use_clone_node - * @param {() => Element} [template_element_fn] + * @param {() => Node} [template_element_fn] * @returns {Element | DocumentFragment | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ @@ -167,7 +168,7 @@ export function open(anchor, use_clone_node, template_element_fn) { /** * @param {null | Text | Comment | Element} anchor * @param {boolean} use_clone_node - * @param {() => Element} [template_element_fn] + * @param {() => Node} [template_element_fn] * @returns {Element | DocumentFragment | Node[]} */ /*#__NO_SIDE_EFFECTS__*/ @@ -175,6 +176,25 @@ export function open_frag(anchor, use_clone_node, template_element_fn) { return open_template(true, use_clone_node, anchor, template_element_fn); } +const space_template = template(' ', false); +const comment_template = template('', true); + +/** + * @param {null | Text | Comment | Element} anchor + */ +/*#__NO_SIDE_EFFECTS__*/ +export function space(anchor) { + return open(anchor, true, space_template); +} + +/** + * @param {null | Text | Comment | Element} anchor + */ +/*#__NO_SIDE_EFFECTS__*/ +export function comment(anchor) { + return open_frag(anchor, true, comment_template); +} + /** * @param {Element | Text} dom * @param {boolean} is_fragment 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 3f92e76526..fb455beeff 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 @@ -3,9 +3,6 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal"; -var Button_default = $.template(` `, true); -var frag = $.template(``, true); - export default function Function_prop_no_getter($$anchor, $$props) { $.push($$props, true); @@ -16,7 +13,7 @@ export default function Function_prop_no_getter($$anchor, $$props) { } /* Init */ - var fragment = $.open_frag($$anchor, true, frag); + var fragment = $.comment($$anchor); var node = $.child_frag(fragment); Button(node, { @@ -24,12 +21,11 @@ export default function Function_prop_no_getter($$anchor, $$props) { onmouseup, children: ($$anchor, $$slotProps) => { /* Init */ - var fragment_1 = $.open_frag($$anchor, true, Button_default); - var text = $.child_frag(fragment_1); + var node_1 = $.space($$anchor); /* Update */ - $.text_effect(text, () => `clicks: ${$.stringify($.get(count))}`); - $.close_frag($$anchor, fragment_1); + $.text_effect(node_1, () => `clicks: ${$.stringify($.get(count))}`); + $.close($$anchor, node_1); } }); From 8118efd115d7ca19b5cae17970cc4db16b844701 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Nov 2023 10:00:39 +0000 Subject: [PATCH 21/46] fix: handle undefined bubble events (#9614) Fixes #9610 --- .changeset/forty-comics-invent.md | 5 +++++ packages/svelte/src/internal/client/runtime.js | 2 +- .../samples/event-handler-undefined/_config.js | 14 ++++++++++++++ .../samples/event-handler-undefined/main.svelte | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 .changeset/forty-comics-invent.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/main.svelte diff --git a/.changeset/forty-comics-invent.md b/.changeset/forty-comics-invent.md new file mode 100644 index 0000000000..e507f3621d --- /dev/null +++ b/.changeset/forty-comics-invent.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: handle undefined bubble events diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 57ee1ac24f..db5cf7be30 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1530,7 +1530,7 @@ export function bubble_event($$props, event) { const events = /** @type {Record} */ (unwrap($$props).$$events)?.[ event.type ]; - const callbacks = is_array(events) ? events.slice() : [events]; + const callbacks = is_array(events) ? events.slice() : events == null ? [] : [events]; let fn; for (fn of callbacks) { // Preserve "this" context diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/_config.js b/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/_config.js new file mode 100644 index 0000000000..662164af47 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/_config.js @@ -0,0 +1,14 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + async test({ assert, target }) { + const input = target.querySelector('input'); + + flushSync(() => { + input?.click(); + }); + + assert.htmlEqual(target.innerHTML, ``); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/main.svelte b/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/main.svelte new file mode 100644 index 0000000000..770c17901c --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/event-handler-undefined/main.svelte @@ -0,0 +1 @@ + From c22ebffb00391e35270a5049da0c61591b23b886 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Nov 2023 12:07:31 +0000 Subject: [PATCH 22/46] chore: untrack keyed validation logic (#9618) * untrack validation logic * chore: untrack keyed validation logic --- .changeset/odd-schools-wait.md | 5 +++++ packages/svelte/src/internal/client/validate.js | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/odd-schools-wait.md diff --git a/.changeset/odd-schools-wait.md b/.changeset/odd-schools-wait.md new file mode 100644 index 0000000000..11fe1bdbac --- /dev/null +++ b/.changeset/odd-schools-wait.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: untrack keyed validation logic diff --git a/packages/svelte/src/internal/client/validate.js b/packages/svelte/src/internal/client/validate.js index 831ccd9014..084d51d8f5 100644 --- a/packages/svelte/src/internal/client/validate.js +++ b/packages/svelte/src/internal/client/validate.js @@ -1,3 +1,4 @@ +import { untrack } from './runtime.js'; import { is_array } from './utils.js'; /** regex of all html void element names */ @@ -69,7 +70,7 @@ export function validate_dynamic_element_tag(tag_fn) { */ export function validate_each_keys(collection, key_fn) { const keys = new Map(); - const maybe_array = collection(); + const maybe_array = untrack(() => collection()); const array = is_array(maybe_array) ? maybe_array : maybe_array == null From 509f92d29ed4d273a79daf613c93c8b5e85e9408 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Nov 2023 13:42:22 +0000 Subject: [PATCH 23/46] fix: correctly assign bind:this with multiples (#9617) * fix: correctly assign bind:this with multiples * better fix * better fix * lint * lint * Update packages/svelte/src/internal/client/render.js --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/dirty-tips-add.md | 5 ++++ .../3-transform/client/visitors/template.js | 6 +++-- packages/svelte/src/internal/client/render.js | 17 ++++++++---- .../samples/binding-this-multiple/_config.js | 25 +++++++++++++++++ .../samples/binding-this-multiple/main.svelte | 27 +++++++++++++++++++ 5 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 .changeset/dirty-tips-add.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte diff --git a/.changeset/dirty-tips-add.md b/.changeset/dirty-tips-add.md new file mode 100644 index 0000000000..305dbd1b16 --- /dev/null +++ b/.changeset/dirty-tips-add.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: correct bind this multiple bindings 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 3108216242..718d43235a 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 @@ -891,6 +891,7 @@ function serialize_inline_component(node, component_name, context) { if (bind_this !== null) { const prev = fn; const assignment = b.assignment('=', bind_this, b.id('$$value')); + const bind_this_id = bind_this; fn = (node_id) => b.call( '$.bind_this', @@ -898,7 +899,8 @@ function serialize_inline_component(node, component_name, context) { b.arrow( [b.id('$$value')], serialize_set_binding(assignment, context, () => context.visit(assignment)) - ) + ), + bind_this_id ); } @@ -2620,7 +2622,7 @@ export const template_visitors = { } case 'this': - call_expr = b.call(`$.bind_this`, state.node, setter); + call_expr = b.call(`$.bind_this`, state.node, setter, node.expression); break; case 'textContent': diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index f32e83ed1d..a20d6126a3 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2,7 +2,6 @@ import { DEV } from 'esm-env'; import { append_child, child, - child_frag, clone_node, create_element, init_operations, @@ -61,7 +60,8 @@ import { push, current_component_context, pop, - schedule_task + schedule_task, + managed_render_effect } from './runtime.js'; import { current_hydration_fragment, @@ -1225,14 +1225,21 @@ export function bind_prop(props, prop, value) { /** * @param {Element} element_or_component * @param {(value: unknown) => void} update + * @param {import('./types.js').MaybeSignal} binding * @returns {void} */ -export function bind_this(element_or_component, update) { +export function bind_this(element_or_component, update, binding) { untrack(() => { update(element_or_component); render_effect(() => () => { - untrack(() => { - update(null); + // Defer to the next tick so that all updates can be reconciled first. + // This solves the case where one variable is shared across multiple this-bindings. + render_effect(() => { + untrack(() => { + if (!is_signal(binding) || binding.v === element_or_component) { + update(null); + } + }); }); }); }); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js new file mode 100644 index 0000000000..07356ad6fd --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/_config.js @@ -0,0 +1,25 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + test({ assert, component, target }) { + const [b1, b2, b3] = target.querySelectorAll('button'); + const first_h1 = target.querySelector('h1'); + + assert.deepEqual(component.log, [undefined, first_h1]); + + flushSync(() => { + b3.click(); + }); + + const third_h1 = target.querySelector('h1'); + + assert.deepEqual(component.log, [undefined, first_h1, third_h1]); + + flushSync(() => { + b1.click(); + }); + + assert.deepEqual(component.log, [undefined, first_h1, third_h1, target.querySelector('h1')]); + } +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte new file mode 100644 index 0000000000..bde2158c51 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/binding-this-multiple/main.svelte @@ -0,0 +1,27 @@ + + +
+
+ + + +
+
+ {#if activeTab === 0} +

Tab 1

+ {/if} + {#if activeTab === 1} +

Tab 2

+ {/if} + {#if activeTab === 2} +

Tab 3

+ {/if} +
+ +
From d8d9be39d303619e6c7aaf402735527f1fef24a0 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Nov 2023 14:55:06 +0000 Subject: [PATCH 24/46] fix: apply correct snippet scope (#9623) --- .changeset/serious-zebras-scream.md | 5 +++++ packages/svelte/src/compiler/phases/scope.js | 1 + .../samples/snippet-scope/Component.svelte | 7 +++++++ .../runtime-runes/samples/snippet-scope/_config.js | 5 +++++ .../runtime-runes/samples/snippet-scope/main.svelte | 13 +++++++++++++ 5 files changed, 31 insertions(+) create mode 100644 .changeset/serious-zebras-scream.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-scope/Component.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-scope/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-scope/main.svelte diff --git a/.changeset/serious-zebras-scream.md b/.changeset/serious-zebras-scream.md new file mode 100644 index 0000000000..9efd62e5b5 --- /dev/null +++ b/.changeset/serious-zebras-scream.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: ensure snippets have correct scope diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 95c192d4d4..6c73002d5b 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -557,6 +557,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }, SnippetBlock(node, context) { + const state = context.state; // Special-case for root-level snippets: they become part of the instance scope const is_top_level = !context.path.at(-2); let scope = state.scope; diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-scope/Component.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-scope/Component.svelte new file mode 100644 index 0000000000..79a2ed25e9 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-scope/Component.svelte @@ -0,0 +1,7 @@ + + +{#if inner} + {@render inner()} +{/if} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-scope/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-scope/_config.js new file mode 100644 index 0000000000..6f04c6270d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-scope/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: `

S

S

v

v

e

e

l

l

t

t

e

e` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-scope/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-scope/main.svelte new file mode 100644 index 0000000000..737d2943e3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-scope/main.svelte @@ -0,0 +1,13 @@ + + +{#each name.split('') as character} +

{character}

+ + {#snippet inner()} + {character} + {/snippet} + +{/each} From c7121aa38c26f5808a61767e4358ca87cfced2c4 Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Fri, 24 Nov 2023 00:03:32 +0900 Subject: [PATCH 25/46] feat: add type of `$effect.active` (#9624) --- .changeset/dirty-garlics-design.md | 5 +++++ packages/svelte/src/main/ambient.d.ts | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 .changeset/dirty-garlics-design.md diff --git a/.changeset/dirty-garlics-design.md b/.changeset/dirty-garlics-design.md new file mode 100644 index 0000000000..525160b762 --- /dev/null +++ b/.changeset/dirty-garlics-design.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: add type of `$effect.active` diff --git a/packages/svelte/src/main/ambient.d.ts b/packages/svelte/src/main/ambient.d.ts index d80e8e4323..f32f1863e9 100644 --- a/packages/svelte/src/main/ambient.d.ts +++ b/packages/svelte/src/main/ambient.d.ts @@ -68,6 +68,28 @@ declare namespace $effect { * @param fn The function to execute */ export function pre(fn: () => void | (() => void)): void; + + /** + * The `$effect.active` rune is an advanced feature that tells you whether or not the code is running inside an effect or inside your template. + * + * Example: + * ```svelte + * + * + *

in template: {$effect.active()}

+ * ``` + * + * This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects. + * + * https://svelte-5-preview.vercel.app/docs/runes#$effect-active + */ + export function active(): boolean; } /** From 7825570b074906453f03be4ead8fd558264834c2 Mon Sep 17 00:00:00 2001 From: Yuichiro Yamashita Date: Fri, 24 Nov 2023 00:49:02 +0900 Subject: [PATCH 26/46] fix typos (#9622) --- .../src/routes/docs/content/01-api/02-runes.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 47017d1a09..7c3ee0953a 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -51,11 +51,11 @@ Derived state is declared with the `$derived` rune: ```diff +

{count} doubled is {doubled}

@@ -92,13 +92,13 @@ To run code whenever specific values change, or when a component is mounted to t ```diff

{count} doubled is {doubled}

@@ -158,7 +158,7 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca
- {#each message as message} + {#each messages as message}

{message}

{/each}
From ef68b66dee1ebc126118e5b837c091e110999c81 Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Thu, 23 Nov 2023 16:19:24 +0000 Subject: [PATCH 27/46] fix: improve member expression mutation logic (#9625) * fix: improve member expression mutation logic * cleanup * Update .changeset/moody-frogs-exist.md --------- Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com> --- .changeset/moody-frogs-exist.md | 5 +++++ .../3-transform/client/visitors/template.js | 16 ++++------------ .../samples/member-mutation/_config.js | 19 +++++++++++++++++++ .../samples/member-mutation/main.svelte | 15 +++++++++++++++ 4 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 .changeset/moody-frogs-exist.md create mode 100644 packages/svelte/tests/runtime-runes/samples/member-mutation/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/member-mutation/main.svelte diff --git a/.changeset/moody-frogs-exist.md b/.changeset/moody-frogs-exist.md new file mode 100644 index 0000000000..c9eb0c6790 --- /dev/null +++ b/.changeset/moody-frogs-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve member expression mutation logic 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 718d43235a..d18c93e54d 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 @@ -2170,17 +2170,15 @@ export const template_visitors = { /** * @param {import('estree').Pattern} expression_for_id - * @param {import('estree').Expression} expression_for_other * @returns {import('#compiler').Binding['mutation']} */ - const create_mutation = (expression_for_id, expression_for_other) => { + const create_mutation = (expression_for_id) => { return (assignment, context) => { if (assignment.left.type !== 'Identifier' && assignment.left.type !== 'MemberExpression') { // serialize_set_binding turns other patterns into IIFEs and separates the assignments // into separate expressions, at which point this is called again with an identifier or member expression return serialize_set_binding(assignment, context, () => assignment); } - const left = object(assignment.left); const value = get_assignment_value(assignment, context); const invalidate = b.call( @@ -2193,11 +2191,7 @@ export const template_visitors = { return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); } else { const original_left = /** @type {import('estree').MemberExpression} */ (assignment.left); - const left = b.member( - expression_for_other, - context.visit(original_left).property, - original_left.computed - ); + const left = context.visit(original_left); const assign = b.assignment(assignment.operator, left, value); return context.state.analysis.runes ? assign : b.sequence([assign, invalidate]); } @@ -2223,8 +2217,7 @@ export const template_visitors = { each_node_meta.array_name ? b.call(each_node_meta.array_name) : collection, index, true - ), - binding.expression + ) ); } else { const unwrapped = binding.expression; @@ -2252,8 +2245,7 @@ export const template_visitors = { binding.expression = b.call(name); binding.mutation = create_mutation( - /** @type {import('estree').Pattern} */ (path.update_expression(unwrapped)), - binding.expression + /** @type {import('estree').Pattern} */ (path.update_expression(unwrapped)) ); } } diff --git a/packages/svelte/tests/runtime-runes/samples/member-mutation/_config.js b/packages/svelte/tests/runtime-runes/samples/member-mutation/_config.js new file mode 100644 index 0000000000..8c2b03e0bf --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/member-mutation/_config.js @@ -0,0 +1,19 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

JSON output

[{"name":{"first":"rob"}}]
`, + + async test({ assert, target }) { + const button = target.querySelector('button'); + + flushSync(() => { + button?.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

JSON output

[{"name":{"first":"dave"}}]
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/member-mutation/main.svelte b/packages/svelte/tests/runtime-runes/samples/member-mutation/main.svelte new file mode 100644 index 0000000000..8d7932f3f7 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/member-mutation/main.svelte @@ -0,0 +1,15 @@ + + +{#each people as person} + +{/each} + +

JSON output

+{#each people as person} +
{JSON.stringify(people)}
+{/each} From 72d3a2a8ce5dcfea1685aa650079b1cd15dcebf2 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:26:19 +0100 Subject: [PATCH 28/46] fix: better attribute casing logic (#9626) - don't lowercase attributes on svg and custom element elements, fixes #9605 - better lowercasing + property alias checking for spreads, fixes #9305 --- .changeset/seven-deers-jam.md | 5 ++ .../3-transform/client/visitors/template.js | 66 +++++++++++-------- .../3-transform/server/transform-server.js | 51 ++++++++++++-- .../svelte/src/compiler/phases/constants.js | 44 +------------ packages/svelte/src/constants.js | 45 +++++++++++++ packages/svelte/src/internal/client/render.js | 53 +++++++++------ packages/svelte/src/internal/server/index.js | 38 +++-------- .../attribute-spread-casing/_config.js | 29 ++++++++ .../attribute-spread-casing/main.svelte | 18 +++++ .../_expected/client/main.svelte.js | 48 ++++++++++++++ .../_expected/server/main.svelte.js | 14 ++++ .../dynamic-attributes-casing/main.svelte | 14 ++++ 12 files changed, 296 insertions(+), 129 deletions(-) create mode 100644 .changeset/seven-deers-jam.md create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/main.svelte create mode 100644 packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js create mode 100644 packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/main.svelte diff --git a/.changeset/seven-deers-jam.md b/.changeset/seven-deers-jam.md new file mode 100644 index 0000000000..cdb0c8d16b --- /dev/null +++ b/.changeset/seven-deers-jam.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: better attribute casing logic 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 d18c93e54d..68328e5453 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 @@ -12,13 +12,7 @@ import { escape_html, infer_namespace } from '../../utils.js'; -import { - AttributeAliases, - DOMBooleanAttributes, - DOMProperties, - PassiveEvents, - VoidElements -} from '../../../constants.js'; +import { DOMProperties, PassiveEvents, VoidElements } from '../../../constants.js'; import { is_custom_element_node, is_element_node } from '../../../nodes.js'; import * as b from '../../../../utils/builders.js'; import { error } from '../../../../errors.js'; @@ -29,6 +23,8 @@ import { serialize_set_binding } from '../utils.js'; import { + AttributeAliases, + DOMBooleanAttributes, EACH_INDEX_REACTIVE, EACH_IS_CONTROLLED, EACH_ITEM_REACTIVE, @@ -37,6 +33,26 @@ import { import { regex_is_valid_identifier } from '../../../patterns.js'; import { javascript_visitors_runes } from './javascript-runes.js'; +/** + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element + * @param {import('#compiler').Attribute} attribute + * @param {{ state: { metadata: { namespace: import('#compiler').Namespace }}}} context + */ +function get_attribute_name(element, attribute, context) { + let name = attribute.name; + if ( + element.type === 'RegularElement' && + !element.metadata.svg && + context.state.metadata.namespace !== 'foreign' + ) { + name = name.toLowerCase(); + if (name in AttributeAliases) { + name = AttributeAliases[name]; + } + } + return name; +} + /** * Serializes each style directive into something like `$.style(element, style_property, value)` * and adds it either to init or update, depending on whether or not the value or the attributes are dynamic. @@ -259,10 +275,11 @@ function setup_select_synchronization(value_binding, context) { * Returns the id of the spread_attribute variable if spread is deemed reactive, `null` otherwise. * @param {Array} attributes * @param {import('../types.js').ComponentContext} context + * @param {import('#compiler').RegularElement} element * @param {import('estree').Identifier} element_id * @returns {string | null} */ -function serialize_element_spread_attributes(attributes, context, element_id) { +function serialize_element_spread_attributes(attributes, context, element, element_id) { let is_reactive = false; /** @type {import('estree').Expression[]} */ @@ -270,7 +287,7 @@ function serialize_element_spread_attributes(attributes, context, element_id) { for (const attribute of attributes) { if (attribute.type === 'Attribute') { - const name = get_attribute_name(attribute, context.state); + const name = get_attribute_name(element, attribute, context); // TODO: handle contains_call_expression const [, value] = serialize_attribute_value(attribute.value, context); values.push(b.object([b.init(name, value)])); @@ -281,6 +298,9 @@ function serialize_element_spread_attributes(attributes, context, element_id) { is_reactive ||= attribute.metadata.dynamic; } + const lowercase_attributes = + element.metadata.svg || is_custom_element_node(element) ? b.false : b.true; + if (is_reactive) { const id = context.state.scope.generate('spread_attributes'); context.state.init.push(b.let(id, undefined)); @@ -294,6 +314,7 @@ function serialize_element_spread_attributes(attributes, context, element_id) { element_id, b.id(id), b.array(values), + lowercase_attributes, b.literal(context.state.analysis.stylesheet.id) ) ) @@ -308,6 +329,7 @@ function serialize_element_spread_attributes(attributes, context, element_id) { element_id, b.literal(null), b.array(values), + lowercase_attributes, b.literal(context.state.analysis.stylesheet.id) ) ) @@ -398,14 +420,15 @@ function serialize_dynamic_element_spread_attributes(attributes, context, elemen * }); * ``` * Returns true if attribute is deemed reactive, false otherwise. + * @param {import('#compiler').RegularElement} element * @param {import('estree').Identifier} node_id * @param {import('#compiler').Attribute} attribute * @param {import('../types.js').ComponentContext} context * @returns {boolean} */ -function serialize_element_attribute_update_assignment(node_id, attribute, context) { +function serialize_element_attribute_update_assignment(element, node_id, attribute, context) { const state = context.state; - const name = get_attribute_name(attribute, state); + const name = get_attribute_name(element, attribute, context); let [contains_call_expression, value] = serialize_attribute_value(attribute.value, context); // The foreign namespace doesn't have any special handling, everything goes through the attr function @@ -672,21 +695,6 @@ function collect_parent_each_blocks(context) { ); } -/** - * @param {import('#compiler').Attribute} attribute - * @param {import('../types.js').ComponentClientTransformState} state - */ -function get_attribute_name(attribute, state) { - let name = attribute.name; - if (state.metadata.namespace !== 'foreign') { - name = name.toLowerCase(); - if (name !== 'class' && name in AttributeAliases) { - name = AttributeAliases[name]; - } - } - return name; -} - /** * @param {import('#compiler').Component | import('#compiler').SvelteComponent | import('#compiler').SvelteSelf} node * @param {string} component_name @@ -1899,7 +1907,7 @@ export const template_visitors = { // Then do attributes let is_attributes_reactive = false; if (node.metadata.has_spread) { - const spread_id = serialize_element_spread_attributes(attributes, context, node_id); + const spread_id = serialize_element_spread_attributes(attributes, context, node, node_id); if (child_metadata.namespace !== 'foreign') { add_select_to_spread_update(spread_id, node, context, node_id); } @@ -1920,7 +1928,7 @@ export const template_visitors = { attribute.name !== 'autofocus' && (attribute.value === true || is_text_attribute(attribute)) ) { - const name = get_attribute_name(attribute, context.state); + const name = get_attribute_name(node, attribute, context); const literal_value = /** @type {import('estree').Literal} */ ( serialize_attribute_value(attribute.value, context)[1] ).value; @@ -1941,7 +1949,7 @@ export const template_visitors = { const is = is_custom_element && child_metadata.namespace !== 'foreign' ? serialize_custom_element_attribute_update_assignment(node_id, attribute, context) - : serialize_element_attribute_update_assignment(node_id, attribute, context); + : serialize_element_attribute_update_assignment(node, node_id, attribute, context); if (is) is_attributes_reactive = true; } } 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 78356d8f5f..505ddeaed1 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 @@ -5,7 +5,6 @@ import * as b from '../../../utils/builders.js'; import is_reference from 'is-reference'; import { ContentEditableBindings, - DOMBooleanAttributes, VoidElements, WhitespaceInsensitiveAttributes } from '../../constants.js'; @@ -15,11 +14,12 @@ import { escape_html, infer_namespace } from '../utils.js'; -import { create_attribute, is_element_node } from '../../nodes.js'; +import { create_attribute, is_custom_element_node, is_element_node } from '../../nodes.js'; import { error } from '../../../errors.js'; import { binding_properties } from '../../bindings.js'; import { regex_starts_with_newline, regex_whitespaces_strict } from '../../patterns.js'; import { remove_types } from '../typescript.js'; +import { DOMBooleanAttributes } from '../../../../constants.js'; /** * @param {string} value @@ -471,6 +471,25 @@ function serialize_set_binding(node, context, fallback) { return fallback(); } +/** + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element + * @param {import('#compiler').Attribute} attribute + * @param {{ state: { metadata: { namespace: import('#compiler').Namespace }}}} context + */ +function get_attribute_name(element, attribute, context) { + let name = attribute.name; + if ( + element.type === 'RegularElement' && + !element.metadata.svg && + context.state.metadata.namespace !== 'foreign' + ) { + name = name.toLowerCase(); + // don't lookup boolean aliases here, the server runtime function does only + // check for the lowercase variants of boolean attributes + } + return name; +} + /** @type {import('./types').Visitors} */ const global_visitors = { Identifier(node, { path, state }) { @@ -690,12 +709,14 @@ function serialize_attribute_value( /** * + * @param {import('#compiler').RegularElement | import('#compiler').SvelteElement} element * @param {Array} attributes * @param {import('#compiler').StyleDirective[]} style_directives * @param {import('#compiler').ClassDirective[]} class_directives * @param {import('./types').ComponentContext} context */ function serialize_element_spread_attributes( + element, attributes, style_directives, class_directives, @@ -706,7 +727,7 @@ function serialize_element_spread_attributes( for (const attribute of attributes) { if (attribute.type === 'Attribute') { - const name = attribute.name.toLowerCase(); + const name = get_attribute_name(element, attribute, context); const value = serialize_attribute_value( attribute.value, context, @@ -718,8 +739,18 @@ function serialize_element_spread_attributes( } } + const lowercase_attributes = + element.type !== 'RegularElement' || element.metadata.svg || is_custom_element_node(element) + ? b.false + : b.true; + const is_svg = element.type === 'RegularElement' && element.metadata.svg ? b.true : b.false; /** @type {import('estree').Expression[]} */ - const args = [b.array(values), b.literal(context.state.analysis.stylesheet.id)]; + const args = [ + b.array(values), + lowercase_attributes, + is_svg, + b.literal(context.state.analysis.stylesheet.id) + ]; if (style_directives.length > 0 || class_directives.length > 0) { const styles = style_directives.map((directive) => @@ -1760,11 +1791,17 @@ function serialize_element_attributes(node, context) { context.state.init.push(...lets); if (has_spread) { - serialize_element_spread_attributes(attributes, style_directives, class_directives, context); + serialize_element_spread_attributes( + node, + attributes, + style_directives, + class_directives, + context + ); } else { for (const attribute of /** @type {import('#compiler').Attribute[]} */ (attributes)) { if (attribute.value === true || is_text_attribute(attribute)) { - const name = attribute.name.toLowerCase(); + const name = get_attribute_name(node, attribute, context); const literal_value = /** @type {import('estree').Literal} */ ( serialize_attribute_value( attribute.value, @@ -1786,7 +1823,7 @@ function serialize_element_attributes(node, context) { continue; } - const name = attribute.name.toLowerCase(); + const name = get_attribute_name(node, attribute, context); const is_boolean = DOMBooleanAttributes.includes(name); const value = serialize_attribute_value( attribute.value, diff --git a/packages/svelte/src/compiler/phases/constants.js b/packages/svelte/src/compiler/phases/constants.js index 394168ea53..0b06c5eda0 100644 --- a/packages/svelte/src/compiler/phases/constants.js +++ b/packages/svelte/src/compiler/phases/constants.js @@ -1,52 +1,12 @@ -export const DOMBooleanAttributes = [ - 'allowfullscreen', - 'async', - 'autofocus', - 'autoplay', - 'checked', - 'controls', - 'default', - 'disabled', - 'formnovalidate', - 'hidden', - 'indeterminate', - 'ismap', - 'loop', - 'multiple', - 'muted', - 'nomodule', - 'novalidate', - 'open', - 'playsinline', - 'readonly', - 'required', - 'reversed', - 'seamless', - 'selected' -]; +import { AttributeAliases, DOMBooleanAttributes } from '../../constants.js'; export const DOMProperties = [ - 'className', + ...Object.values(AttributeAliases), 'value', - 'readOnly', - 'formNoValidate', - 'isMap', - 'noModule', - 'playsInline', 'inert', ...DOMBooleanAttributes ]; -/** @type {Record} */ -export const AttributeAliases = { - class: 'className', - formnovalidate: 'formNoValidate', - ismap: 'isMap', - nomodule: 'noModule', - playsinline: 'playsInline', - readonly: 'readOnly' -}; - export const VoidElements = [ 'area', 'base', diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 17b91d1f11..a8abda2237 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -32,3 +32,48 @@ export const DelegatedEvents = [ /** List of Element events that will be delegated and are passive */ export const PassiveDelegatedEvents = ['touchstart', 'touchmove', 'touchend']; + +/** + * @type {Record} + * List of attribute names that should be aliased to their property names + * because they behave differently between setting them as an attribute and + * setting them as a property. + */ +export const AttributeAliases = { + // no `class: 'className'` because we handle that separately + formnovalidate: 'formNoValidate', + ismap: 'isMap', + nomodule: 'noModule', + playsinline: 'playsInline', + readonly: 'readOnly' +}; + +/** + * Attributes that are boolean, i.e. they are present or not present. + */ +export const DOMBooleanAttributes = [ + 'allowfullscreen', + 'async', + 'autofocus', + 'autoplay', + 'checked', + 'controls', + 'default', + 'disabled', + 'formnovalidate', + 'hidden', + 'indeterminate', + 'ismap', + 'loop', + 'multiple', + 'muted', + 'nomodule', + 'novalidate', + 'open', + 'playsinline', + 'readonly', + 'required', + 'reversed', + 'seamless', + 'selected' +]; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a20d6126a3..a86c05a616 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -27,7 +27,8 @@ import { EACH_INDEX_REACTIVE, EACH_ITEM_REACTIVE, PassiveDelegatedEvents, - DelegatedEvents + DelegatedEvents, + AttributeAliases } from '../../constants.js'; import { create_fragment_from_html, @@ -2702,10 +2703,11 @@ function get_setters(element) { * @param {Element & ElementCSSInlineStyle} dom * @param {Record | null} prev * @param {Record[]} attrs + * @param {boolean} lowercase_attributes * @param {string} css_hash * @returns {Record} */ -export function spread_attributes(dom, prev, attrs, css_hash) { +export function spread_attributes(dom, prev, attrs, lowercase_attributes, css_hash) { const next = Object.assign({}, ...attrs); const has_hash = css_hash.length !== 0; for (const key in prev) { @@ -2724,13 +2726,13 @@ export function spread_attributes(dom, prev, attrs, css_hash) { let value = next[key]; if (value === prev?.[key]) continue; - const prefix = key.slice(0, 2); + const prefix = key[0] + key[1]; // this is faster than key.slice(0, 2) if (prefix === '$$') continue; if (prefix === 'on') { /** @type {{ capture?: true }} */ const opts = {}; - let event_name = key.slice(2).toLowerCase(); + let event_name = key.slice(2); const delegated = DelegatedEvents.includes(event_name); if ( @@ -2762,25 +2764,33 @@ export function spread_attributes(dom, prev, attrs, css_hash) { } else if (key === '__value' || key === 'value') { // @ts-ignore dom.value = dom[key] = dom.__value = value; - } else if (setters.includes(key)) { - if (DEV) { - check_src_in_dev_hydration(dom, key, value); - } - if ( - current_hydration_fragment === null || - // @ts-ignore see attr method for an explanation of src/srcset - (dom[key] !== value && key !== 'src' && key !== 'srcset') - ) { - // @ts-ignore - dom[key] = value; - } - } else if (typeof value !== 'function') { - if (has_hash && key === 'class') { - if (value) value += ' '; - value += css_hash; + } else { + let name = key; + if (lowercase_attributes) { + name = name.toLowerCase(); + name = AttributeAliases[name] || name; } - attr(dom, key, value); + if (setters.includes(name)) { + if (DEV) { + check_src_in_dev_hydration(dom, name, value); + } + if ( + current_hydration_fragment === null || + // @ts-ignore see attr method for an explanation of src/srcset + (dom[name] !== value && name !== 'src' && name !== 'srcset') + ) { + // @ts-ignore + dom[name] = value; + } + } else if (typeof value !== 'function') { + if (has_hash && name === 'class') { + if (value) value += ' '; + value += css_hash; + } + + attr(dom, name, value); + } } } return next; @@ -2811,6 +2821,7 @@ export function spread_dynamic_element_attributes(node, prev, attrs, css_hash) { /** @type {Element & ElementCSSInlineStyle} */ (node), prev, attrs, + node.namespaceURI !== 'http://www.w3.org/2000/svg', css_hash ); } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 7d57737529..4fe85992f4 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -2,6 +2,7 @@ import * as $ from '../client/runtime.js'; import { set_is_ssr } from '../client/runtime.js'; import { is_promise } from '../common.js'; import { subscribe_to_store } from '../../store/utils.js'; +import { DOMBooleanAttributes } from '../../constants.js'; export * from '../client/validate.js'; @@ -31,34 +32,6 @@ const CONTENT_REGEX = /[&<]/g; const INVALID_ATTR_NAME_CHAR_REGEX = /[\s'">/=\u{FDD0}-\u{FDEF}\u{FFFE}\u{FFFF}\u{1FFFE}\u{1FFFF}\u{2FFFE}\u{2FFFF}\u{3FFFE}\u{3FFFF}\u{4FFFE}\u{4FFFF}\u{5FFFE}\u{5FFFF}\u{6FFFE}\u{6FFFF}\u{7FFFE}\u{7FFFF}\u{8FFFE}\u{8FFFF}\u{9FFFE}\u{9FFFF}\u{AFFFE}\u{AFFFF}\u{BFFFE}\u{BFFFF}\u{CFFFE}\u{CFFFF}\u{DFFFE}\u{DFFFF}\u{EFFFE}\u{EFFFF}\u{FFFFE}\u{FFFFF}\u{10FFFE}\u{10FFFF}]/u; -// This is duplicated from the compiler, but we need it at runtime too. -export const DOMBooleanAttributes = [ - 'allowfullscreen', - 'async', - 'autofocus', - 'autoplay', - 'checked', - 'controls', - 'default', - 'disabled', - 'formnovalidate', - 'hidden', - 'indeterminate', - 'ismap', - 'loop', - 'multiple', - 'muted', - 'nomodule', - 'novalidate', - 'open', - 'playsinline', - 'readonly', - 'required', - 'reversed', - 'seamless', - 'selected' -]; - export const VoidElements = new Set([ 'area', 'base', @@ -225,11 +198,13 @@ export function css_props(payload, is_html, props, component) { /** * @param {Record[]} attrs + * @param {boolean} lowercase_attributes + * @param {boolean} is_svg * @param {string} class_hash * @param {{ styles: Record | null; classes: string }} [additional] * @returns {string} */ -export function spread_attributes(attrs, class_hash, additional) { +export function spread_attributes(attrs, lowercase_attributes, is_svg, class_hash, additional) { /** @type {Record} */ const merged_attrs = {}; let key; @@ -276,7 +251,10 @@ export function spread_attributes(attrs, class_hash, additional) { for (name in merged_attrs) { if (INVALID_ATTR_NAME_CHAR_REGEX.test(name)) continue; - const is_boolean = DOMBooleanAttributes.includes(name); + if (lowercase_attributes) { + name = name.toLowerCase(); + } + const is_boolean = !is_svg && DOMBooleanAttributes.includes(name); attr_str += attr(name, merged_attrs[name], is_boolean); } diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/_config.js b/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/_config.js new file mode 100644 index 0000000000..eac7b72914 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/_config.js @@ -0,0 +1,29 @@ +import { test } from '../../test'; + +export default test({ + // There's a slight difference in the output between modes, because the server doesn't know + // whether or not the custom element has the readonly boolean, so it plays it save and + // assumes it does. + html: ` + + + + + + + + + + `, + ssrHtml: ` + + + + + + + + + +` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/main.svelte b/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/main.svelte new file mode 100644 index 0000000000..88cb1cfee5 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/attribute-spread-casing/main.svelte @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + 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 new file mode 100644 index 0000000000..6f53521673 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -0,0 +1,48 @@ +// main.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import "svelte/internal/disclose-version"; +import * as $ from "svelte/internal"; + +var frag = $.template(`
`, true); + +export default function Main($$anchor, $$props) { + $.push($$props, true); + + // needs to be a snapshot test because jsdom does auto-correct the attribute casing + let x = $.source('test'); + let y = $.source(() => 'test'); + /* Init */ + var fragment = $.open_frag($$anchor, false, frag); + var node = $.child_frag(fragment); + var svg = $.sibling($.sibling(node)); + var custom_element = $.sibling($.sibling(svg)); + var div = $.sibling($.sibling(custom_element)); + var svg_1 = $.sibling($.sibling(div)); + var custom_element_1 = $.sibling($.sibling(svg_1)); + + /* Update */ + $.attr_effect(div, "foobar", () => $.get(y)()); + $.attr_effect(svg_1, "viewBox", () => $.get(y)()); + $.set_custom_element_data_effect(custom_element_1, "fooBar", () => $.get(y)()); + + var node_foobar; + var svg_viewBox; + var custom_element_fooBar; + + $.render_effect(() => { + if (node_foobar !== (node_foobar = $.get(x))) { + $.attr(node, "foobar", node_foobar); + } + + if (svg_viewBox !== (svg_viewBox = $.get(x))) { + $.attr(svg, "viewBox", svg_viewBox); + } + + if (custom_element_fooBar !== (custom_element_fooBar = $.get(x))) { + $.set_custom_element_data(custom_element, "fooBar", custom_element_fooBar); + } + }); + + $.close_frag($$anchor, fragment); + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js new file mode 100644 index 0000000000..0352ce77aa --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/server/main.svelte.js @@ -0,0 +1,14 @@ +// main.svelte (Svelte VERSION) +// Note: compiler output will change before 5.0 is released! +import * as $ from "svelte/internal/server"; + +export default function Main($$payload, $$props) { + $.push(true); + + // needs to be a snapshot test because jsdom does auto-correct the attribute casing + let x = 'test'; + let y = () => 'test'; + + $$payload.out += ` `; + $.pop(); +} \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/main.svelte b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/main.svelte new file mode 100644 index 0000000000..a38659a486 --- /dev/null +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/main.svelte @@ -0,0 +1,14 @@ + + +
+ + + + +
+ + From 78a69acfa1ad1555a490b5a3b8cf799ff46f30a8 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:29:06 +0100 Subject: [PATCH 29/46] fix: dont error on stores looking like runes when runes explicitly turned off (#9615) --- .changeset/lovely-items-turn.md | 5 +++++ .../svelte/src/compiler/phases/2-analyze/validation.js | 7 ++++--- .../tests/validator/samples/no-runes-mode/_config.js | 7 +++++++ .../tests/validator/samples/no-runes-mode/input.svelte | 6 ++++++ 4 files changed, 22 insertions(+), 3 deletions(-) create mode 100644 .changeset/lovely-items-turn.md create mode 100644 packages/svelte/tests/validator/samples/no-runes-mode/_config.js create mode 100644 packages/svelte/tests/validator/samples/no-runes-mode/input.svelte diff --git a/.changeset/lovely-items-turn.md b/.changeset/lovely-items-turn.md new file mode 100644 index 0000000000..499b1fadf8 --- /dev/null +++ b/.changeset/lovely-items-turn.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: dont error on stores looking like runes when runes explicitly turned off diff --git a/packages/svelte/src/compiler/phases/2-analyze/validation.js b/packages/svelte/src/compiler/phases/2-analyze/validation.js index 805f673974..e799c38851 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/validation.js +++ b/packages/svelte/src/compiler/phases/2-analyze/validation.js @@ -443,7 +443,7 @@ export const validation = { }; export const validation_legacy = merge(validation, a11y_validators, { - VariableDeclarator(node) { + VariableDeclarator(node, { state }) { if (node.init?.type !== 'CallExpression') return; const callee = node.init.callee; @@ -454,8 +454,9 @@ export const validation_legacy = merge(validation, a11y_validators, { return; } - // TODO check if it's a store subscription that's called? How likely is it that someone uses a store that contains a function? - error(node.init, 'invalid-rune-usage', callee.name); + if (state.scope.get(callee.name)?.kind !== 'store_sub') { + error(node.init, 'invalid-rune-usage', callee.name); + } }, AssignmentExpression(node, { state, path }) { const parent = path.at(-1); diff --git a/packages/svelte/tests/validator/samples/no-runes-mode/_config.js b/packages/svelte/tests/validator/samples/no-runes-mode/_config.js new file mode 100644 index 0000000000..477cb2b131 --- /dev/null +++ b/packages/svelte/tests/validator/samples/no-runes-mode/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + compileOptions: { + runes: false + } +}); diff --git a/packages/svelte/tests/validator/samples/no-runes-mode/input.svelte b/packages/svelte/tests/validator/samples/no-runes-mode/input.svelte new file mode 100644 index 0000000000..daa1686fa1 --- /dev/null +++ b/packages/svelte/tests/validator/samples/no-runes-mode/input.svelte @@ -0,0 +1,6 @@ + + +{x} From b7af2ffabdda5124a95c7e218d8a650c3a6d9cbf Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 23 Nov 2023 16:57:21 +0000 Subject: [PATCH 30/46] Version Packages (next) (#9607) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 9 +++++++++ packages/svelte/CHANGELOG.md | 22 ++++++++++++++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 33 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 0a230cc4ba..c4e518fde5 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -17,11 +17,15 @@ "cold-birds-own", "cuddly-pianos-drop", "curly-lizards-dream", + "dirty-garlics-design", + "dirty-tips-add", "early-ads-tie", "eight-steaks-shout", "eighty-bikes-camp", "fair-crabs-check", + "few-mugs-fail", "fifty-steaks-float", + "forty-comics-invent", "fresh-weeks-trade", "funny-wombats-argue", "good-pianos-jump", @@ -33,15 +37,20 @@ "kind-deers-lay", "lazy-spiders-think", "long-crews-return", + "lovely-items-turn", "lucky-schools-hang", + "moody-frogs-exist", "moody-owls-cry", "new-boats-wait", "odd-needles-joke", + "odd-schools-wait", "poor-eggs-enjoy", "popular-mangos-rest", "quiet-camels-mate", "rich-sheep-burn", "rotten-buckets-develop", + "serious-zebras-scream", + "seven-deers-jam", "sharp-gorillas-impress", "shiny-baboons-play", "shiny-shrimps-march", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 03f8c7b584..30d064c151 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,27 @@ # svelte +## 5.0.0-next.11 + +### Patch Changes + +- feat: add type of `$effect.active` ([#9624](https://github.com/sveltejs/svelte/pull/9624)) + +- fix: correct bind this multiple bindings ([#9617](https://github.com/sveltejs/svelte/pull/9617)) + +- chore: reuse common templates ([#9601](https://github.com/sveltejs/svelte/pull/9601)) + +- fix: handle undefined bubble events ([#9614](https://github.com/sveltejs/svelte/pull/9614)) + +- fix: dont error on stores looking like runes when runes explicitly turned off ([#9615](https://github.com/sveltejs/svelte/pull/9615)) + +- fix: improve member expression mutation logic ([#9625](https://github.com/sveltejs/svelte/pull/9625)) + +- chore: untrack keyed validation logic ([#9618](https://github.com/sveltejs/svelte/pull/9618)) + +- fix: ensure snippets have correct scope ([#9623](https://github.com/sveltejs/svelte/pull/9623)) + +- fix: better attribute casing logic ([#9626](https://github.com/sveltejs/svelte/pull/9626)) + ## 5.0.0-next.10 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 2f577c7ea4..9a4677cc2f 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.10", + "version": "5.0.0-next.11", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index 7c1b4c063b..afa31898e8 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.10'; +export const VERSION = '5.0.0-next.11'; export const PUBLIC_VERSION = '5'; From cb4b1f0a189803bed04adcb90fbd4334782e8469 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 24 Nov 2023 14:51:46 +0100 Subject: [PATCH 31/46] fix: remove constructor overload The original intent was for typing generation tools like svelte2tsx to use the virtual constructor instead, but it does more harm than good. Most notably, the TS error message when props are wrong becomes completely useless. It's better for tooling to silence the deprecation message and switch over to a new virtual constructor once we remove the deprecated one. --- .changeset/ten-worms-reflect.md | 5 +++++ packages/svelte/src/main/public.d.ts | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 .changeset/ten-worms-reflect.md diff --git a/.changeset/ten-worms-reflect.md b/.changeset/ten-worms-reflect.md new file mode 100644 index 0000000000..a78d6b052f --- /dev/null +++ b/.changeset/ten-worms-reflect.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove constructor overload diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index 6851d3f1d7..63f5122bbe 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -52,13 +52,6 @@ export class SvelteComponent< Slots extends Record = any > { [prop: string]: any; - - /** - * For type checking capabilities only. - * Does not exist at runtime. - * ### DO NOT USE! - */ - constructor(props: Props); /** * @deprecated This constructor only exists when using the `asClassComponent` compatibility helper, which * is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See From a3bc7d5698425ec9dde86eb302f2fd56d9da8f96 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 24 Nov 2023 16:34:34 +0100 Subject: [PATCH 32/46] fix: type-level back-compat for default slot and children prop If someone has an existing SvelteComponent type definition and using the slot generic to type the default slot, automatically add that slot to the prop type as "children" --- .changeset/wet-games-fly.md | 5 +++++ packages/svelte/src/main/public.d.ts | 12 ++++++++++-- packages/svelte/tests/types/component.ts | 18 ++++++++++++------ 3 files changed, 27 insertions(+), 8 deletions(-) create mode 100644 .changeset/wet-games-fly.md diff --git a/.changeset/wet-games-fly.md b/.changeset/wet-games-fly.md new file mode 100644 index 0000000000..9f03e26882 --- /dev/null +++ b/.changeset/wet-games-fly.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: type-level back-compat for default slot and children prop diff --git a/packages/svelte/src/main/public.d.ts b/packages/svelte/src/main/public.d.ts index 63f5122bbe..5849835ea2 100644 --- a/packages/svelte/src/main/public.d.ts +++ b/packages/svelte/src/main/public.d.ts @@ -18,6 +18,14 @@ export interface ComponentConstructorOptions< $$inline?: boolean; } +// Utility type for ensuring backwards compatibility on a type level: If there's a default slot, add 'children' to the props if it doesn't exist there already +type PropsWithChildren = Props & + (Props extends { children?: any } + ? {} + : Slots extends { default: any } + ? { children?: Snippet } + : {}); + /** * Can be used to create strongly typed Svelte components. * @@ -57,13 +65,13 @@ export class SvelteComponent< * is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See * https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info. */ - constructor(options: ComponentConstructorOptions); + constructor(options: ComponentConstructorOptions>); /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! * */ - $$prop_def: Props; + $$prop_def: PropsWithChildren; /** * For type checking capabilities only. * Does not exist at runtime. diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 749c648e2f..13ebb65753 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -15,11 +15,11 @@ class LegacyComponent extends SvelteComponent< { slot: { slotProps: boolean } } > {} -// @ts-expect-error const legacyComponent = new LegacyComponent({ target: null as any as Document | Element | ShadowRoot, props: { prop: 'foo', + // @ts-expect-error x: '' } }); @@ -56,14 +56,20 @@ class NewComponent extends SvelteComponent< anExport: string = ''; } -// @ts-expect-error new NewComponent({ - prop: 'foo', - x: '' + target: null as any, + props: { + prop: 'foo', + // @ts-expect-error + x: '' + } }); const newComponent: NewComponent = new NewComponent({ - prop: 'foo' + target: null as any, + props: { + prop: 'foo' + } }); newComponent.$$events_def.event; // @ts-expect-error @@ -123,11 +129,11 @@ instance.anExport === 1; // --------------------------------------------------------------------------- interop const AsLegacyComponent = asClassComponent(newComponent); -// @ts-expect-error new AsLegacyComponent({ target: null as any, props: { prop: '', + // @ts-expect-error x: '' } }); From 63e58318460dbb3485df93d15beb2779a86d2c9a Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 24 Nov 2023 16:36:04 +0100 Subject: [PATCH 33/46] fix: adjust mount and createRoot types they expect the component constructor, not the instance --- .changeset/green-eggs-approve.md | 5 +++++ packages/svelte/src/internal/client/render.js | 4 ++-- packages/svelte/tests/types/component.ts | 20 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) create mode 100644 .changeset/green-eggs-approve.md diff --git a/.changeset/green-eggs-approve.md b/.changeset/green-eggs-approve.md new file mode 100644 index 0000000000..90c7ec357e --- /dev/null +++ b/.changeset/green-eggs-approve.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: adjust mount and createRoot types diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index a86c05a616..afb060639a 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -2915,7 +2915,7 @@ export function unwrap(value) { * @template {Record} Props * @template {Record | undefined} Exports * @template {Record} Events - * @param {import('../../main/public.js').SvelteComponent} component + * @param {typeof import('../../main/public.js').SvelteComponent} component * @param {{ * target: Node; * props?: Props; @@ -3034,7 +3034,7 @@ export function createRoot(component, options) { * @template {Record} Props * @template {Record | undefined} Exports * @template {Record} Events - * @param {import('../../main/public.js').SvelteComponent} component + * @param {typeof import('../../main/public.js').SvelteComponent} component * @param {{ * target: Node; * props?: Props; diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 13ebb65753..1047d56781 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -4,7 +4,8 @@ import { SvelteComponent, type ComponentEvents, type ComponentProps, - type ComponentType + type ComponentType, + mount } from 'svelte'; // --------------------------------------------------------------------------- legacy: classes @@ -103,7 +104,22 @@ const newComponentEvents2: ComponentEvents = { event: new KeyboardEvent('click') }; -const instance = createRoot(newComponent, { +mount(NewComponent, { + target: null as any as Document | Element | ShadowRoot | Text | Comment, + props: { + prop: 'foo', + // @ts-expect-error + x: '' + }, + events: { + event: new MouseEvent('click') + }, + immutable: true, + intro: false, + recover: false +}); + +const instance = createRoot(NewComponent, { target: null as any as Document | Element | ShadowRoot | Text | Comment, props: { prop: 'foo', From 94afdc8a4bb08ba3b72512a3da846343e42e8c71 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 24 Nov 2023 17:46:40 +0100 Subject: [PATCH 34/46] Version Packages (next) (#9633) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index c4e518fde5..8bc596d698 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -29,6 +29,7 @@ "fresh-weeks-trade", "funny-wombats-argue", "good-pianos-jump", + "green-eggs-approve", "green-hounds-play", "honest-icons-change", "hungry-dots-fry", @@ -59,11 +60,13 @@ "sour-rules-march", "strong-lemons-provide", "tall-shrimps-worry", + "ten-worms-reflect", "thirty-flowers-sit", "thirty-ghosts-fix", "thirty-impalas-repair", "tiny-kings-whisper", "two-falcons-buy", + "wet-games-fly", "wicked-clouds-exercise", "wicked-doors-train" ] diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index 30d064c151..eaca4269a3 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.0.0-next.12 + +### Patch Changes + +- fix: adjust mount and createRoot types ([`63e583184`](https://github.com/sveltejs/svelte/commit/63e58318460dbb3485df93d15beb2779a86d2c9a)) + +- fix: remove constructor overload ([`cb4b1f0a1`](https://github.com/sveltejs/svelte/commit/cb4b1f0a189803bed04adcb90fbd4334782e8469)) + +- fix: type-level back-compat for default slot and children prop ([`a3bc7d569`](https://github.com/sveltejs/svelte/commit/a3bc7d5698425ec9dde86eb302f2fd56d9da8f96)) + ## 5.0.0-next.11 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 9a4677cc2f..ab79b8c7d9 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.11", + "version": "5.0.0-next.12", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index afa31898e8..ebc8996f18 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.11'; +export const VERSION = '5.0.0-next.12'; export const PUBLIC_VERSION = '5'; From 02f3f42981e4f7e6960c1ce1f84b01e044be97df Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Nov 2023 21:48:18 +0000 Subject: [PATCH 35/46] fix: apply keyed validation only for keyed each (#9641) --- .changeset/chilled-pumas-invite.md | 5 +++++ .../phases/3-transform/client/visitors/template.js | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/chilled-pumas-invite.md diff --git a/.changeset/chilled-pumas-invite.md b/.changeset/chilled-pumas-invite.md new file mode 100644 index 0000000000..943f2d4836 --- /dev/null +++ b/.changeset/chilled-pumas-invite.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: apply keyed validation only for keyed each 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 68328e5453..c265b89d6d 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 @@ -2279,12 +2279,6 @@ export const template_visitors = { ) : b.literal(null); - if (context.state.options.dev && key_function.type !== 'Literal') { - context.state.init.push( - b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) - ); - } - if (node.index && each_node_meta.contains_group_binding) { // We needed to create a unique identifier for the index above, but we want to use the // original index name in the template, therefore create another binding @@ -2292,6 +2286,12 @@ export const template_visitors = { } if ((each_type & EACH_KEYED) !== 0) { + if (context.state.options.dev && key_function.type !== 'Literal') { + context.state.init.push( + b.stmt(b.call('$.validate_each_keys', b.thunk(collection), key_function)) + ); + } + context.state.after_update.push( b.stmt( b.call( From dee5bed82917cfaf7b283773505ebfdb1423acda Mon Sep 17 00:00:00 2001 From: Dominic Gannaway Date: Fri, 24 Nov 2023 23:02:53 +0000 Subject: [PATCH 36/46] fix: improve each block index handling (#9644) * fix: improve each block index handling * format --- .changeset/thirty-wombats-relax.md | 5 +++++ .../3-transform/client/visitors/template.js | 16 ++++++++++++---- packages/svelte/src/compiler/phases/scope.js | 10 ++++------ .../samples/keyed-each-index-same-2/_config.js | 5 +++++ .../samples/keyed-each-index-same-2/main.svelte | 3 +++ 5 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 .changeset/thirty-wombats-relax.md create mode 100644 packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/_config.js create mode 100644 packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/main.svelte diff --git a/.changeset/thirty-wombats-relax.md b/.changeset/thirty-wombats-relax.md new file mode 100644 index 0000000000..0ed0f40a75 --- /dev/null +++ b/.changeset/thirty-wombats-relax.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: improve each block index handling 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 c265b89d6d..5f002bbebf 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 @@ -2491,7 +2491,7 @@ export const template_visitors = { next(); }, BindDirective(node, context) { - const { state, path } = context; + const { state, path, visit } = context; /** @type {import('estree').Expression[]} */ const properties = []; @@ -2621,10 +2621,18 @@ export const template_visitors = { break; } - case 'this': - call_expr = b.call(`$.bind_this`, state.node, setter, node.expression); + case 'this': { + const expression = node.expression; + call_expr = b.call( + `$.bind_this`, + state.node, + setter, + expression.type === 'Identifier' + ? expression + : /** @type {import('estree').Expression} */ (visit(expression)) + ); break; - + } case 'textContent': case 'innerHTML': case 'innerText': diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 6c73002d5b..c839a78276 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -495,12 +495,10 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } if (node.index) { - scope.declare( - b.id(node.index), - // TODO see logic in EachBlock in dom.ts - node.key ? 'derived' : 'normal', - 'const' - ); + const is_keyed = + node.key && + (node.key.type !== 'Identifier' || !node.index || node.key.name !== node.index); + scope.declare(b.id(node.index), is_keyed ? 'derived' : 'normal', 'const'); } if (node.key) visit(node.key, { scope }); diff --git a/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/_config.js b/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/_config.js new file mode 100644 index 0000000000..8627d28b23 --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/_config.js @@ -0,0 +1,5 @@ +import { test } from '../../test'; + +export default test({ + html: '
0
1
' +}); diff --git a/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/main.svelte b/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/main.svelte new file mode 100644 index 0000000000..c21812be4a --- /dev/null +++ b/packages/svelte/tests/runtime-legacy/samples/keyed-each-index-same-2/main.svelte @@ -0,0 +1,3 @@ +{#each ["a", "b"] as result, i (i)} +
{i}
+{/each} From 7f237c2e41115b420f0d6432c51c85ec3b5ecaf5 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Sat, 25 Nov 2023 00:54:52 +0100 Subject: [PATCH 37/46] fix: omit this bind this arg if we know it's not a signal (#9635) fixes #9629 Co-authored-by: Dominic Gannaway --- .changeset/lovely-rules-eat.md | 5 +++++ .../3-transform/client/visitors/template.js | 16 +++++++++------- .../samples/each-bind-this-member/_config.js | 7 +++++++ .../samples/each-bind-this-member/main.svelte | 11 +++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 .changeset/lovely-rules-eat.md create mode 100644 packages/svelte/tests/runtime-runes/samples/each-bind-this-member/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte diff --git a/.changeset/lovely-rules-eat.md b/.changeset/lovely-rules-eat.md new file mode 100644 index 0000000000..5ecaa2fc8d --- /dev/null +++ b/.changeset/lovely-rules-eat.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: omit this bind this arg if we know it's not a signal 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 5f002bbebf..b5b58c34dc 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 @@ -899,7 +899,10 @@ function serialize_inline_component(node, component_name, context) { if (bind_this !== null) { const prev = fn; const assignment = b.assignment('=', bind_this, b.id('$$value')); - const bind_this_id = bind_this; + const bind_this_id = /** @type {import('estree').Expression} */ ( + // if expression is not an identifier, we know it can't be a signal + bind_this.type === 'Identifier' ? bind_this : undefined + ); fn = (node_id) => b.call( '$.bind_this', @@ -2621,18 +2624,17 @@ export const template_visitors = { break; } - case 'this': { - const expression = node.expression; + case 'this': call_expr = b.call( `$.bind_this`, state.node, setter, - expression.type === 'Identifier' - ? expression - : /** @type {import('estree').Expression} */ (visit(expression)) + /** @type {import('estree').Expression} */ ( + // if expression is not an identifier, we know it can't be a signal + node.expression.type === 'Identifier' ? node.expression : undefined + ) ); break; - } case 'textContent': case 'innerHTML': case 'innerText': diff --git a/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/_config.js b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/_config.js new file mode 100644 index 0000000000..4b5596cb3a --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/_config.js @@ -0,0 +1,7 @@ +import { test } from '../../test'; + +export default test({ + async test({ assert, target, component }) { + assert.equal(target.querySelector('img'), component.items[0].img); + } +}); 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 new file mode 100644 index 0000000000..a685cc9c84 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-bind-this-member/main.svelte @@ -0,0 +1,11 @@ + + +{#each items as item, i} + slider{i} +{/each} From 66d540d3c511b960741b3c41c8d32dc59d650a3f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 25 Nov 2023 14:41:17 -0500 Subject: [PATCH 38/46] Version Packages (next) (#9642) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ packages/svelte/CHANGELOG.md | 10 ++++++++++ packages/svelte/package.json | 2 +- packages/svelte/src/version.js | 2 +- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 8bc596d698..7e15cda5cf 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -12,6 +12,7 @@ "afraid-moose-matter", "brave-walls-destroy", "brown-spoons-boil", + "chilled-pumas-invite", "chilly-dolphins-lick", "clean-eels-beg", "cold-birds-own", @@ -39,6 +40,7 @@ "lazy-spiders-think", "long-crews-return", "lovely-items-turn", + "lovely-rules-eat", "lucky-schools-hang", "moody-frogs-exist", "moody-owls-cry", @@ -64,6 +66,7 @@ "thirty-flowers-sit", "thirty-ghosts-fix", "thirty-impalas-repair", + "thirty-wombats-relax", "tiny-kings-whisper", "two-falcons-buy", "wet-games-fly", diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index eaca4269a3..d8f5cd4fb2 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,15 @@ # svelte +## 5.0.0-next.13 + +### Patch Changes + +- fix: apply keyed validation only for keyed each ([#9641](https://github.com/sveltejs/svelte/pull/9641)) + +- fix: omit this bind this arg if we know it's not a signal ([#9635](https://github.com/sveltejs/svelte/pull/9635)) + +- fix: improve each block index handling ([#9644](https://github.com/sveltejs/svelte/pull/9644)) + ## 5.0.0-next.12 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index ab79b8c7d9..d9ecf218d6 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.12", + "version": "5.0.0-next.13", "type": "module", "types": "./types/index.d.ts", "engines": { diff --git a/packages/svelte/src/version.js b/packages/svelte/src/version.js index ebc8996f18..a007154495 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.12'; +export const VERSION = '5.0.0-next.13'; export const PUBLIC_VERSION = '5'; From 405e9da4ff853c2e8aafde739ba0b2fd406f56b5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 25 Nov 2023 17:29:18 -0500 Subject: [PATCH 39/46] chore: bump esrap (#9649) * bump esrap * changeset --------- Co-authored-by: Rich Harris --- .changeset/stale-comics-look.md | 5 ++++ packages/svelte/package.json | 2 +- pnpm-lock.yaml | 46 ++++++----------------------- sites/svelte-5-preview/package.json | 2 +- 4 files changed, 16 insertions(+), 39 deletions(-) create mode 100644 .changeset/stale-comics-look.md diff --git a/.changeset/stale-comics-look.md b/.changeset/stale-comics-look.md new file mode 100644 index 0000000000..b7f09bd0a6 --- /dev/null +++ b/.changeset/stale-comics-look.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: bump esrap diff --git a/packages/svelte/package.json b/packages/svelte/package.json index d9ecf218d6..3e40ed5a2a 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -120,7 +120,7 @@ "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "esm-env": "^1.0.0", - "esrap": "^1.2.0", + "esrap": "^1.2.1", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 625aaeaaf3..eb260d73ad 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,8 +81,8 @@ importers: specifier: ^1.0.0 version: 1.0.0 esrap: - specifier: ^1.2.0 - version: 1.2.0 + specifier: ^1.2.1 + version: 1.2.1 is-reference: specifier: ^3.0.1 version: 3.0.2 @@ -261,8 +261,8 @@ importers: specifier: ^5.0.1 version: 5.0.2 esrap: - specifier: ^1.2.0 - version: 1.2.0 + specifier: ^1.2.1 + version: 1.2.1 publint: specifier: ^0.2.0 version: 0.2.5 @@ -2231,7 +2231,7 @@ packages: svelte: 4.2.3 tiny-glob: 0.2.9 undici: 5.26.5 - vite: 4.5.0(@types/node@18.18.9) + vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) transitivePeerDependencies: - supports-color @@ -2322,7 +2322,7 @@ packages: '@sveltejs/vite-plugin-svelte': 2.5.1(svelte@4.2.3)(vite@4.5.0) debug: 4.3.4 svelte: 4.2.3 - vite: 4.5.0(@types/node@18.18.9) + vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) transitivePeerDependencies: - supports-color @@ -2356,7 +2356,7 @@ packages: magic-string: 0.30.5 svelte: 4.2.3 svelte-hmr: 0.15.3(svelte@4.2.3) - vite: 4.5.0(@types/node@18.18.9) + vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) vitefu: 0.2.5(vite@4.5.0) transitivePeerDependencies: - supports-color @@ -2828,7 +2828,6 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 - dev: true /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -2956,7 +2955,6 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} - dev: true /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3014,7 +3012,6 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 - dev: true /breakword@1.0.6: resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} @@ -3164,7 +3161,6 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 - dev: true /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3598,7 +3594,6 @@ packages: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true - dev: true /detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} @@ -4056,8 +4051,8 @@ packages: estraverse: 5.3.0 dev: true - /esrap@1.2.0: - resolution: {integrity: sha512-ZWd00MnkN45Hcj+nIV5FPiZk6Nx7RDUL8G0KGp1cg1xSKXOEac0IhMQdsrGswXB1O0fMzH+MXq288Lt0Abpg/Q==} + /esrap@1.2.1: + resolution: {integrity: sha512-dhkcOLfN/aDdMFI1iwPEcy/XqAZzGNfgfEJjZozy2tia6u0dQoZyXzkRshHTckuNsM+c0CYQndY+uRFe3N+AIQ==} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 '@types/estree': 1.0.5 @@ -4232,7 +4227,6 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 - dev: true /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} @@ -4442,7 +4436,6 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 - dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -4688,7 +4681,6 @@ packages: /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} - dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -4769,7 +4761,6 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 - dev: true /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -4814,7 +4805,6 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - dev: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -4830,7 +4820,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 - dev: true /is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -4851,7 +4840,6 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -5127,7 +5115,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: true optional: true /lightningcss-darwin-x64@1.22.1: @@ -5136,7 +5123,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: true optional: true /lightningcss-freebsd-x64@1.22.1: @@ -5145,7 +5131,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: true optional: true /lightningcss-linux-arm-gnueabihf@1.22.1: @@ -5154,7 +5139,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: true optional: true /lightningcss-linux-arm64-gnu@1.22.1: @@ -5163,7 +5147,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /lightningcss-linux-arm64-musl@1.22.1: @@ -5172,7 +5155,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: true optional: true /lightningcss-linux-x64-gnu@1.22.1: @@ -5181,7 +5163,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /lightningcss-linux-x64-musl@1.22.1: @@ -5190,7 +5171,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: true optional: true /lightningcss-win32-x64-msvc@1.22.1: @@ -5199,7 +5179,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: true optional: true /lightningcss@1.22.1: @@ -5217,7 +5196,6 @@ packages: lightningcss-linux-x64-gnu: 1.22.1 lightningcss-linux-x64-musl: 1.22.1 lightningcss-win32-x64-msvc: 1.22.1 - dev: true /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} @@ -5653,7 +5631,6 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} - dev: true /npm-bundled@2.0.1: resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} @@ -5925,7 +5902,6 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -6271,7 +6247,6 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 - dev: true /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} @@ -6465,7 +6440,6 @@ packages: chokidar: 3.5.3 immutable: 4.3.4 source-map-js: 1.0.2 - dev: true /satori-html@0.3.2: resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} @@ -7232,7 +7206,6 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 - dev: true /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -7656,7 +7629,6 @@ packages: sass: 1.69.5 optionalDependencies: fsevents: 2.3.3 - dev: true /vitefu@0.2.5(vite@4.5.0): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} diff --git a/sites/svelte-5-preview/package.json b/sites/svelte-5-preview/package.json index eaf3aecbb1..38d281b105 100644 --- a/sites/svelte-5-preview/package.json +++ b/sites/svelte-5-preview/package.json @@ -18,7 +18,7 @@ "@sveltejs/kit": "^1.22.5", "@sveltejs/site-kit": "6.0.0-next.51", "@types/marked": "^5.0.1", - "esrap": "^1.2.0", + "esrap": "^1.2.1", "marked": "^9.0.0", "publint": "^0.2.0", "shiki": "^0.14.4", From 5619cd9bfc73276f8d831f3adfb3562407ca68d6 Mon Sep 17 00:00:00 2001 From: Dominik G Date: Mon, 27 Nov 2023 14:34:06 +0100 Subject: [PATCH 40/46] chore: update playgrounds/demo (#9666) * chore: update playgrounds/demo to exclude svelte from being optimized and generate an App.svelte so first run doesn't throw * chore: fix format --- playgrounds/demo/package.json | 5 +-- playgrounds/demo/scripts/App.template.svelte | 8 ++++ playgrounds/demo/scripts/create-app-svelte.js | 6 +++ playgrounds/demo/vite.config.js | 6 ++- pnpm-lock.yaml | 41 +++++++++++++++---- 5 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 playgrounds/demo/scripts/App.template.svelte create mode 100644 playgrounds/demo/scripts/create-app-svelte.js diff --git a/playgrounds/demo/package.json b/playgrounds/demo/package.json index 6704c8cf75..410bbab35c 100644 --- a/playgrounds/demo/package.json +++ b/playgrounds/demo/package.json @@ -4,18 +4,17 @@ "version": "0.0.1", "type": "module", "scripts": { + "prepare": "node scripts/create-app-svelte.js", "dev": "vite --host", "ssr": "node ./server.js", "build": "vite build", "preview": "vite preview" }, - "dependencies": { - "svelte": "workspace:*" - }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^2.5.1", "express": "^4.18.2", "nodemon": "^3.0.1", + "svelte": "workspace:*", "vite": "^4.4.9" } } diff --git a/playgrounds/demo/scripts/App.template.svelte b/playgrounds/demo/scripts/App.template.svelte new file mode 100644 index 0000000000..e51190c18f --- /dev/null +++ b/playgrounds/demo/scripts/App.template.svelte @@ -0,0 +1,8 @@ + + +

Demo App

+ diff --git a/playgrounds/demo/scripts/create-app-svelte.js b/playgrounds/demo/scripts/create-app-svelte.js new file mode 100644 index 0000000000..478fa782ad --- /dev/null +++ b/playgrounds/demo/scripts/create-app-svelte.js @@ -0,0 +1,6 @@ +import fs from 'node:fs'; +const destination = new URL('../src/App.svelte', import.meta.url); +if (!fs.existsSync(destination)) { + const template = new URL('./App.template.svelte', import.meta.url); + fs.writeFileSync(destination, fs.readFileSync(template, 'utf-8'), 'utf-8'); +} diff --git a/playgrounds/demo/vite.config.js b/playgrounds/demo/vite.config.js index b77353a796..de42e9cfd4 100644 --- a/playgrounds/demo/vite.config.js +++ b/playgrounds/demo/vite.config.js @@ -2,5 +2,9 @@ import { defineConfig } from 'vite'; import { svelte } from '@sveltejs/vite-plugin-svelte'; export default defineConfig({ - plugins: [svelte()] + plugins: [svelte()], + optimizeDeps: { + // svelte is a local workspace package, optimizing it would require dev server restarts with --force for every change + exclude: ['svelte'] + } }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eb260d73ad..52efe8c739 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -137,10 +137,6 @@ importers: version: 0.2.9 playgrounds/demo: - dependencies: - svelte: - specifier: workspace:* - version: link:../../packages/svelte devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^2.5.1 @@ -151,6 +147,9 @@ importers: nodemon: specifier: ^3.0.1 version: 3.0.1 + svelte: + specifier: workspace:* + version: link:../../packages/svelte vite: specifier: ^4.4.9 version: 4.5.0(@types/node@18.18.9) @@ -2231,7 +2230,7 @@ packages: svelte: 4.2.3 tiny-glob: 0.2.9 undici: 5.26.5 - vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) + vite: 4.5.0(@types/node@18.18.9) transitivePeerDependencies: - supports-color @@ -2322,7 +2321,7 @@ packages: '@sveltejs/vite-plugin-svelte': 2.5.1(svelte@4.2.3)(vite@4.5.0) debug: 4.3.4 svelte: 4.2.3 - vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) + vite: 4.5.0(@types/node@18.18.9) transitivePeerDependencies: - supports-color @@ -2356,7 +2355,7 @@ packages: magic-string: 0.30.5 svelte: 4.2.3 svelte-hmr: 0.15.3(svelte@4.2.3) - vite: 4.5.0(@types/node@20.9.0)(lightningcss@1.22.1)(sass@1.69.5) + vite: 4.5.0(@types/node@18.18.9) vitefu: 0.2.5(vite@4.5.0) transitivePeerDependencies: - supports-color @@ -2828,6 +2827,7 @@ packages: dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 + dev: true /aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -2955,6 +2955,7 @@ packages: /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} + dev: true /bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} @@ -3012,6 +3013,7 @@ packages: engines: {node: '>=8'} dependencies: fill-range: 7.0.1 + dev: true /breakword@1.0.6: resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} @@ -3161,6 +3163,7 @@ packages: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 + dev: true /chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} @@ -3594,6 +3597,7 @@ packages: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} hasBin: true + dev: true /detect-libc@2.0.2: resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==} @@ -4227,6 +4231,7 @@ packages: engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + dev: true /finalhandler@1.2.0: resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} @@ -4436,6 +4441,7 @@ packages: engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 + dev: true /glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} @@ -4681,6 +4687,7 @@ packages: /immutable@4.3.4: resolution: {integrity: sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA==} + dev: true /import-fresh@3.3.0: resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} @@ -4761,6 +4768,7 @@ packages: engines: {node: '>=8'} dependencies: binary-extensions: 2.2.0 + dev: true /is-boolean-object@1.1.2: resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} @@ -4805,6 +4813,7 @@ packages: /is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + dev: true /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} @@ -4820,6 +4829,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + dev: true /is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -4840,6 +4850,7 @@ packages: /is-number@7.0.0: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + dev: true /is-path-inside@3.0.3: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} @@ -5115,6 +5126,7 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true + dev: true optional: true /lightningcss-darwin-x64@1.22.1: @@ -5123,6 +5135,7 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true + dev: true optional: true /lightningcss-freebsd-x64@1.22.1: @@ -5131,6 +5144,7 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true + dev: true optional: true /lightningcss-linux-arm-gnueabihf@1.22.1: @@ -5139,6 +5153,7 @@ packages: cpu: [arm] os: [linux] requiresBuild: true + dev: true optional: true /lightningcss-linux-arm64-gnu@1.22.1: @@ -5147,6 +5162,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /lightningcss-linux-arm64-musl@1.22.1: @@ -5155,6 +5171,7 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true + dev: true optional: true /lightningcss-linux-x64-gnu@1.22.1: @@ -5163,6 +5180,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /lightningcss-linux-x64-musl@1.22.1: @@ -5171,6 +5189,7 @@ packages: cpu: [x64] os: [linux] requiresBuild: true + dev: true optional: true /lightningcss-win32-x64-msvc@1.22.1: @@ -5179,6 +5198,7 @@ packages: cpu: [x64] os: [win32] requiresBuild: true + dev: true optional: true /lightningcss@1.22.1: @@ -5196,6 +5216,7 @@ packages: lightningcss-linux-x64-gnu: 1.22.1 lightningcss-linux-x64-musl: 1.22.1 lightningcss-win32-x64-msvc: 1.22.1 + dev: true /lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} @@ -5631,6 +5652,7 @@ packages: /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + dev: true /npm-bundled@2.0.1: resolution: {integrity: sha512-gZLxXdjEzE/+mOstGDqR6b0EkhJ+kM6fxM6vUuckuctuVPh80Q6pw/rSZj9s4Gex9GxWtIicO1pc8DB9KZWudw==} @@ -5902,6 +5924,7 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + dev: true /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -6247,6 +6270,7 @@ packages: engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + dev: true /rechoir@0.6.2: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} @@ -6440,6 +6464,7 @@ packages: chokidar: 3.5.3 immutable: 4.3.4 source-map-js: 1.0.2 + dev: true /satori-html@0.3.2: resolution: {integrity: sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==} @@ -7206,6 +7231,7 @@ packages: engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + dev: true /toidentifier@1.0.1: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} @@ -7629,6 +7655,7 @@ packages: sass: 1.69.5 optionalDependencies: fsevents: 2.3.3 + dev: true /vitefu@0.2.5(vite@4.5.0): resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} From 83fd00115757d9d06c78a1d65bc77378a552ae2e Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:36:13 +0100 Subject: [PATCH 41/46] fix: prevent reactive snippet from reinitializing unnecessarily (#9665) untrack the invocation itself, only track the snippet function fixes #9652 --- .changeset/light-pens-watch.md | 5 ++ .../3-transform/client/visitors/template.js | 16 ++--- packages/svelte/src/internal/client/render.js | 11 +++- .../samples/snippet-reactive-args/_config.js | 62 +++++++++++++++++++ .../snippet-reactive-args/inner.svelte | 6 ++ .../samples/snippet-reactive-args/main.svelte | 22 +++++++ 6 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 .changeset/light-pens-watch.md create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte create mode 100644 packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte diff --git a/.changeset/light-pens-watch.md b/.changeset/light-pens-watch.md new file mode 100644 index 0000000000..62debd2eb4 --- /dev/null +++ b/.changeset/light-pens-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: prevent reactive snippet from reinitializing unnecessarily 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 b5b58c34dc..9640e6c275 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 @@ -1734,18 +1734,20 @@ export const template_visitors = { if (node.argument) { args.push(b.thunk(/** @type {import('estree').Expression} */ (context.visit(node.argument)))); } - const snippet_function = /** @type {import('estree').Expression} */ ( + + let snippet_function = /** @type {import('estree').Expression} */ ( context.visit(node.expression) ); - const init = b.call( - context.state.options.dev ? b.call('$.validate_snippet', snippet_function) : snippet_function, - ...args - ); + if (context.state.options.dev) { + snippet_function = b.call('$.validate_snippet', snippet_function); + } if (is_reactive) { - context.state.init.push(b.stmt(b.call('$.snippet_effect', b.thunk(init)))); + context.state.init.push( + b.stmt(b.call('$.snippet_effect', b.thunk(snippet_function), ...args)) + ); } else { - context.state.init.push(b.stmt(init)); + context.state.init.push(b.stmt(b.call(snippet_function, ...args))); } }, AnimateDirective(node, { state, visit }) { diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index afb060639a..54c760a4a5 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -3170,13 +3170,18 @@ export function sanitize_slots(props) { } /** - * @param {() => void} create_snippet + * @param {() => Function} get_snippet + * @param {Node} node + * @param {() => any} args * @returns {void} */ -export function snippet_effect(create_snippet) { +export function snippet_effect(get_snippet, node, args) { const block = create_snippet_block(); render_effect(() => { - create_snippet(); + // Only rerender when the snippet function itself changes, + // not when an eagerly-read prop inside the snippet function changes + const snippet = get_snippet(); + untrack(() => snippet(node, args)); return () => { if (block.d !== null) { remove(block.d); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/_config.js new file mode 100644 index 0000000000..ffb50aaa53 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/_config.js @@ -0,0 +1,62 @@ +import { test } from '../../test'; + +export default test({ + html: ` +

snippet: 0

+ + + `, + props: { + get log() { + return []; + } + }, + + async test({ assert, target, component }) { + const [toggle, increment] = target.querySelectorAll('button'); + + await increment?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

snippet: 1

+ + + ` + ); + assert.deepEqual(component.log, []); + + await toggle?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

component: 1

+ + + ` + ); + assert.deepEqual(component.log, [1]); + + await increment?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

component: 2

+ + + ` + ); + assert.deepEqual(component.log, [1]); + + await toggle?.click(); + assert.htmlEqual( + target.innerHTML, + ` +

snippet: 2

+ + + ` + ); + assert.deepEqual(component.log, [1]); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte new file mode 100644 index 0000000000..bbe3a61043 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/inner.svelte @@ -0,0 +1,6 @@ + + +

component: {count}

diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte new file mode 100644 index 0000000000..682fca22dc --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/snippet-reactive-args/main.svelte @@ -0,0 +1,22 @@ + + +{#snippet foo({count})} +

snippet: {count}

+{/snippet} + +{#snippet bar(props)} + +{/snippet} + +{@render snippet({ count, log })} + + + From 617d63dd90737dbe4d622c24a5ae08d229c535f3 Mon Sep 17 00:00:00 2001 From: Simon H <5968653+dummdidumm@users.noreply.github.com> Date: Mon, 27 Nov 2023 14:36:47 +0100 Subject: [PATCH 42/46] fix: take event attributes into account when checking a11y (#9664) also adjusted the warning messages slightly so they make sense for both variants of event handlers fixes #9654 --- .changeset/polite-pumpkins-guess.md | 5 ++++ .../src/compiler/phases/2-analyze/a11y.js | 14 ++++++---- packages/svelte/src/compiler/warnings.js | 4 +-- .../input.svelte | 3 +++ .../warnings.json | 26 ++++++++++++++----- .../warnings.json | 4 +-- .../warnings.json | 8 +++--- .../samples/slot-warning/warnings.json | 2 +- .../samples/slot-warning2/warnings.json | 2 +- 9 files changed, 46 insertions(+), 22 deletions(-) create mode 100644 .changeset/polite-pumpkins-guess.md diff --git a/.changeset/polite-pumpkins-guess.md b/.changeset/polite-pumpkins-guess.md new file mode 100644 index 0000000000..e364f9635d --- /dev/null +++ b/.changeset/polite-pumpkins-guess.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: take event attributes into account when checking a11y diff --git a/packages/svelte/src/compiler/phases/2-analyze/a11y.js b/packages/svelte/src/compiler/phases/2-analyze/a11y.js index 33692ebafa..eb87b5ddbf 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/a11y.js +++ b/packages/svelte/src/compiler/phases/2-analyze/a11y.js @@ -9,7 +9,7 @@ import { } from '../patterns.js'; import { warn } from '../../warnings.js'; import fuzzymatch from '../1-parse/utils/fuzzymatch.js'; -import { is_text_attribute } from '../../utils/ast.js'; +import { is_event_attribute, is_text_attribute } from '../../utils/ast.js'; import { ContentEditableBindings } from '../constants.js'; import { walk } from 'zimmerframe'; @@ -704,10 +704,14 @@ function check_element(node, state, path) { } else if (attribute.type === 'OnDirective') { handlers.add(attribute.name); } else if (attribute.type === 'Attribute') { - attributes.push(attribute); - attribute_map.set(attribute.name, attribute); - if (attribute.name === 'contenteditable') { - has_contenteditable_attr = true; + if (is_event_attribute(attribute)) { + handlers.add(attribute.name.slice(2)); + } else { + attributes.push(attribute); + attribute_map.set(attribute.name, attribute); + if (attribute.name === 'contenteditable') { + has_contenteditable_attr = true; + } } } else if ( attribute.type === 'BindDirective' && diff --git a/packages/svelte/src/compiler/warnings.js b/packages/svelte/src/compiler/warnings.js index 7103f4162c..23e9378c25 100644 --- a/packages/svelte/src/compiler/warnings.js +++ b/packages/svelte/src/compiler/warnings.js @@ -115,7 +115,7 @@ const a11y = { 'a11y-misplaced-scope': () => 'A11y: The scope attribute should only be used with elements', 'a11y-positive-tabindex': () => 'A11y: avoid tabindex values above zero', 'a11y-click-events-have-key-events': () => - 'A11y: visible, non-interactive elements with an on:click event must be accompanied by a keyboard event handler. Consider whether an interactive element such as + + +

{a} + {b} + {c} = {a + b + c}

diff --git a/packages/svelte/tests/validator/samples/runes-referenced-nonstate/warnings.json b/packages/svelte/tests/validator/samples/runes-referenced-nonstate/warnings.json new file mode 100644 index 0000000000..7e251a4e70 --- /dev/null +++ b/packages/svelte/tests/validator/samples/runes-referenced-nonstate/warnings.json @@ -0,0 +1,26 @@ +[ + { + "code": "non-state-reference", + "message": "b is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.", + "start": { + "column": 5, + "line": 3 + }, + "end": { + "column": 6, + "line": 3 + } + }, + { + "code": "non-state-reference", + "message": "c is updated, but is not declared with $state(...). Changing its value will not correctly trigger updates.", + "start": { + "column": 5, + "line": 4 + }, + "end": { + "column": 6, + "line": 4 + } + } +] diff --git a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json b/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json index 628f1f2e9d..5d2b639c8d 100644 --- a/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json +++ b/packages/svelte/tests/validator/samples/runes-state-rune-not-mutated/warnings.json @@ -1,6 +1,6 @@ [ { - "code": "state-rune-not-mutated", + "code": "state-not-mutated", "end": { "column": 11, "line": 3