diff --git a/.changeset/three-buses-sleep.md b/.changeset/three-buses-sleep.md new file mode 100644 index 0000000000..0b46867171 --- /dev/null +++ b/.changeset/three-buses-sleep.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: better error for `bind:this` legacy API usage diff --git a/packages/svelte/messages/client-errors/errors.md b/packages/svelte/messages/client-errors/errors.md index aeeb133226..119328c7d0 100644 --- a/packages/svelte/messages/client-errors/errors.md +++ b/packages/svelte/messages/client-errors/errors.md @@ -10,6 +10,10 @@ > A component is attempting to bind to a non-bindable property `%key%` belonging to %component% (i.e. `<%name% bind:%key%={...}>`). To mark a property as bindable: `let { %key% = $bindable() } = $props()` +## component_api_changed + +> %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information + ## each_key_duplicate > Keyed each block has duplicate key at indexes %a% and %b% diff --git a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js index 5259712266..a32f54ea78 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/transform-client.js @@ -232,6 +232,7 @@ export function client_component(source, analysis, options) { group_binding_declarations.push(b.const(group.name, b.array([]))); } + /** @type {Array} */ const component_returned_object = analysis.exports.map(({ name, alias }) => { const expression = serialize_get_binding(b.id(name), instance_state); @@ -310,41 +311,7 @@ export function client_component(source, analysis, options) { ) ); } else if (options.dev) { - component_returned_object.push( - b.init( - '$set', - b.thunk( - b.block([ - b.throw_error( - `The component shape you get when doing bind:this changed. Updating its properties via $set is no longer valid in Svelte 5. ` + - 'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information' - ) - ]) - ) - ), - b.init( - '$on', - b.thunk( - b.block([ - b.throw_error( - `The component shape you get when doing bind:this changed. Listening to events via $on is no longer valid in Svelte 5. ` + - 'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information' - ) - ]) - ) - ), - b.init( - '$destroy', - b.thunk( - b.block([ - b.throw_error( - `The component shape you get when doing bind:this changed. Destroying such a component via $destroy is no longer valid in Svelte 5. ` + - 'See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information' - ) - ]) - ) - ) - ); + component_returned_object.push(b.spread(b.call(b.id('$.legacy_api')))); } const push_args = [b.id('$$props'), b.literal(analysis.runes)]; diff --git a/packages/svelte/src/internal/client/dev/legacy.js b/packages/svelte/src/internal/client/dev/legacy.js new file mode 100644 index 0000000000..758e1a95b5 --- /dev/null +++ b/packages/svelte/src/internal/client/dev/legacy.js @@ -0,0 +1,20 @@ +import * as e from '../errors.js'; +import { current_component_context } from '../runtime.js'; +import { get_component } from './ownership.js'; + +export function legacy_api() { + const component = current_component_context?.function; + + /** @param {string} method */ + function error(method) { + // @ts-expect-error + const parent = get_component()?.filename ?? 'Something'; + e.component_api_changed(parent, method, component.filename); + } + + return { + $destroy: () => error('$destroy()'), + $on: () => error('$on(...)'), + $set: () => error('$set(...)') + }; +} diff --git a/packages/svelte/src/internal/client/dev/ownership.js b/packages/svelte/src/internal/client/dev/ownership.js index 3fb8ac6320..69b53e85fe 100644 --- a/packages/svelte/src/internal/client/dev/ownership.js +++ b/packages/svelte/src/internal/client/dev/ownership.js @@ -37,7 +37,7 @@ function get_stack() { * Determines which `.svelte` component is responsible for a given state change * @returns {Function | null} */ -function get_component() { +export function get_component() { // first 4 lines are svelte internals; adjust this number if we change the internal call stack const stack = get_stack()?.slice(4); if (!stack) return null; diff --git a/packages/svelte/src/internal/client/errors.js b/packages/svelte/src/internal/client/errors.js index 8a319df1ef..c8e9db5e90 100644 --- a/packages/svelte/src/internal/client/errors.js +++ b/packages/svelte/src/internal/client/errors.js @@ -56,6 +56,25 @@ export function bind_not_bindable(key, component, name) { } } +/** + * %parent% called `%method%` on an instance of %component%, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information + * @param {string} parent + * @param {string} method + * @param {string} component + * @returns {never} + */ +export function component_api_changed(parent, method, component) { + if (DEV) { + const error = new Error(`${"component_api_changed"}\n${`${parent} called \`${method}\` on an instance of ${component}, which is no longer valid in Svelte 5. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more information`}`); + + error.name = 'Svelte error'; + throw error; + } else { + // TODO print a link to the documentation + throw new Error("component_api_changed"); + } +} + /** * Keyed each block has duplicate key `%value%` at indexes %a% and %b% * @param {string} a diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 19a33ac95c..b2b68fcf44 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -6,6 +6,7 @@ export { mark_module_end, add_owner_effect } from './dev/ownership.js'; +export { legacy_api } from './dev/legacy.js'; export { inspect } from './dev/inspect.js'; export { await_block as await } from './dom/blocks/await.js'; export { if_block as if } from './dom/blocks/if.js';