@archie/devtools
Devtools runtime for Archie-generated applications.
Public API
This package exposes only two supported entrypoints:
@archie/devtools->archieDevTools@archie/devtools/client->ArchieDevToolProvider,ArchieDevToolProviderProps
Everything else is internal implementation detail.
Usage
1. Configure the Vite plugin
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { archieDevTools } from '@archie/devtools';
export default defineConfig({
plugins: [react(), archieDevTools()],
});
2. Bootstrap the app runtime
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { ArchieDevToolProvider } from '@archie/devtools/client';
import App, { router } from './App';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ArchieDevToolProvider router={router}>
<App />
</ArchieDevToolProvider>
</StrictMode>,
);
3. Export the router
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
export const router = createBrowserRouter([
// routes
]);
export default function App() {
return <RouterProvider router={router} />;
}
router is optional: omit it when another piece already announces the routes (e.g. the gv2 routes announcer) to mount only the inspector and DnD, without the route listener.
Responsibilities
archieDevTools()handles compile-time editor metadata and internal DnD scope injection.ArchieDevToolProviderhandles runtime bootstrap: DnD provider, route sync, sanitizer install, and inspector initialization.
Requirements
- React 18+
- React Router DOM 6.4+
- Vite 5+
Deploy order
Changes to babel-plugin-editor-meta.ts's __editorMeta prop shape and
inspector.standalone.js's meta-read paths have a load-bearing publish
order — see docs/deploy-order.md.
Identidad data-vid, NodeMeta, protocolo 1.x
babelPluginEditorMeta ahora LEE data-vid (persistido en el source por el
stamper de Genesis, /^[a-z0-9]{6,8}$/) en vez de derivar la identidad de
file:line:col:componentName, que se invalida en cada escritura del source.
Cuando el elemento tiene data-vid, __editorMeta.nodeId === __editorMeta.vid
y el plugin también emite los campos de NodeMeta
(@8base-archie/editor-protocol): dynamicType, isKitOrigin, isChrome,
containerKind y, cuando aplica, lockReason. parentNodeId usa el vid
del padre cuando existe.
sourceHash por archivo
Cada __editorMeta lleva sourceHash: sha256 en hex (64 caracteres) del texto
exacto del archivo que compiló el plugin (state.file.code, UTF-8, sin
normalizar BOM ni CRLF). Es el mismo algoritmo que legacyIdsSourceHash de acg
(createHash('sha256').update(texto).digest('hex')). El inspector lo propaga
en inspectorSource (ELEMENT_SELECTED y demás mensajes que lo llevan), en
editorMeta y en cada entrada de editorMetaChain; el InspectorRef del modo
diseño (inserted, moved, resized, sesiones de arrange) lo lleva como
sourceHash. Un valor mal formado se omite. El host lo compara con
legacyIdsSourceHash para negarse a resolver legacy ids cuando el DOM del
preview viene de una revisión distinta a la del mapa. Los hosts antiguos
ignoran el campo.
Sin data-vid, el plugin cae al direccionamiento legado SOLO cuando la
opción allowLegacyIds es true (default en esta versión — transicional,
se eliminará más adelante); con allowLegacyIds: false el elemento no se
instrumenta.
El plugin nunca estampa data-vid en runtime (eso lo hace el stamper de
Genesis). El plugin Vite (archieDevTools()) solo VALIDA: en dev, un
JSXOpeningElement sin data-vid dispara un warning una vez por archivo
(onMissingVid en babel-plugin-editor-meta.ts).
En runtime, node-key.ts resuelve la identidad con la prioridad
meta.vid → data-vid del DOM → (legado) meta.nodeId → runtime:N, y
fiber-bridge.ts expone getNodeMeta(el), que valida el NodeMeta
ensamblado contra NodeMetaSchema antes de devolverlo. structural-policy.ts
añade isNonDraggableNodeMeta, isNonDroppableNodeMeta y
computeDroppableSet sobre esos mismos campos.
El handshake (INSPECTOR_SCRIPT_LOADED/CAPABILITIES_RESULT) añade
editorProtocolVersion (el PROTOCOL_VERSION semver del paquete de
contrato — un campo NUEVO, nunca superpuesto al protocolVersion numérico
existente que archie-app ya compara estrictamente) y las capabilities
vidIdentity, nodeMeta, refusals. ELEMENT_SELECTED añade vid y
nodeMeta. El vocabulario REFUSED{code, cause, vid} está disponible
(refuseNode en inspector.standalone.js) para cuando un host rechaza un
gesto de arrastre/drop fuera de política.
Consume @8base-archie/editor-protocol como fuente única del contrato
(Vid, NodeMeta, LockReason, los schemas Zod) — ver
src/types/editor-meta.ts, que re-exporta esos tipos en vez de duplicarlos.
data-vid en build de producción
En vite build (command === 'build'), archieDevTools() elimina el
atributo JSX data-vid del código generado — nada en el pipeline de deploy
lo hace por su cuenta, y el atributo no aporta nada en runtime (la identidad
sigue disponible vía __editorMeta.vid, que no se toca). Solo vite dev
conserva data-vid sin cambios: este plugin corre en transform, así que
vite preview sirve el dist/ ya construido por vite build y por lo
tanto NO conserva data-vid — es el mismo build de producción, no un
comando propio de este plugin. Opt-out explícito:
archieDevTools({ stripVidsInBuild: false })
Apagar el design mode y sesión de arrange
Al pasar de design mode ON a OFF (SET_EDITOR_MODE/SYNC_EDITOR_STATE,
STOP_INSPECTION, Ctrl+Shift+D, crash del overlay o desmontaje) el runtime
siempre vacía historial, cambios, drag externo, selección y canUndo/canRedo.
Revertir además en el DOM los cambios de arrange sin guardar (moves, inserts,
resizes, con las inversas del Discard del host) es opt-in del host, para no
revertir lo ya guardado cuando el host es de una versión anterior:
SET_EDITOR_MODEaceptadiscardUnsavedArrangeOnDisable?: boolean. El inspector lo recuerda y lo reenvía al runtime enset-design-mode. Sin el campo (host legado) el DOM no se toca.{ type: 'ARRANGE_SESSION_SAVED' }, enviado por el host tras persistir la sesión, marca lo actual como guardado: el runtime olvida el historial sin tocar el DOM, emite unARRANGE_SESSION_CHANGEDvacío y activa el opt-in. Lo posterior vuelve a ser revertible.
Local linking
pnpm add file:../archie-devtools
After changing the library:
pnpm build
Development
pnpm test
pnpm typecheck
Tests follow a colocated-by-feature convention:
- small modules keep
*.test.ts(x)beside the source file - larger subsystems may use a local
__tests__/folder next to the feature src/client/dnd/design-mode/__tests__/is the reference pattern for multi-file feature coverage
Release notes
0.1.9
Fixes the arrange-session-changed index for a move onto a screen's own
root (0.1.8's screen-root translation) so it matches the direct
ELEMENT_MOVED event.
- Screen-root move index.
recordCommittedMove'safterpayload (the oneserializeArrangeChangereads asevent.payload.after.indexfor thearrange-session-changedchange entry) carried the raw DOM-child index (command.to.index) even when the drop resolved to a screen-root target, instead ofscreenRootTarget.index— the position remapped to count only the screen's own root children. The directelement-movedruntime event already used the remapped index; only the session-change payload lagged. In the common case (no runtime-injected chrome interleaved among a screen's root children) the two indices coincide and nothing broke; the mismatch only surfaced when chrome sat between screen-root children and the drop slot was valid but not adjacent to the chrome node.
0.1.8
Fixes two related cases where a palette drop or an in-canvas move could land somewhere the host would later discard, instead of refusing the gesture up front:
- Screen-root translation. A drop/insert that lands inside the app
shell's children-slot container (kit-internal/chrome,
containerKind: 'expression'— the element that renders{children}) between the screen's own root nodes now translates to a screen-root target instead of an ordinary container insert. The wire message (ELEMENT_INSERTED/ELEMENT_MOVED, andarrange-session-changed's per-change entries) carriesscreenRootFile(the screen's file) and anindexremapped to the position among the screen's own root children —containerVidis unchanged, still the observed DOM container. - Text/expression containers, and every other chrome/kit-internal
container. Any OTHER drop/insert into app-shell/kit chrome, or into a
non-plain (
containerKind !== 'plain') element such as a text-bearing<p>/<h3>/<span>, now refuses the gesture instead of completing — the engine falls back to the nearest enclosing PLAIN container under the pointer when one exists (e.g. the card<div>wrapping a paragraph), or refuses the gesture entirely when none does. This also fixes a gap where the external-palette-insert path (useExternalDragLoop.ts) never applied theNodeMeta-derived chrome/kit-origin/non-plain blocking at all, unlike the in-canvas move path — every palette drop onto text or app-shell chrome used to be accepted and silently discarded by the host afterward.
The screenRootFile wire field is additive: a host whose own
@8base-archie/editor-protocol copy has not yet added
DropMessage.screenRootFile simply ignores it. This package does not
construct InsertOp/MoveOp itself, and does not import any
@8base-archie/editor-protocol schema at runtime (only two plain constants,
VID_ATTR/VID_PATTERN — everything else is import type), so it carries
no dependency-version coupling to this change.
0.1.5
Treats a JSX comment ({/* ... */}) as a plain container, not an expression
one — a container whose only non-element child is a comment now gets
containerKind: 'plain' instead of 'expression'. This is coupled to
archie-code-generation (acg) PR #416: acg's own container-kind handling
must already treat {/* ... */} the same way before this version is
deployed, or containers that used to agree between the two will disagree
once 0.1.5 ships. No code-level gate enforces the pairing — deploy acg #416
first (or together with this version), not after.