Initial commit
This commit is contained in:
13
.claude/settings.local.json
Normal file
13
.claude/settings.local.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Read(/Users/chris/Workspace/dev/Metatrom/examples/Libp2pMDNSApp/**)",
|
||||
"Read(/Users/chris/Workspace/dev/Metatrom/examples/Libp2pMDNSApp/**)",
|
||||
"Read(/Users/chris/Workspace/dev/Metatrom/examples/Libp2pMDNSApp/**)",
|
||||
"Read(/Users/chris/Workspace/dev/Metatrom/examples/Libp2pMDNSApp/scripts/**)",
|
||||
"Read(/Users/chris/Workspace/dev/Metatrom/templates/components/**)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": []
|
||||
}
|
||||
}
|
||||
97
.gitignore
vendored
Normal file
97
.gitignore
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,linux,visualstudiocode
|
||||
|
||||
### Linux ###
|
||||
*~
|
||||
|
||||
# temporary files which can be created if a process still has a handle open of a deleted file
|
||||
.fuse_hidden*
|
||||
|
||||
# KDE directory preferences
|
||||
.directory
|
||||
|
||||
# Linux trash folder which might appear on any partition or disk
|
||||
.Trash-*
|
||||
|
||||
# .nfs files are created when an open file is removed but is still being accessed
|
||||
.nfs*
|
||||
|
||||
### macOS ###
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
### macOS Patch ###
|
||||
# iCloud generated files
|
||||
*.icloud
|
||||
|
||||
### VisualStudioCode ###
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
!.vscode/*.code-snippets
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
### VisualStudioCode Patch ###
|
||||
# Ignore all local history of files
|
||||
.history
|
||||
.ionide
|
||||
|
||||
### Windows ###
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,linux,visualstudiocode
|
||||
349
README.md
Normal file
349
README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# @metatrom/ior-resolver
|
||||
|
||||
IOR (Interoperable Object Reference) resolver for Metro bundler and Node.js with support for local and remote component fetching.
|
||||
|
||||
## Features
|
||||
|
||||
- 🚀 **Metro Bundler Support** - Seamlessly integrate IOR resolution with React Native projects
|
||||
- 🌐 **Remote Component Fetching** - Load components from GitHub, Gitea, IPFS, and P2P networks
|
||||
- 📦 **Local Component Resolution** - Resolve local components using package.json metadata
|
||||
- 🔄 **Hot Reload Support** - Automatic mapping updates during development
|
||||
- 📝 **TypeScript Support** - Auto-generate type declarations for IOR components
|
||||
- ⚡ **Caching** - Intelligent caching of remote components for better performance
|
||||
- 🛠️ **Pre-build Script** - Generate types before development starts
|
||||
- 🎯 **Shared Core** - Consolidated functions to avoid duplication
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @metatrom/ior-resolver
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
@metatrom/ior-resolver/
|
||||
├── bin/ # Executable scripts
|
||||
│ ├── prebuild-ior-types.js # Pre-build script for type generation
|
||||
│ └── NodeImportLoader.ts # Node.js loader implementation
|
||||
├── lib/ # Core library files
|
||||
│ ├── ior-core.js # Shared core functions
|
||||
│ ├── metro-ior-resolver.js # Metro resolver implementation
|
||||
│ └── fetch-remote-component.js # Remote fetching utility
|
||||
├── index.js # Main entry point
|
||||
├── index.d.ts # TypeScript definitions
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Metro Configuration (React Native)
|
||||
|
||||
In your `metro.config.js`:
|
||||
|
||||
```javascript
|
||||
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
|
||||
const { createHotReloadableResolver } = require('@metatrom/ior-resolver');
|
||||
|
||||
const defaultConfig = getDefaultConfig(__dirname);
|
||||
|
||||
const config = {
|
||||
resolver: {
|
||||
resolveRequest: createHotReloadableResolver(__dirname),
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = mergeConfig(defaultConfig, config);
|
||||
```
|
||||
|
||||
### Node.js Loader
|
||||
|
||||
For Node.js projects using TypeScript and IOR imports:
|
||||
|
||||
```javascript
|
||||
// In your loader file
|
||||
import { resolve, load } from '@metatrom/ior-resolver/node';
|
||||
|
||||
// Or register the loader
|
||||
import { register } from 'node:module';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
|
||||
register('@metatrom/ior-resolver/node', pathToFileURL('./'));
|
||||
```
|
||||
|
||||
### Pre-build Script
|
||||
|
||||
Add to your `package.json` scripts to generate TypeScript declarations before development:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"prebuild": "prebuild-ior-types",
|
||||
"dev": "npm run prebuild && npm start"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or run directly:
|
||||
|
||||
```bash
|
||||
npx prebuild-ior-types
|
||||
```
|
||||
|
||||
## IOR Format
|
||||
|
||||
IOR (Interoperable Object Reference) provides a unified way to reference components across different sources:
|
||||
|
||||
### Local Components
|
||||
```javascript
|
||||
import Logger from 'ior:esm:com.metatrom.examples.logger@1.0.0';
|
||||
```
|
||||
|
||||
### Remote Components
|
||||
|
||||
#### GitHub
|
||||
```javascript
|
||||
import Component from 'ior:github:owner/repo:componentname@version';
|
||||
```
|
||||
|
||||
#### Gitea
|
||||
```javascript
|
||||
import Component from 'ior:gitea:instance.com:owner/repo:componentname@version';
|
||||
// Alternative format for repos with slashes in name:
|
||||
import Component from 'ior:gitea:instance.com:owner/repo/componentname@version';
|
||||
```
|
||||
|
||||
#### IPFS
|
||||
```javascript
|
||||
import Component from 'ior:ipfs:QmHash:componentname@version';
|
||||
```
|
||||
|
||||
#### P2P (Coming Soon)
|
||||
```javascript
|
||||
import Component from 'ior:p2p:peer-id:componentname@version';
|
||||
```
|
||||
|
||||
## Component Structure
|
||||
|
||||
Local components should follow this structure:
|
||||
|
||||
```
|
||||
src/components/
|
||||
└── componentname/
|
||||
└── version/
|
||||
├── package.json
|
||||
├── index.ts (or componentname.ts)
|
||||
└── componentname.d.ts (optional)
|
||||
```
|
||||
|
||||
### package.json Example
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "logger",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"metatrom": {
|
||||
"ior": "com.metatrom.examples.logger@1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Core Functions (ior-core.js)
|
||||
|
||||
The shared core module provides common functionality used by both Metro and Node.js implementations:
|
||||
|
||||
- `parseRemoteIOR(ior)` - Parse remote IOR strings
|
||||
- `parsePackageJson(path)` - Synchronously parse package.json
|
||||
- `parsePackageJsonAsync(path)` - Asynchronously parse package.json
|
||||
- `fetchUrl(url)` - Fetch content from URL
|
||||
- `fetchUrlBuffer(url)` - Fetch URL as Buffer
|
||||
- `fetchFromGitHub(config)` - Fetch from GitHub
|
||||
- `fetchFromGitea(config)` - Fetch from Gitea
|
||||
- `fetchFromIPFS(config)` - Fetch from IPFS
|
||||
- `extractArchive(buffer, dir)` - Extract tar.gz archives
|
||||
- `findEntryPoint(dir, name)` - Find component entry point
|
||||
- `generateCacheHash(ior)` - Generate SHA256 hash for caching
|
||||
- `buildLocalIORMappings(root)` - Build local component mappings
|
||||
|
||||
### Metro Resolver Functions
|
||||
|
||||
#### `createIORResolver(projectRoot: string)`
|
||||
Creates a basic IOR resolver for Metro.
|
||||
|
||||
#### `createHotReloadableResolver(projectRoot: string)`
|
||||
Creates an IOR resolver with hot reload support that watches for package.json changes.
|
||||
|
||||
#### `fetchRemoteComponent(ior: string)`
|
||||
Fetches and caches a remote component.
|
||||
|
||||
#### `clearRemoteCache()`
|
||||
Clears all cached remote components.
|
||||
|
||||
### Node.js Loader Functions
|
||||
|
||||
#### `resolve(specifier, context, defaultResolve)`
|
||||
Node.js loader resolve hook for IOR imports.
|
||||
|
||||
#### `load(url, context, defaultLoad)`
|
||||
Node.js loader load hook for TypeScript transpilation.
|
||||
|
||||
#### `clearCache(ior?: string)`
|
||||
Clears cache for a specific IOR or all cached components.
|
||||
|
||||
#### `setCacheTTL(ttl: number)`
|
||||
Sets the cache time-to-live for remote components.
|
||||
|
||||
#### `preloadComponent(ior: string)`
|
||||
Pre-fetches a remote component for faster access.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `DEBUG=true` - Enable debug logging for the Node.js loader
|
||||
- `NODE_ENV=development` - Enable hot reload for Metro resolver
|
||||
|
||||
### Cache Configuration
|
||||
|
||||
The resolver uses different cache locations for different environments:
|
||||
|
||||
- **Metro**: `.ior-cache/remote/` in project root
|
||||
- **Node.js**: OS temp directory under `metatrom-components/remote/`
|
||||
- **Default TTL**: 1 hour (3600000ms)
|
||||
|
||||
### Cache Persistence
|
||||
|
||||
Metro resolver maintains a persistent cache mapping in `.ior-cache/mappings.json` for faster startup times.
|
||||
|
||||
## Architecture
|
||||
|
||||
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
|
||||
|
||||
This design eliminates code duplication and ensures consistency across different environments.
|
||||
|
||||
## Migration from Direct Implementation
|
||||
|
||||
If you're migrating from having these files directly in your project:
|
||||
|
||||
1. Install the package:
|
||||
```bash
|
||||
npm install @metatrom/ior-resolver
|
||||
```
|
||||
|
||||
2. Update `metro.config.js` to use the package import:
|
||||
```javascript
|
||||
const { createHotReloadableResolver } = require('@metatrom/ior-resolver');
|
||||
```
|
||||
|
||||
3. Update `package.json` scripts:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"prebuild": "prebuild-ior-types"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. Remove the old files from your project:
|
||||
- `metro-ior-resolver.js`
|
||||
- `scripts/prebuild-ior-types.js`
|
||||
- `fetch-remote-component.js`
|
||||
- `NodeImportLoader.ts`
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
The package automatically generates TypeScript declarations for IOR components:
|
||||
|
||||
- Local components: Types are read from `.d.ts` files in the component directory
|
||||
- Remote components: Types are fetched from the remote repository
|
||||
- Auto-generated types are saved to `src/types/auto-generated.d.ts`
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Remote components are fetched over HTTPS
|
||||
- Components are cached locally with SHA256 hash verification
|
||||
- Cache TTL ensures components are refreshed periodically
|
||||
- No automatic code execution - components must be explicitly imported
|
||||
|
||||
## Performance
|
||||
|
||||
- **Caching**: Components are cached locally to avoid repeated network requests
|
||||
- **Parallel fetching**: Multiple components can be fetched in parallel
|
||||
- **Hot reload**: Development mode watches for changes without restart
|
||||
- **Type generation**: Types are generated asynchronously without blocking
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"Cannot resolve IOR module"**
|
||||
- Ensure the component exists in the expected location
|
||||
- Check the IOR format is correct
|
||||
- Verify network connectivity for remote components
|
||||
|
||||
2. **"Failed to fetch remote component"**
|
||||
- Check network connectivity
|
||||
- Verify the repository exists and is accessible
|
||||
- Ensure the version tag exists
|
||||
|
||||
3. **Types not generating**
|
||||
- Run `npx prebuild-ior-types` manually
|
||||
- Check for `.d.ts` files in component directories
|
||||
- Verify `src/types/` directory exists
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging:
|
||||
|
||||
```bash
|
||||
DEBUG=true npm start
|
||||
```
|
||||
|
||||
### Clear Cache
|
||||
|
||||
If you encounter stale cache issues:
|
||||
|
||||
```javascript
|
||||
// In your code
|
||||
const { clearRemoteCache } = require('@metatrom/ior-resolver');
|
||||
clearRemoteCache();
|
||||
```
|
||||
|
||||
Or manually delete:
|
||||
```bash
|
||||
rm -rf .ior-cache
|
||||
rm -rf /tmp/metatrom-components # Node.js cache
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
### Development Setup
|
||||
|
||||
1. Clone the repository
|
||||
2. Install dependencies: `npm install`
|
||||
3. Build TypeScript: `npm run build`
|
||||
4. Run tests: `npm test`
|
||||
|
||||
### Code Structure
|
||||
|
||||
- Keep shared functionality in `lib/ior-core.js`
|
||||
- Environment-specific code in respective files
|
||||
- Maintain TypeScript types for all exports
|
||||
- Follow existing code style
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please use the [GitHub issue tracker](https://github.com/metatrom/ior-resolver/issues).
|
||||
725
bin/NodeImportLoader.ts
Normal file
725
bin/NodeImportLoader.ts
Normal file
@@ -0,0 +1,725 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
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
|
||||
const {
|
||||
parseRemoteIOR,
|
||||
parsePackageJsonAsync,
|
||||
fetchFromGitHub,
|
||||
fetchFromGitea,
|
||||
fetchFromIPFS,
|
||||
generateCacheHash,
|
||||
} = require('../lib/ior-core.js');
|
||||
|
||||
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: unknown[]) =>
|
||||
DEBUG && console.log('[NodeImportLoader:DEBUG]', ...args),
|
||||
info: (...args: unknown[]) => console.log('[NodeImportLoader:INFO]', ...args),
|
||||
warn: (...args: unknown[]) =>
|
||||
console.warn('[NodeImportLoader:WARN]', ...args),
|
||||
error: (...args: unknown[]) =>
|
||||
console.error('[NodeImportLoader:ERROR]', ...args),
|
||||
};
|
||||
|
||||
// Type definitions for Node.js loader API
|
||||
interface ResolveContext {
|
||||
parentURL?: string;
|
||||
conditions?: string[];
|
||||
importAttributes?: Record<string, string>;
|
||||
}
|
||||
|
||||
interface LoadContext {
|
||||
format?: string;
|
||||
importAttributes?: Record<string, string>;
|
||||
}
|
||||
|
||||
type ResolveResult = {
|
||||
url: string;
|
||||
format?: string;
|
||||
shortCircuit?: boolean;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
type LoadResult = {
|
||||
format: string;
|
||||
source: string | ArrayBuffer;
|
||||
shortCircuit?: boolean;
|
||||
};
|
||||
|
||||
type DefaultResolve = (
|
||||
specifier: string,
|
||||
context: ResolveContext,
|
||||
defaultResolve: DefaultResolve,
|
||||
) => Promise<ResolveResult>;
|
||||
|
||||
type DefaultLoad = (
|
||||
url: string,
|
||||
context: LoadContext,
|
||||
defaultLoad: DefaultLoad,
|
||||
) => Promise<LoadResult>;
|
||||
|
||||
const componentCache = new Map<string, string>();
|
||||
const remoteCache = new Map<string, RemoteCacheEntry>();
|
||||
const typeCache = new Map<string, TypeCacheEntry>();
|
||||
const processedTypes = new Set<string>();
|
||||
|
||||
interface RemoteCacheEntry {
|
||||
path: string;
|
||||
timestamp: number;
|
||||
ttl: number; // Time to live in milliseconds
|
||||
}
|
||||
|
||||
interface TypeCacheEntry {
|
||||
content: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
interface PackageMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
description?: string;
|
||||
main?: string;
|
||||
metatrom?: {
|
||||
ior: string;
|
||||
capabilities?: {
|
||||
p2p?: boolean;
|
||||
contracts?: boolean;
|
||||
viewer?: boolean;
|
||||
sync?: boolean;
|
||||
};
|
||||
};
|
||||
dependencies?: Record<string, string>;
|
||||
optionalDependencies?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch type definitions for a component
|
||||
*/
|
||||
async function fetchTypeDefinitions(
|
||||
config: any,
|
||||
componentName: string,
|
||||
version: string,
|
||||
): Promise<string | null> {
|
||||
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: string,
|
||||
isRemote: boolean = false,
|
||||
): Promise<void> {
|
||||
// Avoid duplicate processing
|
||||
const cacheKey = `${isRemote ? 'remote:' : 'local:'}${ior}`;
|
||||
if (processedTypes.has(cacheKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let typeContent: string | null = null;
|
||||
|
||||
if (isRemote) {
|
||||
// For remote components, fetch type definitions
|
||||
const config = 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(): Promise<void> {
|
||||
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: string,
|
||||
content: string,
|
||||
ttl = 3600000, // 1 hour default TTL
|
||||
): Promise<string> {
|
||||
const hash = 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: string): Promise<string | null> {
|
||||
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: string): Promise<string> {
|
||||
// Check cache first
|
||||
const cached = await getCachedRemoteComponent(ior);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const config = 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: string;
|
||||
|
||||
switch (config.type) {
|
||||
case 'github':
|
||||
const githubResult = await 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);
|
||||
// Similarly for Gitea
|
||||
content = await fetchComponentFile(config, componentName, version);
|
||||
break;
|
||||
|
||||
case 'ipfs':
|
||||
const ipfsResult = await 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: any,
|
||||
componentName: string,
|
||||
version: string,
|
||||
): Promise<string> {
|
||||
let baseUrl: string;
|
||||
|
||||
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: string): Promise<string> {
|
||||
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 pkg = await 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: unknown) {
|
||||
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: string,
|
||||
context: ResolveContext,
|
||||
defaultResolve: DefaultResolve,
|
||||
): Promise<ResolveResult> {
|
||||
DEBUG &&
|
||||
logger.debug(
|
||||
`Resolving specifier: ${specifier}, parentURL: ${context.parentURL}`,
|
||||
);
|
||||
|
||||
if (specifier.startsWith('ior:')) {
|
||||
// Check if it's a remote IOR
|
||||
const remoteConfig = 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: string,
|
||||
context: LoadContext,
|
||||
defaultLoad: DefaultLoad,
|
||||
): Promise<LoadResult> {
|
||||
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?: string) {
|
||||
if (ior) {
|
||||
componentCache.delete(ior);
|
||||
remoteCache.delete(ior);
|
||||
} else {
|
||||
componentCache.clear();
|
||||
remoteCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cache TTL for remote components
|
||||
*/
|
||||
export function setCacheTTL(ttl: number) {
|
||||
// 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: string): Promise<void> {
|
||||
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(): Promise<void> {
|
||||
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) as PackageMetadata;
|
||||
|
||||
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();
|
||||
})();
|
||||
245
bin/prebuild-ior-types.js
Normal file
245
bin/prebuild-ior-types.js
Normal file
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Pre-build script to generate TypeScript declarations for IOR components
|
||||
* This script:
|
||||
* 1. Scans the codebase for IOR imports
|
||||
* 2. Fetches remote components if not cached
|
||||
* 3. Generates auto-generated.d.ts before development starts
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
// Import the resolver functions
|
||||
const {
|
||||
fetchRemoteComponent,
|
||||
parseRemoteIOR,
|
||||
buildLocalIORMappings
|
||||
} = require('../lib/metro-ior-resolver.js');
|
||||
|
||||
const projectRoot = process.cwd();
|
||||
const srcDir = path.join(projectRoot, 'src');
|
||||
|
||||
// Regex patterns to find IOR imports
|
||||
const IOR_IMPORT_PATTERNS = [
|
||||
// ES6 imports: import { X } from 'ior:...'
|
||||
/import\s+(?:{[^}]+}|\*\s+as\s+\w+|\w+)\s+from\s+['"]ior:([^'"]+)['"]/gm,
|
||||
// ES6 dynamic imports: import('ior:...')
|
||||
/import\s*\(\s*['"]ior:([^'"]+)['"]\s*\)/gm,
|
||||
// CommonJS: require('ior:...')
|
||||
/require\s*\(\s*['"]ior:([^'"]+)['"]\s*\)/gm,
|
||||
];
|
||||
|
||||
/**
|
||||
* Recursively scan directory for JavaScript/TypeScript files
|
||||
*/
|
||||
function* walkFiles(dir) {
|
||||
const files = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file.name);
|
||||
|
||||
// Skip node_modules, build directories, and cache
|
||||
if (file.isDirectory()) {
|
||||
if (!['node_modules', 'build', 'dist', '.ior-cache', '.git'].includes(file.name)) {
|
||||
yield* walkFiles(filePath);
|
||||
}
|
||||
} else if (file.isFile()) {
|
||||
// Only process JS/TS files
|
||||
if (/\.(js|jsx|ts|tsx)$/.test(file.name)) {
|
||||
yield filePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract IOR imports from a file
|
||||
*/
|
||||
function extractIORImports(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const imports = new Set();
|
||||
|
||||
for (const pattern of IOR_IMPORT_PATTERNS) {
|
||||
// Reset regex lastIndex for global patterns
|
||||
pattern.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
// Add the full IOR string (including 'ior:' prefix)
|
||||
imports.add(`ior:${match[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(imports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan entire codebase for IOR imports
|
||||
*/
|
||||
function scanForIORImports() {
|
||||
console.log('[Pre-build] Scanning codebase for IOR imports...');
|
||||
const allImports = new Set();
|
||||
|
||||
// Scan src directory
|
||||
if (fs.existsSync(srcDir)) {
|
||||
for (const filePath of walkFiles(srcDir)) {
|
||||
const imports = extractIORImports(filePath);
|
||||
if (imports.length > 0) {
|
||||
console.log(`[Pre-build] Found ${imports.length} IOR imports in ${path.relative(projectRoot, filePath)}`);
|
||||
imports.forEach(imp => allImports.add(imp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also scan App.tsx if it exists at root
|
||||
const appPath = path.join(projectRoot, 'App.tsx');
|
||||
if (fs.existsSync(appPath)) {
|
||||
const imports = extractIORImports(appPath);
|
||||
if (imports.length > 0) {
|
||||
console.log(`[Pre-build] Found ${imports.length} IOR imports in App.tsx`);
|
||||
imports.forEach(imp => allImports.add(imp));
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(allImports);
|
||||
}
|
||||
|
||||
/**
|
||||
* Categorize imports into local and remote
|
||||
*/
|
||||
function categorizeImports(imports) {
|
||||
const local = [];
|
||||
const remote = [];
|
||||
|
||||
for (const ior of imports) {
|
||||
const remoteConfig = parseRemoteIOR(ior);
|
||||
if (remoteConfig) {
|
||||
remote.push(ior);
|
||||
} else {
|
||||
local.push(ior);
|
||||
}
|
||||
}
|
||||
|
||||
return { local, remote };
|
||||
}
|
||||
|
||||
/**
|
||||
* Save IOR mappings to persistent file
|
||||
*/
|
||||
function saveIORMappings(mappings) {
|
||||
const mappingFile = path.join(projectRoot, '.ior-cache', 'mappings.json');
|
||||
const cacheDir = path.dirname(mappingFile);
|
||||
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
const data = {
|
||||
version: '1.0',
|
||||
generated: new Date().toISOString(),
|
||||
mappings: Object.fromEntries(mappings),
|
||||
};
|
||||
|
||||
fs.writeFileSync(mappingFile, JSON.stringify(data, null, 2), 'utf-8');
|
||||
console.log(`[Pre-build] Saved IOR mappings to ${mappingFile}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-fetch all remote components
|
||||
*/
|
||||
async function prefetchRemoteComponents(remoteImports) {
|
||||
const mappings = new Map();
|
||||
|
||||
console.log(`[Pre-build] Pre-fetching ${remoteImports.length} remote components...`);
|
||||
|
||||
for (const ior of remoteImports) {
|
||||
try {
|
||||
console.log(`[Pre-build] Fetching: ${ior}`);
|
||||
const cachedPath = await fetchRemoteComponent(ior);
|
||||
|
||||
// Extract the cache directory from the entry file path
|
||||
const cacheDir = path.dirname(cachedPath);
|
||||
const hash = path.basename(cacheDir);
|
||||
|
||||
mappings.set(ior, {
|
||||
path: cachedPath,
|
||||
directory: cacheDir,
|
||||
hash: hash,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
|
||||
console.log(`[Pre-build] ✓ Cached at: ${hash}`);
|
||||
} catch (err) {
|
||||
console.error(`[Pre-build] ✗ Failed to fetch ${ior}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execution
|
||||
*/
|
||||
async function main() {
|
||||
console.log('[Pre-build] Starting IOR type pre-generation...');
|
||||
|
||||
try {
|
||||
// Step 1: Scan for IOR imports
|
||||
const imports = scanForIORImports();
|
||||
console.log(`[Pre-build] Found ${imports.length} unique IOR imports`);
|
||||
|
||||
if (imports.length === 0) {
|
||||
console.log('[Pre-build] No IOR imports found, skipping pre-build');
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Categorize imports
|
||||
const { local, remote } = categorizeImports(imports);
|
||||
console.log(`[Pre-build] Local: ${local.length}`);
|
||||
console.log(`[Pre-build] Remote: ${remote.length}`);
|
||||
|
||||
// Step 3: Build local mappings
|
||||
if (local.length > 0) {
|
||||
console.log('[Pre-build] Building local component mappings...');
|
||||
const localMappings = buildLocalIORMappings(projectRoot);
|
||||
console.log(`[Pre-build] Found ${localMappings.size} local components`);
|
||||
}
|
||||
|
||||
// Step 4: Pre-fetch remote components
|
||||
if (remote.length > 0) {
|
||||
const remoteMappings = await prefetchRemoteComponents(remote);
|
||||
|
||||
// Save mappings for cache reconstruction
|
||||
if (remoteMappings.size > 0) {
|
||||
saveIORMappings(remoteMappings);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: The type generation happens automatically during fetchRemoteComponent
|
||||
// Check if auto-generated.d.ts was created
|
||||
const typesFile = path.join(projectRoot, 'src', 'types', 'auto-generated.d.ts');
|
||||
if (fs.existsSync(typesFile)) {
|
||||
const stats = fs.statSync(typesFile);
|
||||
console.log(`[Pre-build] ✓ Generated types file: ${typesFile}`);
|
||||
console.log(`[Pre-build] Size: ${stats.size} bytes`);
|
||||
console.log(`[Pre-build] Modified: ${stats.mtime.toISOString()}`);
|
||||
}
|
||||
|
||||
console.log('[Pre-build] Pre-build completed successfully');
|
||||
} catch (err) {
|
||||
console.error('[Pre-build] Pre-build failed:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run if executed directly
|
||||
if (require.main === module) {
|
||||
main().catch(err => {
|
||||
console.error('[Pre-build] Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { scanForIORImports, prefetchRemoteComponents };
|
||||
91
index.d.ts
vendored
Normal file
91
index.d.ts
vendored
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Type definitions for @metatrom/ior-resolver
|
||||
*/
|
||||
|
||||
export interface IORResolverContext {
|
||||
originModulePath: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface IORResolverResult {
|
||||
type: 'sourceFile';
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
export type IORResolver = (
|
||||
context: IORResolverContext,
|
||||
moduleName: string,
|
||||
platform?: string
|
||||
) => IORResolverResult | undefined;
|
||||
|
||||
export interface RemoteComponentConfig {
|
||||
type: 'github' | 'gitea' | 'ipfs' | 'p2p';
|
||||
owner?: string;
|
||||
repository?: string;
|
||||
componentName?: string;
|
||||
version?: string;
|
||||
branch?: string;
|
||||
instance?: string;
|
||||
hash?: string;
|
||||
peerId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an IOR resolver for Metro bundler
|
||||
* @param projectRoot - The root directory of the project
|
||||
* @returns A resolver function for Metro
|
||||
*/
|
||||
export function createIORResolver(projectRoot: string): IORResolver;
|
||||
|
||||
/**
|
||||
* Create a hot-reloadable IOR resolver for Metro bundler
|
||||
* @param projectRoot - The root directory of the project
|
||||
* @returns A resolver function for Metro with hot reload support
|
||||
*/
|
||||
export function createHotReloadableResolver(projectRoot: string): IORResolver;
|
||||
|
||||
/**
|
||||
* Build mappings for local IOR components
|
||||
* @param projectRoot - The root directory of the project
|
||||
* @returns A Map of IOR to file path mappings
|
||||
*/
|
||||
export function buildIORMappings(projectRoot: string): Map<string, string>;
|
||||
|
||||
/**
|
||||
* Build mappings for local IOR components
|
||||
* @param projectRoot - The root directory of the project
|
||||
* @returns A Map of IOR to file path mappings
|
||||
*/
|
||||
export function buildLocalIORMappings(projectRoot: string): Map<string, string>;
|
||||
|
||||
/**
|
||||
* Fetch a remote component based on IOR
|
||||
* @param ior - The IOR string to resolve
|
||||
* @returns Promise resolving to the cached component path
|
||||
*/
|
||||
export function fetchRemoteComponent(ior: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Parse a remote IOR string
|
||||
* @param ior - The IOR string to parse
|
||||
* @returns The parsed configuration or null if invalid
|
||||
*/
|
||||
export function parseRemoteIOR(ior: string): RemoteComponentConfig | null;
|
||||
|
||||
/**
|
||||
* Clear the remote component cache
|
||||
*/
|
||||
export function clearRemoteCache(): void;
|
||||
|
||||
/**
|
||||
* Metro resolver module
|
||||
*/
|
||||
export const metro: {
|
||||
createIORResolver: typeof createIORResolver;
|
||||
createHotReloadableResolver: typeof createHotReloadableResolver;
|
||||
buildIORMappings: typeof buildIORMappings;
|
||||
buildLocalIORMappings: typeof buildLocalIORMappings;
|
||||
fetchRemoteComponent: typeof fetchRemoteComponent;
|
||||
parseRemoteIOR: typeof parseRemoteIOR;
|
||||
clearRemoteCache: typeof clearRemoteCache;
|
||||
};
|
||||
21
index.js
Normal file
21
index.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Main entry point for @metatrom/ior-resolver
|
||||
* Provides unified exports for both Metro and Node.js environments
|
||||
*/
|
||||
|
||||
// Export Metro resolver functions
|
||||
const metroResolver = require('./lib/metro-ior-resolver.js');
|
||||
|
||||
module.exports = {
|
||||
// Metro exports
|
||||
createIORResolver: metroResolver.createIORResolver,
|
||||
createHotReloadableResolver: metroResolver.createHotReloadableResolver,
|
||||
buildIORMappings: metroResolver.buildIORMappings,
|
||||
buildLocalIORMappings: metroResolver.buildLocalIORMappings,
|
||||
fetchRemoteComponent: metroResolver.fetchRemoteComponent,
|
||||
parseRemoteIOR: metroResolver.parseRemoteIOR,
|
||||
clearRemoteCache: metroResolver.clearRemoteCache,
|
||||
|
||||
// Re-export metro module for convenience
|
||||
metro: metroResolver,
|
||||
};
|
||||
26
lib/fetch-remote-component.js
Normal file
26
lib/fetch-remote-component.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// Suppress console.log from the resolver during fetching
|
||||
const originalLog = console.log;
|
||||
console.log = () => {};
|
||||
|
||||
const { fetchRemoteComponent } = require('./metro-ior-resolver.js');
|
||||
|
||||
// Restore console.log for our output
|
||||
console.log = originalLog;
|
||||
|
||||
const ior = process.argv[2];
|
||||
if (!ior) {
|
||||
console.error('No IOR provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fetchRemoteComponent(ior)
|
||||
.then(path => {
|
||||
// Output only the path on stdout
|
||||
console.log(path);
|
||||
process.exit(0);
|
||||
})
|
||||
.catch(err => {
|
||||
// Output error to stderr
|
||||
console.error('FETCH_ERROR:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
440
lib/ior-core.js
Normal file
440
lib/ior-core.js
Normal file
@@ -0,0 +1,440 @@
|
||||
/**
|
||||
* Core IOR resolver functionality shared between Metro and Node.js implementations
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const https = require('node:https');
|
||||
const http = require('node:http');
|
||||
const { execSync } = require('node:child_process');
|
||||
|
||||
/**
|
||||
* Parse remote IOR format
|
||||
* Format examples:
|
||||
* - ior:github:owner/repo:componentname@version
|
||||
* - ior:gitea:instance.com:owner/repo:componentname@version
|
||||
* - ior:ipfs:QmHash:componentname@version
|
||||
* - ior:p2p:peer-id:componentname@version
|
||||
*/
|
||||
function parseRemoteIOR(ior) {
|
||||
const parts = ior.split(':');
|
||||
|
||||
if (parts.length < 4 || parts[0] !== 'ior') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const type = parts[1];
|
||||
|
||||
switch (type) {
|
||||
case 'github': {
|
||||
// ior:github:owner/repo:componentname@version
|
||||
const [, , repoPath, componentAndVersion] = parts;
|
||||
const [owner, repo] = repoPath.split('/');
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'github',
|
||||
owner,
|
||||
repository: repo,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
case 'gitea': {
|
||||
// Two possible formats:
|
||||
// ior:gitea:instance.com:owner/repo:componentname@version (5 parts)
|
||||
// ior:gitea:instance.com:owner/repo/componentname@version (4 parts - component name in path)
|
||||
|
||||
if (parts.length === 4) {
|
||||
// Format: ior:gitea:instance.com:owner/repo/componentname@version
|
||||
const [, , instance, pathAndVersion] = parts;
|
||||
const [fullPath, version] = pathAndVersion.split('@');
|
||||
const pathParts = fullPath.split('/');
|
||||
|
||||
const owner = pathParts[0];
|
||||
const componentName = pathParts[pathParts.length - 1];
|
||||
const repository = pathParts.slice(1).join('/') || componentName;
|
||||
|
||||
return {
|
||||
type: 'gitea',
|
||||
instance,
|
||||
owner,
|
||||
repository,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
} else if (parts.length === 5) {
|
||||
// Format: ior:gitea:instance.com:owner/repo:componentname@version
|
||||
const [, , instance, repoPath, componentAndVersion] = parts;
|
||||
const repoPathParts = repoPath.split('/');
|
||||
const owner = repoPathParts[0];
|
||||
const repository = repoPathParts.slice(1).join('/');
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'gitea',
|
||||
instance,
|
||||
owner,
|
||||
repository,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
case 'ipfs': {
|
||||
// ior:ipfs:QmHash:componentname@version
|
||||
const [, , hash, componentAndVersion] = parts;
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'ipfs',
|
||||
hash,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
case 'p2p': {
|
||||
// ior:p2p:peer-id:componentname@version
|
||||
const [, , peerId, componentAndVersion] = parts;
|
||||
const [componentName, version] = componentAndVersion.split('@');
|
||||
|
||||
return {
|
||||
type: 'p2p',
|
||||
peerId,
|
||||
componentName,
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse package.json to find IOR mapping
|
||||
*/
|
||||
function parsePackageJson(packagePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(packagePath, 'utf-8');
|
||||
const pkg = JSON.parse(content);
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse package.json asynchronously
|
||||
*/
|
||||
async function parsePackageJsonAsync(packagePath) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(packagePath, 'utf-8');
|
||||
const pkg = JSON.parse(content);
|
||||
return pkg;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content from URL
|
||||
*/
|
||||
function fetchUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.startsWith('https:') ? https : http;
|
||||
|
||||
client.get(url, (res) => {
|
||||
if (res.statusCode === 302 || res.statusCode === 301) {
|
||||
// Handle redirects
|
||||
fetchUrl(res.headers.location).then(resolve).catch(reject);
|
||||
return;
|
||||
}
|
||||
|
||||
if (res.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch URL and return as Buffer
|
||||
*/
|
||||
async function fetchUrlBuffer(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const protocol = url.startsWith('https:') ? https : http;
|
||||
|
||||
protocol.get(url, (response) => {
|
||||
// Handle redirects
|
||||
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||
return fetchUrlBuffer(response.headers.location).then(resolve).catch(reject);
|
||||
}
|
||||
|
||||
if (response.statusCode !== 200) {
|
||||
reject(new Error(`HTTP ${response.statusCode}: ${url}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks = [];
|
||||
response.on('data', (chunk) => chunks.push(chunk));
|
||||
response.on('end', () => resolve(Buffer.concat(chunks)));
|
||||
response.on('error', reject);
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component archive from GitHub
|
||||
*/
|
||||
async function fetchFromGitHub(config) {
|
||||
const { owner, repository, componentName, version } = config;
|
||||
|
||||
// Try both tag formats: "1.0.0" and "v1.0.0"
|
||||
const tagVariants = [version, `v${version}`];
|
||||
|
||||
for (const tag of tagVariants) {
|
||||
// GitHub API endpoint for tarball
|
||||
const archiveUrl = `https://api.github.com/repos/${owner}/${repository}/tarball/${tag}`;
|
||||
|
||||
try {
|
||||
console.log(`[IOR Resolver] Fetching GitHub archive: ${archiveUrl}`);
|
||||
const archiveBuffer = await fetchUrlBuffer(archiveUrl);
|
||||
console.log(`[IOR Resolver] Downloaded archive from GitHub (${archiveBuffer.length} bytes)`);
|
||||
return { archive: archiveBuffer, componentName };
|
||||
} catch (err) {
|
||||
console.log(`[IOR Resolver] Failed to fetch with tag ${tag}: ${err.message}`);
|
||||
// Try next tag variant
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch component from GitHub: ${owner}/${repository}@${version} (tried tags: ${tagVariants.join(', ')})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component archive from Gitea
|
||||
*/
|
||||
async function fetchFromGitea(config) {
|
||||
const { instance, owner, repository, componentName, version } = config;
|
||||
|
||||
// Try both tag formats: "1.0.0" and "v1.0.0"
|
||||
const tagVariants = [version, `v${version}`];
|
||||
|
||||
for (const tag of tagVariants) {
|
||||
// Try to download the archive
|
||||
const archiveUrl = `https://${instance}/api/v1/repos/${owner}/${repository}/archive/${tag}.tar.gz`;
|
||||
|
||||
try {
|
||||
console.log(`[IOR Resolver] Fetching Gitea archive: ${archiveUrl}`);
|
||||
const archiveBuffer = await fetchUrlBuffer(archiveUrl);
|
||||
console.log(`[IOR Resolver] Downloaded archive from Gitea (${archiveBuffer.length} bytes)`);
|
||||
return { archive: archiveBuffer, componentName };
|
||||
} catch (err) {
|
||||
console.log(`[IOR Resolver] Failed to fetch with tag ${tag}: ${err.message}`);
|
||||
// Try next tag variant
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch component from Gitea: ${instance}/${owner}/${repository}@${version} (tried tags: ${tagVariants.join(', ')})`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch component from IPFS
|
||||
*/
|
||||
async function fetchFromIPFS(config) {
|
||||
const { hash, componentName, version } = config;
|
||||
|
||||
// Try multiple IPFS gateways for redundancy
|
||||
const gateways = [
|
||||
`https://ipfs.io/ipfs/${hash}`,
|
||||
`https://gateway.pinata.cloud/ipfs/${hash}`,
|
||||
`https://cloudflare-ipfs.com/ipfs/${hash}`,
|
||||
];
|
||||
|
||||
for (const gateway of gateways) {
|
||||
try {
|
||||
// Try to get main file from package.json first
|
||||
const packageUrl = `${gateway}/${componentName}/${version}/package.json`;
|
||||
let mainFile = `${componentName}.ts`;
|
||||
|
||||
try {
|
||||
const pkgContent = await fetchUrl(packageUrl);
|
||||
const pkgData = JSON.parse(pkgContent);
|
||||
if (pkgData.main) {
|
||||
mainFile = pkgData.main;
|
||||
}
|
||||
} catch {
|
||||
// Use default
|
||||
}
|
||||
|
||||
const urls = [
|
||||
`${gateway}/${componentName}/${version}/${mainFile}`,
|
||||
`${gateway}/${componentName}/${version}/index.ts`,
|
||||
`${gateway}/${componentName}/${version}/index.tsx`,
|
||||
];
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const content = await fetchUrl(url);
|
||||
return { content, url };
|
||||
} catch {
|
||||
// Try next URL
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Try next gateway
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to fetch from IPFS: ${hash}/${componentName}@${version}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract archive to directory
|
||||
*/
|
||||
function extractArchive(archiveBuffer, targetDir, stripComponents = 1) {
|
||||
const tempArchive = path.join(targetDir, '..', `temp-${Date.now()}.tar.gz`);
|
||||
|
||||
// Ensure target directory exists
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write archive to temp file
|
||||
fs.writeFileSync(tempArchive, archiveBuffer);
|
||||
|
||||
try {
|
||||
// Extract archive
|
||||
execSync(`tar -xzf "${tempArchive}" -C "${targetDir}" --strip-components=${stripComponents}`, {
|
||||
stdio: 'pipe'
|
||||
});
|
||||
|
||||
// Clean up temp archive
|
||||
fs.unlinkSync(tempArchive);
|
||||
} catch (err) {
|
||||
// Clean up on failure
|
||||
if (fs.existsSync(tempArchive)) {
|
||||
fs.unlinkSync(tempArchive);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find entry point in a directory
|
||||
*/
|
||||
function findEntryPoint(dir, componentName) {
|
||||
let entryPath = null;
|
||||
|
||||
// Check package.json for main field
|
||||
const packageJsonPath = path.join(dir, 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
if (packageJson.main) {
|
||||
const mainPath = path.join(dir, packageJson.main);
|
||||
if (fs.existsSync(mainPath)) {
|
||||
return mainPath;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Ignore package.json errors
|
||||
}
|
||||
}
|
||||
|
||||
// Try common entry points
|
||||
const possibleEntries = [
|
||||
path.join(dir, 'index.ts'),
|
||||
path.join(dir, 'index.tsx'),
|
||||
path.join(dir, 'index.js'),
|
||||
path.join(dir, 'index.jsx'),
|
||||
path.join(dir, `${componentName}.ts`),
|
||||
path.join(dir, `${componentName}.tsx`),
|
||||
path.join(dir, `${componentName}.js`),
|
||||
path.join(dir, `${componentName}.jsx`),
|
||||
];
|
||||
|
||||
for (const entry of possibleEntries) {
|
||||
if (fs.existsSync(entry)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cache hash for an IOR
|
||||
*/
|
||||
function generateCacheHash(ior) {
|
||||
return crypto.createHash('sha256').update(ior).digest('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan components directory to build local IOR mappings
|
||||
*/
|
||||
function buildLocalIORMappings(projectRoot) {
|
||||
const componentsDir = path.join(projectRoot, 'src', 'components');
|
||||
const mappings = new Map();
|
||||
|
||||
try {
|
||||
const componentDirs = fs.readdirSync(componentsDir, { withFileTypes: true });
|
||||
|
||||
for (const dir of componentDirs) {
|
||||
if (dir.isDirectory()) {
|
||||
const componentPath = path.join(componentsDir, dir.name);
|
||||
const versions = fs.readdirSync(componentPath, { withFileTypes: true });
|
||||
|
||||
for (const version of versions) {
|
||||
if (version.isDirectory()) {
|
||||
const versionPath = path.join(componentPath, version.name);
|
||||
const packagePath = path.join(versionPath, 'package.json');
|
||||
|
||||
const pkg = parsePackageJson(packagePath);
|
||||
if (pkg?.metatrom?.ior) {
|
||||
// Map both with and without esm: prefix
|
||||
const ior = pkg.metatrom.ior;
|
||||
const mainFile = pkg.main || 'index.ts';
|
||||
const resolvedPath = path.join(versionPath, mainFile);
|
||||
|
||||
mappings.set(`ior:esm:${ior}`, resolvedPath);
|
||||
mappings.set(`ior:${ior}`, resolvedPath);
|
||||
|
||||
console.log(`[IOR Resolver] Mapped local: ${ior} -> ${resolvedPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[IOR Resolver] Error building local mappings:', err);
|
||||
}
|
||||
|
||||
return mappings;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseRemoteIOR,
|
||||
parsePackageJson,
|
||||
parsePackageJsonAsync,
|
||||
fetchUrl,
|
||||
fetchUrlBuffer,
|
||||
fetchFromGitHub,
|
||||
fetchFromGitea,
|
||||
fetchFromIPFS,
|
||||
extractArchive,
|
||||
findEntryPoint,
|
||||
generateCacheHash,
|
||||
buildLocalIORMappings,
|
||||
};
|
||||
580
lib/metro-ior-resolver.js
Normal file
580
lib/metro-ior-resolver.js
Normal file
@@ -0,0 +1,580 @@
|
||||
/**
|
||||
* Enhanced Metro resolver with both local and remote IOR support
|
||||
* Uses shared core functionality from ior-core.js
|
||||
*/
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const {
|
||||
parseRemoteIOR,
|
||||
parsePackageJson,
|
||||
fetchFromGitHub,
|
||||
fetchFromGitea,
|
||||
fetchFromIPFS,
|
||||
extractArchive,
|
||||
findEntryPoint,
|
||||
generateCacheHash,
|
||||
buildLocalIORMappings,
|
||||
} = require('./ior-core.js');
|
||||
|
||||
/**
|
||||
* Cache for IOR to path mappings
|
||||
*/
|
||||
const remoteCache = new Map();
|
||||
|
||||
/**
|
||||
* Type cache for auto-generated type declarations
|
||||
*/
|
||||
const typeCache = new Map();
|
||||
const processedTypes = new Set();
|
||||
|
||||
/**
|
||||
* Remote cache configuration
|
||||
*/
|
||||
const CACHE_TTL = 3600000; // 1 hour default TTL
|
||||
// Use project-local cache directory instead of temp directory to avoid watchman issues
|
||||
const CACHE_DIR = path.join(process.cwd(), '.ior-cache', 'remote');
|
||||
|
||||
/**
|
||||
* Ensure cache directory exists
|
||||
*/
|
||||
function ensureCacheDir() {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache remote component locally (full repository)
|
||||
*/
|
||||
async function cacheRemoteComponent(ior, archiveBuffer, componentName) {
|
||||
ensureCacheDir();
|
||||
|
||||
const hash = generateCacheHash(ior);
|
||||
const cacheDir = path.join(CACHE_DIR, hash);
|
||||
|
||||
// Extract archive to cache directory
|
||||
try {
|
||||
extractArchive(archiveBuffer, cacheDir);
|
||||
} catch (err) {
|
||||
console.error(`[IOR Resolver] Failed to extract archive: ${err.message}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
// Find the entry point
|
||||
const entryPath = findEntryPoint(cacheDir, componentName);
|
||||
|
||||
if (!entryPath) {
|
||||
throw new Error(`No entry point found for component ${componentName} in ${cacheDir}`);
|
||||
}
|
||||
|
||||
// Update cache map with entry point
|
||||
remoteCache.set(ior, {
|
||||
path: entryPath,
|
||||
directory: cacheDir,
|
||||
timestamp: Date.now(),
|
||||
ttl: CACHE_TTL,
|
||||
});
|
||||
|
||||
console.log(`[IOR Resolver] Cached remote component: ${ior}`);
|
||||
console.log(`[IOR Resolver] Directory: ${cacheDir}`);
|
||||
console.log(`[IOR Resolver] Entry: ${entryPath}`);
|
||||
|
||||
// Generate type declarations asynchronously (non-blocking)
|
||||
generateTypeDeclaration(ior, cacheDir).catch(err => {
|
||||
console.warn(`[IOR Resolver] Type generation failed for ${ior}:`, err.message);
|
||||
});
|
||||
|
||||
// Update persistent mappings
|
||||
updatePersistentMappings(ior, entryPath, cacheDir);
|
||||
|
||||
return entryPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached remote component if valid
|
||||
*/
|
||||
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 directory (non-blocking)
|
||||
if (entry.directory) {
|
||||
try {
|
||||
fs.rmSync(entry.directory, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify entry file still exists
|
||||
if (fs.existsSync(entry.path)) {
|
||||
console.log(`[IOR Resolver] Using cached component: ${ior}`);
|
||||
console.log(`[IOR Resolver] Entry: ${entry.path}`);
|
||||
return entry.path;
|
||||
}
|
||||
|
||||
remoteCache.delete(ior);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate type declaration for a remote component
|
||||
*/
|
||||
async function generateTypeDeclaration(ior, cacheDir) {
|
||||
// Avoid duplicate processing
|
||||
if (processedTypes.has(ior)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Look for .d.ts files in the cached directory
|
||||
const files = fs.readdirSync(cacheDir);
|
||||
let typeContent = null;
|
||||
let typeFile = null;
|
||||
|
||||
// Priority order for type files
|
||||
const possibleTypeFiles = [
|
||||
files.find(f => f.endsWith('.d.ts')),
|
||||
'index.d.ts',
|
||||
'types.d.ts'
|
||||
].filter(Boolean);
|
||||
|
||||
for (const file of possibleTypeFiles) {
|
||||
const filePath = path.join(cacheDir, file);
|
||||
if (fs.existsSync(filePath)) {
|
||||
typeContent = fs.readFileSync(filePath, 'utf-8');
|
||||
typeFile = file;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeContent) {
|
||||
// Transform module declaration to match the actual import path
|
||||
const moduleRegex = /declare\s+module\s+['"]([^'"]+)['"]/;
|
||||
const match = typeContent.match(moduleRegex);
|
||||
|
||||
if (match && match[1] !== ior) {
|
||||
// Replace the module declaration with the actual IOR
|
||||
typeContent = typeContent.replace(
|
||||
moduleRegex,
|
||||
`declare module '${ior}'`
|
||||
);
|
||||
} else if (!match) {
|
||||
// No module declaration, wrap the content
|
||||
typeContent = `declare module '${ior}' {\n${typeContent}\n}\n`;
|
||||
}
|
||||
|
||||
// Extract the hash from the cache directory path
|
||||
const hash = path.basename(cacheDir);
|
||||
|
||||
// Add to type cache
|
||||
typeCache.set(ior, {
|
||||
content: typeContent,
|
||||
timestamp: Date.now(),
|
||||
source: typeFile,
|
||||
hash: hash
|
||||
});
|
||||
|
||||
// Mark as processed
|
||||
processedTypes.add(ior);
|
||||
|
||||
// Update the global types file
|
||||
await updateGlobalTypes();
|
||||
|
||||
console.log(`[IOR Resolver] Generated type declarations for ${ior} from ${typeFile}`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`[IOR Resolver] Failed to generate types for ${ior}:`, err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the auto-generated types file
|
||||
*/
|
||||
async function updateGlobalTypes() {
|
||||
const projectRoot = process.cwd();
|
||||
const typesDir = path.join(projectRoot, 'src', 'types');
|
||||
const globalTypesPath = path.join(typesDir, 'auto-generated.d.ts');
|
||||
|
||||
let content = '/**\n';
|
||||
content += ' * Auto-generated type declarations for remote IOR components\n';
|
||||
content += ` * Generated at: ${new Date().toISOString()}\n`;
|
||||
content += ' * DO NOT EDIT - This file is automatically maintained by metro-ior-resolver\n';
|
||||
content += ' */\n\n';
|
||||
|
||||
// Add all cached type definitions
|
||||
for (const [ior, entry] of typeCache.entries()) {
|
||||
content += `// Source: ${entry.source || 'unknown'}`;
|
||||
if (entry.hash) {
|
||||
content += `\n// Hash: ${entry.hash}\n`;
|
||||
} else {
|
||||
content += '\n';
|
||||
}
|
||||
content += `// Cached: ${new Date(entry.timestamp).toISOString()}\n`;
|
||||
content += entry.content;
|
||||
content += '\n\n';
|
||||
}
|
||||
|
||||
// Ensure directory exists
|
||||
if (!fs.existsSync(typesDir)) {
|
||||
fs.mkdirSync(typesDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the file
|
||||
fs.writeFileSync(globalTypesPath, content, 'utf-8');
|
||||
|
||||
console.log(`[IOR Resolver] Updated auto-generated types: ${globalTypesPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch remote component based on IOR
|
||||
*/
|
||||
async function fetchRemoteComponent(ior) {
|
||||
// Check cache first
|
||||
const cached = getCachedRemoteComponent(ior);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const config = parseRemoteIOR(ior);
|
||||
if (!config) {
|
||||
throw new Error(`Invalid remote IOR format: ${ior}`);
|
||||
}
|
||||
|
||||
console.log(`[IOR Resolver] Fetching remote component: ${ior}`);
|
||||
|
||||
let result;
|
||||
|
||||
switch (config.type) {
|
||||
case 'github':
|
||||
result = await fetchFromGitHub(config);
|
||||
break;
|
||||
|
||||
case 'gitea':
|
||||
result = await fetchFromGitea(config);
|
||||
break;
|
||||
|
||||
case 'ipfs':
|
||||
result = await fetchFromIPFS(config);
|
||||
break;
|
||||
|
||||
case 'p2p':
|
||||
throw new Error(`P2P fetching not yet implemented for Metro. PeerID: ${config.peerId}`);
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported remote type: ${config.type}`);
|
||||
}
|
||||
|
||||
// Cache the fetched component
|
||||
// For GitHub and Gitea, result contains { archive, componentName }
|
||||
// For IPFS, result contains { content, url }
|
||||
if (result.archive) {
|
||||
const cachedPath = await cacheRemoteComponent(ior, result.archive, result.componentName);
|
||||
return cachedPath;
|
||||
} else {
|
||||
// Legacy path for IPFS (still returns content directly)
|
||||
// Create a simple archive-like structure for IPFS content
|
||||
ensureCacheDir();
|
||||
const hash = generateCacheHash(ior);
|
||||
const cacheDir = path.join(CACHE_DIR, hash);
|
||||
|
||||
if (!fs.existsSync(cacheDir)) {
|
||||
fs.mkdirSync(cacheDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write content to index.ts
|
||||
const entryPath = path.join(cacheDir, 'index.ts');
|
||||
fs.writeFileSync(entryPath, result.content, 'utf-8');
|
||||
|
||||
// Update cache
|
||||
remoteCache.set(ior, {
|
||||
path: entryPath,
|
||||
directory: cacheDir,
|
||||
timestamp: Date.now(),
|
||||
ttl: CACHE_TTL,
|
||||
});
|
||||
|
||||
updatePersistentMappings(ior, entryPath, cacheDir);
|
||||
return entryPath;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced resolver function for Metro with remote support
|
||||
*/
|
||||
function createEnhancedIORResolver(projectRoot) {
|
||||
// Build local mappings on initialization
|
||||
const localMappings = buildLocalIORMappings(projectRoot);
|
||||
|
||||
return function enhancedIORResolver(context, moduleName, platform) {
|
||||
// Only handle IOR imports
|
||||
if (!moduleName.startsWith('ior:')) {
|
||||
// Not an IOR import, return undefined for default resolution
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// First check local mappings
|
||||
const localPath = localMappings.get(moduleName);
|
||||
|
||||
if (localPath) {
|
||||
console.log(`[IOR Resolver] Resolving local: ${moduleName} -> ${localPath}`);
|
||||
|
||||
// Check if file exists with various extensions
|
||||
const extensions = ['', '.ts', '.tsx', '.js', '.jsx'];
|
||||
for (const ext of extensions) {
|
||||
const fullPath = localPath.endsWith('.ts') || localPath.endsWith('.tsx') || localPath.endsWith('.js') || localPath.endsWith('.jsx')
|
||||
? localPath
|
||||
: localPath + ext;
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
return {
|
||||
type: 'sourceFile',
|
||||
filePath: path.resolve(fullPath),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Try index files if main file not found
|
||||
const dirPath = path.dirname(localPath);
|
||||
const indexExtensions = ['/index.ts', '/index.tsx', '/index.js', '/index.jsx'];
|
||||
for (const indexExt of indexExtensions) {
|
||||
const indexPath = dirPath + indexExt;
|
||||
if (fs.existsSync(indexPath)) {
|
||||
return {
|
||||
type: 'sourceFile',
|
||||
filePath: path.resolve(indexPath),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's a remote IOR (synchronously for now)
|
||||
const remoteConfig = parseRemoteIOR(moduleName);
|
||||
if (remoteConfig) {
|
||||
throw new Error(`Remote IOR components not yet supported in sync mode: ${moduleName}`);
|
||||
}
|
||||
|
||||
// IOR not found
|
||||
throw new Error(`Cannot resolve IOR module: ${moduleName}`);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a resolver with hot reload support
|
||||
* Rebuilds mappings when package.json files change
|
||||
*/
|
||||
function createHotReloadableEnhancedResolver(projectRoot) {
|
||||
let resolver = createEnhancedIORResolver(projectRoot);
|
||||
|
||||
// Watch for package.json changes in components directory
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const componentsDir = path.join(projectRoot, 'src', 'components');
|
||||
|
||||
try {
|
||||
// Simple file watcher for package.json files
|
||||
const watchDirs = (dir) => {
|
||||
fs.readdir(dir, { withFileTypes: true }, (err, entries) => {
|
||||
if (err) return;
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
watchDirs(fullPath);
|
||||
} else if (entry.name === 'package.json') {
|
||||
fs.watchFile(fullPath, { interval: 1000 }, () => {
|
||||
console.log(`[IOR Resolver] Detected change in ${fullPath}, rebuilding mappings...`);
|
||||
resolver = createEnhancedIORResolver(projectRoot);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watchDirs(componentsDir);
|
||||
} catch (err) {
|
||||
console.warn('[IOR Resolver] Could not set up file watching:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Return a wrapper function that always uses the latest resolver
|
||||
return (context, moduleName, platform) => {
|
||||
return resolver(context, moduleName, platform);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear remote cache
|
||||
*/
|
||||
function clearRemoteCache() {
|
||||
remoteCache.clear();
|
||||
|
||||
if (fs.existsSync(CACHE_DIR)) {
|
||||
const files = fs.readdirSync(CACHE_DIR);
|
||||
for (const file of files) {
|
||||
try {
|
||||
fs.unlinkSync(path.join(CACHE_DIR, file));
|
||||
} catch {
|
||||
// Ignore errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[IOR Resolver] Remote cache cleared');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update persistent IOR mappings
|
||||
*/
|
||||
function updatePersistentMappings(ior, entryPath, cacheDir) {
|
||||
const mappingFile = path.join(process.cwd(), '.ior-cache', 'mappings.json');
|
||||
|
||||
try {
|
||||
// Load existing mappings or create new
|
||||
let data = { version: '1.0', mappings: {} };
|
||||
if (fs.existsSync(mappingFile)) {
|
||||
try {
|
||||
data = JSON.parse(fs.readFileSync(mappingFile, 'utf-8'));
|
||||
} catch {
|
||||
// Use default if file is corrupted
|
||||
}
|
||||
}
|
||||
|
||||
// Update mappings
|
||||
const hash = path.basename(cacheDir);
|
||||
data.mappings[ior] = {
|
||||
path: entryPath,
|
||||
directory: cacheDir,
|
||||
hash: hash,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
data.generated = new Date().toISOString();
|
||||
|
||||
// Save mappings
|
||||
const mappingDir = path.dirname(mappingFile);
|
||||
if (!fs.existsSync(mappingDir)) {
|
||||
fs.mkdirSync(mappingDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(mappingFile, JSON.stringify(data, null, 2), 'utf-8');
|
||||
} catch (err) {
|
||||
// Non-critical error, just log
|
||||
console.warn('[IOR Resolver] Failed to update mappings:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load persistent IOR mappings if available
|
||||
*/
|
||||
function loadIORMappings() {
|
||||
const mappingFile = path.join(process.cwd(), '.ior-cache', 'mappings.json');
|
||||
|
||||
if (fs.existsSync(mappingFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(mappingFile, 'utf-8'));
|
||||
console.log(`[IOR Resolver] Loaded IOR mappings from ${mappingFile}`);
|
||||
|
||||
// Restore mappings to remoteCache
|
||||
if (data.mappings) {
|
||||
for (const [ior, mapping] of Object.entries(data.mappings)) {
|
||||
// Verify the cached files still exist
|
||||
if (fs.existsSync(mapping.path)) {
|
||||
remoteCache.set(ior, {
|
||||
path: mapping.path,
|
||||
directory: mapping.directory,
|
||||
timestamp: mapping.timestamp,
|
||||
ttl: CACHE_TTL,
|
||||
});
|
||||
}
|
||||
}
|
||||
console.log(`[IOR Resolver] Restored ${remoteCache.size} cached components`);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.warn('[IOR Resolver] Failed to load mappings:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize type declarations for existing cached components
|
||||
*/
|
||||
async function initializeTypesFromCache() {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[IOR Resolver] Scanning cache for existing components...');
|
||||
|
||||
// First try to load persistent mappings
|
||||
loadIORMappings();
|
||||
|
||||
try {
|
||||
const dirs = fs.readdirSync(CACHE_DIR, { withFileTypes: true });
|
||||
|
||||
for (const dir of dirs) {
|
||||
if (dir.isDirectory()) {
|
||||
const cacheDir = path.join(CACHE_DIR, dir.name);
|
||||
|
||||
// Try to determine the IOR from the remoteCache
|
||||
let foundIor = null;
|
||||
for (const [ior, entry] of remoteCache.entries()) {
|
||||
if (entry.directory === cacheDir) {
|
||||
foundIor = ior;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundIor) {
|
||||
// Try to read package.json to get hints about the component
|
||||
const pkgPath = path.join(cacheDir, 'package.json');
|
||||
if (fs.existsSync(pkgPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
||||
if (pkg.name && pkg.version) {
|
||||
console.log(`[IOR Resolver] Found cached component without IOR mapping: ${pkg.name}@${pkg.version}`);
|
||||
}
|
||||
} catch {
|
||||
// Skip if we can't read package.json
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate types for this cached component
|
||||
await generateTypeDeclaration(foundIor, cacheDir);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[IOR Resolver] Finished scanning cache');
|
||||
} catch (err) {
|
||||
console.warn('[IOR Resolver] Failed to initialize types from cache:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize types from cache on module load
|
||||
initializeTypesFromCache().catch(err => {
|
||||
console.warn('[IOR Resolver] Failed to initialize types:', err.message);
|
||||
});
|
||||
|
||||
// Export unified functions
|
||||
module.exports = {
|
||||
// Main exports
|
||||
createIORResolver: createEnhancedIORResolver,
|
||||
createHotReloadableResolver: createHotReloadableEnhancedResolver,
|
||||
buildIORMappings: buildLocalIORMappings,
|
||||
buildLocalIORMappings,
|
||||
fetchRemoteComponent,
|
||||
parseRemoteIOR,
|
||||
clearRemoteCache,
|
||||
};
|
||||
78
package.json
Normal file
78
package.json
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"name": "@metatrom/ior-resolver",
|
||||
"version": "1.0.0",
|
||||
"description": "Interoperable Object Reference (IOR) resolver for Metro bundler and Node.js with support for local and remote component fetching",
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"bin": {
|
||||
"prebuild-ior-types": "./bin/prebuild-ior-types.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"require": "./index.js",
|
||||
"import": "./index.js",
|
||||
"types": "./index.d.ts"
|
||||
},
|
||||
"./metro": {
|
||||
"require": "./lib/metro-ior-resolver.js",
|
||||
"types": "./lib/metro-ior-resolver.d.ts"
|
||||
},
|
||||
"./node": {
|
||||
"import": "./bin/NodeImportLoader.ts",
|
||||
"types": "./bin/NodeImportLoader.d.ts"
|
||||
},
|
||||
"./prebuild": {
|
||||
"require": "./bin/prebuild-ior-types.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "npm run build",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"index.d.ts",
|
||||
"lib/",
|
||||
"bin/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"keywords": [
|
||||
"ior",
|
||||
"resolver",
|
||||
"metro",
|
||||
"react-native",
|
||||
"node",
|
||||
"import",
|
||||
"loader",
|
||||
"remote",
|
||||
"components",
|
||||
"metatrom",
|
||||
"interoperable"
|
||||
],
|
||||
"author": "Metatrom",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-native/metro-config": ">=0.80.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@react-native/metro-config": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/metatrom/ior-resolver.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/metatrom/ior-resolver/issues"
|
||||
},
|
||||
"homepage": "https://github.com/metatrom/ior-resolver#readme"
|
||||
}
|
||||
Reference in New Issue
Block a user