scraps
Version:
Use NodeJS features in Scriptable!
278 lines (242 loc) • 7.54 kB
text/typescript
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;