Show clear cache TTL settings when Metro starts: - Displays whether cache is disabled (TTL=0) - Shows custom TTL in human-readable format (e.g., 1m 30s) - Shows default TTL when no env variable is set - Includes cache directory location This helps developers understand what cache settings are active.
607 lines
17 KiB
JavaScript
607 lines
17 KiB
JavaScript
/**
|
|
* 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,
|
|
}; |