Desde

Concepts

How it works

The bridge, source mapping, and why Desde wraps your dev server instead of replacing it.

Desde lets you click something in a running prototype and edit the source line that produced it. Three pieces make that possible: your own dev server, a script injected into the page called the bridge, and a build-time stamp recording where each element came from in your source.

Nothing here is written into your repo. The injection happens in memory, per request.

The tool wraps your app; it does not replace it#

The Editor runs on your own machine, as a command-line program or as a desktop app. When you point it at a repo, it does not start its own toy build. It loads your config, executes it in its own scope, and merges a few extra plugins on top before starting the dev server.

On a plain Vite project that means calling Vite's own JavaScript API: loadConfigFromFile, then mergeConfig, then createServer. On Nuxt, React Router or Next.js it means driving that framework's own dev server the same way, from inside the Editor's process. Either way your plugins keep precedence on conflicts, and your aliases, PostCSS setup, proxy rules and hot module replacement all keep working, because it is still your config running your build.

That is the load-bearing decision. A tool that reimplements your build has to keep chasing your build. A tool that wraps it inherits whatever you already have.

Note The Editor writes Vue 3 and React source, and nothing else. What it runs them on is wider than plain Vite: since 2026-08-11 it also starts Nuxt, React Router and Next.js for you, each through that framework's own dev server rather than a Vite config at your root. Astro is supported but off by default. SvelteKit, Svelte, Angular, webpack and Create React App are not supported. Whichever path you are on, your repo needs its own node_modules installed, because your config is genuinely executed. See attach mode for the arrangement where you start the dev server yourself.

The bridge#

The bridge is a single JavaScript file that runs inside your prototype's page. It is the only code Desde puts into your app, and a Vite plugin adds it at serve time.

The plugin serves the bridge bundle from a reserved URL on your dev server, then uses Vite's transformIndexHtml hook to append a <script src=…> tag to each HTML response. Vite transforms that HTML in memory; your index.html on disk is never touched.

The bridge is what reads the page: hover and click handling, the inspector overlay, comment and note pins (which render inside the page so they scroll with content), element screenshots, and the engine that produces a stable CSS selector for an element. Its own UI lives in a Shadow DOM so its styles and yours cannot collide.

The shell, and postMessage#

The prototype runs in an <iframe>. Everything around it (panels, chat, the inspector's property list, comment threads) is a separate React application called the shell.

The shell cannot reach into the iframe's DOM directly, and deliberately does not try. The two sides talk only through postMessage, a browser API for passing structured messages between documents.

Messages have a type and a payload. A few real ones:

Shell → bridge:  ACTIVATE_INSPECTOR, ENTER_COMMENT_MODE, NAVIGATE, RELOAD_PROTOTYPE
Bridge → shell:  BRIDGE_READY, ELEMENT_INSPECTED, ELEMENT_DESELECTED

The serve layer publishes the shell's origin into the page before the bundle loads. The bridge drops inbound messages from any other origin, and addresses its outbound messages to the shell rather than broadcasting to "*". Inspection payloads contain source paths and prop values.

The split follows one rule. Anything positioned against the page lives in the bridge; anything needing application state or the network lives in the shell.

Source mapping: data-desde-src#

A click gives you a DOM element. An edit needs a file, line and column. The bridge cannot infer that from rendered DOM, so a second Vite plugin writes it in during compilation.

For a Vue single-file component the plugin parses the <template> block with @vue/compiler-sfc, walks the result, and splices an attribute in after each tag name. For React a sibling plugin does the same to .tsx and .jsx files using Babel. Both run in memory during the module transform.

The result, visible in the browser's element inspector:

<button data-desde-src="src/views/Settings.vue:42:7" class="ui-button">

That is the whole convention: file:line:col, relative to the repo root. When you click, the bridge walks up from the clicked node to the nearest element carrying that attribute and sends the coordinates to the shell. The shell hands them to the edit pipeline, which re-parses the file and finds the same element. See Deterministic editing.

Two sibling stamps ride along. data-desde-v is a short hash of the file content the coordinates were computed from; the edit server compares it against what is on disk now and refuses an edit whose coordinates provably predate the current bytes, so a stale browser tab cannot splice into the wrong line. data-desde-bind:<prop> records the source position and text of a bound expression like :label="title", so the shell can follow a binding to where it is defined instead of guessing.

Warning If a file loses its stamps, the Editor goes inspect-only on it and refuses edits with "No source-location ancestor." The usual cause is a file the stamping plugins skip: anything under node_modules, or a file type neither plugin handles.

One round trip#

You click; the bridge resolves the target, reads its data-desde-src, and sends ELEMENT_INSPECTED. The shell shows the inspector and looks up the component's manifest to decide which prop produced what you clicked (Design-system grounding). You change a value; the shell posts it to the local editor server at POST /api/editor/edit. The file on disk changes, and Vite's hot module replacement updates the iframe.

That last step is why the wrapping matters: the refresh is your own HMR doing its normal job on a file that changed. The edits land as ordinary uncommitted changes in your working tree. See branch mode.