Skip to content

feat: add parent hierarchy to __svelte_meta objects at dev time #16255

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

dummdidumm
Copy link
Member

@dummdidumm dummdidumm commented Jun 27, 2025

This adds a parent property to the __svelte_meta properties that are added to elements at dev time. This property represents the closest non-element parent the element is related to. For example for {#if ...}<div>foo</div>{/if} the parent of the div would be the line/column of the if block. The parent is recursive and goes upwards (through component boundaries) until the root component is reached, which has no parent.

image

part of #11389 - I believe the hierarchy structure along with the line/column/block type info will make things much easier for tooling wanting to represent hierarchies. This deliberately doesn't include any firing of events yet, that can happen later once we've figured out at which times we want/need it (my guess is we want it to fire it exactly when those parent relationships are established).

cc @jacob-8

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

  • 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
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.
  • If this PR changes code within packages/svelte/src, add a changeset (npx changeset).

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

This adds a `parent` property to the `__svelte_meta` properties that are added to elements at dev time. This property represents the closest non-element parent the element is related to. For example for `{#if ...}<div>foo</div>{/if}` the `parent` of the div would be the line/column of the if block.
The parent is recursive and goes upwards (through component boundaries) until the root component is reached, which has no parent.

part of #11389
Copy link

changeset-bot bot commented Jun 27, 2025

🦋 Changeset detected

Latest commit: 3e3d20c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
svelte Patch

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

@svelte-docs-bot
Copy link

Copy link
Contributor

Playground

pnpm add https://pkg.pr.new/svelte@16255

@dominikg
Copy link
Member

how does that work? components are compiled in isolation so the dev runtime would have to calculate these for blocks outside of that component. If the runtime can calculate it, the devtool can do that 🤔

everything would have to be updated on hot updates too, think adding or removing block.

could this information be obtained from/attached to comment nodes we already have instead?

@dominikg
Copy link
Member

How would componentTag look for renamed things like import Foo from 'Bar.svelte' or components passed into others as props. Would it show the prop name? or what about the dot notation from stuff like melt?

if every element had all parents attached i hope data is deduplicated/just linked, otherwise that could be a lot extra on long lists that are deeply nested.

@dummdidumm
Copy link
Member Author

Everything is compiled in isolation. The compiler wraps block/component creations with the info where they are within the file, and the runtime creates sort of a stack from it. You can only go upwards not downward - is HMR smart enough to keep children around when a component higher up the tree is reloaded? If not then this doesn't leak or get outdated. If it does we should be able to add logic to our HMR to handle it.

What would attaching it to comments give us?

Right now component tags are what the compiler will use under the hood I think. We should be able to adjust that to show the original name

@dominikg
Copy link
Member

example for deep nesting

attaching it to comment nodes was just an idea of how it could be possible to put the information of locations where they actually are instead of adding it to all nodes they wrap.

@dummdidumm
Copy link
Member Author

What exactly is your concern with the deeply nested example?

@dominikg
Copy link
Member

time spent & memory consumed. right now svelte_meta is linear, one information about the source code location per node. with this, it becomes exponential and adding blocks at the root level or adding many deeply nested components can quickly escalate things. Why is the full info on each node?

@dummdidumm
Copy link
Member Author

dummdidumm commented Jun 28, 2025

This is a shared stack, it's not recreated for every node. So the overhead is basically zero.

image

The reference to the parent (think about it as a reference, not as a value) is there because it's very convenient for anyone not wanting to build their own tree, you can just get a node and traverse upwards from there.

Copy link
Member

@Rich-Harris Rich-Harris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small simplification

@Rich-Harris
Copy link
Member

I think maybe we should rename $.with_dev_stack to something like $.add_metadata so that it's easier to disambiguate between the two functions by that name

Copy link
Member

@Rich-Harris Rich-Harris left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks great — I think it would make sense to add an entry for RenderTag, but otherwise LGTM

@dummdidumm
Copy link
Member Author

render doesn't make sense to me as a parent, what instead the parent becomes snippet and it has a renderedAt property with the info?

{
  type: 'snippet',
  line: ...,
  column: ...,
  file: ...,
  renderedAt: {
    type: 'component' / 'each' / wherever the render tag is,
    line: ...,
    column: ...,
    file: ...,
    parent: <not sure if this would work out, but could be nice to get that tree aswell>
  }
}

@Rich-Harris
Copy link
Member

Why doesn't it make sense? For me 'parent' means 'the block containing the current branch'

@dummdidumm
Copy link
Member Author

dummdidumm commented Jun 30, 2025

It's not a block though, it renders a block. It also gets fuzzy because when @render is referenced then I would expect the parent to be the thing that surrounds it (e.g. for {#if foo} {@render foo()} {/if} it's reasonable to expect the parent to be the if block) and not the thing that was passed in for it. renderedAt would solve both of these problems by a) making the parent block a snippet and b) giving you the parent tree of the render tag via renderedAt.parent

@Rich-Harris
Copy link
Member

Syntactically a render tag isn't a 'block', but in terms of the effect tree it absolutely is

@dummdidumm
Copy link
Member Author

And what about b) ?

@Rich-Harris
Copy link
Member

I still don't think it makes sense. You should be able to keep tracing .parent.parent.parent all the way to the root. The grandparent of the <span> should be either {#if x} or {#if y}, then c then b then a. IIUC, under your suggestion the grandparent would be whatever happened to surround the snippet, but that's clearly not right:

{#snippet hello()}
  <span>hello</span>
{/snippet}

{#if a}
  {#if b}
    {#if c}
      {#if x}
        {@render hello()}
      {/if}

      {#if foo}
        {@render hello()}
      {/if}
    {/if}
  {/if}
{/if}

The 'parent' is the thing that is rendering the child. Here, that is the {@render ...} tag. The {#snippet ...} is just defining what gets rendered, it's not doing the rendering itself.

@dummdidumm
Copy link
Member Author

This will not work for snippets which are passed indirectly. Imagine a Modal class that renders a snippet which is assigned to a shared context. Every parent tree would end up in the modal component and you would have no way to retrieve a reference to the snippet and go up from there.

<script>
  import { openModal } from '$lib/modal.svelte';
</script>

<button onclick={() => openModal(content)}>open</button>

{#snippet content()}
  <p>will start here but not be able to retrieve the parent of this snippet/component</p>
{/snippet}

Essentially there are two trees and both are of interest depending on your use case (sometimes I want to know the actually-rendered tree, sometimes I want to get the virtual tree and jump the snippet/render "portals") hence the renderedAt proposal. If you insist we can also turn it around and have a renders property on the render entry to find that tree.

@Rich-Harris
Copy link
Member

no way to retrieve a reference to the snippet and go up from there

I guess this is the bit I'm stuck on — what does going up from there mean? It doesn't really make sense to speak of the snippet as belonging to any given instance of the surrounding component (it could be hoisted/exported), but nor does it really make sense to speak of it as belonging to (say) Foo.svelte, since that also doesn't have a parent.

I think perhaps I'm not understanding the concrete use case. What information do you gain from including snippets in the tree? My (possibly incorrect) understanding was that this metadata was meant to allow tooling to map the render tree (i.e. what actually appears in the DOM) to source code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants