zelune 1.6.2 → 1.6.4

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/bin/cli.mjs +13 -54
  3. package/dist/assets/CheckoutScreen-6IdrpKgm.js +229 -0
  4. package/dist/assets/HomeScreen-Dxe9L4I2.js +316 -0
  5. package/dist/assets/LoginScreen-Dp1awI4F.js +189 -0
  6. package/dist/assets/ProductDetailScreen-Wa5_nlRP.js +196 -0
  7. package/dist/assets/ProfileScreen-riAU6vVh.js +185 -0
  8. package/dist/assets/index-hEzd4JVy.js +44697 -0
  9. package/{index.html → dist/index.html} +3 -7
  10. package/mcp/server.mjs +1370 -34
  11. package/package.json +10 -17
  12. package/plugin.json +1 -1
  13. package/socket.yml +11 -0
  14. package/mcp/server.ts +0 -446
  15. package/plugins/viteReactNativeFallbackPlugin.ts +0 -519
  16. package/plugins/viteRnSourceInject.ts +0 -117
  17. package/plugins/viteVirtualScreensPlugin.ts +0 -216
  18. package/src/canvas/CanvasApp.tsx +0 -1655
  19. package/src/canvas/FlowArrowsLayer.tsx +0 -239
  20. package/src/canvas/InspectorPanel.tsx +0 -1494
  21. package/src/canvas/ScreenArtboard.tsx +0 -1491
  22. package/src/canvas/types.ts +0 -23
  23. package/src/main.tsx +0 -80
  24. package/src/runtime/MockNavigationContext.tsx +0 -1795
  25. package/src/runtime/ScreenErrorBoundary.tsx +0 -86
  26. package/src/runtime/nativewindShim.ts +0 -997
  27. package/src/runtime/reactNativeShim.ts +0 -240
  28. package/src/screens/CheckoutScreen.tsx +0 -229
  29. package/src/screens/HomeScreen.tsx +0 -288
  30. package/src/screens/LoginScreen.tsx +0 -176
  31. package/src/screens/ProductDetailScreen.tsx +0 -207
  32. package/src/screens/ProfileScreen.tsx +0 -172
  33. package/src/screens/index.ts +0 -1
  34. package/src/server/apiMiddleware.ts +0 -375
  35. package/src/server/astEngine.ts +0 -2553
  36. package/src/vite-env.d.ts +0 -7
  37. package/tsconfig.json +0 -23
  38. package/vite.config.ts +0 -340
  39. /package/{src/styles.css → dist/assets/index-C7nrS82N.css} +0 -0
@@ -1,519 +0,0 @@
1
- import { parse } from '@babel/parser';
2
- import * as LucideIcons from 'lucide-react';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import type { Plugin } from 'vite';
6
-
7
- const VIRTUAL_STUB_PREFIX = '\0virtual:zelune-native-stub:';
8
- const VIRTUAL_LUCIDE_PREFIX = '\0virtual:zelune-lucide-stub:';
9
-
10
- const SHIMMED_PACKAGE_PREFIXES = [
11
- '@react-navigation/',
12
- 'expo-router',
13
- 'react-native-safe-area-context',
14
- 'expo-status-bar',
15
- '@expo/vector-icons',
16
- 'react-native-vector-icons',
17
- 'expo-linear-gradient',
18
- 'expo-blur',
19
- 'expo-image',
20
- 'react-native-svg',
21
- 'react-native-reanimated',
22
- 'react-native-gesture-handler',
23
- 'expo-haptics',
24
- 'expo-constants',
25
- 'expo-font',
26
- 'expo-splash-screen',
27
- 'expo-secure-store',
28
- 'expo-clipboard',
29
- 'expo-linking',
30
- '@react-native-async-storage/async-storage',
31
- '@shopify/flash-list',
32
- 'zustand',
33
- '@tanstack/react-query',
34
- '@supabase/supabase-js',
35
- ];
36
-
37
- const NATIVE_ONLY_PACKAGE_PATTERN =
38
- /^(react-native-|@react-native|expo|expo-|@expo\/|@gorhom\/|lottie-react-native|moti|nativewind|tamagui|@tamagui\/|@shopify\/)/;
39
-
40
- const CORE_PASSTHROUGH_PACKAGES = new Set([
41
- 'react',
42
- 'react-dom',
43
- 'react-dom/client',
44
- 'react/jsx-runtime',
45
- 'react/jsx-dev-runtime',
46
- 'react-native-web',
47
- 'virtual:zelune-screens',
48
- ]);
49
-
50
- const RESOLVABLE_EXTENSIONS = [
51
- '.web.tsx',
52
- '.web.ts',
53
- '.web.jsx',
54
- '.web.js',
55
- '.tsx',
56
- '.ts',
57
- '.jsx',
58
- '.js',
59
- '.json',
60
- '.css',
61
- ];
62
-
63
- function stripJsonCommentsAndTrailingCommas(input: string): string {
64
- const withoutComments = input.replace(
65
- /("(?:\\.|[^"\\])*")|\/\*[\s\S]*?\*\/|\/\/.*/g,
66
- (_match, quotedString?: string) => quotedString ?? ''
67
- );
68
- return withoutComments.replace(/,\s*([}\]])/g, '$1');
69
- }
70
-
71
- function tryResolveLocalFile(candidateBase: string): string | null {
72
- try {
73
- const cleanBase = candidateBase.split('?')[0];
74
- if (fs.existsSync(cleanBase) && fs.statSync(cleanBase).isFile()) {
75
- return cleanBase;
76
- }
77
- for (const ext of RESOLVABLE_EXTENSIONS) {
78
- const withExt = cleanBase + ext;
79
- if (fs.existsSync(withExt) && fs.statSync(withExt).isFile()) {
80
- return withExt;
81
- }
82
- }
83
- if (fs.existsSync(cleanBase) && fs.statSync(cleanBase).isDirectory()) {
84
- for (const ext of RESOLVABLE_EXTENSIONS) {
85
- const indexFile = path.join(cleanBase, `index${ext}`);
86
- if (fs.existsSync(indexFile) && fs.statSync(indexFile).isFile()) {
87
- return indexFile;
88
- }
89
- }
90
- }
91
- } catch {
92
- // Ignore filesystem errors
93
- }
94
- return null;
95
- }
96
-
97
- function loadTsconfigAliasCandidates(roots: string[]): Array<{ prefix: string; targetDir: string }> {
98
- const mappings: Array<{ prefix: string; targetDir: string }> = [];
99
- for (const rootDir of roots) {
100
- try {
101
- const tsconfigPath = path.join(rootDir, 'tsconfig.json');
102
- if (!fs.existsSync(tsconfigPath)) continue;
103
- const raw = fs.readFileSync(tsconfigPath, 'utf-8');
104
- const parsed = JSON.parse(stripJsonCommentsAndTrailingCommas(raw)) as {
105
- compilerOptions?: {
106
- baseUrl?: string;
107
- paths?: Record<string, string[]>;
108
- };
109
- };
110
- const baseUrl = parsed.compilerOptions?.baseUrl ?? '.';
111
- const paths = parsed.compilerOptions?.paths ?? {};
112
- for (const [pattern, targets] of Object.entries(paths)) {
113
- if (!Array.isArray(targets)) continue;
114
- const cleanPrefix = pattern.replace(/\/\*$/, '');
115
- if (!cleanPrefix) continue;
116
- for (const t of targets) {
117
- const cleanTarget = t.replace(/\/\*$/, '');
118
- const resolvedDir = path.resolve(rootDir, baseUrl, cleanTarget);
119
- mappings.push({ prefix: cleanPrefix, targetDir: resolvedDir });
120
- }
121
- }
122
- } catch {
123
- // Ignore malformed tsconfig.json
124
- }
125
- }
126
- return mappings;
127
- }
128
-
129
- function extractModuleNamedExports(filePath: string): Set<string> {
130
- const exportedNames = new Set<string>();
131
- try {
132
- if (!fs.existsSync(filePath)) return exportedNames;
133
- const content = fs.readFileSync(filePath, 'utf-8');
134
- const ast = parse(content, {
135
- sourceType: 'module',
136
- plugins: ['jsx', 'typescript', 'decorators-legacy'],
137
- errorRecovery: true,
138
- });
139
- for (const stmt of ast.program.body) {
140
- if (stmt.type === 'ExportNamedDeclaration') {
141
- if (stmt.exportKind === 'type') continue;
142
- for (const spec of stmt.specifiers) {
143
- if (spec.type === 'ExportSpecifier' && spec.exportKind !== 'type') {
144
- const name =
145
- spec.exported.type === 'Identifier' ? spec.exported.name : spec.exported.value;
146
- if (name && name !== 'default') exportedNames.add(name);
147
- }
148
- }
149
- const decl = stmt.declaration;
150
- if (decl) {
151
- if (
152
- (decl.type === 'FunctionDeclaration' || decl.type === 'ClassDeclaration') &&
153
- decl.id?.name
154
- ) {
155
- exportedNames.add(decl.id.name);
156
- } else if (decl.type === 'VariableDeclaration') {
157
- for (const d of decl.declarations) {
158
- if (d.id.type === 'Identifier') {
159
- exportedNames.add(d.id.name);
160
- }
161
- }
162
- }
163
- }
164
- }
165
- }
166
- } catch {
167
- // Ignore parse errors
168
- }
169
- return exportedNames;
170
- }
171
-
172
- export function extractNamedImportsFromImporter(
173
- importerPath: string,
174
- targetSpecifier: string
175
- ): string[] {
176
- const names = new Set<string>();
177
- try {
178
- const cleanImporter = importerPath.split('?')[0];
179
- if (!fs.existsSync(cleanImporter)) return [];
180
- const content = fs.readFileSync(cleanImporter, 'utf-8');
181
-
182
- try {
183
- const ast = parse(content, {
184
- sourceType: 'module',
185
- plugins: ['jsx', 'typescript', 'decorators-legacy'],
186
- errorRecovery: true,
187
- });
188
-
189
- for (const stmt of ast.program.body) {
190
- if (
191
- stmt.type === 'ImportDeclaration' &&
192
- stmt.source.value === targetSpecifier &&
193
- stmt.importKind !== 'type'
194
- ) {
195
- for (const spec of stmt.specifiers) {
196
- if (spec.type === 'ImportSpecifier' && spec.importKind !== 'type') {
197
- const importedName =
198
- spec.imported.type === 'Identifier' ? spec.imported.name : spec.imported.value;
199
- if (
200
- importedName &&
201
- importedName !== 'default' &&
202
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(importedName)
203
- ) {
204
- names.add(importedName);
205
- }
206
- }
207
- }
208
- } else if (
209
- stmt.type === 'ExportNamedDeclaration' &&
210
- stmt.source?.value === targetSpecifier &&
211
- stmt.exportKind !== 'type'
212
- ) {
213
- for (const spec of stmt.specifiers) {
214
- if (spec.type === 'ExportSpecifier' && spec.exportKind !== 'type') {
215
- const localName =
216
- spec.local.type === 'Identifier' ? spec.local.name : spec.local.value;
217
- const exportedName =
218
- spec.exported.type === 'Identifier' ? spec.exported.name : spec.exported.value;
219
- if (
220
- localName &&
221
- localName !== 'default' &&
222
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName)
223
- ) {
224
- names.add(localName);
225
- }
226
- if (
227
- exportedName &&
228
- exportedName !== 'default' &&
229
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(exportedName)
230
- ) {
231
- names.add(exportedName);
232
- }
233
- }
234
- }
235
- }
236
- }
237
- return Array.from(names);
238
- } catch {
239
- // Fallback to statement-isolated regex if AST parser fails
240
- const escapedSpec = targetSpecifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
241
- const importRegex = new RegExp(
242
- `(?:import|export)\\s+(?:type\\s+)?(?:[A-Za-z0-9_$]+\\s*,\\s*)?\\{([^}]+)\\}\\s*from\\s*['"]${escapedSpec}['"]`,
243
- 'g'
244
- );
245
- let match: RegExpExecArray | null;
246
- while ((match = importRegex.exec(content)) !== null) {
247
- const items = match[1].split(',');
248
- for (const rawItem of items) {
249
- const cleaned = rawItem.replace(/\btype\s+/g, '').trim();
250
- if (!cleaned) continue;
251
- const parts = cleaned.split(/\s+as\s+/);
252
- const importedName = parts[0]?.trim();
253
- if (
254
- importedName &&
255
- importedName !== 'default' &&
256
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(importedName)
257
- ) {
258
- names.add(importedName);
259
- }
260
- }
261
- }
262
- }
263
- } catch {
264
- // Ignore read errors
265
- }
266
- return Array.from(names);
267
- }
268
-
269
- export function viteReactNativeFallbackPlugin(
270
- targetProjectRoot: string,
271
- mockNavShimPath: string,
272
- workspaceRoot: string = targetProjectRoot
273
- ): Plugin {
274
- const normalizedShimPath = mockNavShimPath.replace(/\\/g, '/');
275
- const reactNativeShimPath = path
276
- .resolve(path.dirname(mockNavShimPath), 'reactNativeShim.ts')
277
- .replace(/\\/g, '/');
278
- const zelunePackageRoot = path.resolve(path.dirname(mockNavShimPath), '../..');
279
- const zeluneDemoScreensDir = path.resolve(zelunePackageRoot, 'src/screens');
280
-
281
- const knownShimExports = extractModuleNamedExports(mockNavShimPath);
282
- const lucideExportKeys = new Set<string>(Object.keys(LucideIcons));
283
-
284
- const uniqueRoots = Array.from(
285
- new Set([path.resolve(targetProjectRoot), path.resolve(workspaceRoot)])
286
- );
287
- const tsconfigMappings = loadTsconfigAliasCandidates(uniqueRoots);
288
-
289
- function resolveProjectAliasImport(source: string): string | null {
290
- for (const mapping of tsconfigMappings) {
291
- if (source === mapping.prefix) {
292
- const found = tryResolveLocalFile(mapping.targetDir);
293
- if (found) return found;
294
- } else if (source.startsWith(mapping.prefix + '/')) {
295
- const rel = source.slice(mapping.prefix.length + 1);
296
- const found = tryResolveLocalFile(path.join(mapping.targetDir, rel));
297
- if (found) return found;
298
- }
299
- }
300
-
301
- if (source.startsWith('@/') || source.startsWith('~/')) {
302
- const rel = source.slice(2);
303
- for (const rootDir of uniqueRoots) {
304
- const directMatch = tryResolveLocalFile(path.join(rootDir, rel));
305
- if (directMatch) return directMatch;
306
- const srcMatch = tryResolveLocalFile(path.join(rootDir, 'src', rel));
307
- if (srcMatch) return srcMatch;
308
- }
309
- }
310
-
311
- return null;
312
- }
313
-
314
- return {
315
- name: 'vite-react-native-fallback',
316
- enforce: 'pre',
317
- resolveId(source, importer) {
318
- if (
319
- !source ||
320
- source.startsWith('\0') ||
321
- source.startsWith('virtual:') ||
322
- source.startsWith('node:') ||
323
- source === 'vitest' ||
324
- CORE_PASSTHROUGH_PACKAGES.has(source)
325
- ) {
326
- return null;
327
- }
328
-
329
- const cleanImporter = importer ? importer.split('?')[0] : '';
330
- const isZeluneInternalImporter =
331
- Boolean(cleanImporter) &&
332
- ((cleanImporter.startsWith(zelunePackageRoot + path.sep) &&
333
- !cleanImporter.startsWith(zeluneDemoScreensDir + path.sep)) ||
334
- cleanImporter.includes(`${path.sep}node_modules${path.sep}`));
335
-
336
- // 1. Exact 'react-native' -> reactNativeShim.ts; subpaths 'react-native/...' -> virtual stub
337
- if (source === 'react-native') {
338
- if (cleanImporter && cleanImporter.replace(/\\/g, '/') === reactNativeShimPath) {
339
- return null;
340
- }
341
- return reactNativeShimPath;
342
- }
343
- if (source.startsWith('react-native/')) {
344
- const requestedNames = cleanImporter
345
- ? extractNamedImportsFromImporter(cleanImporter, source)
346
- : [];
347
- return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
348
- requestedNames.join(',')
349
- )}`;
350
- }
351
-
352
- // 2. 'lucide-react' & 'lucide-react-native' -> synthesize fallback icons for any missing icon names
353
- if (source === 'lucide-react' || source === 'lucide-react-native') {
354
- const requestedNames = cleanImporter
355
- ? extractNamedImportsFromImporter(cleanImporter, source)
356
- : [];
357
- const missingIcons =
358
- lucideExportKeys.size > 0
359
- ? requestedNames.filter((name) => !lucideExportKeys.has(name))
360
- : [];
361
- if (source === 'lucide-react' && missingIcons.length === 0) {
362
- return null;
363
- }
364
- return `${VIRTUAL_LUCIDE_PREFIX}?missing=${encodeURIComponent(missingIcons.join(','))}`;
365
- }
366
-
367
- // 3. Project path aliases (@/..., ~/..., or tsconfig paths)
368
- if (
369
- source.startsWith('@/') ||
370
- source.startsWith('~/') ||
371
- tsconfigMappings.some((m) => source === m.prefix || source.startsWith(m.prefix + '/'))
372
- ) {
373
- const resolvedLocal = resolveProjectAliasImport(source);
374
- if (resolvedLocal) {
375
- return resolvedLocal;
376
- }
377
- const requestedNames = cleanImporter
378
- ? extractNamedImportsFromImporter(cleanImporter, source)
379
- : [];
380
- return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
381
- requestedNames.join(',')
382
- )}`;
383
- }
384
-
385
- // 4. Relative imports (./..., ../...) from target project screens/files
386
- if (source.startsWith('.')) {
387
- if (cleanImporter && !isZeluneInternalImporter) {
388
- const candidate = path.resolve(path.dirname(cleanImporter), source);
389
- const resolvedLocal = tryResolveLocalFile(candidate);
390
- if (resolvedLocal) {
391
- return null; // Let Vite load existing local files normally
392
- }
393
- // Missing local/generated file in target project -> fallback to virtual stub
394
- const requestedNames = extractNamedImportsFromImporter(cleanImporter, source);
395
- return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
396
- requestedNames.join(',')
397
- )}`;
398
- }
399
- return null;
400
- }
401
-
402
- if (source.startsWith('/')) {
403
- return null;
404
- }
405
-
406
- // 5. Known shimmed packages & native-only RN/Expo packages
407
- const matchesKnownShim = SHIMMED_PACKAGE_PREFIXES.some(
408
- (prefix) =>
409
- source === prefix ||
410
- (prefix.endsWith('/') ? source.startsWith(prefix) : source.startsWith(prefix + '/'))
411
- );
412
-
413
- if (matchesKnownShim || NATIVE_ONLY_PACKAGE_PATTERN.test(source)) {
414
- const requestedNames = cleanImporter
415
- ? extractNamedImportsFromImporter(cleanImporter, source)
416
- : [];
417
- return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
418
- requestedNames.join(',')
419
- )}`;
420
- }
421
-
422
- // 6. Any other bare npm package imported by a target project file (avoid loading unbundled CJS/Flow from target node_modules)
423
- if (cleanImporter && !isZeluneInternalImporter) {
424
- const requestedNames = extractNamedImportsFromImporter(cleanImporter, source);
425
- return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
426
- requestedNames.join(',')
427
- )}`;
428
- }
429
-
430
- return null;
431
- },
432
- load(id) {
433
- if (id.startsWith(VIRTUAL_LUCIDE_PREFIX)) {
434
- const qIdx = id.indexOf('?');
435
- const params = qIdx !== -1 ? new URLSearchParams(id.slice(qIdx + 1)) : null;
436
- const rawMissing = params?.get('missing') ?? '';
437
- const missingNames = rawMissing
438
- .split(',')
439
- .map((s) => s.trim())
440
- .filter((s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) && s !== 'default');
441
-
442
- const lines: string[] = [
443
- `export * from 'lucide-react';`,
444
- `import * as LucideIcons from 'lucide-react';`,
445
- `import { createVectorIconComponent } from ${JSON.stringify(normalizedShimPath)};`,
446
- `export default LucideIcons;`,
447
- ];
448
- for (const name of missingNames) {
449
- lines.push(
450
- `export const ${name} = createVectorIconComponent(${JSON.stringify(name)});`
451
- );
452
- }
453
- return lines.join('\n');
454
- }
455
-
456
- if (!id.startsWith(VIRTUAL_STUB_PREFIX)) {
457
- return null;
458
- }
459
-
460
- const qIdx = id.indexOf('?');
461
- const params = qIdx !== -1 ? new URLSearchParams(id.slice(qIdx + 1)) : null;
462
- const rawExports = params?.get('exports') ?? '';
463
- const extraExportNames = rawExports
464
- .split(',')
465
- .map((s) => s.trim())
466
- .filter(
467
- (s) =>
468
- /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) &&
469
- s !== 'default' &&
470
- !knownShimExports.has(s)
471
- );
472
-
473
- const lines: string[] = [
474
- `import React from 'react';`,
475
- `import { View } from 'react-native';`,
476
- `export * from ${JSON.stringify(normalizedShimPath)};`,
477
- `import * as KnownShims from ${JSON.stringify(normalizedShimPath)};`,
478
- `import DefaultShim, { SAFE_CHAINABLE_PROXY } from ${JSON.stringify(normalizedShimPath)};`,
479
- `function createUniversalComponentStub(displayName) {`,
480
- ` const Comp = React.forwardRef(function UniversalNativeStub(props, ref) {`,
481
- ` if (typeof props?.render === 'function') {`,
482
- ` return props.render({`,
483
- ` field: { name: props?.name ?? '', value: props?.defaultValue ?? '', onChange: () => undefined, onBlur: () => undefined, ref: () => undefined },`,
484
- ` fieldState: { invalid: false, isTouched: false, isDirty: false, error: undefined },`,
485
- ` formState: { errors: {}, isSubmitting: false, isValid: true },`,
486
- ` });`,
487
- ` }`,
488
- ` const content = typeof props?.children === 'function' ? props.children(SAFE_CHAINABLE_PROXY) : props?.children;`,
489
- ` return React.createElement(View, { style: props?.style, className: props?.className, ref }, content);`,
490
- ` });`,
491
- ` Comp.displayName = displayName;`,
492
- ` return new Proxy(Comp, {`,
493
- ` get(target, prop) {`,
494
- ` if (prop in target) return target[prop];`,
495
- ` if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {`,
496
- ` return createUniversalComponentStub(displayName + '.' + prop);`,
497
- ` }`,
498
- ` return SAFE_CHAINABLE_PROXY;`,
499
- ` },`,
500
- ` });`,
501
- `}`,
502
- `function resolveStubExport(name) {`,
503
- ` if (name in KnownShims) return KnownShims[name];`,
504
- ` if (DefaultShim && name in DefaultShim) return DefaultShim[name];`,
505
- ` if (/^use[A-Z]/.test(name)) return () => SAFE_CHAINABLE_PROXY;`,
506
- ` if (/^[A-Z]/.test(name)) return createUniversalComponentStub(name);`,
507
- ` return SAFE_CHAINABLE_PROXY;`,
508
- `}`,
509
- `export default DefaultShim;`,
510
- ];
511
-
512
- for (const name of extraExportNames) {
513
- lines.push(`export const ${name} = resolveStubExport(${JSON.stringify(name)});`);
514
- }
515
-
516
- return lines.join('\n');
517
- },
518
- };
519
- }
@@ -1,117 +0,0 @@
1
- import { parse } from '@babel/parser';
2
- import _traverse from '@babel/traverse';
3
- import _generate from '@babel/generator';
4
- import * as t from '@babel/types';
5
- import path from 'node:path';
6
- import type { Plugin } from 'vite';
7
-
8
- // Handle ESM/CJS default export interop for @babel/* packages
9
- const traverse = (_traverse as unknown as { default?: typeof _traverse }).default ?? _traverse;
10
- const generate = (_generate as unknown as { default?: typeof _generate }).default ?? _generate;
11
-
12
- const ALLOWED_SCREEN_PREFIXES = ['src/screens/', 'screens/', 'app/', 'src/app/'];
13
-
14
- export function getJsxTagName(nameNode: t.JSXOpeningElement['name']): string {
15
- if (t.isJSXIdentifier(nameNode)) {
16
- return nameNode.name;
17
- }
18
- if (t.isJSXMemberExpression(nameNode)) {
19
- return `${getJsxTagName(nameNode.object)}.${nameNode.property.name}`;
20
- }
21
- if (t.isJSXNamespacedName(nameNode)) {
22
- return `${nameNode.namespace.name}:${nameNode.name.name}`;
23
- }
24
- return 'Unknown';
25
- }
26
-
27
- export function viteRnSourceInject(workspaceRoot: string): Plugin {
28
- const normalizedRoot = path.resolve(workspaceRoot);
29
-
30
- return {
31
- name: 'vite-rn-source-inject',
32
- enforce: 'pre',
33
- transform(code: string, id: string) {
34
- const cleanId = id.split('?')[0];
35
- if (!cleanId.endsWith('.tsx') && !cleanId.endsWith('.jsx')) {
36
- return null;
37
- }
38
-
39
- const resolvedFile = path.resolve(cleanId);
40
- const relPath = path.relative(normalizedRoot, resolvedFile).replace(/\\/g, '/');
41
-
42
- // Instrument screen files across src/screens/, screens/, app/, and src/app/
43
- const isScreenFile = ALLOWED_SCREEN_PREFIXES.some((prefix) => relPath.startsWith(prefix));
44
- if (!isScreenFile) {
45
- return null;
46
- }
47
-
48
- try {
49
- const ast = parse(code, {
50
- sourceType: 'module',
51
- plugins: ['typescript', 'jsx'],
52
- });
53
-
54
- let jsxIndex = 0;
55
-
56
- traverse(ast, {
57
- JSXOpeningElement(nodePath) {
58
- const { node } = nodePath;
59
- const tagName = getJsxTagName(node.name);
60
-
61
- // Skip React.Fragment or Fragment since fragments do not accept DOM/dataSet props
62
- if (tagName === 'Fragment' || tagName === 'React.Fragment' || tagName === '') {
63
- return;
64
- }
65
-
66
- const currentIndex = String(jsxIndex++);
67
- const line = String(node.loc?.start.line ?? 1);
68
- const col = String(node.loc?.start.column ?? 0);
69
-
70
- // Check if already injected
71
- const hasAgyAttr = node.attributes.some(
72
- (attr) => t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name, { name: 'data-agy-ast-path' })
73
- );
74
- if (hasAgyAttr) {
75
- return;
76
- }
77
-
78
- // Build dataSet={{ agyFile, agyLine, agyCol, agyAstPath, agyTag }} for react-native-web
79
- const dataSetProps: t.ObjectProperty[] = [
80
- t.objectProperty(t.identifier('agyFile'), t.stringLiteral(relPath)),
81
- t.objectProperty(t.identifier('agyLine'), t.stringLiteral(line)),
82
- t.objectProperty(t.identifier('agyCol'), t.stringLiteral(col)),
83
- t.objectProperty(t.identifier('agyAstPath'), t.stringLiteral(currentIndex)),
84
- t.objectProperty(t.identifier('agyTag'), t.stringLiteral(tagName)),
85
- ];
86
-
87
- node.attributes.push(
88
- t.jsxAttribute(
89
- t.jsxIdentifier('dataSet'),
90
- t.jsxExpressionContainer(t.objectExpression(dataSetProps))
91
- ),
92
- t.jsxAttribute(t.jsxIdentifier('data-agy-file'), t.stringLiteral(relPath)),
93
- t.jsxAttribute(t.jsxIdentifier('data-agy-line'), t.stringLiteral(line)),
94
- t.jsxAttribute(t.jsxIdentifier('data-agy-col'), t.stringLiteral(col)),
95
- t.jsxAttribute(t.jsxIdentifier('data-agy-ast-path'), t.stringLiteral(currentIndex)),
96
- t.jsxAttribute(t.jsxIdentifier('data-agy-tag'), t.stringLiteral(tagName))
97
- );
98
- },
99
- });
100
-
101
- const output = generate(ast, {
102
- retainLines: true,
103
- sourceMaps: true,
104
- sourceFileName: relPath,
105
- });
106
-
107
- return {
108
- code: output.code,
109
- map: output.map,
110
- };
111
- } catch {
112
- // Return untransformed code so Vite's error overlay or ScreenErrorBoundary can report it cleanly
113
- return null;
114
- }
115
- },
116
- };
117
- }