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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -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/
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -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
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"
|
"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
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 { 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
|
||||||
Reference in New Issue
Block a user