5 Commits

Author SHA1 Message Date
Chris Daßler
d6be6ec309 Update @react-native/metro-config peer dependency to >=0.79.0
Changed from >=0.79.4 to >=0.79.0 to avoid frequent updates.
This supports React Native 0.79.x series including 0.79.6.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 14:04:04 +02:00
Chris Daßler
163823dfd9 Lower @react-native/metro-config peer dependency to >=0.79.4
Support React Native 0.79.4 to avoid Flow v0.275.0 compatibility issues
that prevent Jest tests from running properly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 13:34:55 +02:00
Chris Daßler
41df685c51 chore: bump version to 1.1.0-beta.3
- Lower @react-native/metro-config peer dependency to >=0.79.6
- Enables compatibility with React Native 0.79.x projects

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 12:57:26 +02:00
Chris Daßler
e05f091342 Fix Metro bundler refresh issue by preventing unnecessary file writes
- Only write auto-generated.d.ts when actual content changes
- Strip timestamp lines when comparing content to detect real changes
- Prevents Metro from refreshing when type content is unchanged
- Fixes flickering "Refreshing..." issue across all connected devices

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 22:40:57 +02:00
Chris Daßler
6e775258d1 feat: add branch support, sync resolution, and dynamic cache TTL
- Add branch detection in parseRemoteIOR for GitHub and Gitea
- Support fetching from branches in addition to tags
- Implement synchronous remote resolution using sync-fetch
- Add dynamic cache TTL based on version type (beta/rc/branch/stable)
- Export sync functions for Metro bundler usage
- Fix remote component resolution in Metro (no more sync errors)
2025-09-29 12:32:41 +02:00
4 changed files with 301 additions and 5595 deletions

View File

@@ -31,14 +31,23 @@ function parseRemoteIOR(ior) {
// ior:github:owner/repo:componentname@version // ior:github:owner/repo:componentname@version
const [, , repoPath, componentAndVersion] = parts; const [, , repoPath, componentAndVersion] = parts;
const [owner, repo] = repoPath.split('/'); const [owner, repo] = repoPath.split('/');
const [componentName, version] = componentAndVersion.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 { return {
type: 'github', type: 'github',
owner, owner,
repository: repo, repository: repo,
componentName, componentName,
version, version: !isBranch ? versionOrBranch : undefined,
branch: isBranch ? versionOrBranch : undefined,
}; };
} }
@@ -50,20 +59,29 @@ function parseRemoteIOR(ior) {
if (parts.length === 4) { if (parts.length === 4) {
// Format: ior:gitea:instance.com:owner/repo/componentname@version // Format: ior:gitea:instance.com:owner/repo/componentname@version
const [, , instance, pathAndVersion] = parts; const [, , instance, pathAndVersion] = parts;
const [fullPath, version] = pathAndVersion.split('@'); const [fullPath, versionOrBranch] = pathAndVersion.split('@');
const pathParts = fullPath.split('/'); const pathParts = fullPath.split('/');
const owner = pathParts[0]; const owner = pathParts[0];
const componentName = pathParts[pathParts.length - 1]; const componentName = pathParts[pathParts.length - 1];
const repository = pathParts.slice(1).join('/') || componentName; 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 { return {
type: 'gitea', type: 'gitea',
instance, instance,
owner, owner,
repository, repository,
componentName, componentName,
version, version: !isBranch ? versionOrBranch : undefined,
branch: isBranch ? versionOrBranch : undefined,
}; };
} else if (parts.length === 5) { } else if (parts.length === 5) {
// Format: ior:gitea:instance.com:owner/repo:componentname@version // Format: ior:gitea:instance.com:owner/repo:componentname@version
@@ -71,7 +89,15 @@ function parseRemoteIOR(ior) {
const repoPathParts = repoPath.split('/'); const repoPathParts = repoPath.split('/');
const owner = repoPathParts[0]; const owner = repoPathParts[0];
const repository = repoPathParts.slice(1).join('/'); const repository = repoPathParts.slice(1).join('/');
const [componentName, version] = componentAndVersion.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 { return {
type: 'gitea', type: 'gitea',
@@ -79,7 +105,8 @@ function parseRemoteIOR(ior) {
owner, owner,
repository, repository,
componentName, componentName,
version, version: !isBranch ? versionOrBranch : undefined,
branch: isBranch ? versionOrBranch : undefined,
}; };
} }
@@ -199,9 +226,23 @@ async function fetchUrlBuffer(url) {
* Fetch component archive from GitHub * Fetch component archive from GitHub
*/ */
async function fetchFromGitHub(config) { async function fetchFromGitHub(config) {
const { owner, repository, componentName, version } = config; const { owner, repository, componentName, version, branch } = config;
// Try both tag formats: "1.0.0" and "v1.0.0" // 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}`]; const tagVariants = [version, `v${version}`];
for (const tag of tagVariants) { for (const tag of tagVariants) {
@@ -226,9 +267,23 @@ async function fetchFromGitHub(config) {
* Fetch component archive from Gitea * Fetch component archive from Gitea
*/ */
async function fetchFromGitea(config) { async function fetchFromGitea(config) {
const { instance, owner, repository, componentName, version } = config; const { instance, owner, repository, componentName, version, branch } = config;
// Try both tag formats: "1.0.0" and "v1.0.0" // 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}`]; const tagVariants = [version, `v${version}`];
for (const tag of tagVariants) { for (const tag of tagVariants) {

View File

@@ -5,6 +5,7 @@
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const syncFetch = require('sync-fetch');
const { const {
parseRemoteIOR, parseRemoteIOR,
parsePackageJson, parsePackageJson,
@@ -28,6 +29,26 @@ const remoteCache = new Map();
const typeCache = new Map(); const typeCache = new Map();
const processedTypes = new Set(); const processedTypes = new Set();
/**
* Determine cache TTL based on version type
*/
function determineCacheTTL(ior) {
const parts = ior.split('@');
const versionOrBranch = parts[parts.length - 1];
if (versionOrBranch.includes('-beta') || versionOrBranch.includes('-alpha')) {
return 5 * 60 * 1000; // 5 minutes for beta/alpha
} else if (versionOrBranch.includes('-rc')) {
return 60 * 60 * 1000; // 1 hour for RC
} else if (versionOrBranch.includes('/')) {
return 60 * 1000; // 1 minute for branches
} else {
return process.env.IOR_CACHE_TTL !== undefined
? parseInt(process.env.IOR_CACHE_TTL)
: 3600000; // Default 1 hour for stable
}
}
/** /**
* Remote cache configuration * Remote cache configuration
*/ */
@@ -90,12 +111,13 @@ async function cacheRemoteComponent(ior, archiveBuffer, componentName) {
throw new Error(`No entry point found for component ${componentName} in ${cacheDir}`); throw new Error(`No entry point found for component ${componentName} in ${cacheDir}`);
} }
// Update cache map with entry point // Update cache map with entry point using dynamic TTL
const ttl = determineCacheTTL(ior);
remoteCache.set(ior, { remoteCache.set(ior, {
path: entryPath, path: entryPath,
directory: cacheDir, directory: cacheDir,
timestamp: Date.now(), timestamp: Date.now(),
ttl: CACHE_TTL, ttl: ttl,
}); });
console.log(`[IOR Resolver] Cached remote component: ${ior}`); console.log(`[IOR Resolver] Cached remote component: ${ior}`);
@@ -256,11 +278,26 @@ async function updateGlobalTypes() {
fs.mkdirSync(typesDir, { recursive: true }); fs.mkdirSync(typesDir, { recursive: true });
} }
// Only write if content has changed (compare without timestamp)
let shouldWrite = true;
if (fs.existsSync(globalTypesPath)) {
const existingContent = fs.readFileSync(globalTypesPath, 'utf-8');
// Remove timestamp lines for comparison
const stripTimestamp = (str) => str.replace(/ \* Generated at: .*\n/g, '').replace(/\/\/ Cached: .*\n/g, '');
if (stripTimestamp(existingContent) === stripTimestamp(content)) {
shouldWrite = false;
console.log(`[IOR Resolver] Type declarations unchanged, skipping write to prevent Metro refresh`);
}
}
if (shouldWrite) {
// Write the file // Write the file
fs.writeFileSync(globalTypesPath, content, 'utf-8'); fs.writeFileSync(globalTypesPath, content, 'utf-8');
console.log(`[IOR Resolver] Updated auto-generated types: ${globalTypesPath}`); console.log(`[IOR Resolver] Updated auto-generated types: ${globalTypesPath}`);
} }
}
/** /**
* Fetch remote component based on IOR * Fetch remote component based on IOR
@@ -335,6 +372,131 @@ async function fetchRemoteComponent(ior) {
} }
} }
/**
* Synchronously fetch and cache remote component
*/
function fetchRemoteComponentSync(ior, config) {
// Check cache first
const cached = getCachedRemoteComponent(ior);
if (cached) {
return cached;
}
console.log(`[IOR Resolver] Sync fetching remote component: ${ior}`);
// Determine URL based on config
let archiveUrl;
if (config.branch) {
// Branch-based URL
if (config.type === 'github') {
archiveUrl = `https://api.github.com/repos/${config.owner}/${config.repository}/tarball/${config.branch}`;
} else if (config.type === 'gitea') {
archiveUrl = `https://${config.instance}/api/v1/repos/${config.owner}/${config.repository}/archive/${config.branch}.tar.gz`;
} else {
throw new Error(`Branch fetching not supported for type: ${config.type}`);
}
} else if (config.version) {
// Version-based URL (try with and without 'v' prefix)
const version = config.version;
if (config.type === 'github') {
archiveUrl = `https://api.github.com/repos/${config.owner}/${config.repository}/tarball/${version}`;
} else if (config.type === 'gitea') {
archiveUrl = `https://${config.instance}/api/v1/repos/${config.owner}/${config.repository}/archive/${version}.tar.gz`;
} else {
throw new Error(`Version fetching not supported for type: ${config.type}`);
}
} else {
throw new Error(`No version or branch specified in config`);
}
try {
// Sync fetch using sync-fetch
console.log(`[IOR Resolver] Fetching from: ${archiveUrl}`);
const response = syncFetch(archiveUrl, {
headers: {
'User-Agent': 'IOR-Resolver/1.0',
'Accept': 'application/octet-stream',
},
});
if (!response.ok) {
// Try with 'v' prefix if it's a version
if (config.version && !config.version.startsWith('v')) {
const versionWithV = `v${config.version}`;
const altUrl = archiveUrl.replace(config.version, versionWithV);
console.log(`[IOR Resolver] Retrying with v prefix: ${altUrl}`);
const altResponse = syncFetch(altUrl, {
headers: {
'User-Agent': 'IOR-Resolver/1.0',
'Accept': 'application/octet-stream',
},
});
if (altResponse.ok) {
const buffer = Buffer.from(altResponse.arrayBuffer());
return cacheRemoteComponentSync(ior, buffer, config.componentName);
}
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const buffer = Buffer.from(response.arrayBuffer());
return cacheRemoteComponentSync(ior, buffer, config.componentName);
} catch (err) {
console.error(`[IOR Resolver] Sync fetch failed: ${err.message}`);
throw new Error(`Failed to fetch remote component: ${ior} - ${err.message}`);
}
}
/**
* Synchronously cache remote component
*/
function cacheRemoteComponentSync(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 using dynamic TTL
const ttl = determineCacheTTL(ior);
remoteCache.set(ior, {
path: entryPath,
directory: cacheDir,
timestamp: Date.now(),
ttl: ttl,
});
console.log(`[IOR Resolver] Sync cached remote component: ${ior}`);
// 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;
}
/** /**
* Enhanced resolver function for Metro with remote support * Enhanced resolver function for Metro with remote support
*/ */
@@ -384,10 +546,18 @@ function createEnhancedIORResolver(projectRoot) {
} }
} }
// Check if it's a remote IOR (synchronously for now) // Check if it's a remote IOR - now with sync support!
const remoteConfig = parseRemoteIOR(moduleName); const remoteConfig = parseRemoteIOR(moduleName);
if (remoteConfig) { if (remoteConfig) {
throw new Error(`Remote IOR components not yet supported in sync mode: ${moduleName}`); try {
const entryPath = fetchRemoteComponentSync(moduleName, remoteConfig);
return {
type: 'sourceFile',
filePath: path.resolve(entryPath),
};
} catch (err) {
throw new Error(`Failed to resolve remote IOR: ${moduleName} - ${err.message}`);
}
} }
// IOR not found // IOR not found
@@ -599,9 +769,16 @@ module.exports = {
// Main exports // Main exports
createIORResolver: createEnhancedIORResolver, createIORResolver: createEnhancedIORResolver,
createHotReloadableResolver: createHotReloadableEnhancedResolver, createHotReloadableResolver: createHotReloadableEnhancedResolver,
// Sync functions for Metro
fetchRemoteComponentSync,
cacheRemoteComponentSync,
// Existing exports
buildIORMappings: buildLocalIORMappings, buildIORMappings: buildLocalIORMappings,
buildLocalIORMappings, buildLocalIORMappings,
fetchRemoteComponent, fetchRemoteComponent,
parseRemoteIOR, parseRemoteIOR,
clearRemoteCache, clearRemoteCache,
determineCacheTTL,
}; };

5583
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@metatrom/ior-resolver", "name": "@metatrom/ior-resolver",
"version": "1.0.0", "version": "1.1.0-beta.5",
"description": "Interoperable Object Reference (IOR) resolver for Metro bundler and Node.js with support for local and remote component fetching", "description": "Interoperable Object Reference (IOR) resolver for Metro bundler and Node.js with support for local and remote component fetching",
"main": "index.js", "main": "index.js",
"types": "index.d.ts", "types": "index.d.ts",
@@ -56,10 +56,11 @@
"author": "Metatrom", "author": "Metatrom",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"typescript": "^5.0.0" "typescript": "^5.0.0",
"sync-fetch": "^0.5.2"
}, },
"peerDependencies": { "peerDependencies": {
"@react-native/metro-config": ">=0.80.0" "@react-native/metro-config": ">=0.79.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@react-native/metro-config": { "@react-native/metro-config": {