zelune 1.5.2 → 1.5.5

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,528 @@
1
+ import { parse } from '@babel/parser';
2
+ import fs from 'node:fs';
3
+ import { createRequire } from 'node:module';
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 reqFromPlugin = createRequire(import.meta.url);
283
+ const lucideExportKeys = new Set<string>();
284
+ try {
285
+ const lucideMod = reqFromPlugin('lucide-react') as Record<string, unknown>;
286
+ for (const k of Object.keys(lucideMod)) {
287
+ lucideExportKeys.add(k);
288
+ }
289
+ } catch {
290
+ // Ignore if lucide-react cannot be eagerly inspected
291
+ }
292
+
293
+ const uniqueRoots = Array.from(
294
+ new Set([path.resolve(targetProjectRoot), path.resolve(workspaceRoot)])
295
+ );
296
+ const tsconfigMappings = loadTsconfigAliasCandidates(uniqueRoots);
297
+
298
+ function resolveProjectAliasImport(source: string): string | null {
299
+ for (const mapping of tsconfigMappings) {
300
+ if (source === mapping.prefix) {
301
+ const found = tryResolveLocalFile(mapping.targetDir);
302
+ if (found) return found;
303
+ } else if (source.startsWith(mapping.prefix + '/')) {
304
+ const rel = source.slice(mapping.prefix.length + 1);
305
+ const found = tryResolveLocalFile(path.join(mapping.targetDir, rel));
306
+ if (found) return found;
307
+ }
308
+ }
309
+
310
+ if (source.startsWith('@/') || source.startsWith('~/')) {
311
+ const rel = source.slice(2);
312
+ for (const rootDir of uniqueRoots) {
313
+ const directMatch = tryResolveLocalFile(path.join(rootDir, rel));
314
+ if (directMatch) return directMatch;
315
+ const srcMatch = tryResolveLocalFile(path.join(rootDir, 'src', rel));
316
+ if (srcMatch) return srcMatch;
317
+ }
318
+ }
319
+
320
+ return null;
321
+ }
322
+
323
+ return {
324
+ name: 'vite-react-native-fallback',
325
+ enforce: 'pre',
326
+ resolveId(source, importer) {
327
+ if (
328
+ !source ||
329
+ source.startsWith('\0') ||
330
+ source.startsWith('virtual:') ||
331
+ source.startsWith('node:') ||
332
+ source === 'vitest' ||
333
+ CORE_PASSTHROUGH_PACKAGES.has(source)
334
+ ) {
335
+ return null;
336
+ }
337
+
338
+ const cleanImporter = importer ? importer.split('?')[0] : '';
339
+ const isZeluneInternalImporter =
340
+ Boolean(cleanImporter) &&
341
+ ((cleanImporter.startsWith(zelunePackageRoot + path.sep) &&
342
+ !cleanImporter.startsWith(zeluneDemoScreensDir + path.sep)) ||
343
+ cleanImporter.includes(`${path.sep}node_modules${path.sep}`));
344
+
345
+ // 1. Exact 'react-native' -> reactNativeShim.ts; subpaths 'react-native/...' -> virtual stub
346
+ if (source === 'react-native') {
347
+ if (cleanImporter && cleanImporter.replace(/\\/g, '/') === reactNativeShimPath) {
348
+ return null;
349
+ }
350
+ return reactNativeShimPath;
351
+ }
352
+ if (source.startsWith('react-native/')) {
353
+ const requestedNames = cleanImporter
354
+ ? extractNamedImportsFromImporter(cleanImporter, source)
355
+ : [];
356
+ return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
357
+ requestedNames.join(',')
358
+ )}`;
359
+ }
360
+
361
+ // 2. 'lucide-react' & 'lucide-react-native' -> synthesize fallback icons for any missing icon names
362
+ if (source === 'lucide-react' || source === 'lucide-react-native') {
363
+ const requestedNames = cleanImporter
364
+ ? extractNamedImportsFromImporter(cleanImporter, source)
365
+ : [];
366
+ const missingIcons =
367
+ lucideExportKeys.size > 0
368
+ ? requestedNames.filter((name) => !lucideExportKeys.has(name))
369
+ : [];
370
+ if (source === 'lucide-react' && missingIcons.length === 0) {
371
+ return null;
372
+ }
373
+ return `${VIRTUAL_LUCIDE_PREFIX}?missing=${encodeURIComponent(missingIcons.join(','))}`;
374
+ }
375
+
376
+ // 3. Project path aliases (@/..., ~/..., or tsconfig paths)
377
+ if (
378
+ source.startsWith('@/') ||
379
+ source.startsWith('~/') ||
380
+ tsconfigMappings.some((m) => source === m.prefix || source.startsWith(m.prefix + '/'))
381
+ ) {
382
+ const resolvedLocal = resolveProjectAliasImport(source);
383
+ if (resolvedLocal) {
384
+ return resolvedLocal;
385
+ }
386
+ const requestedNames = cleanImporter
387
+ ? extractNamedImportsFromImporter(cleanImporter, source)
388
+ : [];
389
+ return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
390
+ requestedNames.join(',')
391
+ )}`;
392
+ }
393
+
394
+ // 4. Relative imports (./..., ../...) from target project screens/files
395
+ if (source.startsWith('.')) {
396
+ if (cleanImporter && !isZeluneInternalImporter) {
397
+ const candidate = path.resolve(path.dirname(cleanImporter), source);
398
+ const resolvedLocal = tryResolveLocalFile(candidate);
399
+ if (resolvedLocal) {
400
+ return null; // Let Vite load existing local files normally
401
+ }
402
+ // Missing local/generated file in target project -> fallback to virtual stub
403
+ const requestedNames = extractNamedImportsFromImporter(cleanImporter, source);
404
+ return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
405
+ requestedNames.join(',')
406
+ )}`;
407
+ }
408
+ return null;
409
+ }
410
+
411
+ if (source.startsWith('/')) {
412
+ return null;
413
+ }
414
+
415
+ // 5. Known shimmed packages & native-only RN/Expo packages
416
+ const matchesKnownShim = SHIMMED_PACKAGE_PREFIXES.some(
417
+ (prefix) =>
418
+ source === prefix ||
419
+ (prefix.endsWith('/') ? source.startsWith(prefix) : source.startsWith(prefix + '/'))
420
+ );
421
+
422
+ if (matchesKnownShim || NATIVE_ONLY_PACKAGE_PATTERN.test(source)) {
423
+ const requestedNames = cleanImporter
424
+ ? extractNamedImportsFromImporter(cleanImporter, source)
425
+ : [];
426
+ return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
427
+ requestedNames.join(',')
428
+ )}`;
429
+ }
430
+
431
+ // 6. Any other bare npm package imported by a target project file (avoid loading unbundled CJS/Flow from target node_modules)
432
+ if (cleanImporter && !isZeluneInternalImporter) {
433
+ const requestedNames = extractNamedImportsFromImporter(cleanImporter, source);
434
+ return `${VIRTUAL_STUB_PREFIX}${encodeURIComponent(source)}?exports=${encodeURIComponent(
435
+ requestedNames.join(',')
436
+ )}`;
437
+ }
438
+
439
+ return null;
440
+ },
441
+ load(id) {
442
+ if (id.startsWith(VIRTUAL_LUCIDE_PREFIX)) {
443
+ const qIdx = id.indexOf('?');
444
+ const params = qIdx !== -1 ? new URLSearchParams(id.slice(qIdx + 1)) : null;
445
+ const rawMissing = params?.get('missing') ?? '';
446
+ const missingNames = rawMissing
447
+ .split(',')
448
+ .map((s) => s.trim())
449
+ .filter((s) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) && s !== 'default');
450
+
451
+ const lines: string[] = [
452
+ `export * from 'lucide-react';`,
453
+ `import * as LucideIcons from 'lucide-react';`,
454
+ `import { createVectorIconComponent } from ${JSON.stringify(normalizedShimPath)};`,
455
+ `export default LucideIcons;`,
456
+ ];
457
+ for (const name of missingNames) {
458
+ lines.push(
459
+ `export const ${name} = createVectorIconComponent(${JSON.stringify(name)});`
460
+ );
461
+ }
462
+ return lines.join('\n');
463
+ }
464
+
465
+ if (!id.startsWith(VIRTUAL_STUB_PREFIX)) {
466
+ return null;
467
+ }
468
+
469
+ const qIdx = id.indexOf('?');
470
+ const params = qIdx !== -1 ? new URLSearchParams(id.slice(qIdx + 1)) : null;
471
+ const rawExports = params?.get('exports') ?? '';
472
+ const extraExportNames = rawExports
473
+ .split(',')
474
+ .map((s) => s.trim())
475
+ .filter(
476
+ (s) =>
477
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(s) &&
478
+ s !== 'default' &&
479
+ !knownShimExports.has(s)
480
+ );
481
+
482
+ const lines: string[] = [
483
+ `import React from 'react';`,
484
+ `import { View } from 'react-native';`,
485
+ `export * from ${JSON.stringify(normalizedShimPath)};`,
486
+ `import * as KnownShims from ${JSON.stringify(normalizedShimPath)};`,
487
+ `import DefaultShim, { SAFE_CHAINABLE_PROXY } from ${JSON.stringify(normalizedShimPath)};`,
488
+ `function createUniversalComponentStub(displayName) {`,
489
+ ` const Comp = React.forwardRef(function UniversalNativeStub(props, ref) {`,
490
+ ` if (typeof props?.render === 'function') {`,
491
+ ` return props.render({`,
492
+ ` field: { name: props?.name ?? '', value: props?.defaultValue ?? '', onChange: () => undefined, onBlur: () => undefined, ref: () => undefined },`,
493
+ ` fieldState: { invalid: false, isTouched: false, isDirty: false, error: undefined },`,
494
+ ` formState: { errors: {}, isSubmitting: false, isValid: true },`,
495
+ ` });`,
496
+ ` }`,
497
+ ` const content = typeof props?.children === 'function' ? props.children(SAFE_CHAINABLE_PROXY) : props?.children;`,
498
+ ` return React.createElement(View, { style: props?.style, ref }, content);`,
499
+ ` });`,
500
+ ` Comp.displayName = displayName;`,
501
+ ` return new Proxy(Comp, {`,
502
+ ` get(target, prop) {`,
503
+ ` if (prop in target) return target[prop];`,
504
+ ` if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {`,
505
+ ` return createUniversalComponentStub(displayName + '.' + prop);`,
506
+ ` }`,
507
+ ` return SAFE_CHAINABLE_PROXY;`,
508
+ ` },`,
509
+ ` });`,
510
+ `}`,
511
+ `function resolveStubExport(name) {`,
512
+ ` if (name in KnownShims) return KnownShims[name];`,
513
+ ` if (DefaultShim && name in DefaultShim) return DefaultShim[name];`,
514
+ ` if (/^use[A-Z]/.test(name)) return () => SAFE_CHAINABLE_PROXY;`,
515
+ ` if (/^[A-Z]/.test(name)) return createUniversalComponentStub(name);`,
516
+ ` return SAFE_CHAINABLE_PROXY;`,
517
+ `}`,
518
+ `export default DefaultShim;`,
519
+ ];
520
+
521
+ for (const name of extraExportNames) {
522
+ lines.push(`export const ${name} = resolveStubExport(${JSON.stringify(name)});`);
523
+ }
524
+
525
+ return lines.join('\n');
526
+ },
527
+ };
528
+ }