@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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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 = [
|
|
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
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
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
|
-
|
|
205
|
+
candidates.push(resolved);
|
|
205
206
|
}
|
|
206
|
-
|
|
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)
|