fix: throw error if snippet is rendered in renderer different from compiler

pull/18058/head
paoloricciuti 1 month ago
parent ce4442d663
commit 7d37884d38

@ -235,6 +235,12 @@ The `%rune%` rune is only available inside `.svelte` and `.svelte.js/ts` files
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
### snippet_renderer_mismatch
```
A snippet created in a component with a custom renderer cannot be rendered by a different renderer
```
### state_descriptors_fixed
```

@ -177,6 +177,10 @@ This can happen if you render a hydratable on the client that was not rendered o
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
## snippet_renderer_mismatch
> A snippet created in a component with a custom renderer cannot be rendered by a different renderer
## state_descriptors_fixed
> Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.

@ -44,6 +44,12 @@ export function RenderTag(node, context) {
);
if (node.metadata.dynamic) {
// In custom renderer components, validate that the snippet is compatible
// with the current renderer before rendering it
if (context.state.analysis.custom_renderer) {
snippet_function = b.call('$.validate_snippet_renderer', b.id('$renderer'), snippet_function);
}
// If we have a chain expression then ensure a nullish snippet function gets turned into an empty one
if (node.expression.type === 'ChainExpression') {
snippet_function = b.logical('??', snippet_function, b.id('$.noop'));
@ -57,6 +63,12 @@ export function RenderTag(node, context) {
)
);
} else {
// In custom renderer components, validate that the snippet is compatible
// with the current renderer before rendering it
if (context.state.analysis.custom_renderer) {
snippet_function = b.call('$.validate_snippet_renderer', b.id('$renderer'), snippet_function);
}
statements.push(
add_svelte_meta(
(node.expression.type === 'CallExpression' ? b.call : b.maybe_call)(

@ -79,6 +79,12 @@ export function SnippetBlock(node, context) {
? b.call('$.wrap_snippet', b.id(context.state.analysis.name), b.function(null, args, body))
: b.arrow(args, body);
// wrap snippets in components with a custom renderer so they can only be
// rendered by the same renderer that compiled them
if (context.state.analysis.custom_renderer) {
snippet = b.call('$.renderer_snippet', b.id('$renderer'), snippet);
}
const declaration = b.const(node.expression, snippet);
// Top-level snippets are hoisted so they can be referenced in the `<script>`

@ -62,6 +62,48 @@ export function wrap_snippet(component, fn) {
return snippet;
}
/**
* Wraps a snippet function created in a component with a custom renderer,
* ensuring it can only be rendered by the same renderer.
* @template {(...args: any[]) => void} T
* @param {any} expected_renderer
* @param {T} fn
* @returns {T}
*/
export function renderer_snippet(expected_renderer, fn) {
var wrapped = /** @type {T} */ (
(.../** @type {any[]} */ args) => {
if (renderer !== expected_renderer) {
e.snippet_renderer_mismatch();
}
return fn(...args);
}
);
// we could technically avoid checking for expected_renderer in the function
// and store it in the returned function to check with `validate_snippet_renderer`
// but this keeps all the changes on the custom renderer side and leave the paths
// of "normal svelte" untouched...since that's the default people are gonna use
// svelte with we should optimize for that case
/** @type {any} */ (wrapped).__renderer = expected_renderer;
return wrapped;
}
/**
* Validates that a snippet function is compatible with the given renderer.
* Used at render sites in custom renderer components.
* @template {((...args: any[]) => void) | null | undefined} T
* @param {any} expected_renderer
* @param {T} fn
* @returns {T}
*/
export function validate_snippet_renderer(expected_renderer, fn) {
if (fn != null && /** @type {any} */ (fn).__renderer !== expected_renderer) {
e.snippet_renderer_mismatch();
}
return fn;
}
/**
* Create a snippet programmatically
* @template {unknown[]} Params

@ -461,6 +461,22 @@ export function set_context_after_init() {
}
}
/**
* A snippet created in a component with a custom renderer cannot be rendered by a different renderer
* @returns {never}
*/
export function snippet_renderer_mismatch() {
if (DEV) {
const error = new Error(`snippet_renderer_mismatch\nA snippet created in a component with a custom renderer cannot be rendered by a different renderer\nhttps://svelte.dev/e/snippet_renderer_mismatch`);
error.name = 'Svelte error';
throw error;
} else {
throw new Error(`https://svelte.dev/e/snippet_renderer_mismatch`);
}
}
/**
* Property descriptors defined on `$state` objects must contain `value` and always be `enumerable`, `configurable` and `writable`.
* @returns {never}

@ -18,7 +18,12 @@ export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js';
export { html } from './dom/blocks/html.js';
export { sanitize_slots, slot } from './dom/blocks/slot.js';
export { snippet, wrap_snippet } from './dom/blocks/snippet.js';
export {
snippet,
wrap_snippet,
renderer_snippet,
validate_snippet_renderer
} from './dom/blocks/snippet.js';
export { component } from './dom/blocks/svelte-component.js';
export { element } from './dom/blocks/svelte-element.js';
export { head } from './dom/blocks/svelte-head.js';

@ -0,0 +1,9 @@
<svelte:options customRenderer={null} />
<script>
let { greeting } = $props();
</script>
<div>
{@render greeting('world')}
</div>

@ -0,0 +1,9 @@
<svelte:options customRenderer={null} />
<script module>
export { greeting };
</script>
{#snippet greeting(name)}
<span>hello {name}</span>
{/snippet}

@ -0,0 +1,9 @@
import { test } from '../../test-dom.test';
export default test({
// The key assertion is that this test does not throw.
// A DOM module snippet imported by a custom renderer component and
// passed to a DOM child component should work without errors.
// We don't check html because the DOM child renders into the real DOM,
// not the custom renderer tree.
});

@ -0,0 +1,8 @@
<script>
import { greeting } from './DomSource.svelte';
import DomChild from './DomChild.svelte';
</script>
<div>
<DomChild {greeting}></DomChild>
</div>

@ -0,0 +1,9 @@
<svelte:options customRenderer={null} />
<script>
let { greeting } = $props();
</script>
<div>
{@render greeting('world')}
</div>

@ -0,0 +1,6 @@
import { test } from '../../test-dom.test';
export default test({
error:
'A snippet created in a component with a custom renderer cannot be rendered by a different renderer'
});

@ -0,0 +1,11 @@
<script>
import Child from './Child.svelte';
</script>
{#snippet greeting(name)}
<span>hello {name}</span>
{/snippet}
<div>
<Child {greeting}></Child>
</div>

@ -0,0 +1,9 @@
<svelte:options customRenderer={null} />
<script module>
export { greeting };
</script>
{#snippet greeting(name)}
<span>hello {name}</span>
{/snippet}

@ -0,0 +1,6 @@
import { test } from '../../test-dom.test';
export default test({
error:
'A snippet created in a component with a custom renderer cannot be rendered by a different renderer'
});

@ -0,0 +1,7 @@
<script>
import { greeting } from './DomComponent.svelte';
</script>
<div>
{@render greeting('world')}
</div>

@ -1,5 +1,6 @@
import { test } from '../../test';
export default test({
error: '`createRawSnippet` cannot be used with a custom renderer'
error:
'A snippet created in a component with a custom renderer cannot be rendered by a different renderer'
});

Loading…
Cancel
Save