diff --git a/packages/bruno-cli/src/commands/run.js b/packages/bruno-cli/src/commands/run.js index 58a4386ce..039932c58 100644 --- a/packages/bruno-cli/src/commands/run.js +++ b/packages/bruno-cli/src/commands/run.js @@ -33,7 +33,7 @@ const printRunSummary = (assertionResults, testResults) => { } testSummary += `, ${totalAssertions} total`; - console.log("\n" + chalk.bold(assertSummary)); + console.log('\n' + chalk.bold(assertSummary)); console.log(chalk.bold(testSummary)); return { @@ -43,7 +43,7 @@ const printRunSummary = (assertionResults, testResults) => { totalTests, passedTests, failedTests - } + }; }; const getBruFilesRecursively = (dir) => { @@ -51,33 +51,34 @@ const getBruFilesRecursively = (dir) => { const getFilesInOrder = (dir) => { let bruJsons = []; - + const traverse = (currentPath) => { const filesInCurrentDir = fs.readdirSync(currentPath); if (currentPath.includes('node_modules')) { return; } - + for (const file of filesInCurrentDir) { const filePath = path.join(currentPath, file); const stats = fs.lstatSync(filePath); - + // todo: we might need a ignore config inside bruno.json - if (stats.isDirectory() && + if ( + stats.isDirectory() && filePath !== environmentsPath && - !filePath.startsWith(".git") && - !filePath.startsWith("node_modules") + !filePath.startsWith('.git') && + !filePath.startsWith('node_modules') ) { traverse(filePath); } } - + const currentDirBruJsons = []; for (const file of filesInCurrentDir) { const filePath = path.join(currentPath, file); const stats = fs.lstatSync(filePath); - + if (!stats.isDirectory() && path.extname(filePath) === '.bru') { const bruContent = fs.readFileSync(filePath, 'utf8'); const bruJson = bruToJson(bruContent); @@ -97,7 +98,7 @@ const getBruFilesRecursively = (dir) => { bruJsons = bruJsons.concat(currentDirBruJsons); }; - + traverse(dir); return bruJsons; }; @@ -119,7 +120,7 @@ const builder = async (yargs) => { }) .option('env', { describe: 'Environment variables', - type: 'string', + type: 'string' }) .option('insecure', { type: 'boolean', @@ -128,18 +129,12 @@ const builder = async (yargs) => { .example('$0 run request.bru', 'Run a request') .example('$0 run request.bru --env local', 'Run a request with the environment set to local') .example('$0 run folder', 'Run all requests in a folder') - .example('$0 run folder -r', 'Run all requests in a folder recursively') + .example('$0 run folder -r', 'Run all requests in a folder recursively'); }; const handler = async function (argv) { try { - let { - filename, - cacert, - env, - insecure, - r: recursive - } = argv; + let { filename, cacert, env, insecure, r: recursive } = argv; const collectionPath = process.cwd(); // todo @@ -147,30 +142,30 @@ const handler = async function (argv) { // will add support in the future to run it from anywhere inside the collection const brunoJsonPath = path.join(collectionPath, 'bruno.json'); const brunoJsonExists = await exists(brunoJsonPath); - if(!brunoJsonExists) { + if (!brunoJsonExists) { console.error(chalk.red(`You can run only at the root of a collection`)); return; } - if(filename && filename.length) { + if (filename && filename.length) { const pathExists = await exists(filename); - if(!pathExists) { + if (!pathExists) { console.error(chalk.red(`File or directory ${filename} does not exist`)); return; } } else { - filename = "./"; + filename = './'; recursive = true; } const collectionVariables = {}; let envVars = {}; - if(env) { + if (env) { const envFile = path.join(collectionPath, 'environments', `${env}.bru`); const envPathExists = await exists(envFile); - if(!envPathExists) { + if (!envPathExists) { console.error(chalk.red(`Environment file not found: `) + chalk.dim(`environments/${env}.bru`)); return; } @@ -181,41 +176,36 @@ const handler = async function (argv) { } const options = getOptions(); - if(insecure) { - options['insecure'] = true + if (insecure) { + options['insecure'] = true; } - if(cacert && cacert.length) { - if(insecure) { + if (cacert && cacert.length) { + if (insecure) { console.error(chalk.red(`Ignoring the cacert option since insecure connections are enabled`)); - } - else { + } else { const pathExists = await exists(cacert); - if(pathExists) { - options['cacert'] = cacert - } - else { + if (pathExists) { + options['cacert'] = cacert; + } else { console.error(chalk.red(`Cacert File ${cacert} does not exist`)); } } } const _isFile = await isFile(filename); - if(_isFile) { + if (_isFile) { console.log(chalk.yellow('Running Request \n')); const bruContent = fs.readFileSync(filename, 'utf8'); const bruJson = bruToJson(bruContent); const result = await runSingleRequest(filename, bruJson, collectionPath, collectionVariables, envVars); - if(result) { - const { - assertionResults, - testResults - } = result; + if (result) { + const { assertionResults, testResults } = result; const summary = printRunSummary(assertionResults, testResults); console.log(chalk.dim(chalk.grey('Done.'))); - if(summary.failedAssertions > 0 || summary.failedTests > 0) { + if (summary.failedAssertions > 0 || summary.failedTests > 0) { process.exit(1); } } else { @@ -224,15 +214,15 @@ const handler = async function (argv) { } const _isDirectory = await isDirectory(filename); - if(_isDirectory) { + if (_isDirectory) { let bruJsons = []; - if(!recursive) { + if (!recursive) { console.log(chalk.yellow('Running Folder \n')); const files = fs.readdirSync(filename); const bruFiles = files.filter((file) => file.endsWith('.bru')); for (const bruFile of bruFiles) { - const bruFilepath = path.join(filename, bruFile) + const bruFilepath = path.join(filename, bruFile); const bruContent = fs.readFileSync(bruFilepath, 'utf8'); const bruJson = bruToJson(bruContent); bruJsons.push({ @@ -257,18 +247,12 @@ const handler = async function (argv) { let testResults = []; for (const iter of bruJsons) { - const { - bruFilepath, - bruJson - } = iter; + const { bruFilepath, bruJson } = iter; const result = await runSingleRequest(bruFilepath, bruJson, collectionPath, collectionVariables, envVars); - if(result) { - const { - assertionResults: _assertionResults, - testResults: _testResults - } = result; - + if (result) { + const { assertionResults: _assertionResults, testResults: _testResults } = result; + assertionResults = assertionResults.concat(_assertionResults); testResults = testResults.concat(_testResults); } @@ -277,20 +261,20 @@ const handler = async function (argv) { const summary = printRunSummary(assertionResults, testResults); console.log(chalk.dim(chalk.grey('Ran all requests.'))); - if(summary.failedAssertions > 0 || summary.failedTests > 0) { + if (summary.failedAssertions > 0 || summary.failedTests > 0) { process.exit(1); } } } catch (err) { - console.log("Something went wrong"); + console.log('Something went wrong'); console.error(chalk.red(err.message)); process.exit(1); } }; module.exports = { - command, - desc, - builder, + command, + desc, + builder, handler }; diff --git a/packages/bruno-cli/src/index.js b/packages/bruno-cli/src/index.js index cf9fa23ee..d54c6861f 100644 --- a/packages/bruno-cli/src/index.js +++ b/packages/bruno-cli/src/index.js @@ -5,7 +5,7 @@ const { CLI_EPILOGUE, CLI_VERSION } = require('./constants'); const printBanner = () => { console.log(chalk.yellow(`Bru CLI ${CLI_VERSION}`)); -} +}; const run = async () => { const argLength = process.argv.length; diff --git a/packages/bruno-cli/src/runner/interpolate-vars.js b/packages/bruno-cli/src/runner/interpolate-vars.js index cf7109ad2..abd76ad47 100644 --- a/packages/bruno-cli/src/runner/interpolate-vars.js +++ b/packages/bruno-cli/src/runner/interpolate-vars.js @@ -6,9 +6,9 @@ Mustache.escape = function (value) { return value; }; -const interpolateVars = (request, envVars = {}, collectionVariables ={}) => { +const interpolateVars = (request, envVars = {}, collectionVariables = {}) => { const interpolate = (str) => { - if(!str || !str.length || typeof str !== "string") { + if (!str || !str.length || typeof str !== 'string') { return str; } @@ -27,29 +27,27 @@ const interpolateVars = (request, envVars = {}, collectionVariables ={}) => { request.headers[key] = interpolate(value); }); - if(request.headers["content-type"] === "application/json") { - if(typeof request.data === "object") { + if (request.headers['content-type'] === 'application/json') { + if (typeof request.data === 'object') { try { let parsed = JSON.stringify(request.data); parsed = interpolate(parsed); request.data = JSON.parse(parsed); - } catch (err) { - } + } catch (err) {} } - if(typeof request.data === "string") { - if(request.data.length) { + if (typeof request.data === 'string') { + if (request.data.length) { request.data = interpolate(request.data); } } - } else if(request.headers["content-type"] === "application/x-www-form-urlencoded") { - if(typeof request.data === "object") { + } else if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { + if (typeof request.data === 'object') { try { let parsed = JSON.stringify(request.data); parsed = interpolate(parsed); request.data = JSON.parse(parsed); - } catch (err) { - } + } catch (err) {} } } else { request.data = interpolate(request.data); diff --git a/packages/bruno-cli/src/runner/run-single-request.js b/packages/bruno-cli/src/runner/run-single-request.js index a831bc65c..d108341bf 100644 --- a/packages/bruno-cli/src/runner/run-single-request.js +++ b/packages/bruno-cli/src/runner/run-single-request.js @@ -19,7 +19,7 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll // make axios work in node using form data // reference: https://github.com/axios/axios/issues/1006#issuecomment-320165427 - if(request.headers && request.headers['content-type'] === 'multipart/form-data') { + if (request.headers && request.headers['content-type'] === 'multipart/form-data') { const form = new FormData(); forOwn(request.data, (value, key) => { form.append(key, value); @@ -30,16 +30,22 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll // run pre-request vars const preRequestVars = get(bruJson, 'request.vars.req'); - if(preRequestVars && preRequestVars.length) { + if (preRequestVars && preRequestVars.length) { const varsRuntime = new VarsRuntime(); varsRuntime.runPreRequestVars(preRequestVars, request, envVariables, collectionVariables, collectionPath); } // run pre request script const requestScriptFile = get(bruJson, 'request.script.req'); - if(requestScriptFile && requestScriptFile.length) { + if (requestScriptFile && requestScriptFile.length) { const scriptRuntime = new ScriptRuntime(); - await scriptRuntime.runRequestScript(requestScriptFile, request, envVariables, collectionVariables, collectionPath); + await scriptRuntime.runRequestScript( + requestScriptFile, + request, + envVariables, + collectionVariables, + collectionPath + ); } // interpolate variables inside request @@ -48,30 +54,29 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll const options = getOptions(); const insecure = get(options, 'insecure', false); const httpsAgentRequestFields = {}; - if(insecure) { + if (insecure) { httpsAgentRequestFields['rejectUnauthorized'] = false; - } - else { + } else { const cacertArray = [options['cacert'], process.env.SSL_CERT_FILE, process.env.NODE_EXTRA_CA_CERTS]; - const cacert = cacertArray.find(el => el); - if(cacert && cacert.length > 1) { + const cacert = cacertArray.find((el) => el); + if (cacert && cacert.length > 1) { try { caCrt = fs.readFileSync(cacert); httpsAgentRequestFields['ca'] = caCrt; - } catch(err) { + } catch (err) { console.log('Error reading CA cert file:' + cacert, err); } } } - if(Object.keys(httpsAgentRequestFields).length > 0) { + if (Object.keys(httpsAgentRequestFields).length > 0) { request.httpsAgent = new https.Agent({ ...httpsAgentRequestFields }); } // stringify the request url encoded params - if(request.headers['content-type'] === 'application/x-www-form-urlencoded') { + if (request.headers['content-type'] === 'application/x-www-form-urlencoded') { request.data = qs.stringify(request.data); } @@ -82,27 +87,48 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll // run post-response vars const postResponseVars = get(bruJson, 'request.vars.res'); - if(postResponseVars && postResponseVars.length) { + if (postResponseVars && postResponseVars.length) { const varsRuntime = new VarsRuntime(); - varsRuntime.runPostResponseVars(postResponseVars, request, response, envVariables, collectionVariables, collectionPath); + varsRuntime.runPostResponseVars( + postResponseVars, + request, + response, + envVariables, + collectionVariables, + collectionPath + ); } // run post response script const responseScriptFile = get(bruJson, 'request.script.res'); - if(responseScriptFile && responseScriptFile.length) { + if (responseScriptFile && responseScriptFile.length) { const scriptRuntime = new ScriptRuntime(); - await scriptRuntime.runResponseScript(responseScriptFile, request, response, envVariables, collectionVariables, collectionPath); + await scriptRuntime.runResponseScript( + responseScriptFile, + request, + response, + envVariables, + collectionVariables, + collectionPath + ); } // run assertions let assertionResults = []; const assertions = get(bruJson, 'request.assertions'); - if(assertions && assertions.length) { + if (assertions && assertions.length) { const assertRuntime = new AssertRuntime(); - assertionResults = assertRuntime.runAssertions(assertions, request, response, envVariables, collectionVariables, collectionPath); + assertionResults = assertRuntime.runAssertions( + assertions, + request, + response, + envVariables, + collectionVariables, + collectionPath + ); each(assertionResults, (r) => { - if(r.status === 'pass') { + if (r.status === 'pass') { console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); } else { console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); @@ -114,15 +140,22 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll // run tests let testResults = []; const testFile = get(bruJson, 'request.tests'); - if(testFile && testFile.length) { + if (testFile && testFile.length) { const testRuntime = new TestRuntime(); - const result = testRuntime.runTests(testFile, request, response, envVariables, collectionVariables, collectionPath); + const result = testRuntime.runTests( + testFile, + request, + response, + envVariables, + collectionVariables, + collectionPath + ); testResults = get(result, 'results', []); } - if(testResults && testResults.length) { + if (testResults && testResults.length) { each(testResults, (testResult) => { - if(testResult.status === 'pass') { + if (testResult.status === 'pass') { console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description)); } else { console.log(chalk.red(` ✕ `) + chalk.red(testResult.description)); @@ -135,32 +168,55 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll testResults }; } catch (err) { - if(err && err.response) { - console.log(chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`)); + if (err && err.response) { + console.log( + chalk.green(stripExtension(filename)) + chalk.dim(` (${err.response.status} ${err.response.statusText})`) + ); // run post-response vars const postResponseVars = get(bruJson, 'request.vars.res'); - if(postResponseVars && postResponseVars.length) { + if (postResponseVars && postResponseVars.length) { const varsRuntime = new VarsRuntime(); - varsRuntime.runPostResponseVars(postResponseVars, request, err.response, envVariables, collectionVariables, collectionPath); + varsRuntime.runPostResponseVars( + postResponseVars, + request, + err.response, + envVariables, + collectionVariables, + collectionPath + ); } // run post response script const responseScriptFile = get(bruJson, 'request.script.res'); - if(responseScriptFile && responseScriptFile.length) { + if (responseScriptFile && responseScriptFile.length) { const scriptRuntime = new ScriptRuntime(); - await scriptRuntime.runResponseScript(responseScriptFile, request, err.response, envVariables, collectionVariables, collectionPath); + await scriptRuntime.runResponseScript( + responseScriptFile, + request, + err.response, + envVariables, + collectionVariables, + collectionPath + ); } // run assertions let assertionResults = []; const assertions = get(bruJson, 'request.assertions'); - if(assertions && assertions.length) { + if (assertions && assertions.length) { const assertRuntime = new AssertRuntime(); - assertionResults = assertRuntime.runAssertions(assertions, request, err.response, envVariables, collectionVariables, collectionPath); + assertionResults = assertRuntime.runAssertions( + assertions, + request, + err.response, + envVariables, + collectionVariables, + collectionPath + ); each(assertionResults, (r) => { - if(r.status === 'pass') { + if (r.status === 'pass') { console.log(chalk.green(` ✓ `) + chalk.dim(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); } else { console.log(chalk.red(` ✕ `) + chalk.red(`assert: ${r.lhsExpr}: ${r.rhsExpr}`)); @@ -172,15 +228,22 @@ const runSingleRequest = async function (filename, bruJson, collectionPath, coll // run tests let testResults = []; const testFile = get(bruJson, 'request.tests'); - if(testFile && testFile.length) { + if (testFile && testFile.length) { const testRuntime = new TestRuntime(); - const result = testRuntime.runTests(testFile, request, err.response, envVariables, collectionVariables, collectionPath); + const result = testRuntime.runTests( + testFile, + request, + err.response, + envVariables, + collectionVariables, + collectionPath + ); testResults = get(result, 'results', []); } - if(testResults && testResults.length) { + if (testResults && testResults.length) { each(testResults, (testResult) => { - if(testResult.status === 'pass') { + if (testResult.status === 'pass') { console.log(chalk.green(` ✓ `) + chalk.dim(testResult.description)); } else { console.log(chalk.red(` ✕ `) + chalk.red(testResult.description)); diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 9d7a578c4..1ba6f0166 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -9,10 +9,10 @@ Mustache.escape = function (value) { /** * The transformer function for converting a BRU file to JSON. - * + * * We map the json response from the bru lang and transform it into the DSL * format that is used by the bruno app - * + * * @param {string} bru The BRU file content. * @returns {object} The JSON representation of the BRU file. */ @@ -20,35 +20,35 @@ const bruToJson = (bru) => { try { const json = bruToJsonV2(bru); - let requestType = _.get(json, "meta.type"); - if(requestType === "http") { - requestType = "http-request" - } else if(requestType === "graphql") { - requestType = "graphql-request"; + let requestType = _.get(json, 'meta.type'); + if (requestType === 'http') { + requestType = 'http-request'; + } else if (requestType === 'graphql') { + requestType = 'graphql-request'; } else { - requestType = "http"; + requestType = 'http'; } - const sequence = _.get(json, "meta.seq") + const sequence = _.get(json, 'meta.seq'); const transformedJson = { - "type": requestType, - "name": _.get(json, "meta.name"), - "seq": !isNaN(sequence) ? Number(sequence) : 1, - "request": { - "method": _.upperCase(_.get(json, "http.method")), - "url": _.get(json, "http.url"), - "params": _.get(json, "query", []), - "headers": _.get(json, "headers", []), - "body": _.get(json, "body", {}), - "vars": _.get(json, "vars", []), - "assertions": _.get(json, "assertions", []), - "script": _.get(json, "script", ""), - "tests": _.get(json, "tests", "") + type: requestType, + name: _.get(json, 'meta.name'), + seq: !isNaN(sequence) ? Number(sequence) : 1, + request: { + method: _.upperCase(_.get(json, 'http.method')), + url: _.get(json, 'http.url'), + params: _.get(json, 'query', []), + headers: _.get(json, 'headers', []), + body: _.get(json, 'body', {}), + vars: _.get(json, 'vars', []), + assertions: _.get(json, 'assertions', []), + script: _.get(json, 'script', ''), + tests: _.get(json, 'tests', '') } }; - transformedJson.request.body.mode = _.get(json, "http.body", "none"); + transformedJson.request.body.mode = _.get(json, 'http.body', 'none'); return transformedJson; } catch (err) { @@ -72,7 +72,7 @@ const getEnvVars = (environment = {}) => { const envVars = {}; _.each(variables, (variable) => { - if(variable.enabled) { + if (variable.enabled) { envVars[variable.name] = Mustache.escape(variable.value); } }); @@ -83,7 +83,7 @@ const getEnvVars = (environment = {}) => { const options = {}; const getOptions = () => { return options; -} +}; module.exports = { bruToJson, diff --git a/packages/bruno-cli/src/utils/common.js b/packages/bruno-cli/src/utils/common.js index 152c287cc..704928022 100644 --- a/packages/bruno-cli/src/utils/common.js +++ b/packages/bruno-cli/src/utils/common.js @@ -12,7 +12,7 @@ const rpad = (str, width) => { paddedStr = paddedStr + ' '; } return paddedStr; -} +}; module.exports = { lpad, diff --git a/packages/bruno-cli/src/utils/filesystem.js b/packages/bruno-cli/src/utils/filesystem.js index 7f4e60a8a..4b066ac68 100644 --- a/packages/bruno-cli/src/utils/filesystem.js +++ b/packages/bruno-cli/src/utils/filesystem.js @@ -2,7 +2,7 @@ const path = require('path'); const fs = require('fs-extra'); const fsPromises = require('fs/promises'); -const exists = async p => { +const exists = async (p) => { try { await fsPromises.access(p); return true; @@ -11,7 +11,7 @@ const exists = async p => { } }; -const isSymbolicLink = filepath => { +const isSymbolicLink = (filepath) => { try { return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink(); } catch (_) { @@ -19,7 +19,7 @@ const isSymbolicLink = filepath => { } }; -const isFile = filepath => { +const isFile = (filepath) => { try { return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile(); } catch (_) { @@ -27,7 +27,7 @@ const isFile = filepath => { } }; -const isDirectory = dirPath => { +const isDirectory = (dirPath) => { try { return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory(); } catch (_) { @@ -35,14 +35,14 @@ const isDirectory = dirPath => { } }; -const normalizeAndResolvePath = pathname => { +const normalizeAndResolvePath = (pathname) => { if (isSymbolicLink(pathname)) { const absPath = path.dirname(pathname); const targetPath = path.resolve(absPath, fs.readlinkSync(pathname)); if (isFile(targetPath) || isDirectory(targetPath)) { return path.resolve(targetPath); } - console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`) + console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`); return ''; } return path.resolve(pathname); @@ -51,29 +51,29 @@ const normalizeAndResolvePath = pathname => { const writeFile = async (pathname, content) => { try { fs.writeFileSync(pathname, content, { - encoding: "utf8" + encoding: 'utf8' }); } catch (err) { return Promise.reject(err); } }; -const hasJsonExtension = filename => { - if (!filename || typeof filename !== 'string') return false - return ['json'].some(ext => filename.toLowerCase().endsWith(`.${ext}`)) -} +const hasJsonExtension = (filename) => { + if (!filename || typeof filename !== 'string') return false; + return ['json'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`)); +}; -const hasBruExtension = filename => { - if (!filename || typeof filename !== 'string') return false - return ['bru'].some(ext => filename.toLowerCase().endsWith(`.${ext}`)) -} +const hasBruExtension = (filename) => { + if (!filename || typeof filename !== 'string') return false; + return ['bru'].some((ext) => filename.toLowerCase().endsWith(`.${ext}`)); +}; const createDirectory = async (dir) => { - if(!dir) { + if (!dir) { throw new Error(`directory: path is null`); } - if (fs.existsSync(dir)){ + if (fs.existsSync(dir)) { throw new Error(`directory: ${dir} already exists`); } @@ -93,15 +93,15 @@ const searchForFiles = (dir, extension) => { } } return results; -} +}; const searchForBruFiles = (dir) => { return searchForFiles(dir, '.bru'); }; const stripExtension = (filename = '') => { - return filename.replace(/\.[^/.]+$/, ""); -} + return filename.replace(/\.[^/.]+$/, ''); +}; const getSubDirectories = (dir) => { try { @@ -112,7 +112,7 @@ const getSubDirectories = (dir) => { }) .sort(); - return subDirectories; + return subDirectories; } catch (err) { return []; }