@abbox/inspector 0.2.1 → 0.4.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.
Files changed (37) hide show
  1. package/dist/browser.d.ts +3 -3
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +3 -3
  4. package/dist/host/client-bridge.js +1 -1
  5. package/dist/host/host-bridge.d.ts.map +1 -1
  6. package/dist/host/host-bridge.js +5 -1
  7. package/dist/host/origin-aliases.d.ts.map +1 -1
  8. package/dist/next/abbox-pre-swc-loader.cjs +44 -0
  9. package/dist/next/babel-config.cjs +1 -0
  10. package/dist/next/index.d.ts +2 -2
  11. package/dist/next/index.d.ts.map +1 -1
  12. package/dist/next/index.js +2 -2
  13. package/dist/next/preview-env.d.ts +8 -1
  14. package/dist/next/preview-env.d.ts.map +1 -1
  15. package/dist/next/preview-env.js +26 -9
  16. package/dist/next/webpack-instrumentation.cjs +104 -26
  17. package/dist/next/with-abbox-inspector.d.ts +9 -4
  18. package/dist/next/with-abbox-inspector.d.ts.map +1 -1
  19. package/dist/next/with-abbox-inspector.js +14 -2
  20. package/dist/protocol.d.ts +4 -0
  21. package/dist/protocol.d.ts.map +1 -1
  22. package/dist/runtime/bootstrap-standby.d.ts +3 -0
  23. package/dist/runtime/bootstrap-standby.d.ts.map +1 -0
  24. package/dist/runtime/bootstrap-standby.js +20 -0
  25. package/dist/runtime/bootstrap.d.ts +5 -2
  26. package/dist/runtime/bootstrap.d.ts.map +1 -1
  27. package/dist/runtime/bootstrap.js +85 -21
  28. package/dist/runtime/inspector.d.ts.map +1 -1
  29. package/dist/runtime/inspector.js +6 -3
  30. package/dist/runtime/metadata.d.ts.map +1 -1
  31. package/dist/runtime/metadata.js +15 -0
  32. package/docs/TRUST.md +13 -6
  33. package/docs/nextjs-integration.md +2 -1
  34. package/package.json +3 -2
  35. package/src/next/abbox-pre-swc-loader.cjs +44 -0
  36. package/src/next/babel-config.cjs +1 -0
  37. package/src/next/webpack-instrumentation.cjs +104 -26
package/dist/browser.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /** Browser / Viewer-safe entry (no Babel, Vite, or Next build tooling). */
2
- export * from "./protocol.js";
3
- export * from "./ir-match.js";
4
- export * from "./instrument/constants.js";
5
2
  export { createInspectorClient } from "./host/client-bridge.js";
6
3
  export { createInspectorHost } from "./host/host-bridge.js";
7
4
  export { validateIncomingMessage } from "./host/validate.js";
5
+ export * from "./instrument/constants.js";
6
+ export * from "./ir-match.js";
7
+ export * from "./protocol.js";
8
8
  export { createInspectorRuntime } from "./runtime/inspector.js";
9
9
  //# sourceMappingURL=browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAE3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AAC5D,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAC"}
package/dist/browser.js CHANGED
@@ -1,8 +1,8 @@
1
1
  /** Browser / Viewer-safe entry (no Babel, Vite, or Next build tooling). */
2
- export * from "./protocol.js";
3
- export * from "./ir-match.js";
4
- export * from "./instrument/constants.js";
5
2
  export { createInspectorClient } from "./host/client-bridge.js";
6
3
  export { createInspectorHost } from "./host/host-bridge.js";
7
4
  export { validateIncomingMessage } from "./host/validate.js";
5
+ export * from "./instrument/constants.js";
6
+ export * from "./ir-match.js";
7
+ export * from "./protocol.js";
8
8
  export { createInspectorRuntime } from "./runtime/inspector.js";
@@ -1,6 +1,6 @@
1
1
  import { ABBBOX_INSPECTOR_CHANNEL, PROTOCOL_VERSION, } from "../protocol.js";
2
- import { withLocalhostAliases } from "./origin-aliases.js";
3
2
  import { createInspectorRuntime } from "../runtime/inspector.js";
3
+ import { withLocalhostAliases } from "./origin-aliases.js";
4
4
  import { validateIncomingMessage } from "./validate.js";
5
5
  export function createInspectorClient(options) {
6
6
  const win = options.window ?? globalThis.window;
@@ -1 +1 @@
1
- {"version":3,"file":"host-bridge.d.ts","sourceRoot":"","sources":["../../src/host/host-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EAGzB,MAAM,gBAAgB,CAAC;AAIxB,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,KAAK,YAAY,GAAG,OAAO,CACzB,mBAAmB,EACnB;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,CAC5B,CAAC,SAAS,CAAC,CAAC;AACb,KAAK,aAAa,GAAG,OAAO,CAC1B,mBAAmB,EACnB;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAC7B,CAAC,SAAS,CAAC,CAAC;AAEb,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,kBAAkB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACtD,0EAA0E;IAC1E,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,qBAAqB,GAC9B,aAAa,CAwDf"}
1
+ {"version":3,"file":"host-bridge.d.ts","sourceRoot":"","sources":["../../src/host/host-bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EAGzB,MAAM,gBAAgB,CAAC;AAIxB,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,0DAA0D;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACvC,CAAC;AAEF,KAAK,YAAY,GAAG,OAAO,CACzB,mBAAmB,EACnB;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,CAC5B,CAAC,SAAS,CAAC,CAAC;AACb,KAAK,aAAa,GAAG,OAAO,CAC1B,mBAAmB,EACnB;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAC7B,CAAC,SAAS,CAAC,CAAC;AAEb,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IAC1C,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,kBAAkB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACtD,0EAA0E;IAC1E,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB,CAAC;AAEF,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,oBAAoB,EAC7B,QAAQ,EAAE,qBAAqB,GAC9B,aAAa,CA4Df"}
@@ -53,7 +53,11 @@ export function createInspectorHost(options, handlers) {
53
53
  send({ ...base(), type: "APPLY_CHANGE_MARKERS", markers });
54
54
  },
55
55
  pingPreview: () => {
56
- send({ ...base(), type: "INSPECTOR_HOST_HELLO" });
56
+ send({
57
+ ...base(),
58
+ type: "INSPECTOR_HOST_HELLO",
59
+ hostOrigin: options.hostOrigin,
60
+ });
57
61
  },
58
62
  };
59
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"origin-aliases.d.ts","sourceRoot":"","sources":["../../src/host/origin-aliases.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,SAAS,MAAM,EAAE,GACzB,MAAM,EAAE,CAWV"}
1
+ {"version":3,"file":"origin-aliases.d.ts","sourceRoot":"","sources":["../../src/host/origin-aliases.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAWzE"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Runs Abbox JSX instrumentation on TSX/JSX before Next's SWC pass.
3
+ * No-ops for paths outside configured app source directories.
4
+ */
5
+ const babel = require("@babel/core");
6
+ const { shouldInstrumentResource } = require("./webpack-instrumentation.cjs");
7
+
8
+ /**
9
+ * @this {import('webpack').LoaderContext<unknown>}
10
+ */
11
+ module.exports = function abboxPreSwcLoader(source) {
12
+ const callback = this.async();
13
+ const options = this.getOptions();
14
+ const rootDir = options.rootDir ?? this.rootContext;
15
+ const resourcePath = this.resourcePath;
16
+
17
+ if (!shouldInstrumentResource(resourcePath, rootDir)) {
18
+ callback(null, source);
19
+ return;
20
+ }
21
+
22
+ babel.transform(
23
+ source,
24
+ {
25
+ filename: resourcePath,
26
+ babelrc: false,
27
+ configFile: false,
28
+ parserOpts: { plugins: ["jsx", "typescript"] },
29
+ plugins: options.plugins,
30
+ sourceMaps: this.sourceMap,
31
+ },
32
+ (error, result) => {
33
+ if (error) {
34
+ callback(error);
35
+ return;
36
+ }
37
+ if (!result?.code) {
38
+ callback(null, source);
39
+ return;
40
+ }
41
+ callback(null, result.code, result.map);
42
+ },
43
+ );
44
+ };
@@ -10,6 +10,7 @@ function createNextBabelConfig(options = {}) {
10
10
  const enabled =
11
11
  options.enabled ??
12
12
  (process.env.ABBBOX_INSPECTOR === "1" ||
13
+ process.env.ABBBOX_INSPECTION_MODE === "production" ||
13
14
  process.env.NEXT_PUBLIC_ABBOX_INSPECTOR === "1" ||
14
15
  process.env.VERCEL_ENV === "preview");
15
16
 
@@ -1,3 +1,3 @@
1
- export { type WithAbboxInspectorOptions, withAbboxInspector, DEFAULT_VIEWER_FRAME_ANCESTORS, } from "./with-abbox-inspector.js";
2
- export { isAbboxInspectorBuildEnabled, isAbboxInspectorRuntimeEnabled, } from "./preview-env.js";
1
+ export { type AbboxInspectionMode, getBakedInspectionMode, getBakedViewerOrigin, isAbboxInspectorBuildEnabled, isAbboxInspectorProductionRuntime, isAbboxInspectorRuntimeEnabled, } from "./preview-env.js";
2
+ export { DEFAULT_VIEWER_FRAME_ANCESTORS, type WithAbboxInspectorOptions, withAbboxInspector, } from "./with-abbox-inspector.js";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,yBAAyB,EAC9B,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,4BAA4B,EAC5B,8BAA8B,GAC/B,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/next/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,sBAAsB,EACtB,oBAAoB,EACpB,4BAA4B,EAC5B,iCAAiC,EACjC,8BAA8B,GAC/B,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,8BAA8B,EAC9B,KAAK,yBAAyB,EAC9B,kBAAkB,GACnB,MAAM,2BAA2B,CAAC"}
@@ -1,2 +1,2 @@
1
- export { withAbboxInspector, DEFAULT_VIEWER_FRAME_ANCESTORS, } from "./with-abbox-inspector.js";
2
- export { isAbboxInspectorBuildEnabled, isAbboxInspectorRuntimeEnabled, } from "./preview-env.js";
1
+ export { getBakedInspectionMode, getBakedViewerOrigin, isAbboxInspectorBuildEnabled, isAbboxInspectorProductionRuntime, isAbboxInspectorRuntimeEnabled, } from "./preview-env.js";
2
+ export { DEFAULT_VIEWER_FRAME_ANCESTORS, withAbboxInspector, } from "./with-abbox-inspector.js";
@@ -1,8 +1,15 @@
1
+ export type AbboxInspectionMode = "preview" | "production";
1
2
  /**
2
3
  * When true, Inspector instrumentation, CSP frame-ancestors, and client bootstrap are allowed.
3
- * Production Vercel deploys (VERCEL_ENV=production) stay off unless ABBBOX_INSPECTOR=1 is set explicitly.
4
+ * Preview Vercel deploys stay on by default; production deploys require explicit
5
+ * `ABBBOX_INSPECTION_MODE=production` (set by @abbox/cli production mode).
4
6
  */
5
7
  export declare function isAbboxInspectorBuildEnabled(): boolean;
8
+ /** Baked inspection mode in the client bundle (not query parameters). */
9
+ export declare function getBakedInspectionMode(): AbboxInspectionMode | "off";
6
10
  /** Client-side: Inspector may connect to the Viewer iframe host. */
7
11
  export declare function isAbboxInspectorRuntimeEnabled(): boolean;
12
+ export declare function isAbboxInspectorProductionRuntime(): boolean;
13
+ /** Primary Viewer origin baked at install time (production mode host validation). */
14
+ export declare function getBakedViewerOrigin(): string | undefined;
8
15
  //# sourceMappingURL=preview-env.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"preview-env.d.ts","sourceRoot":"","sources":["../../src/next/preview-env.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CAItD;AAED,oEAAoE;AACpE,wBAAgB,8BAA8B,IAAI,OAAO,CAOxD"}
1
+ {"version":3,"file":"preview-env.d.ts","sourceRoot":"","sources":["../../src/next/preview-env.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,YAAY,CAAC;AAE3D;;;;GAIG;AACH,wBAAgB,4BAA4B,IAAI,OAAO,CAKtD;AAED,yEAAyE;AACzE,wBAAgB,sBAAsB,IAAI,mBAAmB,GAAG,KAAK,CAOpE;AAED,oEAAoE;AACpE,wBAAgB,8BAA8B,IAAI,OAAO,CAExD;AAED,wBAAgB,iCAAiC,IAAI,OAAO,CAE3D;AAED,qFAAqF;AACrF,wBAAgB,oBAAoB,IAAI,MAAM,GAAG,SAAS,CAGzD"}
@@ -1,22 +1,39 @@
1
1
  /**
2
2
  * When true, Inspector instrumentation, CSP frame-ancestors, and client bootstrap are allowed.
3
- * Production Vercel deploys (VERCEL_ENV=production) stay off unless ABBBOX_INSPECTOR=1 is set explicitly.
3
+ * Preview Vercel deploys stay on by default; production deploys require explicit
4
+ * `ABBBOX_INSPECTION_MODE=production` (set by @abbox/cli production mode).
4
5
  */
5
6
  export function isAbboxInspectorBuildEnabled() {
6
7
  if (process.env.ABBBOX_INSPECTOR === "1")
7
8
  return true;
9
+ if (process.env.ABBBOX_INSPECTION_MODE === "production")
10
+ return true;
8
11
  if (process.env.VERCEL_ENV === "preview")
9
12
  return true;
10
13
  return false;
11
14
  }
15
+ /** Baked inspection mode in the client bundle (not query parameters). */
16
+ export function getBakedInspectionMode() {
17
+ const explicit = process.env.NEXT_PUBLIC_ABBOX_INSPECTION_MODE;
18
+ if (explicit === "production")
19
+ return "production";
20
+ if (explicit === "preview")
21
+ return "preview";
22
+ if (process.env.NEXT_PUBLIC_ABBOX_INSPECTOR === "1")
23
+ return "preview";
24
+ if (process.env.NEXT_PUBLIC_VERCEL_ENV === "preview")
25
+ return "preview";
26
+ return "off";
27
+ }
12
28
  /** Client-side: Inspector may connect to the Viewer iframe host. */
13
29
  export function isAbboxInspectorRuntimeEnabled() {
14
- if (process.env.NEXT_PUBLIC_ABBOX_INSPECTOR === "1")
15
- return true;
16
- if (process.env.VERCEL_ENV === "preview")
17
- return true;
18
- if (typeof process.env.NEXT_PUBLIC_VERCEL_ENV === "string") {
19
- return process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
20
- }
21
- return false;
30
+ return getBakedInspectionMode() !== "off";
31
+ }
32
+ export function isAbboxInspectorProductionRuntime() {
33
+ return getBakedInspectionMode() === "production";
34
+ }
35
+ /** Primary Viewer origin baked at install time (production mode host validation). */
36
+ export function getBakedViewerOrigin() {
37
+ const raw = process.env.NEXT_PUBLIC_ABBOX_VIEWER_ORIGIN;
38
+ return raw?.trim() ? raw.replace(/\/$/, "") : undefined;
22
39
  }
@@ -2,6 +2,10 @@
2
2
  * Apply Abbox JSX instrumentation via webpack + babel-loader on selected app sources.
3
3
  * Does not use babel.config.js, so Next.js keeps SWC (and next/font) for excluded files
4
4
  * such as app/layout.tsx.
5
+ *
6
+ * Next.js App Router client layers compile with SWC before the default Babel pass, so a
7
+ * top-level babel rule is not enough. We prepend an Abbox babel-loader step on the App
8
+ * Router browser/SSR layer rules (before SWC runs).
5
9
  */
6
10
  const fs = require("node:fs");
7
11
  const path = require("node:path");
@@ -9,6 +13,11 @@ const { createRequire } = require("node:module");
9
13
 
10
14
  const LAYOUT_BASENAMES = new Set(["layout.tsx", "layout.jsx"]);
11
15
 
16
+ /** Layers where Next compiles client/app TSX through SWC (must instrument before SWC). */
17
+ const APP_CLIENT_ISSUER_LAYERS = new Set(["app-pages-browser", "ssr"]);
18
+
19
+ const ABBBOX_LOADER_IDENT = "abbox-source-instrumentation";
20
+
12
21
  function resolveInspectorBabelPlugin(rootDir) {
13
22
  const req = createRequire(path.join(rootDir, "package.json"));
14
23
  try {
@@ -18,29 +27,93 @@ function resolveInspectorBabelPlugin(rootDir) {
18
27
  }
19
28
  }
20
29
 
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
30
  function collectIncludeDirs(rootDir) {
32
- return ["app", "components", "src", "pages"]
31
+ return ["app", "components", "src", "pages", "lib", "hooks", "features"]
33
32
  .map((segment) => path.join(rootDir, segment))
34
33
  .filter((dir) => fs.existsSync(dir));
35
34
  }
36
35
 
37
36
  function isRootLayoutResource(resourcePath) {
38
- const base = path.basename(resourcePath);
37
+ const clean = resourcePath.split("?")[0];
38
+ const base = path.basename(clean);
39
39
  if (!LAYOUT_BASENAMES.has(base)) return false;
40
- const parent = path.basename(path.dirname(resourcePath));
40
+ const parent = path.basename(path.dirname(clean));
41
41
  return parent === "app" || parent === "src/app";
42
42
  }
43
43
 
44
+ function shouldInstrumentResource(resourcePath, rootDir) {
45
+ const clean = resourcePath.split("?")[0];
46
+ const includeDirs = collectIncludeDirs(rootDir);
47
+ if (!/\.(tsx|jsx)$/.test(clean)) return false;
48
+ return (
49
+ includeDirs.some((dir) => clean.startsWith(dir)) &&
50
+ !isRootLayoutResource(clean)
51
+ );
52
+ }
53
+
54
+ function loaderIdent(entry) {
55
+ if (!entry) return "";
56
+ if (typeof entry === "string") return entry;
57
+ return String(entry.loader ?? entry.ident ?? "");
58
+ }
59
+
60
+ function resolvePreSwcLoader() {
61
+ return path.join(__dirname, "abbox-pre-swc-loader.cjs");
62
+ }
63
+
64
+ function createAbboxBabelLoaderUse(pluginPath, rootDir) {
65
+ return {
66
+ loader: resolvePreSwcLoader(),
67
+ ident: ABBBOX_LOADER_IDENT,
68
+ options: {
69
+ rootDir,
70
+ plugins: [[pluginPath, { rootDir }]],
71
+ },
72
+ };
73
+ }
74
+
75
+ function ruleUsesSwc(rule) {
76
+ const uses = rule.use
77
+ ? Array.isArray(rule.use)
78
+ ? rule.use
79
+ : [rule.use]
80
+ : [];
81
+ return uses.some((u) => loaderIdent(u).includes("next-swc-loader"));
82
+ }
83
+
84
+ function ruleHasAbboxLoader(rule) {
85
+ const uses = rule.use
86
+ ? Array.isArray(rule.use)
87
+ ? rule.use
88
+ : [rule.use]
89
+ : [];
90
+ return uses.some((u) => loaderIdent(u) === ABBBOX_LOADER_IDENT);
91
+ }
92
+
93
+ /**
94
+ * @param {unknown[]} rules
95
+ * @param {object} abboxUse
96
+ */
97
+ function patchAppRouterLayerRules(rules, abboxUse) {
98
+ if (!Array.isArray(rules)) return;
99
+ for (const rule of rules) {
100
+ if (!rule || typeof rule !== "object") continue;
101
+ if (Array.isArray(rule.oneOf)) {
102
+ patchAppRouterLayerRules(rule.oneOf, abboxUse);
103
+ }
104
+ const layer = rule.issuerLayer;
105
+ if (
106
+ typeof layer === "string" &&
107
+ APP_CLIENT_ISSUER_LAYERS.has(layer) &&
108
+ ruleUsesSwc(rule) &&
109
+ !ruleHasAbboxLoader(rule)
110
+ ) {
111
+ const uses = Array.isArray(rule.use) ? rule.use : [rule.use];
112
+ rule.use = [...uses, abboxUse];
113
+ }
114
+ }
115
+ }
116
+
44
117
  /**
45
118
  * @param {import('webpack').Configuration} webpackConfig
46
119
  * @param {{ dir: string }} options
@@ -48,35 +121,40 @@ function isRootLayoutResource(resourcePath) {
48
121
  function applyAbboxWebpackInstrumentation(webpackConfig, options) {
49
122
  const rootDir = options.dir ?? process.cwd();
50
123
  const pluginPath = resolveInspectorBabelPlugin(rootDir);
51
- const babelLoader = resolveBabelLoader(rootDir);
52
- const nextBabel = resolveNextBabelPreset(rootDir);
53
124
  const includeDirs = collectIncludeDirs(rootDir);
54
125
 
55
126
  if (includeDirs.length === 0) {
56
127
  return webpackConfig;
57
128
  }
58
129
 
130
+ const abboxUse = createAbboxBabelLoaderUse(pluginPath, rootDir);
131
+
132
+ webpackConfig.module = webpackConfig.module ?? { rules: [] };
133
+ webpackConfig.module.rules = webpackConfig.module.rules ?? [];
134
+
135
+ patchAppRouterLayerRules(webpackConfig.module.rules, abboxUse);
136
+
137
+ // Pages Router and non-layered TSX: top-level rule runs before Next's oneOf chains.
59
138
  const rule = {
60
139
  test: /\.(tsx|jsx)$/,
61
- include: (resourcePath) =>
62
- includeDirs.some((dir) => resourcePath.startsWith(dir)) &&
63
- !isRootLayoutResource(resourcePath),
140
+ include: (resourcePath) => shouldInstrumentResource(resourcePath, rootDir),
64
141
  exclude: [/node_modules/],
65
142
  use: {
66
- loader: babelLoader,
143
+ loader: resolvePreSwcLoader(),
67
144
  options: {
68
- babelrc: false,
69
- configFile: false,
70
- presets: [nextBabel],
145
+ rootDir,
71
146
  plugins: [[pluginPath, { rootDir }]],
72
147
  },
73
148
  },
74
149
  };
75
-
76
- webpackConfig.module = webpackConfig.module ?? { rules: [] };
77
- webpackConfig.module.rules = webpackConfig.module.rules ?? [];
78
150
  webpackConfig.module.rules.unshift(rule);
151
+
79
152
  return webpackConfig;
80
153
  }
81
154
 
82
- module.exports = { applyAbboxWebpackInstrumentation, isRootLayoutResource };
155
+ module.exports = {
156
+ applyAbboxWebpackInstrumentation,
157
+ isRootLayoutResource,
158
+ shouldInstrumentResource,
159
+ APP_CLIENT_ISSUER_LAYERS,
160
+ };
@@ -6,18 +6,23 @@ export type NextConfigLike = {
6
6
  [key: string]: unknown;
7
7
  };
8
8
  export type WithAbboxInspectorOptions = {
9
- /** When false, returns config unchanged (default: ABBBOX_INSPECTOR=1). */
9
+ /** When false, returns config unchanged (default: build gate in preview-env). */
10
10
  enabled?: boolean;
11
11
  /**
12
12
  * Origins allowed to embed this app in an iframe (Abbox Viewer).
13
- * Defaults to common local Viewer ports.
13
+ * Defaults to hosted viewer + common local Viewer ports.
14
14
  */
15
15
  frameAncestors?: string[];
16
+ /**
17
+ * Primary Viewer origin for production-mode postMessage validation.
18
+ * Defaults to the first `frameAncestors` entry.
19
+ */
20
+ viewerOrigin?: string;
16
21
  };
17
22
  export declare const DEFAULT_VIEWER_FRAME_ANCESTORS: string[];
18
23
  /**
19
- * Merge Next.js config for local Abbox Live Preview: iframe embedding headers
20
- * and public env flag for client bootstrap.
24
+ * Merge Next.js config for Abbox inspection: iframe embedding headers
25
+ * and public env flags for client bootstrap.
21
26
  *
22
27
  * JSX instrumentation uses a webpack babel-loader rule (no root babel.config.js),
23
28
  * so `next/font` in app/layout.tsx can keep using SWC.
@@ -1 +1 @@
1
- {"version":3,"file":"with-abbox-inspector.d.ts","sourceRoot":"","sources":["../../src/next/with-abbox-inspector.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,CACR,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B,CAAC;AAIF,eAAO,MAAM,8BAA8B,UAM1C,CAAC;AAOF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,yBAA8B,GACtC,CAAC,CA0EH"}
1
+ {"version":3,"file":"with-abbox-inspector.d.ts","sourceRoot":"","sources":["../../src/next/with-abbox-inspector.ts"],"names":[],"mappings":"AAUA,MAAM,MAAM,cAAc,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IACzC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,CACR,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACpC,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG;IACtC,iFAAiF;IACjF,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAIF,eAAO,MAAM,8BAA8B,UAM1C,CAAC;AAcF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAClC,MAAM,EAAE,CAAC,EACT,OAAO,GAAE,yBAA8B,GACtC,CAAC,CAgFH"}
@@ -14,9 +14,15 @@ function inspectorEnabled(explicit) {
14
14
  return explicit;
15
15
  return isAbboxInspectorBuildEnabled();
16
16
  }
17
+ function bakedInspectionMode() {
18
+ if (process.env.ABBBOX_INSPECTION_MODE === "production") {
19
+ return "production";
20
+ }
21
+ return "preview";
22
+ }
17
23
  /**
18
- * Merge Next.js config for local Abbox Live Preview: iframe embedding headers
19
- * and public env flag for client bootstrap.
24
+ * Merge Next.js config for Abbox inspection: iframe embedding headers
25
+ * and public env flags for client bootstrap.
20
26
  *
21
27
  * JSX instrumentation uses a webpack babel-loader rule (no root babel.config.js),
22
28
  * so `next/font` in app/layout.tsx can keep using SWC.
@@ -28,7 +34,11 @@ export function withAbboxInspector(config, options = {}) {
28
34
  const base = config;
29
35
  const { applyAbboxWebpackInstrumentation } = require("./webpack-instrumentation.cjs");
30
36
  const ancestors = options.frameAncestors ?? DEFAULT_VIEWER_FRAME_ANCESTORS;
37
+ const viewerOrigin = options.viewerOrigin?.replace(/\/$/, "") ??
38
+ ancestors.find((a) => a.startsWith("https://")) ??
39
+ ancestors[0];
31
40
  const csp = `frame-ancestors 'self' ${ancestors.join(" ")}`;
41
+ const mode = bakedInspectionMode();
32
42
  const headers = async () => {
33
43
  const existing = typeof base.headers === "function"
34
44
  ? await base.headers()
@@ -72,6 +82,8 @@ export function withAbboxInspector(config, options = {}) {
72
82
  env: {
73
83
  ...(base.env ?? {}),
74
84
  NEXT_PUBLIC_ABBOX_INSPECTOR: "1",
85
+ NEXT_PUBLIC_ABBOX_INSPECTION_MODE: mode,
86
+ NEXT_PUBLIC_ABBOX_VIEWER_ORIGIN: viewerOrigin,
75
87
  NEXT_PUBLIC_VERCEL_ENV: process.env.VERCEL_ENV ?? "",
76
88
  },
77
89
  headers,
@@ -19,6 +19,8 @@ export type InspectorDisableMessage = InspectorMessageBase & {
19
19
  /** Host asks the preview client to resend INSPECTOR_READY (e.g. after iframe load). */
20
20
  export type InspectorHostHelloMessage = InspectorMessageBase & {
21
21
  type: "INSPECTOR_HOST_HELLO";
22
+ /** Viewer origin that opened the preview (validated by the client in production). */
23
+ hostOrigin: string;
22
24
  };
23
25
  export type ElementRect = {
24
26
  x: number;
@@ -42,6 +44,8 @@ export type ElementMetadata = {
42
44
  role: string | null;
43
45
  ariaLabel: string | null;
44
46
  textPreview: string | null;
47
+ /** Native disabled state when the element supports it (e.g. button, input). */
48
+ disabled: boolean | null;
45
49
  attributes: Record<string, string>;
46
50
  };
47
51
  export type RuntimeElementRef = {
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,eAAO,MAAM,gBAAgB,EAAG,CAAU,CAAC;AAE3C,eAAO,MAAM,wBAAwB,EAAG,iBAA0B,CAAC;AAEnE,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,OAAO,wBAAwB,CAAC;IACzC,OAAO,EAAE,OAAO,gBAAgB,CAAC;IACjC,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IACzD,IAAI,EAAE,iBAAiB,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG;IAC1D,IAAI,EAAE,kBAAkB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,oBAAoB,GAAG;IAC3D,IAAI,EAAE,mBAAmB,CAAC;CAC3B,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D,IAAI,EAAE,sBAAsB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAExE,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,qEAAqE;IACrE,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,UAAU,EAAE,iBAAiB,CAAC;IAC9B,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAC9B,aAAa,EAAE,mBAAmB,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,mBAAmB,CAAC;AAEzD,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IACzD,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,EAAE,mBAAmB,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG,oBAAoB,GAAG;IAC1D,IAAI,EAAE,kBAAkB,CAAC;IACzB,OAAO,EAAE,sBAAsB,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IACzD,IAAI,EAAE,iBAAiB,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG,oBAAoB,GAAG;IACzD,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,YAAY,CAAC;AAErD,2FAA2F;AAC3F,MAAM,MAAM,YAAY,GAAG;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,oBAAoB,GAAG;IAC7D,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,YAAY,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC3B,sBAAsB,GACtB,uBAAuB,GACvB,yBAAyB,GACzB,yBAAyB,CAAC;AAE9B,MAAM,MAAM,mBAAmB,GAC3B,qBAAqB,GACrB,qBAAqB,GACrB,sBAAsB,GACtB,qBAAqB,GACrB,qBAAqB,CAAC;AAE1B,MAAM,MAAM,gBAAgB,GAAG,mBAAmB,GAAG,mBAAmB,CAAC;AAEzE,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAS5E"}
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { InspectorHostHelloMessage } from "../protocol.js";
2
+ export declare function parseValidatedHostHello(data: unknown, eventOrigin: string, allowedViewerOrigins: readonly string[]): InspectorHostHelloMessage | null;
3
+ //# sourceMappingURL=bootstrap-standby.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-standby.d.ts","sourceRoot":"","sources":["../../src/runtime/bootstrap-standby.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,gBAAgB,CAAC;AAGhE,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,MAAM,EACnB,oBAAoB,EAAE,SAAS,MAAM,EAAE,GACtC,yBAAyB,GAAG,IAAI,CAkBlC"}
@@ -0,0 +1,20 @@
1
+ import { withLocalhostAliases } from "../host/origin-aliases.js";
2
+ import { getBakedViewerOrigin } from "../next/preview-env.js";
3
+ import { isInspectorMessage } from "../protocol.js";
4
+ export function parseValidatedHostHello(data, eventOrigin, allowedViewerOrigins) {
5
+ if (!allowedViewerOrigins.includes(eventOrigin))
6
+ return null;
7
+ if (!isInspectorMessage(data) || data.type !== "INSPECTOR_HOST_HELLO") {
8
+ return null;
9
+ }
10
+ const hello = data;
11
+ if (!hello.hostOrigin?.trim())
12
+ return null;
13
+ const bakedViewer = getBakedViewerOrigin();
14
+ const allowedHostOrigins = withLocalhostAliases(bakedViewer ? [bakedViewer] : allowedViewerOrigins);
15
+ const normalizedHost = hello.hostOrigin.replace(/\/$/, "");
16
+ const hostOk = allowedHostOrigins.some((o) => o.replace(/\/$/, "") === normalizedHost);
17
+ if (!hostOk)
18
+ return null;
19
+ return hello;
20
+ }
@@ -3,8 +3,11 @@ export type BootstrapOptions = {
3
3
  allowedHostOrigins?: string[];
4
4
  };
5
5
  /**
6
- * Entry for instrumented preview apps. Activates when `abboxSession` is in the
7
- * URL on authorized preview deployments (Vercel preview or local `dev:abbox`).
6
+ * Entry for instrumented apps. Activates when the build baked an authorized
7
+ * inspection mode and either URL session params are present or (production only)
8
+ * a validated INSPECTOR_HOST_HELLO arrives from the Viewer.
8
9
  */
9
10
  export declare function bootstrapInspectorPreview(options?: BootstrapOptions): void;
11
+ /** Test-only reset for vitest isolation. */
12
+ export declare function resetInspectorBootstrapForTests(): void;
10
13
  //# sourceMappingURL=bootstrap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/runtime/bootstrap.ts"],"names":[],"mappings":"AAsBA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B,CAAC;AAEF;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gBAAqB,GAC7B,IAAI,CAyBN"}
1
+ {"version":3,"file":"bootstrap.d.ts","sourceRoot":"","sources":["../../src/runtime/bootstrap.ts"],"names":[],"mappings":"AAuCA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B,CAAC;AA8CF;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gBAAqB,GAC7B,IAAI,CAyCN;AAED,4CAA4C;AAC5C,wBAAgB,+BAA+B,IAAI,IAAI,CAMtD"}
@@ -1,7 +1,8 @@
1
+ import { createInspectorClient, } from "../host/client-bridge.js";
1
2
  import { withLocalhostAliases } from "../host/origin-aliases.js";
2
- import { createInspectorClient } from "../host/client-bridge.js";
3
- import { isAbboxInspectorRuntimeEnabled } from "../next/preview-env.js";
4
- /** Viewer origins used when `abboxHostOrigin` is missing from the iframe URL. */
3
+ import { getBakedViewerOrigin, isAbboxInspectorProductionRuntime, isAbboxInspectorRuntimeEnabled, } from "../next/preview-env.js";
4
+ import { parseValidatedHostHello } from "./bootstrap-standby.js";
5
+ /** Viewer origins used when `abboxHostOrigin` is missing (preview mode only). */
5
6
  const DEFAULT_VIEWER_ORIGINS = [
6
7
  "https://viewer.abbox.com",
7
8
  "http://localhost:5173",
@@ -19,31 +20,94 @@ function viewerOriginsFromReferrer() {
19
20
  return [];
20
21
  }
21
22
  }
23
+ function normalizeOrigin(origin) {
24
+ return origin.replace(/\/$/, "");
25
+ }
26
+ function isAllowedHostOrigin(hostOrigin, allowed) {
27
+ const normalized = normalizeOrigin(hostOrigin);
28
+ return allowed.some((o) => normalizeOrigin(o) === normalized);
29
+ }
30
+ let activeClient = null;
31
+ let standbyMessageListener = null;
32
+ function allowedViewerOriginsForStandby() {
33
+ const bakedViewer = getBakedViewerOrigin();
34
+ return withLocalhostAliases(bakedViewer ? [bakedViewer] : DEFAULT_VIEWER_ORIGINS);
35
+ }
36
+ function removeStandbyListener(win) {
37
+ if (!standbyMessageListener)
38
+ return;
39
+ win.removeEventListener("message", standbyMessageListener);
40
+ standbyMessageListener = null;
41
+ }
42
+ function startInspectorClient(sessionId, allowedHostOrigins) {
43
+ if (activeClient || typeof window === "undefined")
44
+ return;
45
+ removeStandbyListener(window);
46
+ activeClient = createInspectorClient({
47
+ sessionId,
48
+ allowedHostOrigins,
49
+ });
50
+ activeClient.start();
51
+ }
52
+ function installStandbySessionListener(win) {
53
+ if (standbyMessageListener)
54
+ return;
55
+ const allowedViewerOrigins = allowedViewerOriginsForStandby();
56
+ standbyMessageListener = (event) => {
57
+ const hello = parseValidatedHostHello(event.data, event.origin, allowedViewerOrigins);
58
+ if (!hello)
59
+ return;
60
+ startInspectorClient(hello.sessionId, withLocalhostAliases([hello.hostOrigin]));
61
+ };
62
+ win.addEventListener("message", standbyMessageListener);
63
+ }
22
64
  /**
23
- * Entry for instrumented preview apps. Activates when `abboxSession` is in the
24
- * URL on authorized preview deployments (Vercel preview or local `dev:abbox`).
65
+ * Entry for instrumented apps. Activates when the build baked an authorized
66
+ * inspection mode and either URL session params are present or (production only)
67
+ * a validated INSPECTOR_HOST_HELLO arrives from the Viewer.
25
68
  */
26
69
  export function bootstrapInspectorPreview(options = {}) {
27
70
  if (typeof window === "undefined")
28
71
  return;
29
72
  if (!isAbboxInspectorRuntimeEnabled())
30
73
  return;
31
- const params = new URLSearchParams(window.location.search);
74
+ const win = window;
75
+ const params = new URLSearchParams(win.location.search);
32
76
  const sessionFromUrl = params.get("abboxSession");
33
- if (!sessionFromUrl)
34
- return;
35
- const sessionId = options.sessionId ?? sessionFromUrl ?? "local-dev";
36
77
  const hostParam = params.get("abboxHostOrigin");
37
- const allowedHostOrigins = withLocalhostAliases(options.allowedHostOrigins ??
38
- (hostParam
39
- ? [hostParam]
40
- : [
41
- ...viewerOriginsFromReferrer(),
42
- ...DEFAULT_VIEWER_ORIGINS,
43
- ]));
44
- const client = createInspectorClient({
45
- sessionId,
46
- allowedHostOrigins,
47
- });
48
- client.start();
78
+ if (!sessionFromUrl) {
79
+ if (isAbboxInspectorProductionRuntime()) {
80
+ installStandbySessionListener(win);
81
+ }
82
+ return;
83
+ }
84
+ const sessionId = options.sessionId ?? sessionFromUrl;
85
+ let allowedHostOrigins;
86
+ if (isAbboxInspectorProductionRuntime()) {
87
+ if (!hostParam) {
88
+ installStandbySessionListener(win);
89
+ return;
90
+ }
91
+ const bakedViewer = getBakedViewerOrigin();
92
+ const allowed = withLocalhostAliases(options.allowedHostOrigins ??
93
+ (bakedViewer ? [bakedViewer] : DEFAULT_VIEWER_ORIGINS));
94
+ if (!isAllowedHostOrigin(hostParam, allowed))
95
+ return;
96
+ allowedHostOrigins = withLocalhostAliases([hostParam]);
97
+ }
98
+ else {
99
+ allowedHostOrigins = withLocalhostAliases(options.allowedHostOrigins ??
100
+ (hostParam
101
+ ? [hostParam]
102
+ : [...viewerOriginsFromReferrer(), ...DEFAULT_VIEWER_ORIGINS]));
103
+ }
104
+ startInspectorClient(sessionId, allowedHostOrigins);
105
+ }
106
+ /** Test-only reset for vitest isolation. */
107
+ export function resetInspectorBootstrapForTests() {
108
+ if (typeof window !== "undefined") {
109
+ removeStandbyListener(window);
110
+ }
111
+ activeClient?.stop();
112
+ activeClient = null;
49
113
  }
@@ -1 +1 @@
1
- {"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../../src/runtime/inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAgBxE,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,gBAAgB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACpD,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACrD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CAoIlB"}
1
+ {"version":3,"file":"inspector.d.ts","sourceRoot":"","sources":["../../src/runtime/inspector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAgBxE,MAAM,MAAM,uBAAuB,GAAG;IACpC,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACjD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAClD,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,OAAO,CAAC;IACzB,gBAAgB,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;IACpD,iBAAiB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC;IACrD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,gBAAgB,CAsIlB"}
@@ -12,7 +12,7 @@ export function createInspectorRuntime(options) {
12
12
  let pendingTarget = null;
13
13
  const teardownListeners = () => {
14
14
  win.removeEventListener("mousemove", onMove, true);
15
- win.removeEventListener("click", onClickCapture, true);
15
+ win.removeEventListener("pointerdown", onPointerDownCapture, true);
16
16
  win.removeEventListener("scroll", onScrollOrResize, true);
17
17
  win.removeEventListener("resize", onScrollOrResize);
18
18
  };
@@ -33,9 +33,12 @@ export function createInspectorRuntime(options) {
33
33
  pendingTarget = null;
34
34
  });
35
35
  };
36
- const onClickCapture = (event) => {
36
+ /** Disabled controls do not dispatch `click`; use pointer + hit-testing. */
37
+ const onPointerDownCapture = (event) => {
37
38
  if (!enabled)
38
39
  return;
40
+ if (event.button !== 0)
41
+ return;
39
42
  event.preventDefault();
40
43
  event.stopPropagation();
41
44
  const target = deepestElementAt(doc, event.clientX, event.clientY, event.target);
@@ -86,7 +89,7 @@ export function createInspectorRuntime(options) {
86
89
  return;
87
90
  enabled = true;
88
91
  win.addEventListener("mousemove", onMove, true);
89
- win.addEventListener("click", onClickCapture, true);
92
+ win.addEventListener("pointerdown", onPointerDownCapture, true);
90
93
  win.addEventListener("scroll", onScrollOrResize, true);
91
94
  win.addEventListener("resize", onScrollOrResize);
92
95
  },
@@ -1 +1 @@
1
- {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/runtime/metadata.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAYxB,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAarE;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,GAAG,IAAI,GAC5B,mBAAmB,CAGrB;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,eAAe,CAgC5D;AAcD,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAGzE"}
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../../src/runtime/metadata.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,cAAc,EACd,mBAAmB,EACpB,MAAM,gBAAgB,CAAC;AAYxB,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,OAAO,GAAG,cAAc,GAAG,IAAI,CAarE;AAED,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,cAAc,GAAG,IAAI,GAC5B,mBAAmB,CAGrB;AAED,wBAAgB,eAAe,CAAC,EAAE,EAAE,OAAO,GAAG,eAAe,CAiC5D;AA+BD,wBAAgB,YAAY,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAGzE"}
@@ -58,9 +58,24 @@ export function extractMetadata(el) {
58
58
  role,
59
59
  ariaLabel,
60
60
  textPreview,
61
+ disabled: readDisabledState(el),
61
62
  attributes,
62
63
  };
63
64
  }
65
+ function readDisabledState(el) {
66
+ if (el instanceof HTMLButtonElement ||
67
+ el instanceof HTMLInputElement ||
68
+ el instanceof HTMLSelectElement ||
69
+ el instanceof HTMLOptionElement ||
70
+ el instanceof HTMLTextAreaElement ||
71
+ el instanceof HTMLFieldSetElement) {
72
+ return el.disabled;
73
+ }
74
+ if (el instanceof HTMLElement && el.hasAttribute("disabled")) {
75
+ return true;
76
+ }
77
+ return null;
78
+ }
64
79
  function textPreviewFor(el) {
65
80
  const own = Array.from(el.childNodes)
66
81
  .filter((n) => n.nodeType === Node.TEXT_NODE)
package/docs/TRUST.md CHANGED
@@ -1,18 +1,25 @@
1
- # Trust model (Live Preview)
1
+ # Trust model (Live Preview & production inspection)
2
2
 
3
3
  ## What is protected
4
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.
5
+ - **Default (preview-only install)**: `VERCEL_ENV=production` deploys have no JSX instrumentation, no `frame-ancestors` for the viewer, and client bootstrap does not run.
6
+ - **Preview deployments** (`VERCEL_ENV=preview`): instrumentation and iframe embedding headers at build time; runtime follows baked preview mode.
7
+ - **Production inspection (explicit opt-in via `@abbox/cli init --mode production`)**: production Vercel builds use webpack instrumentation and bake `NEXT_PUBLIC_ABBOX_INSPECTION_MODE=production`. Runtime still requires a baked mode flag — query parameters alone cannot enable inspection.
8
+ - **Production runtime**: requires `abboxSession` **and** `abboxHostOrigin` matching the Viewer origin configured at install (`NEXT_PUBLIC_ABBOX_VIEWER_ORIGIN`). No broad Viewer fallback list.
7
9
  - **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`).
10
+ - **Viewer**: preview targets use a host allow-list; production website mode accepts customer HTTPS origins (customer must have opted in on their site).
9
11
 
10
12
  ## What is not authentication
11
13
 
12
14
  - `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
+ - `abboxSession` is a protocol correlation id generated by the Viewer, not proof that the Viewer user owns the embedded site.
16
+ - **Production inspection opt-in** means the site owner deliberately ships instrumentation and `frame-ancestors` allowing the configured Viewer. That is explicit capability exposure, not cryptographic proof of ownership.
17
+ - Abbox does not receive page contents or credentials in this milestone; inspection stays between the Viewer and the customer origin via `postMessage`.
15
18
 
16
19
  ## CSP
17
20
 
18
21
  `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.
22
+
23
+ ## Server-side authorization
24
+
25
+ This milestone does **not** include a backend that verifies domain ownership. Customers who need stronger guarantees should treat production inspection as a controlled internal tool or wait for a future domain-verification flow.
@@ -28,7 +28,8 @@ npm run dev:abbox
28
28
  ## Vercel
29
29
 
30
30
  - **Preview**: `VERCEL_ENV=preview` → `next build --webpack` + inspector headers/runtime.
31
- - **Production**: default `next build` (Turbopack) without instrumentation.
31
+ - **Production (inspectable)**: use `@abbox/cli` production mode (`inspectionMode: "production"` in `.abbox/install.json`) so the build script sets `ABBBOX_INSPECTION_MODE=production` and runs `next build --webpack`. App Router client layers are instrumented **before** SWC via a dedicated babel-loader step.
32
+ - **Production (default)**: plain `next build` without Abbox env → no instrumentation (by design).
32
33
 
33
34
  ## Trust
34
35
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abbox/inspector",
3
- "version": "0.2.1",
3
+ "version": "0.4.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -52,11 +52,12 @@
52
52
  "dist",
53
53
  "src/next/babel-config.cjs",
54
54
  "src/next/webpack-instrumentation.cjs",
55
+ "src/next/abbox-pre-swc-loader.cjs",
55
56
  "docs",
56
57
  "README.md"
57
58
  ],
58
59
  "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
+ "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/abbox-pre-swc-loader.cjs',p+'/abbox-pre-swc-loader.cjs');fs.copyFileSync('src/next/babel-config.cjs',p+'/babel-config.cjs');\"",
60
61
  "typecheck": "tsc -p tsconfig.json --noEmit",
61
62
  "test": "npm run build && vitest run",
62
63
  "lint": "biome ci .",
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Runs Abbox JSX instrumentation on TSX/JSX before Next's SWC pass.
3
+ * No-ops for paths outside configured app source directories.
4
+ */
5
+ const babel = require("@babel/core");
6
+ const { shouldInstrumentResource } = require("./webpack-instrumentation.cjs");
7
+
8
+ /**
9
+ * @this {import('webpack').LoaderContext<unknown>}
10
+ */
11
+ module.exports = function abboxPreSwcLoader(source) {
12
+ const callback = this.async();
13
+ const options = this.getOptions();
14
+ const rootDir = options.rootDir ?? this.rootContext;
15
+ const resourcePath = this.resourcePath;
16
+
17
+ if (!shouldInstrumentResource(resourcePath, rootDir)) {
18
+ callback(null, source);
19
+ return;
20
+ }
21
+
22
+ babel.transform(
23
+ source,
24
+ {
25
+ filename: resourcePath,
26
+ babelrc: false,
27
+ configFile: false,
28
+ parserOpts: { plugins: ["jsx", "typescript"] },
29
+ plugins: options.plugins,
30
+ sourceMaps: this.sourceMap,
31
+ },
32
+ (error, result) => {
33
+ if (error) {
34
+ callback(error);
35
+ return;
36
+ }
37
+ if (!result?.code) {
38
+ callback(null, source);
39
+ return;
40
+ }
41
+ callback(null, result.code, result.map);
42
+ },
43
+ );
44
+ };
@@ -10,6 +10,7 @@ function createNextBabelConfig(options = {}) {
10
10
  const enabled =
11
11
  options.enabled ??
12
12
  (process.env.ABBBOX_INSPECTOR === "1" ||
13
+ process.env.ABBBOX_INSPECTION_MODE === "production" ||
13
14
  process.env.NEXT_PUBLIC_ABBOX_INSPECTOR === "1" ||
14
15
  process.env.VERCEL_ENV === "preview");
15
16
 
@@ -2,6 +2,10 @@
2
2
  * Apply Abbox JSX instrumentation via webpack + babel-loader on selected app sources.
3
3
  * Does not use babel.config.js, so Next.js keeps SWC (and next/font) for excluded files
4
4
  * such as app/layout.tsx.
5
+ *
6
+ * Next.js App Router client layers compile with SWC before the default Babel pass, so a
7
+ * top-level babel rule is not enough. We prepend an Abbox babel-loader step on the App
8
+ * Router browser/SSR layer rules (before SWC runs).
5
9
  */
6
10
  const fs = require("node:fs");
7
11
  const path = require("node:path");
@@ -9,6 +13,11 @@ const { createRequire } = require("node:module");
9
13
 
10
14
  const LAYOUT_BASENAMES = new Set(["layout.tsx", "layout.jsx"]);
11
15
 
16
+ /** Layers where Next compiles client/app TSX through SWC (must instrument before SWC). */
17
+ const APP_CLIENT_ISSUER_LAYERS = new Set(["app-pages-browser", "ssr"]);
18
+
19
+ const ABBBOX_LOADER_IDENT = "abbox-source-instrumentation";
20
+
12
21
  function resolveInspectorBabelPlugin(rootDir) {
13
22
  const req = createRequire(path.join(rootDir, "package.json"));
14
23
  try {
@@ -18,29 +27,93 @@ function resolveInspectorBabelPlugin(rootDir) {
18
27
  }
19
28
  }
20
29
 
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
30
  function collectIncludeDirs(rootDir) {
32
- return ["app", "components", "src", "pages"]
31
+ return ["app", "components", "src", "pages", "lib", "hooks", "features"]
33
32
  .map((segment) => path.join(rootDir, segment))
34
33
  .filter((dir) => fs.existsSync(dir));
35
34
  }
36
35
 
37
36
  function isRootLayoutResource(resourcePath) {
38
- const base = path.basename(resourcePath);
37
+ const clean = resourcePath.split("?")[0];
38
+ const base = path.basename(clean);
39
39
  if (!LAYOUT_BASENAMES.has(base)) return false;
40
- const parent = path.basename(path.dirname(resourcePath));
40
+ const parent = path.basename(path.dirname(clean));
41
41
  return parent === "app" || parent === "src/app";
42
42
  }
43
43
 
44
+ function shouldInstrumentResource(resourcePath, rootDir) {
45
+ const clean = resourcePath.split("?")[0];
46
+ const includeDirs = collectIncludeDirs(rootDir);
47
+ if (!/\.(tsx|jsx)$/.test(clean)) return false;
48
+ return (
49
+ includeDirs.some((dir) => clean.startsWith(dir)) &&
50
+ !isRootLayoutResource(clean)
51
+ );
52
+ }
53
+
54
+ function loaderIdent(entry) {
55
+ if (!entry) return "";
56
+ if (typeof entry === "string") return entry;
57
+ return String(entry.loader ?? entry.ident ?? "");
58
+ }
59
+
60
+ function resolvePreSwcLoader() {
61
+ return path.join(__dirname, "abbox-pre-swc-loader.cjs");
62
+ }
63
+
64
+ function createAbboxBabelLoaderUse(pluginPath, rootDir) {
65
+ return {
66
+ loader: resolvePreSwcLoader(),
67
+ ident: ABBBOX_LOADER_IDENT,
68
+ options: {
69
+ rootDir,
70
+ plugins: [[pluginPath, { rootDir }]],
71
+ },
72
+ };
73
+ }
74
+
75
+ function ruleUsesSwc(rule) {
76
+ const uses = rule.use
77
+ ? Array.isArray(rule.use)
78
+ ? rule.use
79
+ : [rule.use]
80
+ : [];
81
+ return uses.some((u) => loaderIdent(u).includes("next-swc-loader"));
82
+ }
83
+
84
+ function ruleHasAbboxLoader(rule) {
85
+ const uses = rule.use
86
+ ? Array.isArray(rule.use)
87
+ ? rule.use
88
+ : [rule.use]
89
+ : [];
90
+ return uses.some((u) => loaderIdent(u) === ABBBOX_LOADER_IDENT);
91
+ }
92
+
93
+ /**
94
+ * @param {unknown[]} rules
95
+ * @param {object} abboxUse
96
+ */
97
+ function patchAppRouterLayerRules(rules, abboxUse) {
98
+ if (!Array.isArray(rules)) return;
99
+ for (const rule of rules) {
100
+ if (!rule || typeof rule !== "object") continue;
101
+ if (Array.isArray(rule.oneOf)) {
102
+ patchAppRouterLayerRules(rule.oneOf, abboxUse);
103
+ }
104
+ const layer = rule.issuerLayer;
105
+ if (
106
+ typeof layer === "string" &&
107
+ APP_CLIENT_ISSUER_LAYERS.has(layer) &&
108
+ ruleUsesSwc(rule) &&
109
+ !ruleHasAbboxLoader(rule)
110
+ ) {
111
+ const uses = Array.isArray(rule.use) ? rule.use : [rule.use];
112
+ rule.use = [...uses, abboxUse];
113
+ }
114
+ }
115
+ }
116
+
44
117
  /**
45
118
  * @param {import('webpack').Configuration} webpackConfig
46
119
  * @param {{ dir: string }} options
@@ -48,35 +121,40 @@ function isRootLayoutResource(resourcePath) {
48
121
  function applyAbboxWebpackInstrumentation(webpackConfig, options) {
49
122
  const rootDir = options.dir ?? process.cwd();
50
123
  const pluginPath = resolveInspectorBabelPlugin(rootDir);
51
- const babelLoader = resolveBabelLoader(rootDir);
52
- const nextBabel = resolveNextBabelPreset(rootDir);
53
124
  const includeDirs = collectIncludeDirs(rootDir);
54
125
 
55
126
  if (includeDirs.length === 0) {
56
127
  return webpackConfig;
57
128
  }
58
129
 
130
+ const abboxUse = createAbboxBabelLoaderUse(pluginPath, rootDir);
131
+
132
+ webpackConfig.module = webpackConfig.module ?? { rules: [] };
133
+ webpackConfig.module.rules = webpackConfig.module.rules ?? [];
134
+
135
+ patchAppRouterLayerRules(webpackConfig.module.rules, abboxUse);
136
+
137
+ // Pages Router and non-layered TSX: top-level rule runs before Next's oneOf chains.
59
138
  const rule = {
60
139
  test: /\.(tsx|jsx)$/,
61
- include: (resourcePath) =>
62
- includeDirs.some((dir) => resourcePath.startsWith(dir)) &&
63
- !isRootLayoutResource(resourcePath),
140
+ include: (resourcePath) => shouldInstrumentResource(resourcePath, rootDir),
64
141
  exclude: [/node_modules/],
65
142
  use: {
66
- loader: babelLoader,
143
+ loader: resolvePreSwcLoader(),
67
144
  options: {
68
- babelrc: false,
69
- configFile: false,
70
- presets: [nextBabel],
145
+ rootDir,
71
146
  plugins: [[pluginPath, { rootDir }]],
72
147
  },
73
148
  },
74
149
  };
75
-
76
- webpackConfig.module = webpackConfig.module ?? { rules: [] };
77
- webpackConfig.module.rules = webpackConfig.module.rules ?? [];
78
150
  webpackConfig.module.rules.unshift(rule);
151
+
79
152
  return webpackConfig;
80
153
  }
81
154
 
82
- module.exports = { applyAbboxWebpackInstrumentation, isRootLayoutResource };
155
+ module.exports = {
156
+ applyAbboxWebpackInstrumentation,
157
+ isRootLayoutResource,
158
+ shouldInstrumentResource,
159
+ APP_CLIENT_ISSUER_LAYERS,
160
+ };