Fix package by testing real world examples (#1)

Co-authored-by: Chris Daßler <chris.dassler@me.com>
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
2025-08-29 07:42:17 +02:00
parent 7d1935dde5
commit 54d0d245d6
8 changed files with 6393 additions and 26 deletions

3
.gitignore vendored
View File

@@ -95,3 +95,6 @@ $RECYCLE.BIN/
*.lnk *.lnk
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode
# NodeJs
node_modules/

View File

@@ -23,13 +23,16 @@ npm install @metatrom/ior-resolver
``` ```
@metatrom/ior-resolver/ @metatrom/ior-resolver/
├── bin/ # Executable scripts ├── bin/ # Compiled executable scripts
│ ├── prebuild-ior-types.js # Pre-build script for type generation │ ├── prebuild-ior-types.js # Pre-build script for type generation
│ └── NodeImportLoader.ts # Node.js loader implementation │ └── NodeImportLoader.js # Compiled Node.js loader
├── lib/ # Core library files ├── lib/ # Core library files
│ ├── ior-core.js # Shared core functions │ ├── ior-core.js # Shared core functions
│ ├── metro-ior-resolver.js # Metro resolver implementation │ ├── metro-ior-resolver.js # Metro resolver implementation
│ └── fetch-remote-component.js # Remote fetching utility │ └── fetch-remote-component.js # Remote fetching utility
├── src/ # Source files
│ └── loaders/
│ └── NodeImportLoader.ts # Node.js loader TypeScript source
├── index.js # Main entry point ├── index.js # Main entry point
├── index.d.ts # TypeScript definitions ├── index.d.ts # TypeScript definitions
└── package.json └── package.json
@@ -224,9 +227,9 @@ The package is designed with a shared core architecture:
1. **ior-core.js** - Contains all shared functionality for parsing, fetching, and caching 1. **ior-core.js** - Contains all shared functionality for parsing, fetching, and caching
2. **metro-ior-resolver.js** - Metro-specific implementation using the shared core 2. **metro-ior-resolver.js** - Metro-specific implementation using the shared core
3. **NodeImportLoader.ts** - Node.js-specific implementation using the shared core 3. **NodeImportLoader.ts** - Node.js-specific implementation using the shared core (TypeScript source compiled to JavaScript)
This design eliminates code duplication and ensures consistency across different environments. This design eliminates code duplication and ensures consistency across different environments. The Node.js loader is written in TypeScript and compiled to JavaScript during the build process.
## Migration from Direct Implementation ## Migration from Direct Implementation
@@ -331,7 +334,10 @@ Contributions are welcome! Please feel free to submit a Pull Request.
1. Clone the repository 1. Clone the repository
2. Install dependencies: `npm install` 2. Install dependencies: `npm install`
3. Build TypeScript: `npm run build` 3. Build TypeScript: `npm run build`
4. Run tests: `npm test` 4. Build Node.js loader: `npm run build:loader`
5. Run tests: `npm test`
Note: The `prepublishOnly` script automatically runs both build commands before publishing.
### Code Structure ### Code Structure

548
bin/NodeImportLoader.js Normal file
View File

@@ -0,0 +1,548 @@
import { promises as fs } from 'node:fs';
import { register } from 'node:module';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript';
// Import shared core functions - will be loaded dynamically
let iorCore = null;
// Dynamic import of core functions
const loadIorCore = async () => {
if (!iorCore) {
iorCore = await import('../lib/ior-core.js');
}
return iorCore;
};
register('./src/loaders/NodeImportLoader.ts', pathToFileURL('./'));
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = process.cwd(); // Use current working directory since this is a bin script
const DEBUG = process.env.DEBUG === 'true';
// Create a simple console logger to avoid circular dependency issues
const logger = {
debug: (...args) => DEBUG && console.log('[NodeImportLoader:DEBUG]', ...args),
info: (...args) => console.log('[NodeImportLoader:INFO]', ...args),
warn: (...args) => console.warn('[NodeImportLoader:WARN]', ...args),
error: (...args) => console.error('[NodeImportLoader:ERROR]', ...args),
};
const componentCache = new Map();
const remoteCache = new Map();
const typeCache = new Map();
const processedTypes = new Set();
/**
* Fetch type definitions for a component
*/
async function fetchTypeDefinitions(config, componentName, version) {
const baseUrl = config.type === 'github'
? `https://raw.githubusercontent.com/${config.owner}/${config.repository}/${config.branch || 'main'}`
: config.type === 'gitea'
? `https://${config.instance}/${config.owner}/${config.repository}/raw/branch/${config.branch || 'main'}`
: null;
if (!baseUrl)
return null;
const componentPath = `src/components/${componentName}/${version}`;
const possiblePaths = [
`${componentPath}/${componentName}.d.ts`,
`${componentPath}/index.d.ts`,
`${componentPath}/types.d.ts`,
];
for (const typePath of possiblePaths) {
try {
const response = await fetch(`${baseUrl}/${typePath}`);
if (response.ok) {
return await response.text();
}
}
catch {
continue;
}
}
return null;
}
/**
* Generate and save type declarations
*/
async function generateTypeDeclaration(ior, isRemote = false) {
// Avoid duplicate processing
const cacheKey = `${isRemote ? 'remote:' : 'local:'}${ior}`;
if (processedTypes.has(cacheKey)) {
return;
}
try {
let typeContent = null;
if (isRemote) {
// For remote components, fetch type definitions
const core = await loadIorCore();
const config = core.parseRemoteIOR(`ior:${ior}`);
if (config) {
const parts = ior.split(':');
const componentAndVersion = parts[parts.length - 1];
const [componentName, version] = componentAndVersion.split('@');
typeContent = await fetchTypeDefinitions(config, componentName, version);
}
}
else {
// For local components, read the .d.ts file
const atIndex = ior.lastIndexOf('@');
const componentFullName = ior.substring(0, atIndex);
const version = ior.substring(atIndex + 1);
const componentName = componentFullName.split('.').pop();
if (!componentName) {
throw new Error(`Invalid IOR format: ${ior}`);
}
const componentsDir = path.join(projectRoot, 'src', 'components');
const possiblePaths = [
path.join(componentsDir, componentName, version, `${componentName}.d.ts`),
path.join(componentsDir, componentName, version, 'index.d.ts'),
path.join(componentsDir, componentName, version, 'types.d.ts'),
];
for (const dtsPath of possiblePaths) {
try {
typeContent = await fs.readFile(dtsPath, 'utf-8');
break;
}
catch {
continue;
}
}
}
if (typeContent) {
// Check if the type content already contains the correct module declaration
const moduleDeclaration = `ior:${isRemote ? '' : 'esm:'}${ior}`;
// Only process if the content doesn't already have multiple module declarations
// or if it doesn't contain the target module declaration
if (typeContent.includes(`declare module "${moduleDeclaration}"`) ||
typeContent.includes(`declare module '${moduleDeclaration}'`)) {
// Content already has the correct module declaration, use as-is
// This preserves any additional module declarations (like 'hypercore')
}
else if (!typeContent.includes('declare module')) {
// No module declaration at all, wrap the content
typeContent = `declare module "${moduleDeclaration}" {\n${typeContent}\n}\n`;
}
// If it has other module declarations but not our target, leave it as-is
// This preserves files with multiple module declarations
// Save to type cache
typeCache.set(cacheKey, {
content: typeContent,
timestamp: Date.now(),
});
// Write to global types file
await updateGlobalTypes();
}
processedTypes.add(cacheKey);
}
catch (err) {
DEBUG && logger.debug(`Failed to generate types for ${ior}:`, err);
}
}
/**
* Update global types file with all cached types
*/
async function updateGlobalTypes() {
const typesDir = path.join(projectRoot, 'src', 'types');
const globalTypesPath = path.join(typesDir, 'auto-generated.d.ts');
let content = '// Auto-generated type declarations by NodeImportLoader\n';
content += `// Generated at: ${new Date().toISOString()}\n`;
content += '// DO NOT EDIT - This file is automatically maintained\n\n';
// Add all cached type definitions
for (const [key, entry] of typeCache.entries()) {
content += `// ${key}\n`;
content += entry.content;
content += '\n\n';
}
// Ensure directory exists
await fs.mkdir(typesDir, { recursive: true });
// Write the file
await fs.writeFile(globalTypesPath, content, 'utf-8');
DEBUG && logger.debug(`Updated global types: ${globalTypesPath}`);
}
/**
* Cache remote component locally
*/
async function cacheRemoteComponent(ior, content, ttl = 3600000) {
const core = await loadIorCore();
const hash = core.generateCacheHash(ior);
const cacheDir = path.join(tmpdir(), 'metatrom-components', 'remote');
const cachePath = path.join(cacheDir, `${hash}.ts`);
// Create cache directory if it doesn't exist
await fs.mkdir(cacheDir, { recursive: true });
// Write content to cache
await fs.writeFile(cachePath, content, 'utf-8');
// Update cache map
remoteCache.set(ior, {
path: cachePath,
timestamp: Date.now(),
ttl,
});
DEBUG && logger.debug(`Cached remote component: ${ior} -> ${cachePath}`);
return cachePath;
}
/**
* Get cached remote component if valid
*/
async 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 file (non-blocking)
fs.unlink(entry.path).catch(() => {
logger.error(`Error deleting cached file ${entry.path}`);
});
return null;
}
// Verify file still exists
try {
await fs.access(entry.path);
DEBUG && logger.debug(`Using cached component: ${ior}`);
return entry.path;
}
catch {
remoteCache.delete(ior);
return null;
}
}
/**
* Fetch remote component based on IOR
*/
async function fetchRemoteComponent(ior) {
// Check cache first
const cached = await getCachedRemoteComponent(ior);
if (cached) {
return cached;
}
const core = await loadIorCore();
const config = core.parseRemoteIOR(ior);
if (!config) {
throw new Error(`Invalid remote IOR format: ${ior}`);
}
// Extract component name and version from IOR
const parts = ior.split(':');
const componentAndVersion = parts[parts.length - 1];
const [componentName, version] = componentAndVersion.split('@');
if (!componentName || !version) {
throw new Error(`Missing component name or version in IOR: ${ior}`);
}
let content;
switch (config.type) {
case 'github':
const githubResult = await core.fetchFromGitHub(config);
// For Node.js, we need to extract the content differently since we're dealing with archives
// For now, fetch just the main file directly (simplified approach)
content = await fetchComponentFile(config, componentName, version);
break;
case 'gitea':
const giteaResult = await core.fetchFromGitea(config);
// Similarly for Gitea
content = await fetchComponentFile(config, componentName, version);
break;
case 'ipfs':
const ipfsResult = await core.fetchFromIPFS(config);
content = ipfsResult.content;
break;
case 'p2p':
throw new Error(`P2P fetching not yet implemented. PeerID: ${config.peerId}`);
default:
throw new Error(`Unsupported remote type: ${config.type}`);
}
// Cache the fetched component
const cachedPath = await cacheRemoteComponent(ior, content);
return cachedPath;
}
/**
* Fetch component file directly (simplified approach for Node.js)
*/
async function fetchComponentFile(config, componentName, version) {
let baseUrl;
if (config.type === 'github') {
baseUrl = `https://raw.githubusercontent.com/${config.owner}/${config.repository}/main`;
}
else if (config.type === 'gitea') {
baseUrl = `https://${config.instance}/${config.owner}/${config.repository}/raw/branch/main`;
}
else {
throw new Error(`Unsupported type for direct fetch: ${config.type}`);
}
const componentPath = `src/components/${componentName}/${version}`;
// Try to fetch the main component file
const possibleFiles = [
`${componentPath}/index.ts`,
`${componentPath}/${componentName}.ts`,
`${componentPath}/index.tsx`,
`${componentPath}/${componentName}.tsx`,
];
for (const file of possibleFiles) {
try {
const response = await fetch(`${baseUrl}/${file}`);
if (response.ok) {
return await response.text();
}
}
catch {
// Try next file
}
}
throw new Error(`Could not fetch component file for ${componentName}@${version}`);
}
/**
* Get component path for local components using package.json
*/
async function getLocalComponentPath(ior) {
if (componentCache.has(ior))
return componentCache.get(ior);
// IOR format: com.metatrom.examples.componentname@version
const atIndex = ior.lastIndexOf('@');
if (atIndex === -1) {
throw new Error(`Invalid IOR format (missing @): ${ior}`);
}
const componentFullName = ior.substring(0, atIndex);
const version = ior.substring(atIndex + 1);
if (!componentFullName || !version) {
throw new Error(`Invalid IOR format: ${ior}`);
}
// Search for components with matching package.json
const componentsDir = path.join(projectRoot, 'src', 'components');
try {
const componentDirs = await fs.readdir(componentsDir, {
withFileTypes: true,
});
for (const dir of componentDirs) {
if (dir.isDirectory()) {
const versionDir = path.join(componentsDir, dir.name, version);
const packagePath = path.join(versionDir, 'package.json');
try {
const core = await loadIorCore();
const pkg = await core.parsePackageJsonAsync(packagePath);
if (pkg && pkg.metatrom?.ior === ior) {
// Found matching component - prefer index.ts if it exists
const indexPath = path.join(versionDir, 'index.ts');
try {
await fs.access(indexPath);
DEBUG && logger.debug(`Mapped IOR ${ior} to ${indexPath}`);
componentCache.set(ior, indexPath);
return indexPath;
}
catch {
// Fall back to main file specified in package.json
const mainFile = pkg.main || `${dir.name}.ts`;
const tsPath = path.join(versionDir, mainFile);
await fs.access(tsPath);
DEBUG && logger.debug(`Mapped IOR ${ior} to ${tsPath}`);
componentCache.set(ior, tsPath);
return tsPath;
}
}
}
catch (error) {
DEBUG &&
logger.debug(`Skipping ${packagePath}:`, error instanceof Error ? error.message : String(error));
}
}
}
}
catch (err) {
throw new Error(`Failed to read components directory: ${err}`);
}
throw new Error(`No component found for IOR ${ior}`);
}
/**
* Main resolve function for Node.js module resolution
*/
export async function resolve(specifier, context, defaultResolve) {
DEBUG &&
logger.debug(`Resolving specifier: ${specifier}, parentURL: ${context.parentURL}`);
if (specifier.startsWith('ior:')) {
// Check if it's a remote IOR
const core = await loadIorCore();
const remoteConfig = core.parseRemoteIOR(specifier);
if (remoteConfig) {
// Handle remote component
try {
const tsPath = await fetchRemoteComponent(specifier);
// Generate type definitions for remote component
const iorPart = specifier.substring(4); // Remove "ior:"
await generateTypeDeclaration(iorPart, true);
DEBUG && logger.debug(`Resolved remote IOR ${specifier} to ${tsPath}`);
return {
url: pathToFileURL(tsPath).href,
format: 'module',
shortCircuit: true,
type: 'typescript',
};
}
catch (error) {
logger.error(`Failed to fetch remote component ${specifier}:`, error);
throw error;
}
}
else {
// Handle local component with format: ior:esm:com.metatrom.examples.component@version
// Remove the "ior:" prefix and extract the IOR
const iorWithPrefix = specifier.substring(4); // Remove "ior:"
// Check if it has "esm:" prefix
let actualIor = iorWithPrefix;
if (iorWithPrefix.startsWith('esm:')) {
actualIor = iorWithPrefix.substring(4); // Remove "esm:"
}
const tsPath = await getLocalComponentPath(actualIor);
// Generate type definitions for local component
await generateTypeDeclaration(actualIor, false);
DEBUG && logger.debug(`Resolving local IOR ${actualIor} to ${tsPath}`);
return {
url: pathToFileURL(tsPath).href,
format: 'module',
shortCircuit: true,
type: 'typescript',
};
}
}
// Handle relative .ts imports
if (specifier.startsWith('./') || specifier.startsWith('../')) {
const parentURL = context.parentURL
? fileURLToPath(context.parentURL)
: __dirname;
const baseDir = path.dirname(parentURL);
// Try with .ts extension first
const tsPath = path.resolve(baseDir, `${specifier}.ts`);
try {
await fs.access(tsPath);
DEBUG && logger.debug(`Resolving relative .ts file: ${tsPath}`);
return {
url: pathToFileURL(tsPath).href,
format: 'module',
shortCircuit: true,
type: 'typescript',
};
}
catch {
// Try without adding extension (file might already have .ts)
const directPath = path.resolve(baseDir, specifier);
try {
await fs.access(directPath);
if (directPath.endsWith('.ts')) {
DEBUG && logger.debug(`Resolving relative .ts file: ${directPath}`);
return {
url: pathToFileURL(directPath).href,
format: 'module',
shortCircuit: true,
type: 'typescript',
};
}
}
catch {
DEBUG &&
logger.debug(`No .ts file found for ${specifier}, falling back to default resolver`);
}
}
}
return defaultResolve(specifier, context, defaultResolve);
}
/**
* Load and transpile TypeScript files
*/
export async function load(url, context, defaultLoad) {
if (url.endsWith('.ts')) {
const filePath = fileURLToPath(url);
DEBUG && logger.debug(`Loading TypeScript file: ${filePath}`);
try {
const code = await fs.readFile(filePath, 'utf-8');
const transpiled = transpileModule(code, {
compilerOptions: {
module: ModuleKind.ESNext,
target: ScriptTarget.ESNext,
noEmitHelpers: true,
importHelpers: false,
strict: true,
sourceMap: true,
},
});
return {
format: 'module',
source: transpiled.outputText,
shortCircuit: true,
};
}
catch (error) {
logger.error(`Failed to transpile ${filePath}:`, error);
throw error;
}
}
return defaultLoad(url, context, defaultLoad);
}
/**
* Clear cache for a specific IOR or all cached components
*/
export function clearCache(ior) {
if (ior) {
componentCache.delete(ior);
remoteCache.delete(ior);
}
else {
componentCache.clear();
remoteCache.clear();
}
}
/**
* Set cache TTL for remote components
*/
export function setCacheTTL(ttl) {
// This could be made more sophisticated with per-source TTL
for (const entry of remoteCache.values()) {
entry.ttl = ttl;
}
}
/**
* Preload remote components for faster access
*/
export async function preloadComponent(ior) {
try {
await fetchRemoteComponent(ior);
DEBUG && logger.debug(`Preloaded component: ${ior}`);
}
catch (error) {
logger.error(`Failed to preload component ${ior}:`, error);
}
}
/**
* Initialize type generation for all local components
*/
async function initializeLocalTypes() {
const componentsDir = path.join(projectRoot, 'src', 'components');
try {
const dirs = await fs.readdir(componentsDir, { withFileTypes: true });
for (const dir of dirs) {
if (dir.isDirectory()) {
const componentDir = path.join(componentsDir, dir.name);
const versions = await fs.readdir(componentDir, {
withFileTypes: true,
});
for (const version of versions) {
if (version.isDirectory()) {
const packagePath = path.join(componentDir, version.name, 'package.json');
try {
const packageContent = await fs.readFile(packagePath, 'utf-8');
const pkg = JSON.parse(packageContent);
if (pkg.metatrom?.ior) {
await generateTypeDeclaration(pkg.metatrom.ior, false);
}
}
catch {
// Skip if no package.json or invalid
}
}
}
}
}
DEBUG && logger.debug('Initialized type definitions for local components');
}
catch (error) {
DEBUG && logger.debug('Failed to initialize local types:', error);
}
}
// Auto-initialize types on first import
(async () => {
await initializeLocalTypes();
})();

0
bin/prebuild-ior-types.js Normal file → Executable file
View File

5648
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -18,7 +18,7 @@
"types": "./lib/metro-ior-resolver.d.ts" "types": "./lib/metro-ior-resolver.d.ts"
}, },
"./node": { "./node": {
"import": "./bin/NodeImportLoader.ts", "import": "./bin/NodeImportLoader.js",
"types": "./bin/NodeImportLoader.d.ts" "types": "./bin/NodeImportLoader.d.ts"
}, },
"./prebuild": { "./prebuild": {
@@ -27,7 +27,8 @@
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"prepublishOnly": "npm run build", "build:loader": "tsc src/loaders/NodeImportLoader.ts --outDir bin --module esnext --target esnext --moduleResolution node --esModuleInterop true --allowSyntheticDefaultImports true --skipLibCheck true && sed -i.bak \"s|import('../../lib/ior-core.js')|import('../lib/ior-core.js')|g\" bin/NodeImportLoader.js && rm -f bin/NodeImportLoader.js.bak",
"prepublishOnly": "npm run build && npm run build:loader",
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
}, },
"files": [ "files": [
@@ -35,6 +36,7 @@
"index.d.ts", "index.d.ts",
"lib/", "lib/",
"bin/", "bin/",
"src/",
"README.md", "README.md",
"LICENSE" "LICENSE"
], ],
@@ -74,5 +76,8 @@
"bugs": { "bugs": {
"url": "https://github.com/metatrom/ior-resolver/issues" "url": "https://github.com/metatrom/ior-resolver/issues"
}, },
"homepage": "https://github.com/metatrom/ior-resolver#readme" "homepage": "https://github.com/metatrom/ior-resolver#readme",
"devDependencies": {
"@types/node": "^24.3.0"
}
} }

151
plugins/babel-plugin-ior.js Normal file
View File

@@ -0,0 +1,151 @@
/**
* Babel plugin for IOR import transformation
* This plugin can transform IOR imports at build time for better optimization
*/
const fs = require('node:fs');
const path = require('node:path');
// Cache for IOR mappings
let iorMappings = null;
/**
* Build IOR mappings from component package.json files
*/
function buildMappings(projectRoot) {
if (iorMappings) return iorMappings;
iorMappings = new Map();
const componentsDir = path.join(projectRoot, 'src', 'components');
try {
const scanDirectory = (dir, depth = 0) => {
if (depth > 3) return; // Limit recursion depth
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
scanDirectory(fullPath, depth + 1);
} else if (entry.name === 'package.json') {
try {
const content = fs.readFileSync(fullPath, 'utf-8');
const pkg = JSON.parse(content);
if (pkg.metatrom?.ior) {
const componentDir = path.dirname(fullPath);
const mainFile = pkg.main || 'index.ts';
const componentPath = path.join(componentDir, mainFile);
// Create relative path from project root
const relativePath = path.relative(projectRoot, componentPath).replace(/\\/g, '/'); // Normalize path separators
// Store both with and without esm: prefix
iorMappings.set(`ior:esm:${pkg.metatrom.ior}`, `./${relativePath}`);
iorMappings.set(`ior:${pkg.metatrom.ior}`, `./${relativePath}`);
}
} catch (_err) {
// Ignore parse errors
}
}
}
};
scanDirectory(componentsDir);
} catch (_err) {
console.warn('[Babel IOR Plugin] Could not scan components:', _err);
}
return iorMappings;
}
/**
* Babel plugin factory
*/
module.exports = (babel) => {
const { types: t } = babel;
return {
name: 'babel-plugin-ior',
visitor: {
// Transform import declarations
ImportDeclaration(path, state) {
const source = path.node.source.value;
if (source.startsWith('ior:')) {
const projectRoot = state.opts.projectRoot || process.cwd();
const mappings = buildMappings(projectRoot);
const resolvedPath = mappings.get(source);
if (resolvedPath) {
// Replace the IOR import with the resolved path
path.node.source = t.stringLiteral(resolvedPath);
console.log(`[Babel IOR] Transformed: ${source} -> ${resolvedPath}`);
} else {
console.warn(`[Babel IOR] No mapping found for: ${source}`);
}
}
},
// Transform dynamic imports
CallExpression(path, state) {
if (
path.node.callee.type === 'Import' ||
(path.node.callee.type === 'Identifier' && path.node.callee.name === 'require')
) {
const arg = path.node.arguments[0];
if (t.isStringLiteral(arg) && arg.value.startsWith('ior:')) {
const projectRoot = state.opts.projectRoot || process.cwd();
const mappings = buildMappings(projectRoot);
const resolvedPath = mappings.get(arg.value);
if (resolvedPath) {
// Replace the IOR path with the resolved path
path.node.arguments[0] = t.stringLiteral(resolvedPath);
console.log(
`[Babel IOR] Transformed dynamic import: ${arg.value} -> ${resolvedPath}`,
);
}
}
}
},
// Transform require() calls for CommonJS compatibility
MemberExpression(path, state) {
if (
path.node.object.type === 'CallExpression' &&
path.node.object.callee.type === 'Identifier' &&
path.node.object.callee.name === 'require'
) {
const arg = path.node.object.arguments[0];
if (t.isStringLiteral(arg) && arg.value.startsWith('ior:')) {
const projectRoot = state.opts.projectRoot || process.cwd();
const mappings = buildMappings(projectRoot);
const resolvedPath = mappings.get(arg.value);
if (resolvedPath) {
path.node.object.arguments[0] = t.stringLiteral(resolvedPath);
console.log(`[Babel IOR] Transformed require: ${arg.value} -> ${resolvedPath}`);
}
}
}
},
},
};
};
// Export utility functions for testing
module.exports.buildMappings = buildMappings;
module.exports.clearCache = () => {
iorMappings = null;
};

View File

@@ -6,15 +6,16 @@ import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript'; import { ModuleKind, ScriptTarget, transpileModule } from 'typescript';
// Import shared core functions // Import shared core functions - will be loaded dynamically
const { let iorCore: any = null;
parseRemoteIOR,
parsePackageJsonAsync, // Dynamic import of core functions
fetchFromGitHub, const loadIorCore = async () => {
fetchFromGitea, if (!iorCore) {
fetchFromIPFS, iorCore = await import('../../lib/ior-core.js');
generateCacheHash, }
} = require('../lib/ior-core.js'); return iorCore;
};
register('./src/loaders/NodeImportLoader.ts', pathToFileURL('./')); register('./src/loaders/NodeImportLoader.ts', pathToFileURL('./'));
@@ -160,7 +161,8 @@ async function generateTypeDeclaration(
if (isRemote) { if (isRemote) {
// For remote components, fetch type definitions // For remote components, fetch type definitions
const config = parseRemoteIOR(`ior:${ior}`); const core = await loadIorCore();
const config = core.parseRemoteIOR(`ior:${ior}`);
if (config) { if (config) {
const parts = ior.split(':'); const parts = ior.split(':');
const componentAndVersion = parts[parts.length - 1]; const componentAndVersion = parts[parts.length - 1];
@@ -273,7 +275,8 @@ async function cacheRemoteComponent(
content: string, content: string,
ttl = 3600000, // 1 hour default TTL ttl = 3600000, // 1 hour default TTL
): Promise<string> { ): Promise<string> {
const hash = generateCacheHash(ior); const core = await loadIorCore();
const hash = core.generateCacheHash(ior);
const cacheDir = path.join(tmpdir(), 'metatrom-components', 'remote'); const cacheDir = path.join(tmpdir(), 'metatrom-components', 'remote');
const cachePath = path.join(cacheDir, `${hash}.ts`); const cachePath = path.join(cacheDir, `${hash}.ts`);
@@ -336,7 +339,8 @@ async function fetchRemoteComponent(ior: string): Promise<string> {
return cached; return cached;
} }
const config = parseRemoteIOR(ior); const core = await loadIorCore();
const config = core.parseRemoteIOR(ior);
if (!config) { if (!config) {
throw new Error(`Invalid remote IOR format: ${ior}`); throw new Error(`Invalid remote IOR format: ${ior}`);
} }
@@ -354,20 +358,20 @@ async function fetchRemoteComponent(ior: string): Promise<string> {
switch (config.type) { switch (config.type) {
case 'github': case 'github':
const githubResult = await fetchFromGitHub(config); const githubResult = await core.fetchFromGitHub(config);
// For Node.js, we need to extract the content differently since we're dealing with archives // For Node.js, we need to extract the content differently since we're dealing with archives
// For now, fetch just the main file directly (simplified approach) // For now, fetch just the main file directly (simplified approach)
content = await fetchComponentFile(config, componentName, version); content = await fetchComponentFile(config, componentName, version);
break; break;
case 'gitea': case 'gitea':
const giteaResult = await fetchFromGitea(config); const giteaResult = await core.fetchFromGitea(config);
// Similarly for Gitea // Similarly for Gitea
content = await fetchComponentFile(config, componentName, version); content = await fetchComponentFile(config, componentName, version);
break; break;
case 'ipfs': case 'ipfs':
const ipfsResult = await fetchFromIPFS(config); const ipfsResult = await core.fetchFromIPFS(config);
content = ipfsResult.content; content = ipfsResult.content;
break; break;
@@ -458,7 +462,8 @@ async function getLocalComponentPath(ior: string): Promise<string> {
const packagePath = path.join(versionDir, 'package.json'); const packagePath = path.join(versionDir, 'package.json');
try { try {
const pkg = await parsePackageJsonAsync(packagePath); const core = await loadIorCore();
const pkg = await core.parsePackageJsonAsync(packagePath);
if (pkg && pkg.metatrom?.ior === ior) { if (pkg && pkg.metatrom?.ior === ior) {
// Found matching component - prefer index.ts if it exists // Found matching component - prefer index.ts if it exists
@@ -511,7 +516,8 @@ export async function resolve(
if (specifier.startsWith('ior:')) { if (specifier.startsWith('ior:')) {
// Check if it's a remote IOR // Check if it's a remote IOR
const remoteConfig = parseRemoteIOR(specifier); const core = await loadIorCore();
const remoteConfig = core.parseRemoteIOR(specifier);
if (remoteConfig) { if (remoteConfig) {
// Handle remote component // Handle remote component