@dcoder-x/plugin-shared 0.1.6 → 0.1.8

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 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
@@ -6,6 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.RouteExtractor = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
+ const parser_1 = require("@babel/parser");
10
+ const traverse_1 = __importDefault(require("@babel/traverse"));
11
+ // @babel/traverse ships as CJS but may be loaded by an ESM host (e.g. Vite).
12
+ // The CJS-in-ESM import wraps exports in a namespace object, so `.default`
13
+ // may be the module object rather than the function. This guard handles both.
14
+ const traverse = typeof traverse_1.default === 'function' ? traverse_1.default : traverse_1.default.default;
9
15
  class RouteExtractor {
10
16
  constructor(dir) {
11
17
  this.dir = dir;
@@ -114,21 +120,19 @@ class RouteExtractor {
114
120
  async extractReactRouterRoutes(srcDir) {
115
121
  if (!fs_1.default.existsSync(srcDir))
116
122
  return [];
117
- const { parse } = await import('@babel/parser');
118
- const traverse = (await import('@babel/traverse')).default;
119
123
  const results = [];
120
124
  const routerFiles = this.findFilesContaining(srcDir, /createBrowserRouter|<Route/);
121
125
  for (const filePath of routerFiles) {
122
126
  const source = fs_1.default.readFileSync(filePath, 'utf-8');
123
127
  let ast;
124
128
  try {
125
- ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
129
+ ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
126
130
  }
127
131
  catch {
128
132
  continue;
129
133
  }
130
134
  traverse(ast, {
131
- JSXOpeningElement(nodePath) {
135
+ JSXOpeningElement: (nodePath) => {
132
136
  const name = nodePath.node.name?.name;
133
137
  if (name !== 'Route')
134
138
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcoder-x/plugin-shared",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",