/** * 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, versionOrBranch] = componentAndVersion.split('@'); // Detect if it's a branch const isBranch = versionOrBranch && ( versionOrBranch.includes('/') || versionOrBranch.startsWith('main') || versionOrBranch.startsWith('master') || versionOrBranch.startsWith('dev') ); return { type: 'github', owner, repository: repo, componentName, version: !isBranch ? versionOrBranch : undefined, branch: isBranch ? versionOrBranch : undefined, }; } 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, versionOrBranch] = pathAndVersion.split('@'); const pathParts = fullPath.split('/'); const owner = pathParts[0]; const componentName = pathParts[pathParts.length - 1]; const repository = pathParts.slice(1).join('/') || componentName; // Detect if it's a branch const isBranch = versionOrBranch && ( versionOrBranch.includes('/') || versionOrBranch.startsWith('main') || versionOrBranch.startsWith('master') || versionOrBranch.startsWith('dev') ); return { type: 'gitea', instance, owner, repository, componentName, version: !isBranch ? versionOrBranch : undefined, branch: isBranch ? versionOrBranch : undefined, }; } 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, versionOrBranch] = componentAndVersion.split('@'); // Detect if it's a branch const isBranch = versionOrBranch && ( versionOrBranch.includes('/') || versionOrBranch.startsWith('main') || versionOrBranch.startsWith('master') || versionOrBranch.startsWith('dev') ); return { type: 'gitea', instance, owner, repository, componentName, version: !isBranch ? versionOrBranch : undefined, branch: isBranch ? versionOrBranch : undefined, }; } 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, branch } = config; // If it's a branch, fetch from branch if (branch) { const archiveUrl = `https://api.github.com/repos/${owner}/${repository}/tarball/${branch}`; try { console.log(`[IOR Resolver] Fetching GitHub branch: ${branch}`); const archiveBuffer = await fetchUrlBuffer(archiveUrl); console.log(`[IOR Resolver] Downloaded archive from GitHub branch ${branch} (${archiveBuffer.length} bytes)`); return { archive: archiveBuffer, componentName }; } catch (err) { throw new Error(`Failed to fetch branch ${branch} from GitHub: ${owner}/${repository} - ${err.message}`); } } // Otherwise, try 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, branch } = config; // If it's a branch, fetch from branch if (branch) { const archiveUrl = `https://${instance}/api/v1/repos/${owner}/${repository}/archive/${branch}.tar.gz`; try { console.log(`[IOR Resolver] Fetching Gitea branch: ${branch}`); const archiveBuffer = await fetchUrlBuffer(archiveUrl); console.log(`[IOR Resolver] Downloaded archive from Gitea branch ${branch} (${archiveBuffer.length} bytes)`); return { archive: archiveBuffer, componentName }; } catch (err) { throw new Error(`Failed to fetch branch ${branch} from Gitea: ${instance}/${owner}/${repository} - ${err.message}`); } } // Otherwise, try 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, };