@dcoder-x/plugin-shared 0.1.3 → 0.1.4

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 +63 -0
  7. package/dist/extractors/FlowInferrer.d.ts +4 -2
  8. package/dist/extractors/FlowInferrer.js +34 -2
  9. package/dist/extractors/InteractionGraphExtractor.d.ts +34 -0
  10. package/dist/extractors/InteractionGraphExtractor.js +245 -0
  11. package/dist/extractors/SelectorGenerator.d.ts +6 -1
  12. package/dist/extractors/SelectorGenerator.js +28 -3
  13. package/dist/index.d.ts +6 -1
  14. package/dist/index.js +20 -0
  15. package/dist/injection/ClippyIdInjector.d.ts +12 -0
  16. package/dist/injection/ClippyIdInjector.js +91 -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 +2 -0
  20. package/dist/injection/IdStrategy.js +38 -0
  21. package/dist/types.d.ts +102 -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 +32 -1
  29. package/dist/upload/PackageBuilder.js +143 -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,245 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InteractionGraphExtractor = void 0;
4
+ const ComponentContextResolver_1 = require("./ComponentContextResolver");
5
+ /**
6
+ * Extracts interaction graphs from React component ASTs.
7
+ * Maps event handlers -> state mutations -> conditional renders and effects.
8
+ */
9
+ class InteractionGraphExtractor {
10
+ constructor() {
11
+ this.contextResolver = new ComponentContextResolver_1.ComponentContextResolver();
12
+ }
13
+ /**
14
+ * Extract all interactions (trigger -> effect mappings) from a component.
15
+ */
16
+ extractInteractions(componentPath, componentName) {
17
+ const context = this.contextResolver.extractContext(componentPath, componentName);
18
+ const interactions = [];
19
+ // Walk all JSX elements looking for event handlers
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 &&
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);
32
+ if (interaction) {
33
+ interactions.push(interaction);
34
+ }
35
+ }
36
+ }
37
+ },
38
+ });
39
+ return interactions;
40
+ }
41
+ /**
42
+ * Extract trigger/effect pair from a JSX element with an event handler.
43
+ */
44
+ extractTriggerAndEffect(jsxPath, componentPath, eventName, context, settersCalledByEvent) {
45
+ const tagName = jsxPath.node.name?.name || 'unknown';
46
+ const stateVar = settersCalledByEvent[0]; // Take first setter as primary
47
+ const trigger = {
48
+ event: eventName,
49
+ element: tagName,
50
+ setsState: stateVar,
51
+ };
52
+ // Look for conditional effects that depend on the modified state
53
+ const effect = this.findConditionalEffect(jsxPath, componentPath, stateVar);
54
+ if (!effect) {
55
+ return null;
56
+ }
57
+ return { trigger, effect };
58
+ }
59
+ resolveStateSettersForAttribute(attr, context) {
60
+ if (attr.type !== 'JSXAttribute' || !attr.value || attr.value.type !== 'JSXExpressionContainer') {
61
+ return [];
62
+ }
63
+ const expression = attr.value.expression;
64
+ if (!expression)
65
+ return [];
66
+ const setterMap = new Map();
67
+ for (const stateVar of context.stateVariables) {
68
+ if (stateVar.setter) {
69
+ setterMap.set(stateVar.setter, stateVar.name);
70
+ }
71
+ }
72
+ if (expression.type === 'Identifier') {
73
+ const handler = context.eventHandlers.find((h) => h.name === expression.name);
74
+ return handler?.callsStateSetters ?? [];
75
+ }
76
+ if (expression.type === 'ArrowFunctionExpression' || expression.type === 'FunctionExpression') {
77
+ const result = new Set();
78
+ this.collectSettersFromNode(expression.body, setterMap, result);
79
+ return Array.from(result);
80
+ }
81
+ return [];
82
+ }
83
+ collectSettersFromNode(node, setterMap, result) {
84
+ if (!node || typeof node !== 'object')
85
+ return;
86
+ if (node.type === 'CallExpression') {
87
+ const callee = node.callee;
88
+ if (callee?.type === 'Identifier') {
89
+ const stateName = setterMap.get(callee.name);
90
+ if (stateName)
91
+ result.add(stateName);
92
+ }
93
+ else if (callee?.type === 'MemberExpression' && callee.property?.type === 'Identifier') {
94
+ const stateName = setterMap.get(callee.property.name);
95
+ if (stateName)
96
+ result.add(stateName);
97
+ }
98
+ }
99
+ for (const key of Object.keys(node)) {
100
+ const value = node[key];
101
+ if (Array.isArray(value)) {
102
+ for (const child of value) {
103
+ this.collectSettersFromNode(child, setterMap, result);
104
+ }
105
+ }
106
+ else if (value && typeof value === 'object') {
107
+ this.collectSettersFromNode(value, setterMap, result);
108
+ }
109
+ }
110
+ }
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
+ findConditionalEffect(jsxPath, componentPath, stateVar) {
116
+ const directEffect = this.findConditionalEffectInAncestors(jsxPath, stateVar);
117
+ if (directEffect) {
118
+ return directEffect;
119
+ }
120
+ return this.findConditionalEffectInComponent(componentPath, stateVar);
121
+ }
122
+ findConditionalEffectInAncestors(jsxPath, stateVar) {
123
+ let effectFound = null;
124
+ let current = jsxPath.parentPath;
125
+ while (current && !effectFound) {
126
+ const node = current.node;
127
+ // Check for logical AND/OR: condition && <Component />
128
+ if (node.type === 'LogicalExpression') {
129
+ if (this.nodeReferencesState(node.left, stateVar)) {
130
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.right);
131
+ if (rendersWhenTrue) {
132
+ effectFound = {
133
+ type: 'conditionalRender',
134
+ rendersWhenTrue,
135
+ waitStrategy: 'elementAppears',
136
+ selector: `[data-clippy-component='${rendersWhenTrue}']`,
137
+ };
138
+ }
139
+ }
140
+ }
141
+ // Check for ternary: condition ? <Component /> : null
142
+ if (node.type === 'ConditionalExpression') {
143
+ if (this.nodeReferencesState(node.test, stateVar)) {
144
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.consequent);
145
+ const rendersWhenFalse = this.extractComponentNameFromNode(node.alternate);
146
+ if (rendersWhenTrue || rendersWhenFalse) {
147
+ effectFound = {
148
+ type: 'conditionalRender',
149
+ rendersWhenTrue: rendersWhenTrue ?? undefined,
150
+ rendersWhenFalse: rendersWhenFalse ?? undefined,
151
+ waitStrategy: 'elementAppears',
152
+ selector: rendersWhenTrue ? `[data-clippy-component='${rendersWhenTrue}']` : undefined,
153
+ };
154
+ }
155
+ }
156
+ }
157
+ current = current.parentPath;
158
+ }
159
+ return effectFound;
160
+ }
161
+ findConditionalEffectInComponent(componentPath, stateVar) {
162
+ let effectFound = null;
163
+ componentPath.traverse({
164
+ LogicalExpression: (path) => {
165
+ if (effectFound)
166
+ return;
167
+ const node = path.node;
168
+ if (this.nodeReferencesState(node.left, stateVar)) {
169
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.right);
170
+ if (rendersWhenTrue) {
171
+ effectFound = {
172
+ type: 'conditionalRender',
173
+ rendersWhenTrue,
174
+ waitStrategy: 'elementAppears',
175
+ selector: `[data-clippy-component='${rendersWhenTrue}']`,
176
+ };
177
+ }
178
+ }
179
+ },
180
+ ConditionalExpression: (path) => {
181
+ if (effectFound)
182
+ return;
183
+ const node = path.node;
184
+ if (this.nodeReferencesState(node.test, stateVar)) {
185
+ const rendersWhenTrue = this.extractComponentNameFromNode(node.consequent);
186
+ const rendersWhenFalse = this.extractComponentNameFromNode(node.alternate);
187
+ if (rendersWhenTrue || rendersWhenFalse) {
188
+ effectFound = {
189
+ type: 'conditionalRender',
190
+ rendersWhenTrue: rendersWhenTrue ?? undefined,
191
+ rendersWhenFalse: rendersWhenFalse ?? undefined,
192
+ waitStrategy: 'elementAppears',
193
+ selector: rendersWhenTrue ? `[data-clippy-component='${rendersWhenTrue}']` : undefined,
194
+ };
195
+ }
196
+ }
197
+ },
198
+ });
199
+ return effectFound;
200
+ }
201
+ /**
202
+ * Check if a node references a specific state variable by name.
203
+ */
204
+ nodeReferencesState(node, stateVar) {
205
+ if (node.type === 'Identifier' && node.name === stateVar) {
206
+ return true;
207
+ }
208
+ if (node.type === 'MemberExpression') {
209
+ return this.nodeReferencesState(node.object, stateVar);
210
+ }
211
+ if (node.type === 'CallExpression') {
212
+ return this.nodeReferencesState(node.callee, stateVar);
213
+ }
214
+ if (node.type === 'BinaryExpression' || node.type === 'LogicalExpression') {
215
+ return this.nodeReferencesState(node.left, stateVar) || this.nodeReferencesState(node.right, stateVar);
216
+ }
217
+ if (node.type === 'UnaryExpression') {
218
+ return this.nodeReferencesState(node.argument, stateVar);
219
+ }
220
+ return false;
221
+ }
222
+ /**
223
+ * Extract component name from a JSX element or identifier.
224
+ */
225
+ extractComponentNameFromNode(node) {
226
+ if (!node)
227
+ return null;
228
+ if (node.type === 'JSXElement' && node.openingElement?.name?.name) {
229
+ const name = node.openingElement.name.name;
230
+ // Only return if it looks like a component (PascalCase)
231
+ return /^[A-Z]/.test(name) ? name : null;
232
+ }
233
+ if (node.type === 'JSXFragment') {
234
+ return null;
235
+ }
236
+ if (node.type === 'Identifier' && /^[A-Z]/.test(node.name)) {
237
+ return node.name;
238
+ }
239
+ if (node.type === 'MemberExpression' && node.property?.name) {
240
+ return node.property.name;
241
+ }
242
+ return null;
243
+ }
244
+ }
245
+ 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,6 @@
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 './buildId';
4
+ export * from './upload/Adapter';
5
+ export * from './upload/BackendAdapter';
6
+ export * from './upload/BackendUploadAdapter';
package/dist/index.js CHANGED
@@ -1,2 +1,22 @@
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("./buildId"), exports);
20
+ __exportStar(require("./upload/Adapter"), exports);
21
+ __exportStar(require("./upload/BackendAdapter"), exports);
22
+ __exportStar(require("./upload/BackendUploadAdapter"), exports);
@@ -0,0 +1,12 @@
1
+ export interface InjectedEntry {
2
+ clippyId: string;
3
+ component: string;
4
+ tag: string;
5
+ line: number;
6
+ }
7
+ export interface InjectResult {
8
+ source: string;
9
+ injectedCount: number;
10
+ injected: InjectedEntry[];
11
+ }
12
+ export declare function injectClippyIds(source: string, filePath: string): InjectResult;
@@ -0,0 +1,91 @@
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
+ function injectClippyIds(source, filePath) {
12
+ let ast;
13
+ try {
14
+ ast = (0, parser_1.parse)(source, {
15
+ sourceType: 'module',
16
+ plugins: ['typescript', 'jsx'],
17
+ ranges: true,
18
+ });
19
+ }
20
+ catch {
21
+ return { source, injectedCount: 0, injected: [] };
22
+ }
23
+ const edits = [];
24
+ const injected = [];
25
+ (0, traverse_1.default)(ast, {
26
+ JSXOpeningElement(path) {
27
+ const opening = path.node;
28
+ const tagName = (0, HtmlTagGuards_1.getJSXTagName)(opening.name);
29
+ if (!tagName || !(0, HtmlTagGuards_1.isIntrinsicTag)(tagName))
30
+ return;
31
+ const attrs = opening.attributes;
32
+ if ((0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-id'))
33
+ return;
34
+ const componentName = findEnclosingComponentName(path) || (0, IdStrategy_1.deriveComponentName)(filePath);
35
+ const line = opening.loc?.start.line ?? 0;
36
+ const clippyId = (0, IdStrategy_1.deriveClippyId)(componentName, tagName, line);
37
+ const componentAttr = (0, HtmlTagGuards_1.hasAttribute)(attrs, 'data-clippy-component')
38
+ ? ''
39
+ : ` data-clippy-component="${componentName}"`;
40
+ const insertText = ` data-clippy-id="${clippyId}"${componentAttr}`;
41
+ const insertIndex = findAttributeInsertIndex(source, opening.range);
42
+ if (insertIndex === null)
43
+ return;
44
+ edits.push({ index: insertIndex, text: insertText });
45
+ injected.push({ clippyId, component: componentName, tag: tagName, line });
46
+ },
47
+ });
48
+ if (edits.length === 0) {
49
+ return { source, injectedCount: 0, injected: [] };
50
+ }
51
+ const transformed = applyEdits(source, edits);
52
+ return { source: transformed, injectedCount: edits.length, injected };
53
+ }
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;
59
+ }
60
+ if (current.isClassDeclaration() && current.node.id?.name) {
61
+ return current.node.id.name;
62
+ }
63
+ if (current.isVariableDeclarator() && current.node.id?.type === 'Identifier') {
64
+ return current.node.id.name;
65
+ }
66
+ if ((current.isArrowFunctionExpression() || current.isFunctionExpression()) &&
67
+ current.parentPath?.isVariableDeclarator() &&
68
+ current.parentPath.node.id?.type === 'Identifier') {
69
+ return current.parentPath.node.id.name;
70
+ }
71
+ current = current.parentPath;
72
+ }
73
+ return null;
74
+ }
75
+ function findAttributeInsertIndex(source, range) {
76
+ if (!range)
77
+ return null;
78
+ const end = range[1];
79
+ const closeCharIndex = source.lastIndexOf('>', end - 1);
80
+ if (closeCharIndex === -1)
81
+ return null;
82
+ return closeCharIndex;
83
+ }
84
+ function applyEdits(source, edits) {
85
+ const sorted = edits.slice().sort((a, b) => b.index - a.index);
86
+ let result = source;
87
+ for (const edit of sorted) {
88
+ result = result.slice(0, edit.index) + edit.text + result.slice(edit.index);
89
+ }
90
+ return result;
91
+ }
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export declare function deriveComponentName(filePath: string, fallback?: string): string;
2
+ export declare function deriveClippyId(componentName: string, tagName: string, lineNumber: number): string;
@@ -0,0 +1,38 @@
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.deriveComponentName = deriveComponentName;
7
+ exports.deriveClippyId = deriveClippyId;
8
+ const path_1 = __importDefault(require("path"));
9
+ function deriveComponentName(filePath, fallback) {
10
+ if (fallback)
11
+ return fallback;
12
+ const base = path_1.default.basename(filePath, path_1.default.extname(filePath));
13
+ if (!base || base === 'index') {
14
+ return 'UnknownComponent';
15
+ }
16
+ return normalizeComponentName(base);
17
+ }
18
+ function deriveClippyId(componentName, tagName, lineNumber) {
19
+ const sanitizedComponent = normalizeComponentName(componentName);
20
+ const normalizedTag = tagName.replace(/[^a-z0-9]/gi, '') || 'element';
21
+ const stableLine = lineNumber > 0 ? lineNumber : 0;
22
+ return `${sanitizedComponent}-${normalizedTag}-${stableLine}`;
23
+ }
24
+ function normalizeComponentName(value) {
25
+ const sanitized = value
26
+ .replace(/[^A-Za-z0-9]+/g, ' ')
27
+ .trim()
28
+ .split(/\s+/)
29
+ .filter(Boolean)
30
+ .map((segment) => segment
31
+ .replace(/[^A-Za-z0-9]/g, '')
32
+ .replace(/^[^A-Za-z]+/, '')
33
+ .replace(/([a-z])([A-Z])/g, '$1$2'))
34
+ .filter(Boolean)
35
+ .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
36
+ .join('');
37
+ return sanitized || 'Component';
38
+ }
package/dist/types.d.ts CHANGED
@@ -4,6 +4,11 @@ export interface ClippyPluginOptions {
4
4
  skipUpload?: boolean;
5
5
  productionOnly?: boolean;
6
6
  localOutputDir?: string;
7
+ artifactUploadMode?: 'single' | 'split';
8
+ legacyPackageMode?: boolean;
9
+ uploadAdapter?: {
10
+ uploadArtifacts?: (artifacts: PolicyArtifacts) => Promise<UploadResult>;
11
+ };
7
12
  }
8
13
  export type ModuleGraphSource = {
9
14
  type: 'webpack';
@@ -33,9 +38,12 @@ export interface DiscoveredElement {
33
38
  nearbyText: string[];
34
39
  semanticRole: string;
35
40
  interactions: string[];
41
+ loc?: {
42
+ line: number;
43
+ };
36
44
  }
37
45
  export interface SelectorCandidate {
38
- type: 'testid' | 'aria' | 'id' | 'semantic' | 'text' | 'structural';
46
+ type: 'clippy_id' | 'testid' | 'aria' | 'id' | 'semantic' | 'text' | 'structural';
39
47
  value: string;
40
48
  confidence: number;
41
49
  }
@@ -51,9 +59,102 @@ export interface FlowEdge {
51
59
  export interface InferredFlow {
52
60
  id: string;
53
61
  name: string;
62
+ page: string;
54
63
  steps: string[];
55
64
  edges: FlowEdge[];
56
65
  }
66
+ export interface ArtifactMetadata {
67
+ version: string;
68
+ buildId: string;
69
+ generatedAt: string;
70
+ bundler: 'webpack' | 'vite';
71
+ }
72
+ export interface TriggerSpec {
73
+ event: string;
74
+ element: string;
75
+ setsState?: string;
76
+ }
77
+ export interface EffectSpec {
78
+ type: 'conditionalRender' | 'asyncEffect' | 'contextDependency';
79
+ rendersWhenTrue?: string;
80
+ rendersWhenFalse?: string;
81
+ waitStrategy: 'elementAppears' | 'domSettle' | 'none';
82
+ selector?: string;
83
+ settleMs?: number;
84
+ }
85
+ export interface ComponentInteraction {
86
+ trigger: TriggerSpec;
87
+ effect: EffectSpec;
88
+ }
89
+ export interface PolicySelectorEntry {
90
+ clippyId: string;
91
+ selector: string;
92
+ tag: string;
93
+ component: string;
94
+ route: string;
95
+ filePath: string;
96
+ attributes: Array<{
97
+ name: string;
98
+ value: string;
99
+ }>;
100
+ candidates: SelectorCandidate[];
101
+ }
102
+ export interface PolicyFlowStep {
103
+ step: number;
104
+ action: string;
105
+ target: string;
106
+ triggers?: Array<{
107
+ renders: string;
108
+ waitStrategy: 'elementAppears' | 'domSettle' | 'none';
109
+ }>;
110
+ }
111
+ export interface PolicyFlow {
112
+ flowId: string;
113
+ page: string;
114
+ intentPatterns: string[];
115
+ steps: PolicyFlowStep[];
116
+ }
117
+ export interface PolicyComponent {
118
+ name: string;
119
+ filePath: string;
120
+ route: string;
121
+ stateVariables: Array<{
122
+ name: string;
123
+ setter?: string;
124
+ initialValue?: string;
125
+ }>;
126
+ interactions: ComponentInteraction[];
127
+ }
128
+ export interface PolicyDocument extends ArtifactMetadata {
129
+ routes: DiscoveredRoute[];
130
+ selectors: PolicySelectorEntry[];
131
+ components: PolicyComponent[];
132
+ flows: PolicyFlow[];
133
+ }
134
+ export interface SelectorManifestEntry {
135
+ id: string;
136
+ selector: string;
137
+ component: string;
138
+ tag: string;
139
+ route: string;
140
+ }
141
+ export interface SelectorManifest {
142
+ version: string;
143
+ buildId: string;
144
+ generatedAt: string;
145
+ selectors: SelectorManifestEntry[];
146
+ }
147
+ export interface PolicyArtifacts {
148
+ metadata: ArtifactMetadata;
149
+ policy: PolicyDocument;
150
+ selectorManifest: SelectorManifest;
151
+ }
152
+ export interface UploadResult {
153
+ skipped: boolean;
154
+ policyUploaded: boolean;
155
+ selectorsUploaded: boolean;
156
+ mode: 'single' | 'split';
157
+ }
57
158
  export interface KnowledgePackage {
58
159
  buildId: string;
59
160
  generatedAt: string;
@@ -0,0 +1,3 @@
1
+ import type { ClippyPluginOptions, PolicyArtifacts, UploadResult } from '../types';
2
+ export declare function performUpload(artifacts: PolicyArtifacts, options: ClippyPluginOptions): Promise<UploadResult>;
3
+ export default performUpload;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.performUpload = performUpload;
4
+ const Uploader_1 = require("./Uploader");
5
+ async function performUpload(artifacts, options) {
6
+ // Allow adapters to supply a custom upload implementation
7
+ if (options && options.uploadAdapter && typeof options.uploadAdapter.uploadArtifacts === 'function') {
8
+ return options.uploadAdapter.uploadArtifacts(artifacts);
9
+ }
10
+ const uploader = new Uploader_1.Uploader(options);
11
+ return uploader.uploadArtifacts(artifacts);
12
+ }
13
+ exports.default = performUpload;