Ming
Ming

Reputation: 31

How to detect svelte component from DOM?

Currently making an google chrome extension to visualize svelte components, this would only be used only development mode. Currently I am grabbing all svelte components by using const svelteComponets = document.querySelectorAll(`[class^="svelte"]`); on my content scripts but it is grabbing every svelte element. What are some approaches to grab only the components?

Upvotes: 0

Views: 1694

Answers (1)

rixo
rixo

Reputation: 25031

Well you mostly can't get to the Svelte component from the DOM elements.

The reason, appart from Svelte won't give you / expose what's needed, is that there isn't a reliable link between components and elements.

A component can have no elements:

<slot />

Or "maybe no elements":

{#if false}<div />{/if}

It can also have multiple root elements:

<div> A </div>
<div> B </div>
<div> C </div>

By bending the cssHash compiler option a lot, you would probably be able to extract the component "name", maybe class name from the CSS scoping classes generated by Svelte. (Which, in turn could break CSS-only HMR updates with Vite, but that's another story.)

But from there, you won't be able to reliably get to the individual component instances... If we keep the component from the last example, once you've grabbed those 6 divs:

<div> A </div>
<div> B </div>
<div> C </div>
<div> A </div>
<div> B </div>
<div> C </div>

... how do you know where one component instance ends and where the other begins? Or even that there are two components?

I believe, the most reliable way to achieve what you want is probably to use internal Svelte APIs, including those that are used by the actual Svelte dev tools that you want to mimic. (Gotta love when private APIs are the "most reliable"!)

Necessary disclaimer: this only seems reasonable to do this in your case because it is a study subject, and because it's dev only. It would certainly not be wise to rely on this for something important. Private / internal APIs can change with any release without any notice.

If you go in the Svelte REPL and look at the generated JS after enabling the "dev" option, you'll see that the compiler adds some events that are provided for the dev tools.

enter image description here

By trials and experimentation, you can get a sense of how Svelte works, and what dev events are available. You'd also probably need to dig the sources of the compiler itself to understand what's happening with some functions... Being comfortable with a good debugger can help a lot!

For your intended usage, that is build a representation of the Svelte component tree, you'll need to know when a component instance is created, what is its parent component, and when it is destroyed. To add it to the tree, in the right place, and remove it when it goes away. With that you should be able to maintain a representation of the component tree for yourself.

You can know when a component is created with the "SvelteRegisterComponent" dev event (squared in red in the above screenshot). You can know the parent component of a component being instantiated by abusing { current_component } from 'svelte/internal'. And you can know when a component is destroyed by abusing the component's this.$$.on_destroy callbacks (which seems like the most fragile part of our plan).

Going into much more detail about how to proceed with this seems of bit out of scope for this question, but the following basic example should give you some ideas of how you can proceed. See it in action in this REPL.

Here's some code that watches Svelte dev events to maintain a component tree, and exposes it as a Svelte store for easy consumption by others. This code would need to run before your first Svelte component is created (or before the components you want to catch are created...).

import { current_component } from 'svelte/internal';
import { writable } from 'svelte/store';

const nodes = new Map();

const root = { children: [] };

// root components created with `new Component(...)` won't have 
// a parent, so we'll put them in the root node's children
nodes.set(undefined, root);

const tree = writable(root);

// notify the store that its value has changed, even 
// if it's only a mutation of the same object
const notify = () => {
    tree.set(root);
};

document.addEventListener('SvelteRegisterComponent', e => {
    // current_component is the component being initialized; at the time 
    // our event is called, it has already been reverted from the component 
    // that triggered the event to its parent component
    const parentComponent = current_component;
    
    // inspect the event's detail to see what more 
    // fun you could squizze out of it
    const { component, tagName } = e.detail;

    let node = nodes.get(component);
    if (!node) {
        node = { children: [] };
        nodes.set(component, node);
    }
    Object.assign(node, e.detail);

    // children creation is completed before their parent component creation
    // is completed (necessarilly, since the parent needs to create all its
    // children to complete itself); that means that the dev event we're using
    // is fired first for children... and so we may have to add a node for the
    // parent from the (first created) child
    let parent = nodes.get(parentComponent);
    if (!parent) {
        parent = { children: [] };
        nodes.set(parentComponent, parent);
    }
    parent.children.push(node);

    // we're done mutating our tree, let the world know
    notify();

    // abusing a little bit more of Svelte private API, to know when
    // our component will be destroyed / removed from the tree...
    component.$$.on_destroy.push(() => {
        const index = parent.children.indexOf(node);
        if (index >= 0) {
            parent.children.splice(index, 1);
            notify();
        }
    });
});

// export the tree as a read only store
export default { subscribe: tree.subscribe }

Upvotes: 5

Related Questions