Skip to content

Commit b6ca17e

Browse files
authored
Implement pm.require (#976)
1 parent 903677b commit b6ca17e

File tree

11 files changed

+812
-18
lines changed

11 files changed

+812
-18
lines changed

CHANGELOG.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
unreleased:
2+
new features:
3+
- GH-976 Add `pm.require` API to use packages inside scripts
4+
15
4.4.0:
26
date: 2023-11-18
37
new features:

lib/postman-sandbox.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ class PostmanSandbox extends UniversalVM {
9393
executionEventName = 'execution.result.' + id,
9494
executionTimeout = _.get(options, 'timeout', this.executionTimeout),
9595
cursor = _.clone(_.get(options, 'cursor', {})), // clone the cursor as it travels through IPC for mutation
96+
resolvedPackages = _.get(options, 'resolvedPackages'),
9697
debugMode = _.has(options, 'debug') ? options.debug : this.debug;
9798

9899
let waiting;
@@ -126,6 +127,7 @@ class PostmanSandbox extends UniversalVM {
126127
cursor: cursor,
127128
debug: debugMode,
128129
timeout: executionTimeout,
130+
resolvedPackages: resolvedPackages,
129131
legacy: _.get(options, 'legacy')
130132
});
131133
}

lib/sandbox/execute.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const _ = require('lodash'),
99
PostmanTimers = require('./timers'),
1010
PostmanAPI = require('./pmapi'),
1111
PostmanCookieStore = require('./cookie-store'),
12+
createPostmanRequire = require('./pm-require'),
1213

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

124+
disabledAPIs = [
125+
...(initializationOptions.disabledAPIs || [])
126+
],
127+
123128
/**
124129
* Dispatch assertions from `pm.test` or legacy `test` API.
125130
*
@@ -205,6 +210,10 @@ module.exports = function (bridge, glob) {
205210
timers.clearEvent(id, err, res);
206211
});
207212

213+
if (!options.resolvedPackages) {
214+
disabledAPIs.push('require');
215+
}
216+
208217
// send control to the function that executes the context and prepares the scope
209218
executeContext(scope, code, execution,
210219
// if a console is sent, we use it. otherwise this also prevents erroneous referencing to any console
@@ -228,9 +237,8 @@ module.exports = function (bridge, glob) {
228237
},
229238
dispatchAssertions,
230239
new PostmanCookieStore(id, bridge, timers),
231-
{
232-
disabledAPIs: initializationOptions.disabledAPIs
233-
})
240+
createPostmanRequire(options.resolvedPackages, scope),
241+
{ disabledAPIs })
234242
),
235243
dispatchAssertions,
236244
{ disableLegacyAPIs: initializationOptions.disableLegacyAPIs });

lib/sandbox/pm-require.js

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
const MODULE_KEY = '__module_obj', // why not use `module`?
2+
MODULE_WRAPPER = [
3+
'(function (exports, module) {\n',
4+
`\n})(${MODULE_KEY}.exports, ${MODULE_KEY});`
5+
];
6+
7+
/**
8+
* Cache of all files that are available to be required.
9+
*
10+
* @typedef {Object.<string, { data: string } | { error: string }>} FileCache
11+
*/
12+
13+
class PostmanRequireStore {
14+
/**
15+
* @param {FileCache} fileCache - fileCache
16+
*/
17+
constructor (fileCache) {
18+
if (!fileCache) {
19+
throw new Error('File cache is required');
20+
}
21+
22+
this.fileCache = fileCache;
23+
}
24+
25+
/**
26+
* Check if the file is available in the cache.
27+
*
28+
* @param {string} path - path
29+
* @returns {boolean}
30+
*/
31+
hasFile (path) {
32+
return Boolean(this.getFile(path));
33+
}
34+
35+
/**
36+
* Get the file from the cache.
37+
*
38+
* @param {string} path - path
39+
* @returns {Object|undefined} - file
40+
*/
41+
getFile (path) {
42+
return this.fileCache[path];
43+
}
44+
45+
/**
46+
* Get the resolved path for the file.
47+
*
48+
* @param {string} path - path
49+
* @returns {string|undefined} - resolved path
50+
*/
51+
getResolvedPath (path) {
52+
if (this.hasFile(path)) {
53+
return path;
54+
}
55+
}
56+
57+
/**
58+
* Get the file data.
59+
*
60+
* @param {string} path - path
61+
* @returns {string|undefined}
62+
*/
63+
getFileData (path) {
64+
return this.hasFile(path) && this.getFile(path).data;
65+
}
66+
67+
/**
68+
* Check if the file has an error.
69+
*
70+
* @param {string} path - path
71+
* @returns {boolean}
72+
*/
73+
hasError (path) {
74+
return this.hasFile(path) && Boolean(this.getFile(path).error);
75+
}
76+
77+
/**
78+
* Get the file error.
79+
*
80+
* @param {string} path - path
81+
* @returns {string|undefined}
82+
*/
83+
getFileError (path) {
84+
return this.hasError(path) && this.getFile(path).error;
85+
}
86+
}
87+
88+
/**
89+
* @param {FileCache} fileCache - fileCache
90+
* @param {Object} scope - scope
91+
* @returns {Function} - postmanRequire
92+
* @example
93+
* const fileCache = {
94+
* 'path/to/file.js': {
95+
* data: 'module.exports = { foo: "bar" };'
96+
* }
97+
* };
98+
*
99+
* const postmanRequire = createPostmanRequire(fileCache, scope);
100+
*
101+
* const module = postmanRequire('path/to/file.js');
102+
* console.log(module.foo); // bar
103+
*/
104+
function createPostmanRequire (fileCache, scope) {
105+
const store = new PostmanRequireStore(fileCache || {}),
106+
cache = {};
107+
108+
/**
109+
* @param {string} name - name
110+
* @returns {any} - module
111+
*/
112+
function postmanRequire (name) {
113+
const path = store.getResolvedPath(name);
114+
115+
if (!path) {
116+
// Error should contain the name exactly as the user specified,
117+
// and not the resolved path.
118+
throw new Error(`Cannot find module '${name}'`);
119+
}
120+
121+
if (store.hasError(path)) {
122+
throw new Error(`Error while loading module '${name}': ${store.getFileError(path)}`);
123+
}
124+
125+
// Any module should not be evaluated twice, so we use it from the
126+
// cache. If there's a circular dependency, the partially evaluated
127+
// module will be returned from the cache.
128+
if (cache[path]) {
129+
// Always use the resolved path as the ID of the module. This
130+
// ensures that relative paths are handled correctly.
131+
return cache[path].exports;
132+
}
133+
134+
/* eslint-disable-next-line one-var */
135+
const file = store.getFileData(path),
136+
moduleObj = {
137+
id: path,
138+
exports: {}
139+
};
140+
141+
// Add to cache before executing. This ensures that any dependency
142+
// that tries to import it's parent/ancestor gets the cached
143+
// version and not end up in infinite loop.
144+
cache[moduleObj.id] = moduleObj;
145+
146+
/* eslint-disable-next-line one-var */
147+
const wrappedModule = MODULE_WRAPPER[0] + file + MODULE_WRAPPER[1];
148+
149+
scope.import({
150+
[MODULE_KEY]: moduleObj
151+
});
152+
153+
// Note: We're executing the code in the same scope as the one
154+
// which called the `pm.require` function. This is because we want
155+
// to share the global scope across all the required modules. Any
156+
// locals are available inside the required modules and any locals
157+
// created inside the required modules are available to the parent.
158+
//
159+
// Why `async` = true?
160+
// - We want to allow execution of async code like setTimeout etc.
161+
scope.exec(wrappedModule, true, (err) => {
162+
// Bubble up the error to be caught as execution error
163+
if (err) {
164+
throw err;
165+
}
166+
});
167+
168+
scope.unset(MODULE_KEY);
169+
170+
return moduleObj.exports;
171+
}
172+
173+
return postmanRequire;
174+
}
175+
176+
module.exports = createPostmanRequire;

lib/sandbox/pmapi.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,11 @@ const _ = require('lodash'),
4747
* @param {Function} onSkipRequest - callback to execute when pm.execution.skipRequest() called
4848
* @param {Function} onAssertion - callback to execute when pm.expect() called
4949
* @param {Object} cookieStore - cookie store
50+
* @param {Function} requireFn - requireFn
5051
* @param {Object} [options] - options
5152
* @param {Array.<String>} [options.disabledAPIs] - list of disabled APIs
5253
*/
53-
function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, options = {}) {
54+
function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore, requireFn, options = {}) {
5455
// @todo - ensure runtime passes data in a scope format
5556
let iterationData = new VariableScope();
5657

@@ -291,6 +292,16 @@ function Postman (execution, onRequest, onSkipRequest, onAssertion, cookieStore,
291292
*/
292293
current: execution.legacy._eventItemName
293294
})
295+
},
296+
297+
/**
298+
* Imports a package in the script.
299+
*
300+
* @param {String} name - name of the module
301+
* @returns {any} - exports from the module
302+
*/
303+
require: function (name) {
304+
return requireFn(name);
294305
}
295306
}, options.disabledAPIs);
296307

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
},
4444
"dependencies": {
4545
"lodash": "4.17.21",
46-
"postman-collection": "4.3.0",
46+
"postman-collection": "4.4.0",
4747
"teleport-javascript": "1.0.0",
4848
"uvm": "2.1.1"
4949
},
@@ -92,7 +92,7 @@
9292
"terser": "^5.24.0",
9393
"tsd-jsdoc": "^2.5.0",
9494
"tv4": "1.3.0",
95-
"uniscope": "2.0.1",
95+
"uniscope": "2.1.0",
9696
"watchify": "^4.0.0",
9797
"xml2js": "0.4.23"
9898
},

0 commit comments

Comments
 (0)