Skip to content

Commit

Permalink
Merge branch 'release/4.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
codenirvana committed Feb 28, 2024
2 parents d8d4062 + 4393b9d commit cb96131
Show file tree
Hide file tree
Showing 11 changed files with 815 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
4.5.0:
date: 2024-02-28
new features:
- GH-976 Add `pm.require` API to use packages inside scripts

4.4.0:
date: 2023-11-18
new features:
Expand Down
2 changes: 2 additions & 0 deletions lib/postman-sandbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ class PostmanSandbox extends UniversalVM {
executionEventName = 'execution.result.' + id,
executionTimeout = _.get(options, 'timeout', this.executionTimeout),
cursor = _.clone(_.get(options, 'cursor', {})), // clone the cursor as it travels through IPC for mutation
resolvedPackages = _.get(options, 'resolvedPackages'),
debugMode = _.has(options, 'debug') ? options.debug : this.debug;

let waiting;
Expand Down Expand Up @@ -126,6 +127,7 @@ class PostmanSandbox extends UniversalVM {
cursor: cursor,
debug: debugMode,
timeout: executionTimeout,
resolvedPackages: resolvedPackages,
legacy: _.get(options, 'legacy')
});
}
Expand Down
14 changes: 11 additions & 3 deletions lib/sandbox/execute.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const _ = require('lodash'),
PostmanTimers = require('./timers'),
PostmanAPI = require('./pmapi'),
PostmanCookieStore = require('./cookie-store'),
createPostmanRequire = require('./pm-require'),

EXECUTION_RESULT_EVENT_BASE = 'execution.result.',
EXECUTION_REQUEST_EVENT_BASE = 'execution.request.',
Expand Down Expand Up @@ -120,6 +121,10 @@ module.exports = function (bridge, glob) {
// create the execution object
execution = new Execution(id, event, context, { ...options, initializeExecution }),

disabledAPIs = [
...(initializationOptions.disabledAPIs || [])
],

/**
* Dispatch assertions from `pm.test` or legacy `test` API.
*
Expand Down Expand Up @@ -205,6 +210,10 @@ module.exports = function (bridge, glob) {
timers.clearEvent(id, err, res);
});

if (!options.resolvedPackages) {
disabledAPIs.push('require');
}

// send control to the function that executes the context and prepares the scope
executeContext(scope, code, execution,
// if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console
Expand All @@ -228,9 +237,8 @@ module.exports = function (bridge, glob) {
},
dispatchAssertions,
new PostmanCookieStore(id, bridge, timers),
{
disabledAPIs: initializationOptions.disabledAPIs
})
createPostmanRequire(options.resolvedPackages, scope),
{ disabledAPIs })
),
dispatchAssertions,
{ disableLegacyAPIs: initializationOptions.disableLegacyAPIs });
Expand Down
176 changes: 176 additions & 0 deletions lib/sandbox/pm-require.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const MODULE_KEY = '__module_obj', // why not use `module`?
MODULE_WRAPPER = [
'(function (exports, module) {\n',
`\n})(${MODULE_KEY}.exports, ${MODULE_KEY});`
];

/**
* 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 module '${name}'`);
}

if (store.hasError(path)) {
throw new Error(`Error while loading module '${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, true, (err) => {
// Bubble up the error to be caught as execution error
if (err) {
throw err;
}
});

scope.unset(MODULE_KEY);

return moduleObj.exports;
}

return postmanRequire;
}

module.exports = createPostmanRequire;
13 changes: 12 additions & 1 deletion lib/sandbox/pmapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,11 @@ const _ = require('lodash'),
* @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called
* @param {Function} onAssertion - callback to execute when pm.expect() called
* @param {Object} cookieStore - cookie store
* @param {Function} requireFn - requireFn
* @param {Object} [options] - options
* @param {Array.<String>} [options.disabledAPIs] - list of disabled APIs
*/
function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) {
function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, requireFn, options = {}) {
// @todo - ensure runtime passes data in a scope format
let iterationData = new VariableScope();

Expand Down Expand Up @@ -291,6 +292,16 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore,
*/
current: execution.legacy._eventItemName
})
},

/**
* Imports a package in the script.
*
* @param {String} name - name of the module
* @returns {any} - exports from the module
*/
require: function (name) {
return requireFn(name);
}
}, options.disabledAPIs);

Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "postman-sandbox",
"version": "4.4.0",
"version": "4.5.0",
"description": "Sandbox for Postman Scripts to run in Node.js or browser",
"author": "Postman Inc.",
"license": "Apache-2.0",
Expand Down Expand Up @@ -43,7 +43,7 @@
},
"dependencies": {
"lodash": "4.17.21",
"postman-collection": "4.3.0",
"postman-collection": "4.4.0",
"teleport-javascript": "1.0.0",
"uvm": "2.1.1"
},
Expand Down Expand Up @@ -92,7 +92,7 @@
"terser": "^5.24.0",
"tsd-jsdoc": "^2.5.0",
"tv4": "1.3.0",
"uniscope": "2.0.1",
"uniscope": "2.1.0",
"watchify": "^4.0.0",
"xml2js": "0.4.23"
},
Expand Down
Loading

0 comments on commit cb96131

Please sign in to comment.