@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.
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InteractionGraphExtractor = void 0;
4
4
  const ComponentContextResolver_1 = require("./ComponentContextResolver");
5
+ const MODAL_STATE_PATTERN = /^(is|show|open|visible)/i;
5
6
  /**
6
7
  * Extracts interaction graphs from React component ASTs.
7
- * Maps event handlers -> state mutations -> conditional renders and effects.
8
+ * Maps event handlers -> state mutations -> conditional renders, navigation, and async effects.
8
9
  */
9
10
  class InteractionGraphExtractor {
10
11
  constructor() {
@@ -16,21 +17,54 @@ class InteractionGraphExtractor {
16
17
  extractInteractions(componentPath, componentName) {
17
18
  const context = this.contextResolver.extractContext(componentPath, componentName);
18
19
  const interactions = [];
19
- // Walk all JSX elements looking for event handlers
20
20
  componentPath.traverse({
21
21
  JSXOpeningElement: (jsxPath) => {
22
22
  const attributes = jsxPath.node.attributes || [];
23
23
  for (const attr of attributes) {
24
- if (attr.type === 'JSXAttribute' &&
25
- attr.name?.name &&
26
- attr.name.name.startsWith('on')) {
27
- const eventName = attr.name.name;
28
- const settersCalledByEvent = this.resolveStateSettersForAttribute(attr, context);
29
- if (settersCalledByEvent.length === 0)
30
- continue;
31
- const interaction = this.extractTriggerAndEffect(jsxPath, componentPath, eventName, context, settersCalledByEvent);
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
32
  if (interaction) {
33
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
+ });
34
68
  }
35
69
  }
36
70
  }
@@ -38,23 +72,96 @@ class InteractionGraphExtractor {
38
72
  });
39
73
  return interactions;
40
74
  }
41
- /**
42
- * Extract trigger/effect pair from a JSX element with an event handler.
43
- */
44
- extractTriggerAndEffect(jsxPath, componentPath, eventName, context, settersCalledByEvent) {
75
+ buildInteractionFromSetters(jsxPath, componentPath, eventName, context, settersCalledByEvent) {
45
76
  const tagName = jsxPath.node.name?.name || 'unknown';
46
- const stateVar = settersCalledByEvent[0]; // Take first setter as primary
77
+ const stateVar = settersCalledByEvent[0];
47
78
  const trigger = {
48
79
  event: eventName,
49
80
  element: tagName,
50
81
  setsState: stateVar,
51
82
  };
52
- // Look for conditional effects that depend on the modified state
83
+ // Try to find a conditional render driven by this state variable
53
84
  const effect = this.findConditionalEffect(jsxPath, componentPath, stateVar);
54
- if (!effect) {
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') {
55
118
  return null;
56
119
  }
57
- return { trigger, effect };
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;
58
165
  }
59
166
  resolveStateSettersForAttribute(attr, context) {
60
167
  if (attr.type !== 'JSXAttribute' || !attr.value || attr.value.type !== 'JSXExpressionContainer') {
@@ -99,32 +206,23 @@ class InteractionGraphExtractor {
99
206
  for (const key of Object.keys(node)) {
100
207
  const value = node[key];
101
208
  if (Array.isArray(value)) {
102
- for (const child of value) {
209
+ for (const child of value)
103
210
  this.collectSettersFromNode(child, setterMap, result);
104
- }
105
211
  }
106
212
  else if (value && typeof value === 'object') {
107
213
  this.collectSettersFromNode(value, setterMap, result);
108
214
  }
109
215
  }
110
216
  }
111
- /**
112
- * Find conditional renders/effects that depend on a state variable.
113
- * Looks for logical expressions and ternaries using the state var.
114
- */
115
217
  findConditionalEffect(jsxPath, componentPath, stateVar) {
116
- const directEffect = this.findConditionalEffectInAncestors(jsxPath, stateVar);
117
- if (directEffect) {
118
- return directEffect;
119
- }
120
- return this.findConditionalEffectInComponent(componentPath, stateVar);
218
+ return (this.findConditionalEffectInAncestors(jsxPath, stateVar) ||
219
+ this.findConditionalEffectInComponent(componentPath, stateVar));
121
220
  }
122
221
  findConditionalEffectInAncestors(jsxPath, stateVar) {
123
222
  let effectFound = null;
124
223
  let current = jsxPath.parentPath;
125
224
  while (current && !effectFound) {
126
225
  const node = current.node;
127
- // Check for logical AND/OR: condition && <Component />
128
226
  if (node.type === 'LogicalExpression') {
129
227
  if (this.nodeReferencesState(node.left, stateVar)) {
130
228
  const rendersWhenTrue = this.extractComponentNameFromNode(node.right);
@@ -138,7 +236,6 @@ class InteractionGraphExtractor {
138
236
  }
139
237
  }
140
238
  }
141
- // Check for ternary: condition ? <Component /> : null
142
239
  if (node.type === 'ConditionalExpression') {
143
240
  if (this.nodeReferencesState(node.test, stateVar)) {
144
241
  const rendersWhenTrue = this.extractComponentNameFromNode(node.consequent);
@@ -149,7 +246,9 @@ class InteractionGraphExtractor {
149
246
  rendersWhenTrue: rendersWhenTrue ?? undefined,
150
247
  rendersWhenFalse: rendersWhenFalse ?? undefined,
151
248
  waitStrategy: 'elementAppears',
152
- selector: rendersWhenTrue ? `[data-clippy-component='${rendersWhenTrue}']` : undefined,
249
+ selector: rendersWhenTrue
250
+ ? `[data-clippy-component='${rendersWhenTrue}']`
251
+ : undefined,
153
252
  };
154
253
  }
155
254
  }
@@ -190,7 +289,9 @@ class InteractionGraphExtractor {
190
289
  rendersWhenTrue: rendersWhenTrue ?? undefined,
191
290
  rendersWhenFalse: rendersWhenFalse ?? undefined,
192
291
  waitStrategy: 'elementAppears',
193
- selector: rendersWhenTrue ? `[data-clippy-component='${rendersWhenTrue}']` : undefined,
292
+ selector: rendersWhenTrue
293
+ ? `[data-clippy-component='${rendersWhenTrue}']`
294
+ : undefined,
194
295
  };
195
296
  }
196
297
  }
@@ -198,47 +299,34 @@ class InteractionGraphExtractor {
198
299
  });
199
300
  return effectFound;
200
301
  }
201
- /**
202
- * Check if a node references a specific state variable by name.
203
- */
204
302
  nodeReferencesState(node, stateVar) {
205
- if (node.type === 'Identifier' && node.name === stateVar) {
303
+ if (node.type === 'Identifier' && node.name === stateVar)
206
304
  return true;
207
- }
208
- if (node.type === 'MemberExpression') {
305
+ if (node.type === 'MemberExpression')
209
306
  return this.nodeReferencesState(node.object, stateVar);
210
- }
211
- if (node.type === 'CallExpression') {
307
+ if (node.type === 'CallExpression')
212
308
  return this.nodeReferencesState(node.callee, stateVar);
213
- }
214
309
  if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
215
- return this.nodeReferencesState(node.left, stateVar) || this.nodeReferencesState(node.right, stateVar);
310
+ return (this.nodeReferencesState(node.left, stateVar) ||
311
+ this.nodeReferencesState(node.right, stateVar));
216
312
  }
217
- if (node.type === 'UnaryExpression') {
313
+ if (node.type === 'UnaryExpression')
218
314
  return this.nodeReferencesState(node.argument, stateVar);
219
- }
220
315
  return false;
221
316
  }
222
- /**
223
- * Extract component name from a JSX element or identifier.
224
- */
225
317
  extractComponentNameFromNode(node) {
226
318
  if (!node)
227
319
  return null;
228
320
  if (node.type === 'JSXElement' && node.openingElement?.name?.name) {
229
321
  const name = node.openingElement.name.name;
230
- // Only return if it looks like a component (PascalCase)
231
322
  return /^[A-Z]/.test(name) ? name : null;
232
323
  }
233
- if (node.type === 'JSXFragment') {
324
+ if (node.type === 'JSXFragment')
234
325
  return null;
235
- }
236
- if (node.type === 'Identifier' && /^[A-Z]/.test(node.name)) {
326
+ if (node.type === 'Identifier' && /^[A-Z]/.test(node.name))
237
327
  return node.name;
238
- }
239
- if (node.type === 'MemberExpression' && node.property?.name) {
328
+ if (node.type === 'MemberExpression' && node.property?.name)
240
329
  return node.property.name;
241
- }
242
330
  return null;
243
331
  }
244
332
  }
@@ -1,10 +1,6 @@
1
1
  import type { DiscoveredElement, ElementWithSelectors } from '../types';
2
+ import type { InjectedEntry } from '../injection/ClippyIdInjector';
2
3
  export declare class SelectorGenerator {
3
- generate(elements: DiscoveredElement[], injectedMap?: Record<string, Array<{
4
- clippyId: string;
5
- component: string;
6
- tag: string;
7
- line: number;
8
- }>>): ElementWithSelectors[];
4
+ generate(elements: DiscoveredElement[], injectedMap?: Record<string, InjectedEntry[]>): ElementWithSelectors[];
9
5
  private generateForElement;
10
6
  }
@@ -16,14 +16,19 @@ class SelectorGenerator {
16
16
  if (injectedMap && el.filePath) {
17
17
  const entries = injectedMap[el.filePath];
18
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);
19
+ const elLine = el.loc?.line;
20
+ const elLabel = el.label;
21
+ // 1. Exact line match — most reliable
22
+ let match = elLine ? entries.find((e) => e.line === elLine) : null;
23
+ // 2. Label + tag match — handles multiple same-tag elements in one file
24
+ // (e.g. two <button> tags where line lookup fails due to AST offset)
25
+ if (!match && elLabel) {
26
+ match = entries.find((e) => e.tag === el.tag && e.label === elLabel) ?? null;
23
27
  }
24
- // Fallback to tag/component match
28
+ // 3. Tag-only fallback — last resort, may pick the wrong element when
29
+ // multiple same-tag elements exist but is better than no clippy_id at all
25
30
  if (!match) {
26
- match = entries.find((e) => e.tag === el.tag);
31
+ match = entries.find((e) => e.tag === el.tag) ?? null;
27
32
  }
28
33
  if (match) {
29
34
  candidates.push({
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './injection/ClippyIdInjector';
3
+ export * from './injection/IdStrategy';
3
4
  export * from './buildId';
4
5
  export * from './upload/Adapter';
5
6
  export * from './upload/BackendAdapter';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./types"), exports);
18
18
  __exportStar(require("./injection/ClippyIdInjector"), exports);
19
+ __exportStar(require("./injection/IdStrategy"), exports);
19
20
  __exportStar(require("./buildId"), exports);
20
21
  __exportStar(require("./upload/Adapter"), exports);
21
22
  __exportStar(require("./upload/BackendAdapter"), exports);
@@ -3,10 +3,15 @@ export interface InjectedEntry {
3
3
  component: string;
4
4
  tag: string;
5
5
  line: number;
6
+ label?: string;
6
7
  }
7
8
  export interface InjectResult {
8
9
  source: string;
9
10
  injectedCount: number;
10
11
  injected: InjectedEntry[];
11
12
  }
12
- export declare function injectClippyIds(source: string, filePath: string): InjectResult;
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;
@@ -8,7 +8,11 @@ const parser_1 = require("@babel/parser");
8
8
  const traverse_1 = __importDefault(require("@babel/traverse"));
9
9
  const HtmlTagGuards_1 = require("./HtmlTagGuards");
10
10
  const IdStrategy_1 = require("./IdStrategy");
11
- function injectClippyIds(source, filePath) {
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) {
12
16
  let ast;
13
17
  try {
14
18
  ast = (0, parser_1.parse)(source, {
@@ -31,9 +35,22 @@ function injectClippyIds(source, filePath) {
31
35
  const attrs = opening.attributes;
32
36
  if ((0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-id'))
33
37
  return;
34
- const componentName = findEnclosingComponentName(path) || (0, IdStrategy_1.deriveComponentName)(filePath);
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
+ }
35
50
  const line = opening.loc?.start.line ?? 0;
36
- const clippyId = (0, IdStrategy_1.deriveClippyId)(componentName, tagName, line);
51
+ // Extract visible label text from element children for ID enrichment
52
+ const labelText = extractLabelFromOpeningElement(path, attrs, tagName);
53
+ const clippyId = (0, IdStrategy_1.deriveClippyId)(componentName, tagName, line, routePath, labelText ?? undefined);
37
54
  const componentAttr = (0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-component')
38
55
  ? ''
39
56
  : ` data-clippy-component="${componentName}"`;
@@ -42,7 +59,13 @@ function injectClippyIds(source, filePath) {
42
59
  if (insertIndex === null)
43
60
  return;
44
61
  edits.push({ index: insertIndex, text: insertText });
45
- injected.push({ clippyId, component: componentName, tag: tagName, line });
62
+ injected.push({
63
+ clippyId,
64
+ component: componentName,
65
+ tag: tagName,
66
+ line,
67
+ label: labelText ?? undefined,
68
+ });
46
69
  },
47
70
  });
48
71
  if (edits.length === 0) {
@@ -51,26 +74,84 @@ function injectClippyIds(source, filePath) {
51
74
  const transformed = applyEdits(source, edits);
52
75
  return { source: transformed, injectedCount: edits.length, injected };
53
76
  }
54
- function findEnclosingComponentName(path) {
55
- let current = path;
56
- while (current) {
57
- if (current.isFunctionDeclaration() && current.node.id?.name) {
58
- return current.node.id.name;
77
+ /**
78
+ * Extract a usable label from the element's attributes and children.
79
+ * Priority: aria-label > title > visible text content > placeholder > name.
80
+ * For form elements: only aria-label or the last short text (submit button),
81
+ * never the full concatenation of all child field labels.
82
+ */
83
+ function extractLabelFromOpeningElement(path, attrs, tagName) {
84
+ // Check attributes first — fastest path
85
+ for (const attr of attrs) {
86
+ if (attr.type !== 'JSXAttribute')
87
+ continue;
88
+ const name = attr.name?.name;
89
+ if (name !== 'aria-label' && name !== 'title')
90
+ continue;
91
+ if (attr.value?.type === 'StringLiteral')
92
+ return attr.value.value;
93
+ if (attr.value?.type === 'JSXExpressionContainer' &&
94
+ attr.value.expression?.type === 'StringLiteral')
95
+ return attr.value.expression.value;
96
+ }
97
+ // Extract visible text from children
98
+ const parent = path.parentPath?.node;
99
+ if (parent?.children) {
100
+ const text = extractTextFromChildren(parent.children, 0);
101
+ if (tagName === 'form') {
102
+ // Forms: only use the last short text node (submit button label), not the full
103
+ // concatenation of every field label inside the form.
104
+ const lastShort = [...text].reverse().find((t) => t.length >= 2 && t.length <= 30);
105
+ return lastShort || null;
59
106
  }
60
- if (current.isClassDeclaration() && current.node.id?.name) {
61
- return current.node.id.name;
107
+ const joined = text.join(' ').trim();
108
+ if (joined && joined.length <= 60) {
109
+ const sanitized = (0, IdStrategy_1.sanitizeLabelForId)(joined);
110
+ if (sanitized)
111
+ return joined;
62
112
  }
63
- if (current.isVariableDeclarator() && current.node.id?.type === 'Identifier') {
64
- return current.node.id.name;
113
+ }
114
+ // Fallback to placeholder / name for inputs
115
+ for (const attr of attrs) {
116
+ if (attr.type !== 'JSXAttribute')
117
+ continue;
118
+ const name = attr.name?.name;
119
+ if (name !== 'placeholder' && name !== 'name')
120
+ continue;
121
+ if (attr.value?.type === 'StringLiteral')
122
+ return attr.value.value;
123
+ }
124
+ return null;
125
+ }
126
+ function extractTextFromChildren(children, depth) {
127
+ if (depth > 3)
128
+ return [];
129
+ const texts = [];
130
+ for (const child of children) {
131
+ if (child.type === 'JSXText') {
132
+ const t = child.value.trim();
133
+ if (t)
134
+ texts.push(t);
135
+ }
136
+ else if (child.type === 'JSXExpressionContainer') {
137
+ if (child.expression?.type === 'StringLiteral') {
138
+ texts.push(child.expression.value);
139
+ }
140
+ else if (child.expression?.type === 'TemplateLiteral' &&
141
+ child.expression.quasis?.length === 1) {
142
+ const raw = child.expression.quasis[0].value.raw.trim();
143
+ if (raw)
144
+ texts.push(raw);
145
+ }
65
146
  }
66
- if ((current.isArrowFunctionExpression() || current.isFunctionExpression()) &&
67
- current.parentPath?.isVariableDeclarator() &&
68
- current.parentPath.node.id?.type === 'Identifier') {
69
- return current.parentPath.node.id.name;
147
+ else if (child.type === 'JSXElement') {
148
+ const childTag = child.openingElement?.name?.name || '';
149
+ if (!/^(Icon|Svg|Loader|Spinner|Arrow|Check|Plus|Minus|Close|X)[A-Z]?/i.test(childTag)) {
150
+ texts.push(...extractTextFromChildren(child.children || [], depth + 1));
151
+ }
70
152
  }
71
- current = current.parentPath;
72
153
  }
73
- return null;
154
+ return texts.filter(Boolean);
74
155
  }
75
156
  function findAttributeInsertIndex(source, range) {
76
157
  if (!range)
@@ -1,2 +1,36 @@
1
+ import type { NodePath } from '@babel/traverse';
1
2
  export declare function deriveComponentName(filePath: string, fallback?: string): string;
2
- export declare function deriveClippyId(componentName: string, tagName: string, lineNumber: number): string;
3
+ /**
4
+ * Generic Next.js component names that should be replaced by a route-derived name.
5
+ */
6
+ export declare const GENERIC_COMPONENT_NAMES: Set<string>;
7
+ /**
8
+ * Infer the route path from an absolute file path.
9
+ * Handles both App Router (app/…/page.tsx) and Pages Router (pages/….tsx).
10
+ * Returns null if the file is not a route file.
11
+ */
12
+ export declare function inferRouteFromFilePath(filePath: string): string | null;
13
+ /**
14
+ * Convert a route path to a PascalCase component name.
15
+ * /admin/transactions → AdminTransactions
16
+ * /dashboard/forms/[id] → DashboardForms
17
+ * /auth/forgot-password → AuthForgotPassword
18
+ */
19
+ export declare function deriveRouteComponentName(routePath: string): string;
20
+ /**
21
+ * Compute a short route hash for use in stable IDs.
22
+ * Takes a route path (e.g., "/admin/transactions") and produces a 4-char hash.
23
+ */
24
+ export declare function hashRoutePath(routePath: string): string;
25
+ /**
26
+ * Sanitize a label string for embedding in a CSS/HTML attribute ID.
27
+ * TitleCase, max 2 words, alphanumeric only, minimum 2 chars.
28
+ * Returns null if no usable text can be extracted.
29
+ */
30
+ export declare function sanitizeLabelForId(label: string): string | null;
31
+ export declare function deriveClippyId(componentName: string, tagName: string, lineNumber: number, routePath?: string, labelText?: string): string;
32
+ /**
33
+ * Walk the AST upward to find the enclosing React component name.
34
+ * Exported here so both the injector and the extractor use the same logic.
35
+ */
36
+ export declare function findEnclosingComponentName(path: NodePath<any>): string | null;