@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.
Files changed (2) hide show
  1. package/README.md +393 -0
  2. 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.5",
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.6"
13
+ "@dcoder-x/plugin-shared": "^0.1.7"
14
14
  },
15
15
  "peerDependencies": {
16
16
  "next": ">=13.0.0",