@dcoder-x/plugin-shared 0.1.6 → 0.1.7
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 +300 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @dcoder-x/plugin-shared
|
|
2
|
+
|
|
3
|
+
Internal shared library for the Clippy build plugin ecosystem. Contains the AST extractors, selector generators, flow inferrers, and package builders used by `@dcoder-x/next` and `@dcoder-x/vite`.
|
|
4
|
+
|
|
5
|
+
**You do not need to install this package directly.** It is a transitive dependency of the adapter packages and is installed automatically.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## If you are building a custom adapter
|
|
10
|
+
|
|
11
|
+
If you are integrating Clippy with a bundler other than Next.js or Vite, this package provides the full pipeline as composable classes.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @dcoder-x/plugin-shared
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Pipeline overview
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
Source files
|
|
21
|
+
│
|
|
22
|
+
▼
|
|
23
|
+
ClippyIdInjector.injectClippyIds(source, filePath, routePath?)
|
|
24
|
+
│ Injects data-clippy-id and data-clippy-component into compiled HTML elements
|
|
25
|
+
▼
|
|
26
|
+
RouteExtractor.extract()
|
|
27
|
+
│ Discovers all routes from filesystem conventions or React Router AST
|
|
28
|
+
▼
|
|
29
|
+
ComponentExtractor.extract() ComponentExtractor.extractComponents()
|
|
30
|
+
│ Finds all interactive elements │ Extracts state, handlers, interaction graphs
|
|
31
|
+
▼ ▼
|
|
32
|
+
SelectorGenerator.generate(elements, injectedMap)
|
|
33
|
+
│ Matches injected IDs to elements, ranks selector candidates
|
|
34
|
+
▼
|
|
35
|
+
FlowInferrer.infer()
|
|
36
|
+
│ Builds navigation edge graph, detects flow chains, generates intent patterns
|
|
37
|
+
▼
|
|
38
|
+
PackageBuilder.buildArtifacts()
|
|
39
|
+
│ Assembles clippy-policy.json and clippy-selectors.json
|
|
40
|
+
▼
|
|
41
|
+
PackageWriter.writeArtifacts() / Uploader.upload()
|
|
42
|
+
Writes JSON files locally Uploads to Clippy backend
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Minimal custom adapter
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import { injectClippyIds, inferRouteFromFilePath } from '@dcoder-x/plugin-shared'
|
|
49
|
+
import { RouteExtractor } from '@dcoder-x/plugin-shared/extractors/RouteExtractor'
|
|
50
|
+
import { ComponentExtractor } from '@dcoder-x/plugin-shared/extractors/ComponentExtractor'
|
|
51
|
+
import { SelectorGenerator } from '@dcoder-x/plugin-shared/extractors/SelectorGenerator'
|
|
52
|
+
import { FlowInferrer } from '@dcoder-x/plugin-shared/extractors/FlowInferrer'
|
|
53
|
+
import { PackageBuilder } from '@dcoder-x/plugin-shared/upload/PackageBuilder'
|
|
54
|
+
import { PackageWriter } from '@dcoder-x/plugin-shared/upload/PackageWriter'
|
|
55
|
+
import type { ClippyPluginOptions } from '@dcoder-x/plugin-shared'
|
|
56
|
+
|
|
57
|
+
async function runClippyPipeline(
|
|
58
|
+
projectRoot: string,
|
|
59
|
+
buildId: string,
|
|
60
|
+
moduleGraph: Map<string, { id: string; importedIds: readonly string[] }>,
|
|
61
|
+
injectedMap: Record<string, Array<{ clippyId: string; component: string; tag: string; line: number; label?: string }>>,
|
|
62
|
+
options: ClippyPluginOptions
|
|
63
|
+
) {
|
|
64
|
+
const routes = await new RouteExtractor(projectRoot).extract()
|
|
65
|
+
|
|
66
|
+
const extractor = new ComponentExtractor(
|
|
67
|
+
{ type: 'rollup', moduleGraph },
|
|
68
|
+
routes
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const elements = await extractor.extract()
|
|
72
|
+
const components = await extractor.extractComponents()
|
|
73
|
+
const selectors = new SelectorGenerator().generate(elements, injectedMap)
|
|
74
|
+
const flows = new FlowInferrer(routes, elements, components).infer()
|
|
75
|
+
|
|
76
|
+
const artifacts = new PackageBuilder().buildArtifacts({
|
|
77
|
+
projectRoot,
|
|
78
|
+
buildId,
|
|
79
|
+
bundler: 'vite', // or 'webpack'
|
|
80
|
+
routes,
|
|
81
|
+
selectors,
|
|
82
|
+
flows,
|
|
83
|
+
components,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (options.localOutputDir) {
|
|
87
|
+
new PackageWriter().writeArtifacts(options.localOutputDir, artifacts)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Transform hook (injection)
|
|
93
|
+
|
|
94
|
+
Call `injectClippyIds` in your bundler's transform hook for each `.tsx` / `.jsx` / `.ts` / `.js` file:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// In your bundler's transform/loader:
|
|
98
|
+
const routePath = inferRouteFromFilePath(filePath) ?? undefined
|
|
99
|
+
const result = injectClippyIds(sourceCode, filePath, routePath)
|
|
100
|
+
|
|
101
|
+
// result.source — transformed source with data-clippy-id attributes
|
|
102
|
+
// result.injected — metadata array for building the injectedMap
|
|
103
|
+
// result.injectedCount — number of attributes injected (0 = file had no HTML elements)
|
|
104
|
+
|
|
105
|
+
if (result.injectedCount > 0) {
|
|
106
|
+
injectedMap[filePath] = result.injected
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`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.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Exported API
|
|
115
|
+
|
|
116
|
+
### Injection
|
|
117
|
+
|
|
118
|
+
| Export | Description |
|
|
119
|
+
|---|---|
|
|
120
|
+
| `injectClippyIds(source, filePath, routePath?)` | Injects `data-clippy-id` and `data-clippy-component` into all native HTML elements in a JSX/TSX source string |
|
|
121
|
+
| `inferRouteFromFilePath(filePath)` | Derives route path from an absolute file path for App Router / Pages Router files |
|
|
122
|
+
| `deriveClippyId(component, tag, line, routePath?, label?)` | Computes a stable `data-clippy-id` value |
|
|
123
|
+
| `deriveRouteComponentName(routePath)` | Converts `/admin/transactions` → `AdminTransactions` |
|
|
124
|
+
| `GENERIC_COMPONENT_NAMES` | Set of component names treated as generic (Page, Layout, App, etc.) |
|
|
125
|
+
|
|
126
|
+
### Extractors
|
|
127
|
+
|
|
128
|
+
| Export | Description |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `RouteExtractor` | Filesystem + AST route discovery |
|
|
131
|
+
| `ComponentExtractor` | Module-graph traversal, element extraction, component analysis |
|
|
132
|
+
| `SelectorGenerator` | Ranks and matches selector candidates for each element |
|
|
133
|
+
| `FlowInferrer` | Navigation edge graph construction and flow chain detection |
|
|
134
|
+
| `InteractionGraphExtractor` | AST extraction of state → conditional render chains |
|
|
135
|
+
| `ComponentContextResolver` | useState / useReducer / event handler extraction |
|
|
136
|
+
|
|
137
|
+
### Output
|
|
138
|
+
|
|
139
|
+
| Export | Description |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `PackageBuilder` | Assembles `PolicyArtifacts` from pipeline output |
|
|
142
|
+
| `PackageWriter` | Writes `clippy-policy.json` and `clippy-selectors.json` to disk |
|
|
143
|
+
| `Uploader` | HTTP upload client for the Clippy backend |
|
|
144
|
+
|
|
145
|
+
### Types
|
|
146
|
+
|
|
147
|
+
All public types are exported from the package root:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import type {
|
|
151
|
+
ClippyPluginOptions,
|
|
152
|
+
PolicyArtifacts,
|
|
153
|
+
PolicyDocument,
|
|
154
|
+
PolicyComponent,
|
|
155
|
+
PolicyFlow,
|
|
156
|
+
PolicySelectorEntry,
|
|
157
|
+
SelectorManifest,
|
|
158
|
+
SelectorManifestEntry,
|
|
159
|
+
DiscoveredRoute,
|
|
160
|
+
DiscoveredElement,
|
|
161
|
+
ElementWithSelectors,
|
|
162
|
+
SelectorCandidate,
|
|
163
|
+
ComponentInteraction,
|
|
164
|
+
TriggerSpec,
|
|
165
|
+
EffectSpec,
|
|
166
|
+
InferredFlow,
|
|
167
|
+
FlowEdge,
|
|
168
|
+
UploadResult,
|
|
169
|
+
} from '@dcoder-x/plugin-shared'
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Output artifact formats
|
|
175
|
+
|
|
176
|
+
### `clippy-policy.json` — full policy document
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
interface PolicyDocument {
|
|
180
|
+
version: string
|
|
181
|
+
buildId: string
|
|
182
|
+
generatedAt: string
|
|
183
|
+
bundler: 'webpack' | 'vite'
|
|
184
|
+
routes: DiscoveredRoute[]
|
|
185
|
+
selectors: PolicySelectorEntry[]
|
|
186
|
+
components: PolicyComponent[] // only components with state or interactions
|
|
187
|
+
flows: PolicyFlow[]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `clippy-selectors.json` — deduplicated selector manifest
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
interface SelectorManifest {
|
|
195
|
+
version: string
|
|
196
|
+
buildId: string
|
|
197
|
+
generatedAt: string
|
|
198
|
+
selectors: SelectorManifestEntry[]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
interface SelectorManifestEntry {
|
|
202
|
+
id: string // data-clippy-id value
|
|
203
|
+
selector: string // CSS selector string
|
|
204
|
+
component: string // enclosing component name
|
|
205
|
+
tag: string // lowercase HTML tag
|
|
206
|
+
label?: string // human-readable label (may be null)
|
|
207
|
+
routes: string[] // all routes where this element appears
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### `DiscoveredRoute`
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
interface DiscoveredRoute {
|
|
215
|
+
path: string // e.g. "/dashboard/forms/[id]"
|
|
216
|
+
filePath: string // relative to project root, e.g. "app/dashboard/forms/[id]/page.tsx"
|
|
217
|
+
isDynamic: boolean
|
|
218
|
+
params: string[] // e.g. ["id"]
|
|
219
|
+
layout: string | null // relative path to nearest layout file
|
|
220
|
+
routerType: 'app' | 'pages' | 'react-router' | 'tanstack'
|
|
221
|
+
semantic: string // space-separated route segments, reversed
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### `PolicyComponent`
|
|
226
|
+
|
|
227
|
+
Only components with at least one `stateVariable` or `interaction` are included.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
interface PolicyComponent {
|
|
231
|
+
name: string
|
|
232
|
+
filePath: string // relative to project root
|
|
233
|
+
route: string
|
|
234
|
+
stateVariables: Array<{
|
|
235
|
+
name: string
|
|
236
|
+
setter?: string
|
|
237
|
+
initialValue?: string
|
|
238
|
+
}>
|
|
239
|
+
interactions: ComponentInteraction[]
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface ComponentInteraction {
|
|
243
|
+
trigger: {
|
|
244
|
+
event: string // e.g. "onClick"
|
|
245
|
+
element: string // JSX tag that carries the handler
|
|
246
|
+
setsState?: string // state variable the handler mutates
|
|
247
|
+
}
|
|
248
|
+
effect: {
|
|
249
|
+
type: 'conditionalRender' | 'asyncEffect' | 'contextDependency'
|
|
250
|
+
rendersWhenTrue?: string // component name that appears
|
|
251
|
+
rendersWhenFalse?: string // component name that disappears
|
|
252
|
+
waitStrategy: 'elementAppears' | 'domSettle' | 'none'
|
|
253
|
+
selector?: string // [data-clippy-component='...'] selector to wait for
|
|
254
|
+
settleMs?: number // milliseconds to wait for DOM to settle
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### `PolicyFlow`
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
interface PolicyFlow {
|
|
263
|
+
flowId: string
|
|
264
|
+
page: string // starting route
|
|
265
|
+
intentPatterns: string[] // user phrases that match this flow
|
|
266
|
+
steps: PolicyFlowStep[]
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
interface PolicyFlowStep {
|
|
270
|
+
step: number
|
|
271
|
+
action: 'navigate' | 'transition' | 'interact'
|
|
272
|
+
target: string // CSS selector for the element to act on
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Selector ID format
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
ComponentName[-LabelText]-tag-lineNumber
|
|
282
|
+
|
|
283
|
+
Examples:
|
|
284
|
+
DashboardForms-CreateForm-button-138
|
|
285
|
+
AdminTransactions-Approve-button-172
|
|
286
|
+
LoginPage-form-115 (no label — form labels use submit button text only)
|
|
287
|
+
Input-input-7 (shared component, no label from its own file)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
- **ComponentName** — enclosing React component, or route-derived name when generic (e.g., `AdminTransactions` instead of `Page`)
|
|
291
|
+
- **LabelText** — `aria-label` > visible text (TitleCase, max 2 words) > `placeholder`. Omitted for forms (submit button text is used instead of the full field list)
|
|
292
|
+
- **tag** — lowercase HTML tag
|
|
293
|
+
- **lineNumber** — source file line, stable tiebreaker
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## Related packages
|
|
298
|
+
|
|
299
|
+
- [`@dcoder-x/next`](https://www.npmjs.com/package/@dcoder-x/next) — Next.js adapter
|
|
300
|
+
- [`@dcoder-x/vite`](https://www.npmjs.com/package/@dcoder-x/vite) — Vite adapter
|