UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

546 lines 21.1 kB
import os from 'os'; import path from 'path'; import crypto from 'crypto'; import fs from 'fs/promises'; import { existsSync } from 'fs'; import { transform } from 'esbuild'; import { pathToFileURL } from 'url'; import * as clack from '@clack/prompts'; const CDN_URLS = { 'esm.sh': 'https://esm.sh', 'jsr.io': 'https://jsr.io', 'unpkg': 'https://unpkg.com', 'skypack': 'https://cdn.skypack.dev', 'jsdelivr': 'https://cdn.jsdelivr.net/npm' }; export class ModuleLoader { constructor(options = {}) { this.memoryCache = new Map(); this.pendingLoads = new Map(); this.isInitialized = false; this.options = { cache: true, preferredCDN: 'esm.sh', verbose: false, cdnOnly: false, ...options }; this.cacheDir = this.options.cacheDir || path.join(os.homedir(), '.xec', 'module-cache'); } async init() { if (this.isInitialized) return; if (this.options.cache) { await fs.mkdir(this.cacheDir, { recursive: true }); } globalThis.use = (spec) => this.importModule(spec); globalThis.x = (spec) => this.importModule(spec); globalThis.Import = (spec) => this.importModule(spec); globalThis.__xecModuleContext = { import: (spec) => this.importModule(spec) }; try { const scriptUtils = await import('./script-utils.js'); Object.assign(globalThis, scriptUtils.default); Object.assign(globalThis, { $: scriptUtils.$, log: scriptUtils.log, question: scriptUtils.question, confirm: scriptUtils.confirm, select: scriptUtils.select, multiselect: scriptUtils.multiselect, password: scriptUtils.password, spinner: scriptUtils.spinner, echo: scriptUtils.echo, cd: scriptUtils.cd, pwd: scriptUtils.pwd, fs: scriptUtils.fs, glob: scriptUtils.glob, path: scriptUtils.path, os: scriptUtils.os, fetch: scriptUtils.fetch, chalk: scriptUtils.chalk, which: scriptUtils.which, sleep: scriptUtils.sleep, retry: scriptUtils.retry, within: scriptUtils.within, env: scriptUtils.env, setEnv: scriptUtils.setEnv, exit: scriptUtils.exit, kill: scriptUtils.kill, ps: scriptUtils.ps, tmpdir: scriptUtils.tmpdir, tmpfile: scriptUtils.tmpfile, yaml: scriptUtils.yaml, csv: scriptUtils.csv, diff: scriptUtils.diff, parseArgs: scriptUtils.parseArgs, loadEnv: scriptUtils.loadEnv, quote: scriptUtils.quote, template: scriptUtils.template, }); } catch (error) { if (this.options.verbose) { console.warn('[ModuleLoader] Failed to load script utilities:', error); } } this.isInitialized = true; } async importModule(specifier) { const prefixMatch = specifier.match(/^(npm|jsr|esm|unpkg|skypack|jsdelivr):(.*)/); if (prefixMatch) { const [, source, pkg] = prefixMatch; if (source && pkg) { return this.importFromCDN(pkg, source); } } if (this.isLocalModule(specifier)) { const originalImport = globalThis.__originalImport || (async (spec) => import(spec)); return originalImport(specifier); } if (this.pendingLoads.has(specifier)) { return this.pendingLoads.get(specifier); } const loadPromise = this._importModule(specifier); this.pendingLoads.set(specifier, loadPromise); try { return await loadPromise; } finally { this.pendingLoads.delete(specifier); } } async _importModule(specifier) { if (this.options.cdnOnly) { return this.importFromCDN(specifier, 'auto'); } try { const originalImport = globalThis.__originalImport || (async (spec) => import(spec)); return await originalImport(specifier); } catch { return this.importFromCDN(specifier, 'auto'); } } async importFromCDN(pkg, source) { const cacheKey = `${source}:${pkg}`; if (this.pendingLoads.has(cacheKey)) { return this.pendingLoads.get(cacheKey); } const loadPromise = this._importFromCDN(pkg, source); this.pendingLoads.set(cacheKey, loadPromise); try { return await loadPromise; } finally { this.pendingLoads.delete(cacheKey); } } async _importFromCDN(pkg, source) { const cdnUrl = this.getCDNUrl(pkg, source); if (this.options.verbose) { console.debug(`[ModuleLoader] Loading ${pkg} from CDN: ${cdnUrl}`); } try { const cached = await this.loadFromCache(cdnUrl); if (cached) { const cacheEntry = this.memoryCache.get(cdnUrl); return await this.executeModule(cached, cdnUrl, cacheEntry?.headers); } const content = await this.fetchFromCDN(cdnUrl); const transformedContent = this.transformESMContent(content, cdnUrl); if (this.options.cache) { await this.saveToCache(cdnUrl, transformedContent); } return await this.executeModule(transformedContent, cdnUrl); } catch (error) { if (this.options.verbose) { console.error(`[ModuleLoader] Failed to import ${pkg} from CDN:`, error); } throw new Error(`Failed to import module '${pkg}' from CDN: ${error instanceof Error ? error.message : String(error)}`); } } isLocalModule(specifier) { if (this.options.cdnOnly) { return specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('file://') || specifier.startsWith('node:') || path.isAbsolute(specifier); } return specifier.startsWith('@xec-sh/') || specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('file://') || specifier.startsWith('node:') || path.isAbsolute(specifier); } getCDNUrl(pkg, source) { if (pkg.startsWith('http')) return pkg; const cdnKey = source === 'auto' ? this.options.preferredCDN || 'esm.sh' : source === 'npm' || source === 'esm' ? 'esm.sh' : source; const baseUrl = CDN_URLS[cdnKey] || CDN_URLS['esm.sh']; if (source === 'jsr' || (source === 'auto' && pkg.startsWith('@'))) { return `${baseUrl}/jsr/${pkg}${cdnKey === 'esm.sh' ? '?bundle' : ''}`; } return `${baseUrl}/${pkg}${cdnKey === 'esm.sh' ? '?bundle' : ''}`; } async fetchFromCDN(url) { const response = await fetch(url, { headers: { 'User-Agent': 'xec-cli/1.0', 'Accept': 'application/javascript, text/javascript, */*' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const content = await response.text(); const headers = {}; response.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; }); this._tempHeaders = headers; const redirectMatch = content.match(/export (?:\* from|\{ default \} from) ["'](\/.+?)["']/); if (url.includes('esm.sh') && redirectMatch?.[1]) { return this.fetchFromCDN(`https://esm.sh${redirectMatch[1]}`); } return content; } detectModuleType(content, headers) { const contentType = headers?.['content-type'] || ''; const xModuleType = headers?.['x-module-type']; if (xModuleType) return xModuleType; const cleanContent = content .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/.*/g, '') .replace(/["'][^"']*["']/g, ''); const esmPatterns = [ /^\s*import\s+[\w{},\s*]+\s+from\s+["']/m, /^\s*import\s+["']/m, /^\s*export\s+(?:default|const|let|var|function|class|async|{)/m, /^\s*export\s*\{[^}]+\}\s*from\s*["']/m, /^\s*export\s*\*\s*from\s*["']/m ]; const cjsPatterns = [ /^\s*module\.exports\s*=/m, /^\s*exports\.[\w$]+\s*=/m, /^\s*Object\.defineProperty\s*\(\s*exports/m, /\brequire\s*\(["'][^"']+["']\)/ ]; const umdPatterns = [ /typeof\s+exports\s*===\s*["']object["']\s*&&\s*typeof\s+module\s*!==\s*["']undefined["']/, /typeof\s+define\s*===\s*["']function["']\s*&&\s*define\.amd/, /\(function\s*\(.*?\)\s*{[\s\S]*?}\s*\(.*?typeof\s+exports.*?\)\)/ ]; if (umdPatterns.some(pattern => pattern.test(cleanContent))) { return 'umd'; } const hasEsmSyntax = esmPatterns.some(pattern => pattern.test(cleanContent)); const hasCjsSyntax = cjsPatterns.some(pattern => pattern.test(cleanContent)); if (hasEsmSyntax && hasCjsSyntax) { if (cleanContent.includes('__esModule')) { return 'esm'; } return 'umd'; } if (hasEsmSyntax) return 'esm'; if (hasCjsSyntax) return 'cjs'; if (/^\s*\(\s*function\s*\([^)]*\)\s*{/.test(cleanContent)) { return 'cjs'; } return 'unknown'; } async executeModule(content, specifier, headers) { try { const moduleType = this.detectModuleType(content, headers); if (this.options.verbose) { console.debug(`[ModuleLoader] Detected module type for ${specifier}: ${moduleType}`); } switch (moduleType) { case 'esm': return await this.executeESMModule(content, specifier); case 'cjs': return this.executeCJSModule(content); case 'umd': return this.executeUMDModule(content, specifier); case 'unknown': default: try { return await this.executeESMModule(content, specifier); } catch (esmError) { if (this.options.verbose) { console.debug(`[ModuleLoader] ESM execution failed, trying CJS:`, esmError); } return this.executeCJSModule(content); } } } catch (error) { if (this.options.verbose) { console.error(`[ModuleLoader] Failed to execute module ${specifier}:`, error); } throw error; } } async loadFromCache(url) { if (!this.options.cache) return null; const memCached = this.memoryCache.get(url); if (memCached && Date.now() - memCached.timestamp < 3600000) { return memCached.content; } try { const cacheKey = this.getCacheKey(url); const cachePath = path.join(this.cacheDir, `${cacheKey}.js`); const metaPath = path.join(this.cacheDir, `${cacheKey}.meta.json`); const stat = await fs.stat(cachePath); if (Date.now() - stat.mtimeMs > 7 * 24 * 3600000) return null; const content = await fs.readFile(cachePath, 'utf-8'); let headers = {}; try { const meta = JSON.parse(await fs.readFile(metaPath, 'utf-8')); headers = meta.headers || {}; } catch { } this.memoryCache.set(url, { content, timestamp: Date.now(), headers }); return content; } catch { return null; } } async saveToCache(url, content) { if (!this.options.cache) return; const headers = this._tempHeaders || {}; delete this._tempHeaders; this.memoryCache.set(url, { content, timestamp: Date.now(), headers }); const cachePath = path.join(this.cacheDir, `${this.getCacheKey(url)}.js`); await fs.writeFile(cachePath, content).catch(() => { }); const metaPath = path.join(this.cacheDir, `${this.getCacheKey(url)}.meta.json`); await fs.writeFile(metaPath, JSON.stringify({ headers, timestamp: Date.now() })).catch(() => { }); } getCacheKey(url) { return crypto.createHash('sha256').update(url).digest('hex'); } transformESMContent(content, cdnUrl) { if (!cdnUrl.includes('esm.sh')) return content; content = content.replace(/["']\/node\/([^"']+?)["']/g, (match, modulePath) => { const moduleName = modulePath.replace(/\.m?js$/, ''); const quote = match[0]; return `${quote}node:${moduleName}${quote}`; }); return content .replace(/from\s+["'](\/.+?)["']/g, (match, importPath) => { if (!importPath.startsWith('/node/')) { return `from "https://esm.sh${importPath}"`; } return match; }) .replace(/import\s*\(\s*["'](\/.+?)["']\s*\)/g, (match, importPath) => { if (!importPath.startsWith('/node/')) { return `import("https://esm.sh${importPath}")`; } return match; }) .replace(/import\s+["'](\/.+?)["'](?:\s*;)?/g, (match, importPath) => { if (!importPath.startsWith('/node/')) { return `import "https://esm.sh${importPath}"`; } return match; }) .replace(/export\s+(?:\*|\{[^}]+\})\s+from\s+["'](\/.+?)["']/g, (match, importPath) => { if (!importPath.startsWith('/node/')) { return match.replace(importPath, `https://esm.sh${importPath}`); } return match; }); } async executeESMModule(content, specifier) { const tempDir = path.join(this.cacheDir, 'temp'); await fs.mkdir(tempDir, { recursive: true }); const hash = crypto.createHash('sha256').update(specifier).digest('hex').substring(0, 8); const tempFile = path.join(tempDir, `module-${hash}-${Date.now()}.mjs`); try { await fs.writeFile(tempFile, content); const originalImport = globalThis.__originalImport || (async (spec) => import(spec)); const module = await originalImport(pathToFileURL(tempFile).href); await fs.unlink(tempFile).catch(() => { }); return module; } catch (error) { await fs.unlink(tempFile).catch(() => { }); throw error; } } executeCJSModule(content) { const moduleExports = {}; const moduleObj = { exports: moduleExports }; const requireStub = (id) => { if (id === 'util' || id === 'path' || id === 'fs') { throw new Error(`Cannot require '${id}' in browser environment`); } throw new Error(`require('${id}') not supported in CDN modules`); }; try { const func = new Function('exports', 'module', 'require', '__dirname', '__filename', content); func(moduleExports, moduleObj, requireStub, '/', '/module.js'); } catch (error) { const func = new Function('exports', 'module', 'require', content); func(moduleExports, moduleObj, requireStub); } const result = moduleObj.exports; if (result && typeof result === 'object' && Object.keys(result).length === 0) { return { default: {} }; } if (typeof result === 'function') { const wrapped = { default: result }; Object.assign(wrapped, result); return wrapped; } if (result && typeof result === 'object' && 'default' in result) { return result; } const wrapped = { default: result }; if (result && typeof result === 'object') { Object.assign(wrapped, result); } return wrapped; } executeUMDModule(content, specifier) { const moduleExports = {}; const moduleObj = { exports: moduleExports }; const define = (deps, factory) => { if (typeof deps === 'function') { factory = deps; deps = []; } if (factory) { const result = factory(); if (result !== undefined) { moduleObj.exports = result; } } }; define.amd = true; try { const func = new Function('exports', 'module', 'define', 'global', 'globalThis', 'window', 'self', content); const globalObj = globalThis; func(moduleExports, moduleObj, define, globalObj, globalObj, globalObj, globalObj); const result = moduleObj.exports; if (typeof result === 'function') { const wrapped = { default: result }; Object.assign(wrapped, result); return wrapped; } if (result && typeof result === 'object' && 'default' in result) { return result; } const wrapped = { default: result }; if (result && typeof result === 'object') { Object.assign(wrapped, result); } return wrapped; } catch (error) { return this.executeCJSModule(content); } } async transformTypeScript(code, filename) { const result = await transform(code, { format: 'esm', target: 'esnext', loader: filename.endsWith('.tsx') ? 'tsx' : 'ts', sourcemap: false, supported: { 'top-level-await': true } }); return result.code; } async loadScript(scriptPath, args = []) { await this.init(); let content = await fs.readFile(scriptPath, 'utf-8'); const ext = path.extname(scriptPath); if (ext === '.ts' || ext === '.tsx') { content = await this.transformTypeScript(content, scriptPath); } globalThis.__xecScriptContext = { args, argv: [process.argv[0], scriptPath, ...args], __filename: scriptPath, __dirname: path.dirname(scriptPath), }; try { return await this.executeESMModule(content, scriptPath); } finally { delete globalThis.__xecScriptContext; } } async clearCache() { this.memoryCache.clear(); if (existsSync(this.cacheDir)) { const files = await fs.readdir(this.cacheDir); await Promise.all(files.map(file => fs.unlink(path.join(this.cacheDir, file)))); } if (this.options.verbose) { clack.log.success('Module cache cleared'); } } async getCacheStats() { let fileEntries = 0; let totalSize = 0; if (existsSync(this.cacheDir)) { const files = await fs.readdir(this.cacheDir); fileEntries = files.length; for (const file of files) { const stat = await fs.stat(path.join(this.cacheDir, file)); totalSize += stat.size; } } return { memoryEntries: this.memoryCache.size, fileEntries, totalSize }; } } let loader = null; export function getModuleLoader(options) { if (!loader) { loader = new ModuleLoader(options); } return loader; } export async function initializeGlobalModuleContext(options) { const instance = getModuleLoader(options); await instance.init(); } export async function importModule(specifier) { const instance = getModuleLoader(); await instance.init(); return instance.importModule(specifier); } export function createCDNOnlyLoader(options) { return new ModuleLoader({ ...options, cdnOnly: true, verbose: options?.verbose ?? false, cache: options?.cache ?? true, preferredCDN: options?.preferredCDN ?? 'esm.sh' }); } //# sourceMappingURL=module-loader.js.map