@dcoder-x/plugin-shared 0.1.3 → 0.1.5
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/buildId.d.ts +5 -0
- package/dist/buildId.js +17 -0
- package/dist/extractors/ComponentContextResolver.d.ts +37 -0
- package/dist/extractors/ComponentContextResolver.js +167 -0
- package/dist/extractors/ComponentExtractor.d.ts +6 -1
- package/dist/extractors/ComponentExtractor.js +124 -11
- package/dist/extractors/FlowInferrer.d.ts +5 -2
- package/dist/extractors/FlowInferrer.js +117 -14
- package/dist/extractors/InteractionGraphExtractor.d.ts +28 -0
- package/dist/extractors/InteractionGraphExtractor.js +333 -0
- package/dist/extractors/SelectorGenerator.d.ts +6 -1
- package/dist/extractors/SelectorGenerator.js +28 -3
- package/dist/index.d.ts +7 -1
- package/dist/index.js +21 -0
- package/dist/injection/ClippyIdInjector.d.ts +17 -0
- package/dist/injection/ClippyIdInjector.js +164 -0
- package/dist/injection/HtmlTagGuards.d.ts +3 -0
- package/dist/injection/HtmlTagGuards.js +41 -0
- package/dist/injection/IdStrategy.d.ts +36 -0
- package/dist/injection/IdStrategy.js +148 -0
- package/dist/types.d.ts +108 -1
- package/dist/upload/Adapter.d.ts +3 -0
- package/dist/upload/Adapter.js +13 -0
- package/dist/upload/BackendAdapter.d.ts +30 -0
- package/dist/upload/BackendAdapter.js +42 -0
- package/dist/upload/BackendUploadAdapter.d.ts +13 -0
- package/dist/upload/BackendUploadAdapter.js +51 -0
- package/dist/upload/PackageBuilder.d.ts +34 -1
- package/dist/upload/PackageBuilder.js +220 -0
- package/dist/upload/PackageWriter.d.ts +9 -1
- package/dist/upload/PackageWriter.js +26 -0
- package/dist/upload/UploadStrategy.d.ts +11 -0
- package/dist/upload/UploadStrategy.js +40 -0
- package/dist/upload/Uploader.d.ts +6 -1
- package/dist/upload/Uploader.js +48 -3
- package/package.json +1 -1
package/dist/buildId.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveBuildId = deriveBuildId;
|
|
4
|
+
function deriveBuildId(moduleGraph) {
|
|
5
|
+
if (!moduleGraph.size) {
|
|
6
|
+
return `vite_${Date.now()}`;
|
|
7
|
+
}
|
|
8
|
+
const rows = Array.from(moduleGraph.values())
|
|
9
|
+
.map((mod) => `${mod.id}|${[...mod.importedIds].sort().join(',')}`)
|
|
10
|
+
.sort()
|
|
11
|
+
.join('\n');
|
|
12
|
+
let hash = 0;
|
|
13
|
+
for (let i = 0; i < rows.length; i++) {
|
|
14
|
+
hash = (hash * 31 + rows.charCodeAt(i)) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return `vite_${hash.toString(36)}`;
|
|
17
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { NodePath } from '@babel/traverse';
|
|
2
|
+
export interface StateVariable {
|
|
3
|
+
name: string;
|
|
4
|
+
setter?: string;
|
|
5
|
+
initialValue?: string;
|
|
6
|
+
line: number;
|
|
7
|
+
}
|
|
8
|
+
export interface EventHandler {
|
|
9
|
+
name: string;
|
|
10
|
+
events: string[];
|
|
11
|
+
line: number;
|
|
12
|
+
callsStateSetters: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ComponentContext {
|
|
15
|
+
componentName: string;
|
|
16
|
+
stateVariables: StateVariable[];
|
|
17
|
+
eventHandlers: EventHandler[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves state declarations, setters, and event handlers within a component scope.
|
|
21
|
+
* This provides the symbol table needed for interaction graph extraction.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ComponentContextResolver {
|
|
24
|
+
/**
|
|
25
|
+
* Extract state, handlers, and their relationships from a component AST node.
|
|
26
|
+
*/
|
|
27
|
+
extractContext(componentPath: NodePath<any>, componentName: string): ComponentContext;
|
|
28
|
+
/**
|
|
29
|
+
* Extract handler name, event references, and called setters from a function.
|
|
30
|
+
*/
|
|
31
|
+
private extractHandlerInfo;
|
|
32
|
+
/**
|
|
33
|
+
* Map event handlers referenced in JSX attributes to their handler definitions.
|
|
34
|
+
* Returns which state setters a given event attribute calls.
|
|
35
|
+
*/
|
|
36
|
+
getStateSettersCalledByEvent(event: string, handlers: EventHandler[]): string[];
|
|
37
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ComponentContextResolver = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Resolves state declarations, setters, and event handlers within a component scope.
|
|
6
|
+
* This provides the symbol table needed for interaction graph extraction.
|
|
7
|
+
*/
|
|
8
|
+
class ComponentContextResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Extract state, handlers, and their relationships from a component AST node.
|
|
11
|
+
*/
|
|
12
|
+
extractContext(componentPath, componentName) {
|
|
13
|
+
const stateVariables = [];
|
|
14
|
+
const eventHandlers = [];
|
|
15
|
+
const stateSetterMap = new Map(); // setter -> state var
|
|
16
|
+
// First pass: collect useState declarations and build setter map
|
|
17
|
+
componentPath.traverse({
|
|
18
|
+
VariableDeclarator: (varPath) => {
|
|
19
|
+
const id = varPath.node.id;
|
|
20
|
+
const init = varPath.node.init;
|
|
21
|
+
// Detect useState pattern: const [state, setState] = useState(...)
|
|
22
|
+
if (id.type === 'ArrayPattern' &&
|
|
23
|
+
id.elements &&
|
|
24
|
+
id.elements.length === 2 &&
|
|
25
|
+
init?.type === 'CallExpression') {
|
|
26
|
+
const callee = init.callee;
|
|
27
|
+
if ((callee.type === 'Identifier' && callee.name === 'useState') ||
|
|
28
|
+
(callee.type === 'MemberExpression' && callee.property?.name === 'useState')) {
|
|
29
|
+
const stateId = id.elements[0];
|
|
30
|
+
const setterIdNode = id.elements[1];
|
|
31
|
+
if (stateId?.type === 'Identifier' && setterIdNode?.type === 'Identifier') {
|
|
32
|
+
const stateName = stateId.name;
|
|
33
|
+
const setterName = setterIdNode.name;
|
|
34
|
+
stateSetterMap.set(setterName, stateName);
|
|
35
|
+
stateVariables.push({
|
|
36
|
+
name: stateName,
|
|
37
|
+
setter: setterName,
|
|
38
|
+
initialValue: init.arguments?.[0]?.type === 'StringLiteral'
|
|
39
|
+
? init.arguments[0].value
|
|
40
|
+
: undefined,
|
|
41
|
+
line: varPath.node.loc?.start.line ?? 0,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Detect useReducer pattern: const [state, dispatch] = useReducer(...)
|
|
47
|
+
if (id.type === 'ArrayPattern' &&
|
|
48
|
+
id.elements &&
|
|
49
|
+
id.elements.length === 2 &&
|
|
50
|
+
init?.type === 'CallExpression') {
|
|
51
|
+
const callee = init.callee;
|
|
52
|
+
if ((callee.type === 'Identifier' && callee.name === 'useReducer') ||
|
|
53
|
+
(callee.type === 'MemberExpression' && callee.property?.name === 'useReducer')) {
|
|
54
|
+
const stateId = id.elements[0];
|
|
55
|
+
const dispatchIdNode = id.elements[1];
|
|
56
|
+
if (stateId?.type === 'Identifier' && dispatchIdNode?.type === 'Identifier') {
|
|
57
|
+
const stateName = stateId.name;
|
|
58
|
+
const dispatchName = dispatchIdNode.name;
|
|
59
|
+
stateSetterMap.set(dispatchName, stateName);
|
|
60
|
+
stateVariables.push({
|
|
61
|
+
name: stateName,
|
|
62
|
+
setter: dispatchName,
|
|
63
|
+
initialValue: undefined,
|
|
64
|
+
line: varPath.node.loc?.start.line ?? 0,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
// Second pass: find event handler functions and track which setters they call
|
|
72
|
+
componentPath.traverse({
|
|
73
|
+
FunctionDeclaration: (funcPath) => {
|
|
74
|
+
const handlerInfo = this.extractHandlerInfo(funcPath, stateSetterMap);
|
|
75
|
+
if (handlerInfo) {
|
|
76
|
+
handlerInfo.name = funcPath.node.id?.name || handlerInfo.name;
|
|
77
|
+
eventHandlers.push(handlerInfo);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
VariableDeclarator: (varPath) => {
|
|
81
|
+
if ((varPath.node.init?.type === 'ArrowFunctionExpression' ||
|
|
82
|
+
varPath.node.init?.type === 'FunctionExpression') &&
|
|
83
|
+
varPath.node.id?.type === 'Identifier') {
|
|
84
|
+
const handlerName = varPath.node.id.name;
|
|
85
|
+
const funcPath = varPath.get('init');
|
|
86
|
+
const handlerInfo = this.extractHandlerInfo(funcPath, stateSetterMap);
|
|
87
|
+
if (handlerInfo) {
|
|
88
|
+
handlerInfo.name = handlerName;
|
|
89
|
+
eventHandlers.push(handlerInfo);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
// Third pass: annotate which events correspond to each handler reference
|
|
95
|
+
componentPath.traverse({
|
|
96
|
+
JSXAttribute: (attrPath) => {
|
|
97
|
+
const name = attrPath.node.name?.name;
|
|
98
|
+
if (!name || !name.startsWith('on'))
|
|
99
|
+
return;
|
|
100
|
+
const value = attrPath.node.value;
|
|
101
|
+
if (!value || value.type !== 'JSXExpressionContainer')
|
|
102
|
+
return;
|
|
103
|
+
const expression = value.expression;
|
|
104
|
+
if (!expression)
|
|
105
|
+
return;
|
|
106
|
+
const handlerName = expression.type === 'Identifier' ? expression.name : null;
|
|
107
|
+
if (!handlerName)
|
|
108
|
+
return;
|
|
109
|
+
const handler = eventHandlers.find((h) => h.name === handlerName);
|
|
110
|
+
if (!handler)
|
|
111
|
+
return;
|
|
112
|
+
if (!handler.events.includes(name)) {
|
|
113
|
+
handler.events.push(name);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
return {
|
|
118
|
+
componentName,
|
|
119
|
+
stateVariables,
|
|
120
|
+
eventHandlers,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Extract handler name, event references, and called setters from a function.
|
|
125
|
+
*/
|
|
126
|
+
extractHandlerInfo(funcPath, stateSetterMap) {
|
|
127
|
+
const calledSetters = new Set();
|
|
128
|
+
funcPath.traverse({
|
|
129
|
+
CallExpression(callPath) {
|
|
130
|
+
const callee = callPath.node.callee;
|
|
131
|
+
let setterName = null;
|
|
132
|
+
if (callee.type === 'Identifier') {
|
|
133
|
+
setterName = callee.name;
|
|
134
|
+
}
|
|
135
|
+
else if (callee.type === 'MemberExpression' && callee.property?.type === 'Identifier') {
|
|
136
|
+
setterName = callee.property.name;
|
|
137
|
+
}
|
|
138
|
+
if (setterName && stateSetterMap.has(setterName)) {
|
|
139
|
+
calledSetters.add(stateSetterMap.get(setterName));
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
name: funcPath.node.id?.name || 'anonymous',
|
|
145
|
+
events: [],
|
|
146
|
+
line: funcPath.node.loc?.start.line ?? 0,
|
|
147
|
+
callsStateSetters: Array.from(calledSetters),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Map event handlers referenced in JSX attributes to their handler definitions.
|
|
152
|
+
* Returns which state setters a given event attribute calls.
|
|
153
|
+
*/
|
|
154
|
+
getStateSettersCalledByEvent(event, handlers) {
|
|
155
|
+
// Simple heuristic: look for handlers named after the event
|
|
156
|
+
const candidates = handlers.filter((h) => h.name.toLowerCase().includes(event.toLowerCase().replace('on', '')) ||
|
|
157
|
+
h.events.includes(event));
|
|
158
|
+
const allSetters = new Set();
|
|
159
|
+
for (const handler of candidates) {
|
|
160
|
+
for (const setter of handler.callsStateSetters) {
|
|
161
|
+
allSetters.add(setter);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return Array.from(allSetters);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.ComponentContextResolver = ComponentContextResolver;
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type { ModuleGraphSource, DiscoveredRoute, DiscoveredElement } from '../types';
|
|
1
|
+
import type { ModuleGraphSource, DiscoveredRoute, DiscoveredElement, PolicyComponent } from '../types';
|
|
2
2
|
export declare class ComponentExtractor {
|
|
3
3
|
private source;
|
|
4
4
|
private routes;
|
|
5
5
|
constructor(source: ModuleGraphSource, routes: DiscoveredRoute[]);
|
|
6
6
|
extract(): Promise<DiscoveredElement[]>;
|
|
7
|
+
/**
|
|
8
|
+
* Extract component-level analysis including state, handlers, and interaction graphs.
|
|
9
|
+
* Returns PolicyComponent data suitable for policy document generation.
|
|
10
|
+
*/
|
|
11
|
+
extractComponents(): Promise<PolicyComponent[]>;
|
|
7
12
|
private getModuleFilesForRoute;
|
|
8
13
|
private walkWebpackGraph;
|
|
9
14
|
private findWebpackModule;
|
|
@@ -8,6 +8,10 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
9
|
const parser_1 = require("@babel/parser");
|
|
10
10
|
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
11
|
+
const InteractionGraphExtractor_1 = require("./InteractionGraphExtractor");
|
|
12
|
+
const ComponentContextResolver_1 = require("./ComponentContextResolver");
|
|
13
|
+
const HtmlTagGuards_1 = require("../injection/HtmlTagGuards");
|
|
14
|
+
const IdStrategy_1 = require("../injection/IdStrategy");
|
|
11
15
|
class ComponentExtractor {
|
|
12
16
|
constructor(source, routes) {
|
|
13
17
|
this.source = source;
|
|
@@ -26,6 +30,66 @@ class ComponentExtractor {
|
|
|
26
30
|
}
|
|
27
31
|
return elements;
|
|
28
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Extract component-level analysis including state, handlers, and interaction graphs.
|
|
35
|
+
* Returns PolicyComponent data suitable for policy document generation.
|
|
36
|
+
*/
|
|
37
|
+
async extractComponents() {
|
|
38
|
+
const components = [];
|
|
39
|
+
const contextResolver = new ComponentContextResolver_1.ComponentContextResolver();
|
|
40
|
+
const graphExtractor = new InteractionGraphExtractor_1.InteractionGraphExtractor();
|
|
41
|
+
for (const route of this.routes) {
|
|
42
|
+
const moduleFiles = this.getModuleFilesForRoute(route.filePath);
|
|
43
|
+
for (const filePath of moduleFiles) {
|
|
44
|
+
const source = this.readSource(filePath);
|
|
45
|
+
if (!source)
|
|
46
|
+
continue;
|
|
47
|
+
let ast;
|
|
48
|
+
try {
|
|
49
|
+
ast = (0, parser_1.parse)(source, { sourceType: 'module', plugins: ['typescript', 'jsx'] });
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
// Find all component declarations and extract their analysis
|
|
55
|
+
(0, traverse_1.default)(ast, {
|
|
56
|
+
FunctionDeclaration: (path) => {
|
|
57
|
+
const componentName = path.node.id?.name;
|
|
58
|
+
if (componentName && /^[A-Z]/.test(componentName)) {
|
|
59
|
+
const context = contextResolver.extractContext(path, componentName);
|
|
60
|
+
const interactions = graphExtractor.extractInteractions(path, componentName);
|
|
61
|
+
components.push({
|
|
62
|
+
name: componentName,
|
|
63
|
+
filePath,
|
|
64
|
+
route: route.path,
|
|
65
|
+
stateVariables: context.stateVariables,
|
|
66
|
+
interactions,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
VariableDeclarator: (path) => {
|
|
71
|
+
const componentName = path.node.id?.name;
|
|
72
|
+
if (componentName &&
|
|
73
|
+
/^[A-Z]/.test(componentName) &&
|
|
74
|
+
(path.node.init?.type === 'ArrowFunctionExpression' ||
|
|
75
|
+
path.node.init?.type === 'FunctionExpression')) {
|
|
76
|
+
const funcPath = path.get('init');
|
|
77
|
+
const context = contextResolver.extractContext(funcPath, componentName);
|
|
78
|
+
const interactions = graphExtractor.extractInteractions(funcPath, componentName);
|
|
79
|
+
components.push({
|
|
80
|
+
name: componentName,
|
|
81
|
+
filePath,
|
|
82
|
+
route: route.path,
|
|
83
|
+
stateVariables: context.stateVariables,
|
|
84
|
+
interactions,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return components;
|
|
92
|
+
}
|
|
29
93
|
getModuleFilesForRoute(entryFilePath) {
|
|
30
94
|
if (this.source.type === 'webpack') {
|
|
31
95
|
return this.walkWebpackGraph(entryFilePath, this.source.compilation);
|
|
@@ -195,20 +259,32 @@ class ComponentExtractor {
|
|
|
195
259
|
const tagName = nodePath.node.name?.name;
|
|
196
260
|
if (!tagName)
|
|
197
261
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
262
|
+
// Navigation components (Link, NavLink) are included for flow graph building
|
|
263
|
+
// but flagged so the selector manifest skips them
|
|
264
|
+
const isNavigationComponent = (tagName === 'Link' || tagName === 'NavLink' || tagName === 'RouterLink') &&
|
|
265
|
+
nodePath.node.attributes.some((a) => a.name?.name === 'href' || a.name?.name === 'to');
|
|
266
|
+
// Only intrinsic HTML tags get data-clippy-id injected; skip React components
|
|
267
|
+
if (!(0, HtmlTagGuards_1.isIntrinsicTag)(tagName) && !isNavigationComponent)
|
|
268
|
+
return;
|
|
269
|
+
const INTERACTIVE_TAGS = new Set(['button', 'input', 'select', 'textarea', 'a', 'form', 'label']);
|
|
270
|
+
if (!isNavigationComponent && !INTERACTIVE_TAGS.has(tagName))
|
|
201
271
|
return;
|
|
202
272
|
const staticProps = extractStaticProps(nodePath.node.attributes);
|
|
203
273
|
const nearbyText = extractNearbyText(nodePath);
|
|
274
|
+
const label = deriveLabel(staticProps, nearbyText) ?? undefined;
|
|
275
|
+
const componentName = (0, IdStrategy_1.findEnclosingComponentName)(nodePath) ?? undefined;
|
|
204
276
|
elements.push({
|
|
205
277
|
tag: tagName,
|
|
206
278
|
filePath,
|
|
207
279
|
route,
|
|
208
280
|
staticProps,
|
|
209
281
|
nearbyText,
|
|
282
|
+
label,
|
|
283
|
+
component: componentName,
|
|
210
284
|
semanticRole: inferSemanticRole(tagName, staticProps, nearbyText),
|
|
211
285
|
interactions: inferInteractions(tagName, staticProps),
|
|
286
|
+
loc: { line: nodePath.node.loc?.start?.line ?? 0 },
|
|
287
|
+
isNavigationLink: isNavigationComponent,
|
|
212
288
|
});
|
|
213
289
|
},
|
|
214
290
|
});
|
|
@@ -268,18 +344,55 @@ function extractStaticProps(attributes) {
|
|
|
268
344
|
return props;
|
|
269
345
|
}
|
|
270
346
|
function extractNearbyText(nodePath) {
|
|
271
|
-
const texts = [];
|
|
272
347
|
const parent = nodePath.parentPath?.node;
|
|
273
|
-
if (parent?.children)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
348
|
+
if (!parent?.children)
|
|
349
|
+
return [];
|
|
350
|
+
return extractTextFromChildren(parent.children, 0);
|
|
351
|
+
}
|
|
352
|
+
function extractTextFromChildren(children, depth) {
|
|
353
|
+
if (depth > 3)
|
|
354
|
+
return [];
|
|
355
|
+
const texts = [];
|
|
356
|
+
for (const child of children) {
|
|
357
|
+
if (child.type === 'JSXText') {
|
|
358
|
+
const t = child.value.trim();
|
|
359
|
+
if (t)
|
|
360
|
+
texts.push(t);
|
|
361
|
+
}
|
|
362
|
+
else if (child.type === 'JSXExpressionContainer') {
|
|
363
|
+
if (child.expression?.type === 'StringLiteral') {
|
|
364
|
+
texts.push(child.expression.value);
|
|
365
|
+
}
|
|
366
|
+
else if (child.expression?.type === 'TemplateLiteral' &&
|
|
367
|
+
child.expression.quasis?.length === 1) {
|
|
368
|
+
const raw = child.expression.quasis[0].value.raw.trim();
|
|
369
|
+
if (raw)
|
|
370
|
+
texts.push(raw);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else if (child.type === 'JSXElement') {
|
|
374
|
+
const childTag = child.openingElement?.name?.name || '';
|
|
375
|
+
// Skip icon-like leaf components — they contribute no readable text
|
|
376
|
+
if (!/^(Icon|Svg|Loader|Spinner|Arrow|Check|Plus|Minus|Close|X)[A-Z]?/i.test(childTag)) {
|
|
377
|
+
texts.push(...extractTextFromChildren(child.children || [], depth + 1));
|
|
279
378
|
}
|
|
280
379
|
}
|
|
281
380
|
}
|
|
282
|
-
return texts;
|
|
381
|
+
return texts.filter(Boolean);
|
|
382
|
+
}
|
|
383
|
+
function deriveLabel(props, nearbyText) {
|
|
384
|
+
if (props['aria-label'])
|
|
385
|
+
return props['aria-label'];
|
|
386
|
+
if (props['title'])
|
|
387
|
+
return props['title'];
|
|
388
|
+
const joined = nearbyText.join(' ').trim();
|
|
389
|
+
if (joined && joined.length <= 60)
|
|
390
|
+
return joined;
|
|
391
|
+
if (props['placeholder'])
|
|
392
|
+
return props['placeholder'];
|
|
393
|
+
if (props['name'])
|
|
394
|
+
return props['name'];
|
|
395
|
+
return null;
|
|
283
396
|
}
|
|
284
397
|
function inferSemanticRole(tag, props, nearbyText) {
|
|
285
398
|
const label = props['aria-label'] || props['placeholder'] || nearbyText[0] || '';
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import type { DiscoveredRoute, DiscoveredElement, InferredFlow } from '../types';
|
|
1
|
+
import type { DiscoveredRoute, DiscoveredElement, InferredFlow, PolicyComponent } from '../types';
|
|
2
2
|
export declare class FlowInferrer {
|
|
3
3
|
private routes;
|
|
4
4
|
private elements;
|
|
5
|
-
|
|
5
|
+
private components?;
|
|
6
|
+
constructor(routes: DiscoveredRoute[], elements: DiscoveredElement[], components?: PolicyComponent[] | undefined);
|
|
6
7
|
infer(): InferredFlow[];
|
|
7
8
|
private buildEdgeGraph;
|
|
8
9
|
private detectLinearFlows;
|
|
10
|
+
private detectInteractionFlows;
|
|
9
11
|
private buildChain;
|
|
12
|
+
private generateIntentPatterns;
|
|
10
13
|
}
|
|
@@ -1,28 +1,69 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FlowInferrer = void 0;
|
|
4
|
+
const SEMANTIC_INTENT_PATTERNS = {
|
|
5
|
+
'login': ['log in', 'sign in', 'login'],
|
|
6
|
+
'signup': ['sign up', 'create account', 'register', 'get started'],
|
|
7
|
+
'register': ['register', 'sign up', 'create account'],
|
|
8
|
+
'forgot-password': ['forgot password', 'reset password', 'recover account'],
|
|
9
|
+
'reset-password': ['reset my password', 'set new password', 'change password'],
|
|
10
|
+
'verify': ['verify email', 'verify account', 'confirm email'],
|
|
11
|
+
'otp': ['enter code', 'verify code', 'enter otp'],
|
|
12
|
+
'billing': ['upgrade plan', 'manage billing', 'change subscription', 'view billing'],
|
|
13
|
+
'payment-method': ['add payment method', 'update card', 'change payment'],
|
|
14
|
+
'settings': ['settings', 'account settings', 'preferences'],
|
|
15
|
+
'team': ['invite team member', 'manage team', 'add team member'],
|
|
16
|
+
'forms': ['view forms', 'manage forms', 'create form', 'my forms'],
|
|
17
|
+
'builder': ['build form', 'form builder', 'edit form'],
|
|
18
|
+
'submissions': ['view submissions', 'form submissions', 'see responses'],
|
|
19
|
+
'analytics': ['view analytics', 'form analytics', 'see stats'],
|
|
20
|
+
'templates': ['view templates', 'browse templates', 'use template'],
|
|
21
|
+
'studio': ['template studio', 'create template'],
|
|
22
|
+
'transactions': ['view transactions', 'transaction history', 'payment history'],
|
|
23
|
+
'documents': ['view documents', 'open document', 'my documents'],
|
|
24
|
+
'integrations': ['integrations', 'connect integration', 'manage integrations'],
|
|
25
|
+
'developer': ['developer settings', 'api keys', 'developer dashboard'],
|
|
26
|
+
'automations': ['automations', 'manage automations', 'view automations'],
|
|
27
|
+
'campaigns': ['campaigns', 'view campaigns', 'manage campaigns'],
|
|
28
|
+
'website': ['website', 'manage website'],
|
|
29
|
+
'workflows': ['workflows', 'manage workflows'],
|
|
30
|
+
'invite': ['accept invite', 'join team'],
|
|
31
|
+
};
|
|
4
32
|
class FlowInferrer {
|
|
5
|
-
constructor(routes, elements) {
|
|
33
|
+
constructor(routes, elements, components) {
|
|
6
34
|
this.routes = routes;
|
|
7
35
|
this.elements = elements;
|
|
36
|
+
this.components = components;
|
|
8
37
|
}
|
|
9
38
|
infer() {
|
|
10
39
|
const edges = this.buildEdgeGraph();
|
|
11
|
-
|
|
40
|
+
const routeFlows = this.detectLinearFlows(edges);
|
|
41
|
+
const interactionFlows = this.detectInteractionFlows();
|
|
42
|
+
return [...routeFlows, ...interactionFlows];
|
|
12
43
|
}
|
|
13
44
|
buildEdgeGraph() {
|
|
14
45
|
const edges = [];
|
|
15
46
|
const routePaths = new Set(this.routes.map((r) => r.path));
|
|
16
47
|
for (const el of this.elements) {
|
|
48
|
+
// Include both native <a> tags and navigation components (Link, NavLink)
|
|
49
|
+
const isNav = el.tag === 'a' ||
|
|
50
|
+
el.isNavigationLink === true;
|
|
17
51
|
const href = el.staticProps['href'] || el.staticProps['to'];
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
52
|
+
if (!isNav || !href)
|
|
53
|
+
continue;
|
|
54
|
+
// Only build edges to known internal routes
|
|
55
|
+
if (!routePaths.has(href))
|
|
56
|
+
continue;
|
|
57
|
+
const triggerText = el.staticProps['aria-label'] ||
|
|
58
|
+
el.label ||
|
|
59
|
+
el.nearbyText[0] ||
|
|
60
|
+
'link';
|
|
61
|
+
edges.push({
|
|
62
|
+
from: el.route,
|
|
63
|
+
to: href,
|
|
64
|
+
trigger: triggerText,
|
|
65
|
+
confidence: 0.85,
|
|
66
|
+
});
|
|
26
67
|
}
|
|
27
68
|
return edges;
|
|
28
69
|
}
|
|
@@ -40,22 +81,63 @@ class FlowInferrer {
|
|
|
40
81
|
continue;
|
|
41
82
|
const chain = this.buildChain(route.path, adjacency, new Set());
|
|
42
83
|
if (chain.length >= 2) {
|
|
84
|
+
const chainEdges = edges.filter((e) => {
|
|
85
|
+
const s = new Set(chain);
|
|
86
|
+
return s.has(e.from) && s.has(e.to);
|
|
87
|
+
});
|
|
88
|
+
const intentPatterns = this.generateIntentPatterns(chain, chainEdges);
|
|
43
89
|
flows.push({
|
|
44
90
|
id: `flow_${flows.length + 1}`,
|
|
45
91
|
name: chain
|
|
46
92
|
.map((p) => p.split('/').filter(Boolean).pop() || 'home')
|
|
47
93
|
.join(' -> '),
|
|
94
|
+
page: chain[0],
|
|
48
95
|
steps: chain,
|
|
49
|
-
edges:
|
|
50
|
-
|
|
51
|
-
return s.has(e.from) && s.has(e.to);
|
|
52
|
-
}),
|
|
96
|
+
edges: chainEdges,
|
|
97
|
+
intentPatterns,
|
|
53
98
|
});
|
|
54
99
|
chain.forEach((p) => visited.add(p));
|
|
55
100
|
}
|
|
56
101
|
}
|
|
57
102
|
return flows;
|
|
58
103
|
}
|
|
104
|
+
detectInteractionFlows() {
|
|
105
|
+
if (!this.components || this.components.length === 0)
|
|
106
|
+
return [];
|
|
107
|
+
const flows = [];
|
|
108
|
+
for (const component of this.components) {
|
|
109
|
+
for (const interaction of component.interactions || []) {
|
|
110
|
+
// Only emit flows for interactions with a meaningful wait strategy
|
|
111
|
+
if (!interaction.trigger?.event)
|
|
112
|
+
continue;
|
|
113
|
+
if (!interaction.effect || interaction.effect.waitStrategy === 'none')
|
|
114
|
+
continue;
|
|
115
|
+
const routePath = component.route || '/';
|
|
116
|
+
const effectTarget = interaction.effect.selector ||
|
|
117
|
+
(interaction.effect.rendersWhenTrue
|
|
118
|
+
? `[data-clippy-component='${interaction.effect.rendersWhenTrue}']`
|
|
119
|
+
: null);
|
|
120
|
+
if (!effectTarget)
|
|
121
|
+
continue;
|
|
122
|
+
flows.push({
|
|
123
|
+
id: `flow_${flows.length + 1}`,
|
|
124
|
+
name: interaction.effect.rendersWhenTrue || interaction.trigger.event,
|
|
125
|
+
page: routePath,
|
|
126
|
+
steps: [routePath],
|
|
127
|
+
edges: [
|
|
128
|
+
{
|
|
129
|
+
from: routePath,
|
|
130
|
+
to: routePath,
|
|
131
|
+
trigger: interaction.trigger.event,
|
|
132
|
+
confidence: 0.65,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
intentPatterns: [],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return flows;
|
|
140
|
+
}
|
|
59
141
|
buildChain(start, adjacency, seen) {
|
|
60
142
|
if (seen.has(start))
|
|
61
143
|
return [];
|
|
@@ -66,5 +148,26 @@ class FlowInferrer {
|
|
|
66
148
|
const best = [...next].sort((a, b) => b.confidence - a.confidence)[0];
|
|
67
149
|
return [start, ...this.buildChain(best.to, adjacency, seen)];
|
|
68
150
|
}
|
|
151
|
+
generateIntentPatterns(chain, edges) {
|
|
152
|
+
const patterns = new Set();
|
|
153
|
+
// 1. Route-segment semantic lookup
|
|
154
|
+
for (const routePath of chain) {
|
|
155
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
156
|
+
for (const segment of segments) {
|
|
157
|
+
const clean = segment.replace(/[\[\]$:]/g, '').toLowerCase();
|
|
158
|
+
const known = SEMANTIC_INTENT_PATTERNS[clean];
|
|
159
|
+
if (known)
|
|
160
|
+
known.forEach((p) => patterns.add(p));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// 2. Link/button trigger text from the edges themselves
|
|
164
|
+
for (const edge of edges) {
|
|
165
|
+
const trigger = edge.trigger?.trim();
|
|
166
|
+
if (trigger && trigger !== 'link' && trigger.length > 2) {
|
|
167
|
+
patterns.add(trigger.toLowerCase());
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return Array.from(patterns).filter(Boolean);
|
|
171
|
+
}
|
|
69
172
|
}
|
|
70
173
|
exports.FlowInferrer = FlowInferrer;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { NodePath } from '@babel/traverse';
|
|
2
|
+
import type { ComponentInteraction } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Extracts interaction graphs from React component ASTs.
|
|
5
|
+
* Maps event handlers -> state mutations -> conditional renders, navigation, and async effects.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InteractionGraphExtractor {
|
|
8
|
+
private contextResolver;
|
|
9
|
+
/**
|
|
10
|
+
* Extract all interactions (trigger -> effect mappings) from a component.
|
|
11
|
+
*/
|
|
12
|
+
extractInteractions(componentPath: NodePath<any>, componentName: string): ComponentInteraction[];
|
|
13
|
+
private buildInteractionFromSetters;
|
|
14
|
+
/**
|
|
15
|
+
* Detect router.push() / router.replace() / navigate() calls inside a handler.
|
|
16
|
+
* Returns the destination route string if found, otherwise null.
|
|
17
|
+
*/
|
|
18
|
+
private resolveRouterNavigation;
|
|
19
|
+
private findHandlerBody;
|
|
20
|
+
private extractRouterCallFromNode;
|
|
21
|
+
private resolveStateSettersForAttribute;
|
|
22
|
+
private collectSettersFromNode;
|
|
23
|
+
private findConditionalEffect;
|
|
24
|
+
private findConditionalEffectInAncestors;
|
|
25
|
+
private findConditionalEffectInComponent;
|
|
26
|
+
private nodeReferencesState;
|
|
27
|
+
private extractComponentNameFromNode;
|
|
28
|
+
}
|