Fix package by testing real world examples
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -95,3 +95,6 @@ $RECYCLE.BIN/
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode
|
||||
|
||||
# NodeJs
|
||||
node_modules/
|
||||
|
||||
16
README.md
16
README.md
@@ -23,13 +23,16 @@ npm install @metatrom/ior-resolver
|
||||
|
||||
```
|
||||
@metatrom/ior-resolver/
|
||||
├── bin/ # Executable scripts
|
||||
├── bin/ # Compiled executable scripts
|
||||
│ ├── 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
|
||||
│ ├── ior-core.js # Shared core functions
|
||||
│ ├── metro-ior-resolver.js # Metro resolver implementation
|
||||
│ └── fetch-remote-component.js # Remote fetching utility
|
||||
├── src/ # Source files
|
||||
│ └── loaders/
|
||||
│ └── NodeImportLoader.ts # Node.js loader TypeScript source
|
||||
├── index.js # Main entry point
|
||||
├── index.d.ts # TypeScript definitions
|
||||
└── 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
|
||||
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
|
||||
|
||||
@@ -331,7 +334,10 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
1. Clone the repository
|
||||
2. Install dependencies: `npm install`
|
||||
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
|
||||
|
||||
|
||||
548
bin/NodeImportLoader.js
Normal file
548
bin/NodeImportLoader.js
Normal 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
0
bin/prebuild-ior-types.js
Normal file → Executable file
5648
package-lock.json
generated
Normal file
5648
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -18,7 +18,7 @@
|
||||
"types": "./lib/metro-ior-resolver.d.ts"
|
||||
},
|
||||
"./node": {
|
||||
"import": "./bin/NodeImportLoader.ts",
|
||||
"import": "./bin/NodeImportLoader.js",
|
||||
"types": "./bin/NodeImportLoader.d.ts"
|
||||
},
|
||||
"./prebuild": {
|
||||
@@ -27,7 +27,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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"
|
||||
},
|
||||
"files": [
|
||||
@@ -35,6 +36,7 @@
|
||||
"index.d.ts",
|
||||
"lib/",
|
||||
"bin/",
|
||||
"src/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
@@ -74,5 +76,8 @@
|
||||
"bugs": {
|
||||
"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
151
plugins/babel-plugin-ior.js
Normal 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;
|
||||
};
|
||||
@@ -6,15 +6,16 @@ import path from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { ModuleKind, ScriptTarget, transpileModule } from 'typescript';
|
||||
|
||||
// Import shared core functions
|
||||
const {
|
||||
parseRemoteIOR,
|
||||
parsePackageJsonAsync,
|
||||
fetchFromGitHub,
|
||||
fetchFromGitea,
|
||||
fetchFromIPFS,
|
||||
generateCacheHash,
|
||||
} = require('../lib/ior-core.js');
|
||||
// Import shared core functions - will be loaded dynamically
|
||||
let iorCore: any = 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('./'));
|
||||
|
||||
@@ -160,7 +161,8 @@ async function generateTypeDeclaration(
|
||||
|
||||
if (isRemote) {
|
||||
// For remote components, fetch type definitions
|
||||
const config = parseRemoteIOR(`ior:${ior}`);
|
||||
const core = await loadIorCore();
|
||||
const config = core.parseRemoteIOR(`ior:${ior}`);
|
||||
if (config) {
|
||||
const parts = ior.split(':');
|
||||
const componentAndVersion = parts[parts.length - 1];
|
||||
@@ -273,7 +275,8 @@ async function cacheRemoteComponent(
|
||||
content: string,
|
||||
ttl = 3600000, // 1 hour default TTL
|
||||
): Promise<string> {
|
||||
const hash = generateCacheHash(ior);
|
||||
const core = await loadIorCore();
|
||||
const hash = core.generateCacheHash(ior);
|
||||
const cacheDir = path.join(tmpdir(), 'metatrom-components', 'remote');
|
||||
const cachePath = path.join(cacheDir, `${hash}.ts`);
|
||||
|
||||
@@ -336,7 +339,8 @@ async function fetchRemoteComponent(ior: string): Promise<string> {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const config = parseRemoteIOR(ior);
|
||||
const core = await loadIorCore();
|
||||
const config = core.parseRemoteIOR(ior);
|
||||
if (!config) {
|
||||
throw new Error(`Invalid remote IOR format: ${ior}`);
|
||||
}
|
||||
@@ -354,20 +358,20 @@ async function fetchRemoteComponent(ior: string): Promise<string> {
|
||||
|
||||
switch (config.type) {
|
||||
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 now, fetch just the main file directly (simplified approach)
|
||||
content = await fetchComponentFile(config, componentName, version);
|
||||
break;
|
||||
|
||||
case 'gitea':
|
||||
const giteaResult = await fetchFromGitea(config);
|
||||
const giteaResult = await core.fetchFromGitea(config);
|
||||
// Similarly for Gitea
|
||||
content = await fetchComponentFile(config, componentName, version);
|
||||
break;
|
||||
|
||||
case 'ipfs':
|
||||
const ipfsResult = await fetchFromIPFS(config);
|
||||
const ipfsResult = await core.fetchFromIPFS(config);
|
||||
content = ipfsResult.content;
|
||||
break;
|
||||
|
||||
@@ -458,7 +462,8 @@ async function getLocalComponentPath(ior: string): Promise<string> {
|
||||
const packagePath = path.join(versionDir, 'package.json');
|
||||
|
||||
try {
|
||||
const pkg = await parsePackageJsonAsync(packagePath);
|
||||
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
|
||||
@@ -511,7 +516,8 @@ export async function resolve(
|
||||
|
||||
if (specifier.startsWith('ior:')) {
|
||||
// Check if it's a remote IOR
|
||||
const remoteConfig = parseRemoteIOR(specifier);
|
||||
const core = await loadIorCore();
|
||||
const remoteConfig = core.parseRemoteIOR(specifier);
|
||||
|
||||
if (remoteConfig) {
|
||||
// Handle remote component
|
||||
Reference in New Issue
Block a user