react-spot 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # react-spot
2
+
3
+ Dev-only DOM to source opener for React 19, Next.js, and Turbopack.
4
+
5
+ It does not inject `data-*` attributes at compile time. In the browser it finds the React Fiber attached to a clicked DOM node, walks `fiber.return` and owner metadata, reads React development source hints such as `_debugSource`, `_debugStack`, `_debugInfo`, and owner stacks, then calls a local `__open-in-editor` endpoint.
6
+
7
+ This intentionally depends on React private development fields. The library keeps those reads isolated and returns diagnostics when React, Next.js, or Turbopack do not expose enough metadata.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install react-spot
13
+ ```
14
+
15
+ ## Next.js App Router
16
+
17
+ Create the endpoint:
18
+
19
+ ```ts
20
+ // app/__open-in-editor/route.ts
21
+ export { GET, POST, runtime, dynamic } from "react-spot/next";
22
+ ```
23
+
24
+ Install the click listener in a client component:
25
+
26
+ ```tsx
27
+ "use client";
28
+
29
+ import { useEffect } from "react";
30
+ import { installReactSpot } from "react-spot";
31
+
32
+ export function ReactSpotDevtools() {
33
+ useEffect(() => {
34
+ return installReactSpot({
35
+ trigger: "alt"
36
+ });
37
+ }, []);
38
+
39
+ return null;
40
+ }
41
+ ```
42
+
43
+ Render `ReactSpotDevtools` only in development, for example from your root layout.
44
+
45
+ - Alt-clicking an element opens the closest available React source.
46
+ - Alt-right-clicking opens a component ancestry menu so you can pick parent components.
47
+
48
+ ## Options
49
+
50
+ ```ts
51
+ installReactSpot({
52
+ endpoint: "/__open-in-editor",
53
+ trigger: "alt", // "always" | "meta-shift" | "ctrl-shift" | function
54
+ menuMaxEntries: 8,
55
+ onOpen(target) {
56
+ console.log(target.source, target.componentName, target.strategy);
57
+ },
58
+ onError(error) {
59
+ console.warn(error);
60
+ }
61
+ });
62
+ ```
63
+
64
+ Set the editor with one of:
65
+
66
+ ```bash
67
+ REACT_SPOT_EDITOR=code
68
+ REACT_SPOT_EDITOR=cursor
69
+ REACT_SPOT_EDITOR=webstorm
70
+ ```
71
+
72
+ VS Code-like editors receive `-g file:line:column`. JetBrains IDEs receive `--line line file`.
73
+
74
+ ## Custom Next Route Options
75
+
76
+ ```ts
77
+ // app/__open-in-editor/route.ts
78
+ import { createOpenInEditorRoute } from "react-spot/next";
79
+
80
+ export const runtime = "nodejs";
81
+ export const dynamic = "force-dynamic";
82
+
83
+ export const GET = createOpenInEditorRoute({
84
+ projectRoot: process.cwd(),
85
+ editor: "cursor",
86
+ requireLocalhost: true
87
+ });
88
+
89
+ export const POST = GET;
90
+ ```
91
+
92
+ ## How It Resolves Source
93
+
94
+ 1. Finds a DOM expando like `__reactFiber$...`.
95
+ 2. Starts with that host Fiber and walks `fiber.return`.
96
+ 3. Checks direct source objects such as `_debugSource`.
97
+ 4. Parses stack metadata such as `_debugStack`, `_debugInfo`, and owner stacks.
98
+ 5. Follows owner links such as `_debugOwner`.
99
+ 6. Calls `/__open-in-editor?file=...&line=...&column=...`.
100
+
101
+ Server Components and Client Component boundaries may only expose the client entry or nearest hydrated owner. When React/Turbopack do not emit source metadata, `inspectElement` returns `source: null` instead of guessing.
102
+
103
+ ## Public API
104
+
105
+ ```ts
106
+ import {
107
+ installReactSpot,
108
+ inspectElement,
109
+ findFiberFromElement,
110
+ findSourceFromFiber,
111
+ parseSourceFromStack
112
+ } from "react-spot";
113
+ ```
114
+
115
+ `inspectElement(element)` is useful if you want to build your own overlay or command palette instead of opening the editor directly.
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "react-spot",
3
+ "version": "0.0.1",
4
+ "description": "Dev-only DOM to React source opener for React 19, Next.js, and Turbopack.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts"
12
+ }
13
+ },
14
+ "files": [
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "scripts": {
19
+ "test": "node --test",
20
+ "demo:next": "npm --prefix examples/next-turbopack run dev -- --turbo"
21
+ },
22
+ "dependencies": {
23
+ "@jridgewell/source-map": "latest",
24
+ "@radix-ui/react-popover": "latest",
25
+ "@uiw/react-json-view": "latest",
26
+ "convert-source-map": "latest"
27
+ },
28
+ "keywords": [
29
+ "react",
30
+ "nextjs",
31
+ "turbopack",
32
+ "open-in-editor",
33
+ "fiber"
34
+ ],
35
+ "devDependencies": {
36
+ "@babel/parser": "^8.0.0",
37
+ "@types/convert-source-map": "^2.0.3",
38
+ "@types/react": "^19.2.17",
39
+ "react": "^19.2.7",
40
+ "typescript": "^6.0.3"
41
+ }
42
+ }
@@ -0,0 +1,89 @@
1
+ import type { ResolvedSourceInfo } from './source-location-resolver';
2
+ import type { ClickToNodeInfo, Fiber } from './types';
3
+
4
+ /**
5
+ * 经过变换的组件链路条目,用于在弹出菜单中展示。
6
+ *
7
+ * 由 ChainTransformer 从原始 fiber 链路生成,
8
+ * 支持重命名、折叠、覆盖导航目标等变换操作。
9
+ */
10
+ export interface TransformedEntry {
11
+ /** 在链路弹出菜单中显示的标签(替代原始组件名) */
12
+ label: string;
13
+ /**
14
+ * 原始链路条目,用于:
15
+ * - 当 resolveLocation 未提供时回退到默认源码解析
16
+ * - 当 props 未提供时回退到 fiber props
17
+ */
18
+ sourceEntry: ClickToNodeInfo;
19
+ /**
20
+ * 覆盖源码位置解析。用户点击弹出菜单条目时延迟调用。
21
+ * 返回 null 表示解析失败,UI 会回退到 sourceEntry 的默认解析。
22
+ */
23
+ resolveLocation?: () => Promise<{
24
+ source: string;
25
+ line: number;
26
+ column: number;
27
+ } | null>;
28
+ /** 覆盖 props 检查器的数据,默认取 sourceEntry.props */
29
+ props?: Record<string, unknown>;
30
+ }
31
+
32
+ /**
33
+ * 链路变换器的上下文,提供源码解析和 fiber 内省工具。
34
+ *
35
+ * 由库注入,使变换器无需直接导入内部模块。
36
+ */
37
+ export interface ChainTransformContext {
38
+ resolveLocation: (
39
+ stackFrame: string,
40
+ debug?: boolean
41
+ ) => Promise<ResolvedSourceInfo | null>;
42
+ getComponentName: (fiber: Fiber) => string;
43
+ getStackFrame: (fiber: Fiber) => string | undefined;
44
+ debug?: boolean;
45
+ }
46
+
47
+ /**
48
+ * 链路变换器函数签名。
49
+ *
50
+ * 接收原始 fiber 链路(DOM 最近元素在前),返回变换后的条目数组。
51
+ * 必须同步执行——异步工作应延迟到 TransformedEntry.resolveLocation 闭包中。
52
+ */
53
+ export type ChainTransformer = (
54
+ chain: ClickToNodeInfo[],
55
+ context: ChainTransformContext
56
+ ) => TransformedEntry[];
57
+
58
+ /**
59
+ * 默认变换:直接将 fiber 链路映射为展示条目,不做任何折叠或重命名。
60
+ */
61
+ function defaultTransform(chain: ClickToNodeInfo[]): TransformedEntry[] {
62
+ return chain.map((entry) => ({
63
+ label: entry.componentName,
64
+ sourceEntry: entry,
65
+ props: entry.props,
66
+ }));
67
+ }
68
+
69
+ /**
70
+ * 应用链路变换器。
71
+ *
72
+ * 若未配置变换器则使用默认变换(直接映射组件名)。
73
+ *
74
+ * Args:
75
+ * chain: 原始 fiber 链路
76
+ * transformer: 可选的自定义变换器
77
+ * context: 变换上下文
78
+ *
79
+ * Returns:
80
+ * 变换后的展示条目数组
81
+ */
82
+ export function applyTransformer(
83
+ chain: ClickToNodeInfo[],
84
+ transformer: ChainTransformer | undefined,
85
+ context: ChainTransformContext
86
+ ): TransformedEntry[] {
87
+ if (!transformer) return defaultTransform(chain);
88
+ return transformer(chain, context);
89
+ }