UNPKG

scraps

Version:

Use NodeJS features in Scriptable!

261 lines (260 loc) 9.32 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); ; async function Scraps(main) { const fm = FileManager.local().isFileStoredIniCloud(module.filename) ? FileManager.iCloud() : FileManager.local(); const part = (path) => 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).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, 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 = { fs: { readFileSync: (path) => { return fm.read(part(path)); }, readFile: async (path) => { return fm.read(part(path)); }, readdirSync: (path) => { return fm.listContents(part(path)); }, readdir: async (path) => { return fm.listContents(part(path)); }, existsSync: (path) => { return fm.fileExists(part(path)); }, exists: async (path) => { return fm.fileExists(part(path)); }, writeFileSync(path, 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, data) { return this.writeFileSync(path, data); }, }, path: { join: (...args) => { return args .map((p, i) => (i === 0 ? p.replace(/\/+$/, '') : p.replace(/^\/+|\/+$/g, ''))) .filter(Boolean) .join('/'); }, dirname: (path) => { const idx = path.replace(/\/+$/, '').lastIndexOf('/'); return idx > -1 ? path.slice(0, idx) : '.'; }, basename: (path) => { const idx = path.lastIndexOf('/'); return idx > -1 ? path.slice(idx + 1) : path; }, extname: (path) => { 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) => { 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 = {}; class Module { constructor(filename) { this.id = filename; this.filename = filename; this.dirname = builtins.path.dirname(filename); this.exports = {}; this.loaded = false; moduleCache[filename] = this; } ; require(request) { return loadModule(resolveFilename(request, this.dirname), this); } ; id; filename; dirname; exports; loaded; } ; function resolveFilename(request, fromDir) { 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, parentModule) { 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 }; } ; exports.default = Scraps;