fix: error when using `let:` directives on a component with a `children` snippet (#18873)

Closes #13864

A `let:` directive on a component only applies to the default slot
content. If the content is an explicit `{#snippet children()}` block,
the value is never provided, so code like this compiles but fails at
runtime with "value is not defined":

```svelte
<Child let:value>
	{#snippet children()}Value is: {value}{/snippet}
</Child>
```

This adds a compile error on the directive that points to snippet
parameters instead (`{#snippet children({ value })}`, or `{ item: x }`
for `let:item={x}`). Components with a `slot` attribute are not
affected, since there the directive applies to the component itself. An
unused `let:` next to a `children` snippet is also flagged now, but it
never had any effect.

### Before submitting the PR, please make sure you do the following

- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

Co-authored-by: waltu <182410443+waltuov@users.noreply.github.com>
pull/18871/merge
waltu 6 days ago committed by GitHub
parent 75aa6ba38d
commit a956c2bd1f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: error when using `let:` directives on a component with a `children` snippet

@ -677,6 +677,12 @@ Cannot use `$$restProps` in runes mode
`let:` directive at invalid position
```
### let_directive_snippet_conflict
```
Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
```
### mixed_event_handler_syntaxes
```

@ -279,6 +279,10 @@ The same applies to components:
> `let:` directive at invalid position
## let_directive_snippet_conflict
> Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
## mixed_event_handler_syntaxes
> Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax

@ -1213,6 +1213,16 @@ export function let_directive_invalid_placement(node) {
e(node, 'let_directive_invalid_placement', `\`let:\` directive at invalid position\nhttps://svelte.dev/e/let_directive_invalid_placement`);
}
/**
* Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ %name% })}`)
* @param {null | number | NodeLike} node
* @param {string} name
* @returns {never}
*/
export function let_directive_snippet_conflict(node, name) {
e(node, 'let_directive_snippet_conflict', `Cannot use \`let:\` directives on a component that has a \`children\` snippet. Use snippet parameters instead (e.g. \`{#snippet children({ ${name} })}\`)\nhttps://svelte.dev/e/let_directive_snippet_conflict`);
}
/**
* Mixing old (on:%name%) and new syntaxes for event handling is not allowed. Use only the on%name% syntax
* @param {null | number | NodeLike} node

@ -1,6 +1,7 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import * as e from '../../../errors.js';
import { determine_slot } from '../../../utils/slot.js';
/**
* @param {AST.LetDirective} node
@ -21,4 +22,25 @@ export function LetDirective(node, context) {
) {
e.let_directive_invalid_placement(node);
}
// an explicit `children` snippet replaces the default slot content, so the `let:`
// directive would never be provided. if the component has a `slot` attribute, the
// directive applies to the component itself instead, which is fine
if (
(parent.type === 'Component' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteSelf') &&
!determine_slot(parent) &&
parent.fragment.nodes.some(
(child) => child.type === 'SnippetBlock' && child.expression.name === 'children'
)
) {
const { expression } = node;
const pattern =
expression === null || (expression.type === 'Identifier' && expression.name === node.name)
? node.name
: `${node.name}: ${context.state.analysis.source.slice(expression.start, expression.end)}`;
e.let_directive_snippet_conflict(node, pattern);
}
}

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ item: x })}`)',
position: [62, 74]
}
});

@ -0,0 +1,7 @@
<script>
import Child from './irrelevant';
</script>
<Child let:item={x}>
{#snippet children()}{x}{/snippet}
</Child>

@ -0,0 +1,10 @@
import { test } from '../../test';
export default test({
error: {
code: 'let_directive_snippet_conflict',
message:
'Cannot use `let:` directives on a component that has a `children` snippet. Use snippet parameters instead (e.g. `{#snippet children({ value })}`)',
position: [359, 368]
}
});

@ -0,0 +1,23 @@
<script>
import Child from './irrelevant';
import Parent from './irrelevant';
</script>
<!-- ok -->
<Child let:value>
{value}
{#snippet foo()}x{/snippet}
</Child>
<Child>
{#snippet children({ value })}{value}{/snippet}
</Child>
<Parent>
<Child slot="foo" let:value>
{#snippet children()}{value}{/snippet}
</Child>
</Parent>
<!-- invalid -->
<Child let:value>
{#snippet children()}Value is: {value}{/snippet}
</Child>
Loading…
Cancel
Save