reflet-sdk 0.4.3 → 0.5.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/CHANGELOG.md +21 -0
- package/README.md +64 -0
- package/dist/{client-BYuSKJw5.js → client-BWEBWk1k.js} +3 -3
- package/dist/client-BWEBWk1k.js.map +1 -0
- package/dist/client-w4Rkl10y.d.ts.map +1 -1
- package/dist/devtools-layer-EyK7n9w3.js +3935 -0
- package/dist/devtools-layer-EyK7n9w3.js.map +1 -0
- package/dist/devtools-next.d.ts +19 -0
- package/dist/devtools-next.d.ts.map +1 -0
- package/dist/devtools-next.js +25 -0
- package/dist/devtools-next.js.map +1 -0
- package/dist/devtools-vite.d.ts +8 -0
- package/dist/devtools-vite.d.ts.map +1 -0
- package/dist/devtools-vite.js +75 -0
- package/dist/devtools-vite.js.map +1 -0
- package/dist/feedback.d.ts +5 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +60 -1978
- package/dist/feedback.js.map +1 -1
- package/dist/handler-CHP0m7KF.js +713 -0
- package/dist/handler-CHP0m7KF.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/options-7KtQZJj8.d.ts +20 -0
- package/dist/options-7KtQZJj8.d.ts.map +1 -0
- package/dist/picker-DFfC1uIY.js +1907 -0
- package/dist/picker-DFfC1uIY.js.map +1 -0
- package/dist/protocol-Bn6Bg_0r.js +32 -0
- package/dist/protocol-Bn6Bg_0r.js.map +1 -0
- package/dist/react.js +2 -2
- package/dist/{z-index-_Q9PWbfY.js → z-index-CKxBgZTG.js} +3 -2
- package/dist/{z-index-_Q9PWbfY.js.map → z-index-CKxBgZTG.js.map} +1 -1
- package/package.json +15 -2
- package/dist/client-BYuSKJw5.js.map +0 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
import { a as EDITORS, i as DEVTOOLS_STATUS_MARKER, n as DEVTOOLS_REQUEST_HEADER, o as PROXIED_API_PATHS, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-Bn6Bg_0r.js";
|
|
2
|
+
import "./client-BWEBWk1k.js";
|
|
3
|
+
import { SourceMap } from "node:module";
|
|
4
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
7
|
+
import { readFile, realpath, stat } from "node:fs/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
//#region src/devtools/server/json-response.ts
|
|
10
|
+
const JSON_RESPONSE_HEADERS = {
|
|
11
|
+
"cache-control": "no-store",
|
|
12
|
+
"content-type": "application/json; charset=utf-8",
|
|
13
|
+
"x-content-type-options": "nosniff"
|
|
14
|
+
};
|
|
15
|
+
function jsonTextResponse(text, status) {
|
|
16
|
+
return new Response(text, {
|
|
17
|
+
headers: JSON_RESPONSE_HEADERS,
|
|
18
|
+
status
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function jsonResponse(body, status = 200) {
|
|
22
|
+
return jsonTextResponse(JSON.stringify(body), status);
|
|
23
|
+
}
|
|
24
|
+
function errorResponse(error, status) {
|
|
25
|
+
return jsonResponse({ error }, status);
|
|
26
|
+
}
|
|
27
|
+
function failure(error, status) {
|
|
28
|
+
return {
|
|
29
|
+
error,
|
|
30
|
+
ok: false,
|
|
31
|
+
status
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/devtools/server/options.ts
|
|
36
|
+
const DEFAULT_EDITOR = "vscode";
|
|
37
|
+
const TRAILING_SLASHES$1 = /\/+$/;
|
|
38
|
+
function isEditorId(value) {
|
|
39
|
+
return EDITORS.some((editor) => editor === value);
|
|
40
|
+
}
|
|
41
|
+
function presentValue(value) {
|
|
42
|
+
const trimmed = value?.trim();
|
|
43
|
+
return trimmed ? trimmed : void 0;
|
|
44
|
+
}
|
|
45
|
+
function resolveEditor(optionEditor, envEditor) {
|
|
46
|
+
if (isEditorId(optionEditor)) return optionEditor;
|
|
47
|
+
const normalizedEnvEditor = presentValue(envEditor)?.toLowerCase();
|
|
48
|
+
return isEditorId(normalizedEnvEditor) ? normalizedEnvEditor : DEFAULT_EDITOR;
|
|
49
|
+
}
|
|
50
|
+
function resolveDevtoolsOptions(options, env) {
|
|
51
|
+
const apiUrl = presentValue(options.apiUrl) ?? presentValue(env.REFLET_API_URL) ?? "https://harmless-clam-802.convex.site";
|
|
52
|
+
return {
|
|
53
|
+
allowedHosts: (options.allowedHosts ?? presentValue(env.REFLET_DEVTOOLS_HOSTS)?.split(",") ?? []).map((host) => host.trim().toLowerCase()).filter(Boolean),
|
|
54
|
+
apiUrl: apiUrl.replace(TRAILING_SLASHES$1, ""),
|
|
55
|
+
editor: resolveEditor(options.editor, env.REFLET_EDITOR),
|
|
56
|
+
root: resolve(presentValue(options.root) ?? process.cwd()),
|
|
57
|
+
secretKey: presentValue(options.secretKey) ?? presentValue(env.REFLET_SECRET_KEY) ?? null
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/devtools/server/reflet-proxy.ts
|
|
62
|
+
const CREATE_FEEDBACK_PATH = "/api/v1/feedback/create";
|
|
63
|
+
const MISSING_SECRET_KEY_ERROR = "Set REFLET_SECRET_KEY in the dev server environment to reach your Reflet board.";
|
|
64
|
+
const BODYLESS_STATUSES = [
|
|
65
|
+
204,
|
|
66
|
+
205,
|
|
67
|
+
304
|
|
68
|
+
];
|
|
69
|
+
function isProxiedApiPath(path) {
|
|
70
|
+
return PROXIED_API_PATHS.some((proxiedPath) => proxiedPath === path);
|
|
71
|
+
}
|
|
72
|
+
function isJsonObject(value) {
|
|
73
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
74
|
+
}
|
|
75
|
+
async function readForwardedBody(request, apiPath) {
|
|
76
|
+
if (request.method !== "POST") return { body: void 0 };
|
|
77
|
+
const text = await request.text();
|
|
78
|
+
if (apiPath !== CREATE_FEEDBACK_PATH) return { body: text || void 0 };
|
|
79
|
+
let payload;
|
|
80
|
+
try {
|
|
81
|
+
payload = JSON.parse(text);
|
|
82
|
+
} catch {
|
|
83
|
+
return { error: "Feedback must be a JSON object." };
|
|
84
|
+
}
|
|
85
|
+
if (!isJsonObject(payload)) return { error: "Feedback must be a JSON object." };
|
|
86
|
+
return { body: JSON.stringify({
|
|
87
|
+
...payload,
|
|
88
|
+
internal: true
|
|
89
|
+
}) };
|
|
90
|
+
}
|
|
91
|
+
/** Forwards an allowlisted Reflet API call with the secret key; devtools notes are always internal. */
|
|
92
|
+
async function proxyToReflet(request, apiPath, target) {
|
|
93
|
+
if (!isProxiedApiPath(apiPath)) return errorResponse(`Reflet devtools does not forward ${apiPath || "/"}.`, 404);
|
|
94
|
+
if (!target.secretKey) return errorResponse(MISSING_SECRET_KEY_ERROR, 503);
|
|
95
|
+
if (request.method !== "GET" && request.method !== "POST") return errorResponse(`${request.method} is not supported here.`, 405);
|
|
96
|
+
const forwarded = await readForwardedBody(request, apiPath);
|
|
97
|
+
if ("error" in forwarded) return errorResponse(forwarded.error, 400);
|
|
98
|
+
const { search } = new URL(request.url);
|
|
99
|
+
let upstream;
|
|
100
|
+
try {
|
|
101
|
+
upstream = await fetch(`${target.apiUrl}${apiPath}${search}`, {
|
|
102
|
+
body: forwarded.body,
|
|
103
|
+
headers: {
|
|
104
|
+
Accept: "application/json",
|
|
105
|
+
Authorization: `Bearer ${target.secretKey}`,
|
|
106
|
+
"Content-Type": "application/json"
|
|
107
|
+
},
|
|
108
|
+
method: request.method
|
|
109
|
+
});
|
|
110
|
+
} catch {
|
|
111
|
+
return errorResponse(`Could not reach Reflet at ${target.apiUrl}.`, 502);
|
|
112
|
+
}
|
|
113
|
+
const text = await upstream.text();
|
|
114
|
+
return jsonTextResponse(BODYLESS_STATUSES.includes(upstream.status) ? null : text, upstream.status);
|
|
115
|
+
}
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region src/devtools/server/request-guard.ts
|
|
118
|
+
const LOOPBACK_HOSTNAMES = ["localhost", "[::1]"];
|
|
119
|
+
const LOOPBACK_IPV4 = /^127(?:\.\d{1,3}){3}$/;
|
|
120
|
+
function hostnameOf(host) {
|
|
121
|
+
try {
|
|
122
|
+
return new URL(`http://${host}`).hostname.toLowerCase();
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function isAllowedHost(hostname, allowedHosts) {
|
|
128
|
+
return LOOPBACK_HOSTNAMES.includes(hostname) || hostname.endsWith(".localhost") || LOOPBACK_IPV4.test(hostname) || allowedHosts.includes(hostname);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The Host check stops DNS rebinding: a hostile domain resolved to 127.0.0.1
|
|
132
|
+
* is same-origin with itself, so origin checks alone would let it through.
|
|
133
|
+
*/
|
|
134
|
+
function rejectUntrustedRequest(request, allowedHosts) {
|
|
135
|
+
const hostname = hostnameOf(request.headers.get("host") ?? new URL(request.url).host);
|
|
136
|
+
if (!(hostname && isAllowedHost(hostname, allowedHosts))) return errorResponse("Reflet devtools only answers on localhost. Add other dev hostnames to allowedHosts or REFLET_DEVTOOLS_HOSTS.", 403);
|
|
137
|
+
if (request.headers.get("x-reflet-devtools") !== "1") return errorResponse(`Reflet devtools requests must send the ${DEVTOOLS_REQUEST_HEADER} header.`, 403);
|
|
138
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
139
|
+
if (!(fetchSite === null || fetchSite === "same-origin" || fetchSite === "none")) return errorResponse("Reflet devtools only answers requests from the app's own origin.", 403);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//#region src/devtools/server/source/code-search.ts
|
|
144
|
+
const MIN_QUERY_LENGTH = 2;
|
|
145
|
+
const MAX_QUERY_LENGTH = 200;
|
|
146
|
+
const MAX_MATCHES = 20;
|
|
147
|
+
const PREVIEW_LENGTH = 160;
|
|
148
|
+
const PREVIEW_LEAD = 40;
|
|
149
|
+
const SEARCH_TIMEOUT_MS = 4e3;
|
|
150
|
+
const MAX_OUTPUT_BYTES = 1048576;
|
|
151
|
+
const NO_MATCHES_EXIT_CODE = 1;
|
|
152
|
+
const NOT_A_REPOSITORY_EXIT_CODE = 128;
|
|
153
|
+
const SEARCHED_FILES = [
|
|
154
|
+
"*.tsx",
|
|
155
|
+
"*.jsx",
|
|
156
|
+
"*.ts",
|
|
157
|
+
"*.js",
|
|
158
|
+
"*.vue",
|
|
159
|
+
"*.svelte",
|
|
160
|
+
"*.astro",
|
|
161
|
+
"*.mdx"
|
|
162
|
+
];
|
|
163
|
+
const LINE_BREAK = /[\r\n]/;
|
|
164
|
+
function runGitGrep(workspaceRoot, query) {
|
|
165
|
+
const args = [
|
|
166
|
+
"grep",
|
|
167
|
+
"-n",
|
|
168
|
+
"-z",
|
|
169
|
+
"-I",
|
|
170
|
+
"-F",
|
|
171
|
+
"--no-color",
|
|
172
|
+
"--untracked",
|
|
173
|
+
"-e",
|
|
174
|
+
query,
|
|
175
|
+
"--",
|
|
176
|
+
...SEARCHED_FILES
|
|
177
|
+
];
|
|
178
|
+
return new Promise((resolveRun) => {
|
|
179
|
+
execFile("git", args, {
|
|
180
|
+
cwd: workspaceRoot,
|
|
181
|
+
encoding: "utf8",
|
|
182
|
+
maxBuffer: MAX_OUTPUT_BYTES,
|
|
183
|
+
timeout: SEARCH_TIMEOUT_MS
|
|
184
|
+
}, (error, stdout) => resolveRun({
|
|
185
|
+
error,
|
|
186
|
+
stdout
|
|
187
|
+
}));
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
function previewLine(content, query) {
|
|
191
|
+
const text = content.trim();
|
|
192
|
+
if (text.length <= PREVIEW_LENGTH) return text;
|
|
193
|
+
const matchIndex = text.indexOf(query);
|
|
194
|
+
const start = Math.max(0, Math.min(matchIndex - PREVIEW_LEAD, text.length - PREVIEW_LENGTH));
|
|
195
|
+
const end = start + PREVIEW_LENGTH;
|
|
196
|
+
const head = start > 0 ? "…" : "";
|
|
197
|
+
const tail = end < text.length ? "…" : "";
|
|
198
|
+
return `${head}${text.slice(start, end)}${tail}`;
|
|
199
|
+
}
|
|
200
|
+
function parseMatches(stdout, workspaceRoot, query) {
|
|
201
|
+
const matches = [];
|
|
202
|
+
for (const record of stdout.split("\n")) {
|
|
203
|
+
const [path, lineText, content] = record.split("\0");
|
|
204
|
+
const line = Number(lineText);
|
|
205
|
+
if (path !== void 0 && content !== void 0 && Number.isInteger(line) && !path.split("/").includes("node_modules")) {
|
|
206
|
+
matches.push({
|
|
207
|
+
absolutePath: join(workspaceRoot, path),
|
|
208
|
+
line,
|
|
209
|
+
path,
|
|
210
|
+
preview: previewLine(content, query)
|
|
211
|
+
});
|
|
212
|
+
if (matches.length === MAX_MATCHES) break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return matches;
|
|
216
|
+
}
|
|
217
|
+
function describeGitFailure(error) {
|
|
218
|
+
if (error.code === "ENOENT") return failure("Code search needs git installed on the dev server.", 503);
|
|
219
|
+
if (error.code === NOT_A_REPOSITORY_EXIT_CODE) return failure("Code search needs the app to live in a git repository.", 503);
|
|
220
|
+
if (error.killed) return failure("Code search timed out.", 504);
|
|
221
|
+
return failure(`Code search failed: ${error.message}`, 500);
|
|
222
|
+
}
|
|
223
|
+
async function searchCode(workspaceRoot, rawQuery) {
|
|
224
|
+
const query = rawQuery.trim();
|
|
225
|
+
if (query.length < MIN_QUERY_LENGTH || query.length > MAX_QUERY_LENGTH) return failure(`Search for ${MIN_QUERY_LENGTH} to ${MAX_QUERY_LENGTH} characters.`, 400);
|
|
226
|
+
if (LINE_BREAK.test(query)) return failure("Search for a single line of text.", 400);
|
|
227
|
+
const { error, stdout } = await runGitGrep(workspaceRoot, query);
|
|
228
|
+
const matches = parseMatches(stdout, workspaceRoot, query);
|
|
229
|
+
const hasNoMatches = error?.code === NO_MATCHES_EXIT_CODE;
|
|
230
|
+
if (!error || hasNoMatches || matches.length > 0) return {
|
|
231
|
+
ok: true,
|
|
232
|
+
value: { matches }
|
|
233
|
+
};
|
|
234
|
+
return describeGitFailure(error);
|
|
235
|
+
}
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region src/devtools/server/source/project-roots.ts
|
|
238
|
+
function resolveProjectRoots(appRoot) {
|
|
239
|
+
const resolvedRoot = resolve(appRoot);
|
|
240
|
+
const root = existsSync(resolvedRoot) ? realpathSync(resolvedRoot) : resolvedRoot;
|
|
241
|
+
for (let directory = root;; directory = dirname(directory)) {
|
|
242
|
+
if (existsSync(join(directory, ".git"))) return {
|
|
243
|
+
root,
|
|
244
|
+
workspaceRoot: directory
|
|
245
|
+
};
|
|
246
|
+
if (dirname(directory) === directory) return {
|
|
247
|
+
root,
|
|
248
|
+
workspaceRoot: root
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//#endregion
|
|
253
|
+
//#region src/devtools/server/source/jsx-range.ts
|
|
254
|
+
const FAILED = -1;
|
|
255
|
+
const MAX_TAG_LOOKBACK = 2;
|
|
256
|
+
const TAG_NAME_START = /[A-Za-z_$]/;
|
|
257
|
+
const TAG_NAME_CHAR = /[\w$.:-]/;
|
|
258
|
+
const IDENTIFIER_CHAR = /[\w$]/;
|
|
259
|
+
const WHITESPACE = /\s/;
|
|
260
|
+
const JSX_EXPRESSION_LEAD = /[([{,;=?:&|!>]/;
|
|
261
|
+
const JSX_KEYWORD_LEADS = [
|
|
262
|
+
"return",
|
|
263
|
+
"yield",
|
|
264
|
+
"default",
|
|
265
|
+
"await"
|
|
266
|
+
];
|
|
267
|
+
function isTagStart(code, index) {
|
|
268
|
+
if (code.charAt(index) !== "<") return false;
|
|
269
|
+
const next = code.charAt(index + 1);
|
|
270
|
+
return next === ">" || TAG_NAME_START.test(next);
|
|
271
|
+
}
|
|
272
|
+
function startsJsxInExpression(code, index) {
|
|
273
|
+
let cursor = index - 1;
|
|
274
|
+
while (cursor >= 0 && WHITESPACE.test(code.charAt(cursor))) cursor -= 1;
|
|
275
|
+
if (cursor < 0) return true;
|
|
276
|
+
const previous = code.charAt(cursor);
|
|
277
|
+
if (JSX_EXPRESSION_LEAD.test(previous)) return true;
|
|
278
|
+
let wordStart = cursor;
|
|
279
|
+
while (wordStart > 0 && IDENTIFIER_CHAR.test(code.charAt(wordStart - 1))) wordStart -= 1;
|
|
280
|
+
return JSX_KEYWORD_LEADS.includes(code.slice(wordStart, cursor + 1));
|
|
281
|
+
}
|
|
282
|
+
function skipJsString(code, index) {
|
|
283
|
+
const quote = code.charAt(index);
|
|
284
|
+
let cursor = index + 1;
|
|
285
|
+
while (cursor < code.length) {
|
|
286
|
+
const char = code.charAt(cursor);
|
|
287
|
+
if (char === "\\") cursor += 2;
|
|
288
|
+
else if (char === quote) return cursor + 1;
|
|
289
|
+
else if (char === "\n") return index + 1;
|
|
290
|
+
else cursor += 1;
|
|
291
|
+
}
|
|
292
|
+
return index + 1;
|
|
293
|
+
}
|
|
294
|
+
function skipTemplate(code, index) {
|
|
295
|
+
let cursor = index + 1;
|
|
296
|
+
while (cursor < code.length) {
|
|
297
|
+
const char = code.charAt(cursor);
|
|
298
|
+
if (char === "\\") cursor += 2;
|
|
299
|
+
else if (char === "`") return cursor + 1;
|
|
300
|
+
else if (char === "$" && code.charAt(cursor + 1) === "{") {
|
|
301
|
+
cursor = skipJsExpression(code, cursor + 1);
|
|
302
|
+
if (cursor === FAILED) return FAILED;
|
|
303
|
+
} else cursor += 1;
|
|
304
|
+
}
|
|
305
|
+
return FAILED;
|
|
306
|
+
}
|
|
307
|
+
function skipJsToken(code, index) {
|
|
308
|
+
const char = code.charAt(index);
|
|
309
|
+
const next = code.charAt(index + 1);
|
|
310
|
+
if (char === "\"" || char === "'") return skipJsString(code, index);
|
|
311
|
+
if (char === "`") return skipTemplate(code, index);
|
|
312
|
+
if (char === "/" && next === "/") {
|
|
313
|
+
const lineEnd = code.indexOf("\n", index);
|
|
314
|
+
return lineEnd === -1 ? code.length : lineEnd;
|
|
315
|
+
}
|
|
316
|
+
if (char === "/" && next === "*") {
|
|
317
|
+
const commentEnd = code.indexOf("*/", index + 2);
|
|
318
|
+
return commentEnd === -1 ? FAILED : commentEnd + 2;
|
|
319
|
+
}
|
|
320
|
+
if (isTagStart(code, index) && startsJsxInExpression(code, index)) return skipElement(code, index);
|
|
321
|
+
return index + 1;
|
|
322
|
+
}
|
|
323
|
+
function skipJsExpression(code, index) {
|
|
324
|
+
let depth = 0;
|
|
325
|
+
let cursor = index;
|
|
326
|
+
while (cursor < code.length) {
|
|
327
|
+
const char = code.charAt(cursor);
|
|
328
|
+
if (char === "{" || char === "}") {
|
|
329
|
+
depth += char === "{" ? 1 : -1;
|
|
330
|
+
cursor += 1;
|
|
331
|
+
if (depth === 0) return cursor;
|
|
332
|
+
} else {
|
|
333
|
+
cursor = skipJsToken(code, cursor);
|
|
334
|
+
if (cursor === FAILED) return FAILED;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return FAILED;
|
|
338
|
+
}
|
|
339
|
+
function skipTypeArguments(code, index) {
|
|
340
|
+
let depth = 0;
|
|
341
|
+
for (let cursor = index; cursor < code.length; cursor += 1) {
|
|
342
|
+
const char = code.charAt(cursor);
|
|
343
|
+
if (char === "<") depth += 1;
|
|
344
|
+
else if (char === ">") {
|
|
345
|
+
depth -= 1;
|
|
346
|
+
if (depth === 0) return cursor + 1;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return FAILED;
|
|
350
|
+
}
|
|
351
|
+
function skipAttributeToken(code, index) {
|
|
352
|
+
const char = code.charAt(index);
|
|
353
|
+
if (char === "{") return skipJsExpression(code, index);
|
|
354
|
+
if (char === "\"" || char === "'") {
|
|
355
|
+
const closingQuote = code.indexOf(char, index + 1);
|
|
356
|
+
return closingQuote === -1 ? FAILED : closingQuote + 1;
|
|
357
|
+
}
|
|
358
|
+
return index + 1;
|
|
359
|
+
}
|
|
360
|
+
function readTagName(code, index) {
|
|
361
|
+
let cursor = index;
|
|
362
|
+
while (TAG_NAME_CHAR.test(code.charAt(cursor))) cursor += 1;
|
|
363
|
+
return cursor;
|
|
364
|
+
}
|
|
365
|
+
function readOpeningTag(code, index) {
|
|
366
|
+
const nameEnd = readTagName(code, index + 1);
|
|
367
|
+
const name = code.slice(index + 1, nameEnd);
|
|
368
|
+
let cursor = code.charAt(nameEnd) === "<" ? skipTypeArguments(code, nameEnd) : nameEnd;
|
|
369
|
+
while (cursor !== FAILED && cursor < code.length) {
|
|
370
|
+
const char = code.charAt(cursor);
|
|
371
|
+
if (char === ">") return {
|
|
372
|
+
end: cursor + 1,
|
|
373
|
+
name,
|
|
374
|
+
selfClosing: false
|
|
375
|
+
};
|
|
376
|
+
if (char === "/" && code.charAt(cursor + 1) === ">") return {
|
|
377
|
+
end: cursor + 2,
|
|
378
|
+
name,
|
|
379
|
+
selfClosing: true
|
|
380
|
+
};
|
|
381
|
+
cursor = skipAttributeToken(code, cursor);
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
function readClosingTagEnd(code, index, name) {
|
|
386
|
+
let cursor = index + 2;
|
|
387
|
+
while (WHITESPACE.test(code.charAt(cursor))) cursor += 1;
|
|
388
|
+
const nameEnd = readTagName(code, cursor);
|
|
389
|
+
if (code.slice(cursor, nameEnd) !== name) return FAILED;
|
|
390
|
+
cursor = nameEnd;
|
|
391
|
+
while (WHITESPACE.test(code.charAt(cursor))) cursor += 1;
|
|
392
|
+
return code.charAt(cursor) === ">" ? cursor + 1 : FAILED;
|
|
393
|
+
}
|
|
394
|
+
function skipChildren(code, index, name) {
|
|
395
|
+
let cursor = index;
|
|
396
|
+
while (cursor !== FAILED && cursor < code.length) {
|
|
397
|
+
const char = code.charAt(cursor);
|
|
398
|
+
if (char === "<" && code.charAt(cursor + 1) === "/") return readClosingTagEnd(code, cursor, name);
|
|
399
|
+
if (char === "{") cursor = skipJsExpression(code, cursor);
|
|
400
|
+
else if (isTagStart(code, cursor)) cursor = skipElement(code, cursor);
|
|
401
|
+
else cursor += 1;
|
|
402
|
+
}
|
|
403
|
+
return FAILED;
|
|
404
|
+
}
|
|
405
|
+
function skipElement(code, index) {
|
|
406
|
+
const tag = readOpeningTag(code, index);
|
|
407
|
+
if (!tag) return FAILED;
|
|
408
|
+
return tag.selfClosing ? tag.end : skipChildren(code, tag.end, tag.name);
|
|
409
|
+
}
|
|
410
|
+
function lineOffset(code, line) {
|
|
411
|
+
if (!Number.isInteger(line) || line < 1) return FAILED;
|
|
412
|
+
let offset = 0;
|
|
413
|
+
for (let current = 1; current < line; current += 1) {
|
|
414
|
+
const newline = code.indexOf("\n", offset);
|
|
415
|
+
if (newline === -1) return FAILED;
|
|
416
|
+
offset = newline + 1;
|
|
417
|
+
}
|
|
418
|
+
return offset;
|
|
419
|
+
}
|
|
420
|
+
function findTagStart(code, lineStart, lineEnd, column) {
|
|
421
|
+
const origin = Math.min(lineStart + Math.max(column, 0), lineEnd);
|
|
422
|
+
for (let back = 0; back <= MAX_TAG_LOOKBACK; back += 1) {
|
|
423
|
+
const candidate = origin - back;
|
|
424
|
+
if (candidate >= lineStart && isTagStart(code, candidate)) return candidate;
|
|
425
|
+
}
|
|
426
|
+
for (let candidate = origin + 1; candidate < lineEnd; candidate += 1) if (isTagStart(code, candidate)) return candidate;
|
|
427
|
+
return FAILED;
|
|
428
|
+
}
|
|
429
|
+
function countNewlines(code, from, to) {
|
|
430
|
+
let count = 0;
|
|
431
|
+
let newline = code.indexOf("\n", from);
|
|
432
|
+
while (newline !== -1 && newline < to) {
|
|
433
|
+
count += 1;
|
|
434
|
+
newline = code.indexOf("\n", newline + 1);
|
|
435
|
+
}
|
|
436
|
+
return count;
|
|
437
|
+
}
|
|
438
|
+
/** Lines spanned by the JSX element at a 1-based line and 0-based column. */
|
|
439
|
+
function jsxElementLines(code, line, column) {
|
|
440
|
+
const lineStart = lineOffset(code, line);
|
|
441
|
+
if (lineStart === FAILED) return null;
|
|
442
|
+
const newline = code.indexOf("\n", lineStart);
|
|
443
|
+
const tagStart = findTagStart(code, lineStart, newline === -1 ? code.length : newline, column);
|
|
444
|
+
if (tagStart === FAILED) return null;
|
|
445
|
+
const elementEnd = skipElement(code, tagStart);
|
|
446
|
+
if (elementEnd === FAILED) return null;
|
|
447
|
+
return {
|
|
448
|
+
end: line + countNewlines(code, tagStart, elementEnd - 1),
|
|
449
|
+
start: line
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
//#endregion
|
|
453
|
+
//#region src/devtools/server/source/server-frame.ts
|
|
454
|
+
const SERVER_FRAME_PREFIX = /^(?:about|rsc):\/\/React\/Server\//;
|
|
455
|
+
const QUERY_OR_HASH$1 = /[?#].*$/;
|
|
456
|
+
const SOURCE_MAPPING_URL = /\/\/# sourceMappingURL=(\S+)\s*$/;
|
|
457
|
+
const MAX_MAP_BYTES = 52428800;
|
|
458
|
+
const mapCache = /* @__PURE__ */ new Map();
|
|
459
|
+
function isServerFrame(fileName) {
|
|
460
|
+
return SERVER_FRAME_PREFIX.test(fileName);
|
|
461
|
+
}
|
|
462
|
+
function isInside(path, root) {
|
|
463
|
+
const fromRoot = relative(root, path);
|
|
464
|
+
return fromRoot !== "" && !fromRoot.startsWith("..") && !fromRoot.includes(`..${sep}`);
|
|
465
|
+
}
|
|
466
|
+
async function mapPathFor(chunkPath) {
|
|
467
|
+
const code = await readFile(chunkPath, "utf8");
|
|
468
|
+
const declared = SOURCE_MAPPING_URL.exec(code)?.[1];
|
|
469
|
+
return declared ? join(dirname(chunkPath), decodeURIComponent(declared)) : `${chunkPath}.map`;
|
|
470
|
+
}
|
|
471
|
+
async function loadMap(mapPath) {
|
|
472
|
+
const { mtimeMs, size } = await stat(mapPath);
|
|
473
|
+
if (size > MAX_MAP_BYTES) throw new Error("source map too large");
|
|
474
|
+
const cached = mapCache.get(mapPath);
|
|
475
|
+
if (cached?.mtimeMs === mtimeMs) return cached.map;
|
|
476
|
+
const map = new SourceMap(JSON.parse(await readFile(mapPath, "utf8")));
|
|
477
|
+
mapCache.set(mapPath, {
|
|
478
|
+
map,
|
|
479
|
+
mtimeMs
|
|
480
|
+
});
|
|
481
|
+
return map;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Server Components stamp their elements with frames into the dev server's
|
|
485
|
+
* own chunks (`about://React/Server/file:///…/.next/…`); the browser cannot
|
|
486
|
+
* fetch those maps, so they are resolved here from disk.
|
|
487
|
+
*/
|
|
488
|
+
async function mapServerFrame(request, workspaceRoot) {
|
|
489
|
+
const unreadable = failure("This element comes from a Server Component whose source map could not be read.", 404);
|
|
490
|
+
if (request.line === null) return unreadable;
|
|
491
|
+
try {
|
|
492
|
+
const chunkUrl = request.fileName.replace(SERVER_FRAME_PREFIX, "").replace(QUERY_OR_HASH$1, "");
|
|
493
|
+
const chunkPath = await realpath(fileURLToPath(chunkUrl));
|
|
494
|
+
if (!isInside(chunkPath, workspaceRoot)) return failure("Reflet devtools only reads files inside your repository.", 403);
|
|
495
|
+
const origin = (await loadMap(await mapPathFor(chunkPath))).findOrigin(request.line, (request.column ?? 0) + 1);
|
|
496
|
+
if (!("fileName" in origin)) return unreadable;
|
|
497
|
+
return {
|
|
498
|
+
ok: true,
|
|
499
|
+
value: {
|
|
500
|
+
column: origin.columnNumber - 1,
|
|
501
|
+
fileName: origin.fileName,
|
|
502
|
+
line: origin.lineNumber
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
} catch {
|
|
506
|
+
return unreadable;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
//#endregion
|
|
510
|
+
//#region src/devtools/server/source/source-file.ts
|
|
511
|
+
const MAX_SOURCE_BYTES = 1048576;
|
|
512
|
+
const MAX_FILE_NAME_LENGTH = 2048;
|
|
513
|
+
const MAX_NORMALIZE_PASSES = 8;
|
|
514
|
+
const SOURCE_EXTENSIONS = [
|
|
515
|
+
".astro",
|
|
516
|
+
".cjs",
|
|
517
|
+
".js",
|
|
518
|
+
".jsx",
|
|
519
|
+
".mdx",
|
|
520
|
+
".mjs",
|
|
521
|
+
".svelte",
|
|
522
|
+
".ts",
|
|
523
|
+
".tsx",
|
|
524
|
+
".vue"
|
|
525
|
+
];
|
|
526
|
+
const EXTENSION_REFUSAL = `Reflet devtools only reads source files (${SOURCE_EXTENSIONS.join(", ")}).`;
|
|
527
|
+
const OUTSIDE_WORKSPACE_REFUSAL = "Reflet devtools only reads files inside your repository.";
|
|
528
|
+
const NODE_MODULES_REFUSAL = "Reflet devtools does not read files inside node_modules.";
|
|
529
|
+
const QUERY_OR_HASH = /[?#].*$/;
|
|
530
|
+
const TURBOPACK_MODULE_SUFFIX = /(?: \[[\w-]+\])+(?: \([\w-]+\))*$/;
|
|
531
|
+
const URL_PREFIXES = [/^file:\/\//, /^https?:\/\/[^/]*/];
|
|
532
|
+
const BUNDLER_PREFIXES = [
|
|
533
|
+
/^rsc:\/\/React\/[^/]+\//,
|
|
534
|
+
/^webpack-internal:\/{2,}/,
|
|
535
|
+
/^webpack:\/\/[^/]*\//,
|
|
536
|
+
/^turbopack:\/{2,}/,
|
|
537
|
+
/^\[project\]\//,
|
|
538
|
+
/^\(app-pages-browser\)\//,
|
|
539
|
+
/^\([\w-]+\)\/(?=\.\/)/,
|
|
540
|
+
/^\/@fs(?=\/)/,
|
|
541
|
+
/^\.\//
|
|
542
|
+
];
|
|
543
|
+
function decodePath(path) {
|
|
544
|
+
try {
|
|
545
|
+
return decodeURIComponent(path);
|
|
546
|
+
} catch {
|
|
547
|
+
return path;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function stripFirstPrefix(fileName, prefixes) {
|
|
551
|
+
for (const prefix of prefixes) if (prefix.test(fileName)) return fileName.replace(prefix, "");
|
|
552
|
+
return fileName;
|
|
553
|
+
}
|
|
554
|
+
/** Turns a file name from a React debug stack into a filesystem path. */
|
|
555
|
+
function normalizeSourceFileName(rawFileName) {
|
|
556
|
+
let fileName = rawFileName.trim().replace(QUERY_OR_HASH, "").replace(TURBOPACK_MODULE_SUFFIX, "");
|
|
557
|
+
let isUrlEncoded = false;
|
|
558
|
+
for (let pass = 0; pass < MAX_NORMALIZE_PASSES; pass += 1) {
|
|
559
|
+
const withoutUrl = stripFirstPrefix(fileName, URL_PREFIXES);
|
|
560
|
+
isUrlEncoded ||= withoutUrl !== fileName;
|
|
561
|
+
const stripped = stripFirstPrefix(withoutUrl, BUNDLER_PREFIXES);
|
|
562
|
+
if (stripped === fileName) break;
|
|
563
|
+
fileName = stripped;
|
|
564
|
+
}
|
|
565
|
+
return isUrlEncoded ? decodePath(fileName) : fileName;
|
|
566
|
+
}
|
|
567
|
+
function candidatePaths(fileName, roots) {
|
|
568
|
+
const candidates = [join(roots.root, fileName), join(roots.workspaceRoot, fileName)];
|
|
569
|
+
if (isAbsolute(fileName)) candidates.unshift(fileName);
|
|
570
|
+
return [...new Set(candidates)];
|
|
571
|
+
}
|
|
572
|
+
function isSourceExtension(path) {
|
|
573
|
+
return SOURCE_EXTENSIONS.includes(extname(path).toLowerCase());
|
|
574
|
+
}
|
|
575
|
+
function workspaceRelativePath(realPath, workspaceRoot) {
|
|
576
|
+
const relativePath = relative(workspaceRoot, realPath);
|
|
577
|
+
return relativePath === "" || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath) ? null : relativePath.split(sep).join("/");
|
|
578
|
+
}
|
|
579
|
+
async function checkCandidate(candidate, workspaceRoot) {
|
|
580
|
+
let realPath;
|
|
581
|
+
let size;
|
|
582
|
+
try {
|
|
583
|
+
realPath = await realpath(candidate);
|
|
584
|
+
const stats = await stat(realPath);
|
|
585
|
+
if (!stats.isFile()) return { kind: "missing" };
|
|
586
|
+
size = stats.size;
|
|
587
|
+
} catch {
|
|
588
|
+
return { kind: "missing" };
|
|
589
|
+
}
|
|
590
|
+
const relativePath = workspaceRelativePath(realPath, workspaceRoot);
|
|
591
|
+
if (relativePath === null) return {
|
|
592
|
+
kind: "refused",
|
|
593
|
+
reason: OUTSIDE_WORKSPACE_REFUSAL
|
|
594
|
+
};
|
|
595
|
+
if (relativePath.split("/").includes("node_modules")) return {
|
|
596
|
+
kind: "refused",
|
|
597
|
+
reason: NODE_MODULES_REFUSAL
|
|
598
|
+
};
|
|
599
|
+
if (!isSourceExtension(realPath)) return {
|
|
600
|
+
kind: "refused",
|
|
601
|
+
reason: EXTENSION_REFUSAL
|
|
602
|
+
};
|
|
603
|
+
return {
|
|
604
|
+
kind: "found",
|
|
605
|
+
realPath,
|
|
606
|
+
relativePath,
|
|
607
|
+
size
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
async function locateSourceFile(fileName, roots) {
|
|
611
|
+
let refusal = null;
|
|
612
|
+
for (const candidate of candidatePaths(fileName, roots)) {
|
|
613
|
+
const check = await checkCandidate(candidate, roots.workspaceRoot);
|
|
614
|
+
if (check.kind === "found") return {
|
|
615
|
+
ok: true,
|
|
616
|
+
value: check
|
|
617
|
+
};
|
|
618
|
+
if (check.kind === "refused") refusal ??= check.reason;
|
|
619
|
+
}
|
|
620
|
+
if (refusal) return failure(refusal, 403);
|
|
621
|
+
return failure(`Could not find ${fileName} in ${roots.workspaceRoot}.`, 404);
|
|
622
|
+
}
|
|
623
|
+
/** Server-map columns can point past the line; the element then starts at its first tag. */
|
|
624
|
+
function columnWithinLine(code, line, column) {
|
|
625
|
+
const text = code.split("\n")[line - 1] ?? "";
|
|
626
|
+
return column !== null && column < text.length ? column : null;
|
|
627
|
+
}
|
|
628
|
+
async function readSourceFile(frame, roots) {
|
|
629
|
+
if (frame.fileName.length > MAX_FILE_NAME_LENGTH || frame.fileName.includes("\0")) return failure("That source file name is not a valid path.", 400);
|
|
630
|
+
const mapped = isServerFrame(frame.fileName) ? await mapServerFrame(frame, roots.workspaceRoot) : {
|
|
631
|
+
ok: true,
|
|
632
|
+
value: frame
|
|
633
|
+
};
|
|
634
|
+
if (!mapped.ok) return mapped;
|
|
635
|
+
const request = mapped.value;
|
|
636
|
+
const fileName = normalizeSourceFileName(request.fileName);
|
|
637
|
+
if (!fileName) return failure("A source file name is required.", 400);
|
|
638
|
+
if (!isSourceExtension(fileName)) return failure(EXTENSION_REFUSAL, 403);
|
|
639
|
+
const located = await locateSourceFile(fileName, roots);
|
|
640
|
+
if (!located.ok) return located;
|
|
641
|
+
const { realPath, relativePath, size } = located.value;
|
|
642
|
+
if (size > MAX_SOURCE_BYTES) return failure(`${relativePath} is larger than 1 MB, too big to preview.`, 413);
|
|
643
|
+
const code = await readFile(realPath, "utf8");
|
|
644
|
+
const column = request.line === null ? request.column : columnWithinLine(code, request.line, request.column);
|
|
645
|
+
return {
|
|
646
|
+
ok: true,
|
|
647
|
+
value: {
|
|
648
|
+
absolutePath: realPath,
|
|
649
|
+
code,
|
|
650
|
+
column,
|
|
651
|
+
elementLines: request.line === null ? null : jsxElementLines(code, request.line, column ?? 0),
|
|
652
|
+
line: request.line,
|
|
653
|
+
path: relativePath
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
//#region src/devtools/server/handler.ts
|
|
659
|
+
const TRAILING_SLASHES = /\/+$/;
|
|
660
|
+
function outcomeResponse(outcome) {
|
|
661
|
+
return outcome.ok ? jsonResponse(outcome.value) : errorResponse(outcome.error, outcome.status);
|
|
662
|
+
}
|
|
663
|
+
function routeSubPath(pathname) {
|
|
664
|
+
const baseIndex = pathname.indexOf(DEVTOOLS_ROUTE_BASE);
|
|
665
|
+
if (baseIndex === -1) return null;
|
|
666
|
+
return pathname.slice(baseIndex + DEVTOOLS_ROUTE_BASE.length).replace(TRAILING_SLASHES, "");
|
|
667
|
+
}
|
|
668
|
+
function readInteger(value, minimum) {
|
|
669
|
+
if (value === null || value === "") return null;
|
|
670
|
+
const parsed = Number(value);
|
|
671
|
+
return Number.isInteger(parsed) && parsed >= minimum ? parsed : null;
|
|
672
|
+
}
|
|
673
|
+
async function routeRequest(request, settings, projectRoots) {
|
|
674
|
+
const url = new URL(request.url);
|
|
675
|
+
const subPath = routeSubPath(url.pathname);
|
|
676
|
+
if (subPath?.startsWith(`${DEVTOOLS_ENDPOINTS.proxy}/`)) return await proxyToReflet(request, subPath.slice(DEVTOOLS_ENDPOINTS.proxy.length), settings);
|
|
677
|
+
if ((subPath === DEVTOOLS_ENDPOINTS.status || subPath === DEVTOOLS_ENDPOINTS.source || subPath === DEVTOOLS_ENDPOINTS.search) && request.method !== "GET") return errorResponse(`${subPath} only answers GET requests.`, 405);
|
|
678
|
+
switch (subPath) {
|
|
679
|
+
case DEVTOOLS_ENDPOINTS.status: return jsonResponse({
|
|
680
|
+
editor: settings.editor,
|
|
681
|
+
hasSecretKey: settings.secretKey !== null,
|
|
682
|
+
marker: DEVTOOLS_STATUS_MARKER
|
|
683
|
+
});
|
|
684
|
+
case DEVTOOLS_ENDPOINTS.source: return outcomeResponse(await readSourceFile({
|
|
685
|
+
column: readInteger(url.searchParams.get("column"), 0),
|
|
686
|
+
fileName: url.searchParams.get("file") ?? "",
|
|
687
|
+
line: readInteger(url.searchParams.get("line"), 1)
|
|
688
|
+
}, projectRoots()));
|
|
689
|
+
case DEVTOOLS_ENDPOINTS.search: return outcomeResponse(await searchCode(projectRoots().workspaceRoot, url.searchParams.get("q") ?? ""));
|
|
690
|
+
default: return errorResponse(`No Reflet devtools endpoint at ${url.pathname}.`, 404);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
function createDevtoolsHandler(options = {}, env = process.env) {
|
|
694
|
+
const settings = resolveDevtoolsOptions(options, env);
|
|
695
|
+
let roots = null;
|
|
696
|
+
const projectRoots = () => {
|
|
697
|
+
roots ??= resolveProjectRoots(settings.root);
|
|
698
|
+
return roots;
|
|
699
|
+
};
|
|
700
|
+
return async (request) => {
|
|
701
|
+
const rejection = rejectUntrustedRequest(request, settings.allowedHosts);
|
|
702
|
+
if (rejection) return rejection;
|
|
703
|
+
try {
|
|
704
|
+
return await routeRequest(request, settings, projectRoots);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
return errorResponse(`Reflet devtools route failed: ${error instanceof Error ? error.message : String(error)}`, 500);
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
//#endregion
|
|
711
|
+
export { errorResponse as n, createDevtoolsHandler as t };
|
|
712
|
+
|
|
713
|
+
//# sourceMappingURL=handler-CHP0m7KF.js.map
|