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/vite

v0.1.45

Published

Clippy build plugin for **Vite** (React, SvelteKit, Remix, and any Vite-based project). At compile time the plugin:

Readme

@navigate_ai/vite

Clippy build plugin for Vite (React, SvelteKit, Remix, and any Vite-based project). 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/vite
# or
pnpm add @navigate_ai/vite
# or
yarn add @navigate_ai/vite

@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 Vite project):

npm install --save-dev vite @babel/parser @babel/traverse

Quick start

Add clippyVitePlugin to your Vite plugin array:

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { clippyVitePlugin } from '@navigate_ai/vite'

export default defineConfig({
  plugins: [
    react(),
    clippyVitePlugin({
      apiKey: process.env.CLIPPY_API_KEY!,
      projectId: process.env.CLIPPY_PROJECT_ID!,
    }),
  ],
})

The plugin runs automatically during vite build — no additional scripts or CI steps required. It is a no-op during vite dev (apply mode is 'build' only).


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 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').
   * Default: false
   */
  productionOnly?: boolean

  /**
   * Write the generated JSON artifacts to a local directory in addition to
   * (or instead of) uploading them.
   *
   * 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:
   *
   * 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 (never commit API keys — use .env.local or CI secrets)
CLIPPY_API_KEY=pk_live_xxxxxxxxxxxx
CLIPPY_PROJECT_ID=proj_xxxxxxxxxxxx

In Vite, environment variables must be prefixed with VITE_ to be exposed to the browser. Clippy's API key and project ID are only used at build time inside the plugin, so they do not need the VITE_ prefix and should not be exposed to the browser.

// Access in vite.config.ts via process.env (not import.meta.env)
clippyVitePlugin({
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
})

Output files

The plugin produces two files at the end of each build. Both share the same buildId so the Clippy runtime can link them to the active build.

clippy-selectors.json

A flat, deduplicated manifest of every interactive element across all routes. Used for LLM context injection and 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": "SettingsPage-SaveChanges-button-205",

      // CSS selector resolving this element in the live DOM.
      "selector": "[data-clippy-id='SettingsPage-SaveChanges-button-205']",

      "component": "SettingsPage",
      "tag": "button",

      // Human-readable label from aria-label, visible text, or placeholder.
      // null when no static text is available.
      "label": "Save Changes",

      // All routes where this element is rendered.
      // Shared components list multiple routes here rather than repeating.
      "routes": ["/dashboard/settings"]
    }
  ]
}

clippy-policy.json

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

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

  // All discovered routes with relative file paths.
  "routes": [
    {
      "path": "/dashboard/settings",
      "filePath": "src/pages/settings.tsx",
      "isDynamic": false,
      "params": [],
      "layout": null,
      "routerType": "react-router",
      "semantic": "settings dashboard"
    }
  ],

  // All interactive elements with selector candidates.
  "selectors": [ /* ... */ ],

  // Components with meaningful state or interaction data.
  // Purely presentational components (Button, Card, etc.) are excluded.
  "components": [
    {
      "name": "SettingsPage",
      "filePath": "src/pages/settings.tsx",
      "route": "/dashboard/settings",
      "stateVariables": [
        { "name": "isDirty", "setter": "setIsDirty" }
      ],
      "interactions": [
        {
          "trigger": { "event": "onClick", "element": "button", "setsState": "isDirty" },
          "effect": {
            "type": "asyncEffect",
            "waitStrategy": "domSettle",
            "settleMs": 300
          }
        }
      ]
    }
  ],

  // Multi-step navigation flows inferred from Link/anchor elements.
  "flows": [
    {
      "flowId": "flow_1",
      "page": "/auth/login",
      "intentPatterns": ["log in", "sign in", "login"],
      "steps": [
        { "step": 1, "action": "navigate", "target": "[data-clippy-id='LoginPage-form-115']" },
        { "step": 2, "action": "transition", "target": "[data-clippy-id='Dashboard-a-42']" }
      ]
    }
  ]
}

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>Save Changes</button>

<!-- Compiled output (injected by the plugin): -->
<button
  data-clippy-id="SettingsPage-SaveChanges-button-205"
  data-clippy-component="SettingsPage"
>
  Save Changes
</button>

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

  • ComponentName — the enclosing React component, or a route-derived name for page-level components.
  • 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 when multiple elements share the same name.

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

React component calls (<Button />, <Modal />) are never touched — only native HTML tags receive injected attributes.


Router support

| Router | Detection | Notes | |---|---|---| | React Router | AST — createBrowserRouter, <Route path> | Detected in src/ | | TanStack Router | Filesystem — src/routes/**/*.tsx | Dot-separated filenames | | Pages Router (Next.js via Vite) | Filesystem — pages/**/*.tsx | | | App Router (Next.js via Vite) | Filesystem — app/**/page.tsx | Prefer @navigate_ai/next for Next.js |


Using local output for inspection

clippyVitePlugin({
  apiKey: process.env.CLIPPY_API_KEY!,
  projectId: process.env.CLIPPY_PROJECT_ID!,
  localOutputDir: '.clippy-output',
  skipUpload: true,
})

Add .clippy-output to .gitignore.


CI/CD

The plugin runs at the end of vite build and uploads automatically. To control the upload yourself:

clippyVitePlugin({
  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 in the DOM

Confirm you are inspecting the production build output (vite build), not the dev server. The plugin only runs during the build phase.

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

The text:contains() fallback appears when the injected ID can't be matched back to a discovered element. Adding aria-label to the element resolves this permanently.

No flows are generated

Flows are inferred from <a href> and <Link to/href> elements pointing to known routes. If your navigation uses programmatic router.push() without visible links, flows will not be automatically detected. Consider adding visible navigation links or using the Clippy dashboard to record flows explicitly.

Upload is failing

Verify CLIPPY_API_KEY and CLIPPY_PROJECT_ID are set in your build environment (not in VITE_-prefixed variables, which are client-only). Set localOutputDir to confirm artifacts are generating correctly before troubleshooting the upload.


Related packages