fix: declare let: directives before {@const} on slotted elements#18271
Conversation
When a `let:` directive sits on a slotted element (e.g. `<tr slot="row" let:row>`),
its derived declaration was pushed onto the element's `init` statements, which the
Fragment emits after `{@const}` declarations. A sibling `{@const}` that captured the
slot prop therefore referenced it before initialization, throwing "Cannot access
'...' before initialization" in dev mode (where the const is read eagerly).
Route the `let:` declarations into `let_directives` instead so they precede the
`{@const}` declarations, matching how component-level `let:` directives are handled.
🦋 Changeset detectedLatest commit: e2fbe8e The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR fixes a compiler ordering issue so let: directive bindings on slotted elements are declared before {@const} declarations that depend on them (notably in dev mode), and adds a legacy runtime regression test plus a changeset entry.
Changes:
- Adjusts
RegularElementtransform to queuelet:directive bindings viacontext.state.let_directives. - Adds a new runtime-legacy sample to reproduce/guard the slotted
let:+{@const}ordering behavior in dev mode. - Adds a patch changeset describing the fix.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js | Changes how let: directive bindings are queued so they can come before {@const} usage on slotted elements. |
| packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag-slotted/main.svelte | New regression sample exercising let: on a slotted element with an inner {@const} that captures it. |
| packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag-slotted/Nested.svelte | Provides the slot prop (thing) via an {#each} + <slot ... {thing} />. |
| packages/svelte/tests/runtime-legacy/samples/let-directive-and-const-tag-slotted/_config.js | Adds dev-mode legacy runtime test expectation for the new sample. |
| .changeset/quiet-rivers-melt.md | Documents the patch-level fix in release notes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## svelte@5.55.10 ### Patch Changes - fix: unlink errored and otherwise finished batch ([#18264](#18264)) - perf: walk composedPath() directly in delegated event propagation ([#18268](#18268)) - fix: transfer effects when merging batches ([#18254](#18254)) - fix: allow `$derived(await ...)` in disconnected effect roots ([#18273](#18273)) - fix: remove temporary raw-text hydration markers ([#18269](#18269)) - fix: propagate async `@const` blockers through closure references so template expressions like `{(() => host)()}` correctly wait for the awaited value ([#18309](#18309)) - fix: properly unlink batches ([#18298](#18298)) - fix: settle discarded batch ([#18290](#18290)) - fix: declare `let:` directives before `{@const}` declarations on slotted elements ([#18271](#18271)) - fix: resume outro-ed branches if they were kept around ([#18291](#18291)) - fix: avoid waterfall-warning when async resolves to same value ([#18297](#18297)) - fix: correctly coordinate component-level effects inside async blocks ([#18260](#18260)) - fix: make unnecessary commit work less likely ([#18263](#18263)) - chore: add tag name to `a11y_click_events_have_key_events` warning ([#18272](#18272)) - fix: catch rejected promises while merging/committing ([#18266](#18266)) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Fixes #18119 (missed a spot in #16985)
Before submitting the PR, please make sure you do the following
feat:,fix:,chore:, ordocs:.packages/svelte/src, add a changeset (npx changeset).The bug
A
{@const}that captures alet:slot prop throwsReferenceError: Cannot access '<name>' before initializationwhen thelet:directive is on a slotted element (not on the component itself):This is the same class of bug as #16585, but #16985 only fixed the variant where
let:is on the component itself. The slotted-element variant still regresses.Root cause
{@const}declarations are collected intostate.consts, andFragmentemits them as[...state.let_directives, ...state.consts, ...state.init].LetDirectivedeclarations on a component are routed intolet_directives(since #16985), so they correctly come first.But in
RegularElement, thelet:declarations were pushed ontocontext.state.init:Since
initis emitted afterconsts, the generated code declared the{@const}derived before thelet:derived it depends on:In dev mode the
{@const}derived is read eagerly ($.get(props)), so this hits the temporal dead zone and throws.The fix
Push the element's
let:declarations intolet_directivesinstead ofinit, so they precede{@const}declarations, mirroring the component-level handling from #16985. They still come before the element's owninit(and therefore before any attributes that use them), sinceFragmentemitslet_directivesbeforeinit.Testing
Added
runtime-legacy/samples/let-directive-and-const-tag-slotted(compiled withdev: true, which is where the eager read surfaces the error). It fails before this change withCannot access 'thing' before initializationand passes after.Full
runtime-legacy,runtime-runes,server-side-rendering,hydration,snapshot,css,validatorandprintsuites pass.