Skip to content

Commit 72f5539

Browse files
authored
fix: remove runtime validation of components/snippets, rely on types instead (#12507)
closes #12446
1 parent 90d6f57 commit 72f5539

File tree

20 files changed

+69
-209
lines changed

20 files changed

+69
-209
lines changed

.changeset/moody-lions-watch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: remove runtime validation of components/snippets, rely on types instead

packages/svelte/messages/shared-errors/errors.md

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,6 @@
66

77
> `%name%(...)` can only be used during component initialisation
88
9-
## render_tag_invalid_argument
10-
11-
> 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 ...}`
12-
13-
## snippet_used_as_component
14-
15-
> A snippet must be rendered with `{@render ...}`
16-
179
## store_invalid_shape
1810

1911
> `%name%` is not a store with a `subscribe` method

packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/** @import { BlockStatement, CallExpression, Expression, ExpressionStatement, Identifier, Literal, MemberExpression, ObjectExpression, Pattern, Property, Statement, Super, TemplateElement, TemplateLiteral } from 'estree' */
22
/** @import { BindDirective } from '#compiler' */
3-
/** @import { ComponentClientTransformState } from '../types' */
43
import {
54
extract_identifiers,
65
extract_paths,
@@ -933,13 +932,7 @@ function serialize_inline_component(node, component_name, context, anchor = cont
933932

934933
/** @param {Expression} node_id */
935934
let fn = (node_id) => {
936-
return b.call(
937-
context.state.options.dev
938-
? b.call('$.validate_component', b.id(component_name))
939-
: component_name,
940-
node_id,
941-
props_expression
942-
);
935+
return b.call(component_name, node_id, props_expression);
943936
};
944937

945938
if (bind_this !== null) {
@@ -1876,9 +1869,6 @@ export const template_visitors = {
18761869
}
18771870

18781871
let snippet_function = /** @type {Expression} */ (context.visit(callee));
1879-
if (context.state.options.dev) {
1880-
snippet_function = b.call('$.validate_snippet', snippet_function);
1881-
}
18821872

18831873
if (node.metadata.dynamic) {
18841874
context.state.init.push(

packages/svelte/src/compiler/phases/3-transform/server/transform-server.js

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -966,13 +966,7 @@ function serialize_inline_component(node, expression, context) {
966966
if (slot_name === 'default' && !has_children_prop) {
967967
if (lets.length === 0 && children.default.every((node) => node.type !== 'SvelteFragment')) {
968968
// create `children` prop...
969-
push_prop(
970-
b.prop(
971-
'init',
972-
b.id('children'),
973-
context.state.options.dev ? b.call('$.add_snippet_symbol', slot_fn) : slot_fn
974-
)
975-
);
969+
push_prop(b.prop('init', b.id('children'), slot_fn));
976970

977971
// and `$$slots.default: true` so that `<slot>` on the child works
978972
serialized_slots.push(b.init(slot_name, b.true));
@@ -1004,7 +998,7 @@ function serialize_inline_component(node, expression, context) {
1004998
/** @type {import('estree').Statement} */
1005999
let statement = b.stmt(
10061000
(node.type === 'SvelteComponent' ? b.maybe_call : b.call)(
1007-
context.state.options.dev ? b.call('$.validate_component', expression) : expression,
1001+
expression,
10081002
b.id('$$payload'),
10091003
props_expression
10101004
)
@@ -1212,10 +1206,7 @@ const template_visitors = {
12121206
const callee = unwrap_optional(node.expression).callee;
12131207
const raw_args = unwrap_optional(node.expression).arguments;
12141208

1215-
const expression = /** @type {import('estree').Expression} */ (context.visit(callee));
1216-
const snippet_function = context.state.options.dev
1217-
? b.call('$.validate_snippet', expression)
1218-
: expression;
1209+
const snippet_function = /** @type {import('estree').Expression} */ (context.visit(callee));
12191210

12201211
const snippet_args = raw_args.map((arg) => {
12211212
return /** @type {import('estree').Expression} */ (context.visit(arg));
@@ -1498,10 +1489,6 @@ const template_visitors = {
14981489
fn.___snippet = true;
14991490
// TODO hoist where possible
15001491
context.state.init.push(fn);
1501-
1502-
if (context.state.options.dev) {
1503-
context.state.init.push(b.stmt(b.call('$.add_snippet_symbol', node.expression)));
1504-
}
15051492
},
15061493
Component(node, context) {
15071494
serialize_inline_component(node, b.id(node.name), context);

packages/svelte/src/index.d.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
22

3+
import type { Getters } from '#shared';
34
import './ambient.js';
45

56
/**
@@ -104,6 +105,15 @@ export class SvelteComponent<
104105
$set(props: Partial<Props>): void;
105106
}
106107

108+
declare const brand: unique symbol;
109+
type Brand<B> = { [brand]: B };
110+
type Branded<T, B> = T & Brand<B>;
111+
112+
/**
113+
* Internal implementation details that vary between environments
114+
*/
115+
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
116+
107117
/**
108118
* Can be used to create strongly typed Svelte components.
109119
*
@@ -136,7 +146,8 @@ export interface Component<
136146
* @param props The props passed to the component.
137147
*/
138148
(
139-
internal: unknown,
149+
this: void,
150+
internals: ComponentInternals,
140151
props: Props
141152
): {
142153
/**

packages/svelte/src/internal/client/dom/blocks/snippet.js

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** @import { Snippet } from 'svelte' */
22
/** @import { Effect, TemplateNode } from '#client' */
33
/** @import { Getters } from '#shared' */
4-
import { add_snippet_symbol } from '../../../shared/validate.js';
54
import { EFFECT_TRANSPARENT } from '../../constants.js';
65
import { branch, block, destroy_effect, teardown } from '../../reactivity/effects.js';
76
import {
@@ -55,7 +54,7 @@ export function snippet(node, get_snippet, ...args) {
5554
* @param {(node: TemplateNode, ...args: any[]) => void} fn
5655
*/
5756
export function wrap_snippet(component, fn) {
58-
return add_snippet_symbol((/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
57+
return (/** @type {TemplateNode} */ node, /** @type {any[]} */ ...args) => {
5958
var previous_component_function = dev_current_component_function;
6059
set_dev_current_component_function(component);
6160

@@ -64,7 +63,7 @@ export function wrap_snippet(component, fn) {
6463
} finally {
6564
set_dev_current_component_function(previous_component_function);
6665
}
67-
});
66+
};
6867
}
6968

7069
/**
@@ -77,32 +76,33 @@ export function wrap_snippet(component, fn) {
7776
* @returns {Snippet<Params>}
7877
*/
7978
export function createRawSnippet(fn) {
80-
return add_snippet_symbol(
81-
(/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
82-
var snippet = fn(...params);
83-
84-
/** @type {Element} */
85-
var element;
86-
87-
if (hydrating) {
88-
element = /** @type {Element} */ (hydrate_node);
89-
hydrate_next();
90-
} else {
91-
var html = snippet.render().trim();
92-
var fragment = create_fragment_from_html(html);
93-
element = /** @type {Element} */ (fragment.firstChild);
94-
if (DEV && (element.nextSibling !== null || element.nodeType !== 1)) {
95-
w.invalid_raw_snippet_render();
96-
}
97-
anchor.before(element);
98-
}
79+
// @ts-expect-error the types are a lie
80+
return (/** @type {TemplateNode} */ anchor, /** @type {Getters<Params>} */ ...params) => {
81+
var snippet = fn(...params);
82+
83+
/** @type {Element} */
84+
var element;
9985

100-
const result = snippet.setup?.(element);
101-
assign_nodes(element, element);
86+
if (hydrating) {
87+
element = /** @type {Element} */ (hydrate_node);
88+
hydrate_next();
89+
} else {
90+
var html = snippet.render().trim();
91+
var fragment = create_fragment_from_html(html);
92+
element = /** @type {Element} */ (fragment.firstChild);
10293

103-
if (typeof result === 'function') {
104-
teardown(result);
94+
if (DEV && (element.nextSibling !== null || element.nodeType !== 3)) {
95+
w.invalid_raw_snippet_render();
10596
}
97+
98+
anchor.before(element);
99+
}
100+
101+
const result = snippet.setup?.(element);
102+
assign_nodes(element, element);
103+
104+
if (typeof result === 'function') {
105+
teardown(result);
106106
}
107-
);
107+
};
108108
}

packages/svelte/src/internal/client/index.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,7 @@ export { snapshot } from '../shared/clone.js';
165165
export { noop } from '../shared/utils.js';
166166
export {
167167
invalid_default_snippet,
168-
validate_component,
169168
validate_dynamic_element_tag,
170-
validate_snippet,
171169
validate_store,
172170
validate_void_dynamic_element
173171
} from '../shared/validate.js';

packages/svelte/src/internal/client/render.js

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
import { reset_head_anchor } from './dom/blocks/svelte-head.js';
2525
import * as w from './warnings.js';
2626
import * as e from './errors.js';
27-
import { validate_component } from '../shared/validate.js';
2827
import { assign_nodes } from './dom/template.js';
2928

3029
/**
@@ -79,10 +78,6 @@ export function set_text(text, value) {
7978
* @returns {Exports}
8079
*/
8180
export function mount(component, options) {
82-
if (DEV) {
83-
validate_component(component);
84-
}
85-
8681
const anchor = options.anchor ?? options.target.appendChild(empty());
8782
// Don't flush previous effects to ensure order of outer effects stays consistent
8883
return flush_sync(() => _mount(component, { ...options, anchor }), false);
@@ -112,10 +107,6 @@ export function mount(component, options) {
112107
* @returns {Exports}
113108
*/
114109
export function hydrate(component, options) {
115-
if (DEV) {
116-
validate_component(component);
117-
}
118-
119110
options.intro = options.intro ?? false;
120111
const target = options.target;
121112
const was_hydrating = hydrating;

packages/svelte/src/internal/server/blocks/snippet.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/** @import { Snippet } from 'svelte' */
22
/** @import { Payload } from '#server' */
33
/** @import { Getters } from '#shared' */
4-
import { add_snippet_symbol } from '../../shared/validate.js';
54

65
/**
76
* Create a snippet programmatically
@@ -13,10 +12,11 @@ import { add_snippet_symbol } from '../../shared/validate.js';
1312
* @returns {Snippet<Params>}
1413
*/
1514
export function createRawSnippet(fn) {
16-
return add_snippet_symbol((/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
15+
// @ts-expect-error the types are a lie
16+
return (/** @type {Payload} */ payload, /** @type {Params} */ ...args) => {
1717
var getters = /** @type {Getters<Params>} */ (args.map((value) => () => value));
1818
payload.out += fn(...getters)
1919
.render()
2020
.trim();
21-
});
21+
};
2222
}

packages/svelte/src/internal/server/index.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,11 +555,8 @@ export { push_element, pop_element } from './dev.js';
555555
export { snapshot } from '../shared/clone.js';
556556

557557
export {
558-
add_snippet_symbol,
559558
invalid_default_snippet,
560-
validate_component,
561559
validate_dynamic_element_tag,
562-
validate_snippet,
563560
validate_void_dynamic_element
564561
} from '../shared/validate.js';
565562

packages/svelte/src/internal/shared/errors.js

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -35,38 +35,6 @@ export function lifecycle_outside_component(name) {
3535
}
3636
}
3737

38-
/**
39-
* 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 ...}`
40-
* @returns {never}
41-
*/
42-
export function render_tag_invalid_argument() {
43-
if (DEV) {
44-
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 ...}\``);
45-
46-
error.name = 'Svelte error';
47-
throw error;
48-
} else {
49-
// TODO print a link to the documentation
50-
throw new Error("render_tag_invalid_argument");
51-
}
52-
}
53-
54-
/**
55-
* A snippet must be rendered with `{@render ...}`
56-
* @returns {never}
57-
*/
58-
export function snippet_used_as_component() {
59-
if (DEV) {
60-
const error = new Error(`snippet_used_as_component\nA snippet must be rendered with \`{@render ...}\``);
61-
62-
error.name = 'Svelte error';
63-
throw error;
64-
} else {
65-
// TODO print a link to the documentation
66-
throw new Error("snippet_used_as_component");
67-
}
68-
}
69-
7038
/**
7139
* `%name%` is not a store with a `subscribe` method
7240
* @param {string} name

packages/svelte/src/internal/shared/validate.js

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,7 @@ import { is_void } from '../../constants.js';
44
import * as w from './warnings.js';
55
import * as e from './errors.js';
66

7-
const snippet_symbol = Symbol.for('svelte.snippet');
8-
9-
export const invalid_default_snippet = add_snippet_symbol(e.invalid_default_snippet);
10-
11-
/**
12-
* @param {any} fn
13-
* @returns {import('svelte').Snippet}
14-
*/
15-
/*@__NO_SIDE_EFFECTS__*/
16-
export function add_snippet_symbol(fn) {
17-
fn[snippet_symbol] = true;
18-
return fn;
19-
}
20-
21-
/**
22-
* Validate that the function handed to `{@render ...}` is a snippet function, and not some other kind of function.
23-
* @param {any} snippet_fn
24-
*/
25-
export function validate_snippet(snippet_fn) {
26-
if (snippet_fn && snippet_fn[snippet_symbol] !== true) {
27-
e.render_tag_invalid_argument();
28-
}
29-
30-
return snippet_fn;
31-
}
32-
33-
/**
34-
* Validate that the function behind `<Component />` isn't a snippet.
35-
* @param {any} component_fn
36-
*/
37-
export function validate_component(component_fn) {
38-
if (component_fn?.[snippet_symbol] === true) {
39-
e.snippet_used_as_component();
40-
}
41-
42-
return component_fn;
43-
}
7+
export { invalid_default_snippet } from './errors.js';
448

459
/**
4610
* @param {() => string} tag_fn

packages/svelte/tests/runtime-runes/samples/mount-snippet-error/_config.js

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)