diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 57d2a415..d3d4db91 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -4,7 +4,7 @@ To maintain a high standard of quality in our releases, before merging every pull request we ask that you've completed the following: -- [ ] Forked the repo and created your branch from `main` +- [X] Forked the repo and created your branch from `main` - [ ] Made sure tests pass (run `npm it` from the repo root) - [ ] Expanded test coverage related to your changes: - [ ] Added and/or updated unit tests (if appropriate) diff --git a/.github/workflows/binary-build.yml b/.github/workflows/binary-build.yml index a6be55f9..75ccad20 100644 --- a/.github/workflows/binary-build.yml +++ b/.github/workflows/binary-build.yml @@ -44,6 +44,11 @@ jobs: with: deno-version: v1.x + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: v1.1.x + - name: Env run: | echo "Event name: ${{ github.event_name }}" @@ -55,6 +60,7 @@ jobs: VER=`python --version`; echo "Python ver: $VER" VER=`ruby --version`; echo "Ruby ver: $VER" VER=`deno --version`; echo "Deno ver: $VER" + VER=`bun --version`; echo "Bun ver: $VER" echo "OS ver: ${{ runner.os }}" - name: Install diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9717884b..ac9c4291 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -44,6 +44,11 @@ jobs: with: deno-version: v1.x + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: v1.1.x + - name: Env run: | echo "Event name: ${{ github.event_name }}" @@ -55,6 +60,7 @@ jobs: VER=`python --version`; echo "Python ver: $VER" VER=`ruby --version`; echo "Ruby ver: $VER" VER=`deno --version`; echo "Deno ver: $VER" + VER=`bun --version`; echo "Bun ver: $VER" echo "OS ver: ${{ runner.os }}" - name: Install diff --git a/.gitignore b/.gitignore index c8ff8638..939a3ed1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ test/mock/tmp # special case in which we're testing local shared file dependency paths test/mock/dep-warn/**/node_modules/@architect/ + +#WebStorm +.idea \ No newline at end of file diff --git a/changelog.md b/changelog.md index 7b51b877..aa90b6cf 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Architect Sandbox changelog +--- +## [6.0.5] 2024-06-29 + +### Changed + +- Added bun support + --- ## [6.0.5] 2024-04-29 diff --git a/src/invoke-lambda/env/context.js b/src/invoke-lambda/env/context.js index 8f03853e..0c2afc94 100644 --- a/src/invoke-lambda/env/context.js +++ b/src/invoke-lambda/env/context.js @@ -13,7 +13,7 @@ module.exports = function createLambdaContext (params) { let functionVersion = '$LATEST' let invokedFunctionArn = 'sandbox' - if (runtime.startsWith('node') || runtime.startsWith('deno')) { + if (runtime.startsWith('node') || runtime.startsWith('deno') || runtime.startsWith('bun')) { let lambdaContext = { awsRequestId: makeRequestId(), functionName, diff --git a/src/invoke-lambda/exec/index.js b/src/invoke-lambda/exec/index.js index 5ab1c76e..c20d0719 100644 --- a/src/invoke-lambda/exec/index.js +++ b/src/invoke-lambda/exec/index.js @@ -64,6 +64,7 @@ function getRuntime ({ config, handlerModuleSystem }) { if (handlerModuleSystem === 'esm') return 'node-esm' return 'node' } + else if (run.startsWith('bun')) return 'bun' else if (run.startsWith('deno')) return 'deno' else if (run.startsWith('ruby')) return 'ruby' else if (run.startsWith('python')) return 'python' diff --git a/src/invoke-lambda/exec/loader.js b/src/invoke-lambda/exec/loader.js index 901d6c72..b1697e79 100644 --- a/src/invoke-lambda/exec/loader.js +++ b/src/invoke-lambda/exec/loader.js @@ -7,7 +7,7 @@ let handlers module.exports = function () { if (handlers) return handlers handlers = {} - let runtimes = [ 'deno.mjs', 'node.js', 'node-esm.mjs', 'python.py', 'ruby.rb' ] + let runtimes = [ 'bun.mjs', 'deno.mjs', 'node.js', 'node-esm.mjs', 'python.py', 'ruby.rb' ] runtimes.forEach(runtime => { try { let name = runtime.split('.')[0] diff --git a/src/invoke-lambda/exec/runtimes/bun.mjs b/src/invoke-lambda/exec/runtimes/bun.mjs new file mode 100644 index 00000000..d06b30d1 --- /dev/null +++ b/src/invoke-lambda/exec/runtimes/bun.mjs @@ -0,0 +1,68 @@ +/* eslint @stylistic/js/semi: [ 'error', 'always' ] */ +/* global Bun */ +const { __ARC_CONFIG__, __ARC_CONTEXT__, AWS_LAMBDA_RUNTIME_API: runtimeAPI } = Bun.env.toObject(); +const url = p => runtimeAPI + '/2018-06-01/runtime/' + p; +const headers = { 'content-type': 'application/json; charset=utf-8' }; + +(async function main () { + try { + const { handlerFile, handlerMethod } = JSON.parse(__ARC_CONFIG__); + const context = JSON.parse(__ARC_CONTEXT__); + Bun.env.delete('__ARC_CONFIG__'); + Bun.env.delete('__ARC_CONTEXT__'); + + const isPromise = obj => obj && typeof obj.then === 'function'; + + async function run () { + const next = url('invocation/next'); + const response = await fetch(next); + const event = await response.json(); + + const requestID = response.headers.get('Lambda-Runtime-Aws-Request-Id') || response.headers.get('lambda-runtime-aws-request-id'); + const errorEndpoint = url('invocation/' + requestID + '/error'); + const responseEndpoint = url('invocation/' + requestID + '/response'); + + const file = 'file://' + handlerFile; + const mod = await import(file); + const handler = mod[handlerMethod]; + + async function callback (err, result) { + if (err) { + console.log(err); + const errorMessage = err.message || '(unknown error)'; + const errorType = err.name || '(unknown error type)'; + const stackTrace = err.stack ? err.stack.split('\n') : undefined; + const body = JSON.stringify({ errorMessage, errorType, stackTrace }); + await fetch(errorEndpoint, { method: 'POST', headers, body }); + } + else { + const options = { method: 'POST', headers }; + if (result) options.body = JSON.stringify(result); + await fetch(responseEndpoint, options); + } + } + try { + const response = handler(event, context, callback); + if (isPromise(response)) { + response.then(result => callback(null, result)).catch(callback); + } + } + catch (err) { + callback(err); + } + } + await run(); + } + catch (err) { + (async function initError () { + const unknown = 'Unknown init error'; + console.log('Lambda init error:', err || unknown); + const initErrorEndpoint = url('init/error'); + const errorMessage = err.message || unknown; + const errorType = err.name || 'Unknown init error type'; + const stackTrace = err.stack ? err.stack.split('\n') : undefined; + const body = JSON.stringify({ errorMessage, errorType, stackTrace }); + await fetch(initErrorEndpoint, { method: 'POST', headers, body }); + })(); + } +})(); diff --git a/src/lib/runtime-eval.js b/src/lib/runtime-eval.js index 163fb721..302c6e0f 100644 --- a/src/lib/runtime-eval.js +++ b/src/lib/runtime-eval.js @@ -5,6 +5,10 @@ let pyCommandCache * Provides platform-specific runtime-specific commands for child_process spawns */ module.exports = { + bun: (script) => ({ + command: 'bun', + args: [ 'run', '-e', script ], + }), deno: function (script) { return { command: 'deno', diff --git a/src/sandbox/check-runtimes/version-check.js b/src/sandbox/check-runtimes/version-check.js index 789f36f2..4e383647 100644 --- a/src/sandbox/check-runtimes/version-check.js +++ b/src/sandbox/check-runtimes/version-check.js @@ -51,7 +51,9 @@ module.exports = function runtimeVersionCheck (params) { function check (configured, localRuntimes) { let alias = aliases[configured.toLowerCase()] let runtime = alias ? runtimes[alias][0] : configured - let major = ver => ver.split('.')[0] + let major = ver => { + return ver.split('.')[0] + } let minor = ver => ver.split('.')[1] if (runtime.startsWith('nodejs')) { let runtimeVer = runtimeVersions[runtime].wildcard diff --git a/test/mock/multi-handler/app.arc b/test/mock/multi-handler/app.arc index 11b9f157..795b6bff 100644 --- a/test/mock/multi-handler/app.arc +++ b/test/mock/multi-handler/app.arc @@ -2,10 +2,15 @@ multi-handler @http +# Bun +get /bun/index.js +get /bun/index.ts +get /bun/index.tsx + # Deno get /deno/index.js get /deno/mod.js -get /deno/index.ts + get /deno/mod.ts get /deno/index.tsx get /deno/mod.tsx @@ -17,4 +22,4 @@ get /node/esm/index.js get /node/esm/index.mjs @arc -runtime nodejs20.x # The Deno functions have config files +runtime nodejs20.x # The Deno and Bun functions have config files diff --git a/test/mock/multi-handler/src/http/get-bun-index_js/config.arc b/test/mock/multi-handler/src/http/get-bun-index_js/config.arc new file mode 100644 index 00000000..0b0ffc4d --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_js/config.arc @@ -0,0 +1,2 @@ +@aws +runtime bun diff --git a/test/mock/multi-handler/src/http/get-bun-index_js/index.js b/test/mock/multi-handler/src/http/get-bun-index_js/index.js new file mode 100644 index 00000000..93ef7d4c --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_js/index.js @@ -0,0 +1,10 @@ +export async function handler (req) { + let body = req + body.message = 'Hello from get /bun/index.js' + const response = { + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + } + return response +} diff --git a/test/mock/multi-handler/src/http/get-bun-index_ts/config.arc b/test/mock/multi-handler/src/http/get-bun-index_ts/config.arc new file mode 100644 index 00000000..0b0ffc4d --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_ts/config.arc @@ -0,0 +1,2 @@ +@aws +runtime bun diff --git a/test/mock/multi-handler/src/http/get-bun-index_ts/index.ts b/test/mock/multi-handler/src/http/get-bun-index_ts/index.ts new file mode 100644 index 00000000..75f1a0ba --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_ts/index.ts @@ -0,0 +1,10 @@ +export async function handler (req: any) { + let body = req + body.message = 'Hello from get /bun/index.ts' + const response = { + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }; + return response; +} diff --git a/test/mock/multi-handler/src/http/get-bun-index_tsx/config.arc b/test/mock/multi-handler/src/http/get-bun-index_tsx/config.arc new file mode 100644 index 00000000..0b0ffc4d --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_tsx/config.arc @@ -0,0 +1,2 @@ +@aws +runtime bun diff --git a/test/mock/multi-handler/src/http/get-bun-index_tsx/index.tsx b/test/mock/multi-handler/src/http/get-bun-index_tsx/index.tsx new file mode 100644 index 00000000..31379a63 --- /dev/null +++ b/test/mock/multi-handler/src/http/get-bun-index_tsx/index.tsx @@ -0,0 +1,10 @@ +export async function handler (req: any) { + let body = req + body.message = 'Hello from get /bun/index.tsx' + const response = { + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }; + return response; +} diff --git a/test/mock/normal/app.arc b/test/mock/normal/app.arc index f29f05ab..371ec5a4 100644 --- a/test/mock/normal/app.arc +++ b/test/mock/normal/app.arc @@ -21,6 +21,7 @@ get /python3.11 get /python3.8 get /ruby3.2 get /deno +get /bun # Path get /get-p-c/:param/* get /get-c/* diff --git a/test/mock/normal/src/http/get-bun/config.arc b/test/mock/normal/src/http/get-bun/config.arc new file mode 100644 index 00000000..950f8dbd --- /dev/null +++ b/test/mock/normal/src/http/get-bun/config.arc @@ -0,0 +1,5 @@ +@aws +runtime bun +timeout 10 +# concurrency 1 +# memory 1152 diff --git a/test/mock/normal/src/http/get-bun/index.ts b/test/mock/normal/src/http/get-bun/index.ts new file mode 100644 index 00000000..cdbe5062 --- /dev/null +++ b/test/mock/normal/src/http/get-bun/index.ts @@ -0,0 +1,10 @@ +export async function handler (req: any) { + let body = req + body.message = 'Hello from get /bun (running bun)' + const response = { + statusCode: 200, + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body) + }; + return response; +} diff --git a/test/unit/src/invoke-lambda/index-test.js b/test/unit/src/invoke-lambda/index-test.js index f5262d00..6bacfcad 100644 --- a/test/unit/src/invoke-lambda/index-test.js +++ b/test/unit/src/invoke-lambda/index-test.js @@ -10,6 +10,7 @@ let { credentials: creds } = require(join(cwd, 'test', 'utils')) let runtimes = { asap: 0, + bun: 0, deno: 0, node: 0, python: 0, @@ -21,6 +22,7 @@ let exec = (lambda, params, callback) => { let { runtime } = lambda.config let run if (runtime.startsWith('node')) run = 'node' + if (runtime.startsWith('bun')) run = 'bun' if (runtime.startsWith('deno')) run = 'deno' if (runtime.startsWith('python')) run = 'python' if (runtime.startsWith('ruby')) run = 'ruby' @@ -60,7 +62,7 @@ test('Get inventory', t => { }) test('Test runtime invocations', t => { - t.plan(21) + t.plan(24) let lambda lambda = get.http('get /') @@ -125,6 +127,15 @@ test('Test runtime invocations', t => { t.equals(timeout, 10000, 'deno ran with correct timeout') t.deepEqual(result, event, 'deno received event') }) + + lambda = get.http('get /bun') + invoke({ lambda, ...params }, (err, result) => { + if (err) t.end(err) + let { options, timeout } = execPassedParams + t.equals(options.cwd, lambda.src, 'bun passed correct path') + t.equals(timeout, 10000, 'bun ran with correct timeout') + t.deepEqual(result, event, 'bun received event') + }) }) // This test will still hit the node call counter at least once @@ -184,9 +195,10 @@ test('Test ASAP invocation', t => { }) test('Verify call counts from runtime invocations', t => { - t.plan(5) + t.plan(6) t.equals(runtimes.asap, 1, 'ASAP called correct number of times') t.equals(runtimes.deno, 1, 'Deno called correct number of times') + t.equals(runtimes.deno, 1, 'Bun called correct number of times') t.equals(runtimes.node, 5, 'Node called correct number of times') t.equals(runtimes.python, 2, 'Python called correct number of times') t.equals(runtimes.ruby, 1, 'Ruby called correct number of times')