/** * Enhanced Metro resolver with both local and remote IOR support * Uses shared core functionality from ior-core.js */ const fs = require('node:fs'); const path = require('node:path'); const { parseRemoteIOR, parsePackageJson, fetchFromGitHub, fetchFromGitea, fetchFromIPFS, extractArchive, findEntryPoint, generateCacheHash, buildLocalIORMappings, } = require('./ior-core.js'); /** * Cache for IOR to path mappings */ const remoteCache = new Map(); /** * Type cache for auto-generated type declarations */ const typeCache = new Map(); const processedTypes = new Set(); /** * Remote cache configuration */ // Cache TTL: Use environment variable or default to 1 hour // Set IOR_CACHE_TTL=0 to always fetch fresh // Set IOR_CACHE_TTL=60000 for 1 minute cache (useful during development) const CACHE_TTL = process.env.IOR_CACHE_TTL !== undefined ? parseInt(process.env.IOR_CACHE_TTL) : 3600000; // 1 hour default // Use project-local cache directory instead of temp directory to avoid watchman issues const CACHE_DIR = path.join(process.cwd(), '.ior-cache', 'remote'); // Log cache TTL configuration on startup console.log('[IOR Resolver] Cache TTL Configuration:'); if (CACHE_TTL === 0) { console.log('[IOR Resolver] ✓ Cache DISABLED (IOR_CACHE_TTL=0) - Always fetching fresh'); } else if (process.env.IOR_CACHE_TTL !== undefined) { const ttlMinutes = Math.floor(CACHE_TTL / 60000); const ttlSeconds = Math.floor((CACHE_TTL % 60000) / 1000); const ttlFormatted = ttlMinutes > 0 ? `${ttlMinutes}m ${ttlSeconds}s` : `${ttlSeconds}s`; console.log(`[IOR Resolver] ✓ Cache TTL set to ${ttlFormatted} (IOR_CACHE_TTL=${CACHE_TTL}ms)`); } else { console.log('[IOR Resolver] ✓ Cache TTL using default: 1 hour (3600000ms)'); } console.log(`[IOR Resolver] ✓ Cache directory: ${CACHE_DIR}`); /** * Ensure cache directory exists */ function ensureCacheDir() { if (!fs.existsSync(CACHE_DIR)) { fs.mkdirSync(CACHE_DIR, { recursive: true }); } } /** * Cache remote component locally (full repository) */ async function cacheRemoteComponent(ior, archiveBuffer, componentName) { ensureCacheDir(); const hash = generateCacheHash(ior); const cacheDir = path.join(CACHE_DIR, hash); // Extract archive to cache directory try { extractArchive(archiveBuffer, cacheDir); } catch (err) { console.error(`[IOR Resolver] Failed to extract archive: ${err.message}`); throw err; } // Find the entry point const entryPath = findEntryPoint(cacheDir, componentName); if (!entryPath) { throw new Error(`No entry point found for component ${componentName} in ${cacheDir}`); } // Update cache map with entry point remoteCache.set(ior, { path: entryPath, directory: cacheDir, timestamp: Date.now(), ttl: CACHE_TTL, }); console.log(`[IOR Resolver] Cached remote component: ${ior}`); console.log(`[IOR Resolver] Directory: ${cacheDir}`); console.log(`[IOR Resolver] Entry: ${entryPath}`); // Generate type declarations asynchronously (non-blocking) generateTypeDeclaration(ior, cacheDir).catch(err => { console.warn(`[IOR Resolver] Type generation failed for ${ior}:`, err.message); }); // Update persistent mappings updatePersistentMappings(ior, entryPath, cacheDir); return entryPath; } /** * Get cached remote component if valid */ function getCachedRemoteComponent(ior) { const entry = remoteCache.get(ior); if (!entry) { return null; } // Check if cache entry is still valid if (CACHE_TTL === 0 || Date.now() - entry.timestamp > entry.ttl) { if (CACHE_TTL === 0) { console.log(`[IOR Resolver] Cache disabled (IOR_CACHE_TTL=0), fetching fresh: ${ior}`); } else { console.log(`[IOR Resolver] Cache expired for: ${ior}`); } remoteCache.delete(ior); // Try to delete the directory (non-blocking) if (entry.directory) { try { fs.rmSync(entry.directory, { recursive: true, force: true }); } catch { // Ignore errors } } return null; } // Verify entry file still exists if (fs.existsSync(entry.path)) { console.log(`[IOR Resolver] Using cached component: ${ior}`); console.log(`[IOR Resolver] Entry: ${entry.path}`); return entry.path; } remoteCache.delete(ior); return null; } /** * Generate type declaration for a remote component */ async function generateTypeDeclaration(ior, cacheDir) { // Avoid duplicate processing if (processedTypes.has(ior)) { return; } try { // Look for .d.ts files in the cached directory const files = fs.readdirSync(cacheDir); let typeContent = null; let typeFile = null; // Priority order for type files const possibleTypeFiles = [ files.find(f => f.endsWith('.d.ts')), 'index.d.ts', 'types.d.ts' ].filter(Boolean); for (const file of possibleTypeFiles) { const filePath = path.join(cacheDir, file); if (fs.existsSync(filePath)) { typeContent = fs.readFileSync(filePath, 'utf-8'); typeFile = file; break; } } if (typeContent) { // Transform module declaration to match the actual import path const moduleRegex = /declare\s+module\s+['"]([^'"]+)['"]/; const match = typeContent.match(moduleRegex); if (match && match[1] !== ior) { // Replace the module declaration with the actual IOR typeContent = typeContent.replace( moduleRegex, `declare module '${ior}'` ); } else if (!match) { // No module declaration, wrap the content typeContent = `declare module '${ior}' {\n${typeContent}\n}\n`; } // Extract the hash from the cache directory path const hash = path.basename(cacheDir); // Add to type cache typeCache.set(ior, { content: typeContent, timestamp: Date.now(), source: typeFile, hash: hash }); // Mark as processed processedTypes.add(ior); // Update the global types file await updateGlobalTypes(); console.log(`[IOR Resolver] Generated type declarations for ${ior} from ${typeFile}`); } } catch (err) { console.warn(`[IOR Resolver] Failed to generate types for ${ior}:`, err.message); } } /** * Update the auto-generated types file */ async function updateGlobalTypes() { const projectRoot = process.cwd(); const typesDir = path.join(projectRoot, 'src', 'types'); const globalTypesPath = path.join(typesDir, 'auto-generated.d.ts'); let content = '/**\n'; content += ' * Auto-generated type declarations for remote IOR components\n'; content += ` * Generated at: ${new Date().toISOString()}\n`; content += ' * DO NOT EDIT - This file is automatically maintained by metro-ior-resolver\n'; content += ' */\n\n'; // Add all cached type definitions for (const [ior, entry] of typeCache.entries()) { content += `// Source: ${entry.source || 'unknown'}`; if (entry.hash) { content += `\n// Hash: ${entry.hash}\n`; } else { content += '\n'; } content += `// Cached: ${new Date(entry.timestamp).toISOString()}\n`; content += entry.content; content += '\n\n'; } // Ensure directory exists if (!fs.existsSync(typesDir)) { fs.mkdirSync(typesDir, { recursive: true }); } // Write the file fs.writeFileSync(globalTypesPath, content, 'utf-8'); console.log(`[IOR Resolver] Updated auto-generated types: ${globalTypesPath}`); } /** * Fetch remote component based on IOR */ async function fetchRemoteComponent(ior) { // Check cache first const cached = getCachedRemoteComponent(ior); if (cached) { return cached; } const config = parseRemoteIOR(ior); if (!config) { throw new Error(`Invalid remote IOR format: ${ior}`); } console.log(`[IOR Resolver] Fetching remote component: ${ior}`); let result; switch (config.type) { case 'github': result = await fetchFromGitHub(config); break; case 'gitea': result = await fetchFromGitea(config); break; case 'ipfs': result = await fetchFromIPFS(config); break; case 'p2p': throw new Error(`P2P fetching not yet implemented for Metro. PeerID: ${config.peerId}`); default: throw new Error(`Unsupported remote type: ${config.type}`); } // Cache the fetched component // For GitHub and Gitea, result contains { archive, componentName } // For IPFS, result contains { content, url } if (result.archive) { const cachedPath = await cacheRemoteComponent(ior, result.archive, result.componentName); return cachedPath; } else { // Legacy path for IPFS (still returns content directly) // Create a simple archive-like structure for IPFS content ensureCacheDir(); const hash = generateCacheHash(ior); const cacheDir = path.join(CACHE_DIR, hash); if (!fs.existsSync(cacheDir)) { fs.mkdirSync(cacheDir, { recursive: true }); } // Write content to index.ts const entryPath = path.join(cacheDir, 'index.ts'); fs.writeFileSync(entryPath, result.content, 'utf-8'); // Update cache remoteCache.set(ior, { path: entryPath, directory: cacheDir, timestamp: Date.now(), ttl: CACHE_TTL, }); updatePersistentMappings(ior, entryPath, cacheDir); return entryPath; } } /** * Enhanced resolver function for Metro with remote support */ function createEnhancedIORResolver(projectRoot) { // Build local mappings on initialization const localMappings = buildLocalIORMappings(projectRoot); return function enhancedIORResolver(context, moduleName, platform) { // Only handle IOR imports if (!moduleName.startsWith('ior:')) { // Not an IOR import, return undefined for default resolution return undefined; } // First check local mappings const localPath = localMappings.get(moduleName); if (localPath) { console.log(`[IOR Resolver] Resolving local: ${moduleName} -> ${localPath}`); // Check if file exists with various extensions const extensions = ['', '.ts', '.tsx', '.js', '.jsx']; for (const ext of extensions) { const fullPath = localPath.endsWith('.ts') || localPath.endsWith('.tsx') || localPath.endsWith('.js') || localPath.endsWith('.jsx') ? localPath : localPath + ext; if (fs.existsSync(fullPath)) { return { type: 'sourceFile', filePath: path.resolve(fullPath), }; } } // Try index files if main file not found const dirPath = path.dirname(localPath); const indexExtensions = ['/index.ts', '/index.tsx', '/index.js', '/index.jsx']; for (const indexExt of indexExtensions) { const indexPath = dirPath + indexExt; if (fs.existsSync(indexPath)) { return { type: 'sourceFile', filePath: path.resolve(indexPath), }; } } } // Check if it's a remote IOR (synchronously for now) const remoteConfig = parseRemoteIOR(moduleName); if (remoteConfig) { throw new Error(`Remote IOR components not yet supported in sync mode: ${moduleName}`); } // IOR not found throw new Error(`Cannot resolve IOR module: ${moduleName}`); }; } /** * Create a resolver with hot reload support * Rebuilds mappings when package.json files change */ function createHotReloadableEnhancedResolver(projectRoot) { let resolver = createEnhancedIORResolver(projectRoot); // Watch for package.json changes in components directory if (process.env.NODE_ENV === 'development') { const componentsDir = path.join(projectRoot, 'src', 'components'); try { // Simple file watcher for package.json files const watchDirs = (dir) => { fs.readdir(dir, { withFileTypes: true }, (err, entries) => { if (err) return; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { watchDirs(fullPath); } else if (entry.name === 'package.json') { fs.watchFile(fullPath, { interval: 1000 }, () => { console.log(`[IOR Resolver] Detected change in ${fullPath}, rebuilding mappings...`); resolver = createEnhancedIORResolver(projectRoot); }); } } }); }; watchDirs(componentsDir); } catch (err) { console.warn('[IOR Resolver] Could not set up file watching:', err); } } // Return a wrapper function that always uses the latest resolver return (context, moduleName, platform) => { return resolver(context, moduleName, platform); }; } /** * Clear remote cache */ function clearRemoteCache() { remoteCache.clear(); if (fs.existsSync(CACHE_DIR)) { const files = fs.readdirSync(CACHE_DIR); for (const file of files) { try { fs.unlinkSync(path.join(CACHE_DIR, file)); } catch { // Ignore errors } } } console.log('[IOR Resolver] Remote cache cleared'); } /** * Update persistent IOR mappings */ function updatePersistentMappings(ior, entryPath, cacheDir) { const mappingFile = path.join(process.cwd(), '.ior-cache', 'mappings.json'); try { // Load existing mappings or create new let data = { version: '1.0', mappings: {} }; if (fs.existsSync(mappingFile)) { try { data = JSON.parse(fs.readFileSync(mappingFile, 'utf-8')); } catch { // Use default if file is corrupted } } // Update mappings const hash = path.basename(cacheDir); data.mappings[ior] = { path: entryPath, directory: cacheDir, hash: hash, timestamp: Date.now(), }; data.generated = new Date().toISOString(); // Save mappings const mappingDir = path.dirname(mappingFile); if (!fs.existsSync(mappingDir)) { fs.mkdirSync(mappingDir, { recursive: true }); } fs.writeFileSync(mappingFile, JSON.stringify(data, null, 2), 'utf-8'); } catch (err) { // Non-critical error, just log console.warn('[IOR Resolver] Failed to update mappings:', err.message); } } /** * Load persistent IOR mappings if available */ function loadIORMappings() { const mappingFile = path.join(process.cwd(), '.ior-cache', 'mappings.json'); if (fs.existsSync(mappingFile)) { try { const data = JSON.parse(fs.readFileSync(mappingFile, 'utf-8')); console.log(`[IOR Resolver] Loaded IOR mappings from ${mappingFile}`); // Restore mappings to remoteCache if (data.mappings) { for (const [ior, mapping] of Object.entries(data.mappings)) { // Verify the cached files still exist if (fs.existsSync(mapping.path)) { remoteCache.set(ior, { path: mapping.path, directory: mapping.directory, timestamp: mapping.timestamp, ttl: CACHE_TTL, }); } } console.log(`[IOR Resolver] Restored ${remoteCache.size} cached components`); } return true; } catch (err) { console.warn('[IOR Resolver] Failed to load mappings:', err.message); } } return false; } /** * Initialize type declarations for existing cached components */ async function initializeTypesFromCache() { if (!fs.existsSync(CACHE_DIR)) { return; } console.log('[IOR Resolver] Scanning cache for existing components...'); // First try to load persistent mappings loadIORMappings(); try { const dirs = fs.readdirSync(CACHE_DIR, { withFileTypes: true }); for (const dir of dirs) { if (dir.isDirectory()) { const cacheDir = path.join(CACHE_DIR, dir.name); // Try to determine the IOR from the remoteCache let foundIor = null; for (const [ior, entry] of remoteCache.entries()) { if (entry.directory === cacheDir) { foundIor = ior; break; } } if (!foundIor) { // Try to read package.json to get hints about the component const pkgPath = path.join(cacheDir, 'package.json'); if (fs.existsSync(pkgPath)) { try { const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); if (pkg.name && pkg.version) { console.log(`[IOR Resolver] Found cached component without IOR mapping: ${pkg.name}@${pkg.version}`); } } catch { // Skip if we can't read package.json } } continue; } // Generate types for this cached component await generateTypeDeclaration(foundIor, cacheDir); } } console.log('[IOR Resolver] Finished scanning cache'); } catch (err) { console.warn('[IOR Resolver] Failed to initialize types from cache:', err.message); } } // Initialize types from cache on module load initializeTypesFromCache().catch(err => { console.warn('[IOR Resolver] Failed to initialize types:', err.message); }); // Export unified functions module.exports = { // Main exports createIORResolver: createEnhancedIORResolver, createHotReloadableResolver: createHotReloadableEnhancedResolver, buildIORMappings: buildLocalIORMappings, buildLocalIORMappings, fetchRemoteComponent, parseRemoteIOR, clearRemoteCache, };