use types rather than runtime validation to prevent incorrect snippet/component usage

pull/12507/head
Rich Harris 2 years ago
parent 30b143cef0
commit 3a032887d5

@ -2,14 +2,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

@ -969,13 +969,7 @@ function serialize_inline_component(node, expression, context) {
lets.length === 0 &&
children.default.every((node) => node.type !== 'SvelteFragment')
) {
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));
// We additionally add the default slot as a boolean, so that the slot render function on the other
// side knows it should get the content to render from $$props.children
serialized_slots.push(b.init('default', b.true));
@ -1501,10 +1495,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);

@ -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,10 @@ export class SvelteComponent<
$set(props: Partial<Props>): void;
}
declare const brand: unique symbol;
type Brand<B> = { [brand]: B };
type Branded<T, B> = T & Brand<B>;
/**
* Can be used to create strongly typed Svelte components.
*
@ -136,7 +141,8 @@ export interface Component<
* @param props The props passed to the component.
*/
(
internal: unknown,
this: void,
internal: Branded<{}, 'ComponentInternals'>,
props: Props
): {
/**
@ -271,13 +277,12 @@ declare const SnippetReturn: unique symbol;
export interface Snippet<Parameters extends unknown[] = []> {
(
this: void,
internal: Branded<{}, 'SnippetInternals'>,
// this conditional allows tuples but not arrays. Arrays would indicate a
// rest parameter type, which is not supported. If rest parameters are added
// in the future, the condition can be removed.
...args: number extends Parameters['length'] ? never : Parameters
): typeof SnippetReturn & {
_: 'functions passed to {@render ...} tags must use the `Snippet` type imported from "svelte"';
};
...args: number extends Parameters['length'] ? never : Getters<Parameters>
): void;
}
interface DispatchOptions {

@ -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 {
@ -50,10 +49,11 @@ export function snippet(node, get_snippet, ...args) {
* In development, wrap the snippet function so that it passes validation, and so that the
* correct component context is set for ownership checks
* @param {any} component
* @param {(node: TemplateNode, ...args: any[]) => void} fn
* @param {Snippet} fn
* @returns {Snippet}
*/
export function wrap_snippet(component, fn) {
return add_snippet_symbol((/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
return (node, ...args) => {
var previous_component_function = dev_current_component_function;
set_dev_current_component_function(component);
@ -62,7 +62,7 @@ export function wrap_snippet(component, fn) {
} finally {
set_dev_current_component_function(previous_component_function);
}
});
};
}
/**
@ -75,29 +75,27 @@ export function wrap_snippet(component, fn) {
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol(
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
var snippet = fn(...params);
return (anchor, ...params) => {
var snippet = fn(...params);
/** @type {Element} */
var element;
/** @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);
anchor.before(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);
/** @type {TemplateNode} */ (/** @type {unknown} */ (anchor)).before(element);
}
const result = snippet.setup?.(element);
assign_nodes(element, element);
const result = snippet.setup?.(element);
assign_nodes(element, element);
if (typeof result === 'function') {
teardown(result);
}
if (typeof result === 'function') {
teardown(result);
}
);
};
}

@ -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,10 @@ import { add_snippet_symbol } from '../../shared/validate.js';
* @returns {Snippet<Params>}
*/
export function createRawSnippet(fn) {
return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
return (payload, ...args) => {
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
payload.out += fn(...getters)
/** @type {Payload} */ (/** @type {unknown} */ (payload)).out += fn(...getters)
.render()
.trim();
});
};
}

@ -555,7 +555,6 @@ export { push_element, pop_element } from './dev.js';
export { snapshot } from '../shared/clone.js';
export {
add_snippet_symbol,
validate_component,
validate_dynamic_element_tag,
validate_snippet,

@ -4,27 +4,13 @@ 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');
/**
* @param {any} fn
* @returns {import('svelte').Snippet}
*/
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
* @param {Record<string, any> | undefined} $$props Only passed if render tag receives arguments and is for the children prop
*/
export function validate_snippet(snippet_fn, $$props) {
if (
($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') ||
(snippet_fn && snippet_fn[snippet_symbol] !== true)
) {
if ($$props?.$$slots?.default && typeof $$props.$$slots.default !== 'boolean') {
e.render_tag_invalid_argument();
}
@ -36,11 +22,7 @@ export function validate_snippet(snippet_fn, $$props) {
* @param {any} component_fn
*/
export function validate_component(component_fn) {
if (component_fn?.[snippet_symbol] === true) {
e.snippet_used_as_component();
}
return component_fn;
return component_fn; // TODO get rid
}
/**

@ -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 ...}`'
});
Loading…
Cancel
Save