@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:
- Injects stable
data-clippy-idattributes into every interactive HTML element in your compiled output — never modifying your source files. - Analyzes your component tree to extract state variables, event handlers, and conditional render relationships.
- Discovers all routes, navigation links, and user-flow paths.
- Emits two JSON artifacts —
clippy-policy.jsonandclippy-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/traverseQuick 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_xxxxxxxxxxxxIn 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
@navigate_ai/next— same plugin for Next.js projects@navigate_ai/plugin-shared— internal shared extractors (not needed directly)
