feat: auto-require Postman sandbox globals during import (#7878)

This commit is contained in:
gopu-bruno
2026-05-05 19:47:00 +05:30
committed by GitHub
parent d332d8e6b2
commit ab7dd1ff26
2 changed files with 140 additions and 0 deletions

View File

@@ -670,6 +670,71 @@ function processTransformations(ast, transformedNodes) {
});
}
// Postman provides these as sandbox globals. Bruno requires explicit require()
const POSTMAN_LIBRARY_GLOBALS = {
CryptoJS: 'crypto-js',
_: 'lodash',
moment: 'moment',
cheerio: 'cheerio',
tv4: 'tv4'
};
/**
* Inject require() for Postman sandbox globals (CryptoJS, _, moment, cheerio,
* tv4) used as X.foo or X(...) and not visible in any enclosing scope at the
* usage site. Requires are prepended to the program body, sorted alphabetically.
*
* @param {Collection} ast - jscodeshift AST
*/
function injectLibraryRequires(ast) {
const libraryNames = new Set(Object.keys(POSTMAN_LIBRARY_GLOBALS));
const usedLibraries = new Set();
ast.find(j.Identifier).forEach((path) => {
const name = path.value.name;
if (!libraryNames.has(name)) return;
const parent = path.parent.value;
// check for library usage: X.foo / X['foo'] / X[expr] (X is object) or X(...) (X is callee)
const isLibraryUsage
= (parent.type === 'MemberExpression' && parent.object === path.value)
|| (parent.type === 'CallExpression' && parent.callee === path.value);
if (!isLibraryUsage) return;
// skip if the name is bound in any enclosing scope at this position
if (path.scope && path.scope.lookup(name)) return;
usedLibraries.add(name);
});
if (usedLibraries.size === 0) return;
const declarations = [...usedLibraries].sort().map((name) =>
j.variableDeclaration('const', [
j.variableDeclarator(
j.identifier(name),
j.callExpression(j.identifier('require'), [j.literal(POSTMAN_LIBRARY_GLOBALS[name])])
)
])
);
// insert after directive prologue if present
const body = ast.get().value.program.body;
let insertIndex = 0;
while (insertIndex < body.length) {
const node = body[insertIndex];
const isDirective
= node.type === 'ExpressionStatement'
&& node.expression
&& node.expression.type === 'Literal'
&& typeof node.expression.value === 'string';
if (!isDirective) break;
insertIndex++;
}
body.splice(insertIndex, 0, ...declarations);
}
/**
* Translates Postman script code to Bruno script code
* @param {string} code - The Postman script code to translate
@@ -700,6 +765,9 @@ function translateCode(code) {
// Handle special Postman syntax patterns
handleTestsBracketNotation(ast);
// Inject require() for Postman sandbox globals
injectLibraryRequires(ast);
return ast.toSource();
}

View File

@@ -0,0 +1,72 @@
import translateCode from '../../../../src/utils/postman-to-bruno-translator';
describe('Postman Library Globals Auto-Require Injection', () => {
it('injects require for member-position usage (CryptoJS)', () => {
const out = translateCode(`const h = CryptoJS.MD5('hello').toString();`);
expect(out).toContain(`const CryptoJS = require("crypto-js")`);
expect(out).toContain(`CryptoJS.MD5('hello')`);
});
it('injects require for call-position usage (moment)', () => {
const out = translateCode(`const now = moment().format('YYYY');`);
expect(out).toContain(`const moment = require("moment")`);
expect(out).toContain(`moment().format('YYYY')`);
});
it('injects require for computed member access (CryptoJS["MD5"])', () => {
const out = translateCode(`const h = CryptoJS['MD5']('hello').toString();`);
expect(out).toContain(`const CryptoJS = require("crypto-js")`);
});
it('injects multiple requires in alphabetical order', () => {
const out = translateCode(`
const h = CryptoJS.MD5('x').toString();
const c = _.chunk([1,2,3], 2);
const t = moment().unix();
const v = tv4.validate({}, {});
`);
const idxCrypto = out.indexOf(`require("crypto-js")`);
const idxLodash = out.indexOf(`require("lodash")`);
const idxMoment = out.indexOf(`require("moment")`);
const idxTv4 = out.indexOf(`require("tv4")`);
expect(idxCrypto).toBeGreaterThan(-1);
expect(idxCrypto).toBeLessThan(idxLodash);
expect(idxLodash).toBeLessThan(idxMoment);
expect(idxMoment).toBeLessThan(idxTv4);
});
it('does not duplicate when require already exists', () => {
const input = `const CryptoJS = require('crypto-js'); const h = CryptoJS.MD5('x');`;
const out = translateCode(input);
const matches = out.match(/require\(["']crypto-js["']\)/g) || [];
expect(matches).toHaveLength(1);
});
it('does not inject when library name is only a property access', () => {
const out = translateCode(`obj.CryptoJS.doThing();`);
expect(out).not.toContain(`require("crypto-js")`);
});
it('does not inject on script with no library usage', () => {
const input = `const x = 1 + 2;`;
const out = translateCode(input);
expect(out).not.toContain('require(');
expect(out.trim()).toBe(input.trim());
});
it('still injects when name is shadowed only in an inner function/IIFE scope', () => {
const out = translateCode(`
(() => { const _ = 1; return _; })();
_.chunk([1, 2], 1);
`);
expect(out).toContain(`const _ = require("lodash")`);
});
it('preserves "use strict" directive prologue when injecting requires', () => {
const out = translateCode(`'use strict';\nmoment().format('YYYY');`);
const directiveIdx = out.indexOf('\'use strict\'');
const requireIdx = out.indexOf(`require("moment")`);
expect(directiveIdx).toBeGreaterThanOrEqual(0);
expect(requireIdx).toBeGreaterThan(directiveIdx);
});
});