@agent-scope/babel-plugin 1.17.1 → 1.17.3

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 (2) hide show
  1. package/README.md +510 -0
  2. package/package.json +4 -3
package/README.md ADDED
@@ -0,0 +1,510 @@
1
+ # `@agent-scope/babel-plugin`
2
+
3
+ Build-time AST transform that injects two static properties into every React component definition: `__scopeSource` (file + line + column) and `__scopeProps` (TypeScript prop type metadata). No runtime overhead — all analysis happens at compile time.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --save-dev @agent-scope/babel-plugin
9
+ # or
10
+ bun add -d @agent-scope/babel-plugin
11
+ ```
12
+
13
+ ---
14
+
15
+ ## What it does
16
+
17
+ For each detected React component the plugin emits two assignment statements immediately after the component definition:
18
+
19
+ ```typescript
20
+ // Input
21
+ interface ButtonProps {
22
+ variant: 'primary' | 'secondary' | 'ghost';
23
+ size?: 'sm' | 'md' | 'lg';
24
+ disabled?: boolean;
25
+ children: React.ReactNode;
26
+ onClick?: (e: MouseEvent) => void;
27
+ }
28
+
29
+ function Button({ variant, size = 'md', disabled = false, children, onClick }: ButtonProps) {
30
+ return <button type="button" disabled={disabled}>{children}</button>;
31
+ }
32
+
33
+ // Output (after transform)
34
+ function Button({ variant, size = 'md', disabled = false, children, onClick }) {
35
+ return /*#__PURE__*/React.createElement("button", { type: "button", disabled }, children);
36
+ }
37
+ Button.__scopeSource = { filePath: "src/Button.tsx", line: 9, column: 0 };
38
+ Button.__scopeProps = {
39
+ variant: { type: "union", required: true, values: ["primary", "secondary", "ghost"] },
40
+ size: { type: "union", required: false, values: ["sm", "md", "lg"], defaultValue: "md" },
41
+ disabled: { type: "boolean", required: false, defaultValue: false },
42
+ children: { type: "ReactNode", required: true },
43
+ onClick: { type: "function", required: false },
44
+ };
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Architecture
50
+
51
+ ```
52
+ Program.enter
53
+ └── Pre-collect all TS interface / type alias declarations
54
+ into a WeakMap<Program, Map<typeName, TSTypeElement[]>>
55
+ (before @babel/preset-typescript strips them)
56
+
57
+ FunctionDeclaration / VariableDeclaration / ExportDefaultDeclaration
58
+ └── component-detector.ts
59
+ ├── isComponentName() → name starts with uppercase
60
+ └── bodyContainsJSX() → body has a return statement returning JSX
61
+
62
+
63
+ source-injector.ts
64
+ └── buildScopeSourceStatement() → AST node for __scopeSource = {...}
65
+
66
+ prop-type-extractor.ts
67
+ └── resolveTypeReferenceName() → look up interface/type in pre-collected map
68
+ extractPropsFromTypeMembers() → PropMap
69
+ extractDefaultValues() → defaults from destructuring
70
+
71
+
72
+ props-injector.ts
73
+ └── buildScopePropsStatement() → AST node for __scopeProps = {...}
74
+ ```
75
+
76
+ ### Why `Program.enter` pre-collection?
77
+
78
+ `@babel/preset-typescript` removes `TSInterfaceDeclaration` and `TSTypeAliasDeclaration` nodes during the same traversal pass. Because Babel traverses top-down, interface declarations (defined before the component) are visited and removed **before** the `FunctionDeclaration` visitor fires. The plugin collects all type declarations at `Program.enter` — before any child nodes are visited or removed — and stores them in a `WeakMap` keyed by the Program node.
79
+
80
+ ---
81
+
82
+ ## Component detection rules
83
+
84
+ A function is treated as a React component if:
85
+
86
+ 1. **Name starts with an uppercase letter** — `isComponentName(name)` checks `/^[A-Z]/`
87
+ 2. **Body returns JSX** — `bodyContainsJSX()` checks for a `ReturnStatement` whose argument is a `JSXElement` or `JSXFragment` (including via ternary and logical expressions one level deep)
88
+
89
+ Detected patterns:
90
+
91
+ | Pattern | Example |
92
+ |---|---|
93
+ | Named function declaration | `function Button() { return <div/>; }` |
94
+ | Arrow function variable | `const Card = () => <div/>;` |
95
+ | Function expression variable | `const Card = function() { return <div/>; };` |
96
+ | `React.memo()` wrapper | `const Memo = React.memo(() => <div/>);` |
97
+ | `React.forwardRef()` wrapper | `const Ref = React.forwardRef((ref, p) => <input ref={ref}/>);` |
98
+ | Named default export | `export default function Page() { return <main/>; }` |
99
+ | Anonymous default export (arrow/fn) | `export default () => <div/>;` |
100
+
101
+ Not detected (intentionally excluded):
102
+
103
+ - Lowercase-named functions: `function formatDate(d)` → skipped
104
+ - React hooks: `function useCounter()` → skipped (lowercase 'u')
105
+ - Functions that don't return JSX: `function GetData() { return fetch(...); }` → skipped
106
+
107
+ ---
108
+
109
+ ## `__scopeSource` injection
110
+
111
+ **Injected shape**
112
+
113
+ ```typescript
114
+ ComponentName.__scopeSource = {
115
+ filePath: string; // relative path (or opts.filePath override)
116
+ line: number; // 1-based line number of the component declaration
117
+ column: number; // 0-based column number
118
+ };
119
+ ```
120
+
121
+ **`filePath` resolution** (from `plugin.test.ts`)
122
+
123
+ Pass `filePath` in plugin options to override Babel's automatic filename. This is critical in test environments where absolute paths vary by machine:
124
+
125
+ ```javascript
126
+ // babel.config.js
127
+ module.exports = {
128
+ plugins: [['@agent-scope/babel-plugin', { filePath: 'src/Button.tsx' }]],
129
+ };
130
+ ```
131
+
132
+ Without `filePath`, Babel's `state.filename` is used (often an absolute path).
133
+
134
+ ---
135
+
136
+ ## `__scopeProps` injection
137
+
138
+ **Injected shape**
139
+
140
+ ```typescript
141
+ ComponentName.__scopeProps = {
142
+ [propName: string]: {
143
+ type: PropTypeString; // simplified type category
144
+ required: boolean; // true if prop has no '?'
145
+ values?: string[]; // only for 'union' type — the literal string values
146
+ defaultValue?: unknown; // extracted from destructuring default, if present
147
+ };
148
+ };
149
+ ```
150
+
151
+ **`PropTypeString` values**
152
+
153
+ | Value | TypeScript types that map to it |
154
+ |---|---|
155
+ | `"string"` | `string` |
156
+ | `"number"` | `number` |
157
+ | `"boolean"` | `boolean` |
158
+ | `"union"` | A union of all string/number literals: `'a' \| 'b' \| 'c'` |
159
+ | `"function"` | `() => void`, `MouseEvent`, `ChangeEvent`, `React.*Handler` |
160
+ | `"ReactNode"` | `React.ReactNode`, `ReactNode`, `ReactElement`, `React.ReactElement` |
161
+ | `"object"` | `{ ... }` (TSTypeLiteral), mapped types |
162
+ | `"array"` | `T[]`, tuples |
163
+ | `"unknown"` | Intersection, conditional, generic references, `any`, `never`, etc. |
164
+
165
+ **Default value extraction**
166
+
167
+ Destructuring defaults are extracted from the first parameter:
168
+
169
+ ```typescript
170
+ function Button({ size = 'md', disabled = false, count = 0 }: ButtonProps) { ... }
171
+ // → size.defaultValue: "md", disabled.defaultValue: false, count.defaultValue: 0
172
+ ```
173
+
174
+ Supported literal default types: `string`, `number`, `boolean`, `null`, and negative numbers (`-1`).
175
+
176
+ ---
177
+
178
+ ## Configuration
179
+
180
+ ### `babel.config.js`
181
+
182
+ ```javascript
183
+ module.exports = {
184
+ plugins: ['@agent-scope/babel-plugin'],
185
+ };
186
+ ```
187
+
188
+ ### `babel.config.js` with `filePath` override
189
+
190
+ ```javascript
191
+ module.exports = {
192
+ plugins: [
193
+ ['@agent-scope/babel-plugin', { filePath: 'src/MyComponent.tsx' }],
194
+ ],
195
+ };
196
+ ```
197
+
198
+ ### Vite (`vite.config.ts`)
199
+
200
+ ```typescript
201
+ import { defineConfig } from 'vite';
202
+ import react from '@vitejs/plugin-react';
203
+
204
+ export default defineConfig({
205
+ plugins: [
206
+ react({
207
+ babel: {
208
+ plugins: ['@agent-scope/babel-plugin'],
209
+ },
210
+ }),
211
+ ],
212
+ });
213
+ ```
214
+
215
+ ### Next.js (`next.config.js`)
216
+
217
+ ```javascript
218
+ module.exports = {
219
+ experimental: { forceSwcTransforms: false },
220
+ babel: {
221
+ plugins: ['@agent-scope/babel-plugin'],
222
+ },
223
+ };
224
+ ```
225
+
226
+ ---
227
+
228
+ ## Transform examples (from test fixtures)
229
+
230
+ ### Simple function component (`simple-component.tsx`)
231
+
232
+ **Input**
233
+
234
+ ```tsx
235
+ function Button({ label }: { label: string }) {
236
+ return <button type="button">{label}</button>;
237
+ }
238
+ export { Button };
239
+ ```
240
+
241
+ **Output** (TypeScript stripped, JSX transformed)
242
+
243
+ ```javascript
244
+ function Button({ label }) {
245
+ return /*#__PURE__*/React.createElement("button", { type: "button" }, label);
246
+ }
247
+ Button.__scopeSource = { filePath: "src/simple-component.tsx", line: 1, column: 0 };
248
+ Button.__scopeProps = {
249
+ label: { type: "string", required: true },
250
+ };
251
+ ```
252
+
253
+ ---
254
+
255
+ ### Arrow function component (`arrow-component.tsx`)
256
+
257
+ **Input**
258
+
259
+ ```tsx
260
+ const Card = ({ title }: { title: string }) => <div className="card">{title}</div>;
261
+ export { Card };
262
+ ```
263
+
264
+ **Output**
265
+
266
+ ```javascript
267
+ const Card = ({ title }) => /*#__PURE__*/React.createElement("div", { className: "card" }, title);
268
+ Card.__scopeSource = { filePath: "src/arrow-component.tsx", line: 1, column: 0 };
269
+ Card.__scopeProps = {
270
+ title: { type: "string", required: true },
271
+ };
272
+ ```
273
+
274
+ ---
275
+
276
+ ### `React.memo` wrapper (`memo-component.tsx`)
277
+
278
+ **Input**
279
+
280
+ ```tsx
281
+ import React from "react";
282
+ const MemoButton = React.memo(({ label }: { label: string }) => (
283
+ <button type="button">{label}</button>
284
+ ));
285
+ export { MemoButton };
286
+ ```
287
+
288
+ **Output**
289
+
290
+ ```javascript
291
+ const MemoButton = React.memo(({ label }) =>
292
+ /*#__PURE__*/React.createElement("button", { type: "button" }, label)
293
+ );
294
+ MemoButton.__scopeSource = { filePath: "src/memo-component.tsx", line: 3, column: 0 };
295
+ MemoButton.__scopeProps = { label: { type: "string", required: true } };
296
+ ```
297
+
298
+ ---
299
+
300
+ ### `React.forwardRef` wrapper (`forwardref-component.tsx`)
301
+
302
+ **Input**
303
+
304
+ ```tsx
305
+ import React from "react";
306
+ const FancyInput = React.forwardRef<HTMLInputElement, { placeholder: string }>(
307
+ ({ placeholder }, ref) => <input ref={ref} placeholder={placeholder} />,
308
+ );
309
+ export { FancyInput };
310
+ ```
311
+
312
+ **Output**
313
+
314
+ ```javascript
315
+ const FancyInput = React.forwardRef(({ placeholder }, ref) =>
316
+ /*#__PURE__*/React.createElement("input", { ref, placeholder })
317
+ );
318
+ FancyInput.__scopeSource = { filePath: "src/forwardref-component.tsx", line: 3, column: 0 };
319
+ ```
320
+
321
+ ---
322
+
323
+ ### Named default export (`default-export.tsx`)
324
+
325
+ **Input**
326
+
327
+ ```tsx
328
+ export default function HomePage() {
329
+ return <main>Hello World</main>;
330
+ }
331
+ ```
332
+
333
+ **Output**
334
+
335
+ ```javascript
336
+ export default function HomePage() {
337
+ return /*#__PURE__*/React.createElement("main", null, "Hello World");
338
+ }
339
+ HomePage.__scopeSource = { filePath: "src/default-export.tsx", line: 1, column: 0 };
340
+ // __scopeSource injected exactly once
341
+ ```
342
+
343
+ ---
344
+
345
+ ### Full typed component (`typed-function-component.tsx`)
346
+
347
+ **Input**
348
+
349
+ ```tsx
350
+ interface ButtonProps {
351
+ variant: "primary" | "secondary" | "ghost";
352
+ size?: "sm" | "md" | "lg";
353
+ disabled?: boolean;
354
+ children: React.ReactNode;
355
+ onClick?: (e: MouseEvent) => void;
356
+ }
357
+
358
+ function Button({
359
+ variant: _variant,
360
+ size: _size = "md",
361
+ disabled = false,
362
+ children,
363
+ onClick: _onClick,
364
+ }: ButtonProps) {
365
+ return <button type="button" disabled={disabled}>{children}</button>;
366
+ }
367
+ export { Button };
368
+ ```
369
+
370
+ **Output** (`__scopeProps` excerpt)
371
+
372
+ ```javascript
373
+ Button.__scopeSource = { filePath: "src/button.tsx", line: 9, column: 0 };
374
+ Button.__scopeProps = {
375
+ variant: { type: "union", required: true, values: ["primary", "secondary", "ghost"] },
376
+ size: { type: "union", required: false, values: ["sm", "md", "lg"], defaultValue: "md" },
377
+ disabled: { type: "boolean", required: false, defaultValue: false },
378
+ children: { type: "ReactNode", required: true },
379
+ onClick: { type: "function", required: false },
380
+ };
381
+ ```
382
+
383
+ ---
384
+
385
+ ### `React.FC<Props>` arrow component (`typed-arrow-fc.tsx`)
386
+
387
+ **Input**
388
+
389
+ ```tsx
390
+ import type React from "react";
391
+
392
+ interface CardProps {
393
+ title: string;
394
+ count: number;
395
+ active?: boolean;
396
+ }
397
+
398
+ const Card: React.FC<CardProps> = ({ title, count = 0, active = false }) => (
399
+ <div className={active ? "active" : ""}>{title}: {count}</div>
400
+ );
401
+ export { Card };
402
+ ```
403
+
404
+ **Output** (`__scopeProps` excerpt)
405
+
406
+ ```javascript
407
+ Card.__scopeSource = { filePath: "src/card.tsx", line: 9, column: 0 };
408
+ Card.__scopeProps = {
409
+ title: { type: "string", required: true },
410
+ count: { type: "number", required: true, defaultValue: 0 },
411
+ active: { type: "boolean", required: false, defaultValue: false },
412
+ };
413
+ ```
414
+
415
+ ---
416
+
417
+ ### No-props component (`no-props-component.tsx`)
418
+
419
+ **Input**
420
+
421
+ ```tsx
422
+ function Header() {
423
+ return <header>Hello World</header>;
424
+ }
425
+ export { Header };
426
+ ```
427
+
428
+ **Output**
429
+
430
+ ```javascript
431
+ function Header() {
432
+ return /*#__PURE__*/React.createElement("header", null, "Hello World");
433
+ }
434
+ Header.__scopeSource = { filePath: "src/header.tsx", line: 1, column: 0 };
435
+ // __scopeProps is NOT injected — no typed props
436
+ ```
437
+
438
+ ---
439
+
440
+ ### Union type extraction (from `prop-extraction.test.ts`)
441
+
442
+ ```tsx
443
+ type StatusProps = { status: 'active' | 'inactive' | 'pending' };
444
+ function StatusBadge({ status }: StatusProps) { return <span>{status}</span>; }
445
+ ```
446
+
447
+ **Output**
448
+
449
+ ```javascript
450
+ StatusBadge.__scopeProps = {
451
+ status: { type: "union", required: true, values: ["active", "inactive", "pending"] },
452
+ };
453
+ ```
454
+
455
+ ---
456
+
457
+ ### Inline type annotation (from `prop-extraction.test.ts`)
458
+
459
+ ```tsx
460
+ function Avatar({ src, alt }: { src: string; alt?: string }) {
461
+ return <img src={src} alt={alt} />;
462
+ }
463
+ ```
464
+
465
+ **Output**
466
+
467
+ ```javascript
468
+ Avatar.__scopeProps = {
469
+ src: { type: "string", required: true },
470
+ alt: { type: "string", required: false },
471
+ };
472
+ ```
473
+
474
+ ---
475
+
476
+ ### Complex types fall back to `"unknown"` (from `prop-extraction.test.ts`)
477
+
478
+ ```tsx
479
+ interface ComplexProps { combo: A & B; } // → type: "unknown" (intersection)
480
+ interface GenericProps { data: Array<string>; } // → type: "unknown" (generic TSTypeReference)
481
+ ```
482
+
483
+ ---
484
+
485
+ ## Plugin options
486
+
487
+ ```typescript
488
+ interface PluginOptions {
489
+ /** Override the file path injected into __scopeSource.
490
+ * Defaults to Babel's `state.filename`.
491
+ * Pass this explicitly in tests to avoid absolute-path mismatches. */
492
+ filePath?: string;
493
+ }
494
+ ```
495
+
496
+ **Test setup best practice** (from KB: Babel Plugin Test Assertions): Always pass `filePath` as a plugin option in tests and assert against relative paths. Relying on Babel's automatic filename resolution produces absolute paths that break across machines:
497
+
498
+ ```javascript
499
+ transformSync(code, {
500
+ plugins: [['@agent-scope/babel-plugin', { filePath: 'src/Button.tsx' }]],
501
+ });
502
+ expect(output).toContain('filePath: "src/Button.tsx"'); // ✓ portable
503
+ ```
504
+
505
+ ---
506
+
507
+ ## Used by
508
+
509
+ - `@agent-scope/runtime` — reads `Component.__scopeSource` and `Component.__scopeProps` at runtime to enrich `PageReport` nodes with source location and prop type metadata
510
+ - `@agent-scope/cli` — uses prop type metadata to drive `scope render` matrix axis generation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/babel-plugin",
3
- "version": "1.17.1",
3
+ "version": "1.17.3",
4
4
  "description": "Babel plugin for automatic Scope instrumentation via AST transforms",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,7 +20,8 @@
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",
22
22
  "files": [
23
- "dist"
23
+ "dist",
24
+ "README.md"
24
25
  ],
25
26
  "scripts": {
26
27
  "build": "tsup",
@@ -30,7 +31,7 @@
30
31
  },
31
32
  "dependencies": {
32
33
  "@babel/core": "^7.29.0",
33
- "@agent-scope/core": "1.17.1"
34
+ "@agent-scope/core": "1.17.3"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@babel/preset-react": "^7.28.5",