scraps
Version:
Use NodeJS features in Scriptable!
261 lines (260 loc) • 9.32 kB
JavaScript
;
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;