@pinagent/react-native 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -0
- package/dist/babel.cjs +130 -0
- package/dist/babel.cjs.map +1 -0
- package/dist/babel.d.cts +16 -0
- package/dist/babel.d.cts.map +1 -0
- package/dist/babel.d.ts +16 -0
- package/dist/babel.d.ts.map +1 -0
- package/dist/babel.js +130 -0
- package/dist/babel.js.map +1 -0
- package/dist/server.cjs +4684 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +109 -0
- package/dist/server.d.cts.map +1 -0
- package/dist/server.d.ts +110 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +4681 -0
- package/dist/server.js.map +1 -0
- package/package.json +83 -0
- package/src/native/Pinagent.tsx +703 -0
- package/src/native/StreamSheet.tsx +426 -0
- package/src/native/index.ts +9 -0
- package/src/native/inspector.ts +407 -0
- package/src/native/multi-pick.ts +74 -0
- package/src/native/restore.ts +91 -0
- package/src/native/screenshot.ts +34 -0
- package/src/native/submit-outcome.ts +70 -0
- package/src/native/transcript.ts +143 -0
- package/src/native/transport.ts +162 -0
- package/src/native/types.ts +95 -0
- package/src/native/ws-client.ts +173 -0
package/README.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# @pinagent/react-native
|
|
2
|
+
|
|
3
|
+
Tap a view → leave a comment → an agent fixes it, for **React Native &
|
|
4
|
+
Expo**. The RN port of the Pinagent click-to-comment loop.
|
|
5
|
+
|
|
6
|
+
The backend is **unchanged** from the web version — same
|
|
7
|
+
`.pinagent/db.sqlite`, same `Storage`, same `spawnAgent`, same
|
|
8
|
+
`@pinagent/mcp` pull mode. Only two pieces are RN-specific: a
|
|
9
|
+
`<Pinagent/>` widget you mount at your app root, and a Metro dev-server
|
|
10
|
+
middleware. Full design + web→RN mapping:
|
|
11
|
+
[`docs/architecture/react-native.md`](../../docs/architecture/react-native.md).
|
|
12
|
+
|
|
13
|
+
## Package layout (hybrid)
|
|
14
|
+
|
|
15
|
+
| Path | Built? | Typechecked here? | Notes |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| `src/server/` | yes → `dist/server.*` | yes | pure Node; the Metro middleware |
|
|
18
|
+
| `src/native/` | no — ships as source | no (needs RN types) | the RN widget; Metro transpiles it |
|
|
19
|
+
| `example/` | — | — | standalone Expo app, **not** a workspace member |
|
|
20
|
+
|
|
21
|
+
`react` / `react-native` / `react-native-view-shot` are **optional** peer
|
|
22
|
+
deps so the monorepo install stays green; a real consumer must provide
|
|
23
|
+
them. See [How the package is split](../../docs/architecture/react-native.md#how-the-package-is-split).
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Expo
|
|
29
|
+
npm i @pinagent/react-native
|
|
30
|
+
npx expo install react-native-view-shot # optional screenshots
|
|
31
|
+
|
|
32
|
+
# Bare React Native
|
|
33
|
+
npm i @pinagent/react-native react-native-view-shot
|
|
34
|
+
(cd ios && pod install) # iOS native part of view-shot
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use your app's package manager (npm / yarn / pnpm / bun). Then **add the
|
|
38
|
+
Babel source-tagging plugin** — on React 19 / RN 0.81+ this is what makes a
|
|
39
|
+
tap resolve to `file:line` (without it the picker shows "Unknown component"):
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// babel.config.js — dev only, before the preset's JSX transform
|
|
43
|
+
const pinagentSource = require('@pinagent/react-native/babel').default;
|
|
44
|
+
const dev = process.env.NODE_ENV !== 'production';
|
|
45
|
+
|
|
46
|
+
module.exports = (api) => {
|
|
47
|
+
api.cache(true);
|
|
48
|
+
return {
|
|
49
|
+
presets: ['babel-preset-expo'], // bare RN: 'module:@react-native/babel-preset'
|
|
50
|
+
plugins: dev ? [pinagentSource] : [],
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart Metro with a cleared cache after editing Babel config (`expo start -c`
|
|
56
|
+
or `npm start -- --reset-cache`). Full setup — bare RN and monorepo included —
|
|
57
|
+
is in the [`pinagent-setup` skill](https://github.com/Pinagent/pinagent/blob/main/.claude/skills/pinagent-setup/react-native.md).
|
|
58
|
+
|
|
59
|
+
## Usage
|
|
60
|
+
|
|
61
|
+
**Client** — mount once at the app root (renders `null` in release builds):
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { Pinagent } from '@pinagent/react-native';
|
|
65
|
+
|
|
66
|
+
export default function App() {
|
|
67
|
+
return (
|
|
68
|
+
<>
|
|
69
|
+
<YourApp />
|
|
70
|
+
<Pinagent />
|
|
71
|
+
</>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Server** — `metro.config.js`:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
const { pinagentMiddleware } = require('@pinagent/react-native/server');
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
server: {
|
|
83
|
+
enhanceMiddleware: (metroMiddleware, server) =>
|
|
84
|
+
pinagentMiddleware({ projectRoot: __dirname }).chain(metroMiddleware),
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
See [Configuration](#configuration) for the full option/prop/env surface.
|
|
90
|
+
|
|
91
|
+
The middleware mounts `POST /__pinagent/feedback` **and** self-installs the
|
|
92
|
+
`/__pinagent/ws` live-streaming socket on Metro's own port, so a spawned
|
|
93
|
+
agent's run streams live into the app (text, tool calls, result) with
|
|
94
|
+
follow-ups and `ask_user` answering — the native counterpart of the web
|
|
95
|
+
widget's agent tray. Simulators and physical devices work with no extra config.
|
|
96
|
+
|
|
97
|
+
> The streaming socket rides the middleware (not `config.server.websocketEndpoints`)
|
|
98
|
+
> on purpose: **Expo's dev server ignores `websocketEndpoints`** and destroys
|
|
99
|
+
> any upgrade path it doesn't recognise, which would leave the in-app stream
|
|
100
|
+
> sheet stuck on "Connecting…". Routing through `enhanceMiddleware` — which Expo
|
|
101
|
+
> *does* honor — works under both Expo and bare Metro.
|
|
102
|
+
|
|
103
|
+
For an explicit bare-Metro setup you can still spread
|
|
104
|
+
`pinagentWebsocketEndpoints({ projectRoot })` into `config.server.websocketEndpoints`;
|
|
105
|
+
it's redundant with the middleware install but harmless.
|
|
106
|
+
|
|
107
|
+
**Agent pickup** — identical to web. Either let the middleware spawn
|
|
108
|
+
agents, or pull comments into a Claude Code session over `@pinagent/mcp`.
|
|
109
|
+
|
|
110
|
+
A complete, runnable Expo app is in [`example/`](./example).
|
|
111
|
+
|
|
112
|
+
## Configuration
|
|
113
|
+
|
|
114
|
+
### `pinagentMiddleware(opts)` — `metro.config.js`
|
|
115
|
+
|
|
116
|
+
| Option | Type | Default | Notes |
|
|
117
|
+
| --- | --- | --- | --- |
|
|
118
|
+
| `projectRoot` | `string` | — (required) | Where `.pinagent/` lives. Pass `__dirname`. |
|
|
119
|
+
| `spawnMode` | `false \| 'inline' \| 'worktree'` | `'inline'` | Same semantics as the Vite/Next plugins. `false` files the comment only (pull mode); `'inline'` runs the agent in-process and streams it back; `'worktree'` runs it in an isolated git worktree. |
|
|
120
|
+
| `apiKey` | `string` | — | Explicit API key for spawned agent runs. Bridged to the runner as `PINAGENT_AGENT_API_KEY`, exactly like the Vite plugin's `apiKey`. Omit to authenticate against your agentic subscription. |
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
const { pinagentMiddleware } = require('@pinagent/react-native/server');
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
server: {
|
|
127
|
+
enhanceMiddleware: (metroMiddleware, server) =>
|
|
128
|
+
pinagentMiddleware({
|
|
129
|
+
projectRoot: __dirname,
|
|
130
|
+
spawnMode: 'inline',
|
|
131
|
+
apiKey: process.env.MY_PINAGENT_KEY, // optional
|
|
132
|
+
}).chain(metroMiddleware),
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `<Pinagent />` props
|
|
138
|
+
|
|
139
|
+
| Prop | Type | Default | Notes |
|
|
140
|
+
| --- | --- | --- | --- |
|
|
141
|
+
| `projectRoot` | `string` | Metro-injected, else `''` | Makes `_debugSource` file paths project-relative (matching the web babel plugin). |
|
|
142
|
+
| `screenName` | `string` | `Platform.OS` | Route/screen name recorded as the comment `url`. Restored pills are scoped to this value, so pass a stable per-screen name if you want per-screen restore. |
|
|
143
|
+
|
|
144
|
+
### Environment variables
|
|
145
|
+
|
|
146
|
+
| Var | Effect |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `PINAGENT_AGENT_API_KEY` | The agent-run API key. Set it yourself, or let the `apiKey` middleware option set it. The `apiKey` option wins when both are present (it's applied on middleware construction). |
|
|
149
|
+
| `PINAGENT_EDITOR` | Editor command the dev server uses for tap-to-open (e.g. `code -g`). Falls back to common editor CLIs / macOS apps. |
|
|
150
|
+
|
|
151
|
+
**Pinagent never reads `ANTHROPIC_API_KEY` / `OPENAI_API_KEY` implicitly.** The
|
|
152
|
+
`apiKey` option (→ `PINAGENT_AGENT_API_KEY`) is the only key input; with neither
|
|
153
|
+
set, runs fall back to your agentic subscription. RN has no dock, so there's no
|
|
154
|
+
runtime Connections store to override the option/env — the option/env is the
|
|
155
|
+
whole story.
|
|
156
|
+
|
|
157
|
+
## How a tap becomes `file:line`
|
|
158
|
+
|
|
159
|
+
The `@pinagent/react-native/babel` plugin (step 2 of Install, dev-only)
|
|
160
|
+
splices a `data-pa-loc="<file>:<line>:<col>"` prop onto every authored JSX
|
|
161
|
+
element at build time — the direct analog of the web plugin's `data-pa-loc`
|
|
162
|
+
DOM attribute. At tap time RN's dev Inspector locates the view via
|
|
163
|
+
`getInspectorDataForViewAtPoint`, and `src/native/inspector.ts` reads the
|
|
164
|
+
`data-pa-loc` prop back off the host fiber, degrading to `loc: null` (rather
|
|
165
|
+
than throwing) across RN version differences.
|
|
166
|
+
|
|
167
|
+
This replaced the original `_debugSource` approach: **React 19 removed
|
|
168
|
+
`_debugSource`** and **RN 0.81+ dropped the `source` field from
|
|
169
|
+
`getInspectorDataForViewAtPoint`**, so the runtime no longer carries any
|
|
170
|
+
source location — pinagent injects its own at build time, exactly like web.
|
|
171
|
+
|
|
172
|
+
## Multi-select
|
|
173
|
+
|
|
174
|
+
Tap **+ Add element** in the composer to add more targets to the same comment:
|
|
175
|
+
the composer steps aside, you tap another element, and it returns as a
|
|
176
|
+
removable chip. On submit, the extra targets ride along in `additionalAnchors`
|
|
177
|
+
(the same wire shape the web widget sends), landing in the
|
|
178
|
+
`widget_anchors.additional_anchors` column and reaching the agent as
|
|
179
|
+
`additionalTargets` — so a single comment like "make all these buttons match"
|
|
180
|
+
addresses every picked element. A single pick leaves `additional_anchors` null
|
|
181
|
+
(web parity). The screenshot is captured once, at the first pick.
|
|
182
|
+
|
|
183
|
+
## Scope / known cuts
|
|
184
|
+
|
|
185
|
+
- No Fast-Refresh pin re-anchoring; `selector` carries the component name
|
|
186
|
+
chain (RN has no CSS selectors).
|
|
187
|
+
- Breadcrumb re-anchoring applies to the primary pick only; extras keep the
|
|
188
|
+
location they were tapped with (web behaves the same).
|
|
189
|
+
|
|
190
|
+
Live agent streaming is wired automatically by `pinagentMiddleware`.
|
|
191
|
+
|
|
192
|
+
## Tests
|
|
193
|
+
|
|
194
|
+
`tests/metro-middleware.test.ts` drives the middleware against the real
|
|
195
|
+
`Storage` and asserts a feedback POST lands a conversation in
|
|
196
|
+
`.pinagent/db.sqlite`:
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
pnpm --filter @pinagent/agent-runner build # test imports the built backend
|
|
200
|
+
pnpm exec vitest run packages/react-native
|
|
201
|
+
```
|
package/dist/babel.cjs
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
let node_path = require("node:path");
|
|
2
|
+
//#region src/babel.ts
|
|
3
|
+
/**
|
|
4
|
+
* Metro/Babel source-tagging plugin — the React Native analog of the web
|
|
5
|
+
* `@pinagent/babel-plugin`.
|
|
6
|
+
*
|
|
7
|
+
* It splices a `data-pa-loc="<file>:<line>:<col>"` prop (plus a
|
|
8
|
+
* `data-pa-comp="<EnclosingComponent>"` companion) onto every authored JSX
|
|
9
|
+
* element, exactly mirroring what the web babel plugin emits as a DOM
|
|
10
|
+
* attribute. On React Native that prop survives onto the host fiber's
|
|
11
|
+
* `memoizedProps`, so {@link resolvePick} can read it back at tap time.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this exists (and didn't used to)
|
|
14
|
+
*
|
|
15
|
+
* The original RN design leaned on each fiber's `_debugSource`, populated in
|
|
16
|
+
* dev by `@babel/plugin-transform-react-jsx-source` — "reuse RN's, no custom
|
|
17
|
+
* plugin needed". **React 19 removed `_debugSource`** (the `ReactElement`
|
|
18
|
+
* constructor no longer takes a `source` arg; the `__source` prop is consumed
|
|
19
|
+
* by `jsxDEV` and never reaches `memoizedProps`), and **RN 0.81+ dropped the
|
|
20
|
+
* `source` field from `getInspectorDataForViewAtPoint`**. So the runtime no
|
|
21
|
+
* longer carries any source location — we have to inject our own, at build
|
|
22
|
+
* time, the same way web does.
|
|
23
|
+
*
|
|
24
|
+
* Wire it into `babel.config.js` (dev only) BEFORE `babel-preset-expo`'s JSX
|
|
25
|
+
* transform so the attribute is present when JSX lowers to `jsxDEV`:
|
|
26
|
+
*
|
|
27
|
+
* const pinagentSource = require('@pinagent/react-native/babel').default;
|
|
28
|
+
* module.exports = (api) => {
|
|
29
|
+
* api.cache(true);
|
|
30
|
+
* const dev = process.env.NODE_ENV !== 'production';
|
|
31
|
+
* return {
|
|
32
|
+
* presets: ['babel-preset-expo'],
|
|
33
|
+
* plugins: dev ? [pinagentSource] : [],
|
|
34
|
+
* };
|
|
35
|
+
* };
|
|
36
|
+
*
|
|
37
|
+
* Typed loosely (no `@babel/*` type deps) on purpose — like {@link inspector},
|
|
38
|
+
* this is a thin, version-tolerant shim over an external toolchain that this
|
|
39
|
+
* otherwise web-only monorepo doesn't carry types for.
|
|
40
|
+
*/
|
|
41
|
+
/** The attribute the web plugin emits — reused verbatim so reads match. */
|
|
42
|
+
const ATTR = "data-pa-loc";
|
|
43
|
+
/** Companion attribute carrying the enclosing component name. */
|
|
44
|
+
const COMP_ATTR = "data-pa-comp";
|
|
45
|
+
function toPosix(p) {
|
|
46
|
+
return node_path.sep === "/" ? p : p.split(node_path.sep).join("/");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Walk up to the nearest enclosing React component — the closest
|
|
50
|
+
* function/class ancestor with a PascalCase name. Lowercase callbacks
|
|
51
|
+
* (`items.map(x => <Row/>)`) are skipped, so list items report the component
|
|
52
|
+
* that owns the list. Mirrors `@pinagent/babel-plugin`'s `transform.ts`.
|
|
53
|
+
*/
|
|
54
|
+
function enclosingComponentName(path, t) {
|
|
55
|
+
let fn = path.getFunctionParent?.();
|
|
56
|
+
while (fn) {
|
|
57
|
+
const name = inferFunctionName(fn, t);
|
|
58
|
+
if (name && /^[A-Z]/.test(name)) return name;
|
|
59
|
+
fn = fn.getFunctionParent?.();
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function inferFunctionName(fnPath, t) {
|
|
64
|
+
const node = fnPath.node;
|
|
65
|
+
if (t.isFunctionDeclaration(node) && node.id) return node.id.name;
|
|
66
|
+
if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
|
|
67
|
+
const cls = fnPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
|
|
68
|
+
if (cls) {
|
|
69
|
+
const clsNode = cls.node;
|
|
70
|
+
if ((t.isClassDeclaration(clsNode) || t.isClassExpression(clsNode)) && clsNode.id) return clsNode.id.name;
|
|
71
|
+
return nameFromBinding(cls, t);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return nameFromBinding(fnPath, t);
|
|
76
|
+
}
|
|
77
|
+
function nameFromBinding(p, t) {
|
|
78
|
+
const pn = p.parentPath?.node;
|
|
79
|
+
if (!pn) return null;
|
|
80
|
+
if (t.isVariableDeclarator(pn) && t.isIdentifier(pn.id)) return pn.id.name;
|
|
81
|
+
if ((t.isObjectProperty(pn) || t.isObjectMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;
|
|
82
|
+
if ((t.isClassProperty(pn) || t.isClassMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;
|
|
83
|
+
if (t.isAssignmentExpression(pn) && t.isIdentifier(pn.left)) return pn.left.name;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function isFragment(name, t) {
|
|
87
|
+
if (t.isJSXIdentifier(name)) return name.name === "Fragment";
|
|
88
|
+
if (t.isJSXMemberExpression(name)) return t.isJSXIdentifier(name.property) && name.property.name === "Fragment";
|
|
89
|
+
return t.isJSXNamespacedName(name);
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the project root used to make paths relative. */
|
|
92
|
+
function rootFor(state) {
|
|
93
|
+
return state.opts?.projectRoot ?? state.file?.opts?.root ?? state.cwd ?? state.file?.opts?.cwd;
|
|
94
|
+
}
|
|
95
|
+
/** Resolve the file being transformed. */
|
|
96
|
+
function filenameFor(state) {
|
|
97
|
+
return state.filename ?? state.file?.opts?.filename;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The Babel plugin. Default export so a `babel.config.js` can `require()` it
|
|
101
|
+
* and drop the function straight into `plugins`.
|
|
102
|
+
*/
|
|
103
|
+
function pinagentSource(babel) {
|
|
104
|
+
const t = babel.types;
|
|
105
|
+
return {
|
|
106
|
+
name: "pinagent-source",
|
|
107
|
+
visitor: { JSXOpeningElement(path, state) {
|
|
108
|
+
const node = path.node;
|
|
109
|
+
if (isFragment(node.name, t)) return;
|
|
110
|
+
if (node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === ATTR)) return;
|
|
111
|
+
const loc = node.loc?.start;
|
|
112
|
+
if (!loc) return;
|
|
113
|
+
const filename = filenameFor(state);
|
|
114
|
+
if (!filename || filename.includes(`${node_path.sep}node_modules${node_path.sep}`)) return;
|
|
115
|
+
const root = rootFor(state);
|
|
116
|
+
let rel = root && (0, node_path.isAbsolute)(filename) ? (0, node_path.relative)(root, filename) : filename;
|
|
117
|
+
rel = toPosix(rel);
|
|
118
|
+
if (rel.startsWith("../")) return;
|
|
119
|
+
const value = `${rel}:${loc.line}:${loc.column + 1}`;
|
|
120
|
+
const attrs = [t.jsxAttribute(t.jsxIdentifier(ATTR), t.stringLiteral(value))];
|
|
121
|
+
const comp = enclosingComponentName(path, t);
|
|
122
|
+
if (comp) attrs.push(t.jsxAttribute(t.jsxIdentifier(COMP_ATTR), t.stringLiteral(comp)));
|
|
123
|
+
node.attributes.push(...attrs);
|
|
124
|
+
} }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
module.exports = pinagentSource;
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=babel.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel.cjs","names":["sep"],"sources":["../src/babel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n/**\n * Metro/Babel source-tagging plugin — the React Native analog of the web\n * `@pinagent/babel-plugin`.\n *\n * It splices a `data-pa-loc=\"<file>:<line>:<col>\"` prop (plus a\n * `data-pa-comp=\"<EnclosingComponent>\"` companion) onto every authored JSX\n * element, exactly mirroring what the web babel plugin emits as a DOM\n * attribute. On React Native that prop survives onto the host fiber's\n * `memoizedProps`, so {@link resolvePick} can read it back at tap time.\n *\n * ## Why this exists (and didn't used to)\n *\n * The original RN design leaned on each fiber's `_debugSource`, populated in\n * dev by `@babel/plugin-transform-react-jsx-source` — \"reuse RN's, no custom\n * plugin needed\". **React 19 removed `_debugSource`** (the `ReactElement`\n * constructor no longer takes a `source` arg; the `__source` prop is consumed\n * by `jsxDEV` and never reaches `memoizedProps`), and **RN 0.81+ dropped the\n * `source` field from `getInspectorDataForViewAtPoint`**. So the runtime no\n * longer carries any source location — we have to inject our own, at build\n * time, the same way web does.\n *\n * Wire it into `babel.config.js` (dev only) BEFORE `babel-preset-expo`'s JSX\n * transform so the attribute is present when JSX lowers to `jsxDEV`:\n *\n * const pinagentSource = require('@pinagent/react-native/babel').default;\n * module.exports = (api) => {\n * api.cache(true);\n * const dev = process.env.NODE_ENV !== 'production';\n * return {\n * presets: ['babel-preset-expo'],\n * plugins: dev ? [pinagentSource] : [],\n * };\n * };\n *\n * Typed loosely (no `@babel/*` type deps) on purpose — like {@link inspector},\n * this is a thin, version-tolerant shim over an external toolchain that this\n * otherwise web-only monorepo doesn't carry types for.\n */\nimport { isAbsolute, relative, sep } from 'node:path';\n\n/** The attribute the web plugin emits — reused verbatim so reads match. */\nconst ATTR = 'data-pa-loc';\n/** Companion attribute carrying the enclosing component name. */\nconst COMP_ATTR = 'data-pa-comp';\n\n// Minimal structural typing for the slice of Babel we touch. Babel hands the\n// plugin a `types` namespace and node paths; we lean on duck-typing rather\n// than pulling in @types/babel__core.\n// biome-ignore lint/suspicious/noExplicitAny: external babel AST, typed loosely\ntype Any = any;\n\ninterface PluginState {\n filename?: string;\n cwd?: string;\n opts?: { projectRoot?: string };\n file?: { opts?: { filename?: string; cwd?: string; root?: string } };\n}\n\nfunction toPosix(p: string): string {\n return sep === '/' ? p : p.split(sep).join('/');\n}\n\n/**\n * Walk up to the nearest enclosing React component — the closest\n * function/class ancestor with a PascalCase name. Lowercase callbacks\n * (`items.map(x => <Row/>)`) are skipped, so list items report the component\n * that owns the list. Mirrors `@pinagent/babel-plugin`'s `transform.ts`.\n */\nfunction enclosingComponentName(path: Any, t: Any): string | null {\n let fn = path.getFunctionParent?.();\n while (fn) {\n const name = inferFunctionName(fn, t);\n if (name && /^[A-Z]/.test(name)) return name;\n fn = fn.getFunctionParent?.();\n }\n return null;\n}\n\nfunction inferFunctionName(fnPath: Any, t: Any): string | null {\n const node = fnPath.node;\n if (t.isFunctionDeclaration(node) && node.id) return node.id.name;\n if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {\n const cls = fnPath.findParent((p: Any) => p.isClassDeclaration() || p.isClassExpression());\n if (cls) {\n const clsNode = cls.node;\n if ((t.isClassDeclaration(clsNode) || t.isClassExpression(clsNode)) && clsNode.id) {\n return clsNode.id.name;\n }\n return nameFromBinding(cls, t);\n }\n return null;\n }\n return nameFromBinding(fnPath, t);\n}\n\nfunction nameFromBinding(p: Any, t: Any): string | null {\n const pn = p.parentPath?.node;\n if (!pn) return null;\n if (t.isVariableDeclarator(pn) && t.isIdentifier(pn.id)) return pn.id.name;\n if ((t.isObjectProperty(pn) || t.isObjectMethod(pn)) && t.isIdentifier(pn.key))\n return pn.key.name;\n if ((t.isClassProperty(pn) || t.isClassMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;\n if (t.isAssignmentExpression(pn) && t.isIdentifier(pn.left)) return pn.left.name;\n return null;\n}\n\nfunction isFragment(name: Any, t: Any): boolean {\n if (t.isJSXIdentifier(name)) return name.name === 'Fragment';\n if (t.isJSXMemberExpression(name)) {\n return t.isJSXIdentifier(name.property) && name.property.name === 'Fragment';\n }\n return t.isJSXNamespacedName(name);\n}\n\n/** Resolve the project root used to make paths relative. */\nfunction rootFor(state: PluginState): string | undefined {\n return state.opts?.projectRoot ?? state.file?.opts?.root ?? state.cwd ?? state.file?.opts?.cwd;\n}\n\n/** Resolve the file being transformed. */\nfunction filenameFor(state: PluginState): string | undefined {\n return state.filename ?? state.file?.opts?.filename;\n}\n\nexport interface PinagentBabelOptions {\n /** Project root for project-relative paths. Defaults to Babel's cwd/root. */\n projectRoot?: string;\n}\n\n/**\n * The Babel plugin. Default export so a `babel.config.js` can `require()` it\n * and drop the function straight into `plugins`.\n */\nexport default function pinagentSource(babel: { types: Any }): Any {\n const t = babel.types;\n return {\n name: 'pinagent-source',\n visitor: {\n JSXOpeningElement(path: Any, state: PluginState) {\n const node = path.node;\n if (isFragment(node.name, t)) return;\n\n // Already tagged? Idempotent — re-running is a no-op.\n const tagged = node.attributes.some(\n (a: Any) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === ATTR,\n );\n if (tagged) return;\n\n const loc = node.loc?.start;\n if (!loc) return;\n\n const filename = filenameFor(state);\n if (!filename || filename.includes(`${sep}node_modules${sep}`)) return;\n\n const root = rootFor(state);\n let rel = root && isAbsolute(filename) ? relative(root, filename) : filename;\n rel = toPosix(rel);\n // Only tag files inside the project root — skip anything resolved\n // outside it (e.g. the in-tree widget source under an out-of-root\n // package, which the developer never taps on).\n if (rel.startsWith('../')) return;\n\n const value = `${rel}:${loc.line}:${loc.column + 1}`;\n const attrs = [t.jsxAttribute(t.jsxIdentifier(ATTR), t.stringLiteral(value))];\n const comp = enclosingComponentName(path, t);\n if (comp) {\n attrs.push(t.jsxAttribute(t.jsxIdentifier(COMP_ATTR), t.stringLiteral(comp)));\n }\n node.attributes.push(...attrs);\n },\n },\n };\n}\n"],"mappings":"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"}
|
package/dist/babel.d.cts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/babel.d.ts
|
|
2
|
+
type Any = any;
|
|
3
|
+
interface PinagentBabelOptions {
|
|
4
|
+
/** Project root for project-relative paths. Defaults to Babel's cwd/root. */
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Babel plugin. Default export so a `babel.config.js` can `require()` it
|
|
9
|
+
* and drop the function straight into `plugins`.
|
|
10
|
+
*/
|
|
11
|
+
declare function pinagentSource(babel: {
|
|
12
|
+
types: Any;
|
|
13
|
+
}): Any;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { PinagentBabelOptions, pinagentSource as default };
|
|
16
|
+
//# sourceMappingURL=babel.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel.d.cts","names":[],"sources":["../src/babel.ts"],"mappings":";KAkDK,GAAA;AAAA,UA2EY,oBAAA;EA3ET;EA6EN,WAAW;AAAA;AA7EL;AA2ER;;;AA3EQ,iBAoFgB,cAAA,CAAe,KAAA;EAAS,KAAA,EAAO,GAAA;AAAA,IAAQ,GAAG"}
|
package/dist/babel.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/babel.d.ts
|
|
2
|
+
type Any = any;
|
|
3
|
+
interface PinagentBabelOptions {
|
|
4
|
+
/** Project root for project-relative paths. Defaults to Babel's cwd/root. */
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The Babel plugin. Default export so a `babel.config.js` can `require()` it
|
|
9
|
+
* and drop the function straight into `plugins`.
|
|
10
|
+
*/
|
|
11
|
+
declare function pinagentSource(babel: {
|
|
12
|
+
types: Any;
|
|
13
|
+
}): Any;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { PinagentBabelOptions, pinagentSource as default };
|
|
16
|
+
//# sourceMappingURL=babel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel.d.ts","names":[],"sources":["../src/babel.ts"],"mappings":";KAkDK,GAAA;AAAA,UA2EY,oBAAA;EA3ET;EA6EN,WAAW;AAAA;AA7EL;AA2ER;;;AA3EQ,iBAoFgB,cAAA,CAAe,KAAA;EAAS,KAAA,EAAO,GAAA;AAAA,IAAQ,GAAG"}
|
package/dist/babel.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from "node:path";
|
|
2
|
+
//#region src/babel.ts
|
|
3
|
+
/**
|
|
4
|
+
* Metro/Babel source-tagging plugin — the React Native analog of the web
|
|
5
|
+
* `@pinagent/babel-plugin`.
|
|
6
|
+
*
|
|
7
|
+
* It splices a `data-pa-loc="<file>:<line>:<col>"` prop (plus a
|
|
8
|
+
* `data-pa-comp="<EnclosingComponent>"` companion) onto every authored JSX
|
|
9
|
+
* element, exactly mirroring what the web babel plugin emits as a DOM
|
|
10
|
+
* attribute. On React Native that prop survives onto the host fiber's
|
|
11
|
+
* `memoizedProps`, so {@link resolvePick} can read it back at tap time.
|
|
12
|
+
*
|
|
13
|
+
* ## Why this exists (and didn't used to)
|
|
14
|
+
*
|
|
15
|
+
* The original RN design leaned on each fiber's `_debugSource`, populated in
|
|
16
|
+
* dev by `@babel/plugin-transform-react-jsx-source` — "reuse RN's, no custom
|
|
17
|
+
* plugin needed". **React 19 removed `_debugSource`** (the `ReactElement`
|
|
18
|
+
* constructor no longer takes a `source` arg; the `__source` prop is consumed
|
|
19
|
+
* by `jsxDEV` and never reaches `memoizedProps`), and **RN 0.81+ dropped the
|
|
20
|
+
* `source` field from `getInspectorDataForViewAtPoint`**. So the runtime no
|
|
21
|
+
* longer carries any source location — we have to inject our own, at build
|
|
22
|
+
* time, the same way web does.
|
|
23
|
+
*
|
|
24
|
+
* Wire it into `babel.config.js` (dev only) BEFORE `babel-preset-expo`'s JSX
|
|
25
|
+
* transform so the attribute is present when JSX lowers to `jsxDEV`:
|
|
26
|
+
*
|
|
27
|
+
* const pinagentSource = require('@pinagent/react-native/babel').default;
|
|
28
|
+
* module.exports = (api) => {
|
|
29
|
+
* api.cache(true);
|
|
30
|
+
* const dev = process.env.NODE_ENV !== 'production';
|
|
31
|
+
* return {
|
|
32
|
+
* presets: ['babel-preset-expo'],
|
|
33
|
+
* plugins: dev ? [pinagentSource] : [],
|
|
34
|
+
* };
|
|
35
|
+
* };
|
|
36
|
+
*
|
|
37
|
+
* Typed loosely (no `@babel/*` type deps) on purpose — like {@link inspector},
|
|
38
|
+
* this is a thin, version-tolerant shim over an external toolchain that this
|
|
39
|
+
* otherwise web-only monorepo doesn't carry types for.
|
|
40
|
+
*/
|
|
41
|
+
/** The attribute the web plugin emits — reused verbatim so reads match. */
|
|
42
|
+
const ATTR = "data-pa-loc";
|
|
43
|
+
/** Companion attribute carrying the enclosing component name. */
|
|
44
|
+
const COMP_ATTR = "data-pa-comp";
|
|
45
|
+
function toPosix(p) {
|
|
46
|
+
return sep === "/" ? p : p.split(sep).join("/");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Walk up to the nearest enclosing React component — the closest
|
|
50
|
+
* function/class ancestor with a PascalCase name. Lowercase callbacks
|
|
51
|
+
* (`items.map(x => <Row/>)`) are skipped, so list items report the component
|
|
52
|
+
* that owns the list. Mirrors `@pinagent/babel-plugin`'s `transform.ts`.
|
|
53
|
+
*/
|
|
54
|
+
function enclosingComponentName(path, t) {
|
|
55
|
+
let fn = path.getFunctionParent?.();
|
|
56
|
+
while (fn) {
|
|
57
|
+
const name = inferFunctionName(fn, t);
|
|
58
|
+
if (name && /^[A-Z]/.test(name)) return name;
|
|
59
|
+
fn = fn.getFunctionParent?.();
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function inferFunctionName(fnPath, t) {
|
|
64
|
+
const node = fnPath.node;
|
|
65
|
+
if (t.isFunctionDeclaration(node) && node.id) return node.id.name;
|
|
66
|
+
if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {
|
|
67
|
+
const cls = fnPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression());
|
|
68
|
+
if (cls) {
|
|
69
|
+
const clsNode = cls.node;
|
|
70
|
+
if ((t.isClassDeclaration(clsNode) || t.isClassExpression(clsNode)) && clsNode.id) return clsNode.id.name;
|
|
71
|
+
return nameFromBinding(cls, t);
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return nameFromBinding(fnPath, t);
|
|
76
|
+
}
|
|
77
|
+
function nameFromBinding(p, t) {
|
|
78
|
+
const pn = p.parentPath?.node;
|
|
79
|
+
if (!pn) return null;
|
|
80
|
+
if (t.isVariableDeclarator(pn) && t.isIdentifier(pn.id)) return pn.id.name;
|
|
81
|
+
if ((t.isObjectProperty(pn) || t.isObjectMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;
|
|
82
|
+
if ((t.isClassProperty(pn) || t.isClassMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;
|
|
83
|
+
if (t.isAssignmentExpression(pn) && t.isIdentifier(pn.left)) return pn.left.name;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function isFragment(name, t) {
|
|
87
|
+
if (t.isJSXIdentifier(name)) return name.name === "Fragment";
|
|
88
|
+
if (t.isJSXMemberExpression(name)) return t.isJSXIdentifier(name.property) && name.property.name === "Fragment";
|
|
89
|
+
return t.isJSXNamespacedName(name);
|
|
90
|
+
}
|
|
91
|
+
/** Resolve the project root used to make paths relative. */
|
|
92
|
+
function rootFor(state) {
|
|
93
|
+
return state.opts?.projectRoot ?? state.file?.opts?.root ?? state.cwd ?? state.file?.opts?.cwd;
|
|
94
|
+
}
|
|
95
|
+
/** Resolve the file being transformed. */
|
|
96
|
+
function filenameFor(state) {
|
|
97
|
+
return state.filename ?? state.file?.opts?.filename;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The Babel plugin. Default export so a `babel.config.js` can `require()` it
|
|
101
|
+
* and drop the function straight into `plugins`.
|
|
102
|
+
*/
|
|
103
|
+
function pinagentSource(babel) {
|
|
104
|
+
const t = babel.types;
|
|
105
|
+
return {
|
|
106
|
+
name: "pinagent-source",
|
|
107
|
+
visitor: { JSXOpeningElement(path, state) {
|
|
108
|
+
const node = path.node;
|
|
109
|
+
if (isFragment(node.name, t)) return;
|
|
110
|
+
if (node.attributes.some((a) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === ATTR)) return;
|
|
111
|
+
const loc = node.loc?.start;
|
|
112
|
+
if (!loc) return;
|
|
113
|
+
const filename = filenameFor(state);
|
|
114
|
+
if (!filename || filename.includes(`${sep}node_modules${sep}`)) return;
|
|
115
|
+
const root = rootFor(state);
|
|
116
|
+
let rel = root && isAbsolute(filename) ? relative(root, filename) : filename;
|
|
117
|
+
rel = toPosix(rel);
|
|
118
|
+
if (rel.startsWith("../")) return;
|
|
119
|
+
const value = `${rel}:${loc.line}:${loc.column + 1}`;
|
|
120
|
+
const attrs = [t.jsxAttribute(t.jsxIdentifier(ATTR), t.stringLiteral(value))];
|
|
121
|
+
const comp = enclosingComponentName(path, t);
|
|
122
|
+
if (comp) attrs.push(t.jsxAttribute(t.jsxIdentifier(COMP_ATTR), t.stringLiteral(comp)));
|
|
123
|
+
node.attributes.push(...attrs);
|
|
124
|
+
} }
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
export { pinagentSource as default };
|
|
129
|
+
|
|
130
|
+
//# sourceMappingURL=babel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"babel.js","names":[],"sources":["../src/babel.ts"],"sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n/**\n * Metro/Babel source-tagging plugin — the React Native analog of the web\n * `@pinagent/babel-plugin`.\n *\n * It splices a `data-pa-loc=\"<file>:<line>:<col>\"` prop (plus a\n * `data-pa-comp=\"<EnclosingComponent>\"` companion) onto every authored JSX\n * element, exactly mirroring what the web babel plugin emits as a DOM\n * attribute. On React Native that prop survives onto the host fiber's\n * `memoizedProps`, so {@link resolvePick} can read it back at tap time.\n *\n * ## Why this exists (and didn't used to)\n *\n * The original RN design leaned on each fiber's `_debugSource`, populated in\n * dev by `@babel/plugin-transform-react-jsx-source` — \"reuse RN's, no custom\n * plugin needed\". **React 19 removed `_debugSource`** (the `ReactElement`\n * constructor no longer takes a `source` arg; the `__source` prop is consumed\n * by `jsxDEV` and never reaches `memoizedProps`), and **RN 0.81+ dropped the\n * `source` field from `getInspectorDataForViewAtPoint`**. So the runtime no\n * longer carries any source location — we have to inject our own, at build\n * time, the same way web does.\n *\n * Wire it into `babel.config.js` (dev only) BEFORE `babel-preset-expo`'s JSX\n * transform so the attribute is present when JSX lowers to `jsxDEV`:\n *\n * const pinagentSource = require('@pinagent/react-native/babel').default;\n * module.exports = (api) => {\n * api.cache(true);\n * const dev = process.env.NODE_ENV !== 'production';\n * return {\n * presets: ['babel-preset-expo'],\n * plugins: dev ? [pinagentSource] : [],\n * };\n * };\n *\n * Typed loosely (no `@babel/*` type deps) on purpose — like {@link inspector},\n * this is a thin, version-tolerant shim over an external toolchain that this\n * otherwise web-only monorepo doesn't carry types for.\n */\nimport { isAbsolute, relative, sep } from 'node:path';\n\n/** The attribute the web plugin emits — reused verbatim so reads match. */\nconst ATTR = 'data-pa-loc';\n/** Companion attribute carrying the enclosing component name. */\nconst COMP_ATTR = 'data-pa-comp';\n\n// Minimal structural typing for the slice of Babel we touch. Babel hands the\n// plugin a `types` namespace and node paths; we lean on duck-typing rather\n// than pulling in @types/babel__core.\n// biome-ignore lint/suspicious/noExplicitAny: external babel AST, typed loosely\ntype Any = any;\n\ninterface PluginState {\n filename?: string;\n cwd?: string;\n opts?: { projectRoot?: string };\n file?: { opts?: { filename?: string; cwd?: string; root?: string } };\n}\n\nfunction toPosix(p: string): string {\n return sep === '/' ? p : p.split(sep).join('/');\n}\n\n/**\n * Walk up to the nearest enclosing React component — the closest\n * function/class ancestor with a PascalCase name. Lowercase callbacks\n * (`items.map(x => <Row/>)`) are skipped, so list items report the component\n * that owns the list. Mirrors `@pinagent/babel-plugin`'s `transform.ts`.\n */\nfunction enclosingComponentName(path: Any, t: Any): string | null {\n let fn = path.getFunctionParent?.();\n while (fn) {\n const name = inferFunctionName(fn, t);\n if (name && /^[A-Z]/.test(name)) return name;\n fn = fn.getFunctionParent?.();\n }\n return null;\n}\n\nfunction inferFunctionName(fnPath: Any, t: Any): string | null {\n const node = fnPath.node;\n if (t.isFunctionDeclaration(node) && node.id) return node.id.name;\n if (t.isClassMethod(node) || t.isClassPrivateMethod(node)) {\n const cls = fnPath.findParent((p: Any) => p.isClassDeclaration() || p.isClassExpression());\n if (cls) {\n const clsNode = cls.node;\n if ((t.isClassDeclaration(clsNode) || t.isClassExpression(clsNode)) && clsNode.id) {\n return clsNode.id.name;\n }\n return nameFromBinding(cls, t);\n }\n return null;\n }\n return nameFromBinding(fnPath, t);\n}\n\nfunction nameFromBinding(p: Any, t: Any): string | null {\n const pn = p.parentPath?.node;\n if (!pn) return null;\n if (t.isVariableDeclarator(pn) && t.isIdentifier(pn.id)) return pn.id.name;\n if ((t.isObjectProperty(pn) || t.isObjectMethod(pn)) && t.isIdentifier(pn.key))\n return pn.key.name;\n if ((t.isClassProperty(pn) || t.isClassMethod(pn)) && t.isIdentifier(pn.key)) return pn.key.name;\n if (t.isAssignmentExpression(pn) && t.isIdentifier(pn.left)) return pn.left.name;\n return null;\n}\n\nfunction isFragment(name: Any, t: Any): boolean {\n if (t.isJSXIdentifier(name)) return name.name === 'Fragment';\n if (t.isJSXMemberExpression(name)) {\n return t.isJSXIdentifier(name.property) && name.property.name === 'Fragment';\n }\n return t.isJSXNamespacedName(name);\n}\n\n/** Resolve the project root used to make paths relative. */\nfunction rootFor(state: PluginState): string | undefined {\n return state.opts?.projectRoot ?? state.file?.opts?.root ?? state.cwd ?? state.file?.opts?.cwd;\n}\n\n/** Resolve the file being transformed. */\nfunction filenameFor(state: PluginState): string | undefined {\n return state.filename ?? state.file?.opts?.filename;\n}\n\nexport interface PinagentBabelOptions {\n /** Project root for project-relative paths. Defaults to Babel's cwd/root. */\n projectRoot?: string;\n}\n\n/**\n * The Babel plugin. Default export so a `babel.config.js` can `require()` it\n * and drop the function straight into `plugins`.\n */\nexport default function pinagentSource(babel: { types: Any }): Any {\n const t = babel.types;\n return {\n name: 'pinagent-source',\n visitor: {\n JSXOpeningElement(path: Any, state: PluginState) {\n const node = path.node;\n if (isFragment(node.name, t)) return;\n\n // Already tagged? Idempotent — re-running is a no-op.\n const tagged = node.attributes.some(\n (a: Any) => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name) && a.name.name === ATTR,\n );\n if (tagged) return;\n\n const loc = node.loc?.start;\n if (!loc) return;\n\n const filename = filenameFor(state);\n if (!filename || filename.includes(`${sep}node_modules${sep}`)) return;\n\n const root = rootFor(state);\n let rel = root && isAbsolute(filename) ? relative(root, filename) : filename;\n rel = toPosix(rel);\n // Only tag files inside the project root — skip anything resolved\n // outside it (e.g. the in-tree widget source under an out-of-root\n // package, which the developer never taps on).\n if (rel.startsWith('../')) return;\n\n const value = `${rel}:${loc.line}:${loc.column + 1}`;\n const attrs = [t.jsxAttribute(t.jsxIdentifier(ATTR), t.stringLiteral(value))];\n const comp = enclosingComponentName(path, t);\n if (comp) {\n attrs.push(t.jsxAttribute(t.jsxIdentifier(COMP_ATTR), t.stringLiteral(comp)));\n }\n node.attributes.push(...attrs);\n },\n },\n };\n}\n"],"mappings":"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"}
|