@dcoder-x/plugin-shared 0.1.11 → 0.1.13

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.
@@ -19,13 +19,39 @@ class ComponentExtractor {
19
19
  }
20
20
  async extract() {
21
21
  const elements = [];
22
+ // Build a direct filePath → route.path index so that files which are
23
+ // themselves route entries get their own route, not whichever layout or
24
+ // parent route happened to import them first.
25
+ const fileRouteIndex = new Map();
22
26
  for (const route of this.routes) {
23
- const moduleFiles = this.getModuleFilesForRoute(route.filePath);
27
+ fileRouteIndex.set(route.filePath, route.path);
28
+ fileRouteIndex.set(route.filePath.replace(/\\/g, '/'), route.path);
29
+ }
30
+ // Stop the module walk at other route entry files — they will be
31
+ // processed in their own iteration with the correct route assigned.
32
+ const allRouteFiles = new Set();
33
+ for (const route of this.routes) {
34
+ allRouteFiles.add(route.filePath);
35
+ allRouteFiles.add(route.filePath.replace(/\\/g, '/'));
36
+ }
37
+ const seen = new Set(); // deduplicate across routes
38
+ for (const route of this.routes) {
39
+ // The stop boundary excludes the current entry file itself (we need to visit it).
40
+ const stopAt = new Set([...allRouteFiles].filter((f) => f !== route.filePath && f !== route.filePath.replace(/\\/g, '/')));
41
+ const moduleFiles = this.getModuleFilesForRoute(route.filePath, stopAt);
24
42
  for (const filePath of moduleFiles) {
43
+ if (seen.has(filePath))
44
+ continue;
45
+ seen.add(filePath);
25
46
  const source = this.readSource(filePath);
26
47
  if (!source)
27
48
  continue;
28
- elements.push(...this.extractFromSource(source, filePath, route.path));
49
+ // Prefer the route this file is directly registered as; fall back to
50
+ // the route whose module graph we're walking (the layout/parent route).
51
+ const assignedRoute = fileRouteIndex.get(filePath) ??
52
+ fileRouteIndex.get(filePath.replace(/\\/g, '/')) ??
53
+ route.path;
54
+ elements.push(...this.extractFromSource(source, filePath, assignedRoute));
29
55
  }
30
56
  }
31
57
  return elements;
@@ -38,12 +64,30 @@ class ComponentExtractor {
38
64
  const components = [];
39
65
  const contextResolver = new ComponentContextResolver_1.ComponentContextResolver();
40
66
  const graphExtractor = new InteractionGraphExtractor_1.InteractionGraphExtractor();
67
+ const fileRouteIndex = new Map();
68
+ for (const route of this.routes) {
69
+ fileRouteIndex.set(route.filePath, route.path);
70
+ fileRouteIndex.set(route.filePath.replace(/\\/g, '/'), route.path);
71
+ }
72
+ const allRouteFiles = new Set();
73
+ for (const route of this.routes) {
74
+ allRouteFiles.add(route.filePath);
75
+ allRouteFiles.add(route.filePath.replace(/\\/g, '/'));
76
+ }
77
+ const seen = new Set();
41
78
  for (const route of this.routes) {
42
- const moduleFiles = this.getModuleFilesForRoute(route.filePath);
79
+ const stopAt = new Set([...allRouteFiles].filter((f) => f !== route.filePath && f !== route.filePath.replace(/\\/g, '/')));
80
+ const moduleFiles = this.getModuleFilesForRoute(route.filePath, stopAt);
43
81
  for (const filePath of moduleFiles) {
82
+ if (seen.has(filePath))
83
+ continue;
84
+ seen.add(filePath);
44
85
  const source = this.readSource(filePath);
45
86
  if (!source)
46
87
  continue;
88
+ const assignedRoute = fileRouteIndex.get(filePath) ??
89
+ fileRouteIndex.get(filePath.replace(/\\/g, '/')) ??
90
+ route.path;
47
91
  let ast;
48
92
  try {
49
93
  ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
@@ -61,7 +105,7 @@ class ComponentExtractor {
61
105
  components.push({
62
106
  name: componentName,
63
107
  filePath,
64
- route: route.path,
108
+ route: assignedRoute,
65
109
  stateVariables: context.stateVariables,
66
110
  interactions,
67
111
  });
@@ -79,7 +123,7 @@ class ComponentExtractor {
79
123
  components.push({
80
124
  name: componentName,
81
125
  filePath,
82
- route: route.path,
126
+ route: assignedRoute,
83
127
  stateVariables: context.stateVariables,
84
128
  interactions,
85
129
  });
@@ -90,15 +134,13 @@ class ComponentExtractor {
90
134
  }
91
135
  return components;
92
136
  }
93
- getModuleFilesForRoute(entryFilePath) {
137
+ getModuleFilesForRoute(entryFilePath, stopAt) {
94
138
  if (this.source.type === 'webpack') {
95
- return this.walkWebpackGraph(entryFilePath, this.source.compilation);
139
+ return this.walkWebpackGraph(entryFilePath, this.source.compilation, stopAt);
96
140
  }
97
- return this.walkRollupGraph(entryFilePath, this.source.moduleGraph);
141
+ return this.walkRollupGraph(entryFilePath, this.source.moduleGraph, stopAt);
98
142
  }
99
- walkWebpackGraph(entryFilePath, compilation) {
100
- // Normalise to relative forward-slash paths so visited keys and injectedMap
101
- // keys are consistent across macOS and Windows.
143
+ walkWebpackGraph(entryFilePath, compilation, stopAt) {
102
144
  const toRelFwd = (p) => path_1.default.isAbsolute(p)
103
145
  ? path_1.default.relative(process.cwd(), p).replace(/\\/g, '/')
104
146
  : p.replace(/\\/g, '/');
@@ -108,6 +150,9 @@ class ComponentExtractor {
108
150
  const current = queue.shift();
109
151
  if (visited.has(current))
110
152
  continue;
153
+ // Stop recursing into other route entry files — they own their own walk.
154
+ if (stopAt && current !== toRelFwd(entryFilePath) && (stopAt.has(current) || stopAt.has(path_1.default.resolve(process.cwd(), current))))
155
+ continue;
111
156
  visited.add(current);
112
157
  const mod = this.findWebpackModule(compilation, current);
113
158
  let queuedFromWebpackGraph = false;
@@ -119,10 +164,7 @@ class ComponentExtractor {
119
164
  }
120
165
  }
121
166
  }
122
- // Fallback for webpack variants where moduleGraph helpers are unavailable.
123
167
  if (!queuedFromWebpackGraph) {
124
- // getFileImportPaths resolves to absolute paths via resolveImportFile;
125
- // normalise them before queuing.
126
168
  for (const depPath of this.getFileImportPaths(current)) {
127
169
  if (depPath && !depPath.includes('node_modules')) {
128
170
  queue.push(toRelFwd(depPath));
@@ -235,24 +277,25 @@ class ComponentExtractor {
235
277
  }
236
278
  return resolved;
237
279
  }
238
- walkRollupGraph(entryFilePath, graph) {
239
- // Normalise the entry to a relative forward-slash path so it matches the
240
- // keys stored by ClippyVitePlugin (which also normalises to relative).
280
+ walkRollupGraph(entryFilePath, graph, stopAt) {
241
281
  const toRelFwd = (p) => path_1.default.isAbsolute(p)
242
282
  ? path_1.default.relative(process.cwd(), p).replace(/\\/g, '/')
243
283
  : p.replace(/\\/g, '/');
284
+ const entryKey = toRelFwd(entryFilePath);
244
285
  const visited = new Set();
245
- const queue = [toRelFwd(entryFilePath)];
286
+ const queue = [entryKey];
246
287
  while (queue.length > 0) {
247
288
  const current = queue.shift();
248
289
  if (visited.has(current))
249
290
  continue;
291
+ // Stop recursing into other route entry files — they own their own walk.
292
+ if (stopAt && current !== entryKey && (stopAt.has(current) || stopAt.has(path_1.default.resolve(process.cwd(), current))))
293
+ continue;
250
294
  visited.add(current);
251
295
  const mod = graph.get(current);
252
296
  if (!mod)
253
297
  continue;
254
298
  for (const importId of mod.importedIds) {
255
- // importedIds are already relative keys after the vite plugin normalised them
256
299
  if (importId && !importId.includes('node_modules')) {
257
300
  queue.push(toRelFwd(importId));
258
301
  }
@@ -416,8 +459,39 @@ function deriveLabel(tag, props, nearbyText) {
416
459
  return props['placeholder'];
417
460
  if (props['name'])
418
461
  return props['name'];
462
+ // Last resort: extract meaningful words from className for icon-only elements
463
+ // (e.g. password-toggle, close-modal buttons that contain only an SVG).
464
+ const className = props['className'] || props['class'] || '';
465
+ if (className) {
466
+ const meaningful = classNameToLabel(className);
467
+ if (meaningful)
468
+ return meaningful;
469
+ }
419
470
  return null;
420
471
  }
472
+ // Utility tokens that describe layout/spacing/color but not element purpose.
473
+ const TAILWIND_NOISE = new Set([
474
+ 'flex', 'grid', 'block', 'inline', 'hidden', 'relative', 'absolute', 'fixed', 'sticky',
475
+ 'items', 'justify', 'content', 'self', 'grow', 'shrink', 'order',
476
+ 'inset', 'top', 'right', 'bottom', 'left', 'z',
477
+ 'w', 'h', 'min', 'max', 'p', 'px', 'py', 'pt', 'pb', 'pl', 'pr', 'm', 'mx', 'my', 'mt', 'mb', 'ml', 'mr',
478
+ 'gap', 'space', 'divide', 'overflow', 'truncate', 'whitespace', 'break',
479
+ 'text', 'font', 'leading', 'tracking', 'uppercase', 'lowercase', 'capitalize', 'normal',
480
+ 'bg', 'border', 'rounded', 'shadow', 'ring', 'outline', 'opacity', 'transition', 'duration',
481
+ 'hover', 'focus', 'active', 'disabled', 'group', 'peer', 'placeholder',
482
+ 'sr', 'not', 'dark', 'sm', 'md', 'lg', 'xl', '2xl',
483
+ ]);
484
+ function classNameToLabel(className) {
485
+ const words = className
486
+ .split(/[\s\-_/]+/)
487
+ .map((w) => w.replace(/[^a-zA-Z]/g, '').toLowerCase())
488
+ .filter((w) => w.length > 2 && !TAILWIND_NOISE.has(w));
489
+ if (words.length === 0)
490
+ return null;
491
+ // Deduplicate, keep first 3 meaningful words
492
+ const unique = [...new Set(words)].slice(0, 3);
493
+ return unique.join(' ');
494
+ }
421
495
  function inferSemanticRole(tag, props, nearbyText) {
422
496
  const label = props['aria-label'] || props['placeholder'] || nearbyText[0] || '';
423
497
  return `${label} ${tag}`.trim().toLowerCase();
@@ -194,16 +194,32 @@ class RouteExtractor {
194
194
  const expr = elementAttr.value?.expression;
195
195
  if (!expr)
196
196
  return null;
197
- // Try each JSX component name in document order; first match in the import map wins.
198
- // Wrapper components like <Can> or <RouteGuard> won't be in the map, so we fall
199
- // through to the actual page component nested inside them.
197
+ // Collect all JSX component names in document order (outermost first, then
198
+ // children). For each, resolve via the import map, then pick the best candidate:
199
+ // a file under pages/ beats a wrapper/provider/layout/guard/context.
200
200
  const componentNames = this.collectJSXComponentNames(expr);
201
+ const candidates = [];
201
202
  for (const componentName of componentNames) {
202
203
  const resolved = importMap.get(componentName);
203
204
  if (resolved)
204
- return resolved;
205
+ candidates.push(resolved);
205
206
  }
206
- return null;
207
+ if (candidates.length === 0)
208
+ return null;
209
+ // Score: page files rank highest, known non-page patterns rank lowest.
210
+ const score = (filePath) => {
211
+ const p = filePath.replace(/\\/g, '/').toLowerCase();
212
+ if (p.includes('/pages/'))
213
+ return 3;
214
+ if (p.includes('/views/') || p.includes('/screens/'))
215
+ return 2;
216
+ // Wrappers, providers, guards, layouts, contexts are structural — not page components
217
+ if (/\/(contexts?|providers?|guards?|layouts?|wrappers?|hoc|utils?|helpers?)\//.test(p) ||
218
+ /(provider|context|guard|layout|redirect|catchall|catch-all)/.test(p))
219
+ return 0;
220
+ return 1;
221
+ };
222
+ return candidates.reduce((best, c) => (score(c) >= score(best) ? c : best));
207
223
  }
208
224
  collectJSXComponentNames(node) {
209
225
  if (!node)
package/dist/types.d.ts CHANGED
@@ -96,6 +96,7 @@ export interface PolicySelectorEntry {
96
96
  tag: string;
97
97
  component: string;
98
98
  label?: string;
99
+ nearbyText?: string[];
99
100
  route: string;
100
101
  filePath: string;
101
102
  attributes: Array<{
@@ -142,6 +143,7 @@ export interface SelectorManifestEntry {
142
143
  component: string;
143
144
  tag: string;
144
145
  label?: string;
146
+ nearbyText?: string[];
145
147
  routes: string[];
146
148
  }
147
149
  export interface SelectorManifest {
@@ -83,6 +83,7 @@ class PackageBuilder {
83
83
  component: entry.component,
84
84
  tag: entry.tag,
85
85
  label: entry.label,
86
+ nearbyText: entry.nearbyText,
86
87
  routes: [entry.route],
87
88
  });
88
89
  }
@@ -180,6 +181,7 @@ class PackageBuilder {
180
181
  tag: element.tag,
181
182
  component,
182
183
  label: element.label,
184
+ nearbyText: element.nearbyText?.length ? element.nearbyText : undefined,
183
185
  route: element.route,
184
186
  filePath: projectRoot
185
187
  ? this.normalizePath(element.filePath, projectRoot)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcoder-x/plugin-shared",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "private": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",