@promakeai/inspector-hook 1.1.0 → 1.2.0

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/README.md CHANGED
@@ -520,10 +520,9 @@ const inspector = useInspector(iframeRef, {
520
520
 
521
521
  ```typescript
522
522
  interface UrlChangeData {
523
- url: string;
524
- pathname: string;
525
- search: string;
526
- hash: string;
523
+ oldUrl: string;
524
+ newUrl: string;
525
+ timestamp: number;
527
526
  }
528
527
  ```
529
528
 
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,CAkFvB;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,CA4FvB;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"}
@@ -392,3 +392,504 @@ export function updateJSXSource(options) {
392
392
  };
393
393
  }
394
394
  }
395
+ /**
396
+ * Find the full JSX element boundaries (from opening tag to closing tag)
397
+ */
398
+ function findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName) {
399
+ // First find the opening tag
400
+ const tagInfo = findJSXTagAtPosition(sourceCode, lineNumber, columnNumber, tagName);
401
+ if (!tagInfo) {
402
+ return null;
403
+ }
404
+ // Check if self-closing
405
+ if (tagInfo.content.trim().endsWith("/>")) {
406
+ return {
407
+ start: tagInfo.start,
408
+ end: tagInfo.end,
409
+ content: tagInfo.content,
410
+ isSelfClosing: true,
411
+ };
412
+ }
413
+ // Find the matching closing tag
414
+ const afterOpening = sourceCode.slice(tagInfo.end);
415
+ let depth = 1;
416
+ let i = 0;
417
+ let inString = false;
418
+ let stringChar = "";
419
+ while (i < afterOpening.length && depth > 0) {
420
+ const char = afterOpening[i];
421
+ const prevChar = i > 0 ? afterOpening[i - 1] : "";
422
+ // Handle strings
423
+ if ((char === '"' || char === "'" || char === "`") && prevChar !== "\\") {
424
+ if (!inString) {
425
+ inString = true;
426
+ stringChar = char;
427
+ }
428
+ else if (char === stringChar) {
429
+ inString = false;
430
+ stringChar = "";
431
+ }
432
+ }
433
+ if (!inString && char === "<") {
434
+ // Check for opening or closing tag
435
+ const remainingCode = afterOpening.slice(i);
436
+ const tagMatch = remainingCode.match(/^<\/?([A-Za-z_$][A-Za-z0-9_$]*)/);
437
+ if (tagMatch) {
438
+ const isClosing = remainingCode.startsWith("</");
439
+ const foundTag = tagMatch[1];
440
+ if (isClosing && foundTag === tagInfo.foundTagName) {
441
+ depth--;
442
+ if (depth === 0) {
443
+ // Find end of closing tag
444
+ const closingEnd = remainingCode.indexOf(">");
445
+ if (closingEnd !== -1) {
446
+ const fullEnd = tagInfo.end + i + closingEnd + 1;
447
+ return {
448
+ start: tagInfo.start,
449
+ end: fullEnd,
450
+ content: sourceCode.slice(tagInfo.start, fullEnd),
451
+ isSelfClosing: false,
452
+ };
453
+ }
454
+ }
455
+ }
456
+ else if (!isClosing && foundTag === tagInfo.foundTagName) {
457
+ // Check if self-closing
458
+ const tagEndMatch = remainingCode.match(/^<[^>]+?(\/?)>/);
459
+ if (tagEndMatch && !tagEndMatch[1]) {
460
+ depth++;
461
+ }
462
+ }
463
+ }
464
+ }
465
+ i++;
466
+ }
467
+ return null;
468
+ }
469
+ /**
470
+ * Delete a JSX element from source code
471
+ *
472
+ * @param options - Configuration options for deletion
473
+ * @returns Result object with success status, updated code, and optional message
474
+ */
475
+ export function deleteJSXElement(options) {
476
+ const { sourceCode, lineNumber, columnNumber, tagName } = options;
477
+ try {
478
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
479
+ if (!elementBounds) {
480
+ return {
481
+ success: false,
482
+ code: sourceCode,
483
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
484
+ };
485
+ }
486
+ // Get content before and after the element
487
+ let before = sourceCode.slice(0, elementBounds.start);
488
+ let after = sourceCode.slice(elementBounds.end);
489
+ // Clean up whitespace - remove trailing whitespace from before and leading newline from after
490
+ // This prevents leaving empty lines
491
+ const trailingWhitespaceMatch = before.match(/(\s*)$/);
492
+ const leadingWhitespaceMatch = after.match(/^(\s*\n)?/);
493
+ if (trailingWhitespaceMatch && leadingWhitespaceMatch) {
494
+ // If there's a newline after, remove the trailing whitespace before
495
+ if (leadingWhitespaceMatch[0].includes("\n")) {
496
+ before = before.replace(/[ \t]*$/, "");
497
+ after = after.replace(/^[ \t]*\n/, "\n");
498
+ }
499
+ }
500
+ const updatedCode = before + after;
501
+ return {
502
+ success: true,
503
+ code: updatedCode,
504
+ message: `JSX element <${tagName}> deleted successfully`,
505
+ };
506
+ }
507
+ catch (error) {
508
+ return {
509
+ success: false,
510
+ code: sourceCode,
511
+ message: error instanceof Error
512
+ ? error.message
513
+ : "Unknown error occurred during JSX deletion",
514
+ };
515
+ }
516
+ }
517
+ /**
518
+ * Duplicate a JSX element in source code (inserts copy after the original)
519
+ *
520
+ * @param options - Configuration options for duplication
521
+ * @returns Result object with success status, updated code, and optional message
522
+ */
523
+ export function duplicateJSXElement(options) {
524
+ const { sourceCode, lineNumber, columnNumber, tagName } = options;
525
+ try {
526
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
527
+ if (!elementBounds) {
528
+ return {
529
+ success: false,
530
+ code: sourceCode,
531
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
532
+ };
533
+ }
534
+ // Get the indentation of the original element
535
+ const beforeElement = sourceCode.slice(0, elementBounds.start);
536
+ const lastNewline = beforeElement.lastIndexOf("\n");
537
+ const indentation = lastNewline !== -1
538
+ ? beforeElement.slice(lastNewline + 1).match(/^(\s*)/)?.[1] || ""
539
+ : "";
540
+ // Create the duplicated element with proper indentation
541
+ const duplicatedElement = "\n" + indentation + elementBounds.content;
542
+ // Insert the duplicate after the original
543
+ const updatedCode = sourceCode.slice(0, elementBounds.end) +
544
+ duplicatedElement +
545
+ sourceCode.slice(elementBounds.end);
546
+ return {
547
+ success: true,
548
+ code: updatedCode,
549
+ message: `JSX element <${tagName}> duplicated successfully`,
550
+ };
551
+ }
552
+ catch (error) {
553
+ return {
554
+ success: false,
555
+ code: sourceCode,
556
+ message: error instanceof Error
557
+ ? error.message
558
+ : "Unknown error occurred during JSX duplication",
559
+ };
560
+ }
561
+ }
562
+ /**
563
+ * Check if element content is pure text (no JS expressions or nested elements)
564
+ */
565
+ function isPureTextContent(content) {
566
+ // Check for JSX expressions {something}
567
+ if (content.includes("{") && content.includes("}")) {
568
+ return false;
569
+ }
570
+ // Check for nested elements <something>
571
+ if (/<[A-Za-z_$][A-Za-z0-9_$]*/.test(content)) {
572
+ return false;
573
+ }
574
+ return true;
575
+ }
576
+ /**
577
+ * Update the text content of a JSX element
578
+ * Only works if the element contains pure text (no JS expressions or nested elements)
579
+ *
580
+ * @param options - Configuration options for the text update
581
+ * @returns Result object with success status, updated code, and optional message
582
+ */
583
+ export function updateTextContent(options) {
584
+ const { sourceCode, lineNumber, columnNumber, tagName, newText } = options;
585
+ try {
586
+ // Find the full element boundaries
587
+ const elementBounds = findFullJSXElementBoundaries(sourceCode, lineNumber, columnNumber, tagName);
588
+ if (!elementBounds) {
589
+ return {
590
+ success: false,
591
+ code: sourceCode,
592
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
593
+ };
594
+ }
595
+ // Self-closing elements can't have text content
596
+ if (elementBounds.isSelfClosing) {
597
+ return {
598
+ success: false,
599
+ code: sourceCode,
600
+ message: `Element <${tagName}> is self-closing and cannot have text content`,
601
+ };
602
+ }
603
+ // Find the opening tag end and closing tag start
604
+ const tagInfo = findJSXTagAtPosition(sourceCode, lineNumber, columnNumber, tagName);
605
+ if (!tagInfo) {
606
+ return {
607
+ success: false,
608
+ code: sourceCode,
609
+ message: `Could not find opening tag for <${tagName}>`,
610
+ };
611
+ }
612
+ // Extract content between opening and closing tag
613
+ const closingTagPattern = new RegExp(`</${tagName}\\s*>`);
614
+ const afterOpening = sourceCode.slice(tagInfo.end);
615
+ const closingMatch = closingTagPattern.exec(afterOpening);
616
+ if (!closingMatch) {
617
+ return {
618
+ success: false,
619
+ code: sourceCode,
620
+ message: `Could not find closing tag for <${tagName}>`,
621
+ };
622
+ }
623
+ const contentStart = tagInfo.end;
624
+ const contentEnd = tagInfo.end + closingMatch.index;
625
+ const currentContent = sourceCode.slice(contentStart, contentEnd);
626
+ // Check if content is pure text
627
+ if (!isPureTextContent(currentContent)) {
628
+ return {
629
+ success: false,
630
+ code: sourceCode,
631
+ message: `Element <${tagName}> contains JS expressions or nested elements. Text update skipped.`,
632
+ };
633
+ }
634
+ // Replace the content
635
+ const updatedCode = sourceCode.slice(0, contentStart) +
636
+ newText +
637
+ sourceCode.slice(contentEnd);
638
+ return {
639
+ success: true,
640
+ code: updatedCode,
641
+ message: `Text content of <${tagName}> updated successfully`,
642
+ };
643
+ }
644
+ catch (error) {
645
+ return {
646
+ success: false,
647
+ code: sourceCode,
648
+ message: error instanceof Error
649
+ ? error.message
650
+ : "Unknown error occurred during text update",
651
+ };
652
+ }
653
+ }
654
+ /**
655
+ * Group changes by element ID
656
+ */
657
+ function groupChangesByElement(changes) {
658
+ const grouped = new Map();
659
+ for (const change of changes) {
660
+ const elementId = change.element.id || change.element.selector || "unknown";
661
+ const existing = grouped.get(elementId) || [];
662
+ existing.push(change);
663
+ grouped.set(elementId, existing);
664
+ }
665
+ return grouped;
666
+ }
667
+ /**
668
+ * Merge multiple style changes into a single styles object
669
+ */
670
+ function mergeStyleChanges(changes) {
671
+ const mergedStyles = {};
672
+ for (const change of changes) {
673
+ if (change.type === "singleStyle" && change.singleStyle) {
674
+ const { property, currentValue } = change.singleStyle;
675
+ mergedStyles[property] = currentValue;
676
+ }
677
+ else if (change.type === "style" && change.style?.current) {
678
+ // Merge all current styles
679
+ const currentStyles = change.style.current;
680
+ for (const [key, value] of Object.entries(currentStyles)) {
681
+ if (value !== undefined && value !== "") {
682
+ mergedStyles[key] = value;
683
+ }
684
+ }
685
+ }
686
+ }
687
+ return mergedStyles;
688
+ }
689
+ /**
690
+ * Apply a collection of InspectorChange objects to JSX source code
691
+ *
692
+ * This function processes changes in a logical order:
693
+ * 1. Groups changes by element
694
+ * 2. For each element:
695
+ * - If delete: deletes the element, skips other changes for that element
696
+ * - If duplicate: applies styles/text first, then duplicates
697
+ * - Merges all style changes into single update
698
+ * - Applies text changes (only if pure text)
699
+ *
700
+ * @param options - Source code and changes to apply
701
+ * @returns Result with updated code and detailed results for each change
702
+ *
703
+ * @example
704
+ * ```typescript
705
+ * const result = applyChangesToJSXSource({
706
+ * sourceCode: '<div>Hello</div>',
707
+ * changes: [
708
+ * { type: 'singleStyle', singleStyle: { property: 'color', currentValue: 'red' }, ... },
709
+ * { type: 'text', text: { current: 'World' }, ... }
710
+ * ]
711
+ * });
712
+ * ```
713
+ */
714
+ export function applyChangesToJSXSource(options) {
715
+ const { sourceCode, changes } = options;
716
+ const results = [];
717
+ let currentCode = sourceCode;
718
+ let successful = 0;
719
+ let failed = 0;
720
+ let skipped = 0;
721
+ // Group changes by element
722
+ const groupedChanges = groupChangesByElement(changes);
723
+ // Process each element's changes
724
+ for (const [elementId, elementChanges] of groupedChanges) {
725
+ // Check if there's a delete change for this element
726
+ const deleteChange = elementChanges.find((c) => c.type === "delete");
727
+ if (deleteChange) {
728
+ // If delete exists, delete the element and skip all other changes for this element
729
+ const element = deleteChange.element;
730
+ if (element.lineNumber && element.columnNumber !== undefined) {
731
+ const deleteResult = deleteJSXElement({
732
+ sourceCode: currentCode,
733
+ lineNumber: element.lineNumber,
734
+ columnNumber: element.columnNumber,
735
+ tagName: element.tagName,
736
+ });
737
+ results.push({
738
+ changeId: deleteChange.id,
739
+ type: "delete",
740
+ success: deleteResult.success,
741
+ message: deleteResult.message,
742
+ });
743
+ if (deleteResult.success) {
744
+ currentCode = deleteResult.code;
745
+ successful++;
746
+ }
747
+ else {
748
+ failed++;
749
+ }
750
+ // Skip other changes for this element
751
+ for (const change of elementChanges) {
752
+ if (change.id !== deleteChange.id) {
753
+ results.push({
754
+ changeId: change.id,
755
+ type: change.type,
756
+ success: false,
757
+ message: "Skipped: element was deleted",
758
+ });
759
+ skipped++;
760
+ }
761
+ }
762
+ continue;
763
+ }
764
+ else {
765
+ results.push({
766
+ changeId: deleteChange.id,
767
+ type: "delete",
768
+ success: false,
769
+ message: "Missing line/column number for delete operation",
770
+ });
771
+ failed++;
772
+ }
773
+ }
774
+ // Separate changes by type
775
+ const styleChanges = elementChanges.filter((c) => c.type === "singleStyle" || c.type === "style");
776
+ const textChanges = elementChanges.filter((c) => c.type === "text");
777
+ const duplicateChanges = elementChanges.filter((c) => c.type === "duplicate");
778
+ // Get element position from first change that has it
779
+ const firstChange = elementChanges[0];
780
+ const element = firstChange.element;
781
+ if (!element.lineNumber || element.columnNumber === undefined) {
782
+ // Can't process without position
783
+ for (const change of elementChanges) {
784
+ if (change.type !== "delete") {
785
+ results.push({
786
+ changeId: change.id,
787
+ type: change.type,
788
+ success: false,
789
+ message: "Missing line/column number",
790
+ });
791
+ failed++;
792
+ }
793
+ }
794
+ continue;
795
+ }
796
+ // Apply style changes (merged)
797
+ if (styleChanges.length > 0) {
798
+ const mergedStyles = mergeStyleChanges(styleChanges);
799
+ if (Object.keys(mergedStyles).length > 0) {
800
+ const styleResult = updateJSXSource({
801
+ sourceCode: currentCode,
802
+ lineNumber: element.lineNumber,
803
+ columnNumber: element.columnNumber,
804
+ tagName: element.tagName,
805
+ styles: mergedStyles,
806
+ });
807
+ // Record result for each style change
808
+ for (const change of styleChanges) {
809
+ results.push({
810
+ changeId: change.id,
811
+ type: change.type,
812
+ success: styleResult.success,
813
+ message: styleResult.success
814
+ ? "Style applied (merged with other styles)"
815
+ : styleResult.message,
816
+ });
817
+ if (styleResult.success) {
818
+ successful++;
819
+ }
820
+ else {
821
+ failed++;
822
+ }
823
+ }
824
+ if (styleResult.success) {
825
+ currentCode = styleResult.code;
826
+ }
827
+ }
828
+ }
829
+ // Apply text changes
830
+ for (const change of textChanges) {
831
+ if (change.text?.current) {
832
+ const textResult = updateTextContent({
833
+ sourceCode: currentCode,
834
+ lineNumber: element.lineNumber,
835
+ columnNumber: element.columnNumber,
836
+ tagName: element.tagName,
837
+ newText: change.text.current,
838
+ });
839
+ results.push({
840
+ changeId: change.id,
841
+ type: "text",
842
+ success: textResult.success,
843
+ message: textResult.message,
844
+ });
845
+ if (textResult.success) {
846
+ currentCode = textResult.code;
847
+ successful++;
848
+ }
849
+ else {
850
+ // Check if it was skipped due to JS expressions
851
+ if (textResult.message?.includes("JS expressions")) {
852
+ skipped++;
853
+ }
854
+ else {
855
+ failed++;
856
+ }
857
+ }
858
+ }
859
+ }
860
+ // Apply duplicate changes
861
+ for (const change of duplicateChanges) {
862
+ const dupResult = duplicateJSXElement({
863
+ sourceCode: currentCode,
864
+ lineNumber: element.lineNumber,
865
+ columnNumber: element.columnNumber,
866
+ tagName: element.tagName,
867
+ });
868
+ results.push({
869
+ changeId: change.id,
870
+ type: "duplicate",
871
+ success: dupResult.success,
872
+ message: dupResult.message,
873
+ });
874
+ if (dupResult.success) {
875
+ currentCode = dupResult.code;
876
+ successful++;
877
+ }
878
+ else {
879
+ failed++;
880
+ }
881
+ }
882
+ }
883
+ const allSuccessful = failed === 0 && skipped === 0;
884
+ return {
885
+ success: allSuccessful,
886
+ code: currentCode,
887
+ results,
888
+ summary: {
889
+ total: changes.length,
890
+ successful,
891
+ failed,
892
+ skipped,
893
+ },
894
+ };
895
+ }
package/package.json CHANGED
@@ -1,53 +1,54 @@
1
- {
2
- "name": "@promakeai/inspector-hook",
3
- "version": "1.1.0",
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
- },
30
- "keywords": [
31
- "react",
32
- "hook",
33
- "inspector",
34
- "iframe"
35
- ],
36
- "peerDependencies": {
37
- "react": ">=18.0.0",
38
- "react-dom": ">=18.0.0",
39
- "vite": ">=5.0.0"
40
- },
41
- "peerDependenciesMeta": {
42
- "vite": {
43
- "optional": true
44
- }
45
- },
46
- "devDependencies": {
47
- "@promakeai/inspector-types": "^1.0.1",
48
- "vitest": "^1.0.0"
49
- },
50
- "publishConfig": {
51
- "access": "public"
52
- }
53
- }
1
+ {
2
+ "name": "@promakeai/inspector-hook",
3
+ "version": "1.2.0",
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
+ }