@agent-scope/manifest 1.17.1 → 1.17.2

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 +522 -0
  2. package/package.json +4 -3
package/README.md ADDED
@@ -0,0 +1,522 @@
1
+ # @agent-scope/manifest
2
+
3
+ TypeScript AST parser that generates a machine-readable React component registry (manifest) from source code.
4
+
5
+ Walks TypeScript/TSX source files using [ts-morph](https://ts-morph.com/), extracts every React component (function, arrow, class), resolves props types, detects hooks, context dependencies, side effects, renders complexity, and builds a full bi-directional composition tree.
6
+
7
+ ---
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm install @agent-scope/manifest
13
+ ```
14
+
15
+ ---
16
+
17
+ ## What it does / when to use it
18
+
19
+ | Need | Use |
20
+ |------|-----|
21
+ | Generate a JSON registry of all React components in a project | `generateManifest(config)` |
22
+ | Know which components are composed by / compose which others | `manifest.tree` |
23
+ | Determine if a component can be rendered via Satori (SVG) or needs a browser | `complexityClass` |
24
+ | Find which React contexts a component depends on | `requiredContexts` |
25
+ | Know which hooks a component calls | `detectedHooks` |
26
+ | Detect fetch calls, timers, event listener subscriptions | `sideEffects` |
27
+ | Understand all props (types, defaults, required status) | `props` in `ComponentDescriptor` |
28
+
29
+ ---
30
+
31
+ ## Manifest JSON schema
32
+
33
+ ### Top-level `Manifest`
34
+
35
+ ```typescript
36
+ interface Manifest {
37
+ version: "0.1"; // schema version
38
+ generatedAt: string; // ISO 8601 timestamp
39
+ components: Record<string, ComponentDescriptor>; // keyed by component name
40
+ tree: Record<string, TreeNode>; // keyed by component name
41
+ }
42
+ ```
43
+
44
+ ### `ComponentDescriptor` — every field
45
+
46
+ ```typescript
47
+ interface ComponentDescriptor {
48
+ // -----------------------------------------------------------------------
49
+ // Identity
50
+ // -----------------------------------------------------------------------
51
+ filePath: string; // relative path from rootDir, e.g. "src/components/Button.tsx"
52
+ exportType: ExportType; // "named" | "default" | "none"
53
+ displayName: string; // displayName property if set, otherwise function/class name
54
+ loc: { start: number; end: number }; // 1-based line numbers in the source file
55
+
56
+ // -----------------------------------------------------------------------
57
+ // Props
58
+ // -----------------------------------------------------------------------
59
+ props: Record<string, PropDescriptor>; // keyed by prop name
60
+
61
+ // -----------------------------------------------------------------------
62
+ // Composition
63
+ // -----------------------------------------------------------------------
64
+ composes: string[]; // component names rendered directly in this component's JSX
65
+ composedBy: string[]; // component names that render this component in their JSX
66
+
67
+ // -----------------------------------------------------------------------
68
+ // Wrappers
69
+ // -----------------------------------------------------------------------
70
+ forwardedRef: boolean; // true if wrapped with React.forwardRef
71
+ memoized: boolean; // true if wrapped with React.memo
72
+ hocWrappers: string[]; // HOC wrapper names (excluding memo/forwardRef)
73
+
74
+ // -----------------------------------------------------------------------
75
+ // Phase 6: rendering & runtime analysis
76
+ // -----------------------------------------------------------------------
77
+ complexityClass: ComplexityClass; // "simple" | "complex"
78
+ requiredContexts: string[]; // sorted context variable names (e.g. ["AuthCtx", "ThemeCtx"])
79
+ detectedHooks: string[]; // sorted hook names (e.g. ["useCallback", "useEffect", "useState"])
80
+ sideEffects: SideEffects;
81
+ }
82
+ ```
83
+
84
+ ### `PropDescriptor`
85
+
86
+ ```typescript
87
+ interface PropDescriptor {
88
+ type: PropKind; // resolved TypeScript type category
89
+ values?: string[]; // for union types: expanded literal values, e.g. ["primary", "secondary"]
90
+ default?: string; // default value as source-code string, e.g. "'primary'"
91
+ required: boolean; // false if prop is optional (?) or has a default
92
+ rawType: string; // raw TypeScript type string from source
93
+ }
94
+
95
+ type PropKind =
96
+ | "string" | "number" | "boolean"
97
+ | "union" | "object" | "array"
98
+ | "function" | "node" | "element"
99
+ | "any" | "unknown" | "never"
100
+ | "null" | "undefined" | "literal" | "other";
101
+ ```
102
+
103
+ ### `SideEffects`
104
+
105
+ ```typescript
106
+ interface SideEffects {
107
+ fetches: string[]; // detected fetch callee names: "fetch", "axios", "useQuery", etc.
108
+ timers: boolean; // setTimeout / setInterval / requestAnimationFrame detected
109
+ subscriptions: string[]; // subscription methods: "subscribe", "onSnapshot", "addListener", etc.
110
+ globalListeners: boolean; // window.addEventListener or document.addEventListener detected
111
+ }
112
+ ```
113
+
114
+ ### `TreeNode`
115
+
116
+ ```typescript
117
+ interface TreeNode {
118
+ children: string[]; // names of components this component renders directly
119
+ parents: string[]; // names of components that render this component directly
120
+ }
121
+ ```
122
+
123
+ ---
124
+
125
+ ## How complexity classification works
126
+
127
+ Each component receives a `complexityClass` of `"simple"` or `"complex"`. The classification drives rendering strategy:
128
+
129
+ - **`simple`** — flexbox-only layout, standard box model, no animations. Safe to render via Satori (SVG-based renderer, fast).
130
+ - **`complex`** — must render via a full browser pool. Slower but handles all CSS.
131
+
132
+ ### Static CSS analysis
133
+
134
+ The parser scans all inline `style={{ ... }}` objects in JSX for these triggers:
135
+
136
+ | Trigger | Example | Result |
137
+ |---------|---------|--------|
138
+ | CSS Grid properties | `gridTemplateColumns`, `gridArea`, `grid` | `complex` |
139
+ | Absolute / fixed / sticky positioning | `position: "absolute"` | `complex` |
140
+ | CSS animations | `animation`, `animationName`, `animationDuration` | `complex` |
141
+ | CSS transitions | `transition` | `complex` |
142
+ | CSS transforms | `transform`, `transformOrigin` | `complex` |
143
+ | `clipPath`, `willChange`, `contain` | any of these keys | `complex` |
144
+ | Styled-components / `css` template literals | `` styled.div`...` ``, `` css`...` `` | `complex` |
145
+ | Opaque `className` references | `className={styles.root}` (identifier/call) | `complex` |
146
+
147
+ Anything not matching the above stays `"simple"`.
148
+
149
+ **Class components** always default to `"complex"` (safe fallback; lifecycle methods / state patterns are too complex to analyze statically).
150
+
151
+ ### Complexity propagation through the composition tree
152
+
153
+ After all components are classified, complexity propagates **upward** through the tree:
154
+
155
+ > A component is `simple` only if it **and every descendant** are also `simple`. If any child anywhere in the subtree is `complex`, all ancestors are also marked `complex`.
156
+
157
+ Algorithm: bottom-up BFS starting from every initially-complex component, following `composedBy` links upward.
158
+
159
+ **Example from the `basic-tree` fixture:**
160
+
161
+ ```
162
+ App (simple own CSS)
163
+ └── Layout (simple own CSS)
164
+ └── Sidebar (complex — uses `transition: "width 0.2s"`)
165
+ └── NavItem (simple leaf — no complex CSS)
166
+ ```
167
+
168
+ After propagation:
169
+ - `NavItem` → `simple` (leaf, no complex descendants)
170
+ - `Sidebar` → `complex` (own CSS triggers it)
171
+ - `Layout` → `complex` (propagated from Sidebar)
172
+ - `App` → `complex` (propagated from Layout → Sidebar)
173
+
174
+ Propagation unit-test examples (from `manifest.test.ts`):
175
+
176
+ ```typescript
177
+ // Direct propagation
178
+ // Input: Parent(simple) ← Child(complex)
179
+ // Result: Parent becomes complex
180
+ propagateComplexity({ Parent: { complexityClass: "simple", composedBy: [] },
181
+ Child: { complexityClass: "complex", composedBy: ["Parent"] } });
182
+ // Parent.complexityClass === "complex"
183
+
184
+ // Transitive: A → B → C where C is complex → A and B both become complex
185
+ // Diamond: A → B, A → C, both B and C → D(complex) → A, B, C all become complex
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Context detection
191
+
192
+ The parser detects which React contexts a component depends on via two strategies:
193
+
194
+ ### 1. Direct `useContext()` calls
195
+
196
+ ```tsx
197
+ // Inside a component body:
198
+ const theme = useContext(ThemeCtx);
199
+ // → requiredContexts: ["ThemeCtx"]
200
+ ```
201
+
202
+ ### 2. Custom hook resolution
203
+
204
+ ```tsx
205
+ // Component calls a custom hook:
206
+ const theme = useTheme();
207
+
208
+ // In hooks/useTheme.ts:
209
+ export function useTheme() {
210
+ return useContext(ThemeCtx); // ← resolved transitively
211
+ }
212
+ // → requiredContexts: ["ThemeCtx"]
213
+ ```
214
+
215
+ The parser follows relative imports (`"./hooks/useTheme"`) and scans the hook's source file for `useContext(...)` calls. Results are de-duplicated and sorted alphabetically.
216
+
217
+ **`deep-context` fixture** — 5 custom hooks each wrapping a different context:
218
+
219
+ ```typescript
220
+ // DeepConsumer calls: useTheme, useAuth, useLocale, useFeatureFlags, useUserPrefs
221
+ manifest.components.DeepConsumer.requiredContexts;
222
+ // ["AuthCtx", "FeatureFlagsCtx", "LocaleCtx", "ThemeCtx", "UserPrefsCtx"]
223
+
224
+ manifest.components.DeepConsumer.detectedHooks;
225
+ // ["useAuth", "useFeatureFlags", "useLocale", "useTheme", "useUserPrefs"]
226
+ ```
227
+
228
+ ---
229
+
230
+ ## Side-effect detection
231
+
232
+ All `CallExpression` nodes in the component body are scanned recursively (including inside `useEffect`, `useCallback`, event handlers, etc.).
233
+
234
+ | Category | Detected callees |
235
+ |----------|-----------------|
236
+ | `fetches` | `fetch`, `axios.*`, `useQuery`, `useMutation`, `useSWR`, `useInfiniteQuery`, `request` |
237
+ | `timers` | `setTimeout`, `setInterval`, `requestAnimationFrame`, `clearTimeout`, `clearInterval`, `cancelAnimationFrame` |
238
+ | `subscriptions` | `.subscribe()`, `.onSnapshot()`, `.on()`, `.listen()`, `.addListener()` |
239
+ | `globalListeners` | `window.addEventListener`, `document.addEventListener`, `window.removeEventListener`, `document.removeEventListener`, bare `addEventListener` |
240
+
241
+ `fetches` and `subscriptions` are de-duplicated sorted arrays. `timers` and `globalListeners` are booleans.
242
+
243
+ **Example from the `hooks-showcase` fixture:**
244
+
245
+ ```typescript
246
+ // UseEffectDemo uses setInterval inside useEffect
247
+ manifest.components.UseEffectDemo.sideEffects.timers; // true
248
+
249
+ // UseStateDemo has no side effects
250
+ manifest.components.UseStateDemo.sideEffects;
251
+ // { fetches: [], timers: false, subscriptions: [], globalListeners: false }
252
+ ```
253
+
254
+ ---
255
+
256
+ ## `generateManifest(config)`
257
+
258
+ ```typescript
259
+ function generateManifest(config: ManifestConfig): Manifest
260
+ ```
261
+
262
+ Generates the full component manifest by parsing all matching TypeScript/TSX files.
263
+
264
+ ### `ManifestConfig`
265
+
266
+ ```typescript
267
+ interface ManifestConfig {
268
+ rootDir: string; // absolute path to project/package root
269
+ include?: string[]; // glob patterns — default: ["src/**/*.tsx", "src/**/*.ts"]
270
+ exclude?: string[]; // glob patterns — default: ["**/node_modules/**", "**/*.test.*",
271
+ // "**/*.spec.*", "**/dist/**", "**/*.d.ts"]
272
+ tsConfigFilePath?: string; // path to tsconfig.json — default: "<rootDir>/tsconfig.json"
273
+ }
274
+ ```
275
+
276
+ ### Usage
277
+
278
+ ```typescript
279
+ import { generateManifest } from "@agent-scope/manifest";
280
+
281
+ const manifest = generateManifest({
282
+ rootDir: "/path/to/my-project",
283
+ });
284
+
285
+ // Access a specific component
286
+ const button = manifest.components.Button;
287
+ console.log(button.props.variant);
288
+ // { type: "union", values: ["primary", "secondary", "ghost"], required: false, default: "'primary'", rawType: "Variant" }
289
+
290
+ console.log(button.complexityClass); // "simple" or "complex"
291
+ console.log(button.detectedHooks); // ["useCallback", "useState"]
292
+ console.log(button.composedBy); // ["Toolbar", "Form"]
293
+ console.log(button.composes); // ["Icon", "Spinner"]
294
+
295
+ // Walk the composition tree
296
+ const tree = manifest.tree.Button;
297
+ // { children: ["Icon", "Spinner"], parents: ["Toolbar", "Form"] }
298
+ ```
299
+
300
+ ### Custom include/exclude
301
+
302
+ ```typescript
303
+ const manifest = generateManifest({
304
+ rootDir: "/path/to/my-project",
305
+ include: ["src/components/**/*.tsx", "src/features/**/*.tsx"],
306
+ exclude: [
307
+ "**/node_modules/**",
308
+ "**/*.test.*",
309
+ "**/*.stories.*",
310
+ "**/dist/**",
311
+ ],
312
+ tsConfigFilePath: "/path/to/my-project/tsconfig.app.json",
313
+ });
314
+ ```
315
+
316
+ ---
317
+
318
+ ## Component detection rules
319
+
320
+ The parser discovers components from three declaration forms:
321
+
322
+ ### 1. Function declarations
323
+
324
+ ```tsx
325
+ // Named PascalCase function that returns JSX
326
+ export function Button({ label, onClick }: ButtonProps) {
327
+ return <button onClick={onClick}>{label}</button>;
328
+ }
329
+ ```
330
+
331
+ ### 2. Arrow functions / variable declarations (including wrappers)
332
+
333
+ ```tsx
334
+ // Arrow function
335
+ export const Card = ({ title }: CardProps) => <div>{title}</div>;
336
+
337
+ // React.memo wrapper
338
+ export const Layout = React.memo(function Layout({ children }: LayoutProps) {
339
+ return <main>{children}</main>;
340
+ });
341
+
342
+ // React.forwardRef wrapper
343
+ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
344
+ ({ value }, ref) => <input ref={ref} value={value} />,
345
+ );
346
+
347
+ // Export-default memo: export default React.memo(Sidebar)
348
+ export default React.memo(Sidebar);
349
+ ```
350
+
351
+ ### 3. Class components
352
+
353
+ ```tsx
354
+ export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
355
+ render() {
356
+ if (this.state.hasError) return <ErrorFallback />;
357
+ return this.props.children;
358
+ }
359
+ }
360
+ // Always gets complexityClass: "complex"
361
+ // detectedHooks: [] (hooks not valid in class components)
362
+ ```
363
+
364
+ ---
365
+
366
+ ## Manifest query patterns
367
+
368
+ ```typescript
369
+ // All simple components (Satori-renderable)
370
+ const simpleComponents = Object.entries(manifest.components)
371
+ .filter(([, desc]) => desc.complexityClass === "simple")
372
+ .map(([name]) => name);
373
+
374
+ // All components that use a specific context
375
+ const themeConsumers = Object.entries(manifest.components)
376
+ .filter(([, desc]) => desc.requiredContexts.includes("ThemeCtx"))
377
+ .map(([name]) => name);
378
+
379
+ // Leaf components (no children in the composition tree)
380
+ const leaves = Object.entries(manifest.tree)
381
+ .filter(([, node]) => node.children.length === 0)
382
+ .map(([name]) => name);
383
+
384
+ // Root components (no parents)
385
+ const roots = Object.entries(manifest.tree)
386
+ .filter(([, node]) => node.parents.length === 0)
387
+ .map(([name]) => name);
388
+
389
+ // Components with data fetching
390
+ const fetchers = Object.entries(manifest.components)
391
+ .filter(([, desc]) => desc.sideEffects.fetches.length > 0)
392
+ .map(([name, desc]) => ({ name, fetches: desc.sideEffects.fetches }));
393
+
394
+ // Components with default-exported named exports
395
+ const defaultExports = Object.entries(manifest.components)
396
+ .filter(([, desc]) => desc.exportType === "default")
397
+ .map(([name]) => name);
398
+
399
+ // All components with a specific prop
400
+ const hasVariantProp = Object.entries(manifest.components)
401
+ .filter(([, desc]) => "variant" in desc.props)
402
+ .map(([name, desc]) => ({ name, variant: desc.props.variant }));
403
+ ```
404
+
405
+ ---
406
+
407
+ ## Example payloads
408
+
409
+ ### `manifest.components.Sidebar` (from `basic-tree` fixture)
410
+
411
+ ```json
412
+ {
413
+ "filePath": "src/Sidebar.tsx",
414
+ "exportType": "default",
415
+ "displayName": "Sidebar",
416
+ "props": {
417
+ "title": { "type": "string", "required": true, "rawType": "string" },
418
+ "collapsed": { "type": "boolean", "required": true, "rawType": "boolean" },
419
+ "itemCount": { "type": "number", "required": true, "rawType": "number" }
420
+ },
421
+ "composes": ["NavItem"],
422
+ "composedBy": ["Layout"],
423
+ "forwardedRef": false,
424
+ "memoized": true,
425
+ "hocWrappers": [],
426
+ "loc": { "start": 10, "end": 40 },
427
+ "complexityClass": "complex",
428
+ "requiredContexts": [],
429
+ "detectedHooks": [],
430
+ "sideEffects": {
431
+ "fetches": [],
432
+ "timers": false,
433
+ "subscriptions": [],
434
+ "globalListeners": false
435
+ }
436
+ }
437
+ ```
438
+
439
+ Note: `complexityClass: "complex"` because Sidebar uses `transition: "width 0.2s"` in its inline style.
440
+
441
+ ### `manifest.components.DeepConsumer` (from `deep-context` fixture)
442
+
443
+ ```json
444
+ {
445
+ "filePath": "src/DeepConsumer.tsx",
446
+ "exportType": "named",
447
+ "displayName": "DeepConsumer",
448
+ "props": {},
449
+ "composes": [],
450
+ "composedBy": ["App"],
451
+ "forwardedRef": false,
452
+ "memoized": false,
453
+ "hocWrappers": [],
454
+ "loc": { "start": 1, "end": 25 },
455
+ "complexityClass": "simple",
456
+ "requiredContexts": ["AuthCtx", "FeatureFlagsCtx", "LocaleCtx", "ThemeCtx", "UserPrefsCtx"],
457
+ "detectedHooks": ["useAuth", "useFeatureFlags", "useLocale", "useTheme", "useUserPrefs"],
458
+ "sideEffects": {
459
+ "fetches": [],
460
+ "timers": false,
461
+ "subscriptions": [],
462
+ "globalListeners": false
463
+ }
464
+ }
465
+ ```
466
+
467
+ ### `manifest.tree` (from `basic-tree` fixture)
468
+
469
+ ```json
470
+ {
471
+ "App": { "children": ["Layout"], "parents": [] },
472
+ "Layout": { "children": ["Sidebar"], "parents": ["App"] },
473
+ "Sidebar": { "children": ["NavItem"], "parents": ["Layout"] },
474
+ "NavItem": { "children": [], "parents": ["Sidebar"] }
475
+ }
476
+ ```
477
+
478
+ ---
479
+
480
+ ## Internal architecture
481
+
482
+ | Module | Responsibility |
483
+ |--------|----------------|
484
+ | `types.ts` | All TypeScript types (`ComponentDescriptor`, `PropDescriptor`, `Manifest`, `ManifestConfig`, etc.) |
485
+ | `analysis.ts` | `analyzeComplexity`, `detectHooks`, `detectRequiredContexts`, `detectSideEffects`, `propagateComplexity` — all static analysis |
486
+ | `parser.ts` | `generateManifest` — ts-morph project setup, source file iteration, per-file extraction (functions, arrow functions, class components), composition inverse linking, complexity propagation, tree building |
487
+ | `index.ts` | Public re-exports |
488
+
489
+ ### Processing pipeline
490
+
491
+ ```
492
+ ManifestConfig
493
+
494
+
495
+ ts-morph Project (tsconfig-aware)
496
+
497
+
498
+ Source file filtering (include/exclude globs)
499
+
500
+ ├─ for each SourceFile:
501
+ │ ├─ processSourceFile()
502
+ │ │ ├─ Function declarations → extract props, JSX compositions, wrappers
503
+ │ │ ├─ Variable declarations (arrow fns, memo, forwardRef)
504
+ │ │ └─ Class components
505
+ │ │
506
+ │ └─ per component:
507
+ │ ├─ analyzeComplexity() — CSS feature scan
508
+ │ ├─ detectHooks() — /^use[A-Z]/ pattern
509
+ │ ├─ detectRequiredContexts() — useContext() + hook resolution
510
+ │ └─ detectSideEffects() — fetch/timer/subscription/global patterns
511
+
512
+ ├─ Build composedBy (inverse of composes)
513
+ ├─ propagateComplexity() — upward BFS from complex leaves
514
+ └─ Build tree (filtered to manifest-known components)
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Used by
520
+
521
+ - `@agent-scope/cli` — manifest commands (`scope manifest generate`, `scope manifest list`, `scope manifest show`), render commands (uses `complexityClass` to choose Satori vs browser pool), CI commands
522
+ - `@agent-scope/render` — `satori.ts` and `matrix.ts` consume `complexityClass` and `ComponentDescriptor` for render strategy decisions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-scope/manifest",
3
- "version": "1.17.1",
3
+ "version": "1.17.2",
4
4
  "description": "TypeScript AST parser that generates a machine-readable React component registry for Scope",
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",
@@ -29,7 +30,7 @@
29
30
  "clean": "rm -rf dist"
30
31
  },
31
32
  "dependencies": {
32
- "@agent-scope/core": "1.17.1",
33
+ "@agent-scope/core": "1.17.2",
33
34
  "ts-morph": "^25.0.0"
34
35
  },
35
36
  "devDependencies": {