react-detective 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +137 -2
- package/dist/chunk-5TDNB3DH.js +2686 -0
- package/dist/chunk-6EJCTAJS.js +25 -0
- package/dist/chunk-E23QXCCY.js +43 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/cli.js +35 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +2028 -0
- package/dist/init-JUCWNJPS.js +630 -0
- package/dist/loader.cjs +105 -0
- package/dist/loader.js +101 -0
- package/dist/next.cjs +58 -0
- package/dist/next.d.cts +24 -0
- package/dist/next.d.ts +22 -0
- package/dist/next.js +54 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +26 -0
- package/dist/server.d.ts +292 -0
- package/dist/server.js +15 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.js +132 -0
- package/package.json +84 -4
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import http, { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface DetectiveServerOptions {
|
|
5
|
+
/** Project root; source paths and the agent's working directory are relative to it. */
|
|
6
|
+
root: string;
|
|
7
|
+
/**
|
|
8
|
+
* Model for explanations: an alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID.
|
|
9
|
+
* Default `"sonnet"`.
|
|
10
|
+
*/
|
|
11
|
+
model?: string;
|
|
12
|
+
maxTurns?: number;
|
|
13
|
+
/** Knowledge folder name inside the project root. Default `.react-detective`. */
|
|
14
|
+
storeDir?: string;
|
|
15
|
+
/** URL prefix the handler answers on. */
|
|
16
|
+
base?: string;
|
|
17
|
+
}
|
|
18
|
+
type Next = (err?: unknown) => void;
|
|
19
|
+
/**
|
|
20
|
+
* Connect-style middleware shared by the Vite plugin (mounted on Vite's own
|
|
21
|
+
* server) and the standalone server used for Next.js and other bundlers.
|
|
22
|
+
*/
|
|
23
|
+
declare function createDetectiveHandler(opts: DetectiveServerOptions): (req: IncomingMessage, res: ServerResponse, next?: Next) => Promise<void>;
|
|
24
|
+
|
|
25
|
+
interface CommitInfo {
|
|
26
|
+
sha: string;
|
|
27
|
+
author: string;
|
|
28
|
+
date: string;
|
|
29
|
+
subject: string;
|
|
30
|
+
}
|
|
31
|
+
interface HeadInfo {
|
|
32
|
+
commit: string;
|
|
33
|
+
branch: string;
|
|
34
|
+
}
|
|
35
|
+
/** Thin, read-only, never-throwing wrapper around the git CLI. */
|
|
36
|
+
declare class Git {
|
|
37
|
+
private repo;
|
|
38
|
+
private readonly cwd;
|
|
39
|
+
constructor(cwd: string);
|
|
40
|
+
isRepo(): Promise<boolean>;
|
|
41
|
+
head(): Promise<HeadInfo | null>;
|
|
42
|
+
/** True if any of the files have uncommitted changes (or are untracked). */
|
|
43
|
+
isDirty(files: string[]): Promise<boolean>;
|
|
44
|
+
catBlob(sha: string): Promise<string | null>;
|
|
45
|
+
recentCommits(file: string, limit?: number): Promise<CommitInfo[]>;
|
|
46
|
+
logText(file: string, limit: number): Promise<string>;
|
|
47
|
+
blameText(file: string, start: number, end: number): Promise<string>;
|
|
48
|
+
showText(commit: string): Promise<string>;
|
|
49
|
+
private run;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One prop as a call site passes it: `title="Kitchen sink"`, `count={count}`, `{...rest}`. */
|
|
53
|
+
interface PassedProp {
|
|
54
|
+
/** The prop's name; "..." for a spread, "children" for what's written between the tags. */
|
|
55
|
+
name: string;
|
|
56
|
+
/** As written: `"Kitchen sink"`, `count`, `() => save(id)`, `true` for a bare attribute. */
|
|
57
|
+
value: string;
|
|
58
|
+
/** When the value is a name (`{query}`, `{user.name}`): the line in the same file where that name is declared. */
|
|
59
|
+
line?: number;
|
|
60
|
+
}
|
|
61
|
+
/** Where a component is rendered, with what it's given there. */
|
|
62
|
+
interface UsedIn {
|
|
63
|
+
/** `path:line`. */
|
|
64
|
+
ref: string;
|
|
65
|
+
/** The component that renders it there, e.g. "App". */
|
|
66
|
+
parent?: string;
|
|
67
|
+
props?: PassedProp[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
declare const CardSchema: z.ZodObject<{
|
|
71
|
+
summary: z.ZodString;
|
|
72
|
+
props: z.ZodArray<z.ZodObject<{
|
|
73
|
+
name: z.ZodString;
|
|
74
|
+
type: z.ZodString;
|
|
75
|
+
optional: z.ZodBoolean;
|
|
76
|
+
description: z.ZodString;
|
|
77
|
+
}, z.core.$strip>>;
|
|
78
|
+
outline: z.ZodArray<z.ZodObject<{
|
|
79
|
+
children: z.ZodArray<z.ZodObject<{
|
|
80
|
+
children: z.ZodArray<z.ZodObject<{
|
|
81
|
+
kind: z.ZodEnum<{
|
|
82
|
+
function: "function";
|
|
83
|
+
state: "state";
|
|
84
|
+
ref: "ref";
|
|
85
|
+
context: "context";
|
|
86
|
+
store: "store";
|
|
87
|
+
hook: "hook";
|
|
88
|
+
derived: "derived";
|
|
89
|
+
memo: "memo";
|
|
90
|
+
callback: "callback";
|
|
91
|
+
effect: "effect";
|
|
92
|
+
render: "render";
|
|
93
|
+
constant: "constant";
|
|
94
|
+
}>;
|
|
95
|
+
name: z.ZodString;
|
|
96
|
+
line: z.ZodNumber;
|
|
97
|
+
type: z.ZodString;
|
|
98
|
+
initial: z.ZodString;
|
|
99
|
+
returns: z.ZodString;
|
|
100
|
+
when: z.ZodString;
|
|
101
|
+
explanation: z.ZodString;
|
|
102
|
+
}, z.core.$strip>>;
|
|
103
|
+
kind: z.ZodEnum<{
|
|
104
|
+
function: "function";
|
|
105
|
+
state: "state";
|
|
106
|
+
ref: "ref";
|
|
107
|
+
context: "context";
|
|
108
|
+
store: "store";
|
|
109
|
+
hook: "hook";
|
|
110
|
+
derived: "derived";
|
|
111
|
+
memo: "memo";
|
|
112
|
+
callback: "callback";
|
|
113
|
+
effect: "effect";
|
|
114
|
+
render: "render";
|
|
115
|
+
constant: "constant";
|
|
116
|
+
}>;
|
|
117
|
+
name: z.ZodString;
|
|
118
|
+
line: z.ZodNumber;
|
|
119
|
+
type: z.ZodString;
|
|
120
|
+
initial: z.ZodString;
|
|
121
|
+
returns: z.ZodString;
|
|
122
|
+
when: z.ZodString;
|
|
123
|
+
explanation: z.ZodString;
|
|
124
|
+
}, z.core.$strip>>;
|
|
125
|
+
kind: z.ZodEnum<{
|
|
126
|
+
function: "function";
|
|
127
|
+
state: "state";
|
|
128
|
+
ref: "ref";
|
|
129
|
+
context: "context";
|
|
130
|
+
store: "store";
|
|
131
|
+
hook: "hook";
|
|
132
|
+
derived: "derived";
|
|
133
|
+
memo: "memo";
|
|
134
|
+
callback: "callback";
|
|
135
|
+
effect: "effect";
|
|
136
|
+
render: "render";
|
|
137
|
+
constant: "constant";
|
|
138
|
+
}>;
|
|
139
|
+
name: z.ZodString;
|
|
140
|
+
line: z.ZodNumber;
|
|
141
|
+
type: z.ZodString;
|
|
142
|
+
initial: z.ZodString;
|
|
143
|
+
returns: z.ZodString;
|
|
144
|
+
when: z.ZodString;
|
|
145
|
+
explanation: z.ZodString;
|
|
146
|
+
}, z.core.$strip>>;
|
|
147
|
+
usedIn: z.ZodArray<z.ZodObject<{
|
|
148
|
+
ref: z.ZodString;
|
|
149
|
+
}, z.core.$strip>>;
|
|
150
|
+
history: z.ZodArray<z.ZodObject<{
|
|
151
|
+
commit: z.ZodString;
|
|
152
|
+
date: z.ZodOptional<z.ZodString>;
|
|
153
|
+
author: z.ZodOptional<z.ZodString>;
|
|
154
|
+
why: z.ZodString;
|
|
155
|
+
}, z.core.$strip>>;
|
|
156
|
+
fileNotes: z.ZodArray<z.ZodObject<{
|
|
157
|
+
file: z.ZodString;
|
|
158
|
+
summary: z.ZodString;
|
|
159
|
+
exports: z.ZodArray<z.ZodObject<{
|
|
160
|
+
name: z.ZodString;
|
|
161
|
+
description: z.ZodString;
|
|
162
|
+
}, z.core.$strip>>;
|
|
163
|
+
}, z.core.$strip>>;
|
|
164
|
+
}, z.core.$strip>;
|
|
165
|
+
type OutlineFields = {
|
|
166
|
+
type?: string;
|
|
167
|
+
initial?: string;
|
|
168
|
+
when?: string;
|
|
169
|
+
returns?: string;
|
|
170
|
+
/** For a name a custom hook gives (from source only): the returned property, or array position ("0"), it reads. */
|
|
171
|
+
reads?: string;
|
|
172
|
+
/** For context and stores (from source only): what it reads, e.g. "ThemeContext" or "navigator.onLine". */
|
|
173
|
+
from?: string;
|
|
174
|
+
/** For an effect (from source only): the built-in that runs it, e.g. "useLayoutEffect". */
|
|
175
|
+
hook?: string;
|
|
176
|
+
};
|
|
177
|
+
type RawCard = Omit<z.infer<typeof CardSchema>, "fileNotes">;
|
|
178
|
+
type RawItem = RawCard["outline"][number];
|
|
179
|
+
/** Any depth: built from source, renders follow the JSX tree. (The model's schema allows two levels.) */
|
|
180
|
+
type OutlineItem = Omit<RawItem, "type" | "initial" | "when" | "returns" | "children"> & OutlineFields & {
|
|
181
|
+
children: OutlineItem[];
|
|
182
|
+
};
|
|
183
|
+
/** Call sites carry what they pass (read from source), on top of the model's `path:line`. */
|
|
184
|
+
type Card = Omit<RawCard, "outline" | "usedIn"> & {
|
|
185
|
+
outline: OutlineItem[];
|
|
186
|
+
usedIn: UsedIn[];
|
|
187
|
+
v?: number;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
interface ExplainRequest {
|
|
191
|
+
component: string;
|
|
192
|
+
/** Project-relative path, e.g. `src/components/UserCard.tsx`. May be absent for parents we only know by name. */
|
|
193
|
+
file?: string;
|
|
194
|
+
line?: number;
|
|
195
|
+
/** Outer → inner component names, as seen in the page. */
|
|
196
|
+
chain?: string[];
|
|
197
|
+
/** Serialized live props / state from the running app (client components only). */
|
|
198
|
+
props?: unknown;
|
|
199
|
+
state?: unknown;
|
|
200
|
+
/** Text of the element the developer pointed at, to anchor "this button" style questions. */
|
|
201
|
+
elementText?: string;
|
|
202
|
+
/** Free-form follow-up question about this component. */
|
|
203
|
+
question?: string;
|
|
204
|
+
}
|
|
205
|
+
/** What we already know, so the agent can skip work. */
|
|
206
|
+
interface ExplainContext {
|
|
207
|
+
/** Summaries of files whose content is verified unchanged since they were written. */
|
|
208
|
+
notes?: Record<string, string>;
|
|
209
|
+
/** A previous explanation that is outdated because some files it relied on changed. */
|
|
210
|
+
previous?: {
|
|
211
|
+
markdown: string;
|
|
212
|
+
changes: FileChange[];
|
|
213
|
+
unchanged: string[];
|
|
214
|
+
};
|
|
215
|
+
/** Recent commits touching the component's file, newest first. */
|
|
216
|
+
history?: CommitInfo[];
|
|
217
|
+
/** The current (fresh) explanation of the component, as background for a question. */
|
|
218
|
+
explanation?: string;
|
|
219
|
+
/** Still-valid Q&A about this component, then about related ones (those have `component` set). */
|
|
220
|
+
pastQuestions?: {
|
|
221
|
+
question: string;
|
|
222
|
+
answer: string;
|
|
223
|
+
component?: string;
|
|
224
|
+
}[];
|
|
225
|
+
/** Full source of files the agent would open first (the component's own file, imports without notes). Counts as read. */
|
|
226
|
+
sources?: Record<string, string>;
|
|
227
|
+
/** Where the component is rendered, from a text search: `path:line`. */
|
|
228
|
+
usages?: string[];
|
|
229
|
+
/** What the component's file imports from project files, with saved descriptions still valid for the current code. */
|
|
230
|
+
imports?: ImportedExport[];
|
|
231
|
+
}
|
|
232
|
+
interface ImportedExport {
|
|
233
|
+
/** Name as imported (`default` for a default import). */
|
|
234
|
+
name: string;
|
|
235
|
+
/** The file the import statement points at. */
|
|
236
|
+
from: string;
|
|
237
|
+
/** Where it is declared, when that differs from `from` (re-exported through a barrel file). */
|
|
238
|
+
declaredIn?: string;
|
|
239
|
+
line: number;
|
|
240
|
+
kind: string;
|
|
241
|
+
signature: string;
|
|
242
|
+
description: string;
|
|
243
|
+
}
|
|
244
|
+
interface FileChange {
|
|
245
|
+
file: string;
|
|
246
|
+
/** Unified diff from the explained version to now, when the old version could be recovered. */
|
|
247
|
+
diff?: string;
|
|
248
|
+
deleted?: boolean;
|
|
249
|
+
}
|
|
250
|
+
interface ExplainOptions {
|
|
251
|
+
root: string;
|
|
252
|
+
model?: string;
|
|
253
|
+
maxTurns?: number;
|
|
254
|
+
signal?: AbortSignal;
|
|
255
|
+
onProgress?: (text: string) => void;
|
|
256
|
+
context?: ExplainContext;
|
|
257
|
+
/** When set, the agent also gets read-only git history tools. */
|
|
258
|
+
git?: Git;
|
|
259
|
+
/** Anthropic API key the agent signs in with. */
|
|
260
|
+
apiKey: string;
|
|
261
|
+
}
|
|
262
|
+
interface ExplainResult {
|
|
263
|
+
/** Markdown answer (questions) or a markdown rendering of the card (explanations). */
|
|
264
|
+
markdown: string;
|
|
265
|
+
/** Structured explanation, rendered as visual sections by the panel. */
|
|
266
|
+
card?: Card;
|
|
267
|
+
/** Project-relative files the agent opened. */
|
|
268
|
+
filesRead: string[];
|
|
269
|
+
/** Per-file summaries the agent wrote for the files it read. */
|
|
270
|
+
notes: Record<string, string>;
|
|
271
|
+
/** Per-file descriptions of each export (file → export name → description). */
|
|
272
|
+
exports: Record<string, Record<string, string>>;
|
|
273
|
+
costUsd?: number;
|
|
274
|
+
/** Agent steps (model calls) the run took. */
|
|
275
|
+
turns?: number;
|
|
276
|
+
/** The model that actually answered (resolved from an alias like "sonnet"). */
|
|
277
|
+
model?: string;
|
|
278
|
+
}
|
|
279
|
+
declare function explainComponent(req: ExplainRequest, opts: ExplainOptions): Promise<ExplainResult>;
|
|
280
|
+
|
|
281
|
+
declare const DEFAULT_PORT = 5899;
|
|
282
|
+
/**
|
|
283
|
+
* Starts the explain server on 127.0.0.1. Resolves to `null` (without throwing)
|
|
284
|
+
* if the port is already taken, which happens when frameworks evaluate their
|
|
285
|
+
* config in several processes. The first one wins and the others reuse it.
|
|
286
|
+
*/
|
|
287
|
+
declare function startDetectiveServer(opts: DetectiveServerOptions & {
|
|
288
|
+
port?: number;
|
|
289
|
+
silent?: boolean;
|
|
290
|
+
}): Promise<http.Server | null>;
|
|
291
|
+
|
|
292
|
+
export { DEFAULT_PORT, type DetectiveServerOptions, type ExplainRequest, createDetectiveHandler, explainComponent, startDetectiveServer };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_PORT,
|
|
3
|
+
startDetectiveServer
|
|
4
|
+
} from "./chunk-6EJCTAJS.js";
|
|
5
|
+
import {
|
|
6
|
+
createDetectiveHandler,
|
|
7
|
+
explainComponent
|
|
8
|
+
} from "./chunk-5TDNB3DH.js";
|
|
9
|
+
import "./chunk-E23QXCCY.js";
|
|
10
|
+
export {
|
|
11
|
+
DEFAULT_PORT,
|
|
12
|
+
createDetectiveHandler,
|
|
13
|
+
explainComponent,
|
|
14
|
+
startDetectiveServer
|
|
15
|
+
};
|
package/dist/vite.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Plugin } from 'vite';
|
|
2
|
+
|
|
3
|
+
interface DetectiveViteOptions {
|
|
4
|
+
/** Model for explanations: an alias (`"sonnet"`, `"haiku"`, `"opus"`) or a full model ID. Default `"sonnet"`. */
|
|
5
|
+
model?: string;
|
|
6
|
+
maxTurns?: number;
|
|
7
|
+
/** Knowledge folder inside the project root. Default `.react-detective`. */
|
|
8
|
+
storeDir?: string;
|
|
9
|
+
/** Inject the overlay into index.html automatically. Default true. */
|
|
10
|
+
injectClient?: boolean;
|
|
11
|
+
/** Show the Detective badge in the corner. Default true. */
|
|
12
|
+
showLauncher?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Vite plugin: dev-only. Production builds are left completely untouched. */
|
|
15
|
+
declare function detective(options?: DetectiveViteOptions): Plugin;
|
|
16
|
+
|
|
17
|
+
export { type DetectiveViteOptions, detective as default, detective };
|
package/dist/vite.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reloadingHandler
|
|
3
|
+
} from "./chunk-5TDNB3DH.js";
|
|
4
|
+
import "./chunk-E23QXCCY.js";
|
|
5
|
+
|
|
6
|
+
// src/transform.ts
|
|
7
|
+
import { parse } from "@babel/parser";
|
|
8
|
+
import _traverse from "@babel/traverse";
|
|
9
|
+
import MagicString from "magic-string";
|
|
10
|
+
import path from "path";
|
|
11
|
+
var traverse = _traverse.default ?? _traverse;
|
|
12
|
+
var LOC_ATTR = "data-rd-loc";
|
|
13
|
+
var COMP_ATTR = "data-rd-comp";
|
|
14
|
+
var FILE_RE = /\.[cm]?[jt]sx?$/;
|
|
15
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set(["html", "head", "meta", "title", "script", "style", "link", "base", "noscript"]);
|
|
16
|
+
function transformJsx(code, id, root) {
|
|
17
|
+
const filename = id.split("?")[0];
|
|
18
|
+
if (!FILE_RE.test(filename) || filename.includes("node_modules")) return null;
|
|
19
|
+
if (!code.includes("<")) return null;
|
|
20
|
+
const isTs = /\.[cm]?tsx?$/.test(filename);
|
|
21
|
+
if (isTs && !filename.endsWith("x")) return null;
|
|
22
|
+
let ast;
|
|
23
|
+
try {
|
|
24
|
+
ast = parse(code, {
|
|
25
|
+
sourceType: "module",
|
|
26
|
+
errorRecovery: true,
|
|
27
|
+
plugins: isTs ? ["jsx", "typescript", "decorators-legacy"] : ["jsx", "decorators-legacy"]
|
|
28
|
+
});
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const rel = path.relative(root, filename).split(path.sep).join("/");
|
|
33
|
+
const s = new MagicString(code);
|
|
34
|
+
let changed = false;
|
|
35
|
+
traverse(ast, {
|
|
36
|
+
JSXOpeningElement(p) {
|
|
37
|
+
const name = p.node.name;
|
|
38
|
+
if (name.type !== "JSXIdentifier") return;
|
|
39
|
+
const tag = name.name;
|
|
40
|
+
if (!/^[a-z]/.test(tag) || SKIP_TAGS.has(tag)) return;
|
|
41
|
+
const hasAttr = p.node.attributes.some(
|
|
42
|
+
(a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === LOC_ATTR
|
|
43
|
+
);
|
|
44
|
+
if (hasAttr || name.end == null || !p.node.loc) return;
|
|
45
|
+
const comp = findComponentName(p, filename);
|
|
46
|
+
const { line, column } = p.node.loc.start;
|
|
47
|
+
let attrs = ` ${LOC_ATTR}="${rel}:${line}:${column + 1}"`;
|
|
48
|
+
if (comp) attrs += ` ${COMP_ATTR}="${comp}"`;
|
|
49
|
+
s.appendLeft(name.end, attrs);
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (!changed) return null;
|
|
54
|
+
return { code: s.toString(), map: s.generateMap({ source: filename, hires: true, includeContent: true }) };
|
|
55
|
+
}
|
|
56
|
+
function findComponentName(p, filename) {
|
|
57
|
+
let cur = p.getFunctionParent();
|
|
58
|
+
while (cur) {
|
|
59
|
+
const name = functionName(cur);
|
|
60
|
+
if (name && /^[A-Z]/.test(name)) return name;
|
|
61
|
+
if (!name && isDefaultExport(cur)) return componentNameFromFile(filename);
|
|
62
|
+
cur = cur.parentPath?.getFunctionParent() ?? null;
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
function functionName(fn) {
|
|
67
|
+
const node = fn.node;
|
|
68
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression") && node.id) return node.id.name;
|
|
69
|
+
if (node.type === "ClassMethod" || node.type === "ClassPrivateMethod") {
|
|
70
|
+
const cls = fn.parentPath?.parentPath?.node;
|
|
71
|
+
if (cls && (cls.type === "ClassDeclaration" || cls.type === "ClassExpression") && cls.id) return cls.id.name;
|
|
72
|
+
}
|
|
73
|
+
let parent = fn.parentPath;
|
|
74
|
+
while (parent && parent.node.type === "CallExpression") parent = parent.parentPath;
|
|
75
|
+
if (parent?.node.type === "VariableDeclarator" && parent.node.id.type === "Identifier") return parent.node.id.name;
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function isDefaultExport(fn) {
|
|
79
|
+
let parent = fn.parentPath;
|
|
80
|
+
while (parent && parent.node.type === "CallExpression") parent = parent.parentPath;
|
|
81
|
+
return parent?.node.type === "ExportDefaultDeclaration";
|
|
82
|
+
}
|
|
83
|
+
function componentNameFromFile(filename) {
|
|
84
|
+
let base = path.basename(filename).replace(FILE_RE, "");
|
|
85
|
+
if (["page", "layout", "index", "template", "loading", "error", "not-found"].includes(base)) {
|
|
86
|
+
const dir = path.basename(path.dirname(filename));
|
|
87
|
+
const label = base[0].toUpperCase() + base.slice(1).replace(/-(\w)/g, (_, c) => c.toUpperCase());
|
|
88
|
+
return dir && !dir.startsWith("(") && dir !== "app" && dir !== "pages" ? `${pascal(dir)}${label}` : label;
|
|
89
|
+
}
|
|
90
|
+
return pascal(base);
|
|
91
|
+
}
|
|
92
|
+
function pascal(s) {
|
|
93
|
+
return s.replace(/[[\]]/g, "").replace(/(^|[-_.\s]+)(\w)/g, (_, __, c) => c.toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/vite.ts
|
|
97
|
+
var VIRTUAL_ID = "virtual:react-detective";
|
|
98
|
+
var BASE = "/__detective";
|
|
99
|
+
function detective(options = {}) {
|
|
100
|
+
let root = process.cwd();
|
|
101
|
+
return {
|
|
102
|
+
name: "react-detective",
|
|
103
|
+
apply: "serve",
|
|
104
|
+
// Run before @vitejs/plugin-react so we see the original JSX.
|
|
105
|
+
enforce: "pre",
|
|
106
|
+
configResolved(config) {
|
|
107
|
+
root = config.root;
|
|
108
|
+
},
|
|
109
|
+
transform(code, id) {
|
|
110
|
+
return transformJsx(code, id, root);
|
|
111
|
+
},
|
|
112
|
+
configureServer(server) {
|
|
113
|
+
const opts = { root, model: options.model, maxTurns: options.maxTurns, storeDir: options.storeDir, base: BASE };
|
|
114
|
+
server.middlewares.use(reloadingHandler(opts, () => server.ws.send({ type: "full-reload" })));
|
|
115
|
+
},
|
|
116
|
+
resolveId(id) {
|
|
117
|
+
if (id === VIRTUAL_ID || id === `/@id/${VIRTUAL_ID}`) return "\0" + VIRTUAL_ID;
|
|
118
|
+
},
|
|
119
|
+
load(id) {
|
|
120
|
+
if (id === "\0" + VIRTUAL_ID) return `import { init } from "react-detective/client";
|
|
121
|
+
init({ endpoint: ${JSON.stringify(BASE)}, showLauncher: ${options.showLauncher !== false} });`;
|
|
122
|
+
},
|
|
123
|
+
transformIndexHtml() {
|
|
124
|
+
if (options.injectClient === false) return;
|
|
125
|
+
return [{ tag: "script", attrs: { type: "module", src: `/@id/${VIRTUAL_ID}` }, injectTo: "body" }];
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export {
|
|
130
|
+
detective as default,
|
|
131
|
+
detective
|
|
132
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,86 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-detective",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Hover any React component to see what it is, where it lives, and an AI explanation of what it does.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/headbranch/react-detective.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/headbranch/react-detective#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/headbranch/react-detective/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
"./vite": {
|
|
20
|
+
"types": "./dist/vite.d.ts",
|
|
21
|
+
"import": "./dist/vite.js"
|
|
22
|
+
},
|
|
23
|
+
"./next": {
|
|
24
|
+
"types": "./dist/next.d.ts",
|
|
25
|
+
"import": "./dist/next.js",
|
|
26
|
+
"require": "./dist/next.cjs"
|
|
27
|
+
},
|
|
28
|
+
"./next/loader": {
|
|
29
|
+
"require": "./dist/loader.cjs",
|
|
30
|
+
"import": "./dist/loader.js"
|
|
31
|
+
},
|
|
32
|
+
"./react": {
|
|
33
|
+
"types": "./dist/react.d.ts",
|
|
34
|
+
"import": "./dist/react.js"
|
|
35
|
+
},
|
|
36
|
+
"./client": {
|
|
37
|
+
"types": "./dist/client.d.ts",
|
|
38
|
+
"import": "./dist/client.js"
|
|
39
|
+
},
|
|
40
|
+
"./server": {
|
|
41
|
+
"types": "./dist/server.d.ts",
|
|
42
|
+
"import": "./dist/server.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"bin": {
|
|
46
|
+
"react-detective": "./dist/cli.js"
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"test": "node --test --experimental-strip-types test/*.test.ts",
|
|
52
|
+
"release": "release-it"
|
|
53
|
+
},
|
|
54
|
+
"peerDependencies": {
|
|
55
|
+
"next": ">=14",
|
|
56
|
+
"react": ">=18",
|
|
57
|
+
"vite": ">=5"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"vite": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"next": {
|
|
64
|
+
"optional": true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.296",
|
|
69
|
+
"@babel/parser": "^8.0.7",
|
|
70
|
+
"@babel/traverse": "^8.0.7",
|
|
71
|
+
"diff": "^9.0.0",
|
|
72
|
+
"launch-editor": "^2.14.2",
|
|
73
|
+
"magic-string": "^1.4.3",
|
|
74
|
+
"zod": "^4.6.5"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@types/babel__traverse": "^7.28.0",
|
|
78
|
+
"@types/node": "^26.6.5",
|
|
79
|
+
"@types/react": "^19.3.0",
|
|
80
|
+
"react": "^19.3.0",
|
|
81
|
+
"release-it": "^20.2.1",
|
|
82
|
+
"tsup": "^8.5.1",
|
|
83
|
+
"typescript": "^5.9.3",
|
|
84
|
+
"vite": "^8.3.4"
|
|
85
|
+
}
|
|
86
|
+
}
|