From 4d4c58dcb02623cbde5108049de20c62fbff76a9 Mon Sep 17 00:00:00 2001 From: Farhan Ali Raza Date: Wed, 29 Apr 2026 00:08:32 +0500 Subject: [PATCH 1/5] feat: add frontend inspector for mapping DOM nodes to Python source - Add `frontend_inspector`, `frontend_inspector_shortcut`, and `frontend_inspector_editor` config options; "dev" mode fails the prod build (validated at Config init and re-checked at compile time). - New `reflex_base.inspector` package: state flag, capture registry that records source location per component, and an integration module owning compile-time wiring. - `Component.create` stamps emitted tags with a `data-rx-source` id when capture is enabled (Fragments skipped). - Vite config and package.json now react to inspector state and are re-rendered each compile; `launch-editor` pulled in as a dev dep on demand. - Extract framework-frame walking from `console.py` into shared `reflex_base.utils.frames`, reused by the inspector. - Exclude `_inspector_id` from generated pyi stubs. - Docs: new `advanced_onboarding/frontend_inspector.md` page, sidebar entry, and pointer from the configuration guide. - Tests for config validation, runtime gating, capture lifecycle, frame walking, browser contract, and the component data-attribute. --- docs/advanced_onboarding/configuration.md | 4 + .../advanced_onboarding/frontend_inspector.md | 100 ++++++ .../docpage/sidebar/sidebar_items/learn.py | 1 + docs/app/rxconfig.py | 1 + .../assets/inspector/dev_server_middleware.js | 38 +++ .../assets/inspector/inspector.css | 61 ++++ .../reflex_base/assets/inspector/inspector.js | 322 ++++++++++++++++++ .../reflex_base/compiler/inspector_plugins.py | 44 +++ .../src/reflex_base/compiler/templates.py | 12 +- .../src/reflex_base/components/component.py | 19 +- .../reflex-base/src/reflex_base/config.py | 17 + .../src/reflex_base/inspector/__init__.py | 47 +++ .../src/reflex_base/inspector/capture.py | 126 +++++++ .../src/reflex_base/inspector/emit.py | 38 +++ .../src/reflex_base/inspector/integration.py | 181 ++++++++++ .../src/reflex_base/inspector/shortcut.py | 89 +++++ .../src/reflex_base/inspector/state.py | 28 ++ .../src/reflex_base/utils/console.py | 32 +- .../src/reflex_base/utils/frames.py | 165 +++++++++ .../src/reflex_base/utils/pyi_generator.py | 1 + pyi_hashes.json | 119 ------- reflex/app.py | 16 +- reflex/compiler/utils.py | 4 + reflex/utils/frontend_skeleton.py | 36 +- .../components/test_component_inspector.py | 61 ++++ tests/units/reflex_base/compiler/__init__.py | 0 .../compiler/test_inspector_plugins.py | 48 +++ tests/units/reflex_base/inspector/__init__.py | 0 .../inspector/test_browser_contract.py | 82 +++++ .../reflex_base/inspector/test_capture.py | 75 ++++ .../units/reflex_base/inspector/test_emit.py | 56 +++ .../reflex_base/inspector/test_shortcut.py | 38 +++ .../units/reflex_base/inspector/test_state.py | 25 ++ tests/units/test_config.py | 98 ++++++ tests/units/utils/test_frontend_skeleton.py | 73 ++++ 35 files changed, 1916 insertions(+), 141 deletions(-) create mode 100644 docs/advanced_onboarding/frontend_inspector.md create mode 100644 packages/reflex-base/src/reflex_base/assets/inspector/dev_server_middleware.js create mode 100644 packages/reflex-base/src/reflex_base/assets/inspector/inspector.css create mode 100644 packages/reflex-base/src/reflex_base/assets/inspector/inspector.js create mode 100644 packages/reflex-base/src/reflex_base/compiler/inspector_plugins.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/__init__.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/capture.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/emit.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/integration.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/shortcut.py create mode 100644 packages/reflex-base/src/reflex_base/inspector/state.py create mode 100644 packages/reflex-base/src/reflex_base/utils/frames.py create mode 100644 tests/units/components/test_component_inspector.py create mode 100644 tests/units/reflex_base/compiler/__init__.py create mode 100644 tests/units/reflex_base/compiler/test_inspector_plugins.py create mode 100644 tests/units/reflex_base/inspector/__init__.py create mode 100644 tests/units/reflex_base/inspector/test_browser_contract.py create mode 100644 tests/units/reflex_base/inspector/test_capture.py create mode 100644 tests/units/reflex_base/inspector/test_emit.py create mode 100644 tests/units/reflex_base/inspector/test_shortcut.py create mode 100644 tests/units/reflex_base/inspector/test_state.py create mode 100644 tests/units/utils/test_frontend_skeleton.py diff --git a/docs/advanced_onboarding/configuration.md b/docs/advanced_onboarding/configuration.md index d538c8b7c75..b6638dd87a6 100644 --- a/docs/advanced_onboarding/configuration.md +++ b/docs/advanced_onboarding/configuration.md @@ -44,6 +44,10 @@ uv run reflex run --frontend-port 3001 See the [CLI reference](/docs/api-reference/cli) for all the arguments available. +## Frontend Inspector + +For mapping rendered DOM nodes back to the Python source that produced them, see the [Frontend Inspector](/docs/advanced_onboarding/frontend_inspector) page. Enable it in dev with `frontend_inspector="dev"` in your `rxconfig.py`. + ## Customizable App Data Directory The `REFLEX_DIR` environment variable can be set, which allows users to set the location where Reflex writes helper tools like Bun and NodeJS. diff --git a/docs/advanced_onboarding/frontend_inspector.md b/docs/advanced_onboarding/frontend_inspector.md new file mode 100644 index 00000000000..383211ca640 --- /dev/null +++ b/docs/advanced_onboarding/frontend_inspector.md @@ -0,0 +1,100 @@ +```python exec +import reflex as rx +``` + +# Frontend Inspector + +The frontend inspector maps rendered DOM nodes back to the Python source line that created them. Hover an element in the browser, see which `Component.create(...)` call produced it, and click to open that line in your editor. + +It is a development-only tool. Enabling it in a production build is a configuration error. + +## Enable + +Set `frontend_inspector="dev"` in your `rxconfig.py`: + +```python +import reflex as rx + +config = rx.Config( + app_name="my_app", + frontend_inspector="dev", +) +``` + +Run your app in dev mode (`uv run reflex run`). The inspector loads automatically; the `launch-editor` package is added to `.web/package.json` and installed during the same compile pass. + +## Usage + +Three modes: + +- **Hover with `alt` held** — show the overlay while inspecting. The overlay disappears as soon as you release `alt`. +- **`alt+x`** — toggle persistent mode. The overlay stays on; the small `rx-inspect` button in the bottom-right corner reflects the state. +- **Click** — open the source file at the captured line in your editor. + +`Esc` exits persistent mode. Pressing `c` while hovering copies `path:line:column` to the clipboard. + +## Configuration + +```python +config = rx.Config( + app_name="my_app", + frontend_inspector="dev", + # Custom shortcut. Modifier aliases like cmd / option are accepted. + frontend_inspector_shortcut="ctrl+shift+i", + # Optional: override the editor invocation. Empty falls back to + # $REFLEX_EDITOR / $VISUAL / $EDITOR / launch-editor's auto-detection. + frontend_inspector_editor="code -g", +) +``` + +| Field | Default | Notes | +| --- | --- | --- | +| `frontend_inspector` | `"off"` | `"off"` disables it (default), `"dev"` enables it in dev. Prod builds reject `"dev"`. | +| `frontend_inspector_shortcut` | `"alt+x"` | Modifiers: `alt`, `ctrl`, `meta` (`cmd`/`super`/`win`), `shift`. | +| `frontend_inspector_editor` | `""` | Forwarded to [`launch-editor`](https://github.com/yyx990803/launch-editor). | + +## Personal preferences via environment variables + +The shortcut and editor invocation are personal; you usually do not want to commit them to a shared `rxconfig.py`. Reflex reads the matching env vars at config time: + +```bash +REFLEX_FRONTEND_INSPECTOR=dev +REFLEX_FRONTEND_INSPECTOR_SHORTCUT=ctrl+shift+i +REFLEX_FRONTEND_INSPECTOR_EDITOR=cursor +``` + +Set them in your shell, point Reflex at a dotenv file with `REFLEX_ENV_FILE=.env`, or pass `env_file=".env"` to `rx.Config(...)`. Reflex does not auto-discover a `.env` in the project root. + +## Production safety + +`frontend_inspector="dev"` raises `ConfigError` whenever `REFLEX_ENV_MODE=prod`, including: + +- `uv run reflex run --env prod` +- `uv run reflex export --env prod` +- Any deploy that sets `REFLEX_ENV_MODE=prod`. + +The check runs at compile time after the env mode is settled, so the safety net works even when the env is set on the command line. + +## What it does and does not do + +It does: + +- Add a small `data-rx=""` attribute to every component that has a non-Fragment tag. +- Emit `.web/public/__reflex/source-map.json` mapping ids to `(file, line, column, component)`. +- Mount a Vite dev-server middleware at `/__open-in-editor` that calls `launch-editor`. + +It does not: + +- Inspect React state or props at runtime — it is a source-mapping tool, not a React DevTools replacement. +- Run in production. The plugin is registered with `apply: 'serve'` in Vite, so even if a stray asset slipped through, prod builds would not load it. +- Modify your source code. The inspector stores a private id on each component that gets rendered out as a `data-rx` attribute; your `rxconfig.py` and component files are untouched. + +## Programmatic toggle + +When the inspector is loaded, `window.__REFLEX_INSPECTOR__` exposes the runtime API for ad-hoc debugging in the browser console: + +```js +window.__REFLEX_INSPECTOR__.enable(); +window.__REFLEX_INSPECTOR__.toggle(); +window.__REFLEX_INSPECTOR__.sourceCount(); // number of mapped ids +``` diff --git a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py index a0a2149b23e..721faf6c733 100644 --- a/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py +++ b/docs/app/reflex_docs/templates/docpage/sidebar/sidebar_items/learn.py @@ -25,6 +25,7 @@ def get_sidebar_items_learn(): advanced_onboarding.how_reflex_works, advanced_onboarding.configuration, advanced_onboarding.code_structure, + advanced_onboarding.frontend_inspector, ], ), ] diff --git a/docs/app/rxconfig.py b/docs/app/rxconfig.py index 11e5f8b02d2..2249c2bd032 100644 --- a/docs/app/rxconfig.py +++ b/docs/app/rxconfig.py @@ -8,6 +8,7 @@ frontend_packages=[ "tailwindcss-animated", ], + frontend_inspector="dev", telemetry_enabled=False, plugins=[ rx.plugins.TailwindV4Plugin(), diff --git a/packages/reflex-base/src/reflex_base/assets/inspector/dev_server_middleware.js b/packages/reflex-base/src/reflex_base/assets/inspector/dev_server_middleware.js new file mode 100644 index 00000000000..b03a8ad1e54 --- /dev/null +++ b/packages/reflex-base/src/reflex_base/assets/inspector/dev_server_middleware.js @@ -0,0 +1,38 @@ +/* Dev-server middleware that opens a file:line:column in the user's editor. + * + * Mounted by the inspector Vite plugin (and the equivalent Astro hook). + * The browser sends `GET /__open-in-editor?file=::`. + */ + +import launch from "launch-editor"; + +const reflexEditorMiddleware = (req, res, next) => { + if (!req.url || !req.url.startsWith("/__open-in-editor")) { + return next(); + } + let url; + try { + url = new URL(req.url, "http://localhost"); + } catch (err) { + res.statusCode = 400; + res.end("Invalid URL"); + return; + } + const file = url.searchParams.get("file"); + if (!file) { + res.statusCode = 400; + res.end("Missing 'file' query parameter"); + return; + } + const editor = process.env.REFLEX_EDITOR || undefined; + launch(file, editor, (fileName, errorMsg) => { + if (errorMsg) { + // eslint-disable-next-line no-console + console.error("[reflex-inspector] launch-editor:", errorMsg); + } + }); + res.statusCode = 204; + res.end(); +}; + +export default reflexEditorMiddleware; diff --git a/packages/reflex-base/src/reflex_base/assets/inspector/inspector.css b/packages/reflex-base/src/reflex_base/assets/inspector/inspector.css new file mode 100644 index 00000000000..317f3ebe8ec --- /dev/null +++ b/packages/reflex-base/src/reflex_base/assets/inspector/inspector.css @@ -0,0 +1,61 @@ +/* Reflex frontend inspector — dev-only overlay styles. */ + +#__reflex_inspector_outline { + position: fixed; + pointer-events: none; + z-index: 2147483646; + border: 2px solid #4f8cff; + background: rgba(79, 140, 255, 0.08); + border-radius: 2px; + transition: top 60ms linear, left 60ms linear, width 60ms linear, height 60ms linear; + display: none; +} + +#__reflex_inspector_label { + position: fixed; + pointer-events: none; + z-index: 2147483647; + background: #111827; + color: #f9fafb; + font: 500 12px/1.4 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + padding: 4px 8px; + border-radius: 4px; + white-space: nowrap; + display: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); +} + +#__reflex_inspector_label .__reflex_inspector_chain { + color: #9ca3af; + margin-right: 6px; +} + +#__reflex_inspector_label .__reflex_inspector_file { + color: #93c5fd; +} + +#__reflex_inspector_toggle { + position: fixed; + bottom: 16px; + right: 16px; + z-index: 2147483647; + background: #111827; + color: #f9fafb; + border: 1px solid #374151; + border-radius: 999px; + font: 500 12px/1 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + padding: 8px 12px; + cursor: pointer; + opacity: 0.65; + transition: opacity 120ms ease; +} + +#__reflex_inspector_toggle:hover { + opacity: 1; +} + +#__reflex_inspector_toggle[data-active="true"] { + background: #4f8cff; + border-color: #4f8cff; + opacity: 1; +} diff --git a/packages/reflex-base/src/reflex_base/assets/inspector/inspector.js b/packages/reflex-base/src/reflex_base/assets/inspector/inspector.js new file mode 100644 index 00000000000..50f9fb0ae92 --- /dev/null +++ b/packages/reflex-base/src/reflex_base/assets/inspector/inspector.js @@ -0,0 +1,322 @@ +/* Reflex frontend inspector — dev-only browser script. + * + * Reads `data-rx` from rendered elements, looks them up in the source map + * served at /__reflex/source-map.json, draws a hover outline + label, and + * sends clicks to /__open-in-editor so the user's editor can jump to the + * Python call site. + * + * Loaded by the inspector Vite plugin (or Astro integration). The plugin + * also installs the editor middleware on the dev server. + */ + +const RX_DATA_ATTR = "data-rx"; +const SOURCE_MAP_URL = "/__reflex/source-map.json"; +const EDITOR_URL = "/__open-in-editor"; +const SESSION_KEY = "__reflex_inspector_persistent"; +const OUTLINE_ID = "__reflex_inspector_outline"; +const LABEL_ID = "__reflex_inspector_label"; +const TOGGLE_ID = "__reflex_inspector_toggle"; + +const config = window.__REFLEX_INSPECTOR_CONFIG__ || {}; +const shortcut = config.shortcut || { + key: "x", + alt: true, + ctrl: false, + meta: false, + shift: false, +}; + +let sourceMap = null; +let sourceMapPromise = null; +let persistent = false; +let modifierHeld = false; +let lastTarget = null; +let outlineEl = null; +let labelEl = null; +let toggleEl = null; + +const loadSourceMap = () => { + if (sourceMap) return Promise.resolve(sourceMap); + if (sourceMapPromise) return sourceMapPromise; + sourceMapPromise = fetch(SOURCE_MAP_URL, { cache: "no-store" }) + .then((res) => (res.ok ? res.json() : {})) + .catch(() => ({})) + .then((map) => { + sourceMap = map; + return map; + }); + return sourceMapPromise; +}; + +const ensureStylesheet = () => { + const existing = document.getElementById("__reflex_inspector_css"); + if (existing) return; + const link = document.createElement("link"); + link.id = "__reflex_inspector_css"; + link.rel = "stylesheet"; + link.href = "/__reflex/inspector.css"; + document.head.appendChild(link); +}; + +const ensureOverlayElements = () => { + ensureStylesheet(); + if (!outlineEl) { + outlineEl = document.createElement("div"); + outlineEl.id = OUTLINE_ID; + document.body.appendChild(outlineEl); + } + if (!labelEl) { + labelEl = document.createElement("div"); + labelEl.id = LABEL_ID; + document.body.appendChild(labelEl); + } + if (!toggleEl) { + toggleEl = document.createElement("button"); + toggleEl.id = TOGGLE_ID; + toggleEl.type = "button"; + toggleEl.textContent = "rx-inspect"; + toggleEl.addEventListener("click", () => setPersistent(!persistent)); + document.body.appendChild(toggleEl); + } +}; + +const findInspected = (node) => { + while (node && node.nodeType === 1) { + if (node.hasAttribute(RX_DATA_ATTR)) return node; + node = node.parentElement; + } + return null; +}; + +const ancestorChain = (el, depth) => { + const chain = []; + let cur = el.parentElement; + while (cur && chain.length < depth) { + if (cur.hasAttribute(RX_DATA_ATTR)) { + const id = cur.getAttribute(RX_DATA_ATTR); + const info = sourceMap && sourceMap[id]; + if (info && info.component) chain.push(info.component); + } + cur = cur.parentElement; + } + return chain.reverse(); +}; + +const baseName = (file) => { + if (!file) return ""; + const i = Math.max(file.lastIndexOf("/"), file.lastIndexOf("\\")); + return i === -1 ? file : file.slice(i + 1); +}; + +const showOverlayFor = (el) => { + if (!el || !sourceMap) return; + const id = el.getAttribute(RX_DATA_ATTR); + const info = sourceMap[id]; + if (!info) { + hideOverlay(); + return; + } + ensureOverlayElements(); + const rect = el.getBoundingClientRect(); + outlineEl.style.display = "block"; + outlineEl.style.top = `${rect.top}px`; + outlineEl.style.left = `${rect.left}px`; + outlineEl.style.width = `${rect.width}px`; + outlineEl.style.height = `${rect.height}px`; + + const chain = ancestorChain(el, 3); + const breadcrumb = chain.length > 0 ? chain.join(" › ") + " › " : ""; + const file = baseName(info.file); + labelEl.innerHTML = ""; + if (breadcrumb) { + const span = document.createElement("span"); + span.className = "__reflex_inspector_chain"; + span.textContent = breadcrumb; + labelEl.appendChild(span); + } + const name = document.createElement("span"); + name.textContent = `${info.component} `; + labelEl.appendChild(name); + const fileSpan = document.createElement("span"); + fileSpan.className = "__reflex_inspector_file"; + fileSpan.textContent = `${file}:${info.line}`; + labelEl.appendChild(fileSpan); + + labelEl.style.display = "block"; + const labelTop = Math.max(0, rect.top - 24); + labelEl.style.top = `${labelTop}px`; + labelEl.style.left = `${rect.left}px`; +}; + +const hideOverlay = () => { + if (outlineEl) outlineEl.style.display = "none"; + if (labelEl) labelEl.style.display = "none"; +}; + +const openInEditor = async (info) => { + if (!info) return; + const params = new URLSearchParams({ + file: `${info.file}:${info.line}:${info.column || 1}`, + }); + try { + await fetch(`${EDITOR_URL}?${params.toString()}`, { method: "GET" }); + } catch (err) { + console.warn("[reflex-inspector] open-in-editor failed", err); + } +}; + +const copyPath = (info) => { + if (!info || !navigator.clipboard) return; + const text = `${info.file}:${info.line}:${info.column || 1}`; + navigator.clipboard.writeText(text).catch(() => undefined); +}; + +const expectedCode = (key) => { + // Layout-independent code for the configured shortcut key, used as a + // primary match: holding Alt on macOS/Linux can mutate event.key into + // dead/compose characters (e.g. Option+X → "≈"), which would otherwise + // make the second press silently miss. + if (key.length !== 1) return null; + if (key >= "a" && key <= "z") return `Key${key.toUpperCase()}`; + if (key >= "0" && key <= "9") return `Digit${key}`; + return null; +}; + +const matchesShortcut = (event) => { + if (event.repeat) return false; + const code = expectedCode(shortcut.key); + const keyMatch = event.key.toLowerCase() === shortcut.key; + const codeMatch = code !== null && event.code === code; + if (!keyMatch && !codeMatch) return false; + return ( + !!event.altKey === !!shortcut.alt && + !!event.ctrlKey === !!shortcut.ctrl && + !!event.metaKey === !!shortcut.meta && + !!event.shiftKey === !!shortcut.shift + ); +}; + +const isActive = () => persistent || modifierHeld; + +const updateToggle = () => { + if (!toggleEl) return; + toggleEl.dataset.active = String(persistent); +}; + +const setPersistent = async (on) => { + persistent = !!on; + try { + sessionStorage.setItem(SESSION_KEY, persistent ? "1" : "0"); + } catch (_) { + // sessionStorage may be disabled — ignore. + } + if (persistent) await loadSourceMap(); + updateToggle(); + if (!isActive()) hideOverlay(); +}; + +const onMouseMove = (event) => { + if (!isActive()) return; + if (isInspectorChrome(event.target)) { + hideOverlay(); + lastTarget = null; + return; + } + const target = findInspected(event.target); + lastTarget = target; + if (target) showOverlayFor(target); + else hideOverlay(); +}; + +const isInspectorChrome = (node) => { + while (node && node.nodeType === 1) { + if ( + node.id === TOGGLE_ID || + node.id === OUTLINE_ID || + node.id === LABEL_ID + ) { + return true; + } + node = node.parentElement; + } + return false; +}; + +const onClick = (event) => { + if (!isActive()) return; + if (isInspectorChrome(event.target)) return; + const target = findInspected(event.target); + if (!target) return; + event.preventDefault(); + event.stopPropagation(); + const info = sourceMap && sourceMap[target.getAttribute(RX_DATA_ATTR)]; + openInEditor(info); +}; + +const onKeyDown = (event) => { + if (matchesShortcut(event)) { + event.stopPropagation(); + event.preventDefault(); + setPersistent(!persistent); + return; + } + if (event.altKey && !modifierHeld) { + modifierHeld = true; + loadSourceMap(); + } + if (event.key === "Escape" && persistent) { + setPersistent(false); + } + if (event.key.toLowerCase() === "c" && lastTarget && isActive()) { + const info = sourceMap && sourceMap[lastTarget.getAttribute(RX_DATA_ATTR)]; + copyPath(info); + } +}; + +const onKeyUp = (event) => { + if (!event.altKey && modifierHeld) { + modifierHeld = false; + if (!persistent) hideOverlay(); + } +}; + +const installEvents = () => { + document.addEventListener("mousemove", onMouseMove, true); + document.addEventListener("click", onClick, true); + document.addEventListener("keydown", onKeyDown, true); + document.addEventListener("keyup", onKeyUp, true); + window.addEventListener("blur", () => { + modifierHeld = false; + if (!persistent) hideOverlay(); + }); +}; + +const init = () => { + if (window.__REFLEX_INSPECTOR__) return; + ensureOverlayElements(); + installEvents(); + + let initial = false; + try { + initial = sessionStorage.getItem(SESSION_KEY) === "1"; + } catch (_) { + initial = false; + } + if (window.location.search.includes("reflex-inspector")) initial = true; + + window.__REFLEX_INSPECTOR__ = { + enable: () => setPersistent(true), + disable: () => setPersistent(false), + toggle: () => setPersistent(!persistent), + isEnabled: () => persistent, + sourceCount: () => (sourceMap ? Object.keys(sourceMap).length : 0), + }; + + if (initial) setPersistent(true); +}; + +if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init, { once: true }); +} else { + init(); +} diff --git a/packages/reflex-base/src/reflex_base/compiler/inspector_plugins.py b/packages/reflex-base/src/reflex_base/compiler/inspector_plugins.py new file mode 100644 index 00000000000..483b1031caf --- /dev/null +++ b/packages/reflex-base/src/reflex_base/compiler/inspector_plugins.py @@ -0,0 +1,44 @@ +"""JavaScript templates that wire the frontend inspector into the dev server. + +The browser-facing ``