@promakeai/inspector-hook 1.0.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.
@@ -0,0 +1,8 @@
1
+ import { RefObject } from "react";
2
+ import type { InspectorCallbacks, InspectorLabels, InspectorTheme, UseInspectorReturn } from "@promakeai/inspector-types";
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";
7
+ export { inspectorHookPlugin } from "./vite-plugin.js";
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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"}
package/dist/index.js ADDED
@@ -0,0 +1,205 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ export function useInspector(iframeRef, callbacks, labels, theme) {
3
+ const [isInspecting, setIsInspecting] = useState(false);
4
+ /**
5
+ * Send message to iframe
6
+ */
7
+ const sendMessage = useCallback((message) => {
8
+ const iframe = iframeRef.current;
9
+ if (!iframe || !iframe.contentWindow) {
10
+ console.warn("Inspector: iframe not found or not loaded");
11
+ return;
12
+ }
13
+ try {
14
+ iframe.contentWindow.postMessage(message, "*");
15
+ }
16
+ catch (error) {
17
+ console.error("Inspector: Failed to send message:", error);
18
+ }
19
+ }, [iframeRef]);
20
+ /**
21
+ * Toggle inspector mode
22
+ */
23
+ const toggleInspector = useCallback((active) => {
24
+ const newState = active !== undefined ? active : !isInspecting;
25
+ setIsInspecting(newState);
26
+ sendMessage({
27
+ type: "TOGGLE_INSPECTOR",
28
+ active: newState,
29
+ labels: labels,
30
+ theme: theme,
31
+ });
32
+ }, [isInspecting, sendMessage, labels, theme]);
33
+ /**
34
+ * Start inspecting
35
+ */
36
+ const startInspecting = useCallback(() => {
37
+ toggleInspector(true);
38
+ }, [toggleInspector]);
39
+ /**
40
+ * Stop inspecting
41
+ */
42
+ const stopInspecting = useCallback(() => {
43
+ toggleInspector(false);
44
+ }, [toggleInspector]);
45
+ /**
46
+ * Show or hide content input
47
+ */
48
+ const showContentInput = useCallback((show, updateImmediately) => {
49
+ sendMessage({
50
+ type: "SHOW_CONTENT_INPUT",
51
+ show: show,
52
+ updateImmediately: updateImmediately,
53
+ });
54
+ }, [sendMessage]);
55
+ /**
56
+ * Show or hide image input
57
+ */
58
+ const showImageInput = useCallback((show, updateImmediately) => {
59
+ sendMessage({
60
+ type: "SHOW_IMAGE_INPUT",
61
+ show: show,
62
+ updateImmediately: updateImmediately,
63
+ });
64
+ }, [sendMessage]);
65
+ /**
66
+ * Show or hide style editor
67
+ */
68
+ const showStyleEditor = useCallback((show) => {
69
+ sendMessage({
70
+ type: "SHOW_STYLE_EDITOR",
71
+ show: show,
72
+ });
73
+ }, [sendMessage]);
74
+ /**
75
+ * Show or hide "Built with Promake" badge
76
+ */
77
+ const setBadgeVisible = useCallback((visible) => {
78
+ sendMessage({
79
+ type: "SET_BADGE_VISIBLE",
80
+ visible: visible,
81
+ badgeText: labels?.badgeText,
82
+ });
83
+ }, [sendMessage, labels]);
84
+ /**
85
+ * Highlight an element in the iframe
86
+ */
87
+ const highlightElement = useCallback((identifier, options) => {
88
+ sendMessage({
89
+ type: "HIGHLIGHT_ELEMENT",
90
+ identifier: identifier,
91
+ options: options,
92
+ });
93
+ }, [sendMessage]);
94
+ /**
95
+ * Request element info by inspector ID
96
+ */
97
+ const getElementByInspectorId = useCallback((inspectorId) => {
98
+ sendMessage({
99
+ type: "GET_ELEMENT_BY_ID",
100
+ inspectorId: inspectorId,
101
+ });
102
+ }, [sendMessage]);
103
+ /**
104
+ * Toggle child borders visibility
105
+ */
106
+ const setShowChildBorders = useCallback((show) => {
107
+ sendMessage({
108
+ type: "SET_SHOW_CHILD_BORDERS",
109
+ show: show,
110
+ });
111
+ }, [sendMessage]);
112
+ /**
113
+ * Listen for messages from iframe
114
+ */
115
+ useEffect(() => {
116
+ const handleMessage = (event) => {
117
+ // Parse message if it's a string (JSON stringified)
118
+ let messageData;
119
+ if (typeof event.data === "string") {
120
+ try {
121
+ messageData = JSON.parse(event.data);
122
+ }
123
+ catch {
124
+ return; // Invalid JSON, ignore
125
+ }
126
+ }
127
+ else {
128
+ messageData = event.data;
129
+ }
130
+ // Security: Only handle expected message types
131
+ if (!messageData || typeof messageData.type !== "string") {
132
+ return;
133
+ }
134
+ switch (messageData.type) {
135
+ case "INSPECTOR_ELEMENT_SELECTED":
136
+ callbacks?.onElementSelected?.(messageData.data);
137
+ break;
138
+ case "URL_CHANGED":
139
+ callbacks?.onUrlChange?.(messageData.data);
140
+ break;
141
+ case "INSPECTOR_PROMPT_SUBMITTED":
142
+ callbacks?.onPromptSubmitted?.(messageData.data);
143
+ break;
144
+ case "INSPECTOR_TEXT_UPDATED":
145
+ callbacks?.onTextUpdated?.(messageData.data);
146
+ break;
147
+ case "INSPECTOR_IMAGE_UPDATED":
148
+ callbacks?.onImageUpdated?.(messageData.data);
149
+ break;
150
+ case "INSPECTOR_STYLE_UPDATED":
151
+ callbacks?.onStyleUpdated?.(messageData.data);
152
+ break;
153
+ case "INSPECTOR_CLOSED":
154
+ setIsInspecting(false);
155
+ callbacks?.onInspectorClosed?.();
156
+ break;
157
+ case "INSPECTOR_ERROR":
158
+ callbacks?.onError?.(messageData.data);
159
+ break;
160
+ case "ELEMENT_INFO_RESPONSE":
161
+ callbacks?.onElementInfoReceived?.(messageData.data);
162
+ break;
163
+ default:
164
+ // Unknown message type - ignore
165
+ break;
166
+ }
167
+ };
168
+ window.addEventListener("message", handleMessage);
169
+ return () => {
170
+ window.removeEventListener("message", handleMessage);
171
+ };
172
+ }, [callbacks]);
173
+ /**
174
+ * Cleanup: Turn off inspector on unmount
175
+ */
176
+ useEffect(() => {
177
+ return () => {
178
+ if (isInspecting) {
179
+ sendMessage({
180
+ type: "TOGGLE_INSPECTOR",
181
+ active: false,
182
+ labels: labels,
183
+ theme: theme,
184
+ });
185
+ }
186
+ };
187
+ }, [isInspecting, sendMessage]);
188
+ return {
189
+ isInspecting,
190
+ toggleInspector,
191
+ startInspecting,
192
+ stopInspecting,
193
+ showContentInput,
194
+ showImageInput,
195
+ showStyleEditor,
196
+ setBadgeVisible,
197
+ highlightElement,
198
+ getElementByInspectorId,
199
+ setShowChildBorders,
200
+ };
201
+ }
202
+ // Export utility functions
203
+ export { updateJSXSource } from "./utils/jsxUpdater.js";
204
+ // Export Vite plugin
205
+ export { inspectorHookPlugin } from "./vite-plugin.js";
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Server-only utilities for @promakeai/inspector-hook
3
+ * These functions require Node.js environment and should not be used in browser
4
+ */
5
+ export { updateJSXSource } from "./utils/jsxUpdater";
6
+ export type { UpdateJSXSourceOptions, UpdateJSXSourceResult } from "./utils/jsxUpdater";
7
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,YAAY,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC"}
package/dist/server.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Server-only utilities for @promakeai/inspector-hook
3
+ * These functions require Node.js environment and should not be used in browser
4
+ */
5
+ export { updateJSXSource } from "./utils/jsxUpdater";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * JSX Source Code Updater
3
+ * AST-based utility for updating styles and classNames in JSX/TSX source code
4
+ */
5
+ export interface UpdateJSXSourceOptions {
6
+ sourceCode: string;
7
+ lineNumber: number;
8
+ columnNumber: number;
9
+ tagName: string;
10
+ styles?: Record<string, string>;
11
+ className?: string;
12
+ }
13
+ export interface UpdateJSXSourceResult {
14
+ success: boolean;
15
+ code: string;
16
+ message?: string;
17
+ }
18
+ /**
19
+ * Update JSX source code with new styles and/or className
20
+ *
21
+ * @param options - Configuration options for the update
22
+ * @returns Result object with success status, updated code, and optional message
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const result = updateJSXSource({
27
+ * sourceCode: '<div className="foo">Hello</div>',
28
+ * lineNumber: 1,
29
+ * columnNumber: 0,
30
+ * tagName: 'div',
31
+ * styles: { backgroundColor: 'red !important' },
32
+ * className: 'bar'
33
+ * });
34
+ * // result.code: '<div className="foo bar" style={{ backgroundColor: "red !important" }}>Hello</div>'
35
+ * ```
36
+ */
37
+ export declare function updateJSXSource(options: UpdateJSXSourceOptions): UpdateJSXSourceResult;
38
+ //# sourceMappingURL=jsxUpdater.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsxUpdater.d.ts","sourceRoot":"","sources":["../../src/utils/jsxUpdater.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH,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;AAuKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,qBAAqB,CA+DtF"}
@@ -0,0 +1,193 @@
1
+ /**
2
+ * JSX Source Code Updater
3
+ * AST-based utility for updating styles and classNames in JSX/TSX source code
4
+ */
5
+ import * as parser from '@babel/parser';
6
+ import traverse from '@babel/traverse';
7
+ import generate from '@babel/generator';
8
+ import * as t from '@babel/types';
9
+ /**
10
+ * Create AST ObjectExpression from styles object
11
+ */
12
+ function createStyleObjectExpression(styles) {
13
+ return t.objectExpression(Object.entries(styles).map(([key, value]) => t.objectProperty(t.identifier(key), t.stringLiteral(value))));
14
+ }
15
+ /**
16
+ * Find JSX element at specific line and column position
17
+ */
18
+ function findJSXElementAtPosition(ast, targetLine, targetColumn) {
19
+ let foundElement = null;
20
+ traverse(ast, {
21
+ JSXOpeningElement(path) {
22
+ const { loc } = path.node;
23
+ if (loc &&
24
+ loc.start.line === targetLine &&
25
+ loc.start.column === targetColumn) {
26
+ foundElement = path;
27
+ path.stop();
28
+ }
29
+ },
30
+ });
31
+ return foundElement;
32
+ }
33
+ /**
34
+ * Merge new styles with existing style prop
35
+ */
36
+ function mergeStyleProp(jsxOpeningElement, newStyles) {
37
+ const attributes = jsxOpeningElement.node.attributes;
38
+ // Find existing style attribute
39
+ const styleAttrIndex = attributes.findIndex((attr) => t.isJSXAttribute(attr) &&
40
+ t.isJSXIdentifier(attr.name) &&
41
+ attr.name.name === 'style');
42
+ const newStyleObjectExpression = createStyleObjectExpression(newStyles);
43
+ if (styleAttrIndex !== -1) {
44
+ // Update existing style attribute
45
+ const styleAttr = attributes[styleAttrIndex];
46
+ if (t.isJSXExpressionContainer(styleAttr.value) &&
47
+ t.isObjectExpression(styleAttr.value.expression)) {
48
+ // Merge with existing styles
49
+ const existingProps = styleAttr.value.expression.properties;
50
+ const newProps = newStyleObjectExpression.properties;
51
+ // Create map of new style keys
52
+ const newStyleKeys = new Set(Object.keys(newStyles));
53
+ // Filter out existing properties that will be replaced
54
+ const filteredExisting = existingProps.filter((prop) => !t.isObjectProperty(prop) ||
55
+ !t.isIdentifier(prop.key) ||
56
+ !newStyleKeys.has(prop.key.name));
57
+ // Combine: keep non-updated existing properties + add new properties
58
+ styleAttr.value.expression.properties = [
59
+ ...filteredExisting,
60
+ ...newProps,
61
+ ];
62
+ }
63
+ else {
64
+ // Replace entirely if existing value is not a simple object
65
+ styleAttr.value = t.jsxExpressionContainer(newStyleObjectExpression);
66
+ }
67
+ }
68
+ else {
69
+ // Add new style attribute
70
+ const styleAttr = t.jsxAttribute(t.jsxIdentifier('style'), t.jsxExpressionContainer(newStyleObjectExpression));
71
+ attributes.push(styleAttr);
72
+ }
73
+ }
74
+ /**
75
+ * Merge new className with existing className prop
76
+ */
77
+ function mergeClassNameProp(jsxOpeningElement, newClassName) {
78
+ const attributes = jsxOpeningElement.node.attributes;
79
+ // Find existing className attribute
80
+ const classNameAttrIndex = attributes.findIndex((attr) => t.isJSXAttribute(attr) &&
81
+ t.isJSXIdentifier(attr.name) &&
82
+ attr.name.name === 'className');
83
+ if (classNameAttrIndex !== -1) {
84
+ // Merge with existing className
85
+ const classNameAttr = attributes[classNameAttrIndex];
86
+ if (t.isStringLiteral(classNameAttr.value)) {
87
+ // Simple string className
88
+ const existingClasses = classNameAttr.value.value;
89
+ classNameAttr.value = t.stringLiteral(`${existingClasses} ${newClassName}`.trim());
90
+ }
91
+ else if (t.isJSXExpressionContainer(classNameAttr.value) &&
92
+ t.isStringLiteral(classNameAttr.value.expression)) {
93
+ // className={""} format
94
+ const existingClasses = classNameAttr.value.expression.value;
95
+ classNameAttr.value.expression = t.stringLiteral(`${existingClasses} ${newClassName}`.trim());
96
+ }
97
+ else if (t.isJSXExpressionContainer(classNameAttr.value) &&
98
+ t.isTemplateLiteral(classNameAttr.value.expression)) {
99
+ // Template literal className
100
+ const templateLiteral = classNameAttr.value.expression;
101
+ const lastQuasi = templateLiteral.quasis[templateLiteral.quasis.length - 1];
102
+ lastQuasi.value.raw += ` ${newClassName}`;
103
+ lastQuasi.value.cooked = lastQuasi.value.raw;
104
+ }
105
+ else {
106
+ // Complex expression - wrap in template literal
107
+ classNameAttr.value = t.jsxExpressionContainer(t.templateLiteral([
108
+ t.templateElement({ raw: '', cooked: '' }, false),
109
+ t.templateElement({ raw: ` ${newClassName}`, cooked: ` ${newClassName}` }, true),
110
+ ], [classNameAttr.value.expression]));
111
+ }
112
+ }
113
+ else {
114
+ // Add new className attribute
115
+ const classNameAttr = t.jsxAttribute(t.jsxIdentifier('className'), t.stringLiteral(newClassName));
116
+ attributes.push(classNameAttr);
117
+ }
118
+ }
119
+ /**
120
+ * Update JSX source code with new styles and/or className
121
+ *
122
+ * @param options - Configuration options for the update
123
+ * @returns Result object with success status, updated code, and optional message
124
+ *
125
+ * @example
126
+ * ```typescript
127
+ * const result = updateJSXSource({
128
+ * sourceCode: '<div className="foo">Hello</div>',
129
+ * lineNumber: 1,
130
+ * columnNumber: 0,
131
+ * tagName: 'div',
132
+ * styles: { backgroundColor: 'red !important' },
133
+ * className: 'bar'
134
+ * });
135
+ * // result.code: '<div className="foo bar" style={{ backgroundColor: "red !important" }}>Hello</div>'
136
+ * ```
137
+ */
138
+ export function updateJSXSource(options) {
139
+ const { sourceCode, lineNumber, columnNumber, tagName, styles, className } = options;
140
+ try {
141
+ // Parse JSX/TSX source code
142
+ const ast = parser.parse(sourceCode, {
143
+ sourceType: 'module',
144
+ plugins: ['jsx', 'typescript', 'decorators-legacy'],
145
+ });
146
+ // Find the target JSX element at the specified position
147
+ const elementPath = findJSXElementAtPosition(ast, lineNumber, columnNumber);
148
+ if (!elementPath) {
149
+ return {
150
+ success: false,
151
+ code: sourceCode,
152
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
153
+ };
154
+ }
155
+ // Verify tag name matches (optional validation)
156
+ const foundTagName = t.isJSXIdentifier(elementPath.node.name)
157
+ ? elementPath.node.name.name
158
+ : '';
159
+ if (foundTagName.toLowerCase() !== tagName.toLowerCase()) {
160
+ return {
161
+ success: false,
162
+ code: sourceCode,
163
+ message: `Tag name mismatch: expected <${tagName}>, found <${foundTagName}>`,
164
+ };
165
+ }
166
+ // Apply style updates if provided
167
+ if (styles && Object.keys(styles).length > 0) {
168
+ mergeStyleProp(elementPath, styles);
169
+ }
170
+ // Apply className updates if provided
171
+ if (className && className.trim()) {
172
+ mergeClassNameProp(elementPath, className.trim());
173
+ }
174
+ // Generate updated code
175
+ const output = generate(ast, {
176
+ retainLines: true,
177
+ compact: false,
178
+ concise: false,
179
+ });
180
+ return {
181
+ success: true,
182
+ code: output.code,
183
+ message: 'JSX source updated successfully',
184
+ };
185
+ }
186
+ catch (error) {
187
+ return {
188
+ success: false,
189
+ code: sourceCode,
190
+ message: error instanceof Error ? error.message : 'Unknown error occurred during JSX update',
191
+ };
192
+ }
193
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Vite plugin for @promakeai/inspector-hook
3
+ * Configures Babel packages for browser compatibility
4
+ */
5
+ import type { Plugin } from "vite";
6
+ export declare function inspectorHookPlugin(): Plugin;
7
+ //# sourceMappingURL=vite-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../src/vite-plugin.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAEnC,wBAAgB,mBAAmB,IAAI,MAAM,CAe5C"}
@@ -0,0 +1,14 @@
1
+ export function inspectorHookPlugin() {
2
+ return {
3
+ name: "inspector-hook",
4
+ config() {
5
+ return {
6
+ define: {
7
+ "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV || "development"),
8
+ "process.platform": JSON.stringify("browser"),
9
+ "process.version": JSON.stringify("v18.0.0"),
10
+ },
11
+ };
12
+ },
13
+ };
14
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@promakeai/inspector-hook",
3
+ "version": "1.0.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
+ "src"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc --build",
23
+ "dev": "tsc --watch",
24
+ "clean": "rm -rf dist",
25
+ "lint": "tsc --build --force --dry",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "test:coverage": "vitest run --coverage",
29
+ "prepublishOnly": "bun run build"
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": "workspace:*",
49
+ "@types/babel__traverse": "^7.20.0",
50
+ "@types/babel__generator": "^7.6.0",
51
+ "vitest": "^1.0.0"
52
+ },
53
+ "dependencies": {
54
+ "@babel/parser": "^7.23.0",
55
+ "@babel/traverse": "^7.23.0",
56
+ "@babel/generator": "^7.23.0",
57
+ "@babel/types": "^7.23.0"
58
+ },
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/promakeai/inspector.git",
62
+ "directory": "packages/hook"
63
+ },
64
+ "publishConfig": {
65
+ "access": "public"
66
+ }
67
+ }
package/src/index.ts ADDED
@@ -0,0 +1,384 @@
1
+ import { useEffect, useState, useCallback, RefObject } from "react";
2
+ import type {
3
+ InspectorCallbacks,
4
+ InspectorLabels,
5
+ InspectorTheme,
6
+ UseInspectorReturn,
7
+ SelectedElementData,
8
+ UrlChangeData,
9
+ PromptSubmittedData,
10
+ TextUpdatedData,
11
+ ImageUpdatedData,
12
+ StyleUpdatedData,
13
+ ErrorData,
14
+ HighlightOptions,
15
+ ElementInfoData,
16
+ } from "@promakeai/inspector-types";
17
+
18
+ type IframeMessage =
19
+ | {
20
+ type: "TOGGLE_INSPECTOR";
21
+ active: boolean;
22
+ labels?: InspectorLabels;
23
+ theme?: InspectorTheme;
24
+ }
25
+ | {
26
+ type: "SHOW_CONTENT_INPUT";
27
+ show: boolean;
28
+ updateImmediately?: boolean;
29
+ }
30
+ | {
31
+ type: "SHOW_IMAGE_INPUT";
32
+ show: boolean;
33
+ updateImmediately?: boolean;
34
+ }
35
+ | {
36
+ type: "SHOW_STYLE_EDITOR";
37
+ show: boolean;
38
+ }
39
+ | {
40
+ type: "SET_BADGE_VISIBLE";
41
+ visible: boolean;
42
+ badgeText?: string;
43
+ }
44
+ | {
45
+ type: "HIGHLIGHT_ELEMENT";
46
+ identifier: string | SelectedElementData;
47
+ options?: HighlightOptions;
48
+ }
49
+ | {
50
+ type: "GET_ELEMENT_BY_ID";
51
+ inspectorId: string;
52
+ }
53
+ | {
54
+ type: "SET_SHOW_CHILD_BORDERS";
55
+ show: boolean;
56
+ };
57
+
58
+ type InspectorMessage =
59
+ | {
60
+ type: "INSPECTOR_ELEMENT_SELECTED";
61
+ data: SelectedElementData;
62
+ }
63
+ | {
64
+ type: "URL_CHANGED";
65
+ data: UrlChangeData;
66
+ }
67
+ | {
68
+ type: "INSPECTOR_PROMPT_SUBMITTED";
69
+ data: PromptSubmittedData;
70
+ }
71
+ | {
72
+ type: "INSPECTOR_TEXT_UPDATED";
73
+ data: TextUpdatedData;
74
+ }
75
+ | {
76
+ type: "INSPECTOR_IMAGE_UPDATED";
77
+ data: ImageUpdatedData;
78
+ }
79
+ | {
80
+ type: "INSPECTOR_STYLE_UPDATED";
81
+ data: StyleUpdatedData;
82
+ }
83
+ | {
84
+ type: "INSPECTOR_CLOSED";
85
+ }
86
+ | {
87
+ type: "INSPECTOR_ERROR";
88
+ data: ErrorData;
89
+ }
90
+ | {
91
+ type: "ELEMENT_INFO_RESPONSE";
92
+ data: ElementInfoData;
93
+ };
94
+
95
+ export function useInspector(
96
+ iframeRef: RefObject<HTMLIFrameElement>,
97
+ callbacks?: InspectorCallbacks,
98
+ labels?: InspectorLabels,
99
+ theme?: InspectorTheme
100
+ ): UseInspectorReturn {
101
+ const [isInspecting, setIsInspecting] = useState(false);
102
+
103
+ /**
104
+ * Send message to iframe
105
+ */
106
+ const sendMessage = useCallback(
107
+ (message: IframeMessage) => {
108
+ const iframe = iframeRef.current;
109
+ if (!iframe || !iframe.contentWindow) {
110
+ console.warn("Inspector: iframe not found or not loaded");
111
+ return;
112
+ }
113
+
114
+ try {
115
+ iframe.contentWindow.postMessage(message, "*");
116
+ } catch (error) {
117
+ console.error("Inspector: Failed to send message:", error);
118
+ }
119
+ },
120
+ [iframeRef]
121
+ );
122
+
123
+ /**
124
+ * Toggle inspector mode
125
+ */
126
+ const toggleInspector = useCallback(
127
+ (active?: boolean) => {
128
+ const newState = active !== undefined ? active : !isInspecting;
129
+ setIsInspecting(newState);
130
+
131
+ sendMessage({
132
+ type: "TOGGLE_INSPECTOR",
133
+ active: newState,
134
+ labels: labels,
135
+ theme: theme,
136
+ });
137
+ },
138
+ [isInspecting, sendMessage, labels, theme]
139
+ );
140
+
141
+ /**
142
+ * Start inspecting
143
+ */
144
+ const startInspecting = useCallback(() => {
145
+ toggleInspector(true);
146
+ }, [toggleInspector]);
147
+
148
+ /**
149
+ * Stop inspecting
150
+ */
151
+ const stopInspecting = useCallback(() => {
152
+ toggleInspector(false);
153
+ }, [toggleInspector]);
154
+
155
+ /**
156
+ * Show or hide content input
157
+ */
158
+ const showContentInput = useCallback(
159
+ (show: boolean, updateImmediately?: boolean) => {
160
+ sendMessage({
161
+ type: "SHOW_CONTENT_INPUT",
162
+ show: show,
163
+ updateImmediately: updateImmediately,
164
+ });
165
+ },
166
+ [sendMessage]
167
+ );
168
+
169
+ /**
170
+ * Show or hide image input
171
+ */
172
+ const showImageInput = useCallback(
173
+ (show: boolean, updateImmediately?: boolean) => {
174
+ sendMessage({
175
+ type: "SHOW_IMAGE_INPUT",
176
+ show: show,
177
+ updateImmediately: updateImmediately,
178
+ });
179
+ },
180
+ [sendMessage]
181
+ );
182
+
183
+ /**
184
+ * Show or hide style editor
185
+ */
186
+ const showStyleEditor = useCallback(
187
+ (show: boolean) => {
188
+ sendMessage({
189
+ type: "SHOW_STYLE_EDITOR",
190
+ show: show,
191
+ });
192
+ },
193
+ [sendMessage]
194
+ );
195
+
196
+ /**
197
+ * Show or hide "Built with Promake" badge
198
+ */
199
+ const setBadgeVisible = useCallback(
200
+ (visible: boolean) => {
201
+ sendMessage({
202
+ type: "SET_BADGE_VISIBLE",
203
+ visible: visible,
204
+ badgeText: labels?.badgeText,
205
+ });
206
+ },
207
+ [sendMessage, labels]
208
+ );
209
+
210
+ /**
211
+ * Highlight an element in the iframe
212
+ */
213
+ const highlightElement = useCallback(
214
+ (identifier: string | SelectedElementData, options?: HighlightOptions) => {
215
+ sendMessage({
216
+ type: "HIGHLIGHT_ELEMENT",
217
+ identifier: identifier,
218
+ options: options,
219
+ });
220
+ },
221
+ [sendMessage]
222
+ );
223
+
224
+ /**
225
+ * Request element info by inspector ID
226
+ */
227
+ const getElementByInspectorId = useCallback(
228
+ (inspectorId: string) => {
229
+ sendMessage({
230
+ type: "GET_ELEMENT_BY_ID",
231
+ inspectorId: inspectorId,
232
+ });
233
+ },
234
+ [sendMessage]
235
+ );
236
+
237
+ /**
238
+ * Toggle child borders visibility
239
+ */
240
+ const setShowChildBorders = useCallback(
241
+ (show: boolean) => {
242
+ sendMessage({
243
+ type: "SET_SHOW_CHILD_BORDERS",
244
+ show: show,
245
+ });
246
+ },
247
+ [sendMessage]
248
+ );
249
+
250
+ /**
251
+ * Listen for messages from iframe
252
+ */
253
+ useEffect(() => {
254
+ const handleMessage = (event: MessageEvent<string | InspectorMessage>) => {
255
+ // Parse message if it's a string (JSON stringified)
256
+ let messageData: InspectorMessage;
257
+
258
+ if (typeof event.data === "string") {
259
+ try {
260
+ messageData = JSON.parse(event.data);
261
+ } catch {
262
+ return; // Invalid JSON, ignore
263
+ }
264
+ } else {
265
+ messageData = event.data;
266
+ }
267
+
268
+ // Security: Only handle expected message types
269
+ if (!messageData || typeof messageData.type !== "string") {
270
+ return;
271
+ }
272
+
273
+ switch (messageData.type) {
274
+ case "INSPECTOR_ELEMENT_SELECTED":
275
+ callbacks?.onElementSelected?.(messageData.data);
276
+ break;
277
+
278
+ case "URL_CHANGED":
279
+ callbacks?.onUrlChange?.(messageData.data);
280
+ break;
281
+
282
+ case "INSPECTOR_PROMPT_SUBMITTED":
283
+ callbacks?.onPromptSubmitted?.(messageData.data);
284
+ break;
285
+
286
+ case "INSPECTOR_TEXT_UPDATED":
287
+ callbacks?.onTextUpdated?.(messageData.data);
288
+ break;
289
+
290
+ case "INSPECTOR_IMAGE_UPDATED":
291
+ callbacks?.onImageUpdated?.(messageData.data);
292
+ break;
293
+
294
+ case "INSPECTOR_STYLE_UPDATED":
295
+ callbacks?.onStyleUpdated?.(messageData.data);
296
+ break;
297
+
298
+ case "INSPECTOR_CLOSED":
299
+ setIsInspecting(false);
300
+ callbacks?.onInspectorClosed?.();
301
+ break;
302
+
303
+ case "INSPECTOR_ERROR":
304
+ callbacks?.onError?.(messageData.data);
305
+ break;
306
+
307
+ case "ELEMENT_INFO_RESPONSE":
308
+ callbacks?.onElementInfoReceived?.(messageData.data);
309
+ break;
310
+
311
+ default:
312
+ // Unknown message type - ignore
313
+ break;
314
+ }
315
+ };
316
+
317
+ window.addEventListener("message", handleMessage);
318
+
319
+ return () => {
320
+ window.removeEventListener("message", handleMessage);
321
+ };
322
+ }, [callbacks]);
323
+
324
+ /**
325
+ * Cleanup: Turn off inspector on unmount
326
+ */
327
+ useEffect(() => {
328
+ return () => {
329
+ if (isInspecting) {
330
+ sendMessage({
331
+ type: "TOGGLE_INSPECTOR",
332
+ active: false,
333
+ labels: labels,
334
+ theme: theme,
335
+ });
336
+ }
337
+ };
338
+ }, [isInspecting, sendMessage]);
339
+
340
+ return {
341
+ isInspecting,
342
+ toggleInspector,
343
+ startInspecting,
344
+ stopInspecting,
345
+ showContentInput,
346
+ showImageInput,
347
+ showStyleEditor,
348
+ setBadgeVisible,
349
+ highlightElement,
350
+ getElementByInspectorId,
351
+ setShowChildBorders,
352
+ };
353
+ }
354
+
355
+ // Re-export types for convenience
356
+ export type {
357
+ ComponentInfo,
358
+ ElementPosition,
359
+ SelectedElementData,
360
+ UrlChangeData,
361
+ PromptSubmittedData,
362
+ TextUpdatedData,
363
+ ImageUpdatedData,
364
+ StyleChanges,
365
+ StyleUpdatedData,
366
+ ErrorData,
367
+ HighlightOptions,
368
+ ElementInfoData,
369
+ InspectorLabels,
370
+ InspectorTheme,
371
+ ContentInputRequestData,
372
+ InspectorCallbacks,
373
+ UseInspectorReturn,
374
+ } from "@promakeai/inspector-types";
375
+
376
+ // Export utility functions
377
+ export { updateJSXSource } from "./utils/jsxUpdater.js";
378
+ export type {
379
+ UpdateJSXSourceOptions,
380
+ UpdateJSXSourceResult,
381
+ } from "./utils/jsxUpdater.js";
382
+
383
+ // Export Vite plugin
384
+ export { inspectorHookPlugin } from "./vite-plugin.js";
@@ -0,0 +1,274 @@
1
+ /**
2
+ * JSX Source Code Updater
3
+ * AST-based utility for updating styles and classNames in JSX/TSX source code
4
+ */
5
+
6
+ import * as parser from '@babel/parser';
7
+ import traverse from '@babel/traverse';
8
+ import generate from '@babel/generator';
9
+ import * as t from '@babel/types';
10
+
11
+ export interface UpdateJSXSourceOptions {
12
+ sourceCode: string;
13
+ lineNumber: number;
14
+ columnNumber: number;
15
+ tagName: string;
16
+ styles?: Record<string, string>;
17
+ className?: string;
18
+ }
19
+
20
+ export interface UpdateJSXSourceResult {
21
+ success: boolean;
22
+ code: string;
23
+ message?: string;
24
+ }
25
+
26
+ /**
27
+ * Create AST ObjectExpression from styles object
28
+ */
29
+ function createStyleObjectExpression(styles: Record<string, string>): t.ObjectExpression {
30
+ return t.objectExpression(
31
+ Object.entries(styles).map(([key, value]) =>
32
+ t.objectProperty(
33
+ t.identifier(key),
34
+ t.stringLiteral(value)
35
+ )
36
+ )
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Find JSX element at specific line and column position
42
+ */
43
+ function findJSXElementAtPosition(
44
+ ast: t.File,
45
+ targetLine: number,
46
+ targetColumn: number
47
+ ): any {
48
+ let foundElement: any = null;
49
+
50
+ traverse(ast, {
51
+ JSXOpeningElement(path: any) {
52
+ const { loc } = path.node;
53
+ if (
54
+ loc &&
55
+ loc.start.line === targetLine &&
56
+ loc.start.column === targetColumn
57
+ ) {
58
+ foundElement = path;
59
+ path.stop();
60
+ }
61
+ },
62
+ });
63
+
64
+ return foundElement;
65
+ }
66
+
67
+ /**
68
+ * Merge new styles with existing style prop
69
+ */
70
+ function mergeStyleProp(
71
+ jsxOpeningElement: any,
72
+ newStyles: Record<string, string>
73
+ ): void {
74
+ const attributes = jsxOpeningElement.node.attributes;
75
+
76
+ // Find existing style attribute
77
+ const styleAttrIndex = attributes.findIndex(
78
+ (attr: any) =>
79
+ t.isJSXAttribute(attr) &&
80
+ t.isJSXIdentifier(attr.name) &&
81
+ attr.name.name === 'style'
82
+ );
83
+
84
+ const newStyleObjectExpression = createStyleObjectExpression(newStyles);
85
+
86
+ if (styleAttrIndex !== -1) {
87
+ // Update existing style attribute
88
+ const styleAttr = attributes[styleAttrIndex];
89
+
90
+ if (
91
+ t.isJSXExpressionContainer(styleAttr.value) &&
92
+ t.isObjectExpression(styleAttr.value.expression)
93
+ ) {
94
+ // Merge with existing styles
95
+ const existingProps = styleAttr.value.expression.properties;
96
+ const newProps = newStyleObjectExpression.properties;
97
+
98
+ // Create map of new style keys
99
+ const newStyleKeys = new Set(Object.keys(newStyles));
100
+
101
+ // Filter out existing properties that will be replaced
102
+ const filteredExisting = existingProps.filter(
103
+ (prop: any) =>
104
+ !t.isObjectProperty(prop) ||
105
+ !t.isIdentifier(prop.key) ||
106
+ !newStyleKeys.has(prop.key.name)
107
+ );
108
+
109
+ // Combine: keep non-updated existing properties + add new properties
110
+ styleAttr.value.expression.properties = [
111
+ ...filteredExisting,
112
+ ...newProps,
113
+ ];
114
+ } else {
115
+ // Replace entirely if existing value is not a simple object
116
+ styleAttr.value = t.jsxExpressionContainer(newStyleObjectExpression);
117
+ }
118
+ } else {
119
+ // Add new style attribute
120
+ const styleAttr = t.jsxAttribute(
121
+ t.jsxIdentifier('style'),
122
+ t.jsxExpressionContainer(newStyleObjectExpression)
123
+ );
124
+ attributes.push(styleAttr);
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Merge new className with existing className prop
130
+ */
131
+ function mergeClassNameProp(
132
+ jsxOpeningElement: any,
133
+ newClassName: string
134
+ ): void {
135
+ const attributes = jsxOpeningElement.node.attributes;
136
+
137
+ // Find existing className attribute
138
+ const classNameAttrIndex = attributes.findIndex(
139
+ (attr: any) =>
140
+ t.isJSXAttribute(attr) &&
141
+ t.isJSXIdentifier(attr.name) &&
142
+ attr.name.name === 'className'
143
+ );
144
+
145
+ if (classNameAttrIndex !== -1) {
146
+ // Merge with existing className
147
+ const classNameAttr = attributes[classNameAttrIndex];
148
+
149
+ if (t.isStringLiteral(classNameAttr.value)) {
150
+ // Simple string className
151
+ const existingClasses = classNameAttr.value.value;
152
+ classNameAttr.value = t.stringLiteral(`${existingClasses} ${newClassName}`.trim());
153
+ } else if (
154
+ t.isJSXExpressionContainer(classNameAttr.value) &&
155
+ t.isStringLiteral(classNameAttr.value.expression)
156
+ ) {
157
+ // className={""} format
158
+ const existingClasses = classNameAttr.value.expression.value;
159
+ classNameAttr.value.expression = t.stringLiteral(`${existingClasses} ${newClassName}`.trim());
160
+ } else if (
161
+ t.isJSXExpressionContainer(classNameAttr.value) &&
162
+ t.isTemplateLiteral(classNameAttr.value.expression)
163
+ ) {
164
+ // Template literal className
165
+ const templateLiteral = classNameAttr.value.expression;
166
+ const lastQuasi = templateLiteral.quasis[templateLiteral.quasis.length - 1];
167
+ lastQuasi.value.raw += ` ${newClassName}`;
168
+ lastQuasi.value.cooked = lastQuasi.value.raw;
169
+ } else {
170
+ // Complex expression - wrap in template literal
171
+ classNameAttr.value = t.jsxExpressionContainer(
172
+ t.templateLiteral(
173
+ [
174
+ t.templateElement({ raw: '', cooked: '' }, false),
175
+ t.templateElement({ raw: ` ${newClassName}`, cooked: ` ${newClassName}` }, true),
176
+ ],
177
+ [classNameAttr.value.expression]
178
+ )
179
+ );
180
+ }
181
+ } else {
182
+ // Add new className attribute
183
+ const classNameAttr = t.jsxAttribute(
184
+ t.jsxIdentifier('className'),
185
+ t.stringLiteral(newClassName)
186
+ );
187
+ attributes.push(classNameAttr);
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Update JSX source code with new styles and/or className
193
+ *
194
+ * @param options - Configuration options for the update
195
+ * @returns Result object with success status, updated code, and optional message
196
+ *
197
+ * @example
198
+ * ```typescript
199
+ * const result = updateJSXSource({
200
+ * sourceCode: '<div className="foo">Hello</div>',
201
+ * lineNumber: 1,
202
+ * columnNumber: 0,
203
+ * tagName: 'div',
204
+ * styles: { backgroundColor: 'red !important' },
205
+ * className: 'bar'
206
+ * });
207
+ * // result.code: '<div className="foo bar" style={{ backgroundColor: "red !important" }}>Hello</div>'
208
+ * ```
209
+ */
210
+ export function updateJSXSource(options: UpdateJSXSourceOptions): UpdateJSXSourceResult {
211
+ const { sourceCode, lineNumber, columnNumber, tagName, styles, className } = options;
212
+
213
+ try {
214
+ // Parse JSX/TSX source code
215
+ const ast = parser.parse(sourceCode, {
216
+ sourceType: 'module',
217
+ plugins: ['jsx', 'typescript', 'decorators-legacy'],
218
+ });
219
+
220
+ // Find the target JSX element at the specified position
221
+ const elementPath = findJSXElementAtPosition(ast, lineNumber, columnNumber);
222
+
223
+ if (!elementPath) {
224
+ return {
225
+ success: false,
226
+ code: sourceCode,
227
+ message: `Could not find JSX element <${tagName}> at line ${lineNumber}, column ${columnNumber}`,
228
+ };
229
+ }
230
+
231
+ // Verify tag name matches (optional validation)
232
+ const foundTagName = t.isJSXIdentifier(elementPath.node.name)
233
+ ? elementPath.node.name.name
234
+ : '';
235
+
236
+ if (foundTagName.toLowerCase() !== tagName.toLowerCase()) {
237
+ return {
238
+ success: false,
239
+ code: sourceCode,
240
+ message: `Tag name mismatch: expected <${tagName}>, found <${foundTagName}>`,
241
+ };
242
+ }
243
+
244
+ // Apply style updates if provided
245
+ if (styles && Object.keys(styles).length > 0) {
246
+ mergeStyleProp(elementPath, styles);
247
+ }
248
+
249
+ // Apply className updates if provided
250
+ if (className && className.trim()) {
251
+ mergeClassNameProp(elementPath, className.trim());
252
+ }
253
+
254
+ // Generate updated code
255
+ const output = generate(ast, {
256
+ retainLines: true,
257
+ compact: false,
258
+ concise: false,
259
+ });
260
+
261
+ return {
262
+ success: true,
263
+ code: output.code,
264
+ message: 'JSX source updated successfully',
265
+ };
266
+ } catch (error) {
267
+ return {
268
+ success: false,
269
+ code: sourceCode,
270
+ message: error instanceof Error ? error.message : 'Unknown error occurred during JSX update',
271
+ };
272
+ }
273
+ }
274
+
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Vite plugin for @promakeai/inspector-hook
3
+ * Configures Babel packages for browser compatibility
4
+ */
5
+ import type { Plugin } from "vite";
6
+
7
+ export function inspectorHookPlugin(): Plugin {
8
+ return {
9
+ name: "inspector-hook",
10
+ config() {
11
+ return {
12
+ define: {
13
+ "process.env.NODE_ENV": JSON.stringify(
14
+ process.env.NODE_ENV || "development"
15
+ ),
16
+ "process.platform": JSON.stringify("browser"),
17
+ "process.version": JSON.stringify("v18.0.0"),
18
+ },
19
+ };
20
+ },
21
+ };
22
+ }