UNPKG

scraps

Version:

Use NodeJS features in Scriptable!

278 lines (242 loc) 7.54 kB
type Package = { name: string, version: string, dependencies: { [name: string]: string, }, }; declare global { interface Request { loadJSON(): Promise<any>; loadString(): Promise<string>; } // WHY CAN'T I PUT A SEMICOLON HERE! }; async function Scraps(main: Package) { const fm: FileManager = FileManager.local().isFileStoredIniCloud(module.filename) ? FileManager.iCloud() : FileManager.local(); const part = (path: string) => path.replace(/^~/, fm.documentsDirectory()); const baseDir = part('~/node_modules/'); if (!main.dependencies) { main.dependencies = {}; }; // Ensure base directory exists if (!fm.fileExists(baseDir)) { fm.createDirectory(baseDir, true); }; // Pre-fetch and cache all dependencies for (let [pkg, version] of Object.entries(main.dependencies)) { if (pkg.startsWith('@types')) continue; if (version.startsWith('^')) { version = 'latest'; }; if (['latest', 'beta', 'alpha'].includes(version)) { const metaUrl = `https://data.jsdelivr.com/v1/package/npm/${pkg}`; const meta = await (new Request(metaUrl) as any).loadJSON(); main.dependencies[pkg] = meta.tags[version] || version; version = main.dependencies[pkg]; }; const pkgDir = `${baseDir}${pkg}@${version}/`; if (!fm.fileExists(pkgDir)) { const baseUrl = `https://cdn.jsdelivr.net/npm/${pkg}@${version}/`; const metaUrl = `https://data.jsdelivr.com/v1/package/npm/${pkg}@${version}`; const meta = await (new Request(metaUrl)).loadJSON(); async function downloadTree(tree: any, base = '') { console.log(`Downloading ${pkg}@${version}`); for (const file of tree) { if (file.type === 'file') { const relPath = base + file.name; const fileUrl = `${baseUrl}${relPath}`; const fullPath = `${pkgDir}${relPath}`; const localPath = part(fullPath); const exclude = ['.map', '.ts', 'jsx', 'tsx', '.xml', '.php', '.md']; const type = relPath.slice(relPath.lastIndexOf('.')); if (exclude.includes(type)) { continue; }; console.log(`Downloading ${relPath}`); const dir = localPath.slice(0, localPath.lastIndexOf('/')); if (!fm.fileExists(dir)) { fm.createDirectory(dir, true); }; const req = new Request(fileUrl); const code = await req.loadString(); fm.writeString(localPath, code); if (file.name == 'package.json') { await Scraps(JSON.parse(code)); }; } else if (file.type === 'directory') { await downloadTree(file.files, base + file.name + '/'); }; }; }; await downloadTree(meta.files); }; }; // Built-in modules const builtins: any = { fs: { readFileSync: (path: string) => { return fm.read(part(path)); }, readFile: async (path: string) => { return fm.read(part(path)); }, readdirSync: (path: string) => { return fm.listContents(part(path)); }, readdir: async (path: string) => { return fm.listContents(part(path)); }, existsSync: (path: string) => { return fm.fileExists(part(path)) }, exists: async (path: string) => { return fm.fileExists(part(path)) }, writeFileSync(path: string, data: string | Data) { if (typeof data === 'string') { fm.writeString(part(path), data); } else if (data instanceof Data) { fm.write(part(path), data); } else { throw new Error("Unsupported data type for fs.writeFileSync"); }; }, writeFile: async function (path: string, data: string | Data) { return this.writeFileSync(path, data); }, }, path: { join: (...args: string[]) => { return args .map((p, i) => (i === 0 ? p.replace(/\/+$/, '') : p.replace(/^\/+|\/+$/g, ''))) .filter(Boolean) .join('/') ; }, dirname: (path: string) => { const idx = path.replace(/\/+$/, '').lastIndexOf('/'); return idx > -1 ? path.slice(0, idx) : '.'; }, basename: (path: string) => { const idx = path.lastIndexOf('/'); return idx > -1 ? path.slice(idx + 1) : path; }, extname: (path: string) => { const base = path.split('/').pop() || path; const dot = base.lastIndexOf('.'); return dot > -1 ? base.slice(dot) : ''; }, }, os: { platform: () => 'ios', homedir: () => fm.documentsDirectory(), tmpdir: () => fm.temporaryDirectory() }, process: { now: () => 0, env: { 'NODE_ENV': 'Scraps.js' }, }, // Implement in the future util: { inspect: (obj: any) => { return JSON.stringify(obj); }, }, crypto: { randomBytes: () => { console.warn('crypto.randomBytes is not secure'); return new Uint8Array(16).map(() => Math.floor(Math.random() * 256)); }, }, }; // --- Module System --- const moduleCache: { [filename: string]: Module } = {}; class Module { constructor(filename: string) { this.id = filename; this.filename = filename; this.dirname = builtins.path.dirname(filename); this.exports = {}; this.loaded = false; moduleCache[filename] = this; }; public require(request: string) { return loadModule(resolveFilename(request, this.dirname), this); }; public id: string; public filename: string; public dirname: string; public exports: any; public loaded: boolean; }; function resolveFilename(request: string, fromDir: string) { const path = builtins.path; const nodePrefix = 'node'; if (request.startsWith(`${nodePrefix}:`)) { request = request.slice(nodePrefix.length + 1); }; if (builtins[request]) return request; // builtin if (request.startsWith('./') || request.startsWith('../') || request.startsWith('/')) { let filePath = path.join(fromDir, request); if (!filePath.endsWith('.js')) filePath += '.js'; return filePath; }; if (main.dependencies[request]) { const version = main.dependencies[request]; const path = `${baseDir}${request}@${version}/`; const pkgJSON = fm.readString(`${path}package.json`); const pkg = JSON.parse(pkgJSON); return `${path}${pkg.main}`; }; for (let [pkg, version] of Object.entries(main.dependencies)) { console.log(pkg, version); if (request.startsWith(`${pkg}/`)) { const path = `./${pkg}@${version}/`; return resolveFilename(`${path}${request.slice(pkg.length + 1)}`, part('~/node_modules/')); }; }; throw new Error(`Cannot resolve module "${request}" from "${fromDir}"`); }; function loadModule(filename: string, parentModule: Module) { if (builtins[filename]) return builtins[filename]; if (moduleCache[filename]) return moduleCache[filename].exports; if (!fm.fileExists(filename)) throw new Error(`Cannot find module: ${filename}`); const code = fm.readString(filename); const module = new Module(filename); const wrapper = `(function(exports, require, module, process, __filename, __dirname) { ${code} \n})`; let compiledWrapper; try { compiledWrapper = eval(wrapper); } catch (e) { console.warn(`Error compiling ${filename}: ${e}`); return { }; }; compiledWrapper( module.exports, module.require.bind(module), module, builtins['process'], module.filename, module.dirname ); module.loaded = true; return module.exports; }; // Optional cleanup function clear() { fm.remove(baseDir); }; // Top-level loader const topModule = new Module(`${baseDir}main.js`); topModule.require = function (pkg) { return loadModule(resolveFilename(pkg, baseDir), this); }; return { require: topModule.require.bind(topModule), clear, Module, baseDir }; }; export default Scraps;