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/babel-plugin

v1.21.11

Published

Babel plugin for automatic Scope instrumentation via AST transforms

Readme

@agent-scope/babel-plugin

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.

Installation

npm install --save-dev @agent-scope/babel-plugin
# or
bun add -d @agent-scope/babel-plugin

What it does

For each detected React component the plugin emits two assignment statements immediately after the component definition:

// Input
interface ButtonProps {
  variant: 'primary' | 'secondary' | 'ghost';
  size?: 'sm' | 'md' | 'lg';
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: (e: MouseEvent) => void;
}

function Button({ variant, size = 'md', disabled = false, children, onClick }: ButtonProps) {
  return <button type="button" disabled={disabled}>{children}</button>;
}

// Output (after transform)
function Button({ variant, size = 'md', disabled = false, children, onClick }) {
  return /*#__PURE__*/React.createElement("button", { type: "button", disabled }, children);
}
Button.__scopeSource = { filePath: "src/Button.tsx", line: 9, column: 0 };
Button.__scopeProps = {
  variant:  { type: "union",      required: true,  values: ["primary", "secondary", "ghost"] },
  size:     { type: "union",      required: false, values: ["sm", "md", "lg"], defaultValue: "md" },
  disabled: { type: "boolean",    required: false, defaultValue: false },
  children: { type: "ReactNode",  required: true },
  onClick:  { type: "function",   required: false },
};

Architecture

Program.enter
  └── Pre-collect all TS interface / type alias declarations
      into a WeakMap<Program, Map<typeName, TSTypeElement[]>>
      (before @babel/preset-typescript strips them)

FunctionDeclaration / VariableDeclaration / ExportDefaultDeclaration
  └── component-detector.ts
      ├── isComponentName()  → name starts with uppercase
      └── bodyContainsJSX()  → body has a return statement returning JSX
               │
               ▼
      source-injector.ts
      └── buildScopeSourceStatement() → AST node for __scopeSource = {...}

      prop-type-extractor.ts
      └── resolveTypeReferenceName()  → look up interface/type in pre-collected map
          extractPropsFromTypeMembers() → PropMap
          extractDefaultValues()       → defaults from destructuring
               │
               ▼
      props-injector.ts
      └── buildScopePropsStatement() → AST node for __scopeProps = {...}

Why Program.enter pre-collection?

@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.


Component detection rules

A function is treated as a React component if:

  1. Name starts with an uppercase letterisComponentName(name) checks /^[A-Z]/
  2. Body returns JSXbodyContainsJSX() checks for a ReturnStatement whose argument is a JSXElement or JSXFragment (including via ternary and logical expressions one level deep)

Detected patterns:

| Pattern | Example | |---|---| | Named function declaration | function Button() { return <div/>; } | | Arrow function variable | const Card = () => <div/>; | | Function expression variable | const Card = function() { return <div/>; }; | | React.memo() wrapper | const Memo = React.memo(() => <div/>); | | React.forwardRef() wrapper | const Ref = React.forwardRef((ref, p) => <input ref={ref}/>); | | Named default export | export default function Page() { return <main/>; } | | Anonymous default export (arrow/fn) | export default () => <div/>; |

Not detected (intentionally excluded):

  • Lowercase-named functions: function formatDate(d) → skipped
  • React hooks: function useCounter() → skipped (lowercase 'u')
  • Functions that don't return JSX: function GetData() { return fetch(...); } → skipped

__scopeSource injection

Injected shape

ComponentName.__scopeSource = {
  filePath: string;  // relative path (or opts.filePath override)
  line: number;      // 1-based line number of the component declaration
  column: number;    // 0-based column number
};

filePath resolution (from plugin.test.ts)

Pass filePath in plugin options to override Babel's automatic filename. This is critical in test environments where absolute paths vary by machine:

// babel.config.js
module.exports = {
  plugins: [['@agent-scope/babel-plugin', { filePath: 'src/Button.tsx' }]],
};

Without filePath, Babel's state.filename is used (often an absolute path).


__scopeProps injection

Injected shape

ComponentName.__scopeProps = {
  [propName: string]: {
    type: PropTypeString;     // simplified type category
    required: boolean;        // true if prop has no '?'
    values?: string[];        // only for 'union' type — the literal string values
    defaultValue?: unknown;   // extracted from destructuring default, if present
  };
};

PropTypeString values

| Value | TypeScript types that map to it | |---|---| | "string" | string | | "number" | number | | "boolean" | boolean | | "union" | A union of all string/number literals: 'a' \| 'b' \| 'c' | | "function" | () => void, MouseEvent, ChangeEvent, React.*Handler | | "ReactNode" | React.ReactNode, ReactNode, ReactElement, React.ReactElement | | "object" | { ... } (TSTypeLiteral), mapped types | | "array" | T[], tuples | | "unknown" | Intersection, conditional, generic references, any, never, etc. |

Default value extraction

Destructuring defaults are extracted from the first parameter:

function Button({ size = 'md', disabled = false, count = 0 }: ButtonProps) { ... }
// → size.defaultValue: "md", disabled.defaultValue: false, count.defaultValue: 0

Supported literal default types: string, number, boolean, null, and negative numbers (-1).


Configuration

babel.config.js

module.exports = {
  plugins: ['@agent-scope/babel-plugin'],
};

babel.config.js with filePath override

module.exports = {
  plugins: [
    ['@agent-scope/babel-plugin', { filePath: 'src/MyComponent.tsx' }],
  ],
};

Vite (vite.config.ts)

import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [
    react({
      babel: {
        plugins: ['@agent-scope/babel-plugin'],
      },
    }),
  ],
});

Next.js (next.config.js)

module.exports = {
  experimental: { forceSwcTransforms: false },
  babel: {
    plugins: ['@agent-scope/babel-plugin'],
  },
};

Transform examples (from test fixtures)

Simple function component (simple-component.tsx)

Input

function Button({ label }: { label: string }) {
  return <button type="button">{label}</button>;
}
export { Button };

Output (TypeScript stripped, JSX transformed)

function Button({ label }) {
  return /*#__PURE__*/React.createElement("button", { type: "button" }, label);
}
Button.__scopeSource = { filePath: "src/simple-component.tsx", line: 1, column: 0 };
Button.__scopeProps = {
  label: { type: "string", required: true },
};

Arrow function component (arrow-component.tsx)

Input

const Card = ({ title }: { title: string }) => <div className="card">{title}</div>;
export { Card };

Output

const Card = ({ title }) => /*#__PURE__*/React.createElement("div", { className: "card" }, title);
Card.__scopeSource = { filePath: "src/arrow-component.tsx", line: 1, column: 0 };
Card.__scopeProps = {
  title: { type: "string", required: true },
};

React.memo wrapper (memo-component.tsx)

Input

import React from "react";
const MemoButton = React.memo(({ label }: { label: string }) => (
  <button type="button">{label}</button>
));
export { MemoButton };

Output

const MemoButton = React.memo(({ label }) =>
  /*#__PURE__*/React.createElement("button", { type: "button" }, label)
);
MemoButton.__scopeSource = { filePath: "src/memo-component.tsx", line: 3, column: 0 };
MemoButton.__scopeProps = { label: { type: "string", required: true } };

React.forwardRef wrapper (forwardref-component.tsx)

Input

import React from "react";
const FancyInput = React.forwardRef<HTMLInputElement, { placeholder: string }>(
  ({ placeholder }, ref) => <input ref={ref} placeholder={placeholder} />,
);
export { FancyInput };

Output

const FancyInput = React.forwardRef(({ placeholder }, ref) =>
  /*#__PURE__*/React.createElement("input", { ref, placeholder })
);
FancyInput.__scopeSource = { filePath: "src/forwardref-component.tsx", line: 3, column: 0 };

Named default export (default-export.tsx)

Input

export default function HomePage() {
  return <main>Hello World</main>;
}

Output

export default function HomePage() {
  return /*#__PURE__*/React.createElement("main", null, "Hello World");
}
HomePage.__scopeSource = { filePath: "src/default-export.tsx", line: 1, column: 0 };
// __scopeSource injected exactly once

Full typed component (typed-function-component.tsx)

Input

interface ButtonProps {
  variant: "primary" | "secondary" | "ghost";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  children: React.ReactNode;
  onClick?: (e: MouseEvent) => void;
}

function Button({
  variant: _variant,
  size: _size = "md",
  disabled = false,
  children,
  onClick: _onClick,
}: ButtonProps) {
  return <button type="button" disabled={disabled}>{children}</button>;
}
export { Button };

Output (__scopeProps excerpt)

Button.__scopeSource = { filePath: "src/button.tsx", line: 9, column: 0 };
Button.__scopeProps = {
  variant:  { type: "union",     required: true,  values: ["primary", "secondary", "ghost"] },
  size:     { type: "union",     required: false, values: ["sm", "md", "lg"], defaultValue: "md" },
  disabled: { type: "boolean",   required: false, defaultValue: false },
  children: { type: "ReactNode", required: true },
  onClick:  { type: "function",  required: false },
};

React.FC<Props> arrow component (typed-arrow-fc.tsx)

Input

import type React from "react";

interface CardProps {
  title: string;
  count: number;
  active?: boolean;
}

const Card: React.FC<CardProps> = ({ title, count = 0, active = false }) => (
  <div className={active ? "active" : ""}>{title}: {count}</div>
);
export { Card };

Output (__scopeProps excerpt)

Card.__scopeSource = { filePath: "src/card.tsx", line: 9, column: 0 };
Card.__scopeProps = {
  title:  { type: "string",  required: true },
  count:  { type: "number",  required: true, defaultValue: 0 },
  active: { type: "boolean", required: false, defaultValue: false },
};

No-props component (no-props-component.tsx)

Input

function Header() {
  return <header>Hello World</header>;
}
export { Header };

Output

function Header() {
  return /*#__PURE__*/React.createElement("header", null, "Hello World");
}
Header.__scopeSource = { filePath: "src/header.tsx", line: 1, column: 0 };
// __scopeProps is NOT injected — no typed props

Union type extraction (from prop-extraction.test.ts)

type StatusProps = { status: 'active' | 'inactive' | 'pending' };
function StatusBadge({ status }: StatusProps) { return <span>{status}</span>; }

Output

StatusBadge.__scopeProps = {
  status: { type: "union", required: true, values: ["active", "inactive", "pending"] },
};

Inline type annotation (from prop-extraction.test.ts)

function Avatar({ src, alt }: { src: string; alt?: string }) {
  return <img src={src} alt={alt} />;
}

Output

Avatar.__scopeProps = {
  src: { type: "string", required: true },
  alt: { type: "string", required: false },
};

Complex types fall back to "unknown" (from prop-extraction.test.ts)

interface ComplexProps { combo: A & B; }         // → type: "unknown" (intersection)
interface GenericProps { data: Array<string>; }   // → type: "unknown" (generic TSTypeReference)

Plugin options

interface PluginOptions {
  /** Override the file path injected into __scopeSource.
   *  Defaults to Babel's `state.filename`.
   *  Pass this explicitly in tests to avoid absolute-path mismatches. */
  filePath?: string;
}

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:

transformSync(code, {
  plugins: [['@agent-scope/babel-plugin', { filePath: 'src/Button.tsx' }]],
});
expect(output).toContain('filePath: "src/Button.tsx"'); // ✓ portable

Used by

  • @agent-scope/runtime — reads Component.__scopeSource and Component.__scopeProps at runtime to enrich PageReport nodes with source location and prop type metadata
  • @agent-scope/cli — uses prop type metadata to drive scope render matrix axis generation