@anscribe/react 0.1.0
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/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/fiber-pipeline-OQRkNJFF.mjs +382 -0
- package/dist/index.d.mts +11 -0
- package/dist/index.mjs +2 -0
- package/dist/preload.d.mts +23 -0
- package/dist/preload.mjs +76 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 msmps
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# @anscribe/react
|
|
2
|
+
|
|
3
|
+
> Cross-framework React DevTools enrichment for Anscribe Captures.
|
|
4
|
+
|
|
5
|
+
Installs a React DevTools hook that captures React's `currentDispatcherRef` and observes commits. Anscribe's host adapters (`@anscribe/opentui` today; `@anscribe/ink` and friends are the intended future) consume it to enrich Captures with `componentName`, `componentPath`, and source-frame references.
|
|
6
|
+
|
|
7
|
+
This package is the cross-framework substrate. Most users should reach for the host adapter's re-export instead — for OpenTUI that's [`@anscribe/opentui/react/preload`](https://www.npmjs.com/package/@anscribe/opentui).
|
|
8
|
+
|
|
9
|
+
## When to use this package directly
|
|
10
|
+
|
|
11
|
+
Import `@anscribe/react/preload` directly when:
|
|
12
|
+
|
|
13
|
+
- You're building a non-OpenTUI Anscribe host adapter and want to share the enricher.
|
|
14
|
+
- You're embedding the enricher into a custom React TUI bootstrapper where adapter subpaths don't fit your build pipeline.
|
|
15
|
+
|
|
16
|
+
Otherwise, prefer your host adapter's preload subpath.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
bun add @anscribe/react
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`react@>=19.2.0` is a peer dependency.
|
|
25
|
+
|
|
26
|
+
## Use
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// MUST be imported before "react" / "@opentui/react" / your React renderer
|
|
30
|
+
import "@anscribe/react/preload";
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The preload installs `globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__` (or patches an existing one) before React reads it. It must run **before** the first React render, otherwise the renderer is created without the hook attached and metadata enrichment silently no-ops.
|
|
34
|
+
|
|
35
|
+
Adapter-internal API (consumed by host adapters, not end users):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { reactMetadataEnricher, isReactRuntimeEnrichmentAvailable } from "@anscribe/react";
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT © [msmps](https://github.com/msmps)
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { SourceReference } from "@anscribe/core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
//#region src/source-frames.ts
|
|
4
|
+
const V8_FRAME = /^\s*at\s+(?:(.+?)\s+)?\(?(.+?):(\d+):(\d+)\)?\s*$/;
|
|
5
|
+
const WEBKIT_FRAME = /^(?:(.+?)@)?(.+?):(\d+):(\d+)$/;
|
|
6
|
+
const REJECT_PATH = /(?:node_modules\/|@anscribe\/[a-z-]+\/(?:dist|src|bin)\/|\/packages\/(?:core|react|opentui|mcp|ink)\/(?:src|bin)\/)/;
|
|
7
|
+
const REJECT_FN = /^(?:react-stack-bottom-frame|runWithFiberInDEV|commitMount|commitWork)$/;
|
|
8
|
+
const REACT_PACKAGE_BUNDLE_TAIL = /^(?:cjs|umd|esm|dist|build|lib)\/|^index\.[mc]?js(?:$|\/)/;
|
|
9
|
+
const PREFIX_REGEX = /^(?:webpack-internal:\/{2,3}|webpack:\/{2,3}|turbopack:\/{2,3}|bun:\/{2,3}|node:)/;
|
|
10
|
+
const knownReactPackageNames = new Set([
|
|
11
|
+
"react",
|
|
12
|
+
"react-dom",
|
|
13
|
+
"react-reconciler",
|
|
14
|
+
"scheduler"
|
|
15
|
+
]);
|
|
16
|
+
function recordReactRendererPackageName(name) {
|
|
17
|
+
if (typeof name !== "string" || name.length === 0) return;
|
|
18
|
+
knownReactPackageNames.add(name);
|
|
19
|
+
}
|
|
20
|
+
function parseStackFrame(frame) {
|
|
21
|
+
const trimmed = frame.trim();
|
|
22
|
+
if (trimmed.length === 0) return void 0;
|
|
23
|
+
const v8 = V8_FRAME.exec(trimmed);
|
|
24
|
+
if (v8 !== null) return makeParsedFrame(v8[1], v8[2], v8[3], v8[4]);
|
|
25
|
+
const webkit = WEBKIT_FRAME.exec(trimmed);
|
|
26
|
+
if (webkit !== null) return makeParsedFrame(webkit[1], webkit[2], webkit[3], webkit[4]);
|
|
27
|
+
}
|
|
28
|
+
function cleanSourcePath(path) {
|
|
29
|
+
let cleaned = path;
|
|
30
|
+
cleaned = cleaned.replace(PREFIX_REGEX, "");
|
|
31
|
+
if (cleaned.startsWith("file://")) {
|
|
32
|
+
cleaned = cleaned.slice(7);
|
|
33
|
+
try {
|
|
34
|
+
cleaned = decodeURIComponent(cleaned);
|
|
35
|
+
} catch {}
|
|
36
|
+
}
|
|
37
|
+
if (cleaned.startsWith("./")) cleaned = cleaned.slice(2);
|
|
38
|
+
else if (cleaned.startsWith("~/")) cleaned = cleaned.slice(2);
|
|
39
|
+
const queryIndex = cleaned.search(/[?#]/);
|
|
40
|
+
if (queryIndex !== -1) cleaned = cleaned.slice(0, queryIndex);
|
|
41
|
+
return cleaned;
|
|
42
|
+
}
|
|
43
|
+
function isApplicationFrame(path, functionName) {
|
|
44
|
+
if (path.length === 0) return false;
|
|
45
|
+
if (!path.includes("/") && !path.includes("\\")) return false;
|
|
46
|
+
if (containsKnownReactPackageSegment(path)) return false;
|
|
47
|
+
if (REJECT_PATH.test(path)) return false;
|
|
48
|
+
if (functionName !== void 0 && REJECT_FN.test(functionName)) return false;
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
function makeParsedFrame(rawFunctionName, rawFile, rawLine, rawColumn) {
|
|
52
|
+
if (rawFile === void 0 || rawFile.length === 0) return void 0;
|
|
53
|
+
const file = cleanSourcePath(rawFile);
|
|
54
|
+
if (file.length === 0) return void 0;
|
|
55
|
+
const line = rawLine === void 0 ? void 0 : safeParseInt(rawLine);
|
|
56
|
+
const column = rawColumn === void 0 ? void 0 : safeParseInt(rawColumn);
|
|
57
|
+
const functionName = rawFunctionName !== void 0 && rawFunctionName.length > 0 ? rawFunctionName : void 0;
|
|
58
|
+
return {
|
|
59
|
+
file,
|
|
60
|
+
...line !== void 0 && { line },
|
|
61
|
+
...column !== void 0 && { column },
|
|
62
|
+
...functionName !== void 0 && { functionName }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function safeParseInt(value) {
|
|
66
|
+
const parsed = Number.parseInt(value, 10);
|
|
67
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
68
|
+
}
|
|
69
|
+
function containsKnownReactPackageSegment(path) {
|
|
70
|
+
const normalised = path.replace(/\\/g, "/");
|
|
71
|
+
for (const name of knownReactPackageNames) {
|
|
72
|
+
const marker = `/${name}/`;
|
|
73
|
+
const idx = normalised.indexOf(marker);
|
|
74
|
+
if (idx === -1) continue;
|
|
75
|
+
if (name.includes("-")) return true;
|
|
76
|
+
const tail = normalised.slice(idx + marker.length);
|
|
77
|
+
if (REACT_PACKAGE_BUNDLE_TAIL.test(tail)) return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/fiber-pipeline.ts
|
|
83
|
+
const REACT_FORWARD_REF_TYPE = Symbol.for("react.forward_ref");
|
|
84
|
+
const REACT_MEMO_TYPE = Symbol.for("react.memo");
|
|
85
|
+
const REACT_LAZY_TYPE = Symbol.for("react.lazy");
|
|
86
|
+
const ABORT_MESSAGE = "anscribe.dispatcher-probe.abort";
|
|
87
|
+
const DISPATCHER_PROXY = new Proxy(Object.create(null), { get() {
|
|
88
|
+
throw new Error(ABORT_MESSAGE);
|
|
89
|
+
} });
|
|
90
|
+
const probeCache = /* @__PURE__ */ new WeakMap();
|
|
91
|
+
const renderableEnrichment = /* @__PURE__ */ new WeakMap();
|
|
92
|
+
let currentDispatcherRef;
|
|
93
|
+
let reactMetadataEnricherRegistered = false;
|
|
94
|
+
let reactRendererInjected = false;
|
|
95
|
+
function markReactMetadataEnricherRegistered() {
|
|
96
|
+
reactMetadataEnricherRegistered = true;
|
|
97
|
+
}
|
|
98
|
+
function markReactRendererInjected() {
|
|
99
|
+
reactRendererInjected = true;
|
|
100
|
+
}
|
|
101
|
+
function isReactRuntimeEnrichmentAvailable() {
|
|
102
|
+
return reactMetadataEnricherRegistered && reactRendererInjected;
|
|
103
|
+
}
|
|
104
|
+
function reactMetadataEnricher(input) {
|
|
105
|
+
return Effect.sync(() => {
|
|
106
|
+
const enrichment = isObject(input.renderable) ? renderableEnrichment.get(input.renderable) : void 0;
|
|
107
|
+
if (enrichment === void 0) return void 0;
|
|
108
|
+
return enrichment.sourceReferences.length > 0 ? {
|
|
109
|
+
metadata: enrichment.metadata,
|
|
110
|
+
sourceReferences: enrichment.sourceReferences
|
|
111
|
+
} : { metadata: enrichment.metadata };
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function recordReactCommitRoot(root) {
|
|
115
|
+
const current = isObject(root) ? root.current : void 0;
|
|
116
|
+
if (current === void 0 || current === null) return;
|
|
117
|
+
walkFiber(current);
|
|
118
|
+
}
|
|
119
|
+
function walkFiber(fiber) {
|
|
120
|
+
recordFiber(fiber);
|
|
121
|
+
if (fiber.child !== void 0 && fiber.child !== null) walkFiber(fiber.child);
|
|
122
|
+
if (fiber.sibling !== void 0 && fiber.sibling !== null) walkFiber(fiber.sibling);
|
|
123
|
+
}
|
|
124
|
+
function recordFiber(fiber) {
|
|
125
|
+
const renderable = fiber.stateNode;
|
|
126
|
+
if (!isObject(renderable)) return;
|
|
127
|
+
const componentPath = readComponentPath(fiber);
|
|
128
|
+
const componentName = componentPath.at(-1);
|
|
129
|
+
if (componentName === void 0) return;
|
|
130
|
+
const sourceReferences = extractSourceReferences(fiber);
|
|
131
|
+
renderableEnrichment.set(renderable, {
|
|
132
|
+
metadata: {
|
|
133
|
+
componentName,
|
|
134
|
+
componentPath: componentPath.join(" > ")
|
|
135
|
+
},
|
|
136
|
+
sourceReferences
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function readComponentPath(fiber) {
|
|
140
|
+
const path = [];
|
|
141
|
+
for (let current = fiber.return; current !== void 0 && current !== null; current = current.return) {
|
|
142
|
+
const name = readComponentName(current);
|
|
143
|
+
if (name !== void 0 && !isInternalComponentName(name)) path.push(name);
|
|
144
|
+
}
|
|
145
|
+
return path.reverse();
|
|
146
|
+
}
|
|
147
|
+
function readComponentName(fiber) {
|
|
148
|
+
const type = fiber.elementType ?? fiber.type;
|
|
149
|
+
if (typeof type === "string") return;
|
|
150
|
+
if (typeof type === "function") return readNamedFunction(type);
|
|
151
|
+
if (!isObject(type)) return;
|
|
152
|
+
const displayName = readString(type.displayName);
|
|
153
|
+
if (displayName !== void 0) return displayName;
|
|
154
|
+
const nestedType = type.type;
|
|
155
|
+
if (typeof nestedType === "function") return readNamedFunction(nestedType);
|
|
156
|
+
const render = type.render;
|
|
157
|
+
if (typeof render === "function") return readNamedFunction(render);
|
|
158
|
+
const contextDisplayName = isObject(type._context) ? readString(type._context.displayName) : void 0;
|
|
159
|
+
return contextDisplayName === void 0 ? void 0 : `${contextDisplayName}.Provider`;
|
|
160
|
+
}
|
|
161
|
+
function readNamedFunction(fn) {
|
|
162
|
+
return readString(fn.displayName) ?? readString(fn.name);
|
|
163
|
+
}
|
|
164
|
+
function isInternalComponentName(name) {
|
|
165
|
+
return name === "ErrorBoundary" || name === "AppContext.Provider" || name === "Context.Provider" || name === "Provider";
|
|
166
|
+
}
|
|
167
|
+
function extractSourceReferences(fiber) {
|
|
168
|
+
const direct = readFromDebugStack(fiber) ?? readFromOwnerDebugStack(fiber) ?? readFromOwnerDebugSource(fiber) ?? readFromJsxSource(fiber) ?? readFromDebugInfo(fiber);
|
|
169
|
+
if (direct !== void 0) return [direct];
|
|
170
|
+
const componentFunction = unwrapReactComponentFunction(fiber.type);
|
|
171
|
+
if (componentFunction === void 0) return [];
|
|
172
|
+
const cached = probeCache.get(componentFunction);
|
|
173
|
+
if (cached !== void 0) return cached.ref === null ? [] : [cached.ref];
|
|
174
|
+
const probed = probeComponentSource(componentFunction) ?? null;
|
|
175
|
+
probeCache.set(componentFunction, { ref: probed });
|
|
176
|
+
return probed === null ? [] : [probed];
|
|
177
|
+
}
|
|
178
|
+
function readFromDebugStack(fiber) {
|
|
179
|
+
const raw = fiber._debugStack;
|
|
180
|
+
const stack = stackToString(raw);
|
|
181
|
+
if (stack === void 0) return void 0;
|
|
182
|
+
const parsed = pickApplicationFrame(stack);
|
|
183
|
+
if (parsed === void 0) return void 0;
|
|
184
|
+
return makeSourceReference(parsed, "react-debug-stack");
|
|
185
|
+
}
|
|
186
|
+
function readFromOwnerDebugStack(fiber) {
|
|
187
|
+
let owner = fiber._debugOwner;
|
|
188
|
+
while (owner !== void 0 && owner !== null) {
|
|
189
|
+
const stack = stackToString(owner._debugStack);
|
|
190
|
+
if (stack !== void 0) {
|
|
191
|
+
const parsed = pickApplicationFrame(stack);
|
|
192
|
+
if (parsed !== void 0) return makeSourceReference(parsed, "react-debug-owner-stack", readComponentDisplayName(owner.type));
|
|
193
|
+
}
|
|
194
|
+
owner = owner._debugOwner;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function readFromOwnerDebugSource(fiber) {
|
|
198
|
+
let owner = fiber._debugOwner;
|
|
199
|
+
while (owner !== void 0 && owner !== null) {
|
|
200
|
+
const source = readSourceFields(owner._debugSource);
|
|
201
|
+
if (source !== void 0) return makeSourceReference(source, "react-debug-owner", readComponentDisplayName(owner.type));
|
|
202
|
+
owner = owner._debugOwner;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function readFromJsxSource(fiber) {
|
|
206
|
+
const props = fiber.memoizedProps;
|
|
207
|
+
if (!isObject(props)) return void 0;
|
|
208
|
+
const parsed = readSourceFields(props.__source);
|
|
209
|
+
if (parsed === void 0) return void 0;
|
|
210
|
+
return makeSourceReference(parsed, "jsx-runtime-source", readSelfConstructorName(props.__self));
|
|
211
|
+
}
|
|
212
|
+
function readFromDebugInfo(fiber) {
|
|
213
|
+
const info = fiber._debugInfo;
|
|
214
|
+
if (!Array.isArray(info)) return void 0;
|
|
215
|
+
for (const entry of info) {
|
|
216
|
+
if (!isObject(entry)) continue;
|
|
217
|
+
const stack = entry.stack;
|
|
218
|
+
if (typeof stack === "string" && stack.length > 0) {
|
|
219
|
+
const parsed = pickApplicationFrame(stack);
|
|
220
|
+
if (parsed !== void 0) return makeSourceReference(parsed, "react-debug-info", readComponentDisplayName(entry.name ?? void 0));
|
|
221
|
+
}
|
|
222
|
+
const owner = entry.owner;
|
|
223
|
+
if (isObject(owner)) {
|
|
224
|
+
const source = readSourceFields(owner._debugSource);
|
|
225
|
+
if (source !== void 0) return makeSourceReference(source, "react-debug-info", readComponentDisplayName(owner.type ?? entry.name));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function stackToString(value) {
|
|
230
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
231
|
+
if (value instanceof Error && typeof value.stack === "string") return value.stack;
|
|
232
|
+
if (isObject(value) && typeof value.stack === "string") return value.stack;
|
|
233
|
+
}
|
|
234
|
+
function pickApplicationFrame(stack) {
|
|
235
|
+
for (const line of stack.split("\n")) {
|
|
236
|
+
const parsed = parseStackFrame(line);
|
|
237
|
+
if (parsed === void 0) continue;
|
|
238
|
+
if (parsed.file === void 0) continue;
|
|
239
|
+
if (!isApplicationFrame(parsed.file, parsed.functionName)) continue;
|
|
240
|
+
return parsed;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function readSourceFields(raw) {
|
|
244
|
+
if (!isObject(raw)) return void 0;
|
|
245
|
+
const fileName = raw.fileName;
|
|
246
|
+
if (typeof fileName !== "string" || fileName.length === 0) return void 0;
|
|
247
|
+
const cleaned = cleanSourcePath(fileName);
|
|
248
|
+
if (cleaned.length === 0) return void 0;
|
|
249
|
+
const lineNumber = raw.lineNumber;
|
|
250
|
+
const columnNumber = raw.columnNumber;
|
|
251
|
+
return {
|
|
252
|
+
file: cleaned,
|
|
253
|
+
...typeof lineNumber === "number" && Number.isFinite(lineNumber) && { line: lineNumber },
|
|
254
|
+
...typeof columnNumber === "number" && Number.isFinite(columnNumber) && { column: columnNumber }
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function readSelfConstructorName(value) {
|
|
258
|
+
if (!isObject(value)) return void 0;
|
|
259
|
+
const ctor = value.constructor;
|
|
260
|
+
if (typeof ctor !== "function") return void 0;
|
|
261
|
+
const name = ctor.name;
|
|
262
|
+
return typeof name === "string" && name.length > 0 ? name : void 0;
|
|
263
|
+
}
|
|
264
|
+
function makeSourceReference(parsed, origin, componentName) {
|
|
265
|
+
return new SourceReference({
|
|
266
|
+
...parsed.file !== void 0 && { file: parsed.file },
|
|
267
|
+
...parsed.line !== void 0 && { line: parsed.line },
|
|
268
|
+
...parsed.column !== void 0 && { column: parsed.column },
|
|
269
|
+
...parsed.functionName !== void 0 && { functionName: parsed.functionName },
|
|
270
|
+
...componentName !== void 0 && { componentName },
|
|
271
|
+
origin
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
function unwrapReactComponentFunction(value) {
|
|
275
|
+
let current = value;
|
|
276
|
+
const seen = /* @__PURE__ */ new Set();
|
|
277
|
+
while (current !== null && current !== void 0 && !seen.has(current)) {
|
|
278
|
+
seen.add(current);
|
|
279
|
+
if (typeof current === "function") return current;
|
|
280
|
+
if (typeof current !== "object") return;
|
|
281
|
+
const wrapper = current;
|
|
282
|
+
if (wrapper.$$typeof === REACT_FORWARD_REF_TYPE) {
|
|
283
|
+
current = wrapper.render;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (wrapper.$$typeof === REACT_MEMO_TYPE) {
|
|
287
|
+
current = wrapper.type;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (wrapper.$$typeof === REACT_LAZY_TYPE) {
|
|
291
|
+
const init = wrapper._init;
|
|
292
|
+
if (typeof init !== "function") return;
|
|
293
|
+
try {
|
|
294
|
+
current = init(wrapper._payload);
|
|
295
|
+
continue;
|
|
296
|
+
} catch {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Record the `currentDispatcherRef` that the React reconciler hands to
|
|
305
|
+
* `__REACT_DEVTOOLS_GLOBAL_HOOK__.inject`. That object is the live
|
|
306
|
+
* `ReactSharedInternals` of the React instance the application renders with,
|
|
307
|
+
* so reading the dispatcher slot from it always targets the right copy of
|
|
308
|
+
* React even when more than one is resolvable on disk.
|
|
309
|
+
*
|
|
310
|
+
* @internal
|
|
311
|
+
*/
|
|
312
|
+
function setReactCurrentDispatcherRef(ref) {
|
|
313
|
+
if (isObject(ref)) currentDispatcherRef = ref;
|
|
314
|
+
}
|
|
315
|
+
function probeComponentSource(componentFunction) {
|
|
316
|
+
if (isClassComponent(componentFunction)) return void 0;
|
|
317
|
+
const inner = unwrapReactComponentFunction(componentFunction);
|
|
318
|
+
if (inner === void 0) return void 0;
|
|
319
|
+
const slot = locateDispatcherSlot();
|
|
320
|
+
if (slot === void 0) return void 0;
|
|
321
|
+
const original = slot.root[slot.key];
|
|
322
|
+
slot.root[slot.key] = DISPATCHER_PROXY;
|
|
323
|
+
let stack;
|
|
324
|
+
try {
|
|
325
|
+
inner({});
|
|
326
|
+
} catch (error) {
|
|
327
|
+
stack = error instanceof Error && typeof error.stack === "string" ? error.stack : void 0;
|
|
328
|
+
} finally {
|
|
329
|
+
slot.root[slot.key] = original;
|
|
330
|
+
}
|
|
331
|
+
if (stack === void 0) return void 0;
|
|
332
|
+
for (const line of stack.split("\n")) {
|
|
333
|
+
const parsed = parseStackFrame(line);
|
|
334
|
+
if (parsed === void 0 || parsed.file === void 0) continue;
|
|
335
|
+
if (!isApplicationFrame(parsed.file, parsed.functionName)) continue;
|
|
336
|
+
return new SourceReference({
|
|
337
|
+
file: parsed.file,
|
|
338
|
+
...parsed.line !== void 0 && { line: parsed.line },
|
|
339
|
+
...parsed.column !== void 0 && { column: parsed.column },
|
|
340
|
+
...parsed.functionName !== void 0 && { functionName: parsed.functionName },
|
|
341
|
+
origin: "dispatcher-probe"
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function isClassComponent(value) {
|
|
346
|
+
const prototype = value.prototype;
|
|
347
|
+
if (prototype === null || typeof prototype !== "object") return false;
|
|
348
|
+
return prototype.isReactComponent !== void 0;
|
|
349
|
+
}
|
|
350
|
+
function locateDispatcherSlot() {
|
|
351
|
+
const ref = currentDispatcherRef;
|
|
352
|
+
if (ref === void 0) return void 0;
|
|
353
|
+
if ("H" in ref) return {
|
|
354
|
+
root: ref,
|
|
355
|
+
key: "H"
|
|
356
|
+
};
|
|
357
|
+
const legacyDispatcher = ref.ReactCurrentDispatcher;
|
|
358
|
+
if (isObject(legacyDispatcher) && "current" in legacyDispatcher) return {
|
|
359
|
+
root: legacyDispatcher,
|
|
360
|
+
key: "current"
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function readComponentDisplayName(type) {
|
|
364
|
+
if (typeof type === "string" && type.length > 0) return type;
|
|
365
|
+
if (typeof type === "function") {
|
|
366
|
+
const fn = type;
|
|
367
|
+
if (typeof fn.displayName === "string" && fn.displayName.length > 0) return fn.displayName;
|
|
368
|
+
if (typeof fn.name === "string" && fn.name.length > 0) return fn.name;
|
|
369
|
+
}
|
|
370
|
+
if (isObject(type)) {
|
|
371
|
+
const obj = type;
|
|
372
|
+
if (typeof obj.displayName === "string" && obj.displayName.length > 0) return obj.displayName;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function isObject(value) {
|
|
376
|
+
return value !== null && typeof value === "object";
|
|
377
|
+
}
|
|
378
|
+
function readString(value) {
|
|
379
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
380
|
+
}
|
|
381
|
+
//#endregion
|
|
382
|
+
export { recordReactCommitRoot as a, reactMetadataEnricher as i, markReactMetadataEnricherRegistered as n, setReactCurrentDispatcherRef as o, markReactRendererInjected as r, recordReactRendererPackageName as s, isReactRuntimeEnrichmentAvailable as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { CaptureEnrichmentOutput, CapturedTarget, SourceReference } from "@anscribe/core";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/fiber-pipeline.d.ts
|
|
5
|
+
declare function isReactRuntimeEnrichmentAvailable(): boolean;
|
|
6
|
+
declare function reactMetadataEnricher(input: {
|
|
7
|
+
renderable: unknown;
|
|
8
|
+
target: CapturedTarget;
|
|
9
|
+
}): Effect.Effect<CaptureEnrichmentOutput | undefined>;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { isReactRuntimeEnrichmentAvailable, reactMetadataEnricher };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region src/preload.d.ts
|
|
2
|
+
type ReactDevToolsRenderer = Record<string, unknown>;
|
|
3
|
+
type ReactDevToolsHook = {
|
|
4
|
+
supportsFiber?: boolean;
|
|
5
|
+
hasUnsupportedRendererAttached?: boolean;
|
|
6
|
+
renderers?: Map<number, ReactDevToolsRenderer>;
|
|
7
|
+
inject?: (renderer: ReactDevToolsRenderer) => number;
|
|
8
|
+
onCommitFiberRoot?: (rendererId: number, root: unknown, priorityLevel?: unknown, didError?: unknown) => void;
|
|
9
|
+
onScheduleFiberRoot?: (rendererId: number, root: unknown, children: unknown) => void;
|
|
10
|
+
onPostCommitFiberRoot?: (rendererId: number, root: unknown) => void;
|
|
11
|
+
onCommitFiberUnmount?: (rendererId: number, fiber: unknown) => void;
|
|
12
|
+
checkDCE?: (fn: unknown) => void;
|
|
13
|
+
on?: (...args: unknown[]) => void;
|
|
14
|
+
off?: (...args: unknown[]) => void;
|
|
15
|
+
sub?: (...args: unknown[]) => () => void;
|
|
16
|
+
[key: symbol]: true | undefined;
|
|
17
|
+
};
|
|
18
|
+
declare global {
|
|
19
|
+
var __REACT_DEVTOOLS_GLOBAL_HOOK__: ReactDevToolsHook | undefined;
|
|
20
|
+
}
|
|
21
|
+
declare function installReactPreloadHook(): ReactDevToolsHook;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { installReactPreloadHook };
|
package/dist/preload.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { a as recordReactCommitRoot, n as markReactMetadataEnricherRegistered, o as setReactCurrentDispatcherRef, r as markReactRendererInjected, s as recordReactRendererPackageName } from "./fiber-pipeline-OQRkNJFF.mjs";
|
|
2
|
+
//#region src/preload.ts
|
|
3
|
+
const PATCHED = Symbol.for("anscribe.react.preload.patched");
|
|
4
|
+
const ENRICHER_REGISTERED = Symbol.for("anscribe.react.preload.enricherRegistered");
|
|
5
|
+
const noop = () => {};
|
|
6
|
+
installReactPreloadHook();
|
|
7
|
+
function installReactPreloadHook() {
|
|
8
|
+
const hook = getOrCreateHook();
|
|
9
|
+
if (hook[PATCHED] === true) {
|
|
10
|
+
registerReactMetadataEnricher(hook);
|
|
11
|
+
return hook;
|
|
12
|
+
}
|
|
13
|
+
hook[PATCHED] = true;
|
|
14
|
+
registerReactMetadataEnricher(hook);
|
|
15
|
+
hook.supportsFiber = true;
|
|
16
|
+
hook.hasUnsupportedRendererAttached = false;
|
|
17
|
+
hook.renderers ??= /* @__PURE__ */ new Map();
|
|
18
|
+
for (const renderer of hook.renderers.values()) {
|
|
19
|
+
markReactRendererInjected();
|
|
20
|
+
captureDispatcherRef(renderer);
|
|
21
|
+
}
|
|
22
|
+
hook.on ??= noop;
|
|
23
|
+
hook.off ??= noop;
|
|
24
|
+
hook.sub ??= () => noop;
|
|
25
|
+
hook.checkDCE ??= noop;
|
|
26
|
+
hook.onCommitFiberUnmount ??= noop;
|
|
27
|
+
hook.onPostCommitFiberRoot ??= noop;
|
|
28
|
+
hook.onScheduleFiberRoot ??= noop;
|
|
29
|
+
const originalInject = hook.inject;
|
|
30
|
+
let nextRendererId = Math.max(0, ...hook.renderers.keys());
|
|
31
|
+
hook.inject = (renderer) => {
|
|
32
|
+
const rendererId = originalInject?.call(hook, renderer) ?? ++nextRendererId;
|
|
33
|
+
hook.renderers?.set(rendererId, renderer);
|
|
34
|
+
markReactRendererInjected();
|
|
35
|
+
captureDispatcherRef(renderer);
|
|
36
|
+
return rendererId;
|
|
37
|
+
};
|
|
38
|
+
const originalOnCommitFiberRoot = hook.onCommitFiberRoot;
|
|
39
|
+
hook.onCommitFiberRoot = (rendererId, root, priorityLevel, didError) => {
|
|
40
|
+
recordReactCommitRoot(root);
|
|
41
|
+
originalOnCommitFiberRoot?.call(hook, rendererId, root, priorityLevel, didError);
|
|
42
|
+
};
|
|
43
|
+
return hook;
|
|
44
|
+
}
|
|
45
|
+
function captureDispatcherRef(renderer) {
|
|
46
|
+
const ref = renderer.currentDispatcherRef;
|
|
47
|
+
setReactCurrentDispatcherRef(ref);
|
|
48
|
+
const name = renderer.rendererPackageName;
|
|
49
|
+
recordReactRendererPackageName(name);
|
|
50
|
+
}
|
|
51
|
+
function registerReactMetadataEnricher(hook) {
|
|
52
|
+
if (hook[ENRICHER_REGISTERED] === true) return;
|
|
53
|
+
hook[ENRICHER_REGISTERED] = true;
|
|
54
|
+
markReactMetadataEnricherRegistered();
|
|
55
|
+
}
|
|
56
|
+
function getOrCreateHook() {
|
|
57
|
+
const existingHook = globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
58
|
+
if (existingHook !== void 0) return existingHook;
|
|
59
|
+
const hook = {
|
|
60
|
+
supportsFiber: true,
|
|
61
|
+
hasUnsupportedRendererAttached: false,
|
|
62
|
+
renderers: /* @__PURE__ */ new Map(),
|
|
63
|
+
on: noop,
|
|
64
|
+
off: noop,
|
|
65
|
+
sub: () => noop,
|
|
66
|
+
onCommitFiberRoot: noop,
|
|
67
|
+
onScheduleFiberRoot: noop,
|
|
68
|
+
onCommitFiberUnmount: noop,
|
|
69
|
+
onPostCommitFiberRoot: noop,
|
|
70
|
+
checkDCE: noop
|
|
71
|
+
};
|
|
72
|
+
globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__ = hook;
|
|
73
|
+
return hook;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { installReactPreloadHook };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anscribe/react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Anscribe React adapter: drop-in <Anscribe /> component and hooks for capture mode in React TUIs.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agent",
|
|
7
|
+
"anscribe",
|
|
8
|
+
"capture",
|
|
9
|
+
"mcp",
|
|
10
|
+
"opentui",
|
|
11
|
+
"react",
|
|
12
|
+
"tui"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/msmps/anscribe",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/msmps/anscribe/issues"
|
|
17
|
+
},
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"author": "msmps",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/msmps/anscribe.git",
|
|
23
|
+
"directory": "packages/react"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./preload": {
|
|
36
|
+
"types": "./dist/preload.d.mts",
|
|
37
|
+
"import": "./dist/preload.mjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true,
|
|
43
|
+
"registry": "https://registry.npmjs.org"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"effect": "^4.0.0-beta.65",
|
|
47
|
+
"@anscribe/core": "0.1.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"react": ">=19.2.0"
|
|
51
|
+
},
|
|
52
|
+
"peerDependenciesMeta": {
|
|
53
|
+
"react": {
|
|
54
|
+
"optional": true
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"test": "vitest run"
|
|
60
|
+
}
|
|
61
|
+
}
|