@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.
Files changed (36) hide show
  1. package/dist/buildId.d.ts +5 -0
  2. package/dist/buildId.js +17 -0
  3. package/dist/extractors/ComponentContextResolver.d.ts +37 -0
  4. package/dist/extractors/ComponentContextResolver.js +167 -0
  5. package/dist/extractors/ComponentExtractor.d.ts +6 -1
  6. package/dist/extractors/ComponentExtractor.js +124 -11
  7. package/dist/extractors/FlowInferrer.d.ts +5 -2
  8. package/dist/extractors/FlowInferrer.js +117 -14
  9. package/dist/extractors/InteractionGraphExtractor.d.ts +28 -0
  10. package/dist/extractors/InteractionGraphExtractor.js +333 -0
  11. package/dist/extractors/SelectorGenerator.d.ts +6 -1
  12. package/dist/extractors/SelectorGenerator.js +28 -3
  13. package/dist/index.d.ts +7 -1
  14. package/dist/index.js +21 -0
  15. package/dist/injection/ClippyIdInjector.d.ts +17 -0
  16. package/dist/injection/ClippyIdInjector.js +164 -0
  17. package/dist/injection/HtmlTagGuards.d.ts +3 -0
  18. package/dist/injection/HtmlTagGuards.js +41 -0
  19. package/dist/injection/IdStrategy.d.ts +36 -0
  20. package/dist/injection/IdStrategy.js +148 -0
  21. package/dist/types.d.ts +108 -1
  22. package/dist/upload/Adapter.d.ts +3 -0
  23. package/dist/upload/Adapter.js +13 -0
  24. package/dist/upload/BackendAdapter.d.ts +30 -0
  25. package/dist/upload/BackendAdapter.js +42 -0
  26. package/dist/upload/BackendUploadAdapter.d.ts +13 -0
  27. package/dist/upload/BackendUploadAdapter.js +51 -0
  28. package/dist/upload/PackageBuilder.d.ts +34 -1
  29. package/dist/upload/PackageBuilder.js +220 -0
  30. package/dist/upload/PackageWriter.d.ts +9 -1
  31. package/dist/upload/PackageWriter.js +26 -0
  32. package/dist/upload/UploadStrategy.d.ts +11 -0
  33. package/dist/upload/UploadStrategy.js +40 -0
  34. package/dist/upload/Uploader.d.ts +6 -1
  35. package/dist/upload/Uploader.js +48 -3
  36. package/package.json +1 -1
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InteractionGraphExtractor = void 0;
4
+ const ComponentContextResolver_1 = require("./ComponentContextResolver");
5
+ const MODAL_STATE_PATTERN = /^(is|show|open|visible)/i;
6
+ /**
7
+ * Extracts interaction graphs from React component ASTs.
8
+ * Maps event handlers -> state mutations -> conditional renders, navigation, and async effects.
9
+ */
10
+ class InteractionGraphExtractor {
11
+ constructor() {
12
+ this.contextResolver = new ComponentContextResolver_1.ComponentContextResolver();
13
+ }
14
+ /**
15
+ * Extract all interactions (trigger -> effect mappings) from a component.
16
+ */
17
+ extractInteractions(componentPath, componentName) {
18
+ const context = this.contextResolver.extractContext(componentPath, componentName);
19
+ const interactions = [];
20
+ componentPath.traverse({
21
+ JSXOpeningElement: (jsxPath) => {
22
+ const attributes = jsxPath.node.attributes || [];
23
+ for (const attr of attributes) {
24
+ if (attr.type !== 'JSXAttribute' ||
25
+ !attr.name?.name?.startsWith('on'))
26
+ continue;
27
+ const eventName = attr.name.name;
28
+ // Path A: handler calls a known state setter → find conditional effect
29
+ const settersCalledByEvent = this.resolveStateSettersForAttribute(attr, context);
30
+ if (settersCalledByEvent.length > 0) {
31
+ const interaction = this.buildInteractionFromSetters(jsxPath, componentPath, eventName, context, settersCalledByEvent);
32
+ if (interaction) {
33
+ interactions.push(interaction);
34
+ continue;
35
+ }
36
+ }
37
+ // Path B: handler contains router.push / router.replace → navigation effect
38
+ const navTarget = this.resolveRouterNavigation(attr, context);
39
+ if (navTarget) {
40
+ const tagName = jsxPath.node.name?.name || 'unknown';
41
+ interactions.push({
42
+ trigger: { event: eventName, element: tagName },
43
+ effect: {
44
+ type: 'asyncEffect',
45
+ waitStrategy: 'domSettle',
46
+ settleMs: 0,
47
+ },
48
+ });
49
+ continue;
50
+ }
51
+ // Path C: handler calls a setter with a modal/open-like name → domSettle fallback
52
+ if (settersCalledByEvent.length > 0) {
53
+ const hasModalSetter = settersCalledByEvent.some((s) => MODAL_STATE_PATTERN.test(s));
54
+ if (hasModalSetter) {
55
+ const tagName = jsxPath.node.name?.name || 'unknown';
56
+ interactions.push({
57
+ trigger: {
58
+ event: eventName,
59
+ element: tagName,
60
+ setsState: settersCalledByEvent[0],
61
+ },
62
+ effect: {
63
+ type: 'conditionalRender',
64
+ waitStrategy: 'domSettle',
65
+ settleMs: 150,
66
+ },
67
+ });
68
+ }
69
+ }
70
+ }
71
+ },
72
+ });
73
+ return interactions;
74
+ }
75
+ buildInteractionFromSetters(jsxPath, componentPath, eventName, context, settersCalledByEvent) {
76
+ const tagName = jsxPath.node.name?.name || 'unknown';
77
+ const stateVar = settersCalledByEvent[0];
78
+ const trigger = {
79
+ event: eventName,
80
+ element: tagName,
81
+ setsState: stateVar,
82
+ };
83
+ // Try to find a conditional render driven by this state variable
84
+ const effect = this.findConditionalEffect(jsxPath, componentPath, stateVar);
85
+ if (effect) {
86
+ return { trigger, effect };
87
+ }
88
+ // Fallback: if setter name looks modal-like, emit a domSettle effect
89
+ if (MODAL_STATE_PATTERN.test(stateVar)) {
90
+ return {
91
+ trigger,
92
+ effect: {
93
+ type: 'conditionalRender',
94
+ waitStrategy: 'domSettle',
95
+ settleMs: 150,
96
+ },
97
+ };
98
+ }
99
+ // Generic async/loading pattern: setter name contains loading, pending, fetching
100
+ if (/loading|pending|fetching|submitting/i.test(stateVar)) {
101
+ return {
102
+ trigger,
103
+ effect: {
104
+ type: 'asyncEffect',
105
+ waitStrategy: 'domSettle',
106
+ settleMs: 300,
107
+ },
108
+ };
109
+ }
110
+ return null;
111
+ }
112
+ /**
113
+ * Detect router.push() / router.replace() / navigate() calls inside a handler.
114
+ * Returns the destination route string if found, otherwise null.
115
+ */
116
+ resolveRouterNavigation(attr, context) {
117
+ if (attr.type !== 'JSXAttribute' || !attr.value || attr.value.type !== 'JSXExpressionContainer') {
118
+ return null;
119
+ }
120
+ const expression = attr.value.expression;
121
+ if (!expression)
122
+ return null;
123
+ const body = expression.type === 'ArrowFunctionExpression' || expression.type === 'FunctionExpression'
124
+ ? expression.body
125
+ : expression.type === 'Identifier'
126
+ ? this.findHandlerBody(expression.name, context)
127
+ : null;
128
+ if (!body)
129
+ return null;
130
+ return this.extractRouterCallFromNode(body);
131
+ }
132
+ findHandlerBody(handlerName, context) {
133
+ const handler = context.eventHandlers.find((h) => h.name === handlerName);
134
+ return handler?._body ?? null;
135
+ }
136
+ extractRouterCallFromNode(node) {
137
+ if (!node || typeof node !== 'object')
138
+ return null;
139
+ if (node.type === 'CallExpression') {
140
+ const callee = node.callee;
141
+ const isRouterCall = (callee?.type === 'MemberExpression' &&
142
+ (callee.object?.name === 'router' || callee.object?.name === 'navigate') &&
143
+ (callee.property?.name === 'push' || callee.property?.name === 'replace')) ||
144
+ (callee?.type === 'Identifier' && callee.name === 'navigate');
145
+ if (isRouterCall && node.arguments?.[0]?.type === 'StringLiteral') {
146
+ return node.arguments[0].value;
147
+ }
148
+ }
149
+ for (const key of Object.keys(node)) {
150
+ const value = node[key];
151
+ if (Array.isArray(value)) {
152
+ for (const child of value) {
153
+ const found = this.extractRouterCallFromNode(child);
154
+ if (found)
155
+ return found;
156
+ }
157
+ }
158
+ else if (value && typeof value === 'object') {
159
+ const found = this.extractRouterCallFromNode(value);
160
+ if (found)
161
+ return found;
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+ resolveStateSettersForAttribute(attr, context) {
167
+ if (attr.type !== 'JSXAttribute' || !attr.value || attr.value.type !== 'JSXExpressionContainer') {
168
+ return [];
169
+ }
170
+ const expression = attr.value.expression;
171
+ if (!expression)
172
+ return [];
173
+ const setterMap = new Map();
174
+ for (const stateVar of context.stateVariables) {
175
+ if (stateVar.setter) {
176
+ setterMap.set(stateVar.setter, stateVar.name);
177
+ }
178
+ }
179
+ if (expression.type === 'Identifier') {
180
+ const handler = context.eventHandlers.find((h) => h.name === expression.name);
181
+ return handler?.callsStateSetters ?? [];
182
+ }
183
+ if (expression.type === 'ArrowFunctionExpression' || expression.type === 'FunctionExpression') {
184
+ const result = new Set();
185
+ this.collectSettersFromNode(expression.body, setterMap, result);
186
+ return Array.from(result);
187
+ }
188
+ return [];
189
+ }
190
+ collectSettersFromNode(node, setterMap, result) {
191
+ if (!node || typeof node !== 'object')
192
+ return;
193
+ if (node.type === 'CallExpression') {
194
+ const callee = node.callee;
195
+ if (callee?.type === 'Identifier') {
196
+ const stateName = setterMap.get(callee.name);
197
+ if (stateName)
198
+ result.add(stateName);
199
+ }
200
+ else if (callee?.type === 'MemberExpression' && callee.property?.type === 'Identifier') {
201
+ const stateName = setterMap.get(callee.property.name);
202
+ if (stateName)
203
+ result.add(stateName);
204
+ }
205
+ }
206
+ for (const key of Object.keys(node)) {
207
+ const value = node[key];
208
+ if (Array.isArray(value)) {
209
+ for (const child of value)
210
+ this.collectSettersFromNode(child, setterMap, result);
211
+ }
212
+ else if (value && typeof value === 'object') {
213
+ this.collectSettersFromNode(value, setterMap, result);
214
+ }
215
+ }
216
+ }
217
+ findConditionalEffect(jsxPath, componentPath, stateVar) {
218
+ return (this.findConditionalEffectInAncestors(jsxPath, stateVar) ||
219
+ this.findConditionalEffectInComponent(componentPath, stateVar));
220
+ }
221
+ findConditionalEffectInAncestors(jsxPath, stateVar) {
222
+ let effectFound = null;
223
+ let current = jsxPath.parentPath;
224
+ while (current && !effectFound) {
225
+ const node = current.node;
226
+ if (node.type === 'LogicalExpression') {
227
+ if (this.nodeReferencesState(node.left, stateVar)) {
228
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.right);
229
+ if (rendersWhenTrue) {
230
+ effectFound = {
231
+ type: 'conditionalRender',
232
+ rendersWhenTrue,
233
+ waitStrategy: 'elementAppears',
234
+ selector: `[data-clippy-component='${rendersWhenTrue}']`,
235
+ };
236
+ }
237
+ }
238
+ }
239
+ if (node.type === 'ConditionalExpression') {
240
+ if (this.nodeReferencesState(node.test, stateVar)) {
241
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.consequent);
242
+ const rendersWhenFalse = this.extractComponentNameFromNode(node.alternate);
243
+ if (rendersWhenTrue || rendersWhenFalse) {
244
+ effectFound = {
245
+ type: 'conditionalRender',
246
+ rendersWhenTrue: rendersWhenTrue ?? undefined,
247
+ rendersWhenFalse: rendersWhenFalse ?? undefined,
248
+ waitStrategy: 'elementAppears',
249
+ selector: rendersWhenTrue
250
+ ? `[data-clippy-component='${rendersWhenTrue}']`
251
+ : undefined,
252
+ };
253
+ }
254
+ }
255
+ }
256
+ current = current.parentPath;
257
+ }
258
+ return effectFound;
259
+ }
260
+ findConditionalEffectInComponent(componentPath, stateVar) {
261
+ let effectFound = null;
262
+ componentPath.traverse({
263
+ LogicalExpression: (path) => {
264
+ if (effectFound)
265
+ return;
266
+ const node = path.node;
267
+ if (this.nodeReferencesState(node.left, stateVar)) {
268
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.right);
269
+ if (rendersWhenTrue) {
270
+ effectFound = {
271
+ type: 'conditionalRender',
272
+ rendersWhenTrue,
273
+ waitStrategy: 'elementAppears',
274
+ selector: `[data-clippy-component='${rendersWhenTrue}']`,
275
+ };
276
+ }
277
+ }
278
+ },
279
+ ConditionalExpression: (path) => {
280
+ if (effectFound)
281
+ return;
282
+ const node = path.node;
283
+ if (this.nodeReferencesState(node.test, stateVar)) {
284
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.consequent);
285
+ const rendersWhenFalse = this.extractComponentNameFromNode(node.alternate);
286
+ if (rendersWhenTrue || rendersWhenFalse) {
287
+ effectFound = {
288
+ type: 'conditionalRender',
289
+ rendersWhenTrue: rendersWhenTrue ?? undefined,
290
+ rendersWhenFalse: rendersWhenFalse ?? undefined,
291
+ waitStrategy: 'elementAppears',
292
+ selector: rendersWhenTrue
293
+ ? `[data-clippy-component='${rendersWhenTrue}']`
294
+ : undefined,
295
+ };
296
+ }
297
+ }
298
+ },
299
+ });
300
+ return effectFound;
301
+ }
302
+ nodeReferencesState(node, stateVar) {
303
+ if (node.type === 'Identifier' && node.name === stateVar)
304
+ return true;
305
+ if (node.type === 'MemberExpression')
306
+ return this.nodeReferencesState(node.object, stateVar);
307
+ if (node.type === 'CallExpression')
308
+ return this.nodeReferencesState(node.callee, stateVar);
309
+ if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
310
+ return (this.nodeReferencesState(node.left, stateVar) ||
311
+ this.nodeReferencesState(node.right, stateVar));
312
+ }
313
+ if (node.type === 'UnaryExpression')
314
+ return this.nodeReferencesState(node.argument, stateVar);
315
+ return false;
316
+ }
317
+ extractComponentNameFromNode(node) {
318
+ if (!node)
319
+ return null;
320
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name) {
321
+ const name = node.openingElement.name.name;
322
+ return /^[A-Z]/.test(name) ? name : null;
323
+ }
324
+ if (node.type === 'JSXFragment')
325
+ return null;
326
+ if (node.type === 'Identifier' && /^[A-Z]/.test(node.name))
327
+ return node.name;
328
+ if (node.type === 'MemberExpression' && node.property?.name)
329
+ return node.property.name;
330
+ return null;
331
+ }
332
+ }
333
+ exports.InteractionGraphExtractor = InteractionGraphExtractor;
@@ -1,5 +1,10 @@
1
1
  import type { DiscoveredElement, ElementWithSelectors } from '../types';
2
2
  export declare class SelectorGenerator {
3
- generate(elements: DiscoveredElement[]): ElementWithSelectors[];
3
+ generate(elements: DiscoveredElement[], injectedMap?: Record<string, Array<{
4
+ clippyId: string;
5
+ component: string;
6
+ tag: string;
7
+ line: number;
8
+ }>>): ElementWithSelectors[];
4
9
  private generateForElement;
5
10
  }
@@ -2,15 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.SelectorGenerator = void 0;
4
4
  class SelectorGenerator {
5
- generate(elements) {
5
+ generate(elements, injectedMap) {
6
6
  return elements.map((el) => ({
7
7
  ...el,
8
- selectors: this.generateForElement(el),
8
+ selectors: this.generateForElement(el, injectedMap),
9
9
  }));
10
10
  }
11
- generateForElement(el) {
11
+ generateForElement(el, injectedMap) {
12
12
  const candidates = [];
13
13
  const p = el.staticProps;
14
+ // Prefer injected clippy ids when available for the element's file
15
+ try {
16
+ if (injectedMap && el.filePath) {
17
+ const entries = injectedMap[el.filePath];
18
+ if (entries && entries.length) {
19
+ // Prefer exact line match when available
20
+ let match = null;
21
+ if (el.loc && el.loc.line) {
22
+ match = entries.find((e) => e.line === el.loc.line);
23
+ }
24
+ // Fallback to tag/component match
25
+ if (!match) {
26
+ match = entries.find((e) => e.tag === el.tag);
27
+ }
28
+ if (match) {
29
+ candidates.push({
30
+ type: 'clippy_id',
31
+ value: `[data-clippy-id="${match.clippyId}"]`,
32
+ confidence: 0.999,
33
+ });
34
+ }
35
+ }
36
+ }
37
+ }
38
+ catch { }
14
39
  if (p['data-testid']) {
15
40
  candidates.push({
16
41
  type: 'testid',
package/dist/index.d.ts CHANGED
@@ -1 +1,7 @@
1
- export type { ClippyPluginOptions, ModuleGraphSource, RollupModuleInfo, DiscoveredRoute, DiscoveredElement, SelectorCandidate, ElementWithSelectors, FlowEdge, InferredFlow, KnowledgePackage, } from './types';
1
+ export * from './types';
2
+ export * from './injection/ClippyIdInjector';
3
+ export * from './injection/IdStrategy';
4
+ export * from './buildId';
5
+ export * from './upload/Adapter';
6
+ export * from './upload/BackendAdapter';
7
+ export * from './upload/BackendUploadAdapter';
package/dist/index.js CHANGED
@@ -1,2 +1,23 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
2
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./injection/ClippyIdInjector"), exports);
19
+ __exportStar(require("./injection/IdStrategy"), exports);
20
+ __exportStar(require("./buildId"), exports);
21
+ __exportStar(require("./upload/Adapter"), exports);
22
+ __exportStar(require("./upload/BackendAdapter"), exports);
23
+ __exportStar(require("./upload/BackendUploadAdapter"), exports);
@@ -0,0 +1,17 @@
1
+ export interface InjectedEntry {
2
+ clippyId: string;
3
+ component: string;
4
+ tag: string;
5
+ line: number;
6
+ label?: string;
7
+ }
8
+ export interface InjectResult {
9
+ source: string;
10
+ injectedCount: number;
11
+ injected: InjectedEntry[];
12
+ }
13
+ /**
14
+ * Inject data-clippy-id and data-clippy-component attributes into intrinsic HTML elements.
15
+ * IDs incorporate the route-derived component name and visible label text for readability.
16
+ */
17
+ export declare function injectClippyIds(source: string, filePath: string, routePath?: string): InjectResult;
@@ -0,0 +1,164 @@
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.injectClippyIds = injectClippyIds;
7
+ const parser_1 = require("@babel/parser");
8
+ const traverse_1 = __importDefault(require("@babel/traverse"));
9
+ const HtmlTagGuards_1 = require("./HtmlTagGuards");
10
+ const IdStrategy_1 = require("./IdStrategy");
11
+ /**
12
+ * Inject data-clippy-id and data-clippy-component attributes into intrinsic HTML elements.
13
+ * IDs incorporate the route-derived component name and visible label text for readability.
14
+ */
15
+ function injectClippyIds(source, filePath, routePath) {
16
+ let ast;
17
+ try {
18
+ ast = (0, parser_1.parse)(source, {
19
+ sourceType: 'module',
20
+ plugins: ['typescript', 'jsx'],
21
+ ranges: true,
22
+ });
23
+ }
24
+ catch {
25
+ return { source, injectedCount: 0, injected: [] };
26
+ }
27
+ const edits = [];
28
+ const injected = [];
29
+ (0, traverse_1.default)(ast, {
30
+ JSXOpeningElement(path) {
31
+ const opening = path.node;
32
+ const tagName = (0, HtmlTagGuards_1.getJSXTagName)(opening.name);
33
+ if (!tagName || !(0, HtmlTagGuards_1.isIntrinsicTag)(tagName))
34
+ return;
35
+ const attrs = opening.attributes;
36
+ if ((0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-id'))
37
+ return;
38
+ // Resolve component name: prefer enclosing name, fall back to route-derived or filename
39
+ const enclosing = (0, IdStrategy_1.findEnclosingComponentName)(path);
40
+ let componentName;
41
+ if (enclosing && !IdStrategy_1.GENERIC_COMPONENT_NAMES.has(enclosing)) {
42
+ componentName = enclosing;
43
+ }
44
+ else if (routePath) {
45
+ componentName = (0, IdStrategy_1.deriveRouteComponentName)(routePath);
46
+ }
47
+ else {
48
+ componentName = (0, IdStrategy_1.deriveComponentName)(filePath);
49
+ }
50
+ const line = opening.loc?.start.line ?? 0;
51
+ // Extract visible label text from element children for ID enrichment
52
+ const labelText = extractLabelFromOpeningElement(path, attrs);
53
+ const clippyId = (0, IdStrategy_1.deriveClippyId)(componentName, tagName, line, routePath, labelText ?? undefined);
54
+ const componentAttr = (0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-component')
55
+ ? ''
56
+ : ` data-clippy-component="${componentName}"`;
57
+ const insertText = ` data-clippy-id="${clippyId}"${componentAttr}`;
58
+ const insertIndex = findAttributeInsertIndex(source, opening.range);
59
+ if (insertIndex === null)
60
+ return;
61
+ edits.push({ index: insertIndex, text: insertText });
62
+ injected.push({
63
+ clippyId,
64
+ component: componentName,
65
+ tag: tagName,
66
+ line,
67
+ label: labelText ?? undefined,
68
+ });
69
+ },
70
+ });
71
+ if (edits.length === 0) {
72
+ return { source, injectedCount: 0, injected: [] };
73
+ }
74
+ const transformed = applyEdits(source, edits);
75
+ return { source: transformed, injectedCount: edits.length, injected };
76
+ }
77
+ /**
78
+ * Extract a usable label from the element's attributes and children.
79
+ * Priority: aria-label > title > visible text content > placeholder > name.
80
+ */
81
+ function extractLabelFromOpeningElement(path, attrs) {
82
+ // Check attributes first — fastest path
83
+ for (const attr of attrs) {
84
+ if (attr.type !== 'JSXAttribute')
85
+ continue;
86
+ const name = attr.name?.name;
87
+ if (name !== 'aria-label' && name !== 'title')
88
+ continue;
89
+ if (attr.value?.type === 'StringLiteral')
90
+ return attr.value.value;
91
+ if (attr.value?.type === 'JSXExpressionContainer' &&
92
+ attr.value.expression?.type === 'StringLiteral')
93
+ return attr.value.expression.value;
94
+ }
95
+ // Extract visible text from children
96
+ const parent = path.parentPath?.node;
97
+ if (parent?.children) {
98
+ const text = extractTextFromChildren(parent.children, 0);
99
+ const joined = text.join(' ').trim();
100
+ if (joined && joined.length <= 60) {
101
+ const sanitized = (0, IdStrategy_1.sanitizeLabelForId)(joined);
102
+ if (sanitized)
103
+ return joined; // return raw joined text; sanitization happens in deriveClippyId
104
+ }
105
+ }
106
+ // Fallback to placeholder / name for inputs
107
+ for (const attr of attrs) {
108
+ if (attr.type !== 'JSXAttribute')
109
+ continue;
110
+ const name = attr.name?.name;
111
+ if (name !== 'placeholder' && name !== 'name')
112
+ continue;
113
+ if (attr.value?.type === 'StringLiteral')
114
+ return attr.value.value;
115
+ }
116
+ return null;
117
+ }
118
+ function extractTextFromChildren(children, depth) {
119
+ if (depth > 3)
120
+ return [];
121
+ const texts = [];
122
+ for (const child of children) {
123
+ if (child.type === 'JSXText') {
124
+ const t = child.value.trim();
125
+ if (t)
126
+ texts.push(t);
127
+ }
128
+ else if (child.type === 'JSXExpressionContainer') {
129
+ if (child.expression?.type === 'StringLiteral') {
130
+ texts.push(child.expression.value);
131
+ }
132
+ else if (child.expression?.type === 'TemplateLiteral' &&
133
+ child.expression.quasis?.length === 1) {
134
+ const raw = child.expression.quasis[0].value.raw.trim();
135
+ if (raw)
136
+ texts.push(raw);
137
+ }
138
+ }
139
+ else if (child.type === 'JSXElement') {
140
+ const childTag = child.openingElement?.name?.name || '';
141
+ if (!/^(Icon|Svg|Loader|Spinner|Arrow|Check|Plus|Minus|Close|X)[A-Z]?/i.test(childTag)) {
142
+ texts.push(...extractTextFromChildren(child.children || [], depth + 1));
143
+ }
144
+ }
145
+ }
146
+ return texts.filter(Boolean);
147
+ }
148
+ function findAttributeInsertIndex(source, range) {
149
+ if (!range)
150
+ return null;
151
+ const end = range[1];
152
+ const closeCharIndex = source.lastIndexOf('>', end - 1);
153
+ if (closeCharIndex === -1)
154
+ return null;
155
+ return closeCharIndex;
156
+ }
157
+ function applyEdits(source, edits) {
158
+ const sorted = edits.slice().sort((a, b) => b.index - a.index);
159
+ let result = source;
160
+ for (const edit of sorted) {
161
+ result = result.slice(0, edit.index) + edit.text + result.slice(edit.index);
162
+ }
163
+ return result;
164
+ }
@@ -0,0 +1,3 @@
1
+ export declare function isIntrinsicTag(tagName: string): boolean;
2
+ export declare function getJSXTagName(name: any): string | null;
3
+ export declare function hasAttribute(attributes: any[], name: string): boolean;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isIntrinsicTag = isIntrinsicTag;
4
+ exports.getJSXTagName = getJSXTagName;
5
+ exports.hasAttribute = hasAttribute;
6
+ const HTML_TAGS = new Set([
7
+ 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo',
8
+ 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup',
9
+ 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed',
10
+ 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
11
+ 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen',
12
+ 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter',
13
+ 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture',
14
+ 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select',
15
+ 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody',
16
+ 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track',
17
+ 'u', 'ul', 'var', 'video', 'wbr',
18
+ ]);
19
+ function isIntrinsicTag(tagName) {
20
+ return HTML_TAGS.has(tagName);
21
+ }
22
+ function getJSXTagName(name) {
23
+ if (!name)
24
+ return null;
25
+ if (name.type === 'JSXIdentifier') {
26
+ return name.name;
27
+ }
28
+ if (name.type === 'JSXMemberExpression') {
29
+ return null;
30
+ }
31
+ if (name.type === 'JSXNamespacedName') {
32
+ return null;
33
+ }
34
+ return null;
35
+ }
36
+ function hasAttribute(attributes, name) {
37
+ return attributes.some((attr) => attr.type === 'JSXAttribute' &&
38
+ attr.name &&
39
+ attr.name.type === 'JSXIdentifier' &&
40
+ attr.name.name === name);
41
+ }