diff --git a/package-lock.json b/package-lock.json index 88d5e07b4e..1763eb8cf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8854,6 +8854,17 @@ "dev": true, "license": "MIT" }, + "node_modules/esbuild-wasm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.19.8.tgz", + "integrity": "sha512-+5BhFGjW0+3cC5BEcujYfNaslSEBjF+zFHj4a7xff2LLByCJGok3iCyV9/oHpN8OlZrGlnjSduhY1t1QqU1YBQ==", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/escalade": { "version": "3.1.1", "license": "MIT", @@ -21917,7 +21928,7 @@ } }, "packages/artillery": { - "version": "2.0.2", + "version": "2.0.3", "license": "MPL-2.0", "dependencies": { "@artilleryio/int-commons": "*", @@ -21945,6 +21956,7 @@ "dependency-tree": "^10.0.9", "detective": "^5.1.0", "dotenv": "^16.0.1", + "esbuild-wasm": "^0.19.8", "eventemitter3": "^4.0.4", "fs-extra": "^10.1.0", "ip": "^1.1.8", diff --git a/packages/artillery/lib/cmds/run.js b/packages/artillery/lib/cmds/run.js index 74f0d3161b..41781b0d68 100644 --- a/packages/artillery/lib/cmds/run.js +++ b/packages/artillery/lib/cmds/run.js @@ -21,6 +21,7 @@ const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const os = require('os'); +const esbuild = require('esbuild-wasm'); const createLauncher = require('../launch-platform'); const createConsoleReporter = require('../../console-reporter'); @@ -436,6 +437,57 @@ RunCommand.runCommandImplementation = async function (flags, argv, args) { } }; +function replaceProcessorIfTypescript(script, scriptPath, platform) { + const relativeProcessorPath = script.config.processor; + + if (!relativeProcessorPath) { + return script; + } + const extensionType = path.extname(relativeProcessorPath); + + if (extensionType != '.ts') { + return script; + } + + if (platform == 'aws:lambda') { + throw new Error('Typescript processor is not supported on AWS Lambda'); + } + + const actualProcessorPath = path.resolve( + path.dirname(scriptPath), + relativeProcessorPath + ); + const processorFileName = path.basename(actualProcessorPath, extensionType); + + const tmpDir = os.tmpdir(); + const newProcessorPath = path.join( + tmpDir, + `${processorFileName}-${Date.now()}.js` + ); + + try { + esbuild.buildSync({ + entryPoints: [actualProcessorPath], + outfile: newProcessorPath, + bundle: true, + platform: 'node', + format: 'cjs', + sourcemap: 'inline', + sourceRoot: '/' //TODO: review this? + }); + } catch (error) { + throw new Error(`Failed to compile Typescript processor\n${error.message}`); + } + + global.artillery.hasTypescriptProcessor = true; + console.log( + `Bundled Typescript file into JS. New processor path: ${newProcessorPath}` + ); + + script.config.processor = newProcessorPath; + return script; +} + async function prepareTestExecutionPlan(inputFiles, flags, args) { let script1 = {}; @@ -500,7 +552,13 @@ async function prepareTestExecutionPlan(inputFiles, flags, args) { script5.config.statsInterval = script5.config.statsInterval || 30; const script6 = addDefaultPlugins(script5); - return script6; + const script7 = replaceProcessorIfTypescript( + script6, + inputFiles[0], + flags.platform + ); + + return script7; } async function readPayload(script) { diff --git a/packages/artillery/lib/platform/aws-lambda/index.js b/packages/artillery/lib/platform/aws-lambda/index.js index 4df3bc4996..d3824102bb 100644 --- a/packages/artillery/lib/platform/aws-lambda/index.js +++ b/packages/artillery/lib/platform/aws-lambda/index.js @@ -290,7 +290,8 @@ class PlatformLambda { 'detective', 'is-builtin-module', 'try-require', - 'walk-sync' + 'walk-sync', + 'esbuild-wasm' ], { cwd: a9cwd diff --git a/packages/artillery/lib/platform/local/artillery-worker-local.js b/packages/artillery/lib/platform/local/artillery-worker-local.js index 1472755171..ea8b2f34ef 100644 --- a/packages/artillery/lib/platform/local/artillery-worker-local.js +++ b/packages/artillery/lib/platform/local/artillery-worker-local.js @@ -10,6 +10,18 @@ const STATES = require('../worker-states'); const awaitOnEE = require('../../util/await-on-ee'); +const returnWorkerEnv = (needsSourcemap) => { + let env = { ...process.env }; + + if (needsSourcemap) { + env['NODE_OPTIONS'] = process.env.NODE_OPTIONS + ? `${process.env.NODE_OPTIONS} --enable-source-maps` + : '--enable-source-maps'; + } + + return env; +}; + class ArtilleryWorker { constructor(opts) { this.opts = opts; @@ -20,13 +32,19 @@ class ArtilleryWorker { async init(_opts) { this.state = STATES.initializing; - this.worker = new Worker(path.join(__dirname, 'worker.js')); + const workerEnv = returnWorkerEnv(global.artillery.hasTypescriptProcessor); + + this.worker = new Worker(path.join(__dirname, 'worker.js'), { + env: workerEnv + }); this.workerId = this.worker.threadId; this.worker.on('error', this.onError.bind(this)); // TODO: this.worker.on('exit', (exitCode) => { this.events.emit('exit', exitCode); }); + + //eslint-disable-next-line handle-callback-err this.worker.on('messageerror', (err) => {}); // TODO: Expose performance metrics via getHeapSnapshot() and performance object. diff --git a/packages/artillery/package.json b/packages/artillery/package.json index 2740f07b32..b9a444db17 100644 --- a/packages/artillery/package.json +++ b/packages/artillery/package.json @@ -110,6 +110,7 @@ "dependency-tree": "^10.0.9", "detective": "^5.1.0", "dotenv": "^16.0.1", + "esbuild-wasm": "^0.19.8", "eventemitter3": "^4.0.4", "fs-extra": "^10.1.0", "ip": "^1.1.8", diff --git a/packages/artillery/test/cli/run-typescript.test.js b/packages/artillery/test/cli/run-typescript.test.js new file mode 100644 index 0000000000..d15b264ac8 --- /dev/null +++ b/packages/artillery/test/cli/run-typescript.test.js @@ -0,0 +1,71 @@ +const tap = require('tap'); +const { execute, returnTmpPath } = require('../cli/_helpers.js'); +const { createHash } = require('crypto'); +const fs = require('fs'); + +let reportFilePath; +tap.beforeEach(async (t) => { + reportFilePath = returnTmpPath( + `report-${createHash('md5') + .update(t.name) + .digest('hex')}-${Date.now()}.json` + ); +}); + +// tap.test('Can run a Typescript processor', async (t) => { +// const [exitCode, output] = await execute([ +// 'run', +// '-o', +// `${reportFilePath}`, +// 'test/scripts/scenarios-typescript/lodash.yml' +// ]); + +// t.equal(exitCode, 0, 'CLI should exit with code 0'); +// t.ok( +// output.stdout.includes('Got context using lodash: true'), +// 'Should be able to use lodash in a scenario to get context' +// ); +// const json = JSON.parse(fs.readFileSync(reportFilePath, 'utf8')); + +// t.equal( +// json.aggregate.counters['http.codes.200'], +// 2, +// 'Should have made 2 requests' +// ); +// t.equal( +// json.aggregate.counters['hey_from_ts'], +// 2, +// 'Should have emitted 2 custom metrics from ts processor' +// ); +// }); + +tap.test( + 'Failure from a Typescript processor has a resolvable stack trace via source maps', + async (t) => { + const [exitCode, output] = await execute([ + 'run', + '-o', + `${reportFilePath}`, + 'test/scripts/scenarios-typescript/error.yml' + ]); + + t.equal(exitCode, 11, 'CLI should exit with code 11'); + t.ok( + output.stdout.includes('error_from_ts_processor'), + 'Should have logged error from ts processor' + ); + + // Search for the path + const pathRegex = /\((.*?):\d+:\d+\)/; + const match = output.stdout.match(pathRegex); + + // Extract the path if found + const extractedPath = match ? match[1] : null; + + t.ok( + extractedPath.includes('.ts'), + 'Should be using source maps to resolve the path to a .ts file' + ); + t.ok(fs.existsSync(extractedPath), 'Error path should exist'); + } +); diff --git a/packages/artillery/test/scripts/scenarios-typescript/error.yml b/packages/artillery/test/scripts/scenarios-typescript/error.yml new file mode 100644 index 0000000000..cf5074e472 --- /dev/null +++ b/packages/artillery/test/scripts/scenarios-typescript/error.yml @@ -0,0 +1,15 @@ +config: + target: "http://asciiart.artillery.io:8080" + phases: + - duration: 2 + arrivalRate: 1 + name: "Phase 1" + processor: "./processor.ts" + variables: + isTypescript: true + +scenarios: + - flow: + - function: processorWithError + - get: + url: "/" \ No newline at end of file diff --git a/packages/artillery/test/scripts/scenarios-typescript/lodash.yml b/packages/artillery/test/scripts/scenarios-typescript/lodash.yml new file mode 100644 index 0000000000..e417c2787d --- /dev/null +++ b/packages/artillery/test/scripts/scenarios-typescript/lodash.yml @@ -0,0 +1,15 @@ +config: + target: "http://asciiart.artillery.io:8080" + phases: + - duration: 2 + arrivalRate: 1 + name: "Phase 1" + processor: "./processor.ts" + variables: + isTypescript: true + +scenarios: + - flow: + - function: myTest + - get: + url: "/" \ No newline at end of file diff --git a/packages/artillery/test/scripts/scenarios-typescript/processor.ts b/packages/artillery/test/scripts/scenarios-typescript/processor.ts new file mode 100644 index 0000000000..22a2253808 --- /dev/null +++ b/packages/artillery/test/scripts/scenarios-typescript/processor.ts @@ -0,0 +1,16 @@ +import _ from 'lodash'; + +export const myTest = async (context, ee, next) => { + const isTypescript = _.get(context, 'vars.isTypescript'); + + console.log(`Got context using lodash: ${JSON.stringify(isTypescript)}`); + + ee.emit('counter', 'hey_from_ts', 1); + + next(); +}; + +export const processorWithError = async (context, ee, next) => { + throw new Error('error_from_ts_processor'); + next(); +};