@promakeai/inspector-hook 1.1.1 → 1.2.1

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/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { RefObject } from "react";
2
2
  import type { InspectorCallbacks, InspectorLabels, InspectorTheme, UseInspectorReturn } from "@promakeai/inspector-types";
3
3
  export declare function useInspector(iframeRef: RefObject<HTMLIFrameElement>, callbacks?: InspectorCallbacks, labels?: InspectorLabels, theme?: InspectorTheme): UseInspectorReturn;
4
- export type { ComponentInfo, ElementPosition, SelectedElementData, UrlChangeData, PromptSubmittedData, TextUpdatedData, ImageUpdatedData, StyleChanges, StyleUpdatedData, ErrorData, HighlightOptions, ElementInfoData, InspectorLabels, InspectorTheme, ContentInputRequestData, InspectorCallbacks, UseInspectorReturn, } from "@promakeai/inspector-types";
5
- export { updateJSXSource } from "./utils/jsxUpdater.js";
6
- export type { UpdateJSXSourceOptions, UpdateJSXSourceResult, } from "./utils/jsxUpdater.js";
4
+ export type { ComponentInfo, ElementPosition, SelectedElementData, UrlChangeData, PromptSubmittedData, TextUpdatedData, ImageUpdatedData, StyleChanges, StyleUpdatedData, ErrorData, HighlightOptions, ElementInfoData, ElementDeletedData, ElementDuplicatedData, ChangeType, ChangeHistoryEntry, InspectorChange, InspectorChangeType, InspectorChangesSavedData, InspectorLabels, InspectorTheme, ContentInputRequestData, InspectorCallbacks, UseInspectorReturn, } from "@promakeai/inspector-types";
5
+ export { updateJSXSource, deleteJSXElement, duplicateJSXElement, updateTextContent, applyChangesToJSXSource, } from "./utils/jsxUpdater.js";
6
+ export type { UpdateJSXSourceOptions, UpdateJSXSourceResult, DeleteJSXElementOptions, DuplicateJSXElementOptions, UpdateTextContentOptions, ApplyChangesOptions, ApplyChangesResult, ChangeApplicationResult, } from "./utils/jsxUpdater.js";
7
7
  export { inspectorHookPlugin } from "./vite-plugin.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,kBAAkB,EAUnB,MAAM,4BAA4B,CAAC;AA+EpC,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACvC,SAAS,CAAC,EAAE,kBAAkB,EAC9B,MAAM,CAAC,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,cAAc,GACrB,kBAAkB,CA6PpB;AAGD,YAAY,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,gBAAgB,EAChB,eAAe,EACf,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,SAAS,EAAE,MAAM,OAAO,CAAC;AACpE,OAAO,KAAK,EACV,kBAAkB,EAClB,eAAe,EACf,cAAc,EACd,kBAAkB,EAcnB,MAAM,4BAA4B,CAAC;AAsGpC,wBAAgB,YAAY,CAC1B,SAAS,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACvC,SAAS,CAAC,EAAE,kBAAkB,EAC9B,MAAM,CAAC,EAAE,eAAe,EACxB,KAAK,CAAC,EAAE,cAAc,GACrB,kBAAkB,CAuSpB;AAGD,YAAY,EACV,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,aAAa,EACb,mBAAmB,EACnB,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,gBAAgB,EAChB,SAAS,EACT,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,qBAAqB,EACrB,UAAU,EACV,kBAAkB,EAClB,eAAe,EACf,mBAAmB,EACnB,yBAAyB,EACzB,eAAe,EACf,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,mBAAmB,EACnB,iBAAiB,EACjB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,uBAAuB,EACvB,0BAA0B,EAC1B,wBAAwB,EACxB,mBAAmB,EACnB,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -42,6 +42,16 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
42
42
  const stopInspecting = useCallback(() => {
43
43
  toggleInspector(false);
44
44
  }, [toggleInspector]);
45
+ /**
46
+ * Force close inspector - hard reset for emergencies
47
+ * Use when inspector becomes unresponsive
48
+ */
49
+ const forceCloseInspector = useCallback(() => {
50
+ setIsInspecting(false);
51
+ sendMessage({
52
+ type: "FORCE_CLOSE_INSPECTOR",
53
+ });
54
+ }, [sendMessage]);
45
55
  /**
46
56
  * Show or hide content input
47
57
  */
@@ -71,6 +81,15 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
71
81
  show: show,
72
82
  });
73
83
  }, [sendMessage]);
84
+ /**
85
+ * Show or hide actions tab (delete, duplicate, etc.)
86
+ */
87
+ const showActionsTab = useCallback((show) => {
88
+ sendMessage({
89
+ type: "SHOW_ACTIONS_TAB",
90
+ show: show,
91
+ });
92
+ }, [sendMessage]);
74
93
  /**
75
94
  * Show or hide "Built with Promake" badge
76
95
  */
@@ -160,6 +179,18 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
160
179
  case "ELEMENT_INFO_RESPONSE":
161
180
  callbacks?.onElementInfoReceived?.(messageData.data);
162
181
  break;
182
+ case "INSPECTOR_ELEMENT_DELETED":
183
+ callbacks?.onElementDeleted?.(messageData.data);
184
+ break;
185
+ case "INSPECTOR_ELEMENT_DUPLICATED":
186
+ callbacks?.onElementDuplicated?.(messageData.data);
187
+ break;
188
+ case "INSPECTOR_CHANGES_SAVED":
189
+ callbacks?.onChangesSaved?.(messageData.data);
190
+ break;
191
+ case "INSPECTOR_GO_TO_CODE":
192
+ callbacks?.onGoToCode?.(messageData.data);
193
+ break;
163
194
  default:
164
195
  // Unknown message type - ignore
165
196
  break;
@@ -190,9 +221,11 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
190
221
  toggleInspector,
191
222
  startInspecting,
192
223
  stopInspecting,
224
+ forceCloseInspector,
193
225
  showContentInput,
194
226
  showImageInput,
195
227
  showStyleEditor,
228
+ showActionsTab,
196
229
  setBadgeVisible,
197
230
  highlightElement,
198
231
  getElementByInspectorId,
@@ -200,6 +233,6 @@ export function useInspector(iframeRef, callbacks, labels, theme) {
200
233
  };
201
234
  }
202
235
  // Export utility functions
203
- export { updateJSXSource } from "./utils/jsxUpdater.js";
236
+ export { updateJSXSource, deleteJSXElement, duplicateJSXElement, updateTextContent, applyChangesToJSXSource, } from "./utils/jsxUpdater.js";
204
237
  // Export Vite plugin
205
238
  export { inspectorHookPlugin } from "./vite-plugin.js";
@@ -3,6 +3,7 @@
3
3
  * Lightweight regex-based utility for updating styles and classNames in JSX/TSX source code
4
4
  * No Babel dependencies - works perfectly in browser environments!
5
5
  */
6
+ import type { InspectorChange, InspectorChangeType } from "@promakeai/inspector-types";
6
7
  export interface UpdateJSXSourceOptions {
7
8
  sourceCode: string;
8
9
  lineNumber: number;
@@ -16,6 +17,18 @@ export interface UpdateJSXSourceResult {
16
17
  code: string;
17
18
  message?: string;
18
19
  }
20
+ export interface DeleteJSXElementOptions {
21
+ sourceCode: string;
22
+ lineNumber: number;
23
+ columnNumber: number;
24
+ tagName: string;
25
+ }
26
+ export interface DuplicateJSXElementOptions {
27
+ sourceCode: string;
28
+ lineNumber: number;
29
+ columnNumber: number;
30
+ tagName: string;
31
+ }
19
32
  /**
20
33
  * Update JSX source code with new styles and/or className
21
34
  *
@@ -36,4 +49,92 @@ export interface UpdateJSXSourceResult {
36
49
  * ```
37
50
  */
38
51
  export declare function updateJSXSource(options: UpdateJSXSourceOptions): UpdateJSXSourceResult;
52
+ /**
53
+ * Delete a JSX element from source code
54
+ *
55
+ * @param options - Configuration options for deletion
56
+ * @returns Result object with success status, updated code, and optional message
57
+ */
58
+ export declare function deleteJSXElement(options: DeleteJSXElementOptions): UpdateJSXSourceResult;
59
+ /**
60
+ * Duplicate a JSX element in source code (inserts copy after the original)
61
+ *
62
+ * @param options - Configuration options for duplication
63
+ * @returns Result object with success status, updated code, and optional message
64
+ */
65
+ export declare function duplicateJSXElement(options: DuplicateJSXElementOptions): UpdateJSXSourceResult;
66
+ /**
67
+ * Options for updating text content
68
+ */
69
+ export interface UpdateTextContentOptions {
70
+ sourceCode: string;
71
+ lineNumber: number;
72
+ columnNumber: number;
73
+ tagName: string;
74
+ newText: string;
75
+ }
76
+ /**
77
+ * Update the text content of a JSX element
78
+ * Only works if the element contains pure text (no JS expressions or nested elements)
79
+ *
80
+ * @param options - Configuration options for the text update
81
+ * @returns Result object with success status, updated code, and optional message
82
+ */
83
+ export declare function updateTextContent(options: UpdateTextContentOptions): UpdateJSXSourceResult;
84
+ /**
85
+ * Options for applying changes to JSX source
86
+ */
87
+ export interface ApplyChangesOptions {
88
+ sourceCode: string;
89
+ changes: InspectorChange[];
90
+ }
91
+ /**
92
+ * Result of a single change application
93
+ */
94
+ export interface ChangeApplicationResult {
95
+ changeId: string;
96
+ type: InspectorChangeType;
97
+ success: boolean;
98
+ message?: string;
99
+ }
100
+ /**
101
+ * Result of applying all changes
102
+ */
103
+ export interface ApplyChangesResult {
104
+ success: boolean;
105
+ code: string;
106
+ results: ChangeApplicationResult[];
107
+ summary: {
108
+ total: number;
109
+ successful: number;
110
+ failed: number;
111
+ skipped: number;
112
+ };
113
+ }
114
+ /**
115
+ * Apply a collection of InspectorChange objects to JSX source code
116
+ *
117
+ * This function processes changes in a logical order:
118
+ * 1. Groups changes by element
119
+ * 2. For each element:
120
+ * - If delete: deletes the element, skips other changes for that element
121
+ * - If duplicate: applies styles/text first, then duplicates
122
+ * - Merges all style changes into single update
123
+ * - Applies text changes (only if pure text)
124
+ *
125
+ * @param options - Source code and changes to apply
126
+ * @returns Result with updated code and detailed results for each change
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const result = applyChangesToJSXSource({
131
+ * sourceCode: '<div>Hello</div>',
132
+ * changes: [
133
+ * { type: 'singleStyle', singleStyle: { property: 'color', currentValue: 'red' }, ... },
134
+ * { type: 'text', text: { current: 'World' }, ... }
135
+ * ]
136
+ * });
137
+ * ```
138
+ */
139
+ export declare function applyChangesToJSXSource(options: ApplyChangesOptions): ApplyChangesResult;
39
140
  //# sourceMappingURL=jsxUpdater.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jsxUpdater.d.ts","sourceRoot":"","sources":["../../src/utils/jsxUpdater.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA4YD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAkFvB"}
1
+ {"version":3,"file":"jsxUpdater.d.ts","sourceRoot":"","sources":["../../src/utils/jsxUpdater.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EAEpB,MAAM,4BAA4B,CAAC;AAEpC,MAAM,WAAW,sBAAsB;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AA4YD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,sBAAsB,GAC9B,qBAAqB,CAmFvB;AAoGD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,uBAAuB,GAC/B,qBAAqB,CAqDvB;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,0BAA0B,GAClC,qBAAqB,CAmDvB;AAED;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAmBD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,wBAAwB,GAChC,qBAAqB,CA+FvB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,mBAAmB,CAAC;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,uBAAuB,EAAE,CAAC;IACnC,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;KACjB,CAAC;CACH;AA4CD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,mBAAmB,GAC3B,kBAAkB,CA2MpB"}
@@ -347,14 +347,9 @@ export function updateJSXSource(options) {
347
347
  message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
348
348
  };
349
349
  }
350
- // Verify tag name matches
351
- if (tagInfo.foundTagName.toLowerCase() !== tagName.toLowerCase()) {
352
- return {
353
- success: false,
354
- code: sourceCode,
355
- message: `Tag name mismatch: expected <${tagName}>, found <${tagInfo.foundTagName}>`,
356
- };
357
- }
350
+ // Check for tag name mismatch (warning only - continue anyway)
351
+ // This handles cases like Link component rendering as <a> in DOM
352
+ const tagNameMismatch = tagInfo.foundTagName.toLowerCase() !== tagName.toLowerCase();
358
353
  // Validate that the element has proper closing tag
359
354
  if (!hasProperClosingTag(sourceCode, tagInfo.start, tagInfo.end, tagInfo.foundTagName)) {
360
355
  return {
@@ -376,10 +371,14 @@ export function updateJSXSource(options) {
376
371
  const updatedCode = sourceCode.slice(0, tagInfo.start) +
377
372
  updatedTagContent +
378
373
  sourceCode.slice(tagInfo.end);
374
+ // Include warning in message if tag names didn't match
375
+ const message = tagNameMismatch
376
+ ? `JSX source updated successfully (note: expected <${tagName}>, found <${tagInfo.foundTagName}>)`
377
+ : "JSX source updated successfully";
379
378
  return {
380
379
  success: true,
381
380
  code: updatedCode,
382
- message: "JSX source updated successfully",
381
+ message,
383
382
  };
384
383
  }
385
384
  catch (error) {
@@ -392,3 +391,506 @@ export function updateJSXSource(options) {
392
391
  };
393
392
  }
394
393
  }
394
+ /**
395
+ * Find the full JSX element boundaries (from opening tag to closing tag)
396
+ */
397
+ function findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName) {
398
+ // First find the opening tag
399
+ const tagInfo = findJSXTagAtPosition(sourceCode, lineNumber, columnNumber, tagName);
400
+ if (!tagInfo) {
401
+ return null;
402
+ }
403
+ // Check if self-closing
404
+ if (tagInfo.content.trim().endsWith("/>")) {
405
+ return {
406
+ start: tagInfo.start,
407
+ end: tagInfo.end,
408
+ content: tagInfo.content,
409
+ isSelfClosing: true,
410
+ };
411
+ }
412
+ // Find the matching closing tag
413
+ const afterOpening = sourceCode.slice(tagInfo.end);
414
+ let depth = 1;
415
+ let i = 0;
416
+ let inString = false;
417
+ let stringChar = "";
418
+ while (i < afterOpening.length && depth > 0) {
419
+ const char = afterOpening[i];
420
+ const prevChar = i > 0 ? afterOpening[i - 1] : "";
421
+ // Handle strings
422
+ if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
423
+ if (!inString) {
424
+ inString = true;
425
+ stringChar = char;
426
+ }
427
+ else if (char === stringChar) {
428
+ inString = false;
429
+ stringChar = "";
430
+ }
431
+ }
432
+ if (!inString && char === "<") {
433
+ // Check for opening or closing tag
434
+ const remainingCode = afterOpening.slice(i);
435
+ const tagMatch = remainingCode.match(/^<\/?([A-Za-z_$][A-Za-z0-9_$]*)/);
436
+ if (tagMatch) {
437
+ const isClosing = remainingCode.startsWith("</");
438
+ const foundTag = tagMatch[1];
439
+ if (isClosing && foundTag === tagInfo.foundTagName) {
440
+ depth--;
441
+ if (depth === 0) {
442
+ // Find end of closing tag
443
+ const closingEnd = remainingCode.indexOf(">");
444
+ if (closingEnd !== -1) {
445
+ const fullEnd = tagInfo.end + i + closingEnd + 1;
446
+ return {
447
+ start: tagInfo.start,
448
+ end: fullEnd,
449
+ content: sourceCode.slice(tagInfo.start, fullEnd),
450
+ isSelfClosing: false,
451
+ };
452
+ }
453
+ }
454
+ }
455
+ else if (!isClosing && foundTag === tagInfo.foundTagName) {
456
+ // Check if self-closing
457
+ const tagEndMatch = remainingCode.match(/^<[^>]+?(\/?)>/);
458
+ if (tagEndMatch && !tagEndMatch[1]) {
459
+ depth++;
460
+ }
461
+ }
462
+ }
463
+ }
464
+ i++;
465
+ }
466
+ return null;
467
+ }
468
+ /**
469
+ * Delete a JSX element from source code
470
+ *
471
+ * @param options - Configuration options for deletion
472
+ * @returns Result object with success status, updated code, and optional message
473
+ */
474
+ export function deleteJSXElement(options) {
475
+ const { sourceCode, lineNumber, columnNumber, tagName } = options;
476
+ try {
477
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
478
+ if (!elementBounds) {
479
+ return {
480
+ success: false,
481
+ code: sourceCode,
482
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
483
+ };
484
+ }
485
+ // Get content before and after the element
486
+ let before = sourceCode.slice(0, elementBounds.start);
487
+ let after = sourceCode.slice(elementBounds.end);
488
+ // Clean up whitespace - remove trailing whitespace from before and leading newline from after
489
+ // This prevents leaving empty lines
490
+ const trailingWhitespaceMatch = before.match(/(\s*)$/);
491
+ const leadingWhitespaceMatch = after.match(/^(\s*\n)?/);
492
+ if (trailingWhitespaceMatch && leadingWhitespaceMatch) {
493
+ // If there's a newline after, remove the trailing whitespace before
494
+ if (leadingWhitespaceMatch[0].includes("\n")) {
495
+ before = before.replace(/[ \t]*$/, "");
496
+ after = after.replace(/^[ \t]*\n/, "\n");
497
+ }
498
+ }
499
+ const updatedCode = before + after;
500
+ return {
501
+ success: true,
502
+ code: updatedCode,
503
+ message: `JSX element <${tagName}> deleted successfully`,
504
+ };
505
+ }
506
+ catch (error) {
507
+ return {
508
+ success: false,
509
+ code: sourceCode,
510
+ message: error instanceof Error
511
+ ? error.message
512
+ : "Unknown error occurred during JSX deletion",
513
+ };
514
+ }
515
+ }
516
+ /**
517
+ * Duplicate a JSX element in source code (inserts copy after the original)
518
+ *
519
+ * @param options - Configuration options for duplication
520
+ * @returns Result object with success status, updated code, and optional message
521
+ */
522
+ export function duplicateJSXElement(options) {
523
+ const { sourceCode, lineNumber, columnNumber, tagName } = options;
524
+ try {
525
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
526
+ if (!elementBounds) {
527
+ return {
528
+ success: false,
529
+ code: sourceCode,
530
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
531
+ };
532
+ }
533
+ // Get the indentation of the original element
534
+ const beforeElement = sourceCode.slice(0, elementBounds.start);
535
+ const lastNewline = beforeElement.lastIndexOf("\n");
536
+ const indentation = lastNewline !== -1
537
+ ? beforeElement.slice(lastNewline + 1).match(/^(\s*)/)?.[1] || ""
538
+ : "";
539
+ // Create the duplicated element with proper indentation
540
+ const duplicatedElement = "\n" + indentation + elementBounds.content;
541
+ // Insert the duplicate after the original
542
+ const updatedCode = sourceCode.slice(0, elementBounds.end) +
543
+ duplicatedElement +
544
+ sourceCode.slice(elementBounds.end);
545
+ return {
546
+ success: true,
547
+ code: updatedCode,
548
+ message: `JSX element <${tagName}> duplicated successfully`,
549
+ };
550
+ }
551
+ catch (error) {
552
+ return {
553
+ success: false,
554
+ code: sourceCode,
555
+ message: error instanceof Error
556
+ ? error.message
557
+ : "Unknown error occurred during JSX duplication",
558
+ };
559
+ }
560
+ }
561
+ /**
562
+ * Check if element content is pure text (no JS expressions or nested elements)
563
+ */
564
+ function isPureTextContent(content) {
565
+ // Check for JSX expressions {something}
566
+ if (content.includes("{") && content.includes("}")) {
567
+ return false;
568
+ }
569
+ // Check for nested elements <something>
570
+ if (/<[A-Za-z_$][A-Za-z0-9_$]*/.test(content)) {
571
+ return false;
572
+ }
573
+ return true;
574
+ }
575
+ /**
576
+ * Update the text content of a JSX element
577
+ * Only works if the element contains pure text (no JS expressions or nested elements)
578
+ *
579
+ * @param options - Configuration options for the text update
580
+ * @returns Result object with success status, updated code, and optional message
581
+ */
582
+ export function updateTextContent(options) {
583
+ const { sourceCode, lineNumber, columnNumber, tagName, newText } = options;
584
+ try {
585
+ // Find the full element boundaries
586
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
587
+ if (!elementBounds) {
588
+ return {
589
+ success: false,
590
+ code: sourceCode,
591
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
592
+ };
593
+ }
594
+ // Self-closing elements can't have text content
595
+ if (elementBounds.isSelfClosing) {
596
+ return {
597
+ success: false,
598
+ code: sourceCode,
599
+ message: `Element <${tagName}> is self-closing and cannot have text content`,
600
+ };
601
+ }
602
+ // Find the opening tag end and closing tag start
603
+ const tagInfo = findJSXTagAtPosition(sourceCode, lineNumber, columnNumber, tagName);
604
+ if (!tagInfo) {
605
+ return {
606
+ success: false,
607
+ code: sourceCode,
608
+ message: `Could not find opening tag for <${tagName}>`,
609
+ };
610
+ }
611
+ // Use the found tag name for closing tag search (handles Link -> a mapping etc.)
612
+ const actualTagName = tagInfo.foundTagName;
613
+ // Extract content between opening and closing tag
614
+ const closingTagPattern = new RegExp(`</${actualTagName}\\s*>`);
615
+ const afterOpening = sourceCode.slice(tagInfo.end);
616
+ const closingMatch = closingTagPattern.exec(afterOpening);
617
+ if (!closingMatch) {
618
+ return {
619
+ success: false,
620
+ code: sourceCode,
621
+ message: `Could not find closing tag for <${actualTagName}>`,
622
+ };
623
+ }
624
+ const contentStart = tagInfo.end;
625
+ const contentEnd = tagInfo.end + closingMatch.index;
626
+ const currentContent = sourceCode.slice(contentStart, contentEnd);
627
+ // Check if content is pure text
628
+ if (!isPureTextContent(currentContent)) {
629
+ return {
630
+ success: false,
631
+ code: sourceCode,
632
+ message: `Element <${actualTagName}> contains JS expressions or nested elements. Text update skipped.`,
633
+ };
634
+ }
635
+ // Replace the content
636
+ const updatedCode = sourceCode.slice(0, contentStart) +
637
+ newText +
638
+ sourceCode.slice(contentEnd);
639
+ return {
640
+ success: true,
641
+ code: updatedCode,
642
+ message: `Text content of <${actualTagName}> updated successfully`,
643
+ };
644
+ }
645
+ catch (error) {
646
+ return {
647
+ success: false,
648
+ code: sourceCode,
649
+ message: error instanceof Error
650
+ ? error.message
651
+ : "Unknown error occurred during text update",
652
+ };
653
+ }
654
+ }
655
+ /**
656
+ * Group changes by element ID
657
+ */
658
+ function groupChangesByElement(changes) {
659
+ const grouped = new Map();
660
+ for (const change of changes) {
661
+ const elementId = change.element.id || change.element.selector || "unknown";
662
+ const existing = grouped.get(elementId) || [];
663
+ existing.push(change);
664
+ grouped.set(elementId, existing);
665
+ }
666
+ return grouped;
667
+ }
668
+ /**
669
+ * Merge multiple style changes into a single styles object
670
+ */
671
+ function mergeStyleChanges(changes) {
672
+ const mergedStyles = {};
673
+ for (const change of changes) {
674
+ if (change.type === "singleStyle" && change.singleStyle) {
675
+ const { property, currentValue } = change.singleStyle;
676
+ mergedStyles[property] = currentValue;
677
+ }
678
+ else if (change.type === "style" && change.style?.current) {
679
+ // Merge all current styles
680
+ const currentStyles = change.style.current;
681
+ for (const [key, value] of Object.entries(currentStyles)) {
682
+ if (value !== undefined && value !== "") {
683
+ mergedStyles[key] = value;
684
+ }
685
+ }
686
+ }
687
+ }
688
+ return mergedStyles;
689
+ }
690
+ /**
691
+ * Apply a collection of InspectorChange objects to JSX source code
692
+ *
693
+ * This function processes changes in a logical order:
694
+ * 1. Groups changes by element
695
+ * 2. For each element:
696
+ * - If delete: deletes the element, skips other changes for that element
697
+ * - If duplicate: applies styles/text first, then duplicates
698
+ * - Merges all style changes into single update
699
+ * - Applies text changes (only if pure text)
700
+ *
701
+ * @param options - Source code and changes to apply
702
+ * @returns Result with updated code and detailed results for each change
703
+ *
704
+ * @example
705
+ * ```typescript
706
+ * const result = applyChangesToJSXSource({
707
+ * sourceCode: '<div>Hello</div>',
708
+ * changes: [
709
+ * { type: 'singleStyle', singleStyle: { property: 'color', currentValue: 'red' }, ... },
710
+ * { type: 'text', text: { current: 'World' }, ... }
711
+ * ]
712
+ * });
713
+ * ```
714
+ */
715
+ export function applyChangesToJSXSource(options) {
716
+ const { sourceCode, changes } = options;
717
+ const results = [];
718
+ let currentCode = sourceCode;
719
+ let successful = 0;
720
+ let failed = 0;
721
+ let skipped = 0;
722
+ // Group changes by element
723
+ const groupedChanges = groupChangesByElement(changes);
724
+ // Process each element's changes
725
+ for (const [elementId, elementChanges] of groupedChanges) {
726
+ // Check if there's a delete change for this element
727
+ const deleteChange = elementChanges.find((c) => c.type === "delete");
728
+ if (deleteChange) {
729
+ // If delete exists, delete the element and skip all other changes for this element
730
+ const element = deleteChange.element;
731
+ if (element.lineNumber && element.columnNumber !== undefined) {
732
+ const deleteResult = deleteJSXElement({
733
+ sourceCode: currentCode,
734
+ lineNumber: element.lineNumber,
735
+ columnNumber: element.columnNumber,
736
+ tagName: element.tagName,
737
+ });
738
+ results.push({
739
+ changeId: deleteChange.id,
740
+ type: "delete",
741
+ success: deleteResult.success,
742
+ message: deleteResult.message,
743
+ });
744
+ if (deleteResult.success) {
745
+ currentCode = deleteResult.code;
746
+ successful++;
747
+ }
748
+ else {
749
+ failed++;
750
+ }
751
+ // Skip other changes for this element
752
+ for (const change of elementChanges) {
753
+ if (change.id !== deleteChange.id) {
754
+ results.push({
755
+ changeId: change.id,
756
+ type: change.type,
757
+ success: false,
758
+ message: "Skipped: element was deleted",
759
+ });
760
+ skipped++;
761
+ }
762
+ }
763
+ continue;
764
+ }
765
+ else {
766
+ results.push({
767
+ changeId: deleteChange.id,
768
+ type: "delete",
769
+ success: false,
770
+ message: "Missing line/column number for delete operation",
771
+ });
772
+ failed++;
773
+ }
774
+ }
775
+ // Separate changes by type
776
+ const styleChanges = elementChanges.filter((c) => c.type === "singleStyle" || c.type === "style");
777
+ const textChanges = elementChanges.filter((c) => c.type === "text");
778
+ const duplicateChanges = elementChanges.filter((c) => c.type === "duplicate");
779
+ // Get element position from first change that has it
780
+ const firstChange = elementChanges[0];
781
+ const element = firstChange.element;
782
+ if (!element.lineNumber || element.columnNumber === undefined) {
783
+ // Can't process without position
784
+ for (const change of elementChanges) {
785
+ if (change.type !== "delete") {
786
+ results.push({
787
+ changeId: change.id,
788
+ type: change.type,
789
+ success: false,
790
+ message: "Missing line/column number",
791
+ });
792
+ failed++;
793
+ }
794
+ }
795
+ continue;
796
+ }
797
+ // Apply style changes (merged)
798
+ if (styleChanges.length > 0) {
799
+ const mergedStyles = mergeStyleChanges(styleChanges);
800
+ if (Object.keys(mergedStyles).length > 0) {
801
+ const styleResult = updateJSXSource({
802
+ sourceCode: currentCode,
803
+ lineNumber: element.lineNumber,
804
+ columnNumber: element.columnNumber,
805
+ tagName: element.tagName,
806
+ styles: mergedStyles,
807
+ });
808
+ // Record result for each style change
809
+ for (const change of styleChanges) {
810
+ results.push({
811
+ changeId: change.id,
812
+ type: change.type,
813
+ success: styleResult.success,
814
+ message: styleResult.success
815
+ ? "Style applied (merged with other styles)"
816
+ : styleResult.message,
817
+ });
818
+ if (styleResult.success) {
819
+ successful++;
820
+ }
821
+ else {
822
+ failed++;
823
+ }
824
+ }
825
+ if (styleResult.success) {
826
+ currentCode = styleResult.code;
827
+ }
828
+ }
829
+ }
830
+ // Apply text changes
831
+ for (const change of textChanges) {
832
+ if (change.text?.current) {
833
+ const textResult = updateTextContent({
834
+ sourceCode: currentCode,
835
+ lineNumber: element.lineNumber,
836
+ columnNumber: element.columnNumber,
837
+ tagName: element.tagName,
838
+ newText: change.text.current,
839
+ });
840
+ results.push({
841
+ changeId: change.id,
842
+ type: "text",
843
+ success: textResult.success,
844
+ message: textResult.message,
845
+ });
846
+ if (textResult.success) {
847
+ currentCode = textResult.code;
848
+ successful++;
849
+ }
850
+ else {
851
+ // Check if it was skipped due to JS expressions
852
+ if (textResult.message?.includes("JS expressions")) {
853
+ skipped++;
854
+ }
855
+ else {
856
+ failed++;
857
+ }
858
+ }
859
+ }
860
+ }
861
+ // Apply duplicate changes
862
+ for (const change of duplicateChanges) {
863
+ const dupResult = duplicateJSXElement({
864
+ sourceCode: currentCode,
865
+ lineNumber: element.lineNumber,
866
+ columnNumber: element.columnNumber,
867
+ tagName: element.tagName,
868
+ });
869
+ results.push({
870
+ changeId: change.id,
871
+ type: "duplicate",
872
+ success: dupResult.success,
873
+ message: dupResult.message,
874
+ });
875
+ if (dupResult.success) {
876
+ currentCode = dupResult.code;
877
+ successful++;
878
+ }
879
+ else {
880
+ failed++;
881
+ }
882
+ }
883
+ }
884
+ const allSuccessful = failed === 0 && skipped === 0;
885
+ return {
886
+ success: allSuccessful,
887
+ code: currentCode,
888
+ results,
889
+ summary: {
890
+ total: changes.length,
891
+ successful,
892
+ failed,
893
+ skipped,
894
+ },
895
+ };
896
+ }
package/package.json CHANGED
@@ -1,54 +1,54 @@
1
- {
2
- "name": "@promakeai/inspector-hook",
3
- "version": "1.1.1",
4
- "description": "React hook for controlling inspector in parent applications",
5
- "author": "Promake",
6
- "type": "module",
7
- "main": "./dist/index.js",
8
- "module": "./dist/index.js",
9
- "types": "./dist/index.d.ts",
10
- "exports": {
11
- ".": {
12
- "types": "./dist/index.d.ts",
13
- "import": "./dist/index.js"
14
- },
15
- "./package.json": "./package.json"
16
- },
17
- "files": [
18
- "dist"
19
- ],
20
- "scripts": {
21
- "build": "tsc --build",
22
- "dev": "tsc --watch",
23
- "clean": "rm -rf dist",
24
- "lint": "tsc --build --force --dry",
25
- "test": "vitest run",
26
- "test:watch": "vitest",
27
- "test:coverage": "vitest run --coverage",
28
- "prepublishOnly": "bun run build",
29
- "release": "bun run build && npm publish --access public"
30
- },
31
- "keywords": [
32
- "react",
33
- "hook",
34
- "inspector",
35
- "iframe"
36
- ],
37
- "peerDependencies": {
38
- "react": ">=18.0.0",
39
- "react-dom": ">=18.0.0",
40
- "vite": ">=5.0.0"
41
- },
42
- "peerDependenciesMeta": {
43
- "vite": {
44
- "optional": true
45
- }
46
- },
47
- "devDependencies": {
48
- "@promakeai/inspector-types": "1.0.2",
49
- "vitest": "^1.0.0"
50
- },
51
- "publishConfig": {
52
- "access": "public"
53
- }
54
- }
1
+ {
2
+ "name": "@promakeai/inspector-hook",
3
+ "version": "1.2.1",
4
+ "description": "React hook for controlling inspector in parent applications",
5
+ "author": "Promake",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc --build",
22
+ "dev": "tsc --watch",
23
+ "clean": "rm -rf dist",
24
+ "lint": "tsc --build --force --dry",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:coverage": "vitest run --coverage",
28
+ "prepublishOnly": "bun run build",
29
+ "release": "bun run build && npm publish --access public"
30
+ },
31
+ "keywords": [
32
+ "react",
33
+ "hook",
34
+ "inspector",
35
+ "iframe"
36
+ ],
37
+ "peerDependencies": {
38
+ "react": ">=18.0.0",
39
+ "react-dom": ">=18.0.0",
40
+ "vite": ">=5.0.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "vite": {
44
+ "optional": true
45
+ }
46
+ },
47
+ "devDependencies": {
48
+ "@promakeai/inspector-types": "1.0.2",
49
+ "vitest": "^1.0.0"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }