@dcoder-x/plugin-shared 0.1.0
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/dist/extractors/ComponentExtractor.d.ts +12 -0
- package/dist/extractors/ComponentExtractor.js +194 -0
- package/dist/extractors/FlowInferrer.d.ts +10 -0
- package/dist/extractors/FlowInferrer.js +70 -0
- package/dist/extractors/RouteExtractor.d.ts +14 -0
- package/dist/extractors/RouteExtractor.js +188 -0
- package/dist/extractors/SelectorGenerator.d.ts +5 -0
- package/dist/extractors/SelectorGenerator.js +62 -0
- package/dist/extractors/SemanticAnalyser.d.ts +4 -0
- package/dist/extractors/SemanticAnalyser.js +17 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +2 -0
- package/dist/upload/PackageBuilder.d.ts +5 -0
- package/dist/upload/PackageBuilder.js +20 -0
- package/dist/upload/PackageWriter.d.ts +7 -0
- package/dist/upload/PackageWriter.js +22 -0
- package/dist/upload/Uploader.d.ts +10 -0
- package/dist/upload/Uploader.js +71 -0
- package/package.json +27 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ModuleGraphSource, DiscoveredRoute, DiscoveredElement } from '../types';
|
|
2
|
+
export declare class ComponentExtractor {
|
|
3
|
+
private source;
|
|
4
|
+
private routes;
|
|
5
|
+
constructor(source: ModuleGraphSource, routes: DiscoveredRoute[]);
|
|
6
|
+
extract(): Promise<DiscoveredElement[]>;
|
|
7
|
+
private getModuleFilesForRoute;
|
|
8
|
+
private walkWebpackGraph;
|
|
9
|
+
private walkRollupGraph;
|
|
10
|
+
private extractFromSource;
|
|
11
|
+
private readSource;
|
|
12
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ComponentExtractor = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const parser_1 = require("@babel/parser");
|
|
10
|
+
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
11
|
+
class ComponentExtractor {
|
|
12
|
+
constructor(source, routes) {
|
|
13
|
+
this.source = source;
|
|
14
|
+
this.routes = routes;
|
|
15
|
+
}
|
|
16
|
+
async extract() {
|
|
17
|
+
const elements = [];
|
|
18
|
+
for (const route of this.routes) {
|
|
19
|
+
const moduleFiles = this.getModuleFilesForRoute(route.filePath);
|
|
20
|
+
for (const filePath of moduleFiles) {
|
|
21
|
+
const source = this.readSource(filePath);
|
|
22
|
+
if (!source)
|
|
23
|
+
continue;
|
|
24
|
+
elements.push(...this.extractFromSource(source, filePath, route.path));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return elements;
|
|
28
|
+
}
|
|
29
|
+
getModuleFilesForRoute(entryFilePath) {
|
|
30
|
+
if (this.source.type === 'webpack') {
|
|
31
|
+
return this.walkWebpackGraph(entryFilePath, this.source.compilation);
|
|
32
|
+
}
|
|
33
|
+
return this.walkRollupGraph(entryFilePath, this.source.moduleGraph);
|
|
34
|
+
}
|
|
35
|
+
walkWebpackGraph(entryFilePath, compilation) {
|
|
36
|
+
const visited = new Set();
|
|
37
|
+
const queue = [entryFilePath];
|
|
38
|
+
while (queue.length > 0) {
|
|
39
|
+
const current = queue.shift();
|
|
40
|
+
if (visited.has(current))
|
|
41
|
+
continue;
|
|
42
|
+
visited.add(current);
|
|
43
|
+
const mod = compilation.moduleGraph.getModuleByIdentifier(current);
|
|
44
|
+
if (!mod)
|
|
45
|
+
continue;
|
|
46
|
+
for (const dep of compilation.moduleGraph.getOutgoingDependencies(mod)) {
|
|
47
|
+
const depModule = compilation.moduleGraph.getResolvedModule(dep);
|
|
48
|
+
const depPath = normalizeModulePath(depModule?.resource || depModule?.userRequest);
|
|
49
|
+
if (depPath && !depPath.includes('node_modules')) {
|
|
50
|
+
queue.push(depPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return Array.from(visited);
|
|
55
|
+
}
|
|
56
|
+
walkRollupGraph(entryFilePath, graph) {
|
|
57
|
+
const visited = new Set();
|
|
58
|
+
const queue = [entryFilePath];
|
|
59
|
+
while (queue.length > 0) {
|
|
60
|
+
const current = queue.shift();
|
|
61
|
+
if (visited.has(current))
|
|
62
|
+
continue;
|
|
63
|
+
visited.add(current);
|
|
64
|
+
const mod = graph.get(current);
|
|
65
|
+
if (!mod)
|
|
66
|
+
continue;
|
|
67
|
+
for (const importId of mod.importedIds) {
|
|
68
|
+
const importPath = normalizeModulePath(importId, current);
|
|
69
|
+
if (importPath && !importPath.includes('node_modules')) {
|
|
70
|
+
queue.push(importPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return Array.from(visited);
|
|
75
|
+
}
|
|
76
|
+
extractFromSource(source, filePath, route) {
|
|
77
|
+
const elements = [];
|
|
78
|
+
let ast;
|
|
79
|
+
try {
|
|
80
|
+
ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
(0, traverse_1.default)(ast, {
|
|
86
|
+
JSXOpeningElement(nodePath) {
|
|
87
|
+
const tagName = nodePath.node.name?.name;
|
|
88
|
+
if (!tagName)
|
|
89
|
+
return;
|
|
90
|
+
const isInteractive = ['button', 'input', 'select', 'textarea', 'a', 'form'].includes(tagName) ||
|
|
91
|
+
nodePath.node.attributes.some((attr) => attr.type === 'JSXAttribute' && attr.name?.name === 'onClick');
|
|
92
|
+
if (!isInteractive)
|
|
93
|
+
return;
|
|
94
|
+
const staticProps = extractStaticProps(nodePath.node.attributes);
|
|
95
|
+
const nearbyText = extractNearbyText(nodePath);
|
|
96
|
+
elements.push({
|
|
97
|
+
tag: tagName,
|
|
98
|
+
filePath,
|
|
99
|
+
route,
|
|
100
|
+
staticProps,
|
|
101
|
+
nearbyText,
|
|
102
|
+
semanticRole: inferSemanticRole(tagName, staticProps, nearbyText),
|
|
103
|
+
interactions: inferInteractions(tagName, staticProps),
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
return elements;
|
|
108
|
+
}
|
|
109
|
+
readSource(filePath) {
|
|
110
|
+
try {
|
|
111
|
+
return fs_1.default.readFileSync(filePath, 'utf-8');
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.ComponentExtractor = ComponentExtractor;
|
|
119
|
+
function normalizeModulePath(rawPath, fromFile) {
|
|
120
|
+
if (!rawPath)
|
|
121
|
+
return null;
|
|
122
|
+
// Ignore virtual and plugin-internal module IDs.
|
|
123
|
+
if (rawPath.startsWith('\u0000') || rawPath.startsWith('virtual:'))
|
|
124
|
+
return null;
|
|
125
|
+
let cleaned = rawPath;
|
|
126
|
+
if (cleaned.startsWith('file://')) {
|
|
127
|
+
try {
|
|
128
|
+
cleaned = new URL(cleaned).pathname;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Strip query/hash fragments from bundler IDs.
|
|
135
|
+
cleaned = cleaned.split('?')[0].split('#')[0];
|
|
136
|
+
if (!cleaned)
|
|
137
|
+
return null;
|
|
138
|
+
// Resolve relative IDs against the importing file when available.
|
|
139
|
+
if (fromFile && cleaned.startsWith('.')) {
|
|
140
|
+
cleaned = path_1.default.resolve(path_1.default.dirname(fromFile), cleaned);
|
|
141
|
+
}
|
|
142
|
+
return path_1.default.normalize(cleaned);
|
|
143
|
+
}
|
|
144
|
+
function extractStaticProps(attributes) {
|
|
145
|
+
const props = {};
|
|
146
|
+
for (const attr of attributes) {
|
|
147
|
+
if (attr.type !== 'JSXAttribute')
|
|
148
|
+
continue;
|
|
149
|
+
const key = attr.name?.name;
|
|
150
|
+
if (!key)
|
|
151
|
+
continue;
|
|
152
|
+
if (attr.value?.type === 'StringLiteral') {
|
|
153
|
+
props[key] = attr.value.value;
|
|
154
|
+
}
|
|
155
|
+
if (attr.value?.type === 'JSXExpressionContainer' &&
|
|
156
|
+
attr.value.expression?.type === 'StringLiteral') {
|
|
157
|
+
props[key] = attr.value.expression.value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return props;
|
|
161
|
+
}
|
|
162
|
+
function extractNearbyText(nodePath) {
|
|
163
|
+
const texts = [];
|
|
164
|
+
const parent = nodePath.parentPath?.node;
|
|
165
|
+
if (parent?.children) {
|
|
166
|
+
for (const child of parent.children) {
|
|
167
|
+
if (child.type === 'JSXText') {
|
|
168
|
+
const text = child.value.trim();
|
|
169
|
+
if (text)
|
|
170
|
+
texts.push(text);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return texts;
|
|
175
|
+
}
|
|
176
|
+
function inferSemanticRole(tag, props, nearbyText) {
|
|
177
|
+
const label = props['aria-label'] || props['placeholder'] || nearbyText[0] || '';
|
|
178
|
+
return `${label} ${tag}`.trim().toLowerCase();
|
|
179
|
+
}
|
|
180
|
+
function inferInteractions(tag, props) {
|
|
181
|
+
if (tag === 'button')
|
|
182
|
+
return ['click'];
|
|
183
|
+
if (tag === 'a')
|
|
184
|
+
return ['click', 'navigate'];
|
|
185
|
+
if (tag === 'input')
|
|
186
|
+
return props['type'] === 'submit' ? ['click'] : ['type'];
|
|
187
|
+
if (tag === 'select')
|
|
188
|
+
return ['select'];
|
|
189
|
+
if (tag === 'textarea')
|
|
190
|
+
return ['type'];
|
|
191
|
+
if (tag === 'form')
|
|
192
|
+
return ['submit'];
|
|
193
|
+
return ['click'];
|
|
194
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DiscoveredRoute, DiscoveredElement, InferredFlow } from '../types';
|
|
2
|
+
export declare class FlowInferrer {
|
|
3
|
+
private routes;
|
|
4
|
+
private elements;
|
|
5
|
+
constructor(routes: DiscoveredRoute[], elements: DiscoveredElement[]);
|
|
6
|
+
infer(): InferredFlow[];
|
|
7
|
+
private buildEdgeGraph;
|
|
8
|
+
private detectLinearFlows;
|
|
9
|
+
private buildChain;
|
|
10
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowInferrer = void 0;
|
|
4
|
+
class FlowInferrer {
|
|
5
|
+
constructor(routes, elements) {
|
|
6
|
+
this.routes = routes;
|
|
7
|
+
this.elements = elements;
|
|
8
|
+
}
|
|
9
|
+
infer() {
|
|
10
|
+
const edges = this.buildEdgeGraph();
|
|
11
|
+
return this.detectLinearFlows(edges);
|
|
12
|
+
}
|
|
13
|
+
buildEdgeGraph() {
|
|
14
|
+
const edges = [];
|
|
15
|
+
const routePaths = new Set(this.routes.map((r) => r.path));
|
|
16
|
+
for (const el of this.elements) {
|
|
17
|
+
const href = el.staticProps['href'] || el.staticProps['to'];
|
|
18
|
+
if (el.tag === 'a' && href && routePaths.has(href)) {
|
|
19
|
+
edges.push({
|
|
20
|
+
from: el.route,
|
|
21
|
+
to: href,
|
|
22
|
+
trigger: el.staticProps['aria-label'] || el.nearbyText[0] || 'link',
|
|
23
|
+
confidence: 0.85,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return edges;
|
|
28
|
+
}
|
|
29
|
+
detectLinearFlows(edges) {
|
|
30
|
+
const adjacency = new Map();
|
|
31
|
+
for (const edge of edges) {
|
|
32
|
+
if (!adjacency.has(edge.from))
|
|
33
|
+
adjacency.set(edge.from, []);
|
|
34
|
+
adjacency.get(edge.from).push(edge);
|
|
35
|
+
}
|
|
36
|
+
const flows = [];
|
|
37
|
+
const visited = new Set();
|
|
38
|
+
for (const route of this.routes) {
|
|
39
|
+
if (visited.has(route.path))
|
|
40
|
+
continue;
|
|
41
|
+
const chain = this.buildChain(route.path, adjacency, new Set());
|
|
42
|
+
if (chain.length >= 2) {
|
|
43
|
+
flows.push({
|
|
44
|
+
id: `flow_${flows.length + 1}`,
|
|
45
|
+
name: chain
|
|
46
|
+
.map((p) => p.split('/').filter(Boolean).pop() || 'home')
|
|
47
|
+
.join(' -> '),
|
|
48
|
+
steps: chain,
|
|
49
|
+
edges: edges.filter((e) => {
|
|
50
|
+
const s = new Set(chain);
|
|
51
|
+
return s.has(e.from) && s.has(e.to);
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
chain.forEach((p) => visited.add(p));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return flows;
|
|
58
|
+
}
|
|
59
|
+
buildChain(start, adjacency, seen) {
|
|
60
|
+
if (seen.has(start))
|
|
61
|
+
return [];
|
|
62
|
+
seen.add(start);
|
|
63
|
+
const next = adjacency.get(start);
|
|
64
|
+
if (!next?.length)
|
|
65
|
+
return [start];
|
|
66
|
+
const best = [...next].sort((a, b) => b.confidence - a.confidence)[0];
|
|
67
|
+
return [start, ...this.buildChain(best.to, adjacency, seen)];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
exports.FlowInferrer = FlowInferrer;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DiscoveredRoute } from '../types';
|
|
2
|
+
export declare class RouteExtractor {
|
|
3
|
+
private dir;
|
|
4
|
+
constructor(dir: string);
|
|
5
|
+
extract(): Promise<DiscoveredRoute[]>;
|
|
6
|
+
private walkAppDir;
|
|
7
|
+
private walkPagesDir;
|
|
8
|
+
private walkTanStackDir;
|
|
9
|
+
private extractReactRouterRoutes;
|
|
10
|
+
private findFilesContaining;
|
|
11
|
+
private extractParams;
|
|
12
|
+
private findNearestLayout;
|
|
13
|
+
private deriveSemanticMeaning;
|
|
14
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RouteExtractor = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
class RouteExtractor {
|
|
10
|
+
constructor(dir) {
|
|
11
|
+
this.dir = dir;
|
|
12
|
+
}
|
|
13
|
+
async extract() {
|
|
14
|
+
const routes = [];
|
|
15
|
+
const appDir = path_1.default.join(this.dir, 'app');
|
|
16
|
+
const pagesDir = path_1.default.join(this.dir, 'pages');
|
|
17
|
+
const srcDir = path_1.default.join(this.dir, 'src');
|
|
18
|
+
if (fs_1.default.existsSync(appDir)) {
|
|
19
|
+
routes.push(...this.walkAppDir(appDir, ''));
|
|
20
|
+
}
|
|
21
|
+
if (fs_1.default.existsSync(pagesDir)) {
|
|
22
|
+
routes.push(...this.walkPagesDir(pagesDir));
|
|
23
|
+
}
|
|
24
|
+
const routesDir = path_1.default.join(this.dir, 'src', 'routes');
|
|
25
|
+
if (fs_1.default.existsSync(routesDir)) {
|
|
26
|
+
routes.push(...this.walkTanStackDir(routesDir));
|
|
27
|
+
}
|
|
28
|
+
if (routes.length === 0) {
|
|
29
|
+
const reactRouterRoutes = await this.extractReactRouterRoutes(srcDir);
|
|
30
|
+
routes.push(...reactRouterRoutes);
|
|
31
|
+
}
|
|
32
|
+
return routes;
|
|
33
|
+
}
|
|
34
|
+
walkAppDir(dir, routePrefix) {
|
|
35
|
+
const results = [];
|
|
36
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
39
|
+
if (entry.isDirectory()) {
|
|
40
|
+
const isRouteGroup = entry.name.startsWith('(') && entry.name.endsWith(')');
|
|
41
|
+
const segment = isRouteGroup ? '' : `/${entry.name}`;
|
|
42
|
+
results.push(...this.walkAppDir(fullPath, routePrefix + segment));
|
|
43
|
+
}
|
|
44
|
+
if (entry.isFile() && /^page\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
45
|
+
const routePath = routePrefix || '/';
|
|
46
|
+
results.push({
|
|
47
|
+
path: routePath,
|
|
48
|
+
filePath: fullPath,
|
|
49
|
+
isDynamic: routePath.includes('['),
|
|
50
|
+
params: this.extractParams(routePath),
|
|
51
|
+
layout: this.findNearestLayout(dir),
|
|
52
|
+
routerType: 'app',
|
|
53
|
+
semantic: this.deriveSemanticMeaning(routePath),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return results;
|
|
58
|
+
}
|
|
59
|
+
walkPagesDir(dir) {
|
|
60
|
+
const results = [];
|
|
61
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
64
|
+
if (entry.isDirectory() && entry.name !== 'api') {
|
|
65
|
+
results.push(...this.walkPagesDir(fullPath));
|
|
66
|
+
}
|
|
67
|
+
if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
68
|
+
const relativePath = path_1.default
|
|
69
|
+
.relative(path_1.default.join(this.dir, 'pages'), fullPath)
|
|
70
|
+
.replace(/\.(tsx?|jsx?)$/, '')
|
|
71
|
+
.replace(/\\/g, '/');
|
|
72
|
+
// index.tsx maps to the parent segment path.
|
|
73
|
+
// examples:
|
|
74
|
+
// pages/index.tsx -> /
|
|
75
|
+
// pages/blog/index.tsx -> /blog
|
|
76
|
+
const routePath = relativePath === 'index'
|
|
77
|
+
? '/'
|
|
78
|
+
: `/${relativePath.replace(/\/index$/, '')}`;
|
|
79
|
+
results.push({
|
|
80
|
+
path: routePath,
|
|
81
|
+
filePath: fullPath,
|
|
82
|
+
isDynamic: routePath.includes('['),
|
|
83
|
+
params: this.extractParams(routePath),
|
|
84
|
+
layout: null,
|
|
85
|
+
routerType: 'pages',
|
|
86
|
+
semantic: this.deriveSemanticMeaning(routePath),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
}
|
|
92
|
+
walkTanStackDir(dir) {
|
|
93
|
+
const results = [];
|
|
94
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
97
|
+
const name = entry.name.replace(/\.(tsx?|jsx?)$/, '');
|
|
98
|
+
if (name === '__root' || name === 'index')
|
|
99
|
+
continue;
|
|
100
|
+
const routePath = '/' + name.replace(/\./g, '/');
|
|
101
|
+
results.push({
|
|
102
|
+
path: routePath,
|
|
103
|
+
filePath: path_1.default.join(dir, entry.name),
|
|
104
|
+
isDynamic: routePath.includes('$'),
|
|
105
|
+
params: this.extractParams(routePath),
|
|
106
|
+
layout: null,
|
|
107
|
+
routerType: 'tanstack',
|
|
108
|
+
semantic: this.deriveSemanticMeaning(routePath),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
async extractReactRouterRoutes(srcDir) {
|
|
115
|
+
if (!fs_1.default.existsSync(srcDir))
|
|
116
|
+
return [];
|
|
117
|
+
const { parse } = await import('@babel/parser');
|
|
118
|
+
const traverse = (await import('@babel/traverse')).default;
|
|
119
|
+
const results = [];
|
|
120
|
+
const routerFiles = this.findFilesContaining(srcDir, /createBrowserRouter|<Route/);
|
|
121
|
+
for (const filePath of routerFiles) {
|
|
122
|
+
const source = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
123
|
+
let ast;
|
|
124
|
+
try {
|
|
125
|
+
ast = parse(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
traverse(ast, {
|
|
131
|
+
JSXOpeningElement(nodePath) {
|
|
132
|
+
const name = nodePath.node.name?.name;
|
|
133
|
+
if (name !== 'Route')
|
|
134
|
+
return;
|
|
135
|
+
const pathAttr = nodePath.node.attributes.find((a) => a.name?.name === 'path');
|
|
136
|
+
const routePath = pathAttr?.value?.value;
|
|
137
|
+
if (routePath) {
|
|
138
|
+
results.push({
|
|
139
|
+
path: routePath,
|
|
140
|
+
filePath,
|
|
141
|
+
isDynamic: routePath.includes(':'),
|
|
142
|
+
params: (routePath.match(/:(\w+)/g) || []).map((p) => p.slice(1)),
|
|
143
|
+
layout: null,
|
|
144
|
+
routerType: 'react-router',
|
|
145
|
+
semantic: this.deriveSemanticMeaning(routePath),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
findFilesContaining(dir, pattern) {
|
|
154
|
+
const results = [];
|
|
155
|
+
const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const fullPath = path_1.default.join(dir, entry.name);
|
|
158
|
+
if (entry.isDirectory() && entry.name !== 'node_modules') {
|
|
159
|
+
results.push(...this.findFilesContaining(fullPath, pattern));
|
|
160
|
+
}
|
|
161
|
+
if (entry.isFile() && /\.(tsx?|jsx?)$/.test(entry.name)) {
|
|
162
|
+
const content = fs_1.default.readFileSync(fullPath, 'utf-8');
|
|
163
|
+
if (pattern.test(content))
|
|
164
|
+
results.push(fullPath);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
extractParams(routePath) {
|
|
170
|
+
const nextParams = (routePath.match(/\[([^\]]+)\]/g) || []).map((m) => m.replace(/[\[\]\.]/g, ''));
|
|
171
|
+
const rrParams = (routePath.match(/:(\w+)/g) || []).map((m) => m.slice(1));
|
|
172
|
+
return [...nextParams, ...rrParams];
|
|
173
|
+
}
|
|
174
|
+
findNearestLayout(dir) {
|
|
175
|
+
return (['layout.tsx', 'layout.ts', 'layout.jsx', 'layout.js']
|
|
176
|
+
.map((f) => path_1.default.join(dir, f))
|
|
177
|
+
.find((f) => fs_1.default.existsSync(f)) || null);
|
|
178
|
+
}
|
|
179
|
+
deriveSemanticMeaning(routePath) {
|
|
180
|
+
return routePath
|
|
181
|
+
.split('/')
|
|
182
|
+
.filter(Boolean)
|
|
183
|
+
.filter((s) => !s.startsWith('[') && !s.startsWith(':') && !s.startsWith('$'))
|
|
184
|
+
.reverse()
|
|
185
|
+
.join(' ');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.RouteExtractor = RouteExtractor;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SelectorGenerator = void 0;
|
|
4
|
+
class SelectorGenerator {
|
|
5
|
+
generate(elements) {
|
|
6
|
+
return elements.map((el) => ({
|
|
7
|
+
...el,
|
|
8
|
+
selectors: this.generateForElement(el),
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
11
|
+
generateForElement(el) {
|
|
12
|
+
const candidates = [];
|
|
13
|
+
const p = el.staticProps;
|
|
14
|
+
if (p['data-testid']) {
|
|
15
|
+
candidates.push({
|
|
16
|
+
type: 'testid',
|
|
17
|
+
value: `[data-testid="${p['data-testid']}"]`,
|
|
18
|
+
confidence: 0.97,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (p['aria-label']) {
|
|
22
|
+
candidates.push({
|
|
23
|
+
type: 'aria',
|
|
24
|
+
value: `${el.tag}[aria-label="${p['aria-label']}"]`,
|
|
25
|
+
confidence: 0.93,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (p['role']) {
|
|
29
|
+
candidates.push({
|
|
30
|
+
type: 'aria',
|
|
31
|
+
value: `[role="${p['role']}"]`,
|
|
32
|
+
confidence: 0.88,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (p['id'] && !isDynamicId(p['id'])) {
|
|
36
|
+
candidates.push({
|
|
37
|
+
type: 'id',
|
|
38
|
+
value: `#${p['id']}`,
|
|
39
|
+
confidence: 0.85,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (el.tag === 'input' && p['name']) {
|
|
43
|
+
candidates.push({
|
|
44
|
+
type: 'semantic',
|
|
45
|
+
value: `input[name="${p['name']}"]`,
|
|
46
|
+
confidence: 0.8,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (el.tag === 'button' && el.nearbyText[0]) {
|
|
50
|
+
candidates.push({
|
|
51
|
+
type: 'text',
|
|
52
|
+
value: `button:contains("${el.nearbyText[0]}")`,
|
|
53
|
+
confidence: 0.72,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return candidates.sort((a, b) => b.confidence - a.confidence);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
exports.SelectorGenerator = SelectorGenerator;
|
|
60
|
+
function isDynamicId(id) {
|
|
61
|
+
return /\d{3,}/.test(id) || id.includes('__') || id.startsWith(':');
|
|
62
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SemanticAnalyser = void 0;
|
|
4
|
+
class SemanticAnalyser {
|
|
5
|
+
deriveRouteSemantic(routePath) {
|
|
6
|
+
return routePath
|
|
7
|
+
.split('/')
|
|
8
|
+
.filter(Boolean)
|
|
9
|
+
.filter((s) => !s.startsWith('[') && !s.startsWith(':') && !s.startsWith('$'))
|
|
10
|
+
.reverse()
|
|
11
|
+
.join(' ');
|
|
12
|
+
}
|
|
13
|
+
deriveElementSemantic(label, tag) {
|
|
14
|
+
return `${label} ${tag}`.trim().toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.SemanticAnalyser = SemanticAnalyser;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ClippyPluginOptions, ModuleGraphSource, RollupModuleInfo, DiscoveredRoute, DiscoveredElement, SelectorCandidate, ElementWithSelectors, FlowEdge, InferredFlow, KnowledgePackage, } from './types';
|
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export interface ClippyPluginOptions {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
projectId: string;
|
|
4
|
+
skipUpload?: boolean;
|
|
5
|
+
productionOnly?: boolean;
|
|
6
|
+
localOutputDir?: string;
|
|
7
|
+
}
|
|
8
|
+
export type ModuleGraphSource = {
|
|
9
|
+
type: 'webpack';
|
|
10
|
+
compilation: any;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'rollup';
|
|
13
|
+
moduleGraph: Map<string, RollupModuleInfo>;
|
|
14
|
+
};
|
|
15
|
+
export interface RollupModuleInfo {
|
|
16
|
+
id: string;
|
|
17
|
+
importedIds: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export interface DiscoveredRoute {
|
|
20
|
+
path: string;
|
|
21
|
+
filePath: string;
|
|
22
|
+
isDynamic: boolean;
|
|
23
|
+
params: string[];
|
|
24
|
+
layout: string | null;
|
|
25
|
+
routerType: 'app' | 'pages' | 'react-router' | 'tanstack';
|
|
26
|
+
semantic: string;
|
|
27
|
+
}
|
|
28
|
+
export interface DiscoveredElement {
|
|
29
|
+
tag: string;
|
|
30
|
+
filePath: string;
|
|
31
|
+
route: string;
|
|
32
|
+
staticProps: Record<string, string>;
|
|
33
|
+
nearbyText: string[];
|
|
34
|
+
semanticRole: string;
|
|
35
|
+
interactions: string[];
|
|
36
|
+
}
|
|
37
|
+
export interface SelectorCandidate {
|
|
38
|
+
type: 'testid' | 'aria' | 'id' | 'semantic' | 'text' | 'structural';
|
|
39
|
+
value: string;
|
|
40
|
+
confidence: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ElementWithSelectors extends DiscoveredElement {
|
|
43
|
+
selectors: SelectorCandidate[];
|
|
44
|
+
}
|
|
45
|
+
export interface FlowEdge {
|
|
46
|
+
from: string;
|
|
47
|
+
to: string;
|
|
48
|
+
trigger: string;
|
|
49
|
+
confidence: number;
|
|
50
|
+
}
|
|
51
|
+
export interface InferredFlow {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
steps: string[];
|
|
55
|
+
edges: FlowEdge[];
|
|
56
|
+
}
|
|
57
|
+
export interface KnowledgePackage {
|
|
58
|
+
buildId: string;
|
|
59
|
+
generatedAt: string;
|
|
60
|
+
bundler: 'webpack' | 'vite';
|
|
61
|
+
routes: DiscoveredRoute[];
|
|
62
|
+
components: DiscoveredElement[];
|
|
63
|
+
selectors: ElementWithSelectors[];
|
|
64
|
+
flows: InferredFlow[];
|
|
65
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PackageBuilder = void 0;
|
|
7
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
8
|
+
class PackageBuilder {
|
|
9
|
+
buildPackage(data) {
|
|
10
|
+
return {
|
|
11
|
+
...data,
|
|
12
|
+
generatedAt: new Date().toISOString(),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
build(data) {
|
|
16
|
+
const pkg = this.buildPackage(data);
|
|
17
|
+
return zlib_1.default.gzipSync(JSON.stringify(pkg));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.PackageBuilder = PackageBuilder;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.PackageWriter = void 0;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const zlib_1 = __importDefault(require("zlib"));
|
|
10
|
+
class PackageWriter {
|
|
11
|
+
write(outputDir, knowledgePackage) {
|
|
12
|
+
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
|
13
|
+
const baseName = `${knowledgePackage.bundler}-${knowledgePackage.buildId}`;
|
|
14
|
+
const jsonPath = path_1.default.join(outputDir, `${baseName}.json`);
|
|
15
|
+
const gzipPath = path_1.default.join(outputDir, `${baseName}.json.gz`);
|
|
16
|
+
const content = JSON.stringify(knowledgePackage, null, 2);
|
|
17
|
+
fs_1.default.writeFileSync(jsonPath, content, 'utf-8');
|
|
18
|
+
fs_1.default.writeFileSync(gzipPath, zlib_1.default.gzipSync(content));
|
|
19
|
+
return { jsonPath, gzipPath };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.PackageWriter = PackageWriter;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ClippyPluginOptions } from '../types';
|
|
2
|
+
export declare class Uploader {
|
|
3
|
+
private options;
|
|
4
|
+
private endpoint;
|
|
5
|
+
private requestTimeoutMs;
|
|
6
|
+
private maxAttempts;
|
|
7
|
+
constructor(options: ClippyPluginOptions);
|
|
8
|
+
upload(compressedPackage: Buffer): Promise<boolean>;
|
|
9
|
+
private uploadOnce;
|
|
10
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.Uploader = void 0;
|
|
7
|
+
const https_1 = __importDefault(require("https"));
|
|
8
|
+
class Uploader {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.options = options;
|
|
11
|
+
this.endpoint = 'https://api.clippy.dev/v1/knowledge';
|
|
12
|
+
this.requestTimeoutMs = 10000;
|
|
13
|
+
this.maxAttempts = 3;
|
|
14
|
+
}
|
|
15
|
+
async upload(compressedPackage) {
|
|
16
|
+
if (this.options.skipUpload)
|
|
17
|
+
return false;
|
|
18
|
+
let lastError = null;
|
|
19
|
+
for (let attempt = 1; attempt <= this.maxAttempts; attempt++) {
|
|
20
|
+
try {
|
|
21
|
+
await this.uploadOnce(compressedPackage);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
lastError = err;
|
|
26
|
+
if (attempt < this.maxAttempts) {
|
|
27
|
+
await delay(250 * Math.pow(2, attempt - 1));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
throw lastError || new Error('Upload failed after retries');
|
|
32
|
+
}
|
|
33
|
+
async uploadOnce(compressedPackage) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
let settled = false;
|
|
36
|
+
const finalize = (fn) => {
|
|
37
|
+
if (settled)
|
|
38
|
+
return;
|
|
39
|
+
settled = true;
|
|
40
|
+
fn();
|
|
41
|
+
};
|
|
42
|
+
const req = https_1.default.request(this.endpoint, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${this.options.apiKey}`,
|
|
46
|
+
'X-Project-Id': this.options.projectId,
|
|
47
|
+
'Content-Type': 'application/octet-stream',
|
|
48
|
+
'Content-Encoding': 'gzip',
|
|
49
|
+
'Content-Length': compressedPackage.length,
|
|
50
|
+
},
|
|
51
|
+
}, (res) => {
|
|
52
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
53
|
+
finalize(resolve);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
finalize(() => reject(new Error(`Upload failed: ${res.statusCode}`)));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
req.setTimeout(this.requestTimeoutMs, () => {
|
|
60
|
+
req.destroy(new Error('Upload timed out'));
|
|
61
|
+
});
|
|
62
|
+
req.on('error', (err) => finalize(() => reject(err)));
|
|
63
|
+
req.write(compressedPackage);
|
|
64
|
+
req.end();
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.Uploader = Uploader;
|
|
69
|
+
function delay(ms) {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dcoder-x/plugin-shared",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./dist/index.js",
|
|
15
|
+
"./extractors/RouteExtractor": "./dist/extractors/RouteExtractor.js",
|
|
16
|
+
"./extractors/ComponentExtractor": "./dist/extractors/ComponentExtractor.js",
|
|
17
|
+
"./extractors/SelectorGenerator": "./dist/extractors/SelectorGenerator.js",
|
|
18
|
+
"./extractors/FlowInferrer": "./dist/extractors/FlowInferrer.js",
|
|
19
|
+
"./extractors/SemanticAnalyser": "./dist/extractors/SemanticAnalyser.js",
|
|
20
|
+
"./upload/PackageBuilder": "./dist/upload/PackageBuilder.js",
|
|
21
|
+
"./upload/PackageWriter": "./dist/upload/PackageWriter.js",
|
|
22
|
+
"./upload/Uploader": "./dist/upload/Uploader.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json"
|
|
26
|
+
}
|
|
27
|
+
}
|