From 70b07aa14ca4fd34a272da981f8c1ef038e7b710 Mon Sep 17 00:00:00 2001 From: SweetRetry Date: Wed, 19 Aug 2026 20:22:47 +0800 Subject: [PATCH 1/2] Add production source location plugins Add opt-in Next.js and Vite build integrations backed by a compact source manifest. Resolve instrumented DOM elements in the Agentation runtime and cover workspace, binding, and loader-order behavior. --- package.json | 1 + .../source-location.instrumentation.test.ts | 44 ++++ package/src/utils/source-location.ts | 58 +++++ plugins/source-locations/README.md | 83 +++++++ plugins/source-locations/core.cjs | 230 ++++++++++++++++++ plugins/source-locations/core.test.cjs | 70 ++++++ plugins/source-locations/next.cjs | 80 ++++++ plugins/source-locations/package.json | 26 ++ plugins/source-locations/plugin.test.cjs | 62 +++++ plugins/source-locations/turbopack-loader.cjs | 25 ++ plugins/source-locations/vite.cjs | 102 ++++++++ pnpm-lock.yaml | 6 + pnpm-workspace.yaml | 1 + 13 files changed, 788 insertions(+) create mode 100644 package/src/utils/source-location.instrumentation.test.ts create mode 100644 plugins/source-locations/README.md create mode 100644 plugins/source-locations/core.cjs create mode 100644 plugins/source-locations/core.test.cjs create mode 100644 plugins/source-locations/next.cjs create mode 100644 plugins/source-locations/package.json create mode 100644 plugins/source-locations/plugin.test.cjs create mode 100644 plugins/source-locations/turbopack-loader.cjs create mode 100644 plugins/source-locations/vite.cjs diff --git a/package.json b/package.json index 397c027f..f281d8ea 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "dev": "pnpm --filter agentation watch & pnpm --filter feedback-tool-example dev", "build": "pnpm --filter agentation build", + "test": "pnpm --filter agentation test && pnpm --filter @agentation/source-locations test", "example": "pnpm --filter feedback-tool-example dev", "pack": "cd package && pnpm pack", "mcp": "pnpm --filter agentation-mcp start", diff --git a/package/src/utils/source-location.instrumentation.test.ts b/package/src/utils/source-location.instrumentation.test.ts new file mode 100644 index 00000000..3de7588a --- /dev/null +++ b/package/src/utils/source-location.instrumentation.test.ts @@ -0,0 +1,44 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { getSourceLocation } from "./source-location"; + +describe("build-time source location instrumentation", () => { + afterEach(() => { + delete globalThis.__AGENTATION_SOURCE_MANIFEST__; + }); + + it("resolves an exact source location without a React fiber", () => { + const element = document.createElement("button"); + element.setAttribute("data-agentation-id", "source-id"); + globalThis.__AGENTATION_SOURCE_MANIFEST__ = { + version: 1, + files: ["src/app/Inspector.tsx"], + locations: { "source-id": [0, 24, 6] }, + }; + + expect(getSourceLocation(element)).toEqual({ + found: true, + source: { + fileName: "src/app/Inspector.tsx", + lineNumber: 24, + columnNumber: 6, + }, + isReactApp: true, + isProduction: false, + }); + }); + + it("falls back when the manifest does not contain the injected id", () => { + const element = document.createElement("button"); + element.setAttribute("data-agentation-id", "missing-id"); + globalThis.__AGENTATION_SOURCE_MANIFEST__ = { + version: 1, + files: [], + locations: {}, + }; + + expect(getSourceLocation(element)).toMatchObject({ + found: false, + reason: "no-fiber", + }); + }); +}); diff --git a/package/src/utils/source-location.ts b/package/src/utils/source-location.ts index d23b3309..b5578f9a 100644 --- a/package/src/utils/source-location.ts +++ b/package/src/utils/source-location.ts @@ -36,6 +36,20 @@ export interface SourceLocation { reactVersion?: string; } +/** Compact build-time source location tuple: file index, line, column. */ +export type SourceLocationTuple = [number, number, number]; + +/** Build-time instrumentation manifest used by `data-agentation-id`. */ +export interface SourceLocationManifest { + version: 1; + files: string[]; + locations: Record; +} + +declare global { + var __AGENTATION_SOURCE_MANIFEST__: SourceLocationManifest | undefined; +} + /** * Result of source location detection */ @@ -114,6 +128,40 @@ interface ReactDOMElement extends HTMLElement { _reactRootContainer?: unknown; } +const SOURCE_LOCATION_ATTRIBUTE = "data-agentation-id"; + +/** + * Resolve source metadata injected by an Agentation build integration. + * This is intentionally checked before React internals because it remains + * stable in production builds and identifies the exact JSX host element. + */ +function findInstrumentedSource(element: HTMLElement): SourceLocation | null { + const sourceId = element.getAttribute(SOURCE_LOCATION_ATTRIBUTE); + if (!sourceId) return null; + + const manifest = globalThis.__AGENTATION_SOURCE_MANIFEST__; + if (manifest?.version !== 1) return null; + + const source = manifest.locations[sourceId]; + if ( + !source || + typeof source[0] !== "number" || + typeof source[1] !== "number" || + typeof source[2] !== "number" + ) { + return null; + } + + const fileName = manifest.files[source[0]]; + if (typeof fileName !== "string") return null; + + return { + fileName, + lineNumber: source[1], + columnNumber: source[2], + }; +} + // React fiber tag constants (for reference) const FIBER_TAGS = { FunctionComponent: 0, @@ -671,6 +719,16 @@ function probeSourceWalk( * ``` */ export function getSourceLocation(element: HTMLElement): SourceLocationResult { + const instrumentedSource = findInstrumentedSource(element); + if (instrumentedSource) { + return { + found: true, + source: instrumentedSource, + isReactApp: true, + isProduction: false, + }; + } + // Try to get fiber directly from the element (same approach as getReactComponentName) // This avoids detectReactApp() whose production heuristic can give false positives const fiber = getFiberFromElement(element); diff --git a/plugins/source-locations/README.md b/plugins/source-locations/README.md new file mode 100644 index 00000000..d114e772 --- /dev/null +++ b/plugins/source-locations/README.md @@ -0,0 +1,83 @@ +# Agentation source locations + +This first-party plugin lives in the Agentation monorepo because its manifest +format is consumed directly by the Agentation runtime and must evolve atomically +with that lookup contract. + +Build-time JSX instrumentation for exact production source locations. It adds a +short `data-agentation-id` to rendered DOM and generates a manifest that maps the +ID back to `file:line:column` without shipping source code. + +The manifest stores file paths once and represents each location as a compact +`[fileIndex, line, column]` tuple. + +Instrumentation is enabled for production builds only by default. Development +servers stay unchanged. Pass `enabled: true` only when development instrumentation +is intentional. + +## Next.js 16 (Turbopack) + +Single-project repositories use `src` and `public` by default: + +```js +const { withAgentationSourceLocations } = require("@agentation/source-locations/next"); + +module.exports = withAgentationSourceLocations({}); +``` + +For a monorepo, distinguish the application directory that owns `public` from the +repository root used in reported source paths: + +```js +const path = require("node:path"); +const { withAgentationSourceLocations } = require("@agentation/source-locations/next"); + +module.exports = withAgentationSourceLocations(nextConfig, { + projectDir: __dirname, + rootDir: path.resolve(__dirname, "../.."), + sourceDirs: ["apps/web/src", "packages/ui/src"], +}); +``` + +Load the generated manifest before Agentation in the production document: + +```tsx +import Script from "next/script"; + +export default function RootLayout({ children }) { + return ( + + +