@vibexdotnew/inspector 0.0.1 → 0.0.2
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/expo/config.cjs +52 -0
- package/dist/expo/config.cjs.map +1 -0
- package/dist/expo/config.d.cts +48 -0
- package/dist/expo/config.d.ts +48 -0
- package/dist/expo/config.js +26 -0
- package/dist/expo/config.js.map +1 -0
- package/dist/expo/index.cjs +1018 -0
- package/dist/expo/index.cjs.map +1 -0
- package/dist/expo/index.d.cts +40 -0
- package/dist/expo/index.d.ts +40 -0
- package/dist/expo/index.js +994 -0
- package/dist/expo/index.js.map +1 -0
- package/dist/next/index.cjs +0 -14
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.js +0 -14
- package/dist/next/index.js.map +1 -1
- package/expo/metro-transformer.js +395 -0
- package/package.json +42 -7
|
@@ -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.
|
|
4
|
-
"description": "Visual inspection and element tagging for Next.js applications",
|
|
3
|
+
"version": "0.0.2",
|
|
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,25 +62,45 @@
|
|
|
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
|
}
|
|
71
|
-
}
|
|
106
|
+
}
|