@abbox/inspector 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -2
- package/dist/browser.d.ts +9 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +8 -0
- package/dist/host/client-bridge.d.ts +13 -0
- package/dist/host/client-bridge.d.ts.map +1 -0
- package/dist/host/client-bridge.js +75 -0
- package/dist/host/host-bridge.d.ts +34 -0
- package/dist/host/host-bridge.d.ts.map +1 -0
- package/dist/host/host-bridge.js +59 -0
- package/dist/host/origin-aliases.d.ts +3 -0
- package/dist/host/origin-aliases.d.ts.map +1 -0
- package/dist/host/origin-aliases.js +13 -0
- package/dist/host/validate.d.ts +8 -0
- package/dist/host/validate.d.ts.map +1 -0
- package/dist/host/validate.js +13 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/instrument/babel-plugin.d.ts +8 -0
- package/dist/instrument/babel-plugin.d.ts.map +1 -0
- package/dist/instrument/babel-plugin.js +26 -0
- package/dist/instrument/constants.d.ts +10 -0
- package/dist/instrument/constants.d.ts.map +1 -0
- package/dist/instrument/constants.js +17 -0
- package/dist/ir-match.d.ts +34 -0
- package/dist/ir-match.d.ts.map +1 -0
- package/dist/ir-match.js +129 -0
- package/dist/next/babel-config.cjs +39 -0
- package/dist/next/index.d.ts +3 -0
- package/dist/next/index.d.ts.map +1 -0
- package/dist/next/index.js +2 -0
- package/dist/next/preview-env.d.ts +8 -0
- package/dist/next/preview-env.d.ts.map +1 -0
- package/dist/next/preview-env.js +22 -0
- package/dist/next/webpack-instrumentation.cjs +82 -0
- package/dist/next/with-abbox-inspector.d.ts +26 -0
- package/dist/next/with-abbox-inspector.d.ts.map +1 -0
- package/dist/next/with-abbox-inspector.js +80 -0
- package/dist/protocol.d.ts +92 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +12 -0
- package/dist/react/AbboxInspectorBoot.d.ts +3 -0
- package/dist/react/AbboxInspectorBoot.d.ts.map +1 -0
- package/dist/react/AbboxInspectorBoot.js +10 -0
- package/dist/runtime/bootstrap.d.ts +10 -0
- package/dist/runtime/bootstrap.d.ts.map +1 -0
- package/dist/runtime/bootstrap.js +49 -0
- package/dist/runtime/element-ref.d.ts +4 -0
- package/dist/runtime/element-ref.d.ts.map +1 -0
- package/dist/runtime/element-ref.js +14 -0
- package/dist/runtime/inspector.d.ts +18 -0
- package/dist/runtime/inspector.d.ts.map +1 -0
- package/dist/runtime/inspector.js +137 -0
- package/dist/runtime/metadata.d.ts +6 -0
- package/dist/runtime/metadata.d.ts.map +1 -0
- package/dist/runtime/metadata.js +81 -0
- package/dist/runtime/overlay.d.ts +12 -0
- package/dist/runtime/overlay.d.ts.map +1 -0
- package/dist/runtime/overlay.js +104 -0
- package/dist/vite-plugin.d.ts +8 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.js +29 -0
- package/docs/TRUST.md +18 -0
- package/docs/architecture.md +21 -0
- package/docs/implementation-report.md +23 -0
- package/docs/integration.md +21 -0
- package/docs/known-limitations.md +8 -0
- package/docs/nextjs-integration.md +35 -0
- package/package.json +90 -4
- package/src/next/babel-config.cjs +39 -0
- package/src/next/webpack-instrumentation.cjs +82 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ABBBOX_SOURCE_ATTR, parseSourceAttribute, } from "../instrument/constants.js";
|
|
2
|
+
const SENSITIVE_ATTR_PREFIXES = ["data-", "on"];
|
|
3
|
+
const SENSITIVE_ATTR_NAMES = new Set([
|
|
4
|
+
"value",
|
|
5
|
+
"nonce",
|
|
6
|
+
"autocomplete",
|
|
7
|
+
"name",
|
|
8
|
+
"href",
|
|
9
|
+
"src",
|
|
10
|
+
]);
|
|
11
|
+
export function findSourceLocation(el) {
|
|
12
|
+
let node = el;
|
|
13
|
+
while (node) {
|
|
14
|
+
const raw = node.getAttribute(ABBBOX_SOURCE_ATTR);
|
|
15
|
+
if (raw) {
|
|
16
|
+
const parsed = parseSourceAttribute(raw);
|
|
17
|
+
if (parsed) {
|
|
18
|
+
return { ...parsed, raw };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
node = node.parentElement;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export function sourceMappingStatus(source) {
|
|
26
|
+
if (!source)
|
|
27
|
+
return "unresolved";
|
|
28
|
+
return "resolved";
|
|
29
|
+
}
|
|
30
|
+
export function extractMetadata(el) {
|
|
31
|
+
const tagName = el.tagName.toLowerCase();
|
|
32
|
+
const id = el.id || null;
|
|
33
|
+
const classList = Array.from(el.classList);
|
|
34
|
+
const role = el.getAttribute("role");
|
|
35
|
+
const ariaLabel = el.getAttribute("aria-label");
|
|
36
|
+
const textPreview = textPreviewFor(el);
|
|
37
|
+
const attributes = {};
|
|
38
|
+
for (const attr of Array.from(el.attributes)) {
|
|
39
|
+
if (attr.name === ABBBOX_SOURCE_ATTR)
|
|
40
|
+
continue;
|
|
41
|
+
if (SENSITIVE_ATTR_NAMES.has(attr.name))
|
|
42
|
+
continue;
|
|
43
|
+
if (SENSITIVE_ATTR_PREFIXES.some((p) => attr.name.startsWith(p)) &&
|
|
44
|
+
attr.name !== "data-testid") {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (attr.value.length > 120) {
|
|
48
|
+
attributes[attr.name] = `${attr.value.slice(0, 117)}…`;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
attributes[attr.name] = attr.value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
tagName,
|
|
56
|
+
id,
|
|
57
|
+
classList,
|
|
58
|
+
role,
|
|
59
|
+
ariaLabel,
|
|
60
|
+
textPreview,
|
|
61
|
+
attributes,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function textPreviewFor(el) {
|
|
65
|
+
const own = Array.from(el.childNodes)
|
|
66
|
+
.filter((n) => n.nodeType === Node.TEXT_NODE)
|
|
67
|
+
.map((n) => n.textContent?.trim() ?? "")
|
|
68
|
+
.join(" ")
|
|
69
|
+
.trim();
|
|
70
|
+
if (own)
|
|
71
|
+
return own.slice(0, 160);
|
|
72
|
+
const text = el.textContent?.trim() ?? "";
|
|
73
|
+
if (!text)
|
|
74
|
+
return null;
|
|
75
|
+
return text.slice(0, 160);
|
|
76
|
+
}
|
|
77
|
+
export function sourceKeyFor(source) {
|
|
78
|
+
if (!source)
|
|
79
|
+
return null;
|
|
80
|
+
return `${source.file}:${source.line}:${source.column}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ChangeMarker, ElementRect } from "../protocol.js";
|
|
2
|
+
export type OverlayHandles = {
|
|
3
|
+
highlight: HTMLDivElement;
|
|
4
|
+
markerLayer: HTMLDivElement;
|
|
5
|
+
destroy: () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function createOverlay(root: Document): OverlayHandles;
|
|
8
|
+
export declare function positionHighlight(highlight: HTMLDivElement, rect: ElementRect): void;
|
|
9
|
+
export declare function hideHighlight(highlight: HTMLDivElement): void;
|
|
10
|
+
export declare function rectForElement(el: Element): ElementRect;
|
|
11
|
+
export declare function renderChangeMarkers(layer: HTMLDivElement, markers: ChangeMarker[], resolveElement: (refId: string) => Element | null): void;
|
|
12
|
+
//# sourceMappingURL=overlay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overlay.d.ts","sourceRoot":"","sources":["../../src/runtime/overlay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAKhE,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,EAAE,cAAc,CAAC;IAC1B,WAAW,EAAE,cAAc,CAAC;IAC5B,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,cAAc,CAiC5D;AAED,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,cAAc,EACzB,IAAI,EAAE,WAAW,GAChB,IAAI,CAQN;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,cAAc,GAAG,IAAI,CAE7D;AAED,wBAAgB,cAAc,CAAC,EAAE,EAAE,OAAO,GAAG,WAAW,CAQvD;AAID,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,cAAc,EACrB,OAAO,EAAE,YAAY,EAAE,EACvB,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,GAChD,IAAI,CAoCN"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const HIGHLIGHT_Z = 2147483646;
|
|
2
|
+
const MARKER_Z = 2147483645;
|
|
3
|
+
export function createOverlay(root) {
|
|
4
|
+
const highlight = root.createElement("div");
|
|
5
|
+
highlight.setAttribute("data-abbox-inspector", "highlight");
|
|
6
|
+
Object.assign(highlight.style, {
|
|
7
|
+
position: "fixed",
|
|
8
|
+
pointerEvents: "none",
|
|
9
|
+
border: "2px solid #2563eb",
|
|
10
|
+
background: "rgba(37, 99, 235, 0.12)",
|
|
11
|
+
borderRadius: "2px",
|
|
12
|
+
zIndex: String(HIGHLIGHT_Z),
|
|
13
|
+
display: "none",
|
|
14
|
+
boxSizing: "border-box",
|
|
15
|
+
});
|
|
16
|
+
const markerLayer = root.createElement("div");
|
|
17
|
+
markerLayer.setAttribute("data-abbox-inspector", "markers");
|
|
18
|
+
Object.assign(markerLayer.style, {
|
|
19
|
+
position: "fixed",
|
|
20
|
+
inset: "0",
|
|
21
|
+
pointerEvents: "none",
|
|
22
|
+
zIndex: String(MARKER_Z),
|
|
23
|
+
});
|
|
24
|
+
root.body.append(highlight, markerLayer);
|
|
25
|
+
return {
|
|
26
|
+
highlight,
|
|
27
|
+
markerLayer,
|
|
28
|
+
destroy: () => {
|
|
29
|
+
highlight.remove();
|
|
30
|
+
markerLayer.remove();
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function positionHighlight(highlight, rect) {
|
|
35
|
+
Object.assign(highlight.style, {
|
|
36
|
+
display: "block",
|
|
37
|
+
left: `${rect.x}px`,
|
|
38
|
+
top: `${rect.y}px`,
|
|
39
|
+
width: `${rect.width}px`,
|
|
40
|
+
height: `${rect.height}px`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export function hideHighlight(highlight) {
|
|
44
|
+
highlight.style.display = "none";
|
|
45
|
+
}
|
|
46
|
+
export function rectForElement(el) {
|
|
47
|
+
const r = el.getBoundingClientRect();
|
|
48
|
+
return {
|
|
49
|
+
x: r.x,
|
|
50
|
+
y: r.y,
|
|
51
|
+
width: r.width,
|
|
52
|
+
height: r.height,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const markerNodes = new Map();
|
|
56
|
+
export function renderChangeMarkers(layer, markers, resolveElement) {
|
|
57
|
+
for (const [id, node] of markerNodes) {
|
|
58
|
+
if (!markers.some((m) => m.elementRefId === id)) {
|
|
59
|
+
node.remove();
|
|
60
|
+
markerNodes.delete(id);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const marker of markers) {
|
|
64
|
+
const el = resolveElement(marker.elementRefId);
|
|
65
|
+
if (!el)
|
|
66
|
+
continue;
|
|
67
|
+
const rect = el.getBoundingClientRect();
|
|
68
|
+
let dot = markerNodes.get(marker.elementRefId);
|
|
69
|
+
if (!dot) {
|
|
70
|
+
dot = layer.ownerDocument.createElement("div");
|
|
71
|
+
dot.setAttribute("data-abbox-change-marker", marker.kind);
|
|
72
|
+
dot.title = marker.label;
|
|
73
|
+
Object.assign(dot.style, {
|
|
74
|
+
position: "fixed",
|
|
75
|
+
width: "12px",
|
|
76
|
+
height: "12px",
|
|
77
|
+
borderRadius: "50%",
|
|
78
|
+
background: marker.kind === "mock" ? "#f59e0b" : "#ef4444",
|
|
79
|
+
border: "2px solid #fff",
|
|
80
|
+
boxShadow: "0 0 0 1px rgba(0,0,0,0.2)",
|
|
81
|
+
pointerEvents: "none",
|
|
82
|
+
});
|
|
83
|
+
if (marker.pulse !== false) {
|
|
84
|
+
dot.style.animation = "abbox-inspector-pulse 1.4s ease-in-out infinite";
|
|
85
|
+
ensurePulseStyle(layer.ownerDocument);
|
|
86
|
+
}
|
|
87
|
+
layer.append(dot);
|
|
88
|
+
markerNodes.set(marker.elementRefId, dot);
|
|
89
|
+
}
|
|
90
|
+
dot.style.left = `${rect.right - 8}px`;
|
|
91
|
+
dot.style.top = `${rect.top - 4}px`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function ensurePulseStyle(doc) {
|
|
95
|
+
if (doc.getElementById("abbox-inspector-pulse-style"))
|
|
96
|
+
return;
|
|
97
|
+
const style = doc.createElement("style");
|
|
98
|
+
style.id = "abbox-inspector-pulse-style";
|
|
99
|
+
style.textContent = `@keyframes abbox-inspector-pulse {
|
|
100
|
+
0%, 100% { transform: scale(1); opacity: 1; }
|
|
101
|
+
50% { transform: scale(1.35); opacity: 0.75; }
|
|
102
|
+
}`;
|
|
103
|
+
doc.head.append(style);
|
|
104
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from "vite";
|
|
2
|
+
export type AbboxInspectorViteOptions = {
|
|
3
|
+
rootDir?: string;
|
|
4
|
+
/** When false, instrumentation is skipped (e.g. production builds). */
|
|
5
|
+
enabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function abboxInspectorVitePlugin(options?: AbboxInspectorViteOptions): Plugin;
|
|
8
|
+
//# sourceMappingURL=vite-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAGnC,MAAM,MAAM,yBAAyB,GAAG;IACtC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uEAAuE;IACvE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAA8B,GACtC,MAAM,CAuBR"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { abboxSourceBabelPlugin } from "./instrument/babel-plugin.js";
|
|
2
|
+
export function abboxInspectorVitePlugin(options = {}) {
|
|
3
|
+
const enabled = options.enabled ?? true;
|
|
4
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
5
|
+
return {
|
|
6
|
+
name: "abbox-inspector-vite",
|
|
7
|
+
enforce: "pre",
|
|
8
|
+
async transform(code, id) {
|
|
9
|
+
if (!enabled)
|
|
10
|
+
return null;
|
|
11
|
+
if (!/\.(tsx|jsx)$/.test(id))
|
|
12
|
+
return null;
|
|
13
|
+
if (id.includes("node_modules"))
|
|
14
|
+
return null;
|
|
15
|
+
const babel = await import("@babel/core");
|
|
16
|
+
const result = await babel.transformAsync(code, {
|
|
17
|
+
filename: id,
|
|
18
|
+
cwd: rootDir,
|
|
19
|
+
babelrc: false,
|
|
20
|
+
configFile: false,
|
|
21
|
+
plugins: [[abboxSourceBabelPlugin, { rootDir }]],
|
|
22
|
+
sourceMaps: true,
|
|
23
|
+
});
|
|
24
|
+
if (!result?.code)
|
|
25
|
+
return null;
|
|
26
|
+
return { code: result.code, map: result.map ?? undefined };
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/docs/TRUST.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Trust model (Live Preview)
|
|
2
|
+
|
|
3
|
+
## What is protected
|
|
4
|
+
|
|
5
|
+
- **Production deployments** (`VERCEL_ENV=production`): no JSX instrumentation, no `frame-ancestors` for the viewer, client bootstrap does not run (even if `?abboxSession` is present).
|
|
6
|
+
- **Preview deployments** (`VERCEL_ENV=preview`): instrumentation and iframe embedding headers are enabled at build time.
|
|
7
|
+
- **postMessage**: channel, version, session id, and origin allow-lists on host and client. No `targetOrigin: "*"`.
|
|
8
|
+
- **Viewer**: preview URL host allow-list (configurable via `VITE_ABBOX_PREVIEW_HOST_ALLOWLIST`).
|
|
9
|
+
|
|
10
|
+
## What is not authentication
|
|
11
|
+
|
|
12
|
+
- `VERCEL_ENV=preview` only means “this build was a Vercel preview deployment.” Anyone with the preview URL can load that deployment.
|
|
13
|
+
- `abboxSession` in the query string is a protocol correlation id, not a secret.
|
|
14
|
+
- Customers should treat Vercel preview URLs as capability URLs (protect with Vercel access controls if needed).
|
|
15
|
+
|
|
16
|
+
## CSP
|
|
17
|
+
|
|
18
|
+
`withAbboxInspector` adds `Content-Security-Policy: frame-ancestors …` without removing other CSP headers you configure. Remove conflicting `X-Frame-Options: DENY` in your platform if embedding fails.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Layers
|
|
4
|
+
|
|
5
|
+
1. **Protocol** (`src/protocol.ts`) — versioned `postMessage` envelopes (`abbox-inspector` channel).
|
|
6
|
+
2. **Instrumentation** (`src/instrument/`) — Babel plugin writing `data-abbox-src` on JSX.
|
|
7
|
+
3. **Runtime** (`src/runtime/`) — hover highlight, click-to-select (capture phase, default actions prevented in inspect mode), change-marker dots.
|
|
8
|
+
4. **Host bridge** (`src/host/`) — origin + session validation; client runs inside preview, host runs in Viewer.
|
|
9
|
+
5. **IR match** (`src/ir-match.ts`) — optional file:line index for Product IR records (viewer builds the index).
|
|
10
|
+
|
|
11
|
+
## Identity
|
|
12
|
+
|
|
13
|
+
- **Runtime element ref** — assigned per DOM node for the preview session (`el_1`, …).
|
|
14
|
+
- **Source key** — `file:line:column` from instrumentation (not stable across commits).
|
|
15
|
+
- Future PR diffing should attach markers by runtime ref or source key at preview time; diff computation stays outside this package.
|
|
16
|
+
|
|
17
|
+
## Security
|
|
18
|
+
|
|
19
|
+
- Messages require matching `sessionId`, `channel`, and `version`.
|
|
20
|
+
- Origins are explicitly allow-listed (preview origin on the host; viewer origin on the client).
|
|
21
|
+
- No `postMessage` target `*`.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Implementation report (milestone 1)
|
|
2
|
+
|
|
3
|
+
## Delivered
|
|
4
|
+
|
|
5
|
+
- Standalone `abbox-inspector` package with protocol, runtime overlay, host/client bridges, Babel JSX instrumentation, IR match helpers, Vitest suite, Vite example on port 5174.
|
|
6
|
+
- **Next.js**: `withAbboxInspector`, `next-babel` helper, `AbboxInspectorBoot`, package `default` export conditions for Next config loading.
|
|
7
|
+
- **TodoDreams** integration (local, uncommitted): `dev:abbox`, Babel + webpack, iframe CSP, client boot.
|
|
8
|
+
- Viewer **Live Preview** workspace; PM sidebar with provenance-labeled compiler/interpreter facts; improved IR match confidence (`exact` / `ambiguous` / `fuzzy` / `none`).
|
|
9
|
+
- Mock change marker button; markers clear when inspect mode disables.
|
|
10
|
+
|
|
11
|
+
## Onlook
|
|
12
|
+
|
|
13
|
+
Reviewed overlay and instrumentation approaches under their OSS license. Adopted: capture-phase selection, fixed overlay layers, build-time source attributes. Rejected: bundling Onlook as a dependency; full editor coupling.
|
|
14
|
+
|
|
15
|
+
## Tests
|
|
16
|
+
|
|
17
|
+
Run `npm run ci` in `abbox-inspector` and `abbox-viewer` after `npm install` (viewer depends on `file:../abbox-inspector`).
|
|
18
|
+
|
|
19
|
+
## Future GitHub diff
|
|
20
|
+
|
|
21
|
+
- Host will supply `ChangeMarker[]` from branch comparison.
|
|
22
|
+
- Inspector positions markers using `elementRefId` from the active preview session.
|
|
23
|
+
- Stable mapping may later combine IR ids (`sec_*`, `elm_*`) with source hashes.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Integration
|
|
2
|
+
|
|
3
|
+
## Preview application
|
|
4
|
+
|
|
5
|
+
1. Add dependency: `"abbox-inspector": "file:../abbox-inspector"` (or published package).
|
|
6
|
+
2. Enable Babel plugin in Vite React config or use `abboxInspectorVitePlugin`.
|
|
7
|
+
3. Call `bootstrapInspectorPreview()` once at startup (see `example/`).
|
|
8
|
+
|
|
9
|
+
## Abbox Viewer
|
|
10
|
+
|
|
11
|
+
- New hash route `#/inspector` and **Live Preview** nav item.
|
|
12
|
+
- `VisualInspectorView` embeds an iframe and uses `createInspectorHost`.
|
|
13
|
+
- Product IR sidebar uses `buildProductIrFileLineIndex` + `matchSourceToIr`.
|
|
14
|
+
|
|
15
|
+
## Environment
|
|
16
|
+
|
|
17
|
+
Viewer optional env: `VITE_ABBOX_PREVIEW_URL` (default `http://localhost:5174/?abboxSession=local-dev`).
|
|
18
|
+
|
|
19
|
+
## Change markers (mock)
|
|
20
|
+
|
|
21
|
+
Host sends `APPLY_CHANGE_MARKERS` with `{ elementRefId, label, kind: "mock" }`. The runtime draws a pulsing dot; this is demonstration-only until a branch diff service exists.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Known limitations
|
|
2
|
+
|
|
3
|
+
- **Instrumentation coverage** — only JSX/TSX files processed by the Babel plugin; dynamic `createElement` calls are not mapped.
|
|
4
|
+
- **Source stability** — line/column identities shift when unrelated lines are edited; no cross-commit stable element IDs yet.
|
|
5
|
+
- **Product IR match** — file:line index only; many runtime selections will not match IR until paths align with compiler evidence.
|
|
6
|
+
- **iframe** — auth-gated apps, strict CSP, or `X-Frame-Options` may block embedding; the viewer surfaces load errors without breaking other tabs.
|
|
7
|
+
- **Fiber** — React Fiber mapping is not required and not implemented in this milestone.
|
|
8
|
+
- **Next.js** — example uses Vite; Next.js integration uses the same Babel plugin via `next.config` `babel` customization (documented path, not shipped as a preset here).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Next.js integration
|
|
2
|
+
|
|
3
|
+
## Requirements
|
|
4
|
+
|
|
5
|
+
- **App Router** with `app/layout.tsx`
|
|
6
|
+
- **Webpack** for instrumented builds (see `scripts/abbox-vercel-build.mjs` from `@abbox/cli init`)
|
|
7
|
+
- **babel-loader** devDependency (added by CLI)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx @abbox/cli init
|
|
13
|
+
npx @abbox/cli doctor
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## How instrumentation works
|
|
17
|
+
|
|
18
|
+
- **No root `babel.config.js`** — avoids forcing Babel for the whole app (which breaks `next/font`).
|
|
19
|
+
- `withAbboxInspector()` adds a **webpack `babel-loader` rule** for `app/**` and `components/**` JSX/TSX.
|
|
20
|
+
- **`app/layout.tsx` is excluded** so `next/font` stays on SWC.
|
|
21
|
+
|
|
22
|
+
## Local dev
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm run dev:abbox
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Vercel
|
|
29
|
+
|
|
30
|
+
- **Preview**: `VERCEL_ENV=preview` → `next build --webpack` + inspector headers/runtime.
|
|
31
|
+
- **Production**: default `next build` (Turbopack) without instrumentation.
|
|
32
|
+
|
|
33
|
+
## Trust
|
|
34
|
+
|
|
35
|
+
See [TRUST.md](./TRUST.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abbox/inspector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
}
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "Runtime visual inspection for React/Next.js previews with source-backed element selection.",
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.12"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/browser.d.ts",
|
|
16
|
+
"import": "./dist/browser.js",
|
|
17
|
+
"default": "./dist/browser.js"
|
|
18
|
+
},
|
|
19
|
+
"./node": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./runtime": {
|
|
25
|
+
"types": "./dist/runtime/bootstrap.d.ts",
|
|
26
|
+
"import": "./dist/runtime/bootstrap.js",
|
|
27
|
+
"default": "./dist/runtime/bootstrap.js"
|
|
28
|
+
},
|
|
29
|
+
"./vite": {
|
|
30
|
+
"types": "./dist/vite-plugin.d.ts",
|
|
31
|
+
"import": "./dist/vite-plugin.js",
|
|
32
|
+
"default": "./dist/vite-plugin.js"
|
|
33
|
+
},
|
|
34
|
+
"./next": {
|
|
35
|
+
"types": "./dist/next/index.d.ts",
|
|
36
|
+
"import": "./dist/next/index.js",
|
|
37
|
+
"default": "./dist/next/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./next-babel": "./src/next/babel-config.cjs",
|
|
40
|
+
"./babel": {
|
|
41
|
+
"types": "./dist/instrument/babel-plugin.d.ts",
|
|
42
|
+
"import": "./dist/instrument/babel-plugin.js",
|
|
43
|
+
"default": "./dist/instrument/babel-plugin.js"
|
|
44
|
+
},
|
|
45
|
+
"./react": {
|
|
46
|
+
"types": "./dist/react/AbboxInspectorBoot.d.ts",
|
|
47
|
+
"import": "./dist/react/AbboxInspectorBoot.js",
|
|
48
|
+
"default": "./dist/react/AbboxInspectorBoot.js"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"src/next/babel-config.cjs",
|
|
54
|
+
"src/next/webpack-instrumentation.cjs",
|
|
55
|
+
"docs",
|
|
56
|
+
"README.md"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs');const p='dist/next';fs.mkdirSync(p,{recursive:true});fs.copyFileSync('src/next/webpack-instrumentation.cjs',p+'/webpack-instrumentation.cjs');fs.copyFileSync('src/next/babel-config.cjs',p+'/babel-config.cjs');\"",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
61
|
+
"test": "npm run build && vitest run",
|
|
62
|
+
"lint": "biome ci .",
|
|
63
|
+
"format": "biome check --write .",
|
|
64
|
+
"ci": "npm run typecheck && npm run test && npm run lint && npm run build",
|
|
65
|
+
"example:dev": "npm run build && npm --prefix example run dev",
|
|
66
|
+
"example:build": "npm run build && npm --prefix example run build"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@babel/core": "^7.28.4",
|
|
70
|
+
"@babel/types": "^7.28.4"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"babel-loader": "^9.2.1 || ^10.0.0",
|
|
74
|
+
"next": ">=14.0.0",
|
|
75
|
+
"react": ">=18"
|
|
76
|
+
},
|
|
77
|
+
"peerDependenciesMeta": {
|
|
78
|
+
"babel-loader": {
|
|
79
|
+
"optional": false
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"@types/react": "^19.3.0",
|
|
84
|
+
"react": "^19.3.0",
|
|
85
|
+
"@biomejs/biome": "2.5.15",
|
|
86
|
+
"@types/babel__core": "^7.20.5",
|
|
87
|
+
"@types/node": "^22.20.5",
|
|
88
|
+
"happy-dom": "^20.0.10",
|
|
89
|
+
"typescript": "^5.7.3",
|
|
90
|
+
"vitest": "^5.0.4"
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CommonJS babel config fragment for Next.js apps.
|
|
3
|
+
* Copy or require from babel.config.cjs when ABBBOX_INSPECTOR=1.
|
|
4
|
+
*/
|
|
5
|
+
const { createRequire } = require("node:module");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
|
|
8
|
+
function createNextBabelConfig(options = {}) {
|
|
9
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
10
|
+
const enabled =
|
|
11
|
+
options.enabled ??
|
|
12
|
+
(process.env.ABBBOX_INSPECTOR === "1" ||
|
|
13
|
+
process.env.NEXT_PUBLIC_ABBOX_INSPECTOR === "1" ||
|
|
14
|
+
process.env.VERCEL_ENV === "preview");
|
|
15
|
+
|
|
16
|
+
const req = createRequire(path.join(rootDir, "package.json"));
|
|
17
|
+
let pluginPath = "@abbox/inspector/babel";
|
|
18
|
+
try {
|
|
19
|
+
pluginPath = req.resolve("@abbox/inspector/babel");
|
|
20
|
+
} catch {
|
|
21
|
+
try {
|
|
22
|
+
pluginPath = req.resolve("abbox-inspector/babel");
|
|
23
|
+
} catch {
|
|
24
|
+
pluginPath = req.resolve(
|
|
25
|
+
path.join(
|
|
26
|
+
rootDir,
|
|
27
|
+
"node_modules/@abbox/inspector/dist/instrument/babel-plugin.js",
|
|
28
|
+
),
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
presets: ["next/babel"],
|
|
35
|
+
plugins: enabled ? [[pluginPath, { rootDir }]] : [],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = { createNextBabelConfig };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply Abbox JSX instrumentation via webpack + babel-loader on selected app sources.
|
|
3
|
+
* Does not use babel.config.js, so Next.js keeps SWC (and next/font) for excluded files
|
|
4
|
+
* such as app/layout.tsx.
|
|
5
|
+
*/
|
|
6
|
+
const fs = require("node:fs");
|
|
7
|
+
const path = require("node:path");
|
|
8
|
+
const { createRequire } = require("node:module");
|
|
9
|
+
|
|
10
|
+
const LAYOUT_BASENAMES = new Set(["layout.tsx", "layout.jsx"]);
|
|
11
|
+
|
|
12
|
+
function resolveInspectorBabelPlugin(rootDir) {
|
|
13
|
+
const req = createRequire(path.join(rootDir, "package.json"));
|
|
14
|
+
try {
|
|
15
|
+
return req.resolve("@abbox/inspector/babel");
|
|
16
|
+
} catch {
|
|
17
|
+
return req.resolve("abbox-inspector/babel");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolveBabelLoader(rootDir) {
|
|
22
|
+
const req = createRequire(path.join(rootDir, "package.json"));
|
|
23
|
+
return req.resolve("babel-loader");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveNextBabelPreset(rootDir) {
|
|
27
|
+
const req = createRequire(path.join(rootDir, "package.json"));
|
|
28
|
+
return req.resolve("next/babel");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function collectIncludeDirs(rootDir) {
|
|
32
|
+
return ["app", "components", "src", "pages"]
|
|
33
|
+
.map((segment) => path.join(rootDir, segment))
|
|
34
|
+
.filter((dir) => fs.existsSync(dir));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isRootLayoutResource(resourcePath) {
|
|
38
|
+
const base = path.basename(resourcePath);
|
|
39
|
+
if (!LAYOUT_BASENAMES.has(base)) return false;
|
|
40
|
+
const parent = path.basename(path.dirname(resourcePath));
|
|
41
|
+
return parent === "app" || parent === "src/app";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {import('webpack').Configuration} webpackConfig
|
|
46
|
+
* @param {{ dir: string }} options
|
|
47
|
+
*/
|
|
48
|
+
function applyAbboxWebpackInstrumentation(webpackConfig, options) {
|
|
49
|
+
const rootDir = options.dir ?? process.cwd();
|
|
50
|
+
const pluginPath = resolveInspectorBabelPlugin(rootDir);
|
|
51
|
+
const babelLoader = resolveBabelLoader(rootDir);
|
|
52
|
+
const nextBabel = resolveNextBabelPreset(rootDir);
|
|
53
|
+
const includeDirs = collectIncludeDirs(rootDir);
|
|
54
|
+
|
|
55
|
+
if (includeDirs.length === 0) {
|
|
56
|
+
return webpackConfig;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const rule = {
|
|
60
|
+
test: /\.(tsx|jsx)$/,
|
|
61
|
+
include: (resourcePath) =>
|
|
62
|
+
includeDirs.some((dir) => resourcePath.startsWith(dir)) &&
|
|
63
|
+
!isRootLayoutResource(resourcePath),
|
|
64
|
+
exclude: [/node_modules/],
|
|
65
|
+
use: {
|
|
66
|
+
loader: babelLoader,
|
|
67
|
+
options: {
|
|
68
|
+
babelrc: false,
|
|
69
|
+
configFile: false,
|
|
70
|
+
presets: [nextBabel],
|
|
71
|
+
plugins: [[pluginPath, { rootDir }]],
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
webpackConfig.module = webpackConfig.module ?? { rules: [] };
|
|
77
|
+
webpackConfig.module.rules = webpackConfig.module.rules ?? [];
|
|
78
|
+
webpackConfig.module.rules.unshift(rule);
|
|
79
|
+
return webpackConfig;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { applyAbboxWebpackInstrumentation, isRootLayoutResource };
|