@navigate_ai/plugin-shared
v0.1.41
Published
Internal shared library for the Clippy build plugin ecosystem. Contains the AST extractors, selector generators, flow inferrers, and package builders used by `@navigate_ai/next` and `@navigate_ai/vite`.
Readme
@navigate_ai/plugin-shared
Internal shared library for the Clippy build plugin ecosystem. Contains the AST extractors, selector generators, flow inferrers, and package builders used by @navigate_ai/next and @navigate_ai/vite.
You do not need to install this package directly. It is a transitive dependency of the adapter packages and is installed automatically.
If you are building a custom adapter
If you are integrating Clippy with a bundler other than Next.js or Vite, this package provides the full pipeline as composable classes.
npm install @navigate_ai/plugin-sharedPipeline overview
Source files
│
▼
ClippyIdInjector.injectClippyIds(source, filePath, routePath?)
│ Injects data-clippy-id and data-clippy-component into compiled HTML elements
▼
RouteExtractor.extract()
│ Discovers all routes from filesystem conventions or React Router AST
▼
ComponentExtractor.extract() ComponentExtractor.extractComponents()
│ Finds all interactive elements │ Extracts state, handlers, interaction graphs
▼ ▼
SelectorGenerator.generate(elements, injectedMap)
│ Matches injected IDs to elements, ranks selector candidates
▼
FlowInferrer.infer()
│ Builds navigation edge graph, detects flow chains, generates intent patterns
▼
PackageBuilder.buildArtifacts()
│ Assembles clippy-policy.json and clippy-selectors.json
▼
PackageWriter.writeArtifacts() / Uploader.upload()
Writes JSON files locally Uploads to Clippy backendMinimal custom adapter
import { injectClippyIds, inferRouteFromFilePath } from '@navigate_ai/plugin-shared'
import { RouteExtractor } from '@navigate_ai/plugin-shared/extractors/RouteExtractor'
import { ComponentExtractor } from '@navigate_ai/plugin-shared/extractors/ComponentExtractor'
import { SelectorGenerator } from '@navigate_ai/plugin-shared/extractors/SelectorGenerator'
import { FlowInferrer } from '@navigate_ai/plugin-shared/extractors/FlowInferrer'
import { PackageBuilder } from '@navigate_ai/plugin-shared/upload/PackageBuilder'
import { PackageWriter } from '@navigate_ai/plugin-shared/upload/PackageWriter'
import type { ClippyPluginOptions } from '@navigate_ai/plugin-shared'
async function runClippyPipeline(
projectRoot: string,
buildId: string,
moduleGraph: Map<string, { id: string; importedIds: readonly string[] }>,
injectedMap: Record<string, Array<{ clippyId: string; component: string; tag: string; line: number; label?: string }>>,
options: ClippyPluginOptions
) {
const routes = await new RouteExtractor(projectRoot).extract()
const extractor = new ComponentExtractor(
{ type: 'rollup', moduleGraph },
routes
)
const elements = await extractor.extract()
const components = await extractor.extractComponents()
const selectors = new SelectorGenerator().generate(elements, injectedMap)
const flows = new FlowInferrer(routes, elements, components).infer()
const artifacts = new PackageBuilder().buildArtifacts({
projectRoot,
buildId,
bundler: 'vite', // or 'webpack'
routes,
selectors,
flows,
components,
})
if (options.localOutputDir) {
new PackageWriter().writeArtifacts(options.localOutputDir, artifacts)
}
}Transform hook (injection)
Call injectClippyIds in your bundler's transform hook for each .tsx / .jsx / .ts / .js file:
// In your bundler's transform/loader:
const routePath = inferRouteFromFilePath(filePath) ?? undefined
const result = injectClippyIds(sourceCode, filePath, routePath)
// result.source — transformed source with data-clippy-id attributes
// result.injected — metadata array for building the injectedMap
// result.injectedCount — number of attributes injected (0 = file had no HTML elements)
if (result.injectedCount > 0) {
injectedMap[filePath] = result.injected
}inferRouteFromFilePath detects App Router (app/**/page.tsx) and Pages Router (pages/**/*.tsx) conventions and returns the route path string (e.g., /dashboard/forms). Returns null for non-route files.
Exported API
Injection
| Export | Description |
|---|---|
| injectClippyIds(source, filePath, routePath?) | Injects data-clippy-id and data-clippy-component into all native HTML elements in a JSX/TSX source string |
| inferRouteFromFilePath(filePath) | Derives route path from an absolute file path for App Router / Pages Router files |
| deriveClippyId(component, tag, line, routePath?, label?) | Computes a stable data-clippy-id value |
| deriveRouteComponentName(routePath) | Converts /admin/transactions → AdminTransactions |
| GENERIC_COMPONENT_NAMES | Set of component names treated as generic (Page, Layout, App, etc.) |
Extractors
| Export | Description |
|---|---|
| RouteExtractor | Filesystem + AST route discovery |
| ComponentExtractor | Module-graph traversal, element extraction, component analysis |
| SelectorGenerator | Ranks and matches selector candidates for each element |
| FlowInferrer | Navigation edge graph construction and flow chain detection |
| InteractionGraphExtractor | AST extraction of state → conditional render chains |
| ComponentContextResolver | useState / useReducer / event handler extraction |
Output
| Export | Description |
|---|---|
| PackageBuilder | Assembles PolicyArtifacts from pipeline output |
| PackageWriter | Writes clippy-policy.json and clippy-selectors.json to disk |
| Uploader | HTTP upload client for the Clippy backend |
Types
All public types are exported from the package root:
import type {
ClippyPluginOptions,
PolicyArtifacts,
PolicyDocument,
PolicyComponent,
PolicyFlow,
PolicySelectorEntry,
SelectorManifest,
SelectorManifestEntry,
DiscoveredRoute,
DiscoveredElement,
ElementWithSelectors,
SelectorCandidate,
ComponentInteraction,
TriggerSpec,
EffectSpec,
InferredFlow,
FlowEdge,
UploadResult,
} from '@navigate_ai/plugin-shared'Output artifact formats
clippy-policy.json — full policy document
interface PolicyDocument {
version: string
buildId: string
generatedAt: string
bundler: 'webpack' | 'vite'
routes: DiscoveredRoute[]
selectors: PolicySelectorEntry[]
components: PolicyComponent[] // only components with state or interactions
flows: PolicyFlow[]
}clippy-selectors.json — deduplicated selector manifest
interface SelectorManifest {
version: string
buildId: string
generatedAt: string
selectors: SelectorManifestEntry[]
}
interface SelectorManifestEntry {
id: string // data-clippy-id value
selector: string // CSS selector string
component: string // enclosing component name
tag: string // lowercase HTML tag
label?: string // human-readable label (may be null)
routes: string[] // all routes where this element appears
}DiscoveredRoute
interface DiscoveredRoute {
path: string // e.g. "/dashboard/forms/[id]"
filePath: string // relative to project root, e.g. "app/dashboard/forms/[id]/page.tsx"
isDynamic: boolean
params: string[] // e.g. ["id"]
layout: string | null // relative path to nearest layout file
routerType: 'app' | 'pages' | 'react-router' | 'tanstack'
semantic: string // space-separated route segments, reversed
}PolicyComponent
Only components with at least one stateVariable or interaction are included.
interface PolicyComponent {
name: string
filePath: string // relative to project root
route: string
stateVariables: Array<{
name: string
setter?: string
initialValue?: string
}>
interactions: ComponentInteraction[]
}
interface ComponentInteraction {
trigger: {
event: string // e.g. "onClick"
element: string // JSX tag that carries the handler
setsState?: string // state variable the handler mutates
}
effect: {
type: 'conditionalRender' | 'asyncEffect' | 'contextDependency'
rendersWhenTrue?: string // component name that appears
rendersWhenFalse?: string // component name that disappears
waitStrategy: 'elementAppears' | 'domSettle' | 'none'
selector?: string // [data-clippy-component='...'] selector to wait for
settleMs?: number // milliseconds to wait for DOM to settle
}
}PolicyFlow
interface PolicyFlow {
flowId: string
page: string // starting route
intentPatterns: string[] // user phrases that match this flow
steps: PolicyFlowStep[]
}
interface PolicyFlowStep {
step: number
action: 'navigate' | 'transition' | 'interact'
target: string // CSS selector for the element to act on
}Selector ID format
ComponentName[-LabelText]-tag-lineNumber
Examples:
DashboardForms-CreateForm-button-138
AdminTransactions-Approve-button-172
LoginPage-form-115 (no label — form labels use submit button text only)
Input-input-7 (shared component, no label from its own file)- ComponentName — enclosing React component, or route-derived name when generic (e.g.,
AdminTransactionsinstead ofPage) - LabelText —
aria-label> visible text (TitleCase, max 2 words) >placeholder. Omitted for forms (submit button text is used instead of the full field list) - tag — lowercase HTML tag
- lineNumber — source file line, stable tiebreaker
Related packages
@navigate_ai/next— Next.js adapter@navigate_ai/vite— Vite adapter
