npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@agent-scope/manifest

v1.20.0

Published

TypeScript AST parser that generates a machine-readable React component registry for Scope

Readme

@agent-scope/manifest

TypeScript AST parser that generates a machine-readable React component registry (manifest) from source code.

Walks TypeScript/TSX source files using ts-morph, 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.


Installation

npm install @agent-scope/manifest

What it does / when to use it

| Need | Use | |------|-----| | Generate a JSON registry of all React components in a project | generateManifest(config) | | Know which components are composed by / compose which others | manifest.tree | | Determine if a component can be rendered via Satori (SVG) or needs a browser | complexityClass | | Find which React contexts a component depends on | requiredContexts | | Know which hooks a component calls | detectedHooks | | Detect fetch calls, timers, event listener subscriptions | sideEffects | | Understand all props (types, defaults, required status) | props in ComponentDescriptor |


Manifest JSON schema

Top-level Manifest

interface Manifest {
  version: "0.1";          // schema version
  generatedAt: string;     // ISO 8601 timestamp
  components: Record<string, ComponentDescriptor>; // keyed by component name
  tree: Record<string, TreeNode>;                  // keyed by component name
}

ComponentDescriptor — every field

interface ComponentDescriptor {
  // -----------------------------------------------------------------------
  // Identity
  // -----------------------------------------------------------------------
  filePath: string;        // relative path from rootDir, e.g. "src/components/Button.tsx"
  exportType: ExportType;  // "named" | "default" | "none"
  displayName: string;     // displayName property if set, otherwise function/class name
  loc: { start: number; end: number }; // 1-based line numbers in the source file

  // -----------------------------------------------------------------------
  // Props
  // -----------------------------------------------------------------------
  props: Record<string, PropDescriptor>;  // keyed by prop name

  // -----------------------------------------------------------------------
  // Composition
  // -----------------------------------------------------------------------
  composes: string[];    // component names rendered directly in this component's JSX
  composedBy: string[];  // component names that render this component in their JSX

  // -----------------------------------------------------------------------
  // Wrappers
  // -----------------------------------------------------------------------
  forwardedRef: boolean;   // true if wrapped with React.forwardRef
  memoized: boolean;       // true if wrapped with React.memo
  hocWrappers: string[];   // HOC wrapper names (excluding memo/forwardRef)

  // -----------------------------------------------------------------------
  // Phase 6: rendering & runtime analysis
  // -----------------------------------------------------------------------
  complexityClass: ComplexityClass; // "simple" | "complex"
  requiredContexts: string[];       // sorted context variable names (e.g. ["AuthCtx", "ThemeCtx"])
  detectedHooks: string[];          // sorted hook names (e.g. ["useCallback", "useEffect", "useState"])
  sideEffects: SideEffects;
}

PropDescriptor

interface PropDescriptor {
  type: PropKind;      // resolved TypeScript type category
  values?: string[];   // for union types: expanded literal values, e.g. ["primary", "secondary"]
  default?: string;    // default value as source-code string, e.g. "'primary'"
  required: boolean;   // false if prop is optional (?) or has a default
  rawType: string;     // raw TypeScript type string from source
}

type PropKind =
  | "string" | "number" | "boolean"
  | "union" | "object" | "array"
  | "function" | "node" | "element"
  | "any" | "unknown" | "never"
  | "null" | "undefined" | "literal" | "other";

SideEffects

interface SideEffects {
  fetches: string[];        // detected fetch callee names: "fetch", "axios", "useQuery", etc.
  timers: boolean;          // setTimeout / setInterval / requestAnimationFrame detected
  subscriptions: string[];  // subscription methods: "subscribe", "onSnapshot", "addListener", etc.
  globalListeners: boolean; // window.addEventListener or document.addEventListener detected
}

TreeNode

interface TreeNode {
  children: string[]; // names of components this component renders directly
  parents: string[];  // names of components that render this component directly
}

How complexity classification works

Each component receives a complexityClass of "simple" or "complex". The classification drives rendering strategy:

  • simple — flexbox-only layout, standard box model, no animations. Safe to render via Satori (SVG-based renderer, fast).
  • complex — must render via a full browser pool. Slower but handles all CSS.

Static CSS analysis

The parser scans all inline style={{ ... }} objects in JSX for these triggers:

| Trigger | Example | Result | |---------|---------|--------| | CSS Grid properties | gridTemplateColumns, gridArea, grid | complex | | Absolute / fixed / sticky positioning | position: "absolute" | complex | | CSS animations | animation, animationName, animationDuration | complex | | CSS transitions | transition | complex | | CSS transforms | transform, transformOrigin | complex | | clipPath, willChange, contain | any of these keys | complex | | Styled-components / css template literals | styled.div`...`, css`...` | complex | | Opaque className references | className={styles.root} (identifier/call) | complex |

Anything not matching the above stays "simple".

Class components always default to "complex" (safe fallback; lifecycle methods / state patterns are too complex to analyze statically).

Complexity propagation through the composition tree

After all components are classified, complexity propagates upward through the tree:

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.

Algorithm: bottom-up BFS starting from every initially-complex component, following composedBy links upward.

Example from the basic-tree fixture:

App (simple own CSS)
└── Layout (simple own CSS)
    └── Sidebar (complex — uses `transition: "width 0.2s"`)
        └── NavItem (simple leaf — no complex CSS)

After propagation:

  • NavItemsimple (leaf, no complex descendants)
  • Sidebarcomplex (own CSS triggers it)
  • Layoutcomplex (propagated from Sidebar)
  • Appcomplex (propagated from Layout → Sidebar)

Propagation unit-test examples (from manifest.test.ts):

// Direct propagation
// Input:  Parent(simple) ← Child(complex)
// Result: Parent becomes complex
propagateComplexity({ Parent: { complexityClass: "simple", composedBy: [] },
                      Child:  { complexityClass: "complex", composedBy: ["Parent"] } });
// Parent.complexityClass === "complex"

// Transitive: A → B → C where C is complex → A and B both become complex
// Diamond: A → B, A → C, both B and C → D(complex) → A, B, C all become complex

Context detection

The parser detects which React contexts a component depends on via two strategies:

1. Direct useContext() calls

// Inside a component body:
const theme = useContext(ThemeCtx);
// → requiredContexts: ["ThemeCtx"]

2. Custom hook resolution

// Component calls a custom hook:
const theme = useTheme();

// In hooks/useTheme.ts:
export function useTheme() {
  return useContext(ThemeCtx); // ← resolved transitively
}
// → requiredContexts: ["ThemeCtx"]

The parser follows relative imports ("./hooks/useTheme") and scans the hook's source file for useContext(...) calls. Results are de-duplicated and sorted alphabetically.

deep-context fixture — 5 custom hooks each wrapping a different context:

// DeepConsumer calls: useTheme, useAuth, useLocale, useFeatureFlags, useUserPrefs
manifest.components.DeepConsumer.requiredContexts;
// ["AuthCtx", "FeatureFlagsCtx", "LocaleCtx", "ThemeCtx", "UserPrefsCtx"]

manifest.components.DeepConsumer.detectedHooks;
// ["useAuth", "useFeatureFlags", "useLocale", "useTheme", "useUserPrefs"]

Side-effect detection

All CallExpression nodes in the component body are scanned recursively (including inside useEffect, useCallback, event handlers, etc.).

| Category | Detected callees | |----------|-----------------| | fetches | fetch, axios.*, useQuery, useMutation, useSWR, useInfiniteQuery, request | | timers | setTimeout, setInterval, requestAnimationFrame, clearTimeout, clearInterval, cancelAnimationFrame | | subscriptions | .subscribe(), .onSnapshot(), .on(), .listen(), .addListener() | | globalListeners | window.addEventListener, document.addEventListener, window.removeEventListener, document.removeEventListener, bare addEventListener |

fetches and subscriptions are de-duplicated sorted arrays. timers and globalListeners are booleans.

Example from the hooks-showcase fixture:

// UseEffectDemo uses setInterval inside useEffect
manifest.components.UseEffectDemo.sideEffects.timers; // true

// UseStateDemo has no side effects
manifest.components.UseStateDemo.sideEffects;
// { fetches: [], timers: false, subscriptions: [], globalListeners: false }

generateManifest(config)

function generateManifest(config: ManifestConfig): Manifest

Generates the full component manifest by parsing all matching TypeScript/TSX files.

ManifestConfig

interface ManifestConfig {
  rootDir: string;          // absolute path to project/package root
  include?: string[];       // glob patterns — default: ["src/**/*.tsx", "src/**/*.ts"]
  exclude?: string[];       // glob patterns — default: ["**/node_modules/**", "**/*.test.*",
                            //                            "**/*.spec.*", "**/dist/**", "**/*.d.ts"]
  tsConfigFilePath?: string; // path to tsconfig.json — default: "<rootDir>/tsconfig.json"
}

Usage

import { generateManifest } from "@agent-scope/manifest";

const manifest = generateManifest({
  rootDir: "/path/to/my-project",
});

// Access a specific component
const button = manifest.components.Button;
console.log(button.props.variant);
// { type: "union", values: ["primary", "secondary", "ghost"], required: false, default: "'primary'", rawType: "Variant" }

console.log(button.complexityClass);  // "simple" or "complex"
console.log(button.detectedHooks);   // ["useCallback", "useState"]
console.log(button.composedBy);      // ["Toolbar", "Form"]
console.log(button.composes);        // ["Icon", "Spinner"]

// Walk the composition tree
const tree = manifest.tree.Button;
// { children: ["Icon", "Spinner"], parents: ["Toolbar", "Form"] }

Custom include/exclude

const manifest = generateManifest({
  rootDir: "/path/to/my-project",
  include: ["src/components/**/*.tsx", "src/features/**/*.tsx"],
  exclude: [
    "**/node_modules/**",
    "**/*.test.*",
    "**/*.stories.*",
    "**/dist/**",
  ],
  tsConfigFilePath: "/path/to/my-project/tsconfig.app.json",
});

Component detection rules

The parser discovers components from three declaration forms:

1. Function declarations

// Named PascalCase function that returns JSX
export function Button({ label, onClick }: ButtonProps) {
  return <button onClick={onClick}>{label}</button>;
}

2. Arrow functions / variable declarations (including wrappers)

// Arrow function
export const Card = ({ title }: CardProps) => <div>{title}</div>;

// React.memo wrapper
export const Layout = React.memo(function Layout({ children }: LayoutProps) {
  return <main>{children}</main>;
});

// React.forwardRef wrapper
export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ value }, ref) => <input ref={ref} value={value} />,
);

// Export-default memo: export default React.memo(Sidebar)
export default React.memo(Sidebar);

3. Class components

export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
  render() {
    if (this.state.hasError) return <ErrorFallback />;
    return this.props.children;
  }
}
// Always gets complexityClass: "complex"
// detectedHooks: [] (hooks not valid in class components)

Manifest query patterns

// All simple components (Satori-renderable)
const simpleComponents = Object.entries(manifest.components)
  .filter(([, desc]) => desc.complexityClass === "simple")
  .map(([name]) => name);

// All components that use a specific context
const themeConsumers = Object.entries(manifest.components)
  .filter(([, desc]) => desc.requiredContexts.includes("ThemeCtx"))
  .map(([name]) => name);

// Leaf components (no children in the composition tree)
const leaves = Object.entries(manifest.tree)
  .filter(([, node]) => node.children.length === 0)
  .map(([name]) => name);

// Root components (no parents)
const roots = Object.entries(manifest.tree)
  .filter(([, node]) => node.parents.length === 0)
  .map(([name]) => name);

// Components with data fetching
const fetchers = Object.entries(manifest.components)
  .filter(([, desc]) => desc.sideEffects.fetches.length > 0)
  .map(([name, desc]) => ({ name, fetches: desc.sideEffects.fetches }));

// Components with default-exported named exports
const defaultExports = Object.entries(manifest.components)
  .filter(([, desc]) => desc.exportType === "default")
  .map(([name]) => name);

// All components with a specific prop
const hasVariantProp = Object.entries(manifest.components)
  .filter(([, desc]) => "variant" in desc.props)
  .map(([name, desc]) => ({ name, variant: desc.props.variant }));

Example payloads

manifest.components.Sidebar (from basic-tree fixture)

{
  "filePath": "src/Sidebar.tsx",
  "exportType": "default",
  "displayName": "Sidebar",
  "props": {
    "title":     { "type": "string",  "required": true, "rawType": "string" },
    "collapsed": { "type": "boolean", "required": true, "rawType": "boolean" },
    "itemCount": { "type": "number",  "required": true, "rawType": "number" }
  },
  "composes":    ["NavItem"],
  "composedBy":  ["Layout"],
  "forwardedRef": false,
  "memoized":    true,
  "hocWrappers": [],
  "loc": { "start": 10, "end": 40 },
  "complexityClass": "complex",
  "requiredContexts": [],
  "detectedHooks": [],
  "sideEffects": {
    "fetches": [],
    "timers": false,
    "subscriptions": [],
    "globalListeners": false
  }
}

Note: complexityClass: "complex" because Sidebar uses transition: "width 0.2s" in its inline style.

manifest.components.DeepConsumer (from deep-context fixture)

{
  "filePath": "src/DeepConsumer.tsx",
  "exportType": "named",
  "displayName": "DeepConsumer",
  "props": {},
  "composes": [],
  "composedBy": ["App"],
  "forwardedRef": false,
  "memoized": false,
  "hocWrappers": [],
  "loc": { "start": 1, "end": 25 },
  "complexityClass": "simple",
  "requiredContexts": ["AuthCtx", "FeatureFlagsCtx", "LocaleCtx", "ThemeCtx", "UserPrefsCtx"],
  "detectedHooks": ["useAuth", "useFeatureFlags", "useLocale", "useTheme", "useUserPrefs"],
  "sideEffects": {
    "fetches": [],
    "timers": false,
    "subscriptions": [],
    "globalListeners": false
  }
}

manifest.tree (from basic-tree fixture)

{
  "App":     { "children": ["Layout"],  "parents": [] },
  "Layout":  { "children": ["Sidebar"], "parents": ["App"] },
  "Sidebar": { "children": ["NavItem"], "parents": ["Layout"] },
  "NavItem": { "children": [],          "parents": ["Sidebar"] }
}

Internal architecture

| Module | Responsibility | |--------|----------------| | types.ts | All TypeScript types (ComponentDescriptor, PropDescriptor, Manifest, ManifestConfig, etc.) | | analysis.ts | analyzeComplexity, detectHooks, detectRequiredContexts, detectSideEffects, propagateComplexity — all static analysis | | 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 | | index.ts | Public re-exports |

Processing pipeline

ManifestConfig
    │
    ▼
ts-morph Project (tsconfig-aware)
    │
    ▼
Source file filtering (include/exclude globs)
    │
    ├─ for each SourceFile:
    │   ├─ processSourceFile()
    │   │   ├─ Function declarations → extract props, JSX compositions, wrappers
    │   │   ├─ Variable declarations (arrow fns, memo, forwardRef)
    │   │   └─ Class components
    │   │
    │   └─ per component:
    │       ├─ analyzeComplexity()     — CSS feature scan
    │       ├─ detectHooks()           — /^use[A-Z]/ pattern
    │       ├─ detectRequiredContexts() — useContext() + hook resolution
    │       └─ detectSideEffects()     — fetch/timer/subscription/global patterns
    │
    ├─ Build composedBy (inverse of composes)
    ├─ propagateComplexity() — upward BFS from complex leaves
    └─ Build tree (filtered to manifest-known components)

Used by

  • @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
  • @agent-scope/rendersatori.ts and matrix.ts consume complexityClass and ComponentDescriptor for render strategy decisions