From ab7dd1ff26dcadb6fb471c58bdea6ac7b84a15a3 Mon Sep 17 00:00:00 2001 From: gopu-bruno Date: Tue, 5 May 2026 19:47:00 +0530 Subject: [PATCH] feat: auto-require Postman sandbox globals during import (#7878) --- .../src/utils/postman-to-bruno-translator.js | 68 ++++++++++++++++++ .../postman-library-globals.test.js | 72 +++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-library-globals.test.js diff --git a/packages/bruno-converters/src/utils/postman-to-bruno-translator.js b/packages/bruno-converters/src/utils/postman-to-bruno-translator.js index df27aea4c..90324ad12 100644 --- a/packages/bruno-converters/src/utils/postman-to-bruno-translator.js +++ b/packages/bruno-converters/src/utils/postman-to-bruno-translator.js @@ -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(); } diff --git a/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-library-globals.test.js b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-library-globals.test.js new file mode 100644 index 000000000..bc53fa933 --- /dev/null +++ b/packages/bruno-converters/tests/postman/postman-translations/transpiler-tests/postman-library-globals.test.js @@ -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); + }); +});