@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
546 lines • 21.1 kB
JavaScript
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