@dcoder-x/next 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +393 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# @dcoder-x/next
|
|
2
|
+
|
|
3
|
+
Clippy build plugin for **Next.js** (App Router and Pages Router). At compile time the plugin:
|
|
4
|
+
|
|
5
|
+
1. Injects stable `data-clippy-id` attributes into every interactive HTML element in your compiled output — never modifying your source files.
|
|
6
|
+
2. Analyzes your component tree to extract state variables, event handlers, and conditional render relationships.
|
|
7
|
+
3. Discovers all routes, navigation links, and user-flow paths.
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @dcoder-x/next
|
|
16
|
+
# or
|
|
17
|
+
pnpm add @dcoder-x/next
|
|
18
|
+
# or
|
|
19
|
+
yarn add @dcoder-x/next
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`@dcoder-x/plugin-shared` is a transitive dependency and is installed automatically. You do not need to install it separately.
|
|
23
|
+
|
|
24
|
+
**Peer dependencies** (already present in any Next.js project):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install --save-dev next webpack @babel/parser @babel/traverse
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Quick start
|
|
33
|
+
|
|
34
|
+
Wrap your Next.js config with `withClippy`:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// next.config.ts
|
|
38
|
+
import type { NextConfig } from 'next'
|
|
39
|
+
import { withClippy } from '@dcoder-x/next'
|
|
40
|
+
|
|
41
|
+
const nextConfig: NextConfig = {
|
|
42
|
+
// your existing Next.js config
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export default withClippy(nextConfig, {
|
|
46
|
+
apiKey: process.env.CLIPPY_API_KEY!,
|
|
47
|
+
projectId: process.env.CLIPPY_PROJECT_ID!,
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// next.config.js (CommonJS)
|
|
53
|
+
const { withClippy } = require('@dcoder-x/next')
|
|
54
|
+
|
|
55
|
+
/** @type {import('next').NextConfig} */
|
|
56
|
+
const nextConfig = {}
|
|
57
|
+
|
|
58
|
+
module.exports = withClippy(nextConfig, {
|
|
59
|
+
apiKey: process.env.CLIPPY_API_KEY,
|
|
60
|
+
projectId: process.env.CLIPPY_PROJECT_ID,
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
On every production build (`next build`) the plugin runs automatically — no additional scripts or CI steps required.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
interface ClippyPluginOptions {
|
|
72
|
+
/** Your Clippy API key. Get this from the Clippy developer dashboard. */
|
|
73
|
+
apiKey: string
|
|
74
|
+
|
|
75
|
+
/** Your Clippy project ID. Found in the project settings page. */
|
|
76
|
+
projectId: string
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Skip uploading artifacts to the Clippy backend.
|
|
80
|
+
* Useful in local development or when you want to inspect the output
|
|
81
|
+
* before uploading. Artifacts are still written if localOutputDir is set.
|
|
82
|
+
* Default: false
|
|
83
|
+
*/
|
|
84
|
+
skipUpload?: boolean
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Only run the plugin during production builds (NODE_ENV === 'production').
|
|
88
|
+
* When true, the plugin is a no-op in development mode (next dev).
|
|
89
|
+
* Default: false — the plugin runs on every build including dev.
|
|
90
|
+
*/
|
|
91
|
+
productionOnly?: boolean
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Write the generated JSON artifacts to a local directory in addition to
|
|
95
|
+
* (or instead of) uploading them. Useful for inspecting output and for
|
|
96
|
+
* CI pipelines that handle uploads separately.
|
|
97
|
+
*
|
|
98
|
+
* Two files are written:
|
|
99
|
+
* <localOutputDir>/clippy-policy.json
|
|
100
|
+
* <localOutputDir>/clippy-selectors.json
|
|
101
|
+
*
|
|
102
|
+
* Example: '.clippy-output'
|
|
103
|
+
*/
|
|
104
|
+
localOutputDir?: string
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* How to upload artifacts to the Clippy backend.
|
|
108
|
+
*
|
|
109
|
+
* 'split' — upload clippy-policy.json and clippy-selectors.json as separate
|
|
110
|
+
* requests. Recommended for large projects.
|
|
111
|
+
* 'single' — bundle both artifacts into one gzipped payload.
|
|
112
|
+
*
|
|
113
|
+
* Default: 'split'
|
|
114
|
+
*/
|
|
115
|
+
artifactUploadMode?: 'single' | 'split'
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Provide a custom upload function. When set, the built-in HTTP uploader
|
|
119
|
+
* is bypassed entirely. Use this to integrate with your own artifact
|
|
120
|
+
* storage, a proxy endpoint, or a CI/CD pipeline step.
|
|
121
|
+
*
|
|
122
|
+
* Example — upload to your own backend:
|
|
123
|
+
*
|
|
124
|
+
* uploadAdapter: {
|
|
125
|
+
* uploadArtifacts: async (artifacts) => {
|
|
126
|
+
* await myApi.uploadPolicy(artifacts.policy)
|
|
127
|
+
* await myApi.uploadSelectors(artifacts.selectorManifest)
|
|
128
|
+
* return { skipped: false, policyUploaded: true, selectorsUploaded: true, mode: 'split' }
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
uploadAdapter?: {
|
|
133
|
+
uploadArtifacts?: (artifacts: PolicyArtifacts) => Promise<UploadResult>
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Recommended environment variable setup
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
# .env.local (never commit this)
|
|
142
|
+
CLIPPY_API_KEY=pk_live_xxxxxxxxxxxx
|
|
143
|
+
CLIPPY_PROJECT_ID=proj_xxxxxxxxxxxx
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# CI/CD environment variables
|
|
148
|
+
CLIPPY_API_KEY=pk_live_xxxxxxxxxxxx
|
|
149
|
+
CLIPPY_PROJECT_ID=proj_xxxxxxxxxxxx
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Output files
|
|
155
|
+
|
|
156
|
+
The plugin produces two files per build. Both share the same `buildId` so the Clippy runtime can match them to the active build.
|
|
157
|
+
|
|
158
|
+
### `clippy-selectors.json`
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
```jsonc
|
|
163
|
+
{
|
|
164
|
+
"version": "1.0.0",
|
|
165
|
+
"buildId": "abc123",
|
|
166
|
+
"generatedAt": "2026-06-02T10:00:00.000Z",
|
|
167
|
+
"selectors": [
|
|
168
|
+
{
|
|
169
|
+
// Stable ID injected as data-clippy-id in the compiled DOM.
|
|
170
|
+
// Format: ComponentName[-LabelText]-tag-lineNumber
|
|
171
|
+
"id": "DashboardForms-CreateForm-button-138",
|
|
172
|
+
|
|
173
|
+
// CSS selector that resolves this element in the live DOM.
|
|
174
|
+
// Always data-clippy-id when injection succeeded; falls back
|
|
175
|
+
// to aria-label, data-testid, or button:contains() selectors.
|
|
176
|
+
"selector": "[data-clippy-id='DashboardForms-CreateForm-button-138']",
|
|
177
|
+
|
|
178
|
+
// The React component that contains this element.
|
|
179
|
+
"component": "DashboardForms",
|
|
180
|
+
|
|
181
|
+
// The HTML tag of the injected element.
|
|
182
|
+
"tag": "button",
|
|
183
|
+
|
|
184
|
+
// Human-readable label derived from aria-label, visible text,
|
|
185
|
+
// placeholder, or name attribute. null when none is available.
|
|
186
|
+
"label": "Create Form",
|
|
187
|
+
|
|
188
|
+
// Every route where this element appears. Shared components
|
|
189
|
+
// (Input, Textarea, etc.) list all routes here rather than
|
|
190
|
+
// appearing as duplicate entries.
|
|
191
|
+
"routes": ["/dashboard/forms"]
|
|
192
|
+
}
|
|
193
|
+
]
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `clippy-policy.json`
|
|
198
|
+
|
|
199
|
+
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.
|
|
200
|
+
|
|
201
|
+
```jsonc
|
|
202
|
+
{
|
|
203
|
+
"version": "1.0.0",
|
|
204
|
+
"buildId": "abc123",
|
|
205
|
+
"generatedAt": "2026-06-02T10:00:00.000Z",
|
|
206
|
+
"bundler": "webpack",
|
|
207
|
+
|
|
208
|
+
// All discovered routes with relative file paths.
|
|
209
|
+
"routes": [
|
|
210
|
+
{
|
|
211
|
+
"path": "/dashboard/forms",
|
|
212
|
+
"filePath": "app/dashboard/forms/page.tsx",
|
|
213
|
+
"isDynamic": false,
|
|
214
|
+
"params": [],
|
|
215
|
+
"layout": "app/dashboard/layout.tsx",
|
|
216
|
+
"routerType": "app",
|
|
217
|
+
"semantic": "forms dashboard"
|
|
218
|
+
}
|
|
219
|
+
],
|
|
220
|
+
|
|
221
|
+
// All interactive elements with their selector candidates.
|
|
222
|
+
"selectors": [
|
|
223
|
+
{
|
|
224
|
+
"clippyId": "DashboardForms-CreateForm-button-138",
|
|
225
|
+
"selector": "[data-clippy-id='DashboardForms-CreateForm-button-138']",
|
|
226
|
+
"tag": "button",
|
|
227
|
+
"component": "DashboardForms",
|
|
228
|
+
"label": "Create Form",
|
|
229
|
+
"route": "/dashboard/forms",
|
|
230
|
+
"filePath": "app/dashboard/forms/page.tsx",
|
|
231
|
+
"attributes": [{ "name": "type", "value": "submit" }],
|
|
232
|
+
"candidates": [
|
|
233
|
+
{ "type": "clippy_id", "value": "[data-clippy-id='...']", "confidence": 0.999 },
|
|
234
|
+
{ "type": "aria", "value": "button[aria-label='Create Form']", "confidence": 0.93 }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
],
|
|
238
|
+
|
|
239
|
+
// Components with meaningful state or interaction data.
|
|
240
|
+
// Pure presentational components (Button, Card, etc.) are excluded.
|
|
241
|
+
"components": [
|
|
242
|
+
{
|
|
243
|
+
"name": "DashboardForms",
|
|
244
|
+
"filePath": "app/dashboard/forms/page.tsx",
|
|
245
|
+
"route": "/dashboard/forms",
|
|
246
|
+
"stateVariables": [
|
|
247
|
+
{ "name": "isOpen", "setter": "setIsOpen", "initialValue": "false" }
|
|
248
|
+
],
|
|
249
|
+
"interactions": [
|
|
250
|
+
{
|
|
251
|
+
"trigger": { "event": "onClick", "element": "button", "setsState": "isOpen" },
|
|
252
|
+
"effect": {
|
|
253
|
+
"type": "conditionalRender",
|
|
254
|
+
"rendersWhenTrue": "CreateFormModal",
|
|
255
|
+
"waitStrategy": "elementAppears",
|
|
256
|
+
"selector": "[data-clippy-component='CreateFormModal']"
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
|
|
263
|
+
// Multi-step navigation flows inferred from Link/anchor elements.
|
|
264
|
+
"flows": [
|
|
265
|
+
{
|
|
266
|
+
"flowId": "flow_1",
|
|
267
|
+
"page": "/auth/signup",
|
|
268
|
+
"intentPatterns": ["sign up", "create account", "register", "get started"],
|
|
269
|
+
"steps": [
|
|
270
|
+
{ "step": 1, "action": "navigate", "target": "[data-clippy-id='SignupContent-form-126']" },
|
|
271
|
+
{ "step": 2, "action": "transition", "target": "[data-clippy-id='PlanSelectionContent-button-176']" },
|
|
272
|
+
{ "step": 3, "action": "transition", "target": "[data-clippy-id='OTPVerificationPage-form-163']" }
|
|
273
|
+
]
|
|
274
|
+
}
|
|
275
|
+
]
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## How stable selectors work
|
|
282
|
+
|
|
283
|
+
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`):
|
|
284
|
+
|
|
285
|
+
```html
|
|
286
|
+
<!-- Your source (never modified): -->
|
|
287
|
+
<button className="primary">Create Form</button>
|
|
288
|
+
|
|
289
|
+
<!-- Compiled output (injected by the plugin): -->
|
|
290
|
+
<button
|
|
291
|
+
className="primary"
|
|
292
|
+
data-clippy-id="DashboardForms-CreateForm-button-138"
|
|
293
|
+
data-clippy-component="DashboardForms"
|
|
294
|
+
>
|
|
295
|
+
Create Form
|
|
296
|
+
</button>
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The `data-clippy-id` format is `ComponentName[-LabelText]-tag-lineNumber`:
|
|
300
|
+
|
|
301
|
+
- **ComponentName** — the enclosing React component, or a route-derived name for page components (e.g., `AdminTransactions` instead of `Page`).
|
|
302
|
+
- **LabelText** — the element's visible text or `aria-label`, sanitized to TitleCase, max two words. Omitted when no static text is available.
|
|
303
|
+
- **tag** — the lowercase HTML tag.
|
|
304
|
+
- **lineNumber** — the element's position in the source file, acting as a stable tiebreaker.
|
|
305
|
+
|
|
306
|
+
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.
|
|
307
|
+
|
|
308
|
+
React components (`<Button />`, `<AlertDialog />`) are never touched — only native HTML elements.
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
## Router support
|
|
313
|
+
|
|
314
|
+
| Router | Detection | Notes |
|
|
315
|
+
|---|---|---|
|
|
316
|
+
| App Router (Next.js 13+) | Filesystem — `app/**/page.tsx` | Route groups like `(auth)` are stripped |
|
|
317
|
+
| Pages Router | Filesystem — `pages/**/*.tsx` | `api/` directory skipped |
|
|
318
|
+
| React Router | AST — `createBrowserRouter`, `<Route path>` | Detected in `src/` |
|
|
319
|
+
| TanStack Router | Filesystem — `src/routes/**/*.tsx` | Dot-separated filenames |
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Using local output for inspection
|
|
324
|
+
|
|
325
|
+
During development, write artifacts locally to inspect them before uploading:
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
export default withClippy(nextConfig, {
|
|
329
|
+
apiKey: process.env.CLIPPY_API_KEY!,
|
|
330
|
+
projectId: process.env.CLIPPY_PROJECT_ID!,
|
|
331
|
+
localOutputDir: '.clippy-output',
|
|
332
|
+
skipUpload: true, // don't upload, just write locally
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Add `.clippy-output` to `.gitignore`.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## CI/CD
|
|
341
|
+
|
|
342
|
+
The plugin runs during `next build` and uploads automatically. No extra CI step is needed. If you prefer to control the upload yourself:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
withClippy(nextConfig, {
|
|
346
|
+
apiKey: process.env.CLIPPY_API_KEY!,
|
|
347
|
+
projectId: process.env.CLIPPY_PROJECT_ID!,
|
|
348
|
+
localOutputDir: 'dist/clippy',
|
|
349
|
+
skipUpload: true,
|
|
350
|
+
uploadAdapter: {
|
|
351
|
+
uploadArtifacts: async (artifacts) => {
|
|
352
|
+
// your upload logic
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
})
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## Troubleshooting
|
|
361
|
+
|
|
362
|
+
**`data-clippy-id` is not appearing in the DOM**
|
|
363
|
+
|
|
364
|
+
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.
|
|
365
|
+
|
|
366
|
+
**IDs contain `Page` as the component name**
|
|
367
|
+
|
|
368
|
+
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.
|
|
369
|
+
|
|
370
|
+
**Selectors show `button:contains(...)` instead of `data-clippy-id`**
|
|
371
|
+
|
|
372
|
+
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.
|
|
373
|
+
|
|
374
|
+
**Upload is failing**
|
|
375
|
+
|
|
376
|
+
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.
|
|
377
|
+
|
|
378
|
+
**Plugin is running in development but I only want production**
|
|
379
|
+
|
|
380
|
+
```ts
|
|
381
|
+
withClippy(nextConfig, {
|
|
382
|
+
apiKey: process.env.CLIPPY_API_KEY!,
|
|
383
|
+
projectId: process.env.CLIPPY_PROJECT_ID!,
|
|
384
|
+
productionOnly: true,
|
|
385
|
+
})
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
## Related packages
|
|
391
|
+
|
|
392
|
+
- [`@dcoder-x/vite`](https://www.npmjs.com/package/@dcoder-x/vite) — same plugin for Vite projects
|
|
393
|
+
- [`@dcoder-x/plugin-shared`](https://www.npmjs.com/package/@dcoder-x/plugin-shared) — internal shared extractors (not needed directly)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcoder-x/next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"access": "public"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@dcoder-x/plugin-shared": "^0.1.
|
|
13
|
+
"@dcoder-x/plugin-shared": "^0.1.7"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
16
|
"next": ">=13.0.0",
|