Initial commit
This commit is contained in:
26
lib/fetch-remote-component.js
Normal file
26
lib/fetch-remote-component.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Suppress console.log from the resolver during fetching
|
||||
const originalLog = console.log;
|
||||
console.log = () => {};
|
||||
|
||||
const { fetchRemoteComponent } = require('./metro-ior-resolver.js');
|
||||
|
||||
// Restore console.log for our output
|
||||
console.log = originalLog;
|
||||
|
||||
const ior = process.argv[2];
|
||||
if (!ior) {
|
||||
console.error('No IOR provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fetchRemoteComponent(ior)
|
||||
.then(path => {
|
||||
// Output only the path on stdout
|
||||
console.log(path);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
// Output error to stderr
|
||||
console.error('FETCH_ERROR:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
440
lib/ior-core.js
Normal file
440
lib/ior-core.js
Normal file
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* Core IOR resolver functionality shared between Metro and Node.js implementations
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const https = require('node:https');
|
||||
const http = require('node:http');
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
/**
|
||||
* Parse remote IOR format
|
||||
* Format examples:
|
||||
* - ior:github:owner/repo:componentname@version
|
||||
* - ior:gitea:instance.com:owner/repo:componentname@version
|
||||
* - ior:ipfs:QmHash:componentname@version
|
||||
* - ior:p2p:peer-id:componentname@version
|
||||
*/
|
||||
function parseRemoteIOR(ior) {
|
||||
const parts = ior.split(':');
|
||||
|
||||
if (parts.length < 4 || parts[0] !== 'ior') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = parts[1];
|
||||
|
||||
switch (type) {
|
||||
case 'github': {
|
||||
// ior:github:owner/repo:componentname@version
|
||||
const [, , repoPath, componentAndVersion] = parts;
|
||||
const [owner, repo] = repoPath.split('/');
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'github',
|
||||
owner,
|
||||
repository: repo,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
case 'gitea': {
|
||||
// Two possible formats:
|
||||
// ior:gitea:instance.com:owner/repo:componentname@version (5 parts)
|
||||
// ior:gitea:instance.com:owner/repo/componentname@version (4 parts - component name in path)
|
||||
|
||||
if (parts.length === 4) {
|
||||
// Format: ior:gitea:instance.com:owner/repo/componentname@version
|
||||
const [, , instance, pathAndVersion] = parts;
|
||||
const [fullPath, version] = pathAndVersion.split('@');
|
||||
const pathParts = fullPath.split('/');
|
||||
|
||||
const owner = pathParts[0];
|
||||
const componentName = pathParts[pathParts.length - 1];
|
||||
const repository = pathParts.slice(1).join('/') || componentName;
|
||||
|
||||
return {
|
||||
type: 'gitea',
|
||||
instance,
|
||||
owner,
|
||||
repository,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
} else if (parts.length === 5) {
|
||||
// Format: ior:gitea:instance.com:owner/repo:componentname@version
|
||||
const [, , instance, repoPath, componentAndVersion] = parts;
|
||||
const repoPathParts = repoPath.split('/');
|
||||
const owner = repoPathParts[0];
|
||||
const repository = repoPathParts.slice(1).join('/');
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'gitea',
|
||||
instance,
|
||||
owner,
|
||||
repository,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'ipfs': {
|
||||
// ior:ipfs:QmHash:componentname@version
|
||||
const [, , hash, componentAndVersion] = parts;
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'ipfs',
|
||||
hash,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
case 'p2p': {
|
||||
// ior:p2p:peer-id:componentname@version
|
||||
const [, , peerId, componentAndVersion] = parts;
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'p2p',
|
||||
peerId,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse package.json to find IOR mapping
|
||||
*/
|
||||
function parsePackageJson(packagePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(packagePath, 'utf-8');
|
||||
const pkg = JSON.parse(content);
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse package.json asynchronously
|
||||
*/
|
||||
async function parsePackageJsonAsync(packagePath) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(packagePath, 'utf-8');
|
||||
const pkg = JSON.parse(content);
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content from URL
|
||||
*/
|
||||
function fetchUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https:') ? https : http;
|
||||
|
||||
client.get(url, (res) => {
|
||||
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||
// Handle redirects
|
||||
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch URL and return as Buffer
|
||||
*/
|
||||
async function fetchUrlBuffer(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https:') ? https : http;
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
// Handle redirects
|
||||
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||
return fetchUrlBuffer(response.headers.location).then(resolve).catch(reject);
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
response.on('error', reject);
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component archive from GitHub
|
||||
*/
|
||||
async function fetchFromGitHub(config) {
|
||||
const { owner, repository, componentName, version } = config;
|
||||
|
||||
// Try both tag formats: "1.0.0" and "v1.0.0"
|
||||
const tagVariants = [version, `v${version}`];
|
||||
|
||||
for (const tag of tagVariants) {
|
||||
// GitHub API endpoint for tarball
|
||||
const archiveUrl = `https://api.github.com/repos/${owner}/${repository}/tarball/${tag}`;
|
||||
|
||||
try {
|
||||
console.log(`[IOR Resolver] Fetching GitHub archive: ${archiveUrl}`);
|
||||
const archiveBuffer = await fetchUrlBuffer(archiveUrl);
|
||||
console.log(`[IOR Resolver] Downloaded archive from GitHub (${archiveBuffer.length} bytes)`);
|
||||
return { archive: archiveBuffer, componentName };
|
||||
} catch (err) {
|
||||
console.log(`[IOR Resolver] Failed to fetch with tag ${tag}: ${err.message}`);
|
||||
// Try next tag variant
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch component from GitHub: ${owner}/${repository}@${version} (tried tags: ${tagVariants.join(', ')})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component archive from Gitea
|
||||
*/
|
||||
async function fetchFromGitea(config) {
|
||||
const { instance, owner, repository, componentName, version } = config;
|
||||
|
||||
// Try both tag formats: "1.0.0" and "v1.0.0"
|
||||
const tagVariants = [version, `v${version}`];
|
||||
|
||||
for (const tag of tagVariants) {
|
||||
// Try to download the archive
|
||||
const archiveUrl = `https://${instance}/api/v1/repos/${owner}/${repository}/archive/${tag}.tar.gz`;
|
||||
|
||||
try {
|
||||
console.log(`[IOR Resolver] Fetching Gitea archive: ${archiveUrl}`);
|
||||
const archiveBuffer = await fetchUrlBuffer(archiveUrl);
|
||||
console.log(`[IOR Resolver] Downloaded archive from Gitea (${archiveBuffer.length} bytes)`);
|
||||
return { archive: archiveBuffer, componentName };
|
||||
} catch (err) {
|
||||
console.log(`[IOR Resolver] Failed to fetch with tag ${tag}: ${err.message}`);
|
||||
// Try next tag variant
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch component from Gitea: ${instance}/${owner}/${repository}@${version} (tried tags: ${tagVariants.join(', ')})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component from IPFS
|
||||
*/
|
||||
async function fetchFromIPFS(config) {
|
||||
const { hash, componentName, version } = config;
|
||||
|
||||
// Try multiple IPFS gateways for redundancy
|
||||
const gateways = [
|
||||
`https://ipfs.io/ipfs/${hash}`,
|
||||
`https://gateway.pinata.cloud/ipfs/${hash}`,
|
||||
`https://cloudflare-ipfs.com/ipfs/${hash}`,
|
||||
];
|
||||
|
||||
for (const gateway of gateways) {
|
||||
try {
|
||||
// Try to get main file from package.json first
|
||||
const packageUrl = `${gateway}/${componentName}/${version}/package.json`;
|
||||
let mainFile = `${componentName}.ts`;
|
||||
|
||||
try {
|
||||
const pkgContent = await fetchUrl(packageUrl);
|
||||
const pkgData = JSON.parse(pkgContent);
|
||||
if (pkgData.main) {
|
||||
mainFile = pkgData.main;
|
||||
}
|
||||
} catch {
|
||||
// Use default
|
||||
}
|
||||
|
||||
const urls = [
|
||||
`${gateway}/${componentName}/${version}/${mainFile}`,
|
||||
`${gateway}/${componentName}/${version}/index.ts`,
|
||||
`${gateway}/${componentName}/${version}/index.tsx`,
|
||||
];
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const content = await fetchUrl(url);
|
||||
return { content, url };
|
||||
} catch {
|
||||
// Try next URL
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Try next gateway
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch from IPFS: ${hash}/${componentName}@${version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract archive to directory
|
||||
*/
|
||||
function extractArchive(archiveBuffer, targetDir, stripComponents = 1) {
|
||||
const tempArchive = path.join(targetDir, '..', `temp-${Date.now()}.tar.gz`);
|
||||
|
||||
// Ensure target directory exists
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write archive to temp file
|
||||
fs.writeFileSync(tempArchive, archiveBuffer);
|
||||
|
||||
try {
|
||||
// Extract archive
|
||||
execSync(`tar -xzf "${tempArchive}" -C "${targetDir}" --strip-components=${stripComponents}`, {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
// Clean up temp archive
|
||||
fs.unlinkSync(tempArchive);
|
||||
} catch (err) {
|
||||
// Clean up on failure
|
||||
if (fs.existsSync(tempArchive)) {
|
||||
fs.unlinkSync(tempArchive);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find entry point in a directory
|
||||
*/
|
||||
function findEntryPoint(dir, componentName) {
|
||||
let entryPath = null;
|
||||
|
||||
// Check package.json for main field
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
if (packageJson.main) {
|
||||
const mainPath = path.join(dir, packageJson.main);
|
||||
if (fs.existsSync(mainPath)) {
|
||||
return mainPath;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore package.json errors
|
||||
}
|
||||
}
|
||||
|
||||
// Try common entry points
|
||||
const possibleEntries = [
|
||||
path.join(dir, 'index.ts'),
|
||||
path.join(dir, 'index.tsx'),
|
||||
path.join(dir, 'index.js'),
|
||||
path.join(dir, 'index.jsx'),
|
||||
path.join(dir, `${componentName}.ts`),
|
||||
path.join(dir, `${componentName}.tsx`),
|
||||
path.join(dir, `${componentName}.js`),
|
||||
path.join(dir, `${componentName}.jsx`),
|
||||
];
|
||||
|
||||
for (const entry of possibleEntries) {
|
||||
if (fs.existsSync(entry)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache hash for an IOR
|
||||
*/
|
||||
function generateCacheHash(ior) {
|
||||
return crypto.createHash('sha256').update(ior).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan components directory to build local IOR mappings
|
||||
*/
|
||||
function buildLocalIORMappings(projectRoot) {
|
||||
const componentsDir = path.join(projectRoot, 'src', 'components');
|
||||
const mappings = new Map();
|
||||
|
||||
try {
|
||||
const componentDirs = fs.readdirSync(componentsDir, { withFileTypes: true });
|
||||
|
||||
for (const dir of componentDirs) {
|
||||
if (dir.isDirectory()) {
|
||||
const componentPath = path.join(componentsDir, dir.name);
|
||||
const versions = fs.readdirSync(componentPath, { withFileTypes: true });
|
||||
|
||||
for (const version of versions) {
|
||||
if (version.isDirectory()) {
|
||||
const versionPath = path.join(componentPath, version.name);
|
||||
const packagePath = path.join(versionPath, 'package.json');
|
||||
|
||||
const pkg = parsePackageJson(packagePath);
|
||||
if (pkg?.metatrom?.ior) {
|
||||
// Map both with and without esm: prefix
|
||||
const ior = pkg.metatrom.ior;
|
||||
const mainFile = pkg.main || 'index.ts';
|
||||
const resolvedPath = path.join(versionPath, mainFile);
|
||||
|
||||
mappings.set(`ior:esm:${ior}`, resolvedPath);
|
||||
mappings.set(`ior:${ior}`, resolvedPath);
|
||||
|
||||
console.log(`[IOR Resolver] Mapped local: ${ior} -> ${resolvedPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[IOR Resolver] Error building local mappings:', err);
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseRemoteIOR,
|
||||
parsePackageJson,
|
||||
parsePackageJsonAsync,
|
||||
fetchUrl,
|
||||
fetchUrlBuffer,
|
||||
fetchFromGitHub,
|
||||
fetchFromGitea,
|
||||
fetchFromIPFS,
|
||||
extractArchive,
|
||||
findEntryPoint,
|
||||
generateCacheHash,
|
||||
buildLocalIORMappings,
|
||||
};
|
||||
580
lib/metro-ior-resolver.js
Normal file
580
lib/metro-ior-resolver.js
Normal file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
const CACHE_TTL = 3600000; // 1 hour default TTL
|
||||
// Use project-local cache directory instead of temp directory to avoid watchman issues
|
||||
const CACHE_DIR = path.join(process.cwd(), '.ior-cache', 'remote');
|
||||
|
||||
/**
|
||||
* 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 (Date.now() - entry.timestamp > entry.ttl) {
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user