postman-sandbox
Version:
Sandbox for Postman Scripts to run in Node.js or browser
190 lines (164 loc) • 5.48 kB
JavaScript
const { LEGACY_GLOBS } = require('./postman-legacy-interface'),
MODULE_KEY = '__module_obj', // why not use `module`?
MODULE_WRAPPER = [
'((exports, module) => {\n',
`\n})(${MODULE_KEY}.exports, ${MODULE_KEY});`
],
PACKAGE_TYPE_REGEX = /^[^:]+:[^:]+$/,
// This is used to determine if the package is external or internal.
// External packages are those that are not part of the Postman's
// Package Library and follow the format `registry:package`.
isExternalPackage = (name) => { return PACKAGE_TYPE_REGEX.test(name); };
/**
* Cache of all files that are available to be required.
*
* @typedef {Object.<string, { data: string } | { error: string }>} FileCache
*/
class PostmanRequireStore {
/**
* @param {FileCache} fileCache - fileCache
*/
constructor (fileCache) {
if (!fileCache) {
throw new Error('File cache is required');
}
this.fileCache = fileCache;
}
/**
* Check if the file is available in the cache.
*
* @param {string} path - path
* @returns {boolean}
*/
hasFile (path) {
return Boolean(this.getFile(path));
}
/**
* Get the file from the cache.
*
* @param {string} path - path
* @returns {Object|undefined} - file
*/
getFile (path) {
return this.fileCache[path];
}
/**
* Get the resolved path for the file.
*
* @param {string} path - path
* @returns {string|undefined} - resolved path
*/
getResolvedPath (path) {
if (this.hasFile(path)) {
return path;
}
}
/**
* Get the file data.
*
* @param {string} path - path
* @returns {string|undefined}
*/
getFileData (path) {
return this.hasFile(path) && this.getFile(path).data;
}
/**
* Check if the file has an error.
*
* @param {string} path - path
* @returns {boolean}
*/
hasError (path) {
return this.hasFile(path) && Boolean(this.getFile(path).error);
}
/**
* Get the file error.
*
* @param {string} path - path
* @returns {string|undefined}
*/
getFileError (path) {
return this.hasError(path) && this.getFile(path).error;
}
}
/**
* @param {FileCache} fileCache - fileCache
* @param {Object} scope - scope
* @returns {Function} - postmanRequire
* @example
* const fileCache = {
* 'path/to/file.js': {
* data: 'module.exports = { foo: "bar" };'
* }
* };
*
* const postmanRequire = createPostmanRequire(fileCache, scope);
*
* const module = postmanRequire('path/to/file.js');
* console.log(module.foo); // bar
*/
function createPostmanRequire (fileCache, scope) {
const store = new PostmanRequireStore(fileCache || {}),
cache = {};
/**
* @param {string} name - name
* @returns {any} - module
*/
function postmanRequire (name) {
const path = store.getResolvedPath(name);
if (!path) {
// Error should contain the name exactly as the user specified,
// and not the resolved path.
throw new Error(`Cannot find package '${name}'`);
}
if (store.hasError(path)) {
throw new Error(`Error while loading package '${name}': ${store.getFileError(path)}`);
}
// Any module should not be evaluated twice, so we use it from the
// cache. If there's a circular dependency, the partially evaluated
// module will be returned from the cache.
if (cache[path]) {
// Always use the resolved path as the ID of the module. This
// ensures that relative paths are handled correctly.
return cache[path].exports;
}
/* eslint-disable-next-line one-var */
const file = store.getFileData(path),
moduleObj = {
id: path,
exports: {}
};
// Add to cache before executing. This ensures that any dependency
// that tries to import it's parent/ancestor gets the cached
// version and not end up in infinite loop.
cache[moduleObj.id] = moduleObj;
/* eslint-disable-next-line one-var */
const wrappedModule = MODULE_WRAPPER[0] + file + MODULE_WRAPPER[1];
scope.import({
[MODULE_KEY]: moduleObj
});
// Note: We're executing the code in the same scope as the one
// which called the `pm.require` function. This is because we want
// to share the global scope across all the required modules. Any
// locals are available inside the required modules and any locals
// created inside the required modules are available to the parent.
//
// Why `async` = true?
// - We want to allow execution of async code like setTimeout etc.
scope.exec(wrappedModule, {
async: true,
block: isExternalPackage(name) ?
LEGACY_GLOBS.concat('pm') :
LEGACY_GLOBS
}, (err) => {
// Bubble up the error to be caught as execution error
if (err) {
throw new Error(`Error in package '${name}': ${err.message ? err.message : err}`);
}
});
scope.unset(MODULE_KEY);
return moduleObj.exports;
}
return postmanRequire;
}
module.exports = createPostmanRequire;