diff --git a/.changeset/moody-lions-watch.md b/.changeset/moody-lions-watch.md new file mode 100644 index 000000000000..8efd727883a2 --- /dev/null +++ b/.changeset/moody-lions-watch.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: remove runtime validation of components/snippets, rely on types instead diff --git a/packages/svelte/messages/shared-errors/errors.md b/packages/svelte/messages/shared-errors/errors.md index 8748182a8104..ef030c8a1c91 100644 --- a/packages/svelte/messages/shared-errors/errors.md +++ b/packages/svelte/messages/shared-errors/errors.md @@ -6,14 +6,6 @@ > `%name%(...)` can only be used during component initialisation -## render_tag_invalid_argument - -> The argument to `{@render ...}` must be a snippet function, not a component or a slot with a `let:` directive or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}` - -## snippet_used_as_component - -> A snippet must be rendered with `{@render ...}` - ## store_invalid_shape > `%name%` is not a store with a `subscribe` method 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 4683a945bd2d..79bc9889e14b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1,6 +1,5 @@ /** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */ /** @import { BindDirective } from '#compiler' */ -/** @import { ComponentClientTransformState } from '../types' */ import { extract_identifiers, extract_paths, @@ -929,13 +928,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont /** @param {Expression} node_id */ let fn = (node_id) => { - return b.call( - context.state.options.dev - ? b.call('$.validate_component', b.id(component_name)) - : component_name, - node_id, - props_expression - ); + return b.call(component_name, node_id, props_expression); }; if (bind_this !== null) { @@ -1868,9 +1861,6 @@ export const template_visitors = { } let snippet_function = /** @type {Expression} */ (context.visit(callee)); - if (context.state.options.dev) { - snippet_function = b.call('$.validate_snippet', snippet_function); - } if (node.metadata.dynamic) { context.state.init.push( 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 1d0695b5f0d0..0a5d05a6ed85 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 @@ -966,13 +966,7 @@ function serialize_inline_component(node, expression, context) { if (slot_name === 'default' && !has_children_prop) { if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) { // create `children` prop... - push_prop( - b.prop( - 'init', - b.id('children'), - context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn - ) - ); + push_prop(b.prop('init', b.id('children'), slot_fn)); // and `$$slots.default: true` so that `` on the child works serialized_slots.push(b.init(slot_name, b.true)); @@ -1004,7 +998,7 @@ function serialize_inline_component(node, expression, context) { /** @type {import('estree').Statement} */ let statement = b.stmt( (node.type === 'SvelteComponent' ? b.maybe_call : b.call)( - context.state.options.dev ? b.call('$.validate_component', expression) : expression, + expression, b.id('$$payload'), props_expression ) @@ -1212,10 +1206,7 @@ const template_visitors = { const callee = unwrap_optional(node.expression).callee; const raw_args = unwrap_optional(node.expression).arguments; - const expression = /** @type {import('estree').Expression} */ (context.visit(callee)); - const snippet_function = context.state.options.dev - ? b.call('$.validate_snippet', expression) - : expression; + const snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee)); const snippet_args = raw_args.map((arg) => { return /** @type {import('estree').Expression} */ (context.visit(arg)); @@ -1498,10 +1489,6 @@ const template_visitors = { fn.___snippet = true; // TODO hoist where possible context.state.init.push(fn); - - if (context.state.options.dev) { - context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression))); - } }, Component(node, context) { serialize_inline_component(node, b.id(node.name), context); diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 511bb6c02ec0..5a052e3c1253 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -1,5 +1,6 @@ // This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are). +import type { Getters } from '#shared'; import './ambient.js'; /** @@ -104,6 +105,15 @@ export class SvelteComponent< $set(props: Partial): void; } +declare const brand: unique symbol; +type Brand = { [brand]: B }; +type Branded = T & Brand; + +/** + * Internal implementation details that vary between environments + */ +export type ComponentInternals = Branded<{}, 'ComponentInternals'>; + /** * Can be used to create strongly typed Svelte components. * @@ -136,7 +146,8 @@ export interface Component< * @param props The props passed to the component. */ ( - internal: unknown, + this: void, + internals: ComponentInternals, props: Props ): { /** diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index ea65e5b8531a..6a6203d16ee2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -1,7 +1,6 @@ /** @import { Snippet } from 'svelte' */ /** @import { Effect, TemplateNode } from '#client' */ /** @import { Getters } from '#shared' */ -import { add_snippet_symbol } from '../../../shared/validate.js'; import { EFFECT_TRANSPARENT } from '../../constants.js'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { @@ -55,7 +54,7 @@ export function snippet(node, get_snippet, ...args) { * @param {(node: TemplateNode, ...args: any[]) => void} fn */ export function wrap_snippet(component, fn) { - return add_snippet_symbol((/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => { + return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => { var previous_component_function = dev_current_component_function; set_dev_current_component_function(component); @@ -64,7 +63,7 @@ export function wrap_snippet(component, fn) { } finally { set_dev_current_component_function(previous_component_function); } - }); + }; } /** @@ -77,32 +76,33 @@ export function wrap_snippet(component, fn) { * @returns {Snippet} */ export function createRawSnippet(fn) { - return add_snippet_symbol( - (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { - var snippet = fn(...params); - - /** @type {Element} */ - var element; - - if (hydrating) { - element = /** @type {Element} */ (hydrate_node); - hydrate_next(); - } else { - var html = snippet.render().trim(); - var fragment = create_fragment_from_html(html); - element = /** @type {Element} */ (fragment.firstChild); - if (DEV && (element.nextSibling !== null || element.nodeType !== 1)) { - w.invalid_raw_snippet_render(); - } - anchor.before(element); - } + // @ts-expect-error the types are a lie + return (/** @type {TemplateNode} */ anchor, /** @type {Getters} */ ...params) => { + var snippet = fn(...params); + + /** @type {Element} */ + var element; - const result = snippet.setup?.(element); - assign_nodes(element, element); + if (hydrating) { + element = /** @type {Element} */ (hydrate_node); + hydrate_next(); + } else { + var html = snippet.render().trim(); + var fragment = create_fragment_from_html(html); + element = /** @type {Element} */ (fragment.firstChild); - if (typeof result === 'function') { - teardown(result); + if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) { + w.invalid_raw_snippet_render(); } + + anchor.before(element); + } + + const result = snippet.setup?.(element); + assign_nodes(element, element); + + if (typeof result === 'function') { + teardown(result); } - ); + }; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index 859e85f22c50..6af6e0a4a667 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -165,9 +165,7 @@ export { snapshot } from '../shared/clone.js'; export { noop } from '../shared/utils.js'; export { invalid_default_snippet, - validate_component, validate_dynamic_element_tag, - validate_snippet, validate_store, validate_void_dynamic_element } from '../shared/validate.js'; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 8510d49dab43..21cc096325af 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -24,7 +24,6 @@ import { import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; -import { validate_component } from '../shared/validate.js'; import { assign_nodes } from './dom/template.js'; /** @@ -79,10 +78,6 @@ export function set_text(text, value) { * @returns {Exports} */ export function mount(component, options) { - if (DEV) { - validate_component(component); - } - const anchor = options.anchor ?? options.target.appendChild(empty()); // Don't flush previous effects to ensure order of outer effects stays consistent return flush_sync(() => _mount(component, { ...options, anchor }), false); @@ -112,10 +107,6 @@ export function mount(component, options) { * @returns {Exports} */ export function hydrate(component, options) { - if (DEV) { - validate_component(component); - } - options.intro = options.intro ?? false; const target = options.target; const was_hydrating = hydrating; diff --git a/packages/svelte/src/internal/server/blocks/snippet.js b/packages/svelte/src/internal/server/blocks/snippet.js index b9f72063f4d7..d9469be511a0 100644 --- a/packages/svelte/src/internal/server/blocks/snippet.js +++ b/packages/svelte/src/internal/server/blocks/snippet.js @@ -1,7 +1,6 @@ /** @import { Snippet } from 'svelte' */ /** @import { Payload } from '#server' */ /** @import { Getters } from '#shared' */ -import { add_snippet_symbol } from '../../shared/validate.js'; /** * Create a snippet programmatically @@ -13,10 +12,11 @@ import { add_snippet_symbol } from '../../shared/validate.js'; * @returns {Snippet} */ export function createRawSnippet(fn) { - return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { + // @ts-expect-error the types are a lie + return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => { var getters = /** @type {Getters} */ (args.map((value) => () => value)); payload.out += fn(...getters) .render() .trim(); - }); + }; } diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 6300924ab278..b962e0e65ac6 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -555,11 +555,8 @@ export { push_element, pop_element } from './dev.js'; export { snapshot } from '../shared/clone.js'; export { - add_snippet_symbol, invalid_default_snippet, - validate_component, validate_dynamic_element_tag, - validate_snippet, validate_void_dynamic_element } from '../shared/validate.js'; diff --git a/packages/svelte/src/internal/shared/errors.js b/packages/svelte/src/internal/shared/errors.js index 8f0ec8eea443..0d72800fb764 100644 --- a/packages/svelte/src/internal/shared/errors.js +++ b/packages/svelte/src/internal/shared/errors.js @@ -35,38 +35,6 @@ export function lifecycle_outside_component(name) { } } -/** - * The argument to `{@render ...}` must be a snippet function, not a component or a slot with a `let:` directive or some other kind of function. If you want to dynamically render one snippet or another, use `$derived` and pass its result to `{@render ...}` - * @returns {never} - */ -export function render_tag_invalid_argument() { - if (DEV) { - const error = new Error(`render_tag_invalid_argument\nThe argument to \`{@render ...}\` must be a snippet function, not a component or a slot with a \`let:\` directive or some other kind of function. If you want to dynamically render one snippet or another, use \`$derived\` and pass its result to \`{@render ...}\``); - - error.name = 'Svelte error'; - throw error; - } else { - // TODO print a link to the documentation - throw new Error("render_tag_invalid_argument"); - } -} - -/** - * A snippet must be rendered with `{@render ...}` - * @returns {never} - */ -export function snippet_used_as_component() { - if (DEV) { - const error = new Error(`snippet_used_as_component\nA snippet must be rendered with \`{@render ...}\``); - - error.name = 'Svelte error'; - throw error; - } else { - // TODO print a link to the documentation - throw new Error("snippet_used_as_component"); - } -} - /** * `%name%` is not a store with a `subscribe` method * @param {string} name diff --git a/packages/svelte/src/internal/shared/validate.js b/packages/svelte/src/internal/shared/validate.js index a8acc167c876..d0b5f323377b 100644 --- a/packages/svelte/src/internal/shared/validate.js +++ b/packages/svelte/src/internal/shared/validate.js @@ -4,43 +4,7 @@ import { is_void } from '../../constants.js'; import * as w from './warnings.js'; import * as e from './errors.js'; -const snippet_symbol = Symbol.for('svelte.snippet'); - -export const invalid_default_snippet = add_snippet_symbol(e.invalid_default_snippet); - -/** - * @param {any} fn - * @returns {import('svelte').Snippet} - */ -/*@__NO_SIDE_EFFECTS__*/ -export function add_snippet_symbol(fn) { - fn[snippet_symbol] = true; - return fn; -} - -/** - * Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function. - * @param {any} snippet_fn - */ -export function validate_snippet(snippet_fn) { - if (snippet_fn && snippet_fn[snippet_symbol] !== true) { - e.render_tag_invalid_argument(); - } - - return snippet_fn; -} - -/** - * Validate that the function behind `` isn't a snippet. - * @param {any} component_fn - */ -export function validate_component(component_fn) { - if (component_fn?.[snippet_symbol] === true) { - e.snippet_used_as_component(); - } - - return component_fn; -} +export { invalid_default_snippet } from './errors.js'; /** * @param {() => string} tag_fn diff --git a/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/_config.js b/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/_config.js deleted file mode 100644 index 3ba2b358ea52..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/_config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - async test({ assert, target }) { - const div = target.querySelector('div'); - assert.htmlEqual(div?.innerHTML || '', ''); - }, - runtime_error: 'snippet_used_as_component\nA snippet must be rendered with `{@render ...}`' -}); diff --git a/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/main.svelte b/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/main.svelte deleted file mode 100644 index 075fbba24ee8..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/mount-snippet-error/main.svelte +++ /dev/null @@ -1,14 +0,0 @@ - - -
-{#snippet foo()} - shouldnt be rendered -{/snippet} \ No newline at end of file diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/_config.js deleted file mode 100644 index 288edc26a614..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - error: 'render_tag_invalid_argument' -}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/main.svelte deleted file mode 100644 index d07df452da8b..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-1/main.svelte +++ /dev/null @@ -1,7 +0,0 @@ - - -{@render not_a_snippet()} diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/_config.js b/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/_config.js deleted file mode 100644 index b549144bf51c..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/_config.js +++ /dev/null @@ -1,8 +0,0 @@ -import { test } from '../../test'; - -export default test({ - compileOptions: { - dev: true - }, - error: 'snippet_used_as_component\nA snippet must be rendered with `{@render ...}`' -}); diff --git a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/main.svelte b/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/main.svelte deleted file mode 100644 index e24e9549e32e..000000000000 --- a/packages/svelte/tests/runtime-runes/samples/snippet-validation-error-2/main.svelte +++ /dev/null @@ -1,5 +0,0 @@ -{#snippet Foo()} -

hello

-{/snippet} - - diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index fc806537969c..eccf08548ec1 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -6,7 +6,8 @@ import { type ComponentType, mount, hydrate, - type Component + type Component, + type ComponentInternals } from 'svelte'; import { render } from 'svelte/server'; @@ -338,5 +339,5 @@ render(functionComponent, { // but should always pass in tsc (because it will never know about this fact) import Foo from './doesntexist.svelte'; -Foo(null, { a: true }); +Foo(null as unknown as ComponentInternals, { a: true }); const f: Foo = new Foo({ target: document.body, props: { a: true } }); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 86f33588a67b..ba71b15d0508 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -101,6 +101,15 @@ declare module 'svelte' { $set(props: Partial): void; } + const brand: unique symbol; + type Brand = { [brand]: B }; + type Branded = T & Brand; + + /** + * Internal implementation details that vary between environments + */ + export type ComponentInternals = Branded<{}, 'ComponentInternals'>; + /** * Can be used to create strongly typed Svelte components. * @@ -133,7 +142,8 @@ declare module 'svelte' { * @param props The props passed to the component. */ ( - internal: unknown, + this: void, + internals: ComponentInternals, props: Props ): { /** @@ -293,6 +303,9 @@ declare module 'svelte' { : [type: Type, parameter: EventMap[Type], options?: DispatchOptions] ): boolean; } + type Getters = { + [K in keyof T]: () => T[K]; + }; /** * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. * It must be called during the component's initialisation (but doesn't need to live *inside* the component; @@ -457,9 +470,6 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; - type Getters = { - [K in keyof T]: () => T[K]; - }; export {}; }