fix: remove runtime validation of components/snippets, rely on types instead (#12507)

closes #12446
pull/12562/head
Rich Harris 5 months ago committed by GitHub
parent 90d6f573e3
commit 72f5539f51
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: remove runtime validation of components/snippets, rely on types instead

@ -6,14 +6,6 @@
> `%name%(...)` can only be used during component initialisation > `%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 ## store_invalid_shape
> `%name%` is not a store with a `subscribe` method > `%name%` is not a store with a `subscribe` method

@ -1,6 +1,5 @@
/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */ /** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
/** @import { BindDirective } from '#compiler' */ /** @import { BindDirective } from '#compiler' */
/** @import { ComponentClientTransformState } from '../types' */
import { import {
extract_identifiers, extract_identifiers,
extract_paths, extract_paths,
@ -933,13 +932,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
/** @param {Expression} node_id */ /** @param {Expression} node_id */
let fn = (node_id) => { let fn = (node_id) => {
return b.call( return b.call(component_name, node_id, props_expression);
context.state.options.dev
? b.call('$.validate_component', b.id(component_name))
: component_name,
node_id,
props_expression
);
}; };
if (bind_this !== null) { if (bind_this !== null) {
@ -1876,9 +1869,6 @@ export const template_visitors = {
} }
let snippet_function = /** @type {Expression} */ (context.visit(callee)); 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) { if (node.metadata.dynamic) {
context.state.init.push( context.state.init.push(

@ -966,13 +966,7 @@ function serialize_inline_component(node, expression, context) {
if (slot_name === 'default' && !has_children_prop) { if (slot_name === 'default' && !has_children_prop) {
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) { if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
// create `children` prop... // create `children` prop...
push_prop( push_prop(b.prop('init', b.id('children'), slot_fn));
b.prop(
'init',
b.id('children'),
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
)
);
// and `$$slots.default: true` so that `<slot>` on the child works // and `$$slots.default: true` so that `<slot>` on the child works
serialized_slots.push(b.init(slot_name, b.true)); serialized_slots.push(b.init(slot_name, b.true));
@ -1004,7 +998,7 @@ function serialize_inline_component(node, expression, context) {
/** @type {import('estree').Statement} */ /** @type {import('estree').Statement} */
let statement = b.stmt( let statement = b.stmt(
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)( (node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
context.state.options.dev ? b.call('$.validate_component', expression) : expression, expression,
b.id('$$payload'), b.id('$$payload'),
props_expression props_expression
) )
@ -1212,10 +1206,7 @@ const template_visitors = {
const callee = unwrap_optional(node.expression).callee; const callee = unwrap_optional(node.expression).callee;
const raw_args = unwrap_optional(node.expression).arguments; const raw_args = unwrap_optional(node.expression).arguments;
const expression = /** @type {import('estree').Expression} */ (context.visit(callee)); const snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
const snippet_function = context.state.options.dev
? b.call('$.validate_snippet', expression)
: expression;
const snippet_args = raw_args.map((arg) => { const snippet_args = raw_args.map((arg) => {
return /** @type {import('estree').Expression} */ (context.visit(arg)); return /** @type {import('estree').Expression} */ (context.visit(arg));
@ -1498,10 +1489,6 @@ const template_visitors = {
fn.___snippet = true; fn.___snippet = true;
// TODO hoist where possible // TODO hoist where possible
context.state.init.push(fn); 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) { Component(node, context) {
serialize_inline_component(node, b.id(node.name), context); serialize_inline_component(node, b.id(node.name), context);

@ -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). // 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'; import './ambient.js';
/** /**
@ -104,6 +105,15 @@ export class SvelteComponent<
$set(props: Partial<Props>): void; $set(props: Partial<Props>): void;
} }
declare const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Internal implementation details that vary between environments
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
/** /**
* Can be used to create strongly typed Svelte components. * Can be used to create strongly typed Svelte components.
* *
@ -136,7 +146,8 @@ export interface Component<
* @param props The props passed to the component. * @param props The props passed to the component.
*/ */
( (
internal: unknown, this: void,
internals: ComponentInternals,
props: Props props: Props
): { ): {
/** /**

@ -1,7 +1,6 @@
/** @import { Snippet } from 'svelte' */ /** @import { Snippet } from 'svelte' */
/** @import { Effect, TemplateNode } from '#client' */ /** @import { Effect, TemplateNode } from '#client' */
/** @import { Getters } from '#shared' */ /** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../../shared/validate.js';
import { EFFECT_TRANSPARENT } from '../../constants.js'; import { EFFECT_TRANSPARENT } from '../../constants.js';
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js'; import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
import { import {
@ -55,7 +54,7 @@ export function snippet(node, get_snippet, ...args) {
* @param {(node: TemplateNode, ...args: any[]) => void} fn * @param {(node: TemplateNode, ...args: any[]) => void} fn
*/ */
export function wrap_snippet(component, 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; var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component); set_dev_current_component_function(component);
@ -64,7 +63,7 @@ export function wrap_snippet(component, fn) {
} finally { } finally {
set_dev_current_component_function(previous_component_function); set_dev_current_component_function(previous_component_function);
} }
}); };
} }
/** /**
@ -77,8 +76,8 @@ export function wrap_snippet(component, fn) {
* @returns {Snippet<Params>} * @returns {Snippet<Params>}
*/ */
export function createRawSnippet(fn) { export function createRawSnippet(fn) {
return add_snippet_symbol( // @ts-expect-error the types are a lie
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => { return (/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params); var snippet = fn(...params);
/** @type {Element} */ /** @type {Element} */
@ -91,9 +90,11 @@ export function createRawSnippet(fn) {
var html = snippet.render().trim(); var html = snippet.render().trim();
var fragment = create_fragment_from_html(html); var fragment = create_fragment_from_html(html);
element = /** @type {Element} */ (fragment.firstChild); element = /** @type {Element} */ (fragment.firstChild);
if (DEV && (element.nextSibling !== null || element.nodeType !== 1)) {
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
w.invalid_raw_snippet_render(); w.invalid_raw_snippet_render();
} }
anchor.before(element); anchor.before(element);
} }
@ -103,6 +104,5 @@ export function createRawSnippet(fn) {
if (typeof result === 'function') { if (typeof result === 'function') {
teardown(result); teardown(result);
} }
} };
);
} }

@ -165,9 +165,7 @@ export { snapshot } from '../shared/clone.js';
export { noop } from '../shared/utils.js'; export { noop } from '../shared/utils.js';
export { export {
invalid_default_snippet, invalid_default_snippet,
validate_component,
validate_dynamic_element_tag, validate_dynamic_element_tag,
validate_snippet,
validate_store, validate_store,
validate_void_dynamic_element validate_void_dynamic_element
} from '../shared/validate.js'; } from '../shared/validate.js';

@ -24,7 +24,6 @@ import {
import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
import { validate_component } from '../shared/validate.js';
import { assign_nodes } from './dom/template.js'; import { assign_nodes } from './dom/template.js';
/** /**
@ -79,10 +78,6 @@ export function set_text(text, value) {
* @returns {Exports} * @returns {Exports}
*/ */
export function mount(component, options) { export function mount(component, options) {
if (DEV) {
validate_component(component);
}
const anchor = options.anchor ?? options.target.appendChild(empty()); const anchor = options.anchor ?? options.target.appendChild(empty());
// Don't flush previous effects to ensure order of outer effects stays consistent // Don't flush previous effects to ensure order of outer effects stays consistent
return flush_sync(() => _mount(component, { ...options, anchor }), false); return flush_sync(() => _mount(component, { ...options, anchor }), false);
@ -112,10 +107,6 @@ export function mount(component, options) {
* @returns {Exports} * @returns {Exports}
*/ */
export function hydrate(component, options) { export function hydrate(component, options) {
if (DEV) {
validate_component(component);
}
options.intro = options.intro ?? false; options.intro = options.intro ?? false;
const target = options.target; const target = options.target;
const was_hydrating = hydrating; const was_hydrating = hydrating;

@ -1,7 +1,6 @@
/** @import { Snippet } from 'svelte' */ /** @import { Snippet } from 'svelte' */
/** @import { Payload } from '#server' */ /** @import { Payload } from '#server' */
/** @import { Getters } from '#shared' */ /** @import { Getters } from '#shared' */
import { add_snippet_symbol } from '../../shared/validate.js';
/** /**
* Create a snippet programmatically * Create a snippet programmatically
@ -13,10 +12,11 @@ import { add_snippet_symbol } from '../../shared/validate.js';
* @returns {Snippet<Params>} * @returns {Snippet<Params>}
*/ */
export function createRawSnippet(fn) { 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<Params>} */ (args.map((value) => () => value)); var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out += fn(...getters) payload.out += fn(...getters)
.render() .render()
.trim(); .trim();
}); };
} }

@ -555,11 +555,8 @@ export { push_element, pop_element } from './dev.js';
export { snapshot } from '../shared/clone.js'; export { snapshot } from '../shared/clone.js';
export { export {
add_snippet_symbol,
invalid_default_snippet, invalid_default_snippet,
validate_component,
validate_dynamic_element_tag, validate_dynamic_element_tag,
validate_snippet,
validate_void_dynamic_element validate_void_dynamic_element
} from '../shared/validate.js'; } from '../shared/validate.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 * `%name%` is not a store with a `subscribe` method
* @param {string} name * @param {string} name

@ -4,43 +4,7 @@ import { is_void } from '../../constants.js';
import * as w from './warnings.js'; import * as w from './warnings.js';
import * as e from './errors.js'; import * as e from './errors.js';
const snippet_symbol = Symbol.for('svelte.snippet'); export { invalid_default_snippet } from './errors.js';
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 `<Component />` 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;
}
/** /**
* @param {() => string} tag_fn * @param {() => string} tag_fn

@ -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 ...}`'
});

@ -1,14 +0,0 @@
<script>
import { onMount, mount } from 'svelte';
let el;
onMount(() => {
mount(foo, { target: el });
});
</script>
<div bind:this={el}></div>
{#snippet foo()}
shouldnt be rendered
{/snippet}

@ -1,8 +0,0 @@
import { test } from '../../test';
export default test({
compileOptions: {
dev: true
},
error: 'render_tag_invalid_argument'
});

@ -1,7 +0,0 @@
<script>
function not_a_snippet() {
console.log('hello');
}
</script>
{@render not_a_snippet()}

@ -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 ...}`'
});

@ -6,7 +6,8 @@ import {
type ComponentType, type ComponentType,
mount, mount,
hydrate, hydrate,
type Component type Component,
type ComponentInternals
} from 'svelte'; } from 'svelte';
import { render } from 'svelte/server'; 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) // but should always pass in tsc (because it will never know about this fact)
import Foo from './doesntexist.svelte'; 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 } }); const f: Foo = new Foo({ target: document.body, props: { a: true } });

@ -101,6 +101,15 @@ declare module 'svelte' {
$set(props: Partial<Props>): void; $set(props: Partial<Props>): void;
} }
const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Internal implementation details that vary between environments
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
/** /**
* Can be used to create strongly typed Svelte components. * Can be used to create strongly typed Svelte components.
* *
@ -133,7 +142,8 @@ declare module 'svelte' {
* @param props The props passed to the component. * @param props The props passed to the component.
*/ */
( (
internal: unknown, this: void,
internals: ComponentInternals,
props: Props props: Props
): { ): {
/** /**
@ -293,6 +303,9 @@ declare module 'svelte' {
: [type: Type, parameter: EventMap[Type], options?: DispatchOptions] : [type: Type, parameter: EventMap[Type], options?: DispatchOptions]
): boolean; ): boolean;
} }
type Getters<T> = {
[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. * 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; * 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 * https://svelte.dev/docs/svelte#getallcontexts
* */ * */
export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T; export function getAllContexts<T extends Map<any, any> = Map<any, any>>(): T;
type Getters<T> = {
[K in keyof T]: () => T[K];
};
export {}; export {};
} }

Loading…
Cancel
Save