@vibexdotnew/inspector 0.0.1 → 0.0.3

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,395 @@
1
+ /**
2
+ * Metro transformer that automatically:
3
+ * 1. Wraps _layout.tsx with ErrorBoundary for error handling
4
+ * 2. Injects VisualInspector for element inspection (development only)
5
+ * 3. Adds dataSet attributes to all JSX elements for element inspection
6
+ * - vibexLoc: "filePath:lineNumber:columnNumber" (maps to data-vibex-loc)
7
+ * - vibexName: component/tag name (maps to data-vibex-name)
8
+ * Uses Babel AST transformations for reliable code modification
9
+ */
10
+
11
+ const parser = require("@babel/parser");
12
+ const traverse = require("@babel/traverse").default;
13
+ const generate = require("@babel/generator").default;
14
+ const t = require("@babel/types");
15
+
16
+ const babelTransformer = require("@expo/metro-config/babel-transformer");
17
+
18
+
19
+ /**
20
+ * Checks if the file is the root _layout.tsx
21
+ */
22
+ function isRootLayout(filename) {
23
+ return (
24
+ (filename.includes("app/_layout.tsx") ||
25
+ filename.includes("app/_layout.js")) &&
26
+ !filename.includes("app/(") // Exclude grouped layouts
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Checks if the file should have dataSet attributes added
32
+ */
33
+ function shouldAddDataAttributes(filename) {
34
+ const ext = filename.split(".").pop();
35
+ return ["ts", "tsx", "js", "jsx"].includes(ext);
36
+ }
37
+
38
+ /**
39
+ * Gets the relative file path from the project root
40
+ */
41
+ function getRelativeFilePath(filename) {
42
+ const cwd = process.cwd();
43
+ if (filename.startsWith(cwd)) {
44
+ return filename.slice(cwd.length + 1);
45
+ }
46
+ return filename;
47
+ }
48
+
49
+ /**
50
+ * Parses code with Babel, trying different configurations
51
+ */
52
+ function parseCode(code) {
53
+ const parserOptions = {
54
+ sourceType: "module",
55
+ plugins: [
56
+ "jsx",
57
+ "typescript",
58
+ "decorators-legacy",
59
+ "classProperties",
60
+ "classPrivateProperties",
61
+ "classPrivateMethods",
62
+ "exportDefaultFrom",
63
+ "exportNamespaceFrom",
64
+ "dynamicImport",
65
+ "nullishCoalescingOperator",
66
+ "optionalChaining",
67
+ "objectRestSpread",
68
+ ],
69
+ };
70
+
71
+ try {
72
+ return parser.parse(code, parserOptions);
73
+ } catch (error) {
74
+ // Try without TypeScript plugin
75
+ const jsOptions = {
76
+ ...parserOptions,
77
+ plugins: parserOptions.plugins.filter((p) => p !== "typescript"),
78
+ };
79
+ return parser.parse(code, jsOptions);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Adds dataSet attributes to JSX elements for element inspection
85
+ */
86
+ function addDataSetAttributes(ast, filePath) {
87
+ const relativeFilePath = getRelativeFilePath(filePath);
88
+
89
+ traverse(ast, {
90
+ JSXElement(path) {
91
+ try {
92
+ const node = path.node.openingElement;
93
+
94
+ if (!node || !node.name || node.type !== "JSXOpeningElement") {
95
+ return;
96
+ }
97
+
98
+ // Get the tag name
99
+ let tagName = "";
100
+ if (t.isJSXIdentifier(node.name)) {
101
+ tagName = node.name.name;
102
+ } else if (t.isJSXMemberExpression(node.name)) {
103
+ tagName = `${node.name.object.name}.${node.name.property.name}`;
104
+ } else {
105
+ return;
106
+ }
107
+
108
+ // Skip if already has dataSet attribute with our properties
109
+ const hasFlexDataSet = node.attributes.some(
110
+ (attr) =>
111
+ t.isJSXAttribute(attr) &&
112
+ t.isJSXIdentifier(attr.name) &&
113
+ attr.name.name === "dataSet" &&
114
+ t.isJSXExpressionContainer(attr.value) &&
115
+ t.isObjectExpression(attr.value.expression) &&
116
+ attr.value.expression.properties.some(
117
+ (prop) =>
118
+ t.isObjectProperty(prop) &&
119
+ t.isIdentifier(prop.key) &&
120
+ prop.key.name === "vibexLoc"
121
+ )
122
+ );
123
+
124
+ if (hasFlexDataSet) {
125
+ return;
126
+ }
127
+
128
+ const lineNumber = node.loc?.start.line || 0;
129
+ const colNumber = node.loc?.start.column || 0;
130
+
131
+ // Create dataSet properties matching Next.js inspector format
132
+ // vibexLoc format: "filePath:lineNumber:columnNumber"
133
+ // vibexName: tag/component name
134
+ const dataSetProperties = [
135
+ t.objectProperty(
136
+ t.identifier("vibexLoc"),
137
+ t.stringLiteral(`${relativeFilePath}:${lineNumber}:${colNumber}`)
138
+ ),
139
+ t.objectProperty(
140
+ t.identifier("vibexName"),
141
+ t.stringLiteral(tagName)
142
+ ),
143
+ ];
144
+
145
+ // Check if there's an existing dataSet attribute to merge with
146
+ const existingDataSetIndex = node.attributes.findIndex(
147
+ (attr) =>
148
+ t.isJSXAttribute(attr) &&
149
+ t.isJSXIdentifier(attr.name) &&
150
+ attr.name.name === "dataSet"
151
+ );
152
+
153
+ if (existingDataSetIndex !== -1) {
154
+ const existingAttr = node.attributes[existingDataSetIndex];
155
+ if (
156
+ t.isJSXExpressionContainer(existingAttr.value) &&
157
+ t.isObjectExpression(existingAttr.value.expression)
158
+ ) {
159
+ // Merge with existing dataSet
160
+ existingAttr.value.expression.properties.push(
161
+ ...dataSetProperties
162
+ );
163
+ }
164
+ } else {
165
+ // Create new dataSet attribute
166
+ const dataSetAttribute = t.jsxAttribute(
167
+ t.jsxIdentifier("dataSet"),
168
+ t.jsxExpressionContainer(
169
+ t.objectExpression(dataSetProperties)
170
+ )
171
+ );
172
+ node.attributes.push(dataSetAttribute);
173
+ }
174
+ } catch (error) {
175
+ // Skip elements that can't be processed
176
+ }
177
+ },
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Wraps the root layout with error boundary, visual inspector, and optional providers
183
+ */
184
+ function wrapRootLayout(ast, environment) {
185
+ let originalFunctionName = null;
186
+ let defaultExportPath = null;
187
+
188
+ // Find the default export function
189
+ traverse(ast, {
190
+ ExportDefaultDeclaration(path) {
191
+ const declaration = path.node.declaration;
192
+ if (t.isFunctionDeclaration(declaration) && declaration.id) {
193
+ originalFunctionName = declaration.id.name;
194
+ defaultExportPath = path;
195
+ }
196
+ },
197
+ });
198
+
199
+ if (!originalFunctionName || !defaultExportPath) {
200
+ return false;
201
+ }
202
+
203
+ // Remove 'export default' from the original function
204
+ const originalFunction = defaultExportPath.node.declaration;
205
+ defaultExportPath.replaceWith(originalFunction);
206
+
207
+ // Build wrapper imports and components
208
+ // Wrappers are applied in reverse order (last one wraps outermost)
209
+ const wrappers = [];
210
+ const siblings = []; // Components to render alongside the main content
211
+
212
+ // Always add ErrorBoundary as outermost wrapper (added last, wraps everything)
213
+ if (environment === "development") {
214
+ wrappers.push({
215
+ importName: "ErrorBoundary",
216
+ importPath: "@/components/ErrorBoundary",
217
+ isDefault: true,
218
+ });
219
+
220
+ // Add VisualInspector as a sibling (rendered alongside main content)
221
+ siblings.push({
222
+ importName: "VisualInspector",
223
+ importPath: "@/components/VisualInspector",
224
+ isDefault: true,
225
+ });
226
+ }
227
+
228
+ // Add imports at the top of the file
229
+ traverse(ast, {
230
+ Program(path) {
231
+ // Add wrapper imports
232
+ for (const wrapper of wrappers) {
233
+ let importDeclaration;
234
+ if (wrapper.isDefault) {
235
+ // Default import: import ErrorBoundary from "@/components/ErrorBoundary"
236
+ importDeclaration = t.importDeclaration(
237
+ [t.importDefaultSpecifier(t.identifier(wrapper.importName))],
238
+ t.stringLiteral(wrapper.importPath)
239
+ );
240
+ } else {
241
+ // Named import: import { Name } from "path"
242
+ importDeclaration = t.importDeclaration(
243
+ [
244
+ t.importSpecifier(
245
+ t.identifier(wrapper.importName),
246
+ t.identifier(wrapper.importName)
247
+ ),
248
+ ],
249
+ t.stringLiteral(wrapper.importPath)
250
+ );
251
+ }
252
+ path.node.body.unshift(importDeclaration);
253
+ }
254
+
255
+ // Add sibling component imports
256
+ for (const sibling of siblings) {
257
+ let importDeclaration;
258
+ if (sibling.isDefault) {
259
+ importDeclaration = t.importDeclaration(
260
+ [t.importDefaultSpecifier(t.identifier(sibling.importName))],
261
+ t.stringLiteral(sibling.importPath)
262
+ );
263
+ } else {
264
+ importDeclaration = t.importDeclaration(
265
+ [
266
+ t.importSpecifier(
267
+ t.identifier(sibling.importName),
268
+ t.identifier(sibling.importName)
269
+ ),
270
+ ],
271
+ t.stringLiteral(sibling.importPath)
272
+ );
273
+ }
274
+ path.node.body.unshift(importDeclaration);
275
+ }
276
+ },
277
+ });
278
+
279
+ // Create the original component JSX
280
+ const originalComponentJSX = t.jsxElement(
281
+ t.jsxOpeningElement(t.jsxIdentifier(originalFunctionName), [], true),
282
+ null,
283
+ [],
284
+ true
285
+ );
286
+
287
+ // Create sibling component elements
288
+ const siblingElements = siblings.map((sibling) =>
289
+ t.jsxElement(
290
+ t.jsxOpeningElement(t.jsxIdentifier(sibling.importName), [], true),
291
+ null,
292
+ [],
293
+ true
294
+ )
295
+ );
296
+
297
+ // Combine original component with siblings using a Fragment
298
+ let contentJSX;
299
+ if (siblingElements.length > 0) {
300
+ // Use React Fragment to group multiple children: <><OriginalComponent /><Sibling1 /><Sibling2 /></>
301
+ contentJSX = t.jsxFragment(
302
+ t.jsxOpeningFragment(),
303
+ t.jsxClosingFragment(),
304
+ [originalComponentJSX, ...siblingElements]
305
+ );
306
+ } else {
307
+ contentJSX = originalComponentJSX;
308
+ }
309
+
310
+ // Wrap with each wrapper component
311
+ let wrappedJSX = contentJSX;
312
+ for (const wrapper of wrappers) {
313
+ wrappedJSX = t.jsxElement(
314
+ t.jsxOpeningElement(t.jsxIdentifier(wrapper.importName), [], false),
315
+ t.jsxClosingElement(t.jsxIdentifier(wrapper.importName)),
316
+ [wrappedJSX],
317
+ false
318
+ );
319
+ }
320
+
321
+ // Create the wrapper function
322
+ const wrapperFunction = t.functionDeclaration(
323
+ t.identifier("FlexRootLayoutWrapper"),
324
+ [],
325
+ t.blockStatement([t.returnStatement(wrappedJSX)])
326
+ );
327
+
328
+ // Add export default for the wrapper
329
+ const exportDefault = t.exportDefaultDeclaration(wrapperFunction);
330
+
331
+ // Add the wrapper function at the end
332
+ traverse(ast, {
333
+ Program(path) {
334
+ path.node.body.push(exportDefault);
335
+ },
336
+ });
337
+
338
+ return true;
339
+ }
340
+
341
+ /**
342
+ * Transforms source code by adding dataSet attributes and Flex providers
343
+ */
344
+ function transformSource(src, filename, environment) {
345
+ try {
346
+ const ast = parseCode(src);
347
+ const isLayout = isRootLayout(filename);
348
+
349
+ // Add dataSet attributes to all JSX elements (only in development)
350
+ if (environment === "development" && shouldAddDataAttributes(filename)) {
351
+ addDataSetAttributes(ast, filename);
352
+ }
353
+
354
+ // Wrap root layout with error boundary and providers
355
+ if (isLayout) {
356
+ wrapRootLayout(ast, environment);
357
+ }
358
+
359
+ // Generate code from AST
360
+ const output = generate(ast, {
361
+ retainLines: true,
362
+ compact: false,
363
+ jsescOption: { minimal: true },
364
+ });
365
+
366
+ return output.code;
367
+ } catch (error) {
368
+ // If transformation fails, return original source
369
+ return src;
370
+ }
371
+ }
372
+
373
+ async function transform(props) {
374
+ const environment =
375
+ (props.options?.dev ?? process.env.NODE_ENV === "development")
376
+ ? "development"
377
+ : "production";
378
+
379
+ const shouldTransform =
380
+ isRootLayout(props.filename) ||
381
+ (environment === "development" && shouldAddDataAttributes(props.filename));
382
+
383
+ if (shouldTransform) {
384
+ const transformedSrc = transformSource(
385
+ props.src,
386
+ props.filename,
387
+ environment
388
+ );
389
+ return babelTransformer.transform({ ...props, src: transformedSrc });
390
+ }
391
+
392
+ return babelTransformer.transform(props);
393
+ }
394
+
395
+ module.exports = { transform };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vibexdotnew/inspector",
3
- "version": "0.0.1",
4
- "description": "Visual inspection and element tagging for Next.js applications",
3
+ "version": "0.0.3",
4
+ "description": "Visual inspection and element tagging for Next.js and Expo applications",
5
5
  "author": "Vibex",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -11,11 +11,14 @@
11
11
  },
12
12
  "keywords": [
13
13
  "nextjs",
14
+ "expo",
15
+ "react-native",
14
16
  "inspector",
15
17
  "visual-editing",
16
18
  "jsx",
17
19
  "webpack-loader",
18
- "turbopack"
20
+ "turbopack",
21
+ "metro"
19
22
  ],
20
23
  "type": "module",
21
24
  "sideEffects": false,
@@ -33,11 +36,23 @@
33
36
  "import": "./dist/next/loader.js",
34
37
  "require": "./dist/next/loader.cjs"
35
38
  },
36
- "./next/loader.js": "./next/element-tagger.js"
39
+ "./next/loader.js": "./next/element-tagger.js",
40
+ "./expo": {
41
+ "types": "./dist/expo/index.d.ts",
42
+ "import": "./dist/expo/index.js",
43
+ "require": "./dist/expo/index.cjs"
44
+ },
45
+ "./expo/config": {
46
+ "types": "./dist/expo/config.d.ts",
47
+ "import": "./dist/expo/config.js",
48
+ "require": "./dist/expo/config.cjs"
49
+ },
50
+ "./expo/transformer.js": "./expo/metro-transformer.js"
37
51
  },
38
52
  "files": [
39
53
  "dist",
40
- "next"
54
+ "next",
55
+ "expo"
41
56
  ],
42
57
  "scripts": {
43
58
  "build": "tsup",
@@ -47,24 +62,44 @@
47
62
  },
48
63
  "dependencies": {
49
64
  "@babel/parser": "^7.24.0",
65
+ "@babel/traverse": "^7.24.0",
66
+ "@babel/generator": "^7.24.0",
67
+ "@babel/types": "^7.24.0",
50
68
  "magic-string": "^0.30.0",
51
69
  "estree-walker": "^3.0.0"
52
70
  },
53
71
  "peerDependencies": {
54
72
  "next": ">=14.0.0",
55
73
  "react": ">=18.0.0",
56
- "react-dom": ">=18.0.0"
74
+ "react-dom": ">=18.0.0",
75
+ "react-native": ">=0.72.0",
76
+ "expo": ">=49.0.0",
77
+ "@expo/metro-config": ">=0.10.0"
57
78
  },
58
79
  "peerDependenciesMeta": {
59
80
  "next": {
60
81
  "optional": true
82
+ },
83
+ "react-dom": {
84
+ "optional": true
85
+ },
86
+ "react-native": {
87
+ "optional": true
88
+ },
89
+ "expo": {
90
+ "optional": true
91
+ },
92
+ "@expo/metro-config": {
93
+ "optional": true
61
94
  }
62
95
  },
63
96
  "devDependencies": {
64
97
  "@types/react": "^18.0.0",
98
+ "@types/react-native": "^0.72.0",
65
99
  "next": "^15.0.0",
66
100
  "react": "^18.0.0",
67
101
  "react-dom": "^18.0.0",
102
+ "react-native": "^0.76.0",
68
103
  "tsup": "^8.0.0",
69
104
  "typescript": "^5.8.3"
70
105
  }