UNPKG

module-sandbox

Version:

A v8 isolate sandbox with require support

154 lines (129 loc) 4.61 kB
const fs = require('fs') const p = require('path') const { builtinModules } = require('module') const resolveModule = require('browser-resolve') const detective = require('detective') const ivm = require('@andrewosh/isolated-vm') const NODE_MODULES = new Map([ ['os', 'os-browserify'], ['events', 'events/'], ['buffer', 'buffer/'], ['util', 'node-util'], ['crypto', 'crypto-browserify'], ['path', 'path-browserify'], ['stream', 'stream-browserify'], ['querystring', 'querystring-es3'], ['string_decoder', 'string_decoder/'], ['url', 'url/'] ]) const LOCAL_PREFIX = '@local:' module.exports = class RequireController { constructor (fs, rootContext, opts = {}) { this.fs = fs this.rootContext = rootContext this.signal = new SharedArrayBuffer(64) this._overrides = opts.overrides || new Map() this._signal = new Int32Array(this.signal) this._cache = new Map() this._nodeResolved = new Map() this._loaded = null } async _selectFs (name, from) { if (typeof this.fs !== 'function') return { name, from, fs: this.fs, prefix: '' } return this.fs(name, from) } async load (name, from, has) { if (this._loaded) throw new Error('Did not fetch previous load results') has = new Set(has) const resolved = new Set() const cache = new Map() const dups = [] const modules = [] const self = this await add(name, from) this._loaded = [...modules, ...dups] this._signal[0] = 1 Atomics.notify(this._signal, 0, 1) async function add (name, from) { const filename = await self._browserResolve(name, from, cache) if (!filename) return null const id = name + '\n' + filename + '\n' + from if (has.has(filename)) { if (resolved.has(id)) return null resolved.add(id) dups.push([name, filename, from, '']) return null } has.add(filename) resolved.add(id) let src = '' if (filename.startsWith(LOCAL_PREFIX)) { src = await fs.promises.readFile(filename.slice(LOCAL_PREFIX.length), { encoding: 'utf-8' }) } else { const { fs, name } = await self._selectFs(filename, from) src = await fs.readFile(name, { encoding: 'utf-8' }) } modules.push([name, filename, from, src]) const m = detective(src) for (const name of m) { await add(name, filename) } } } fetch () { if (!this._loaded) throw new Error('Did not load results, so there\'s nothing to fetch') const loaded = new ivm.ExternalCopy(this._loaded).copyInto({ release: true }) this._loaded = null return loaded } _resolveOpts (fs, basedir) { return { basedir, readFile: (path, cb) => fs.readFile(path).then(res => cb(null, res), err => cb(err)), isFile: (path, cb) => { fs.stat(path).then(st => cb(null, st.isFile()), (err) => { if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false) return cb(err) }) }, isDirectory: (path, cb) => fs.stat(path).then(st => cb(null, st.isDirectory()), err => { if (err.code === 'ENOENT' || err.code === 'ENOTDIR') return cb(null, false) return cb(err) }), realpath: (path, cb) => cb(null, path) } } _nodeResolve (name) { if (this._nodeResolved.has(name)) return this._nodeResolved.get(name) this._nodeResolved.set(name, require.resolve(name)) return this._nodeResolved.get(name) } async _browserResolve (name, from, cache) { const isLocal = from.startsWith(LOCAL_PREFIX) if (NODE_MODULES.has(name)) return LOCAL_PREFIX + this._nodeResolve(NODE_MODULES.get(name)) if (this._overrides.has(name) && !isLocal) { return LOCAL_PREFIX + this._overrides.get(name) } if (builtinModules.includes(name)) return null const fsResolved = await this._selectFs(name, from) let basedir = null if (isLocal) { from = from.slice(LOCAL_PREFIX.length) basedir = p.dirname(from) fsResolved.fs = fs.promises } else { basedir = p.dirname(fsResolved.from) } const id = name + '\n' + basedir if (cache.has(id)) return cache.get(id) return new Promise((resolve, reject) => { resolveModule(fsResolved.name, this._resolveOpts(fsResolved.fs, basedir), (err, filename) => { if (err) return reject(err) if (isLocal) filename = LOCAL_PREFIX + filename else if (fsResolved.prefix) filename = fsResolved.prefix + filename cache.set(id, filename) return resolve(filename) }) }) } }