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

@navigate-ai/next

v0.1.19

Published

Clippy build plugin for **Next.js** (App Router and Pages Router). At compile time the plugin:

Readme

@navigate_ai/next

Clippy build plugin for Next.js (App Router and Pages Router). At compile time the plugin:

  1. Injects stable data-clippy-id attributes into every interactive HTML element in your compiled output — never modifying your source files.
  2. Analyzes your component tree to extract state variables, event handlers, and conditional render relationships.
  3. Discovers all routes, navigation links, and user-flow paths.
  4. Emits two JSON artifacts — clippy-policy.json and clippy-selectors.json — that the Clippy runtime uses to execute user prompts locally without LLM calls.

Installation

npm install @navigate_ai/next
# or
pnpm add @navigate_ai/next
# or
yarn add @navigate_ai/next

@navigate_ai/plugin-shared is a transitive dependency and is installed automatically. You do not need to install it separately.

Peer dependencies (already present in any Next.js project):

npm install --save-dev next webpack @babel/parser @babel/traverse

Quick start

Wrap your Next.js config with withClippy:

// next.config.ts
import type { NextConfig } from 'next'
import { withClippy } from '@navigate_ai/next'

const nextConfig: NextConfig = {
  // your existing Next.js config
}

export default withClippy(nextConfig, {
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
})
// next.config.js (CommonJS)
const { withClippy } = require('@navigate_ai/next')

/** @type {import('next').NextConfig} */
const nextConfig = {}

module.exports = withClippy(nextConfig, {
  apiKey: process.env.CLIPPY_API_KEY,
  projectId: process.env.CLIPPY_PROJECT_ID,
})

On every production build (next build) the plugin runs automatically — no additional scripts or CI steps required.


Configuration

interface ClippyPluginOptions {
  /** Your Clippy API key. Get this from the Clippy developer dashboard. */
  apiKey: string

  /** Your Clippy project ID. Found in the project settings page. */
  projectId: string

  /**
   * Skip uploading artifacts to the Clippy backend.
   * Useful in local development or when you want to inspect the output
   * before uploading. Artifacts are still written if localOutputDir is set.
   * Default: false
   */
  skipUpload?: boolean

  /**
   * Only run the plugin during production builds (NODE_ENV === 'production').
   * When true, the plugin is a no-op in development mode (next dev).
   * Default: false — the plugin runs on every build including dev.
   */
  productionOnly?: boolean

  /**
   * Write the generated JSON artifacts to a local directory in addition to
   * (or instead of) uploading them. Useful for inspecting output and for
   * CI pipelines that handle uploads separately.
   *
   * Two files are written:
   *   <localOutputDir>/clippy-policy.json
   *   <localOutputDir>/clippy-selectors.json
   *
   * Example: '.clippy-output'
   */
  localOutputDir?: string

  /**
   * How to upload artifacts to the Clippy backend.
   *
   * 'split'  — upload clippy-policy.json and clippy-selectors.json as separate
   *             requests. Recommended for large projects.
   * 'single' — bundle both artifacts into one gzipped payload.
   *
   * Default: 'split'
   */
  artifactUploadMode?: 'single' | 'split'

  /**
   * Provide a custom upload function. When set, the built-in HTTP uploader
   * is bypassed entirely. Use this to integrate with your own artifact
   * storage, a proxy endpoint, or a CI/CD pipeline step.
   *
   * Example — upload to your own backend:
   *
   * uploadAdapter: {
   *   uploadArtifacts: async (artifacts) => {
   *     await myApi.uploadPolicy(artifacts.policy)
   *     await myApi.uploadSelectors(artifacts.selectorManifest)
   *     return { skipped: false, policyUploaded: true, selectorsUploaded: true, mode: 'split' }
   *   }
   * }
   */
  uploadAdapter?: {
    uploadArtifacts?: (artifacts: PolicyArtifacts) => Promise<UploadResult>
  }
}

Recommended environment variable setup

# .env.local (never commit this)
CLIPPY_API_KEY=pk_live_xxxxxxxxxxxx
CLIPPY_PROJECT_ID=proj_xxxxxxxxxxxx
# CI/CD environment variables
CLIPPY_API_KEY=pk_live_xxxxxxxxxxxx
CLIPPY_PROJECT_ID=proj_xxxxxxxxxxxx

Output files

The plugin produces two files per build. Both share the same buildId so the Clippy runtime can match them to the active build.

clippy-selectors.json

A flat, deduplicated manifest of every interactive element across all routes. This is what gets injected into LLM prompts for Tier 3 execution and used for selector health monitoring.

{
  "version": "1.0.0",
  "buildId": "abc123",
  "generatedAt": "2026-06-02T10:00:00.000Z",
  "selectors": [
    {
      // Stable ID injected as data-clippy-id in the compiled DOM.
      // Format: ComponentName[-LabelText]-tag-lineNumber
      "id": "DashboardForms-CreateForm-button-138",

      // CSS selector that resolves this element in the live DOM.
      // Always data-clippy-id when injection succeeded; falls back
      // to aria-label, data-testid, or button:contains() selectors.
      "selector": "[data-clippy-id='DashboardForms-CreateForm-button-138']",

      // The React component that contains this element.
      "component": "DashboardForms",

      // The HTML tag of the injected element.
      "tag": "button",

      // Human-readable label derived from aria-label, visible text,
      // placeholder, or name attribute. null when none is available.
      "label": "Create Form",

      // Every route where this element appears. Shared components
      // (Input, Textarea, etc.) list all routes here rather than
      // appearing as duplicate entries.
      "routes": ["/dashboard/forms"]
    }
  ]
}

clippy-policy.json

The full knowledge document used by the Clippy Policy Executor for Tier 1 (no-LLM) flow execution and by the backend for Tier 3 LLM context enrichment.

{
  "version": "1.0.0",
  "buildId": "abc123",
  "generatedAt": "2026-06-02T10:00:00.000Z",
  "bundler": "webpack",

  // All discovered routes with relative file paths.
  "routes": [
    {
      "path": "/dashboard/forms",
      "filePath": "app/dashboard/forms/page.tsx",
      "isDynamic": false,
      "params": [],
      "layout": "app/dashboard/layout.tsx",
      "routerType": "app",
      "semantic": "forms dashboard"
    }
  ],

  // All interactive elements with their selector candidates.
  "selectors": [
    {
      "clippyId": "DashboardForms-CreateForm-button-138",
      "selector": "[data-clippy-id='DashboardForms-CreateForm-button-138']",
      "tag": "button",
      "component": "DashboardForms",
      "label": "Create Form",
      "route": "/dashboard/forms",
      "filePath": "app/dashboard/forms/page.tsx",
      "attributes": [{ "name": "type", "value": "submit" }],
      "candidates": [
        { "type": "clippy_id", "value": "[data-clippy-id='...']", "confidence": 0.999 },
        { "type": "aria", "value": "button[aria-label='Create Form']", "confidence": 0.93 }
      ]
    }
  ],

  // Components with meaningful state or interaction data.
  // Pure presentational components (Button, Card, etc.) are excluded.
  "components": [
    {
      "name": "DashboardForms",
      "filePath": "app/dashboard/forms/page.tsx",
      "route": "/dashboard/forms",
      "stateVariables": [
        { "name": "isOpen", "setter": "setIsOpen", "initialValue": "false" }
      ],
      "interactions": [
        {
          "trigger": { "event": "onClick", "element": "button", "setsState": "isOpen" },
          "effect": {
            "type": "conditionalRender",
            "rendersWhenTrue": "CreateFormModal",
            "waitStrategy": "elementAppears",
            "selector": "[data-clippy-component='CreateFormModal']"
          }
        }
      ]
    }
  ],

  // Multi-step navigation flows inferred from Link/anchor elements.
  "flows": [
    {
      "flowId": "flow_1",
      "page": "/auth/signup",
      "intentPatterns": ["sign up", "create account", "register", "get started"],
      "steps": [
        { "step": 1, "action": "navigate", "target": "[data-clippy-id='SignupContent-form-126']" },
        { "step": 2, "action": "transition", "target": "[data-clippy-id='PlanSelectionContent-button-176']" },
        { "step": 3, "action": "transition", "target": "[data-clippy-id='OTPVerificationPage-form-163']" }
      ]
    }
  ]
}

How stable selectors work

The plugin transforms JSX at compile time — not your source files — to add two attributes to every native HTML element (button, input, a, form, select, textarea, label):

<!-- Your source (never modified): -->
<button className="primary">Create Form</button>

<!-- Compiled output (injected by the plugin): -->
<button
  className="primary"
  data-clippy-id="DashboardForms-CreateForm-button-138"
  data-clippy-component="DashboardForms"
>
  Create Form
</button>

The data-clippy-id format is ComponentName[-LabelText]-tag-lineNumber:

  • ComponentName — the enclosing React component, or a route-derived name for page components (e.g., AdminTransactions instead of Page).
  • LabelText — the element's visible text or aria-label, sanitized to TitleCase, max two words. Omitted when no static text is available.
  • tag — the lowercase HTML tag.
  • lineNumber — the element's position in the source file, acting as a stable tiebreaker.

IDs are stable across builds as long as the component name and the element's file position don't change. CSS class hashes change on every build; data-clippy-id does not.

React components (<Button />, <AlertDialog />) are never touched — only native HTML elements.


Router support

| Router | Detection | Notes | |---|---|---| | App Router (Next.js 13+) | Filesystem — app/**/page.tsx | Route groups like (auth) are stripped | | Pages Router | Filesystem — pages/**/*.tsx | api/ directory skipped | | React Router | AST — createBrowserRouter, <Route path> | Detected in src/ | | TanStack Router | Filesystem — src/routes/**/*.tsx | Dot-separated filenames |


Using local output for inspection

During development, write artifacts locally to inspect them before uploading:

export default withClippy(nextConfig, {
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
  localOutputDir: '.clippy-output',
  skipUpload: true, // don't upload, just write locally
})

Add .clippy-output to .gitignore.


CI/CD

The plugin runs during next build and uploads automatically. No extra CI step is needed. If you prefer to control the upload yourself:

withClippy(nextConfig, {
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
  localOutputDir: 'dist/clippy',
  skipUpload: true,
  uploadAdapter: {
    uploadArtifacts: async (artifacts) => {
      // your upload logic
    },
  },
})

Troubleshooting

data-clippy-id is not appearing in the DOM

The loader only runs on non-server webpack compilations. Confirm you are inspecting the client-side rendered HTML, not server-rendered markup before hydration. Also check that the element is a native HTML tag — React components like <Button /> are intentionally skipped.

IDs contain Page as the component name

This happens for files outside the app/ directory where route inference can't determine the route. Add an aria-label to the element — the label will override the generic name in the ID.

Selectors show button:contains(...) instead of data-clippy-id

The text:contains() fallback is used when the injected ID cannot be matched back to a discovered element (typically a line number mismatch between compilation passes). It still works at runtime but is less stable than the injected ID. Adding aria-label to the button resolves this permanently.

Upload is failing

Verify CLIPPY_API_KEY and CLIPPY_PROJECT_ID are set in your build environment. Set localOutputDir to confirm the artifacts are being generated correctly before troubleshooting the upload.

Plugin is running in development but I only want production

withClippy(nextConfig, {
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
  productionOnly: true,
})

Related packages