@ekkolyth/devtools 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/dist/index.d.ts +4 -0
- package/dist/index.js +879 -0
- package/dist/loader/index.d.ts +12 -0
- package/dist/loader/index.js +133 -0
- package/package.json +33 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
import { Button, Input, Label, RadioGroup, RadioGroupItem } from "@ekkolyth/ui";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { useThemeContext } from "@ekkolyth/ui/themes/provider";
|
|
5
|
+
//#region src/inspector/pick-lock.ts
|
|
6
|
+
let pickerArmed = false;
|
|
7
|
+
function isPickerArmed() {
|
|
8
|
+
return pickerArmed;
|
|
9
|
+
}
|
|
10
|
+
function setPickerArmed(value) {
|
|
11
|
+
pickerArmed = value;
|
|
12
|
+
}
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/inspector/source-attr.ts
|
|
15
|
+
const SOURCE_ATTRIBUTE = "data-ekko-source";
|
|
16
|
+
function parseSourceAttribute(value) {
|
|
17
|
+
const lastColon = value.lastIndexOf(":");
|
|
18
|
+
if (lastColon === -1) return null;
|
|
19
|
+
const secondColon = value.lastIndexOf(":", lastColon - 1);
|
|
20
|
+
if (secondColon === -1) return null;
|
|
21
|
+
const columnPart = value.slice(lastColon + 1);
|
|
22
|
+
const linePart = value.slice(secondColon + 1, lastColon);
|
|
23
|
+
if (columnPart === "" || linePart === "") return null;
|
|
24
|
+
const column = Number(columnPart);
|
|
25
|
+
const line = Number(linePart);
|
|
26
|
+
const file = value.slice(0, secondColon);
|
|
27
|
+
if (!Number.isFinite(column) || !Number.isFinite(line)) return null;
|
|
28
|
+
if (file === "") return null;
|
|
29
|
+
return {
|
|
30
|
+
column,
|
|
31
|
+
file,
|
|
32
|
+
line
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function findSourceElement(start) {
|
|
36
|
+
const value = (start?.closest(`[data-ekko-source]`) ?? null)?.getAttribute(SOURCE_ATTRIBUTE);
|
|
37
|
+
return value === null || value === void 0 ? null : parseSourceAttribute(value);
|
|
38
|
+
}
|
|
39
|
+
function launchEditorUrl(location) {
|
|
40
|
+
return `/__nextjs_launch-editor?file=${encodeURIComponent(location.file)}&line1=${location.line}&column1=${location.column}`;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/inspector/overlay.tsx
|
|
44
|
+
function isChord(event) {
|
|
45
|
+
return event.metaKey && event.shiftKey;
|
|
46
|
+
}
|
|
47
|
+
function InspectorOverlay() {
|
|
48
|
+
const [armed, setArmed] = useState(false);
|
|
49
|
+
const [hover, setHover] = useState(null);
|
|
50
|
+
const [failed, setFailed] = useState(false);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
const disarm = () => {
|
|
53
|
+
setArmed(false);
|
|
54
|
+
setHover(null);
|
|
55
|
+
setFailed(false);
|
|
56
|
+
};
|
|
57
|
+
const onKeyDown = (event) => {
|
|
58
|
+
if (isChord(event)) setArmed(true);
|
|
59
|
+
if (event.key === "Escape") disarm();
|
|
60
|
+
};
|
|
61
|
+
const onKeyUp = (event) => {
|
|
62
|
+
if (!isChord(event)) disarm();
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("keydown", onKeyDown);
|
|
65
|
+
window.addEventListener("keyup", onKeyUp);
|
|
66
|
+
window.addEventListener("blur", disarm);
|
|
67
|
+
return () => {
|
|
68
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
69
|
+
window.removeEventListener("keyup", onKeyUp);
|
|
70
|
+
window.removeEventListener("blur", disarm);
|
|
71
|
+
};
|
|
72
|
+
}, []);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!armed) return;
|
|
75
|
+
const openInEditor = (location) => {
|
|
76
|
+
Promise.resolve(fetch(launchEditorUrl(location))).then((response) => setFailed(!response.ok)).catch(() => setFailed(true));
|
|
77
|
+
};
|
|
78
|
+
const onMouseMove = (event) => {
|
|
79
|
+
if (!isChord(event)) {
|
|
80
|
+
setArmed(false);
|
|
81
|
+
setHover(null);
|
|
82
|
+
setFailed(false);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const target = event.target;
|
|
86
|
+
if (!(target instanceof Element)) return;
|
|
87
|
+
const box = target.getBoundingClientRect();
|
|
88
|
+
setHover({
|
|
89
|
+
location: findSourceElement(target),
|
|
90
|
+
rect: {
|
|
91
|
+
height: box.height,
|
|
92
|
+
left: box.left,
|
|
93
|
+
top: box.top,
|
|
94
|
+
width: box.width
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
setFailed(false);
|
|
98
|
+
};
|
|
99
|
+
const onClick = (event) => {
|
|
100
|
+
if (isPickerArmed()) return;
|
|
101
|
+
if (!isChord(event)) {
|
|
102
|
+
setArmed(false);
|
|
103
|
+
setHover(null);
|
|
104
|
+
setFailed(false);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const target = event.target;
|
|
108
|
+
if (!(target instanceof Element)) return;
|
|
109
|
+
const location = findSourceElement(target);
|
|
110
|
+
if (location === null) return;
|
|
111
|
+
event.preventDefault();
|
|
112
|
+
event.stopPropagation();
|
|
113
|
+
openInEditor(location);
|
|
114
|
+
};
|
|
115
|
+
document.addEventListener("mousemove", onMouseMove, true);
|
|
116
|
+
document.addEventListener("click", onClick, true);
|
|
117
|
+
return () => {
|
|
118
|
+
document.removeEventListener("mousemove", onMouseMove, true);
|
|
119
|
+
document.removeEventListener("click", onClick, true);
|
|
120
|
+
};
|
|
121
|
+
}, [armed]);
|
|
122
|
+
if (!armed || hover === null) return null;
|
|
123
|
+
const label = hover.location === null ? "no source" : failed ? `could not open ${hover.location.file}` : `${hover.location.file}:${hover.location.line}`;
|
|
124
|
+
return /* @__PURE__ */ jsx("div", {
|
|
125
|
+
className: "pointer-events-none fixed z-[10000] border-2 border-primary bg-primary/10",
|
|
126
|
+
"data-testid": "ekko-inspector",
|
|
127
|
+
style: {
|
|
128
|
+
height: hover.rect.height,
|
|
129
|
+
left: hover.rect.left,
|
|
130
|
+
top: hover.rect.top,
|
|
131
|
+
width: hover.rect.width
|
|
132
|
+
},
|
|
133
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
134
|
+
className: "absolute top-0 left-0 -translate-y-full bg-primary px-1 text-primary-foreground text-xs",
|
|
135
|
+
children: label
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/probes/result.ts
|
|
141
|
+
function ok(data) {
|
|
142
|
+
return {
|
|
143
|
+
data,
|
|
144
|
+
status: "ok"
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function probeError(message) {
|
|
148
|
+
return {
|
|
149
|
+
message,
|
|
150
|
+
status: "error"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function unavailable(reason) {
|
|
154
|
+
return {
|
|
155
|
+
reason,
|
|
156
|
+
status: "unavailable"
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function message(error) {
|
|
160
|
+
return error instanceof Error ? error.message : String(error);
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/probes/network.ts
|
|
164
|
+
function isCssResource(entry) {
|
|
165
|
+
if (entry.initiatorType === "css") return true;
|
|
166
|
+
return (entry.name.split("?")[0] ?? entry.name).endsWith(".css");
|
|
167
|
+
}
|
|
168
|
+
function readCssTimings() {
|
|
169
|
+
if (typeof performance?.getEntriesByType !== "function") return unavailable("PerformanceResourceTiming unsupported");
|
|
170
|
+
try {
|
|
171
|
+
return ok(performance.getEntriesByType("resource").filter(isCssResource).map((entry) => ({
|
|
172
|
+
fromCache: entry.transferSize === 0,
|
|
173
|
+
name: entry.name,
|
|
174
|
+
transferSize: entry.transferSize
|
|
175
|
+
})));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return probeError(message(error));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function readCacheStatus(url) {
|
|
181
|
+
try {
|
|
182
|
+
const response = await fetch(url, {
|
|
183
|
+
cache: "no-store",
|
|
184
|
+
method: "HEAD"
|
|
185
|
+
});
|
|
186
|
+
return ok({
|
|
187
|
+
cfCacheStatus: response.headers.get("cf-cache-status"),
|
|
188
|
+
status: response.status
|
|
189
|
+
});
|
|
190
|
+
} catch (error) {
|
|
191
|
+
return probeError(message(error));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function readServiceWorkers() {
|
|
195
|
+
if (typeof navigator === "undefined" || !navigator.serviceWorker) return unavailable("serviceWorker unsupported");
|
|
196
|
+
try {
|
|
197
|
+
return ok((await navigator.serviceWorker.getRegistrations()).map((registration) => ({
|
|
198
|
+
active: registration.active !== null,
|
|
199
|
+
scope: registration.scope
|
|
200
|
+
})));
|
|
201
|
+
} catch (error) {
|
|
202
|
+
return probeError(message(error));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function readCacheKeys() {
|
|
206
|
+
if (typeof caches === "undefined") return unavailable("CacheStorage unsupported");
|
|
207
|
+
try {
|
|
208
|
+
return ok(await caches.keys());
|
|
209
|
+
} catch (error) {
|
|
210
|
+
return probeError(message(error));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//#endregion
|
|
214
|
+
//#region src/probes/stylesheets.ts
|
|
215
|
+
function inspectSheet(sheet) {
|
|
216
|
+
try {
|
|
217
|
+
return {
|
|
218
|
+
href: sheet.href,
|
|
219
|
+
readable: true,
|
|
220
|
+
ruleCount: sheet.cssRules.length
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return {
|
|
224
|
+
href: sheet.href,
|
|
225
|
+
readable: false,
|
|
226
|
+
ruleCount: null
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function readStylesheets() {
|
|
231
|
+
try {
|
|
232
|
+
return ok(Array.from(document.styleSheets).map(inspectSheet));
|
|
233
|
+
} catch (error) {
|
|
234
|
+
return probeError(message(error));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
//#endregion
|
|
238
|
+
//#region src/panels/message.tsx
|
|
239
|
+
function Message({ result }) {
|
|
240
|
+
if (result.status === "ok") return null;
|
|
241
|
+
return /* @__PURE__ */ jsx("p", {
|
|
242
|
+
className: result.status === "error" ? "text-destructive" : "text-muted-foreground",
|
|
243
|
+
children: result.status === "error" ? result.message : result.reason
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function Row({ label, value }) {
|
|
247
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
248
|
+
className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
|
|
249
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
250
|
+
className: "text-muted-foreground",
|
|
251
|
+
children: label
|
|
252
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
253
|
+
className: "font-mono",
|
|
254
|
+
children: value
|
|
255
|
+
})]
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/panels/cache/panel.tsx
|
|
260
|
+
function withOccurrenceKeys(list, baseKey) {
|
|
261
|
+
const seen = /* @__PURE__ */ new Map();
|
|
262
|
+
return list.map((item) => {
|
|
263
|
+
const base = baseKey(item);
|
|
264
|
+
const count = seen.get(base) ?? 0;
|
|
265
|
+
seen.set(base, count + 1);
|
|
266
|
+
return {
|
|
267
|
+
...item,
|
|
268
|
+
key: count === 0 ? base : `${base}#${count}`
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
function keyedStylesheets(list) {
|
|
273
|
+
return withOccurrenceKeys(list, (sheet) => sheet.href ?? "inline");
|
|
274
|
+
}
|
|
275
|
+
function CachePanel() {
|
|
276
|
+
const [cacheKeys, setCacheKeys] = useState(null);
|
|
277
|
+
const [workers, setWorkers] = useState(null);
|
|
278
|
+
const [pageStatus, setPageStatus] = useState(null);
|
|
279
|
+
const [assetStatuses, setAssetStatuses] = useState([]);
|
|
280
|
+
const mountedRef = useRef(true);
|
|
281
|
+
const sheets = readStylesheets();
|
|
282
|
+
const timings = readCssTimings();
|
|
283
|
+
const probe = useCallback(async () => {
|
|
284
|
+
const currentSheets = readStylesheets();
|
|
285
|
+
const hrefs = currentSheets.status === "ok" ? currentSheets.data.map((sheet) => sheet.href).filter((href) => href !== null) : [];
|
|
286
|
+
const [keys, registrations, status, assets] = await Promise.all([
|
|
287
|
+
readCacheKeys(),
|
|
288
|
+
readServiceWorkers(),
|
|
289
|
+
readCacheStatus(window.location.href),
|
|
290
|
+
Promise.all(hrefs.map((href) => readCacheStatus(href)))
|
|
291
|
+
]);
|
|
292
|
+
if (!mountedRef.current) return;
|
|
293
|
+
setCacheKeys(keys);
|
|
294
|
+
setWorkers(registrations);
|
|
295
|
+
setPageStatus(status);
|
|
296
|
+
setAssetStatuses(hrefs.map((href, index) => ({
|
|
297
|
+
href,
|
|
298
|
+
result: assets[index]
|
|
299
|
+
})));
|
|
300
|
+
}, []);
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
mountedRef.current = true;
|
|
303
|
+
probe();
|
|
304
|
+
return () => {
|
|
305
|
+
mountedRef.current = false;
|
|
306
|
+
};
|
|
307
|
+
}, [probe]);
|
|
308
|
+
const reprobe = useCallback(() => {
|
|
309
|
+
probe();
|
|
310
|
+
}, [probe]);
|
|
311
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
312
|
+
className: "flex flex-col gap-4",
|
|
313
|
+
children: [
|
|
314
|
+
/* @__PURE__ */ jsx(Button, {
|
|
315
|
+
className: "self-start",
|
|
316
|
+
onClick: reprobe,
|
|
317
|
+
size: "sm",
|
|
318
|
+
type: "button",
|
|
319
|
+
variant: "outline",
|
|
320
|
+
children: "Re-probe"
|
|
321
|
+
}),
|
|
322
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
323
|
+
className: "pb-1 font-medium",
|
|
324
|
+
children: "CSS transfer"
|
|
325
|
+
}), timings.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: withOccurrenceKeys(timings.data, (timing) => timing.name).map((timing) => /* @__PURE__ */ jsxs("li", {
|
|
326
|
+
className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
|
|
327
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
328
|
+
className: "truncate font-mono",
|
|
329
|
+
children: timing.name
|
|
330
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
331
|
+
className: "shrink-0 text-muted-foreground",
|
|
332
|
+
children: timing.fromCache ? "from cache" : `${timing.transferSize} B`
|
|
333
|
+
})]
|
|
334
|
+
}, timing.key)) }) : /* @__PURE__ */ jsx(Message, { result: timings })] }),
|
|
335
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
336
|
+
className: "pb-1 font-medium",
|
|
337
|
+
children: "Stylesheets"
|
|
338
|
+
}), sheets.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: keyedStylesheets(sheets.data).map((sheet) => /* @__PURE__ */ jsxs("li", {
|
|
339
|
+
className: "flex justify-between gap-4 border-t py-1 first:border-t-0",
|
|
340
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
341
|
+
className: "truncate font-mono",
|
|
342
|
+
children: sheet.href ?? "inline"
|
|
343
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
344
|
+
className: "shrink-0 text-muted-foreground",
|
|
345
|
+
children: sheet.readable ? `${sheet.ruleCount} rules` : "unreadable"
|
|
346
|
+
})]
|
|
347
|
+
}, sheet.key)) }) : /* @__PURE__ */ jsx(Message, { result: sheets })] }),
|
|
348
|
+
/* @__PURE__ */ jsxs("section", { children: [
|
|
349
|
+
/* @__PURE__ */ jsx("h3", {
|
|
350
|
+
className: "pb-1 font-medium",
|
|
351
|
+
children: "Edge (cf-cache-status)"
|
|
352
|
+
}),
|
|
353
|
+
pageStatus === null ? null : pageStatus.status === "ok" ? /* @__PURE__ */ jsx(Row, {
|
|
354
|
+
label: "page",
|
|
355
|
+
value: pageStatus.data.cfCacheStatus ?? "—"
|
|
356
|
+
}) : /* @__PURE__ */ jsx(Message, { result: pageStatus }),
|
|
357
|
+
assetStatuses.map(({ href, result }) => result.status === "ok" ? /* @__PURE__ */ jsx(Row, {
|
|
358
|
+
label: href,
|
|
359
|
+
value: result.data.cfCacheStatus ?? "—"
|
|
360
|
+
}, href) : /* @__PURE__ */ jsxs("div", {
|
|
361
|
+
className: "flex flex-col gap-1 border-t py-1 first:border-t-0",
|
|
362
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
363
|
+
className: "truncate font-mono text-muted-foreground",
|
|
364
|
+
children: href
|
|
365
|
+
}), /* @__PURE__ */ jsx(Message, { result })]
|
|
366
|
+
}, href))
|
|
367
|
+
] }),
|
|
368
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
369
|
+
className: "pb-1 font-medium",
|
|
370
|
+
children: "Service workers"
|
|
371
|
+
}), workers === null ? null : workers.status === "ok" ? workers.data.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
372
|
+
className: "text-muted-foreground",
|
|
373
|
+
children: "none registered"
|
|
374
|
+
}) : /* @__PURE__ */ jsx("ul", { children: workers.data.map((worker) => /* @__PURE__ */ jsxs("li", {
|
|
375
|
+
className: "font-mono",
|
|
376
|
+
children: [worker.scope, worker.active ? " (active)" : ""]
|
|
377
|
+
}, worker.scope)) }) : /* @__PURE__ */ jsx(Message, { result: workers })] }),
|
|
378
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
379
|
+
className: "pb-1 font-medium",
|
|
380
|
+
children: "Cache storage"
|
|
381
|
+
}), cacheKeys === null ? null : cacheKeys.status === "ok" ? /* @__PURE__ */ jsx("ul", { children: cacheKeys.data.map((key) => /* @__PURE__ */ jsx("li", {
|
|
382
|
+
className: "font-mono",
|
|
383
|
+
children: key
|
|
384
|
+
}, key)) }) : /* @__PURE__ */ jsx(Message, { result: cacheKeys })] })
|
|
385
|
+
]
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/shell/corner-context.tsx
|
|
390
|
+
const CornerContext = createContext(null);
|
|
391
|
+
function useCorner() {
|
|
392
|
+
const value = useContext(CornerContext);
|
|
393
|
+
if (value === null) throw new Error("useCorner must be used within CornerContext.Provider");
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/shell/storage.ts
|
|
398
|
+
const STORAGE_PREFIX = "ekko-devtools";
|
|
399
|
+
const CORNERS = [
|
|
400
|
+
"bottom-right",
|
|
401
|
+
"bottom-left",
|
|
402
|
+
"top-right",
|
|
403
|
+
"top-left"
|
|
404
|
+
];
|
|
405
|
+
const DEFAULT_STATE = {
|
|
406
|
+
activeTab: "theme",
|
|
407
|
+
corner: "bottom-right",
|
|
408
|
+
open: false
|
|
409
|
+
};
|
|
410
|
+
function isCorner(value) {
|
|
411
|
+
return CORNERS.includes(value);
|
|
412
|
+
}
|
|
413
|
+
function readItem(key) {
|
|
414
|
+
if (typeof window === "undefined") return null;
|
|
415
|
+
try {
|
|
416
|
+
return window.localStorage.getItem(`${STORAGE_PREFIX}.${key}`);
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function writeItem(key, value) {
|
|
422
|
+
if (typeof window === "undefined") return;
|
|
423
|
+
try {
|
|
424
|
+
window.localStorage.setItem(`${STORAGE_PREFIX}.${key}`, value);
|
|
425
|
+
} catch {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function readShellState() {
|
|
430
|
+
const storedCorner = readItem("corner");
|
|
431
|
+
return {
|
|
432
|
+
activeTab: readItem("tab") ?? DEFAULT_STATE.activeTab,
|
|
433
|
+
corner: storedCorner !== null && isCorner(storedCorner) ? storedCorner : DEFAULT_STATE.corner,
|
|
434
|
+
open: readItem("open") === "true"
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function writeShellState(patch) {
|
|
438
|
+
if (patch.activeTab !== void 0) writeItem("tab", patch.activeTab);
|
|
439
|
+
if (patch.corner !== void 0) writeItem("corner", patch.corner);
|
|
440
|
+
if (patch.open !== void 0) writeItem("open", String(patch.open));
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
443
|
+
//#region src/panels/preferences/panel.tsx
|
|
444
|
+
const CORNER_LABELS = {
|
|
445
|
+
"bottom-right": "Bottom right",
|
|
446
|
+
"bottom-left": "Bottom left",
|
|
447
|
+
"top-right": "Top right",
|
|
448
|
+
"top-left": "Top left"
|
|
449
|
+
};
|
|
450
|
+
function PreferencesPanel() {
|
|
451
|
+
const { corner, setCorner } = useCorner();
|
|
452
|
+
return /* @__PURE__ */ jsx("div", {
|
|
453
|
+
className: "flex flex-col gap-4",
|
|
454
|
+
children: /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
455
|
+
className: "pb-1 font-medium",
|
|
456
|
+
children: "Position"
|
|
457
|
+
}), /* @__PURE__ */ jsx(RadioGroup, {
|
|
458
|
+
onValueChange: (value) => setCorner(value),
|
|
459
|
+
value: corner,
|
|
460
|
+
children: CORNERS.map((option) => /* @__PURE__ */ jsxs("div", {
|
|
461
|
+
className: "flex items-center gap-2",
|
|
462
|
+
children: [
|
|
463
|
+
/* @__PURE__ */ jsx(RadioGroupItem, {
|
|
464
|
+
id: `devtools-corner-${option}`,
|
|
465
|
+
value: option
|
|
466
|
+
}),
|
|
467
|
+
/* @__PURE__ */ jsx(Label, {
|
|
468
|
+
htmlFor: `devtools-corner-${option}`,
|
|
469
|
+
children: CORNER_LABELS[option]
|
|
470
|
+
}),
|
|
471
|
+
option === "bottom-left" && /* @__PURE__ */ jsx("span", {
|
|
472
|
+
className: "text-muted-foreground text-sm",
|
|
473
|
+
children: "overlaps Next's dev indicator"
|
|
474
|
+
})
|
|
475
|
+
]
|
|
476
|
+
}, option))
|
|
477
|
+
})] })
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
//#endregion
|
|
481
|
+
//#region src/probes/theme-dom.ts
|
|
482
|
+
function readThemeDom() {
|
|
483
|
+
if (typeof document === "undefined" || typeof window === "undefined") return unavailable("no document");
|
|
484
|
+
try {
|
|
485
|
+
const root = document.documentElement;
|
|
486
|
+
return ok({
|
|
487
|
+
bodyBackgroundColor: window.getComputedStyle(document.body).backgroundColor,
|
|
488
|
+
dataTheme: root.getAttribute("data-theme"),
|
|
489
|
+
htmlClasses: Array.from(root.classList)
|
|
490
|
+
});
|
|
491
|
+
} catch (error) {
|
|
492
|
+
return probeError(message(error));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
496
|
+
//#region src/probes/ui-storage.ts
|
|
497
|
+
function readUiThemeStorage(storageKey = "ekko-ui") {
|
|
498
|
+
if (typeof window === "undefined" || !window.localStorage) return unavailable("no window");
|
|
499
|
+
try {
|
|
500
|
+
const read = (key) => window.localStorage.getItem(`${storageKey}.${key}`);
|
|
501
|
+
return ok({
|
|
502
|
+
themeDark: read("theme-dark"),
|
|
503
|
+
themeLight: read("theme-light"),
|
|
504
|
+
themeMode: read("theme-mode")
|
|
505
|
+
});
|
|
506
|
+
} catch (error) {
|
|
507
|
+
return probeError(message(error));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
//#endregion
|
|
511
|
+
//#region src/panels/theme/panel.tsx
|
|
512
|
+
function ThemePanel() {
|
|
513
|
+
const theme = useThemeContext();
|
|
514
|
+
const [, forceUpdate] = useState(0);
|
|
515
|
+
useEffect(() => {
|
|
516
|
+
const bump = () => forceUpdate((n) => n + 1);
|
|
517
|
+
window.addEventListener("storage", bump);
|
|
518
|
+
const observer = new MutationObserver(bump);
|
|
519
|
+
observer.observe(document.documentElement, { attributeFilter: ["class", "data-theme"] });
|
|
520
|
+
return () => {
|
|
521
|
+
window.removeEventListener("storage", bump);
|
|
522
|
+
observer.disconnect();
|
|
523
|
+
};
|
|
524
|
+
}, []);
|
|
525
|
+
const dom = readThemeDom();
|
|
526
|
+
const storage = readUiThemeStorage();
|
|
527
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
528
|
+
className: "flex flex-col gap-4",
|
|
529
|
+
children: [
|
|
530
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
531
|
+
className: "pb-1 font-medium",
|
|
532
|
+
children: "Context"
|
|
533
|
+
}), theme === null ? /* @__PURE__ */ jsx("p", {
|
|
534
|
+
className: "text-muted-foreground",
|
|
535
|
+
children: "no theme provider above this panel"
|
|
536
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
537
|
+
/* @__PURE__ */ jsx(Row, {
|
|
538
|
+
label: "appearance",
|
|
539
|
+
value: theme.appearance
|
|
540
|
+
}),
|
|
541
|
+
/* @__PURE__ */ jsx(Row, {
|
|
542
|
+
label: "resolvedMode",
|
|
543
|
+
value: theme.resolvedMode
|
|
544
|
+
}),
|
|
545
|
+
/* @__PURE__ */ jsx(Row, {
|
|
546
|
+
label: "resolvedTheme",
|
|
547
|
+
value: theme.resolvedTheme
|
|
548
|
+
})
|
|
549
|
+
] })] }),
|
|
550
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
551
|
+
className: "pb-1 font-medium",
|
|
552
|
+
children: "DOM"
|
|
553
|
+
}), dom.status === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
554
|
+
/* @__PURE__ */ jsx(Row, {
|
|
555
|
+
label: "html[data-theme]",
|
|
556
|
+
value: dom.data.dataTheme ?? "—"
|
|
557
|
+
}),
|
|
558
|
+
/* @__PURE__ */ jsx(Row, {
|
|
559
|
+
label: "html class",
|
|
560
|
+
value: dom.data.htmlClasses.join(" ") || "—"
|
|
561
|
+
}),
|
|
562
|
+
/* @__PURE__ */ jsx(Row, {
|
|
563
|
+
label: "body background",
|
|
564
|
+
value: dom.data.bodyBackgroundColor
|
|
565
|
+
})
|
|
566
|
+
] }) : /* @__PURE__ */ jsx(Message, { result: dom })] }),
|
|
567
|
+
/* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
568
|
+
className: "pb-1 font-medium",
|
|
569
|
+
children: "Storage"
|
|
570
|
+
}), storage.status === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
571
|
+
/* @__PURE__ */ jsx(Row, {
|
|
572
|
+
label: "ekko-ui.theme-mode",
|
|
573
|
+
value: storage.data.themeMode ?? "—"
|
|
574
|
+
}),
|
|
575
|
+
/* @__PURE__ */ jsx(Row, {
|
|
576
|
+
label: "ekko-ui.theme-light",
|
|
577
|
+
value: storage.data.themeLight ?? "—"
|
|
578
|
+
}),
|
|
579
|
+
/* @__PURE__ */ jsx(Row, {
|
|
580
|
+
label: "ekko-ui.theme-dark",
|
|
581
|
+
value: storage.data.themeDark ?? "—"
|
|
582
|
+
})
|
|
583
|
+
] }) : /* @__PURE__ */ jsx(Message, { result: storage })] })
|
|
584
|
+
]
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region src/probes/cascade.ts
|
|
589
|
+
function isStyleRule(rule) {
|
|
590
|
+
return "selectorText" in rule && "style" in rule;
|
|
591
|
+
}
|
|
592
|
+
function isGroupingRule(rule) {
|
|
593
|
+
return "cssRules" in rule;
|
|
594
|
+
}
|
|
595
|
+
function collectMatches(rules, property, target, href, out) {
|
|
596
|
+
for (const rule of Array.from(rules)) if (isStyleRule(rule)) {
|
|
597
|
+
const value = rule.style.getPropertyValue(property).trim();
|
|
598
|
+
if (value === "") continue;
|
|
599
|
+
try {
|
|
600
|
+
if (!target.matches(rule.selectorText)) continue;
|
|
601
|
+
} catch {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
out.push({
|
|
605
|
+
href,
|
|
606
|
+
selector: rule.selectorText,
|
|
607
|
+
value,
|
|
608
|
+
wins: false
|
|
609
|
+
});
|
|
610
|
+
} else if (isGroupingRule(rule)) collectMatches(rule.cssRules, property, target, href, out);
|
|
611
|
+
}
|
|
612
|
+
function resolveCustomProperty(property, target) {
|
|
613
|
+
try {
|
|
614
|
+
let computed = "";
|
|
615
|
+
try {
|
|
616
|
+
computed = window.getComputedStyle(target).getPropertyValue(property).trim();
|
|
617
|
+
} catch {}
|
|
618
|
+
const matches = [];
|
|
619
|
+
let unreadableSheets = 0;
|
|
620
|
+
for (const sheet of Array.from(document.styleSheets)) try {
|
|
621
|
+
collectMatches(sheet.cssRules, property, target, sheet.href, matches);
|
|
622
|
+
} catch {
|
|
623
|
+
unreadableSheets += 1;
|
|
624
|
+
}
|
|
625
|
+
const winnerIndex = matches.findLastIndex((match) => match.value === computed);
|
|
626
|
+
if (winnerIndex !== -1) matches[winnerIndex].wins = true;
|
|
627
|
+
return ok({
|
|
628
|
+
computed,
|
|
629
|
+
matches,
|
|
630
|
+
unreadableSheets,
|
|
631
|
+
winnerFound: winnerIndex !== -1
|
|
632
|
+
});
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return probeError(message(error));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
//#endregion
|
|
638
|
+
//#region src/panels/tokens/panel.tsx
|
|
639
|
+
function targetLabel(element) {
|
|
640
|
+
const tag = element.tagName.toLowerCase();
|
|
641
|
+
const slot = element.getAttribute("data-slot");
|
|
642
|
+
return slot === null ? tag : `${tag}[data-slot=${slot}]`;
|
|
643
|
+
}
|
|
644
|
+
function TokensPanel() {
|
|
645
|
+
const [property, setProperty] = useState("");
|
|
646
|
+
const [target, setTarget] = useState(document.documentElement);
|
|
647
|
+
const [picking, setPicking] = useState(false);
|
|
648
|
+
useEffect(() => {
|
|
649
|
+
if (!picking) return;
|
|
650
|
+
setPickerArmed(true);
|
|
651
|
+
const onClick = (event) => {
|
|
652
|
+
const clicked = event.target;
|
|
653
|
+
if (!(clicked instanceof Element)) return;
|
|
654
|
+
event.preventDefault();
|
|
655
|
+
event.stopPropagation();
|
|
656
|
+
setTarget(clicked);
|
|
657
|
+
setPicking(false);
|
|
658
|
+
};
|
|
659
|
+
document.addEventListener("click", onClick, true);
|
|
660
|
+
return () => {
|
|
661
|
+
document.removeEventListener("click", onClick, true);
|
|
662
|
+
setPickerArmed(false);
|
|
663
|
+
};
|
|
664
|
+
}, [picking]);
|
|
665
|
+
const trimmed = property.trim();
|
|
666
|
+
const report = trimmed === "" ? null : resolveCustomProperty(trimmed, target);
|
|
667
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
668
|
+
className: "flex flex-col gap-3",
|
|
669
|
+
children: [
|
|
670
|
+
/* @__PURE__ */ jsx(Row, {
|
|
671
|
+
label: "target",
|
|
672
|
+
value: targetLabel(target)
|
|
673
|
+
}),
|
|
674
|
+
/* @__PURE__ */ jsxs("div", {
|
|
675
|
+
className: "flex gap-2",
|
|
676
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
677
|
+
className: "self-start",
|
|
678
|
+
onClick: () => setPicking(true),
|
|
679
|
+
size: "sm",
|
|
680
|
+
type: "button",
|
|
681
|
+
variant: "outline",
|
|
682
|
+
children: picking ? "click an element…" : "Pick element"
|
|
683
|
+
}), target !== document.documentElement && /* @__PURE__ */ jsx(Button, {
|
|
684
|
+
className: "self-start",
|
|
685
|
+
onClick: () => setTarget(document.documentElement),
|
|
686
|
+
size: "sm",
|
|
687
|
+
type: "button",
|
|
688
|
+
variant: "outline",
|
|
689
|
+
children: "Reset to html"
|
|
690
|
+
})]
|
|
691
|
+
}),
|
|
692
|
+
/* @__PURE__ */ jsxs("div", {
|
|
693
|
+
className: "flex flex-col gap-1",
|
|
694
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
695
|
+
className: "text-muted-foreground",
|
|
696
|
+
htmlFor: "devtools-custom-property",
|
|
697
|
+
children: "Custom property"
|
|
698
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
699
|
+
className: "font-mono",
|
|
700
|
+
id: "devtools-custom-property",
|
|
701
|
+
onChange: (event) => setProperty(event.target.value),
|
|
702
|
+
placeholder: "--sidebar-accent",
|
|
703
|
+
value: property
|
|
704
|
+
})]
|
|
705
|
+
}),
|
|
706
|
+
report === null ? null : report.status !== "ok" ? /* @__PURE__ */ jsx(Message, { result: report }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
707
|
+
/* @__PURE__ */ jsx(Row, {
|
|
708
|
+
label: "computed",
|
|
709
|
+
value: report.data.computed || "—"
|
|
710
|
+
}),
|
|
711
|
+
report.data.winnerFound ? null : /* @__PURE__ */ jsx("p", {
|
|
712
|
+
className: "text-muted-foreground",
|
|
713
|
+
children: "no declaration matches the computed value — an inline style or an unreadable sheet won"
|
|
714
|
+
}),
|
|
715
|
+
/* @__PURE__ */ jsx("ul", {
|
|
716
|
+
className: "flex flex-col",
|
|
717
|
+
children: report.data.matches.map((match) => /* @__PURE__ */ jsxs("li", {
|
|
718
|
+
className: "flex flex-col border-t py-1 first:border-t-0",
|
|
719
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
720
|
+
className: "font-mono",
|
|
721
|
+
children: [/* @__PURE__ */ jsx("span", { children: match.selector }), match.wins ? /* @__PURE__ */ jsx("span", { children: " ← wins" }) : null]
|
|
722
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
723
|
+
className: "text-muted-foreground",
|
|
724
|
+
children: [
|
|
725
|
+
match.value,
|
|
726
|
+
" · ",
|
|
727
|
+
match.href ?? "inline"
|
|
728
|
+
]
|
|
729
|
+
})]
|
|
730
|
+
}, `${match.href ?? "inline"}-${match.selector}-${match.value}`))
|
|
731
|
+
}),
|
|
732
|
+
report.data.unreadableSheets === 0 ? null : /* @__PURE__ */ jsxs("p", {
|
|
733
|
+
className: "text-muted-foreground",
|
|
734
|
+
children: [
|
|
735
|
+
report.data.unreadableSheets,
|
|
736
|
+
" unreadable sheet",
|
|
737
|
+
report.data.unreadableSheets === 1 ? "" : "s"
|
|
738
|
+
]
|
|
739
|
+
})
|
|
740
|
+
] })
|
|
741
|
+
]
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
//#endregion
|
|
745
|
+
//#region src/panels/registry.tsx
|
|
746
|
+
const PANELS = [
|
|
747
|
+
{
|
|
748
|
+
id: "theme",
|
|
749
|
+
render: () => /* @__PURE__ */ jsx(ThemePanel, {}),
|
|
750
|
+
title: "Theme"
|
|
751
|
+
},
|
|
752
|
+
{
|
|
753
|
+
id: "tokens",
|
|
754
|
+
render: () => /* @__PURE__ */ jsx(TokensPanel, {}),
|
|
755
|
+
title: "Tokens"
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
id: "cache",
|
|
759
|
+
render: () => /* @__PURE__ */ jsx(CachePanel, {}),
|
|
760
|
+
title: "Cache"
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
id: "preferences",
|
|
764
|
+
render: () => /* @__PURE__ */ jsx(PreferencesPanel, {}),
|
|
765
|
+
title: "Preferences"
|
|
766
|
+
}
|
|
767
|
+
];
|
|
768
|
+
//#endregion
|
|
769
|
+
//#region src/shell/corner.ts
|
|
770
|
+
const CORNER_CONTAINER_CLASSES = {
|
|
771
|
+
"bottom-right": "fixed z-[9999] flex flex-col-reverse items-end gap-2 right-4 bottom-4",
|
|
772
|
+
"bottom-left": "fixed z-[9999] flex flex-col-reverse items-start gap-2 left-4 bottom-4",
|
|
773
|
+
"top-right": "fixed z-[9999] flex flex-col items-end gap-2 right-4 top-4",
|
|
774
|
+
"top-left": "fixed z-[9999] flex flex-col items-start gap-2 left-4 top-4"
|
|
775
|
+
};
|
|
776
|
+
//#endregion
|
|
777
|
+
//#region src/shell/heart-icon.tsx
|
|
778
|
+
const HEART_ICON_DATA_URI = "data:image/png;base64,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";
|
|
779
|
+
function HeartIcon() {
|
|
780
|
+
return /* @__PURE__ */ jsx("img", {
|
|
781
|
+
alt: "",
|
|
782
|
+
className: "h-5 w-5",
|
|
783
|
+
src: HEART_ICON_DATA_URI
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/shell/devtools.tsx
|
|
788
|
+
function panelFor(id) {
|
|
789
|
+
return PANELS.find((panel) => panel.id === id) ?? PANELS[0];
|
|
790
|
+
}
|
|
791
|
+
function EkkoDevtools() {
|
|
792
|
+
const [open, setOpen] = useState(DEFAULT_STATE.open);
|
|
793
|
+
const [activeTab, setActiveTab] = useState(DEFAULT_STATE.activeTab);
|
|
794
|
+
const [corner, setCornerState] = useState(DEFAULT_STATE.corner);
|
|
795
|
+
useEffect(() => {
|
|
796
|
+
const stored = readShellState();
|
|
797
|
+
setOpen(stored.open);
|
|
798
|
+
setActiveTab(stored.activeTab);
|
|
799
|
+
setCornerState(stored.corner);
|
|
800
|
+
}, []);
|
|
801
|
+
const toggle = useCallback(() => {
|
|
802
|
+
setOpen((previous) => {
|
|
803
|
+
writeShellState({ open: !previous });
|
|
804
|
+
return !previous;
|
|
805
|
+
});
|
|
806
|
+
}, []);
|
|
807
|
+
const setCorner = useCallback((next) => {
|
|
808
|
+
setCornerState(next);
|
|
809
|
+
writeShellState({ corner: next });
|
|
810
|
+
}, []);
|
|
811
|
+
useEffect(() => {
|
|
812
|
+
const onKeyDown = (event) => {
|
|
813
|
+
if (event.metaKey && event.shiftKey && !event.ctrlKey && event.code === "KeyU") {
|
|
814
|
+
event.preventDefault();
|
|
815
|
+
toggle();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (event.key === "Escape") setOpen((previous) => {
|
|
819
|
+
if (previous) writeShellState({ open: false });
|
|
820
|
+
return false;
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
window.addEventListener("keydown", onKeyDown);
|
|
824
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
825
|
+
}, [toggle]);
|
|
826
|
+
const selectTab = (id) => {
|
|
827
|
+
setActiveTab(id);
|
|
828
|
+
writeShellState({ activeTab: id });
|
|
829
|
+
};
|
|
830
|
+
const cornerContextValue = useMemo(() => ({
|
|
831
|
+
corner,
|
|
832
|
+
setCorner
|
|
833
|
+
}), [corner, setCorner]);
|
|
834
|
+
const containerClasses = CORNER_CONTAINER_CLASSES[corner];
|
|
835
|
+
const active = panelFor(activeTab);
|
|
836
|
+
return /* @__PURE__ */ jsxs(CornerContext.Provider, {
|
|
837
|
+
value: cornerContextValue,
|
|
838
|
+
children: [/* @__PURE__ */ jsx(InspectorOverlay, {}), /* @__PURE__ */ jsxs("div", {
|
|
839
|
+
className: containerClasses,
|
|
840
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
841
|
+
"aria-expanded": open,
|
|
842
|
+
"aria-label": "Open Ekko devtools",
|
|
843
|
+
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full border bg-card shadow-lg",
|
|
844
|
+
onClick: toggle,
|
|
845
|
+
type: "button",
|
|
846
|
+
children: /* @__PURE__ */ jsx(HeartIcon, {})
|
|
847
|
+
}), open && /* @__PURE__ */ jsxs("section", {
|
|
848
|
+
"aria-label": "Ekko devtools",
|
|
849
|
+
className: "flex h-[28rem] w-[26rem] flex-col overflow-hidden rounded-xl border bg-card shadow-2xl",
|
|
850
|
+
role: "dialog",
|
|
851
|
+
children: [/* @__PURE__ */ jsxs("header", {
|
|
852
|
+
className: "flex items-center gap-1 border-b px-2 py-1",
|
|
853
|
+
children: [PANELS.map((panel) => /* @__PURE__ */ jsx(Button, {
|
|
854
|
+
"aria-pressed": panel.id === active?.id,
|
|
855
|
+
className: "aria-pressed:bg-accent aria-pressed:text-accent-foreground",
|
|
856
|
+
onClick: () => selectTab(panel.id),
|
|
857
|
+
size: "sm",
|
|
858
|
+
type: "button",
|
|
859
|
+
variant: "ghost",
|
|
860
|
+
children: panel.title
|
|
861
|
+
}, panel.id)), /* @__PURE__ */ jsx(Button, {
|
|
862
|
+
"aria-label": "Close Ekko devtools",
|
|
863
|
+
className: "ml-auto text-muted-foreground",
|
|
864
|
+
onClick: toggle,
|
|
865
|
+
size: "icon-sm",
|
|
866
|
+
type: "button",
|
|
867
|
+
variant: "ghost",
|
|
868
|
+
children: "×"
|
|
869
|
+
})]
|
|
870
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
871
|
+
className: "flex-1 overflow-y-auto p-3 text-sm",
|
|
872
|
+
children: active === void 0 ? null : active.render()
|
|
873
|
+
})]
|
|
874
|
+
})]
|
|
875
|
+
})]
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
//#endregion
|
|
879
|
+
export { EkkoDevtools };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region loader/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* @this {{ getOptions?: () => unknown; resourcePath: string }}
|
|
4
|
+
* @param {string} source
|
|
5
|
+
* @returns {string}
|
|
6
|
+
*/
|
|
7
|
+
declare function ekkoDevtoolsLoader(this: {
|
|
8
|
+
getOptions?: () => unknown;
|
|
9
|
+
resourcePath: string;
|
|
10
|
+
}, source: string): string;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { ekkoDevtoolsLoader as default };
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import MagicString from "magic-string";
|
|
3
|
+
import { parseSync, visitorKeys } from "oxc-parser";
|
|
4
|
+
//#region loader/inject-source.js
|
|
5
|
+
const SOURCE_ATTRIBUTE = "data-ekko-source";
|
|
6
|
+
/**
|
|
7
|
+
* @param {any} node
|
|
8
|
+
* @param {(child: any) => void} visit
|
|
9
|
+
*/
|
|
10
|
+
function forEachChild(node, visit) {
|
|
11
|
+
for (const key of visitorKeys[node.type] ?? []) {
|
|
12
|
+
const value = node[key];
|
|
13
|
+
if (Array.isArray(value)) {
|
|
14
|
+
for (const child of value) if (child && typeof child.type === "string") visit(child);
|
|
15
|
+
} else if (value && typeof value.type === "string") visit(value);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** @param {any} name */
|
|
19
|
+
function nameOf(name) {
|
|
20
|
+
if (name.type === "JSXIdentifier") return name.name;
|
|
21
|
+
if (name.type === "JSXMemberExpression") return `${nameOf(name.object)}.${name.property.name}`;
|
|
22
|
+
return `${name.namespace.name}:${name.name.name}`;
|
|
23
|
+
}
|
|
24
|
+
/** @param {any[]} params */
|
|
25
|
+
function propsNameOf(params) {
|
|
26
|
+
const first = params[0];
|
|
27
|
+
if (!first) return null;
|
|
28
|
+
if (first.type === "Identifier") return first.name;
|
|
29
|
+
if (first.type === "ObjectPattern") {
|
|
30
|
+
for (const property of first.properties) if (property.type === "RestElement" && property.argument.type === "Identifier") return property.argument.name;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* @param {any} node
|
|
36
|
+
* @param {string | null} propsName
|
|
37
|
+
*/
|
|
38
|
+
function shouldStamp(node, propsName) {
|
|
39
|
+
const name = nameOf(node.name);
|
|
40
|
+
if (name === "Fragment" || name === "React.Fragment") return false;
|
|
41
|
+
for (const attribute of node.attributes) {
|
|
42
|
+
if (attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier" && attribute.name.name === "data-ekko-source") return false;
|
|
43
|
+
if (attribute.type === "JSXSpreadAttribute" && attribute.argument.type === "Identifier" && attribute.argument.name === propsName) return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
/** @param {string} code */
|
|
48
|
+
function lineStarts(code) {
|
|
49
|
+
const starts = [0];
|
|
50
|
+
for (let i = 0; i < code.length; i += 1) if (code[i] === "\n") starts.push(i + 1);
|
|
51
|
+
return starts;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* @param {number[]} starts
|
|
55
|
+
* @param {number} offset
|
|
56
|
+
*/
|
|
57
|
+
function locOf(starts, offset) {
|
|
58
|
+
let low = 0;
|
|
59
|
+
let high = starts.length - 1;
|
|
60
|
+
while (low < high) {
|
|
61
|
+
const mid = Math.ceil((low + high) / 2);
|
|
62
|
+
if (starts[mid] <= offset) low = mid;
|
|
63
|
+
else high = mid - 1;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
column: offset - starts[low] + 1,
|
|
67
|
+
line: low + 1
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} filePath
|
|
72
|
+
* @returns {string}
|
|
73
|
+
*/
|
|
74
|
+
function stampedPathFor(filePath) {
|
|
75
|
+
if (!path.isAbsolute(filePath)) return filePath;
|
|
76
|
+
const relative = path.relative(process.cwd(), filePath);
|
|
77
|
+
if (relative === "") return filePath;
|
|
78
|
+
return relative.split(path.sep).join("/");
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* @param {string} code
|
|
82
|
+
* @param {string} filePath
|
|
83
|
+
* @returns {{ code: string } | undefined}
|
|
84
|
+
*/
|
|
85
|
+
function addSourceToJsx(code, filePath) {
|
|
86
|
+
let parsed;
|
|
87
|
+
try {
|
|
88
|
+
parsed = parseSync(filePath, code, {
|
|
89
|
+
lang: "tsx",
|
|
90
|
+
sourceType: "module"
|
|
91
|
+
});
|
|
92
|
+
} catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (parsed.errors.length > 0) return void 0;
|
|
96
|
+
const stampedPath = stampedPathFor(filePath);
|
|
97
|
+
const magic = new MagicString(code);
|
|
98
|
+
const starts = lineStarts(code);
|
|
99
|
+
let stamped = false;
|
|
100
|
+
/**
|
|
101
|
+
* @param {any} node
|
|
102
|
+
* @param {string | null} propsName
|
|
103
|
+
*/
|
|
104
|
+
const walk = (node, propsName) => {
|
|
105
|
+
let scopeProps = propsName;
|
|
106
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") scopeProps = propsNameOf(node.params);
|
|
107
|
+
if (node.type === "JSXOpeningElement" && shouldStamp(node, scopeProps)) {
|
|
108
|
+
const loc = locOf(starts, node.start);
|
|
109
|
+
const insertAt = node.end - (node.selfClosing ? 2 : 1);
|
|
110
|
+
const attribute = `${!/\s/.test(code[insertAt - 1]) ? " " : ""}${SOURCE_ATTRIBUTE}="${stampedPath}:${loc.line}:${loc.column}"`;
|
|
111
|
+
magic.appendLeft(insertAt, attribute);
|
|
112
|
+
stamped = true;
|
|
113
|
+
}
|
|
114
|
+
forEachChild(node, (child) => walk(child, scopeProps));
|
|
115
|
+
};
|
|
116
|
+
walk(parsed.program, null);
|
|
117
|
+
return stamped ? { code: magic.toString() } : void 0;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
//#region loader/index.js
|
|
121
|
+
const EXCLUDE = /node_modules/;
|
|
122
|
+
/**
|
|
123
|
+
* @this {{ getOptions?: () => unknown; resourcePath: string }}
|
|
124
|
+
* @param {string} source
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function ekkoDevtoolsLoader(source) {
|
|
128
|
+
const filePath = this.resourcePath;
|
|
129
|
+
if (EXCLUDE.test(filePath) || !/\.[jt]sx$/.test(filePath)) return source;
|
|
130
|
+
return addSourceToJsx(source, filePath)?.code ?? source;
|
|
131
|
+
}
|
|
132
|
+
//#endregion
|
|
133
|
+
export { ekkoDevtoolsLoader as default };
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ekkolyth/devtools",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./loader": {
|
|
17
|
+
"import": "./dist/loader/index.js",
|
|
18
|
+
"default": "./dist/loader/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"magic-string": "^0.30.21",
|
|
23
|
+
"oxc-parser": "^0.127.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@ekkolyth/ui": "0.1.18",
|
|
27
|
+
"react": "^19.2.0",
|
|
28
|
+
"react-dom": "^19.2.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|