UNPKG

@usebruno/converters

Version:

The converters package is responsible for converting collections from one format to a Bruno collection. It can be used as a standalone package or as a part of the Bruno framework.

707 lines (624 loc) 23.3 kB
const j = require('jscodeshift'); const cloneDeep = require('lodash/cloneDeep'); /** * Efficiently builds a string representation of a member expression without using toSource() * * @param {Object} node - The member expression node from the AST * @return {string} - String representation of the member expression (e.g., "pm.environment.get") */ function getMemberExpressionString(node) { // Handle base case: if this is an Identifier if (node.type === 'Identifier') { return node.name; } // Handle member expressions if (node.type === 'MemberExpression') { const objectStr = getMemberExpressionString(node.object); // For computed properties like obj[prop], we need special handling if (node.computed) { // For literals like obj["prop"], we can include them in the string if (node.property.type === 'Literal' && typeof node.property.value === 'string') { return `${objectStr}.${node.property.value}`; } // For other computed properties, we can't reliably represent them as a simple string return `${objectStr}.[computed]`; } // For regular property access like obj.prop if (node.property.type === 'Identifier') { return `${objectStr}.${node.property.name}`; } } return '[unsupported]'; } // Simple 1:1 translations for straightforward replacements const simpleTranslations = { // Global Variables 'pm.globals.get': 'bru.getGlobalEnvVar', 'pm.globals.set': 'bru.setGlobalEnvVar', // Environment variables 'pm.environment.get': 'bru.getEnvVar', 'pm.environment.set': 'bru.setEnvVar', 'pm.environment.name': 'bru.getEnvName()', 'pm.environment.unset': 'bru.deleteEnvVar', // Variables 'pm.variables.get': 'bru.getVar', 'pm.variables.set': 'bru.setVar', 'pm.variables.has': 'bru.hasVar', // Collection variables 'pm.collectionVariables.get': 'bru.getVar', 'pm.collectionVariables.set': 'bru.setVar', 'pm.collectionVariables.has': 'bru.hasVar', 'pm.collectionVariables.unset': 'bru.deleteVar', // Request flow control 'pm.setNextRequest': 'bru.setNextRequest', // Testing 'pm.test': 'test', 'pm.expect': 'expect', 'pm.expect.fail': 'expect.fail', // Request properties 'pm.request.url': 'req.getUrl()', 'pm.request.method': 'req.getMethod()', 'pm.request.headers': 'req.getHeaders()', 'pm.request.body': 'req.getBody()', // Response properties 'pm.response.json': 'res.getBody', 'pm.response.code': 'res.getStatus()', 'pm.response.status': 'res.statusText', 'pm.response.responseTime': 'res.getResponseTime()', 'pm.response.statusText': 'res.statusText', 'pm.response.headers': 'res.getHeaders()', // Execution control 'pm.execution.skipRequest': 'bru.runner.skipRequest', // Legacy Postman API (deprecated) (we can use pm instead of postman, as we are converting all postman references to pm in the code as the part of pre-processing) 'pm.setEnvironmentVariable': 'bru.setEnvVar', 'pm.getEnvironmentVariable': 'bru.getEnvVar', 'pm.clearEnvironmentVariable': 'bru.deleteEnvVar', }; /* Complex transformations that need custom handling * Note: Transform functions can return either a single node or an array of nodes. * When returning an array of nodes, each node in the array will be inserted * as a separate statement, which allows a single Postman expression to be * transformed into multiple Bruno statements (e.g. for complex assertions). */ const complexTransformations = [ // pm.environment.has requires special handling { pattern: 'pm.environment.has', transform: (path, j) => { const callExpr = path.parent.value; const args = callExpr.arguments; // Create: bru.getEnvVar(arg) !== undefined && bru.getEnvVar(arg) !== null return j.logicalExpression( '&&', j.binaryExpression( '!==', j.callExpression(j.identifier('bru.getEnvVar'), args), j.identifier('undefined') ), j.binaryExpression( '!==', j.callExpression(j.identifier('bru.getEnvVar'), args), j.identifier('null') ) ); } }, { pattern: 'pm.response.text', transform: (_, j) => { return j.callExpression(j.identifier('JSON.stringify'), [j.identifier('res.getBody()')]); } }, { pattern: 'pm.response.headers.get', transform: (path, j) => { return j.callExpression(j.identifier('res.getHeader'), path.parent.value.arguments); } }, // Handle pm.response.to.have.status { pattern: 'pm.response.to.have.status', transform: (path, j) => { const callExpr = path.parent.value; const args = callExpr.arguments; // Create: expect(res.getStatus()).to.equal(arg) return j.callExpression( j.memberExpression( j.callExpression( j.identifier('expect'), [ j.callExpression( j.identifier('res.getStatus'), [] ) ] ), j.identifier('to.equal') ), args ); } }, // handle 'pm.response.to.have.header' to expect(res.getHeaders()).to.have.property(args) { pattern: 'pm.response.to.have.header', transform: (path, j) => { const callExpr = path.parent.value; const args = callExpr.arguments; if (args.length > 0) { // Apply toLowerCase() to the first argument args[0] = j.callExpression( j.memberExpression( args[0], j.identifier('toLowerCase') ), [] ); } // Create: expect(res.getHeaders()).to.have.property(args) return j.callExpression( j.memberExpression( j.callExpression( j.identifier('expect'), [ j.callExpression( j.identifier('res.getHeaders'), [] ) ] ), j.identifier('to.have.property') ), args ); } }, // handle pm.response.to.have.body to expect(res.getBody()).to.equal(arg) { pattern: 'pm.response.to.have.body', transform: (path, j) => { const callExpr = path.parent.value; const args = callExpr.arguments; return j.callExpression( j.memberExpression( j.callExpression(j.identifier('expect'), [j.identifier('res.getBody()')]), j.identifier('to.equal') ), args ); } }, // Handle pm.execution.setNextRequest(null) { pattern: 'pm.execution.setNextRequest', transform: (path, j) => { const callExpr = path.parent.value; const args = callExpr.arguments; // If argument is null or 'null', transform to bru.runner.stopExecution() if ( args[0].type === 'Literal' && (args[0].value === null || args[0].value === 'null') ) { return j.callExpression( j.identifier('bru.runner.stopExecution'), [] ); } // Otherwise, keep as bru.runner.setNextRequest with the same argument return j.callExpression( j.identifier('bru.runner.setNextRequest'), args ); } }, ]; // Create a map for complex transformations to enable O(1) lookups const complexTransformationsMap = {}; complexTransformations.forEach(transform => { complexTransformationsMap[transform.pattern] = transform; }); const varInitsToReplace = new Set(['pm', 'postman', 'pm.request','pm.response', 'pm.test', 'pm.expect', 'pm.environment', 'pm.variables', 'pm.collectionVariables', 'pm.execution', 'pm.globals']); /** * Process all transformations (both simple and complex) in the AST in a single pass * @param {Object} ast - jscodeshift AST * @param {Set} transformedNodes - Set of already transformed nodes */ function processTransformations(ast, transformedNodes) { ast.find(j.MemberExpression).forEach(path => { if (transformedNodes.has(path.node)) return; // Get string representation using our utility function const memberExprStr = getMemberExpressionString(path.value); // First check for simple transformations (O(1)) if (simpleTranslations.hasOwnProperty(memberExprStr)) { const replacement = simpleTranslations[memberExprStr]; j(path).replaceWith(j.identifier(replacement)); transformedNodes.add(path.node); return; // Skip complex transformation check if simple transformation applied } // Then check for complex transformations (O(1)) if (complexTransformationsMap.hasOwnProperty(memberExprStr) && path.parent.value.type === 'CallExpression') { const transform = complexTransformationsMap[memberExprStr]; const replacement = transform.transform(path, j); if (Array.isArray(replacement)) { replacement.forEach((nodePath, index) => { if(index === 0) { j(path.parent).replaceWith(nodePath); } else { j(path.parent.parent).insertAfter(nodePath); } transformedNodes.add(nodePath.node); transformedNodes.add(path.parent.node); }); } else { j(path.parent).replaceWith(replacement); transformedNodes.add(path.node); transformedNodes.add(path.parent.node); } } }); } /** * Translates Postman script code to Bruno script code * @param {string} code - The Postman script code to translate * @returns {string} The translated Bruno script code */ function translateCode(code) { // Replace 'postman' with 'pm' using regex before creating the AST // This is more efficient than an AST traversal code = code.replace(/\bpostman\b/g, 'pm'); const ast = j(code); // Keep track of transformed nodes to avoid double-processing const transformedNodes = new Set(); // Preprocess the code to resolve all aliases preprocessAliases(ast); // Process all transformations in a single pass processTransformations(ast, transformedNodes); // Handle special Postman syntax patterns handleTestsBracketNotation(ast); return ast.toSource(); } /** * Preprocess all variable aliases in the AST to simplify later transformations * @param {Object} ast - jscodeshift AST */ function preprocessAliases(ast) { // Create a symbol table to track what each variable references const symbolTable = new Map(); const MAX_ITERATIONS = 5; let iterations = 0; // Keep preprocessing until no more changes can be made let changesMade; do { changesMade = false; // First pass: Identify all variables findVariableDefinitions(ast, symbolTable); // Second pass: Replace all variable references with their resolved values changesMade = resolveVariableReferences(ast, symbolTable) || false; // Third pass: Clean up variable declarations that are no longer needed changesMade = removeResolvedDeclarations(ast, symbolTable) || false; iterations++; } while (changesMade && iterations < MAX_ITERATIONS); } /** * Find all variable definitions and track what they reference * @param {Object} ast - jscodeshift AST * @param {Map} symbolTable - Map to track variable references */ function findVariableDefinitions(ast, symbolTable) { // Use a single traversal to handle both direct assignments and object destructuring ast.find(j.VariableDeclarator).forEach(path => { // Only process nodes that have an initializer if (!path.value.init) return; // Handle direct assignments: const response = pm.response if (path.value.id.type === 'Identifier') { const varName = path.value.id.name; // If it's a direct identifier, just map it if (path.value.init.type === 'Identifier') { symbolTable.set(varName, { type: 'identifier', value: path.value.init.name }); } // If it's a member expression, store both parts else if (path.value.init.type === 'MemberExpression') { const sourceCode = getMemberExpressionString(path.value.init); symbolTable.set(varName, { type: 'memberExpression', value: sourceCode, node: path.value.init }); } } // Handle object destructuring: const { response } = pm else if (path.value.id.type === 'ObjectPattern' && path.value.init.type === 'Identifier') { const source = path.value.init.name; path.value.id.properties.forEach(prop => { if (prop.key.name && prop.value.type === 'Identifier') { const destVarName = prop.value.name; symbolTable.set(destVarName, { type: 'memberExpression', value: `${source}.${prop.key.name}`, node: j.memberExpression( j.identifier(source), j.identifier(prop.key.name) ) }); } }); } }); } /** * Resolve variable references by replacing them with their original values * @param {Object} ast - jscodeshift AST * @param {Map} symbolTable - Map of variable references * @returns {boolean} Whether any changes were made */ function resolveVariableReferences(ast, symbolTable) { let changesMade = false; /** * Example of what this function does: * * Input Postman code: * const response = pm.response; * const jsonData = response.json(); // response is a reference to pm.response * * After resolution: * const response = pm.response; * const jsonData = pm.response.json(); // response reference is replaced with pm.response * * Then in the next preprocessing phase, unnecessary variables like 'response' will be removed. */ // Replace all identifier references with their resolved values ast.find(j.Identifier).forEach(path => { const varName = path.value.name; /** * Skip specific types of identifiers that shouldn't be replaced: * * Case 1: Variable definitions (left side of declarations) * ----------------------------------------------------- * In code like: * const response = pm.response; * ^ * We shouldn't replace 'response' on the left side with pm.response, * which would result in: const pm.response = pm.response; (invalid syntax) * * Case 2: Property names in member expressions * ----------------------------------------------------- * In code like: * console.log(response.status); * ^ * We shouldn't replace the 'status' property name with anything, * only the 'response' object reference should be replaced. * * We only want to replace identifiers that are being used as references, * not the ones being defined or used as property names. */ // Skip if this is a variable definition or property name if (path.parent.value.type === 'VariableDeclarator' && path.parent.value.id === path.value) { return; } if (path.parent.value.type === 'MemberExpression' && path.parent.value.property === path.value && !path.parent.value.computed) { return; } // Only replace if this is a known variable if (!symbolTable.has(varName)) return; const symbolInfo = symbolTable.get(varName); if(!varInitsToReplace.has(symbolInfo.value)) { return; } const newNode = cloneDeep(symbolInfo.node); j(path).replaceWith(newNode); symbolTable.set(varName, { type: 'memberExpression', value: symbolInfo.value, node: newNode }); changesMade = true; }); return changesMade; } /** * Remove variable declarations that have been resolved * @param {Object} ast - jscodeshift AST * @param {Map} symbolTable - Map of variable references * @returns {boolean} Whether any changes were made */ function removeResolvedDeclarations(ast, symbolTable) { let changesMade = false; /** * Example of what this function does: * * Original Postman code: * const response = pm.response; * const jsonData = response.json(); * console.log(jsonData.name); * * After variable resolution: * const response = pm.response; // This declaration is now redundant * const jsonData = pm.response.json(); // This value has been resolved * console.log(jsonData.name); // This still references jsonData * * Final code after this cleanup step: * const jsonData = pm.response.json(); // response variable declaration is removed * console.log(jsonData.name); // jsonData is kept since it's still referenced * * We only remove declarations that: * 1. Have been fully resolved (references to pm.* objects) * 2. No longer provide any value (since all references were replaced with resolved values) */ // Use a single traversal to handle both regular variable declarations and destructuring ast.find(j.VariableDeclarator).forEach(path => { // Case 1: Handle regular variable declarations if (path.value.id.type === 'Identifier') { const varName = path.value.id.name; const replacement = symbolTable.get(varName); if(!replacement || !varInitsToReplace.has(replacement.value)) return; /** * This code differentiates between two types of variable declarations: * * Example 1: Single variable declaration * ----------------------------------- * Input: const response = pm.response; * Action: The entire statement can be removed * Output: [statement removed] * * Example 2: Multiple variables in one declaration * ----------------------------------- * Input: const response = pm.response, unrelated = 5; * Action: Only remove the 'response' declarator, keep the others * Output: const unrelated = 5; * * We need this distinction to ensure we don't accidentally remove * unrelated variables that happen to be declared in the same statement. */ const declarationPath = j(path).closest(j.VariableDeclaration); if (declarationPath.get().value.declarations.length === 1) { declarationPath.remove(); } else { // Otherwise just remove this declarator j(path).remove(); } changesMade = true; } // Case 2: Handle destructuring of pm else if (path.value.id.type === 'ObjectPattern' && path.value.init && path.value.init.type === 'Identifier' && path.value.init.name === 'pm') { /** * Example of destructuring removal: * * Original Postman code: * const { response, environment } = pm; * console.log(response.json().name); * console.log(environment.get("variable")); * * After variable resolution steps: * const { response, environment } = pm; // This destructuring is now redundant * console.log(pm.response.json().name); // 'response' references already replaced with pm.response * console.log(pm.environment.get("variable")); // 'environment' references replaced * * Final code after this cleanup step: * console.log(pm.response.json().name); // Destructuring declaration is completely removed * console.log(pm.environment.get("variable")); * * This step specifically targets the Postman pattern of destructuring the pm object, * which is common in Postman scripts but needs to be removed in the Bruno conversion. */ const declarationPath = j(path).closest(j.VariableDeclaration); if (declarationPath.get().value.declarations.length === 1) { declarationPath.remove(); } else { j(path).remove(); } changesMade = true; } }); return changesMade; } /** * Handle Postman's tests["..."] = ... syntax * @param {Object} ast - jscodeshift AST */ function handleTestsBracketNotation(ast) { // Find the ExpressionStatement that contains the assignment ast.find(j.ExpressionStatement, { expression: { type: 'AssignmentExpression', left: { type: 'MemberExpression', object: { name: 'tests' }, computed: true, property: {} // Accept any property type } } }).forEach(path => { // Get the assignment expression const assignment = path.value.expression; const left = assignment.left; // Verify it's a valid tests[] expression if (left.object.type === 'Identifier' && left.object.name === 'tests' && left.computed === true) { const property = left.property; const rightSide = assignment.right; // Handle string literals if (property.type === 'Literal' && typeof property.value === 'string') { const testName = property.value; // Replace with test() function call j(path).replaceWith( j.expressionStatement( j.callExpression( j.identifier('test'), [ j.literal(testName), j.functionExpression( null, [], j.blockStatement([ j.expressionStatement( j.memberExpression( j.callExpression( j.identifier('expect'), [ j.callExpression( j.identifier('Boolean'), [rightSide] ) ] ), j.identifier('to.be.true') ) ) ]) ) ] ) ) ); } // Handle template literals else if (property.type === 'TemplateLiteral') { // Create a template literal with the same quasi and expressions const templateLiteral = j.templateLiteral( property.quasis, property.expressions ); // Replace with test() function call using template literal j(path).replaceWith( j.expressionStatement( j.callExpression( j.identifier('test'), [ templateLiteral, j.functionExpression( null, [], j.blockStatement([ j.expressionStatement( j.memberExpression( j.callExpression( j.identifier('expect'), [ j.callExpression( j.identifier('Boolean'), [rightSide] ) ] ), j.identifier('to.be.true') ) ) ]) ) ] ) ) ); } } }); } export { getMemberExpressionString }; export default translateCode;