zelune 1.5.1 → 1.5.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.
- package/CHANGELOG.md +30 -0
- package/README.md +8 -15
- package/bin/cli.mjs +17 -6
- package/index.html +9 -2
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/plugins/viteReactNativeFallbackPlugin.ts +568 -0
- package/plugins/viteVirtualScreensPlugin.ts +139 -8
- package/src/canvas/CanvasApp.tsx +111 -23
- package/src/canvas/FlowArrowsLayer.tsx +5 -5
- package/src/canvas/InspectorPanel.tsx +58 -49
- package/src/canvas/ScreenArtboard.tsx +90 -29
- package/src/main.tsx +70 -1
- package/src/runtime/MockNavigationContext.tsx +1007 -38
- package/src/runtime/reactNativeShim.ts +141 -0
- package/src/screens/ProductDetailScreen.tsx +32 -59
- package/src/server/astEngine.ts +10 -1
- package/src/styles.css +182 -21
- package/vite.config.ts +136 -67
|
@@ -0,0 +1,568 @@
|
|
|
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 from ${JSON.stringify(normalizedShimPath)};`,
|
|
488
|
+
`function createSafeProxy() {`,
|
|
489
|
+
` const fnTarget = function(...args) {`,
|
|
490
|
+
` if (args.length === 1 && (typeof args[0] === 'number' || typeof args[0] === 'string')) {`,
|
|
491
|
+
` return args[0];`,
|
|
492
|
+
` }`,
|
|
493
|
+
` return proxy;`,
|
|
494
|
+
` };`,
|
|
495
|
+
` const arrTarget = [];`,
|
|
496
|
+
` const proxy = new Proxy(arrTarget, {`,
|
|
497
|
+
` get(target, prop) {`,
|
|
498
|
+
` if (prop === 'length') return 0;`,
|
|
499
|
+
` if (prop === Symbol.iterator) {`,
|
|
500
|
+
` return function* () {`,
|
|
501
|
+
` yield proxy;`,
|
|
502
|
+
` yield fnTarget;`,
|
|
503
|
+
` };`,
|
|
504
|
+
` }`,
|
|
505
|
+
` if (prop === Symbol.toPrimitive) {`,
|
|
506
|
+
` return (hint) => (hint === 'number' ? 0 : '');`,
|
|
507
|
+
` }`,
|
|
508
|
+
` if (prop === 'valueOf') return () => 0;`,
|
|
509
|
+
` if (prop === 'toString') return () => '';`,
|
|
510
|
+
` if (prop === 'then') {`,
|
|
511
|
+
` return (onFulfilled, onRejected) => Promise.resolve({ data: null, error: null }).then(onFulfilled, onRejected);`,
|
|
512
|
+
` }`,
|
|
513
|
+
` if (prop === 'catch') return () => Promise.resolve({ data: null, error: null });`,
|
|
514
|
+
` if (prop === 'finally') return (cb) => Promise.resolve({ data: null, error: null }).finally(cb);`,
|
|
515
|
+
` if (prop === 'isLoading' || prop === 'loading' || prop === 'isPending' || prop === 'isError' || prop === 'disabled') return false;`,
|
|
516
|
+
` if (prop === 'isReady' || prop === 'granted' || prop === 'isSuccess' || prop === ' isAuthenticated') return true;`,
|
|
517
|
+
` if (prop === 'error' || prop === 'user' || prop === 'session') return null;`,
|
|
518
|
+
` if (prop === 'status') return 'granted';`,
|
|
519
|
+
` if (prop === 'data' || prop === 'items' || prop === 'results' || prop === 'list') return [];`,
|
|
520
|
+
` if (prop === 'ref') return { current: null };`,
|
|
521
|
+
` if (prop === 'value') return 0;`,
|
|
522
|
+
` if (prop === 'map' || prop === 'filter' || prop === 'slice' || prop === 'flatMap') return () => [];`,
|
|
523
|
+
` if (prop === 'forEach') return () => undefined;`,
|
|
524
|
+
` if (prop === 'reduce') return (_fn, init) => init;`,
|
|
525
|
+
` if (prop in target) return Reflect.get(target, prop);`,
|
|
526
|
+
` return fnTarget;`,
|
|
527
|
+
` },`,
|
|
528
|
+
` });`,
|
|
529
|
+
` return proxy;`,
|
|
530
|
+
`}`,
|
|
531
|
+
`function createUniversalComponentStub(displayName) {`,
|
|
532
|
+
` const Comp = React.forwardRef(function UniversalNativeStub(props, ref) {`,
|
|
533
|
+
` return React.createElement(View, { style: props?.style, ref }, props?.children);`,
|
|
534
|
+
` });`,
|
|
535
|
+
` Comp.displayName = displayName;`,
|
|
536
|
+
` return new Proxy(Comp, {`,
|
|
537
|
+
` get(target, prop) {`,
|
|
538
|
+
` if (prop in target) return target[prop];`,
|
|
539
|
+
` if (typeof prop === 'string' && /^[A-Z]/.test(prop)) {`,
|
|
540
|
+
` return createUniversalComponentStub(displayName + '.' + prop);`,
|
|
541
|
+
` }`,
|
|
542
|
+
` return () => createSafeProxy();`,
|
|
543
|
+
` },`,
|
|
544
|
+
` });`,
|
|
545
|
+
`}`,
|
|
546
|
+
`function resolveStubExport(name) {`,
|
|
547
|
+
` if (name in KnownShims) return KnownShims[name];`,
|
|
548
|
+
` if (DefaultShim && name in DefaultShim) return DefaultShim[name];`,
|
|
549
|
+
` if (/^use[A-Z]/.test(name)) return () => createSafeProxy();`,
|
|
550
|
+
` if (/^[A-Z]/.test(name)) return createUniversalComponentStub(name);`,
|
|
551
|
+
` return (...args) => {`,
|
|
552
|
+
` if (args.length === 1 && (typeof args[0] === 'number' || typeof args[0] === 'string')) {`,
|
|
553
|
+
` return args[0];`,
|
|
554
|
+
` }`,
|
|
555
|
+
` return createSafeProxy();`,
|
|
556
|
+
` };`,
|
|
557
|
+
`}`,
|
|
558
|
+
`export default DefaultShim;`,
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
for (const name of extraExportNames) {
|
|
562
|
+
lines.push(`export const ${name} = resolveStubExport(${JSON.stringify(name)});`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return lines.join('\n');
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
}
|