@dcoder-x/plugin-shared 0.1.4 → 0.1.6
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.js +70 -11
- package/dist/extractors/FlowInferrer.d.ts +19 -0
- package/dist/extractors/FlowInferrer.js +156 -18
- package/dist/extractors/InteractionGraphExtractor.d.ts +7 -13
- package/dist/extractors/InteractionGraphExtractor.js +143 -55
- package/dist/extractors/SelectorGenerator.d.ts +2 -6
- package/dist/extractors/SelectorGenerator.js +11 -6
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/injection/ClippyIdInjector.d.ts +6 -1
- package/dist/injection/ClippyIdInjector.js +100 -19
- package/dist/injection/IdStrategy.d.ts +35 -1
- package/dist/injection/IdStrategy.js +112 -2
- package/dist/types.d.ts +7 -1
- package/dist/upload/PackageBuilder.d.ts +13 -1
- package/dist/upload/PackageBuilder.js +146 -38
- package/package.json +1 -1
|
@@ -10,6 +10,8 @@ const parser_1 = require("@babel/parser");
|
|
|
10
10
|
const traverse_1 = __importDefault(require("@babel/traverse"));
|
|
11
11
|
const InteractionGraphExtractor_1 = require("./InteractionGraphExtractor");
|
|
12
12
|
const ComponentContextResolver_1 = require("./ComponentContextResolver");
|
|
13
|
+
const HtmlTagGuards_1 = require("../injection/HtmlTagGuards");
|
|
14
|
+
const IdStrategy_1 = require("../injection/IdStrategy");
|
|
13
15
|
class ComponentExtractor {
|
|
14
16
|
constructor(source, routes) {
|
|
15
17
|
this.source = source;
|
|
@@ -257,21 +259,32 @@ class ComponentExtractor {
|
|
|
257
259
|
const tagName = nodePath.node.name?.name;
|
|
258
260
|
if (!tagName)
|
|
259
261
|
return;
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
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))
|
|
263
271
|
return;
|
|
264
272
|
const staticProps = extractStaticProps(nodePath.node.attributes);
|
|
265
273
|
const nearbyText = extractNearbyText(nodePath);
|
|
274
|
+
const label = deriveLabel(tagName, staticProps, nearbyText) ?? undefined;
|
|
275
|
+
const componentName = (0, IdStrategy_1.findEnclosingComponentName)(nodePath) ?? undefined;
|
|
266
276
|
elements.push({
|
|
267
277
|
tag: tagName,
|
|
268
278
|
filePath,
|
|
269
279
|
route,
|
|
270
280
|
staticProps,
|
|
271
281
|
nearbyText,
|
|
282
|
+
label,
|
|
283
|
+
component: componentName,
|
|
272
284
|
semanticRole: inferSemanticRole(tagName, staticProps, nearbyText),
|
|
273
285
|
interactions: inferInteractions(tagName, staticProps),
|
|
274
286
|
loc: { line: nodePath.node.loc?.start?.line ?? 0 },
|
|
287
|
+
isNavigationLink: isNavigationComponent,
|
|
275
288
|
});
|
|
276
289
|
},
|
|
277
290
|
});
|
|
@@ -331,18 +344,64 @@ function extractStaticProps(attributes) {
|
|
|
331
344
|
return props;
|
|
332
345
|
}
|
|
333
346
|
function extractNearbyText(nodePath) {
|
|
334
|
-
const texts = [];
|
|
335
347
|
const parent = nodePath.parentPath?.node;
|
|
336
|
-
if (parent?.children)
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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));
|
|
342
378
|
}
|
|
343
379
|
}
|
|
344
380
|
}
|
|
345
|
-
return texts;
|
|
381
|
+
return texts.filter(Boolean);
|
|
382
|
+
}
|
|
383
|
+
function deriveLabel(tag, props, nearbyText) {
|
|
384
|
+
// Forms: child text is a concatenation of every field label + button inside the form.
|
|
385
|
+
// That is not a useful description. Use aria-label if present, otherwise take only
|
|
386
|
+
// the last short text node (typically the submit button label).
|
|
387
|
+
if (tag === 'form') {
|
|
388
|
+
if (props['aria-label'])
|
|
389
|
+
return props['aria-label'];
|
|
390
|
+
const lastShort = [...nearbyText].reverse().find((t) => t.length >= 2 && t.length <= 30);
|
|
391
|
+
return lastShort || null;
|
|
392
|
+
}
|
|
393
|
+
if (props['aria-label'])
|
|
394
|
+
return props['aria-label'];
|
|
395
|
+
if (props['title'])
|
|
396
|
+
return props['title'];
|
|
397
|
+
const joined = nearbyText.join(' ').trim();
|
|
398
|
+
if (joined && joined.length <= 60)
|
|
399
|
+
return joined;
|
|
400
|
+
if (props['placeholder'])
|
|
401
|
+
return props['placeholder'];
|
|
402
|
+
if (props['name'])
|
|
403
|
+
return props['name'];
|
|
404
|
+
return null;
|
|
346
405
|
}
|
|
347
406
|
function inferSemanticRole(tag, props, nearbyText) {
|
|
348
407
|
const label = props['aria-label'] || props['placeholder'] || nearbyText[0] || '';
|
|
@@ -7,6 +7,25 @@ export declare class FlowInferrer {
|
|
|
7
7
|
infer(): InferredFlow[];
|
|
8
8
|
private buildEdgeGraph;
|
|
9
9
|
private detectLinearFlows;
|
|
10
|
+
/**
|
|
11
|
+
* Fix 3b: A coherent chain stays within the same top-level route domain.
|
|
12
|
+
* A chain that jumps from /auth/* to / or from /dashboard/* to /auth/signup
|
|
13
|
+
* is an artifact of footer/layout navigation links, not a real user flow.
|
|
14
|
+
*/
|
|
15
|
+
private isCoherentChain;
|
|
10
16
|
private detectInteractionFlows;
|
|
11
17
|
private buildChain;
|
|
18
|
+
private generateIntentPatterns;
|
|
19
|
+
/**
|
|
20
|
+
* Returns true if a string is a usable user intent phrase.
|
|
21
|
+
* Rejects blocklisted words, PascalCase component names, very short strings,
|
|
22
|
+
* and URL-like strings.
|
|
23
|
+
*/
|
|
24
|
+
private isValidIntentPattern;
|
|
25
|
+
/**
|
|
26
|
+
* Fix 4: Remove intent patterns that appear across more than 30% of all flows.
|
|
27
|
+
* These are structural navigation phrases (sidebar, footer links) that ended up
|
|
28
|
+
* attached to many unrelated flows rather than genuine user intents.
|
|
29
|
+
*/
|
|
30
|
+
private deduplicateCrossFlowNoise;
|
|
12
31
|
}
|
|
@@ -1,6 +1,41 @@
|
|
|
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
|
+
};
|
|
32
|
+
// Fix 4: words that are never meaningful user intent phrases
|
|
33
|
+
const INTENT_BLOCKLIST = new Set([
|
|
34
|
+
'back', 'next', 'continue', 'click', 'button', 'here', 'more',
|
|
35
|
+
'loader2', 'loader', 'spinner', 'close', 'x', 'menu', 'open',
|
|
36
|
+
'cancel', 'ok', 'yes', 'no', 'loading', 'view all', 'learn more',
|
|
37
|
+
'read more', 'see more', 'link', 'skip', 'done', 'go',
|
|
38
|
+
]);
|
|
4
39
|
class FlowInferrer {
|
|
5
40
|
constructor(routes, elements, components) {
|
|
6
41
|
this.routes = routes;
|
|
@@ -11,24 +46,39 @@ class FlowInferrer {
|
|
|
11
46
|
const edges = this.buildEdgeGraph();
|
|
12
47
|
const routeFlows = this.detectLinearFlows(edges);
|
|
13
48
|
const interactionFlows = this.detectInteractionFlows();
|
|
14
|
-
|
|
49
|
+
const combined = [...routeFlows, ...interactionFlows];
|
|
50
|
+
// Fix 4: remove patterns that appear in too many flows — they are structural
|
|
51
|
+
// navigation chrome (sidebars, footers) rather than user intent signals
|
|
52
|
+
return this.deduplicateCrossFlowNoise(combined);
|
|
15
53
|
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Edge graph
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
16
57
|
buildEdgeGraph() {
|
|
17
58
|
const edges = [];
|
|
18
59
|
const routePaths = new Set(this.routes.map((r) => r.path));
|
|
19
60
|
for (const el of this.elements) {
|
|
61
|
+
const isNav = el.tag === 'a' || el.isNavigationLink === true;
|
|
20
62
|
const href = el.staticProps['href'] || el.staticProps['to'];
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
63
|
+
if (!isNav || !href)
|
|
64
|
+
continue;
|
|
65
|
+
if (!routePaths.has(href))
|
|
66
|
+
continue;
|
|
67
|
+
const triggerText = el.staticProps['aria-label'] ||
|
|
68
|
+
el.label ||
|
|
69
|
+
el.nearbyText[0] ||
|
|
70
|
+
'link';
|
|
71
|
+
// Fix 3a: demote confidence for elements from layout/footer/header files.
|
|
72
|
+
// These links appear on every page and corrupt flow chains when followed.
|
|
73
|
+
const isLayoutSource = /footer|layout|header|navbar|sidebar|navigation/i.test(el.filePath || '');
|
|
74
|
+
const confidence = isLayoutSource ? 0.3 : 0.85;
|
|
75
|
+
edges.push({ from: el.route, to: href, trigger: triggerText, confidence });
|
|
29
76
|
}
|
|
30
77
|
return edges;
|
|
31
78
|
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Linear flow detection
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
32
82
|
detectLinearFlows(edges) {
|
|
33
83
|
const adjacency = new Map();
|
|
34
84
|
for (const edge of edges) {
|
|
@@ -42,7 +92,11 @@ class FlowInferrer {
|
|
|
42
92
|
if (visited.has(route.path))
|
|
43
93
|
continue;
|
|
44
94
|
const chain = this.buildChain(route.path, adjacency, new Set());
|
|
45
|
-
if (chain.length >= 2) {
|
|
95
|
+
if (chain.length >= 2 && this.isCoherentChain(chain)) {
|
|
96
|
+
const chainEdges = edges.filter((e) => {
|
|
97
|
+
const s = new Set(chain);
|
|
98
|
+
return s.has(e.from) && s.has(e.to);
|
|
99
|
+
});
|
|
46
100
|
flows.push({
|
|
47
101
|
id: `flow_${flows.length + 1}`,
|
|
48
102
|
name: chain
|
|
@@ -50,29 +104,47 @@ class FlowInferrer {
|
|
|
50
104
|
.join(' -> '),
|
|
51
105
|
page: chain[0],
|
|
52
106
|
steps: chain,
|
|
53
|
-
edges:
|
|
54
|
-
|
|
55
|
-
return s.has(e.from) && s.has(e.to);
|
|
56
|
-
}),
|
|
107
|
+
edges: chainEdges,
|
|
108
|
+
intentPatterns: this.generateIntentPatterns(chain, chainEdges),
|
|
57
109
|
});
|
|
58
110
|
chain.forEach((p) => visited.add(p));
|
|
59
111
|
}
|
|
60
112
|
}
|
|
61
113
|
return flows;
|
|
62
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Fix 3b: A coherent chain stays within the same top-level route domain.
|
|
117
|
+
* A chain that jumps from /auth/* to / or from /dashboard/* to /auth/signup
|
|
118
|
+
* is an artifact of footer/layout navigation links, not a real user flow.
|
|
119
|
+
*/
|
|
120
|
+
isCoherentChain(chain) {
|
|
121
|
+
const firstSegment = (p) => p.split('/').filter(Boolean)[0] ?? 'root';
|
|
122
|
+
const domain = firstSegment(chain[0]);
|
|
123
|
+
return chain.every((r) => firstSegment(r) === domain);
|
|
124
|
+
}
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// Interaction flow detection
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
63
128
|
detectInteractionFlows() {
|
|
64
|
-
if (!this.components || this.components.length === 0)
|
|
129
|
+
if (!this.components || this.components.length === 0)
|
|
65
130
|
return [];
|
|
66
|
-
}
|
|
67
131
|
const flows = [];
|
|
68
132
|
for (const component of this.components) {
|
|
69
133
|
for (const interaction of component.interactions || []) {
|
|
70
|
-
const routePath = component.route || '/';
|
|
71
134
|
if (!interaction.trigger?.event)
|
|
72
135
|
continue;
|
|
136
|
+
if (!interaction.effect || interaction.effect.waitStrategy === 'none')
|
|
137
|
+
continue;
|
|
138
|
+
const routePath = component.route || '/';
|
|
139
|
+
const effectTarget = interaction.effect.selector ||
|
|
140
|
+
(interaction.effect.rendersWhenTrue
|
|
141
|
+
? `[data-clippy-component='${interaction.effect.rendersWhenTrue}']`
|
|
142
|
+
: null);
|
|
143
|
+
if (!effectTarget)
|
|
144
|
+
continue;
|
|
73
145
|
flows.push({
|
|
74
146
|
id: `flow_${flows.length + 1}`,
|
|
75
|
-
name:
|
|
147
|
+
name: interaction.effect.rendersWhenTrue || interaction.trigger.event,
|
|
76
148
|
page: routePath,
|
|
77
149
|
steps: [routePath],
|
|
78
150
|
edges: [
|
|
@@ -83,11 +155,15 @@ class FlowInferrer {
|
|
|
83
155
|
confidence: 0.65,
|
|
84
156
|
},
|
|
85
157
|
],
|
|
158
|
+
intentPatterns: [],
|
|
86
159
|
});
|
|
87
160
|
}
|
|
88
161
|
}
|
|
89
162
|
return flows;
|
|
90
163
|
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Chain builder
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
91
167
|
buildChain(start, adjacency, seen) {
|
|
92
168
|
if (seen.has(start))
|
|
93
169
|
return [];
|
|
@@ -98,5 +174,67 @@ class FlowInferrer {
|
|
|
98
174
|
const best = [...next].sort((a, b) => b.confidence - a.confidence)[0];
|
|
99
175
|
return [start, ...this.buildChain(best.to, adjacency, seen)];
|
|
100
176
|
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Intent pattern generation (Fix 4)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
generateIntentPatterns(chain, edges) {
|
|
181
|
+
const patterns = new Set();
|
|
182
|
+
// 1. Route-segment semantic lookup
|
|
183
|
+
for (const routePath of chain) {
|
|
184
|
+
const segments = routePath.split('/').filter(Boolean);
|
|
185
|
+
for (const segment of segments) {
|
|
186
|
+
const clean = segment.replace(/[\[\]$:]/g, '').toLowerCase();
|
|
187
|
+
const known = SEMANTIC_INTENT_PATTERNS[clean];
|
|
188
|
+
if (known)
|
|
189
|
+
known.forEach((p) => patterns.add(p));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 2. Link/button trigger text from the edges — apply noise filter
|
|
193
|
+
for (const edge of edges) {
|
|
194
|
+
const trigger = edge.trigger?.trim();
|
|
195
|
+
if (trigger && this.isValidIntentPattern(trigger)) {
|
|
196
|
+
patterns.add(trigger.toLowerCase());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return Array.from(patterns).filter(Boolean);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Returns true if a string is a usable user intent phrase.
|
|
203
|
+
* Rejects blocklisted words, PascalCase component names, very short strings,
|
|
204
|
+
* and URL-like strings.
|
|
205
|
+
*/
|
|
206
|
+
isValidIntentPattern(pattern) {
|
|
207
|
+
if (!pattern || pattern.length < 4)
|
|
208
|
+
return false;
|
|
209
|
+
const lower = pattern.toLowerCase();
|
|
210
|
+
if (INTENT_BLOCKLIST.has(lower))
|
|
211
|
+
return false;
|
|
212
|
+
// Reject PascalCase strings — these are component names leaking through
|
|
213
|
+
if (/^[A-Z][a-zA-Z0-9]+$/.test(pattern))
|
|
214
|
+
return false;
|
|
215
|
+
// Reject paths and URLs
|
|
216
|
+
if (pattern.startsWith('/') || pattern.startsWith('http'))
|
|
217
|
+
return false;
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Fix 4: Remove intent patterns that appear across more than 30% of all flows.
|
|
222
|
+
* These are structural navigation phrases (sidebar, footer links) that ended up
|
|
223
|
+
* attached to many unrelated flows rather than genuine user intents.
|
|
224
|
+
*/
|
|
225
|
+
deduplicateCrossFlowNoise(flows) {
|
|
226
|
+
if (flows.length === 0)
|
|
227
|
+
return flows;
|
|
228
|
+
const threshold = Math.max(3, Math.ceil(flows.length * 0.3));
|
|
229
|
+
const counts = new Map();
|
|
230
|
+
flows.forEach((f) => (f.intentPatterns ?? []).forEach((p) => counts.set(p, (counts.get(p) ?? 0) + 1)));
|
|
231
|
+
const globalNoise = new Set([...counts.entries()].filter(([, n]) => n > threshold).map(([p]) => p));
|
|
232
|
+
if (globalNoise.size === 0)
|
|
233
|
+
return flows;
|
|
234
|
+
return flows.map((f) => ({
|
|
235
|
+
...f,
|
|
236
|
+
intentPatterns: (f.intentPatterns ?? []).filter((p) => !globalNoise.has(p)),
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
101
239
|
}
|
|
102
240
|
exports.FlowInferrer = FlowInferrer;
|
|
@@ -2,7 +2,7 @@ import type { NodePath } from '@babel/traverse';
|
|
|
2
2
|
import type { ComponentInteraction } from '../types';
|
|
3
3
|
/**
|
|
4
4
|
* Extracts interaction graphs from React component ASTs.
|
|
5
|
-
* Maps event handlers -> state mutations -> conditional renders and effects.
|
|
5
|
+
* Maps event handlers -> state mutations -> conditional renders, navigation, and async effects.
|
|
6
6
|
*/
|
|
7
7
|
export declare class InteractionGraphExtractor {
|
|
8
8
|
private contextResolver;
|
|
@@ -10,25 +10,19 @@ export declare class InteractionGraphExtractor {
|
|
|
10
10
|
* Extract all interactions (trigger -> effect mappings) from a component.
|
|
11
11
|
*/
|
|
12
12
|
extractInteractions(componentPath: NodePath<any>, componentName: string): ComponentInteraction[];
|
|
13
|
+
private buildInteractionFromSetters;
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
+
* Detect router.push() / router.replace() / navigate() calls inside a handler.
|
|
16
|
+
* Returns the destination route string if found, otherwise null.
|
|
15
17
|
*/
|
|
16
|
-
private
|
|
18
|
+
private resolveRouterNavigation;
|
|
19
|
+
private findHandlerBody;
|
|
20
|
+
private extractRouterCallFromNode;
|
|
17
21
|
private resolveStateSettersForAttribute;
|
|
18
22
|
private collectSettersFromNode;
|
|
19
|
-
/**
|
|
20
|
-
* Find conditional renders/effects that depend on a state variable.
|
|
21
|
-
* Looks for logical expressions and ternaries using the state var.
|
|
22
|
-
*/
|
|
23
23
|
private findConditionalEffect;
|
|
24
24
|
private findConditionalEffectInAncestors;
|
|
25
25
|
private findConditionalEffectInComponent;
|
|
26
|
-
/**
|
|
27
|
-
* Check if a node references a specific state variable by name.
|
|
28
|
-
*/
|
|
29
26
|
private nodeReferencesState;
|
|
30
|
-
/**
|
|
31
|
-
* Extract component name from a JSX element or identifier.
|
|
32
|
-
*/
|
|
33
27
|
private extractComponentNameFromNode;
|
|
34
28
|
}
|