diff --git a/.eslintignore b/.eslintignore index a99edb6a8..1b9efa436 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,4 @@ **/dist **/node_modules + +src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js \ No newline at end of file diff --git a/src/lambda/__tests__/LambdaFunction.test.js b/src/lambda/__tests__/LambdaFunction.test.js index a9c85998e..729e8c22b 100644 --- a/src/lambda/__tests__/LambdaFunction.test.js +++ b/src/lambda/__tests__/LambdaFunction.test.js @@ -27,52 +27,52 @@ describe('LambdaFunction', () => { { description: 'should return result when handler is context.done', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.contextDoneHandler', + handler: 'fixtures/lambdaFunction-fixture.contextDoneHandler', }, { description: 'should return result when handler is context.done which is deferred', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.contextDoneHandlerDeferred', + handler: 'fixtures/lambdaFunction-fixture.contextDoneHandlerDeferred', }, { description: 'should return result when handler is context.succeed', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.contextSucceedHandler', + handler: 'fixtures/lambdaFunction-fixture.contextSucceedHandler', }, { description: 'should return result when handler is context.succeed which is deferred', expected: 'foo', handler: - 'fixtures/lambdaFunction.fixture.contextSucceedHandlerDeferred', + 'fixtures/lambdaFunction-fixture.contextSucceedHandlerDeferred', }, { description: 'should return result when handler is a callback', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.callbackHandler', + handler: 'fixtures/lambdaFunction-fixture.callbackHandler', }, { description: 'should return result when handler is a callback which is deferred', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.callbackHandlerDeferred', + handler: 'fixtures/lambdaFunction-fixture.callbackHandlerDeferred', }, { description: 'should return result when handler returns a promise', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.promiseHandler', + handler: 'fixtures/lambdaFunction-fixture.promiseHandler', }, { description: 'should return result when handler returns a promise which is deferred', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.promiseHandlerDeferred', + handler: 'fixtures/lambdaFunction-fixture.promiseHandlerDeferred', }, { description: 'should return result when handler is an async function', expected: 'foo', - handler: 'fixtures/lambdaFunction.fixture.asyncFunctionHandler', + handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandler', }, // NOTE: mix and matching of callbacks and promises is not recommended, // nonetheless, we test some of the behaviour to match AWS execution precedence @@ -81,33 +81,33 @@ describe('LambdaFunction', () => { 'should return result when handler returns a callback but defines a callback parameter', expected: 'Hello Promise!', handler: - 'fixtures/lambdaFunction.fixture.promiseWithDefinedCallbackHandler', + 'fixtures/lambdaFunction-fixture.promiseWithDefinedCallbackHandler', }, { description: 'should return result when handler calls context.succeed and context.done', expected: 'Hello Context.succeed!', handler: - 'fixtures/lambdaFunction.fixture.contextSucceedWithContextDoneHandler', + 'fixtures/lambdaFunction-fixture.contextSucceedWithContextDoneHandler', }, { description: 'should return result when handler calls callback and context.done', expected: 'Hello Callback!', handler: - 'fixtures/lambdaFunction.fixture.callbackWithContextDoneHandler', + 'fixtures/lambdaFunction-fixture.callbackWithContextDoneHandler', }, { description: 'should return result when handler calls callback and returns Promise', expected: 'Hello Callback!', - handler: 'fixtures/lambdaFunction.fixture.callbackWithPromiseHandler', + handler: 'fixtures/lambdaFunction-fixture.callbackWithPromiseHandler', }, { description: 'should return result when handler calls callback inside returned Promise', expected: 'Hello Callback!', - handler: 'fixtures/lambdaFunction.fixture.callbackInsidePromiseHandler', + handler: 'fixtures/lambdaFunction-fixture.callbackInsidePromiseHandler', }, ].forEach(({ description, expected, handler }) => { it(description, async () => { @@ -132,7 +132,7 @@ describe('LambdaFunction', () => { it('should pass remaining time to LambdaContext', async () => { const functionDefinition = { - handler: 'fixtures/lambdaFunction.fixture.remainingExecutionTimeHandler', + handler: 'fixtures/lambdaFunction-fixture.remainingExecutionTimeHandler', } const options = {} const lambdaFunction = new LambdaFunction( @@ -152,7 +152,7 @@ describe('LambdaFunction', () => { it.skip('should use default lambda timeout when timeout is not provided', async () => { const functionDefinition = { - handler: 'fixtures/lambdaFunction.fixture.defaultTimeoutHandler', + handler: 'fixtures/lambdaFunction-fixture.defaultTimeoutHandler', } const options = {} const lambdaFunction = new LambdaFunction( @@ -175,7 +175,7 @@ describe('LambdaFunction', () => { // // might run flaky (unreliable) // test('executionTimeInMillis should return execution time', async () => { // const functionDefinition = { - // handler: 'fixtures/lambdaFunction.fixture.executionTimeInMillisHandler', + // handler: 'fixtures/lambdaFunction-fixture.executionTimeInMillisHandler', // } // const options = {} // const lambdaFunction = new LambdaFunction( diff --git a/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js b/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject-fixture.js similarity index 86% rename from src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js rename to src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject-fixture.js index 313c8cb31..0dfa9856c 100644 --- a/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js +++ b/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsJSONObject-fixture.js @@ -12,7 +12,7 @@ export default class LambdaFunctionThatReturnsJSONObject { serverless = { config: { serverlessPath: '', - servicePath: resolve(__dirname), + servicePath: resolve(__dirname, '../..'), }, service: { provider: { @@ -27,8 +27,7 @@ export default class LambdaFunctionThatReturnsJSONObject { getByFunctionName(functionName) { const functionDefinition = { - handler: - '../../fixtures/lambdaFunction.fixture.asyncFunctionHandlerObject', + handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandlerObject', } this.#lambdaFunction = new LambdaFunction( diff --git a/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js b/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString-fixture.js similarity index 87% rename from src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js rename to src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString-fixture.js index e5554ee49..5619046ab 100644 --- a/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js +++ b/src/lambda/__tests__/fixtures/Lambda/LambdaFunctionThatReturnsNativeString-fixture.js @@ -12,7 +12,7 @@ export default class LambdaFunctionThatReturnsNativeString { serverless = { config: { serverlessPath: '', - servicePath: resolve(__dirname), + servicePath: resolve(__dirname, '../..'), }, service: { provider: { @@ -27,7 +27,7 @@ export default class LambdaFunctionThatReturnsNativeString { getByFunctionName(functionName) { const functionDefinition = { - handler: '../../fixtures/lambdaFunction.fixture.asyncFunctionHandler', + handler: 'fixtures/lambdaFunction-fixture.asyncFunctionHandler', } this.#lambdaFunction = new LambdaFunction( diff --git a/src/lambda/__tests__/fixtures/lambdaFunction.fixture.js b/src/lambda/__tests__/fixtures/lambdaFunction-fixture.js similarity index 100% rename from src/lambda/__tests__/fixtures/lambdaFunction.fixture.js rename to src/lambda/__tests__/fixtures/lambdaFunction-fixture.js diff --git a/src/lambda/__tests__/routes/invocations/InvocationsController.test.js b/src/lambda/__tests__/routes/invocations/InvocationsController.test.js index 0b54a5500..92e7419fb 100644 --- a/src/lambda/__tests__/routes/invocations/InvocationsController.test.js +++ b/src/lambda/__tests__/routes/invocations/InvocationsController.test.js @@ -1,7 +1,7 @@ import assert from 'node:assert' import InvocationsController from '../../../routes/invocations/InvocationsController.js' -import LambdaFunctionThatReturnsJSONObject from '../../fixtures/Lambda/LambdaFunctionThatReturnsJSONObject.fixture.js' -import LambdaFunctionThatReturnsNativeString from '../../fixtures/Lambda/LambdaFunctionThatReturnsNativeString.fixture.js' +import LambdaFunctionThatReturnsJSONObject from '../../fixtures/Lambda/LambdaFunctionThatReturnsJSONObject-fixture.js' +import LambdaFunctionThatReturnsNativeString from '../../fixtures/Lambda/LambdaFunctionThatReturnsNativeString-fixture.js' describe('InvocationController', () => { const functionName = 'foo' diff --git a/src/lambda/handler-runner/HandlerRunner.js b/src/lambda/handler-runner/HandlerRunner.js index b9f781ff8..d8d3c1d85 100644 --- a/src/lambda/handler-runner/HandlerRunner.js +++ b/src/lambda/handler-runner/HandlerRunner.js @@ -25,10 +25,10 @@ export default class HandlerRunner { async #loadRunner() { const { useChildProcesses, useDocker, useInProcess } = this.#options - const { functionKey, handlerName, handlerPath, runtime, timeout } = + const { functionKey, handler, runtime, servicePath, timeout } = this.#funOptions - log.debug(`Loading handler... (${handlerPath})`) + log.debug(`Loading handler... (${handler})`) if (useDocker) { // https://github.com/lambci/docker-lambda/issues/329 @@ -73,10 +73,10 @@ export default class HandlerRunner { return new InProcessRunner( functionKey, - handlerPath, - handlerName, this.#env, timeout, + handler, + servicePath, ) } diff --git a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js index 6c735250a..03c7c087d 100644 --- a/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js +++ b/src/lambda/handler-runner/child-process-runner/ChildProcessRunner.js @@ -11,19 +11,19 @@ export default class ChildProcessRunner { #functionKey = null - #handlerName = null + #handler = null - #handlerPath = null + #servicePath = null #timeout = null constructor(funOptions, env) { - const { functionKey, handlerName, handlerPath, timeout } = funOptions + const { functionKey, handler, servicePath, timeout } = funOptions this.#env = env this.#functionKey = functionKey - this.#handlerName = handlerName - this.#handlerPath = handlerPath + this.#handler = handler + this.#servicePath = servicePath this.#timeout = timeout } @@ -34,7 +34,7 @@ export default class ChildProcessRunner { async run(event, context) { const childProcess = execaNode( childProcessHelperPath, - [this.#functionKey, this.#handlerName, this.#handlerPath], + [this.#functionKey, this.#handler, this.#servicePath], { env: this.#env, stdio: 'inherit', diff --git a/src/lambda/handler-runner/child-process-runner/childProcessHelper.js b/src/lambda/handler-runner/child-process-runner/childProcessHelper.js index a59d8be8a..c8f3270ab 100644 --- a/src/lambda/handler-runner/child-process-runner/childProcessHelper.js +++ b/src/lambda/handler-runner/child-process-runner/childProcessHelper.js @@ -21,7 +21,7 @@ process.on('uncaughtException', (err) => { }) }) -const [, , functionKey, handlerName, handlerPath] = argv +const [, , functionKey, handler, servicePath] = argv process.on('message', async (messageData) => { const { context, event, timeout } = messageData @@ -29,10 +29,10 @@ process.on('message', async (messageData) => { // TODO we could probably cache this in the module scope? const inProcessRunner = new InProcessRunner( functionKey, - handlerPath, - handlerName, process.env, timeout, + handler, + servicePath, ) const result = await inProcessRunner.run(event, context) diff --git a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js index 475f10855..53c0765e0 100644 --- a/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +++ b/src/lambda/handler-runner/in-process-runner/InProcessRunner.js @@ -1,29 +1,26 @@ -import { createRequire } from 'node:module' import { performance } from 'node:perf_hooks' import process from 'node:process' -import { log } from '@serverless/utils/log.js' +import { load } from './aws-lambda-ric/UserFunction.js' const { floor } = Math const { assign } = Object -const require = createRequire(import.meta.url) - export default class InProcessRunner { #env = null #functionKey = null - #handlerName = null + #handler = null - #handlerPath = null + #servicePath = null #timeout = null - constructor(functionKey, handlerPath, handlerName, env, timeout) { + constructor(functionKey, env, timeout, handler, servicePath) { this.#env = env this.#functionKey = functionKey - this.#handlerName = handlerName - this.#handlerPath = handlerPath + this.#handler = handler + this.#servicePath = servicePath this.#timeout = timeout } @@ -32,37 +29,13 @@ export default class InProcessRunner { cleanup() {} async run(event, context) { - // check if the handler module path exists - if (!require.resolve(this.#handlerPath)) { - throw new Error( - `Could not find handler module '${this.#handlerPath}' for function '${ - this.#functionKey - }'.`, - ) - } - // process.env should be available in the handler module scope as well as in the handler function scope // NOTE: Don't use Object spread (...) here! // otherwise the values of the attached props are not coerced to a string // e.g. process.env.foo = 1 should be coerced to '1' (string) assign(process.env, this.#env) - let handler - - try { - // eslint-disable-next-line import/no-dynamic-require - ;({ [this.#handlerName]: handler } = require(this.#handlerPath)) - } catch (err) { - log.error(err) - } - - if (typeof handler !== 'function') { - throw new Error( - `offline: handler '${this.#handlerName}' in ${ - this.#handlerPath - } is not a function`, - ) - } + const handler = await load(this.#servicePath, this.#handler) let callback diff --git a/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js b/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js new file mode 100644 index 000000000..1542880a7 --- /dev/null +++ b/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js @@ -0,0 +1,359 @@ +'use strict' + +const { pathToFileURL } = require('node:url') + +// node_modules/lambda-runtime/dist/node16/UserFunction.js +;(function () { + const __getOwnPropNames = Object.getOwnPropertyNames + const __commonJS = (cb, mod) => + function __require() { + return ( + mod || + (0, cb[__getOwnPropNames(cb)[0]])( + (mod = { exports: {} }).exports, + mod, + ), + mod.exports + ) + } + const require_Errors = __commonJS({ + 'Errors.js': function (exports2, module2) { + 'use strict' + + const util = require('util') + function _isError(obj) { + return ( + obj && + obj.name && + obj.message && + obj.stack && + typeof obj.name === 'string' && + typeof obj.message === 'string' && + typeof obj.stack === 'string' + ) + } + function intoError(err) { + if (err instanceof Error) { + return err + } + return new Error(err) + } + module2.exports.intoError = intoError + function toRapidResponse(error) { + try { + if (util.types.isNativeError(error) || _isError(error)) { + return { + errorType: error.name, + errorMessage: error.message, + trace: error.stack.split('\n'), + } + } + return { + errorType: typeof error, + errorMessage: error.toString(), + trace: [], + } + } catch (_err) { + return { + errorType: 'handled', + errorMessage: + 'callback called with Error argument, but there was a problem while retrieving one or more of its message, name, and stack', + } + } + } + module2.exports.toRapidResponse = toRapidResponse + module2.exports.toFormatted = (error) => { + try { + return ` ${JSON.stringify(error, (_k, v) => + _withEnumerableProperties(v), + )}` + } catch (err) { + return ` ${JSON.stringify(toRapidResponse(error))}` + } + } + function _withEnumerableProperties(error) { + if (error instanceof Error) { + const ret = { + errorType: error.name, + errorMessage: error.message, + code: error.code, + ...error, + } + if (typeof error.stack === 'string') { + ret.stack = error.stack.split('\n') + } + return ret + } + return error + } + const errorClasses = [ + class ImportModuleError extends Error {}, + class HandlerNotFound extends Error {}, + class MalformedHandlerName extends Error {}, + class UserCodeSyntaxError extends Error {}, + class MalformedStreamingHandler extends Error {}, + class InvalidStreamingOperation extends Error {}, + class UnhandledPromiseRejection extends Error { + constructor(reason, promise) { + super(reason) + this.reason = reason + this.promise = promise + } + }, + ] + errorClasses.forEach((e) => { + module2.exports[e.name] = e + e.prototype.name = `Runtime.${e.name}` + }) + }, + }) + const require_VerboseLog = __commonJS({ + 'VerboseLog.js': function (exports2) { + 'use strict' + + const EnvVarName = 'AWS_LAMBDA_RUNTIME_VERBOSE' + const Tag = 'RUNTIME' + const Verbosity = (() => { + if (!process.env[EnvVarName]) { + return 0 + } + try { + const verbosity = parseInt(process.env[EnvVarName]) + return verbosity < 0 ? 0 : verbosity > 3 ? 3 : verbosity + } catch (_) { + return 0 + } + })() + exports2.logger = function (category) { + return { + verbose() { + if (Verbosity >= 1) { + console.log.apply(null, [Tag, category, ...arguments]) + } + }, + vverbose() { + if (Verbosity >= 2) { + console.log.apply(null, [Tag, category, ...arguments]) + } + }, + vvverbose() { + if (Verbosity >= 3) { + console.log.apply(null, [Tag, category, ...arguments]) + } + }, + } + } + }, + }) + const require_HttpResponseStream = __commonJS({ + 'HttpResponseStream.js': function (exports2, module2) { + 'use strict' + + const METADATA_PRELUDE_CONTENT_TYPE = + 'application/vnd.awslambda.http-integration-response' + const DELIMITER_LEN = 8 + const HttpResponseStream2 = class { + static from(underlyingStream, prelude) { + underlyingStream.setContentType(METADATA_PRELUDE_CONTENT_TYPE) + const metadataPrelude = JSON.stringify(prelude) + underlyingStream._onBeforeFirstWrite = (write) => { + write(metadataPrelude) + write(new Uint8Array(DELIMITER_LEN)) + } + return underlyingStream + } + } + module2.exports.HttpResponseStream = HttpResponseStream2 + }, + }) + const path = require('path') + const fs = require('fs') + const { + HandlerNotFound, + MalformedHandlerName, + ImportModuleError, + UserCodeSyntaxError, + } = require_Errors() + const { verbose } = require_VerboseLog().logger('LOADER') + const { HttpResponseStream } = require_HttpResponseStream() + const FUNCTION_EXPR = /^([^.]*)\.(.*)$/ + const RELATIVE_PATH_SUBSTRING = '..' + const HANDLER_STREAMING = Symbol.for('aws.lambda.runtime.handler.streaming') + const STREAM_RESPONSE = 'response' + const NoGlobalAwsLambda = + process.env.AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA === '1' || + process.env.AWS_LAMBDA_NODEJS_NO_GLOBAL_AWSLAMBDA === 'true' + function _moduleRootAndHandler(fullHandlerString) { + const handlerString = path.basename(fullHandlerString) + const moduleRoot = fullHandlerString.substring( + 0, + fullHandlerString.indexOf(handlerString), + ) + return [moduleRoot, handlerString] + } + function _splitHandlerString(handler) { + const match = handler.match(FUNCTION_EXPR) + if (!match || match.length != 3) { + throw new MalformedHandlerName('Bad handler') + } + return [match[1], match[2]] + } + function _resolveHandler(object, nestedProperty) { + return nestedProperty.split('.').reduce((nested, key) => { + return nested && nested[key] + }, object) + } + function _tryRequireFile(file, extension) { + const path2 = file + (extension || '') + verbose('Try loading as commonjs:', path2) + return fs.existsSync(path2) ? require(path2) : void 0 + } + async function _tryAwaitImport(file, extension) { + const path2 = file + (extension || '') + verbose('Try loading as esmodule:', path2) + if (fs.existsSync(path2)) { + return await import(pathToFileURL(path2).href) + } + return void 0 + } + function _hasFolderPackageJsonTypeModule(folder) { + if (folder.endsWith('/node_modules')) { + return false + } + const pj = path.join(folder, '/package.json') + if (fs.existsSync(pj)) { + try { + const pkg = JSON.parse(fs.readFileSync(pj)) + if (pkg) { + if (pkg.type === 'module') { + verbose(`'type: module' detected in ${pj}`) + return true + } + verbose(`'type: module' not detected in ${pj}`) + return false + } + } catch (e) { + console.warn( + `${pj} cannot be read, it will be ignored for ES module detection purposes.`, + e, + ) + return false + } + } + if (folder === '/') { + return false + } + return _hasFolderPackageJsonTypeModule(path.resolve(folder, '..')) + } + function _hasPackageJsonTypeModule(file) { + const jsPath = `${file}.js` + return fs.existsSync(jsPath) + ? _hasFolderPackageJsonTypeModule(path.resolve(path.dirname(jsPath))) + : false + } + async function _tryRequire(appRoot, moduleRoot, module2) { + verbose( + 'Try loading as commonjs: ', + module2, + ' with paths: ,', + appRoot, + moduleRoot, + ) + const lambdaStylePath = path.resolve(appRoot, moduleRoot, module2) + const extensionless = _tryRequireFile(lambdaStylePath) + if (extensionless) { + return extensionless + } + const pjHasModule = _hasPackageJsonTypeModule(lambdaStylePath) + if (!pjHasModule) { + const loaded2 = _tryRequireFile(lambdaStylePath, '.js') + if (loaded2) { + return loaded2 + } + } + const loaded = + (pjHasModule && (await _tryAwaitImport(lambdaStylePath, '.js'))) || + (await _tryAwaitImport(lambdaStylePath, '.mjs')) || + _tryRequireFile(lambdaStylePath, '.cjs') + if (loaded) { + return loaded + } + verbose( + 'Try loading as commonjs: ', + module2, + ' with path(s): ', + appRoot, + moduleRoot, + ) + const nodeStylePath = require.resolve(module2, { + paths: [appRoot, moduleRoot], + }) + return require(nodeStylePath) + } + async function _loadUserApp(appRoot, moduleRoot, module2) { + if (!NoGlobalAwsLambda) { + globalThis.awslambda = { + streamifyResponse: (handler) => { + handler[HANDLER_STREAMING] = STREAM_RESPONSE + return handler + }, + HttpResponseStream, + } + } + try { + return await _tryRequire(appRoot, moduleRoot, module2) + } catch (e) { + if (e instanceof SyntaxError) { + throw new UserCodeSyntaxError(e) + } else if (e.code !== void 0 && e.code === 'MODULE_NOT_FOUND') { + verbose('globalPaths', JSON.stringify(require('module').globalPaths)) + throw new ImportModuleError(e) + } else { + throw e + } + } + } + function _throwIfInvalidHandler(fullHandlerString) { + if (fullHandlerString.includes(RELATIVE_PATH_SUBSTRING)) { + throw new MalformedHandlerName( + `'${fullHandlerString}' is not a valid handler name. Use absolute paths when specifying root directories in handler names.`, + ) + } + } + function _isHandlerStreaming(handler) { + if ( + typeof handler[HANDLER_STREAMING] === 'undefined' || + handler[HANDLER_STREAMING] === null || + handler[HANDLER_STREAMING] === false + ) { + return false + } + if (handler[HANDLER_STREAMING] === STREAM_RESPONSE) { + return STREAM_RESPONSE + } + throw new MalformedStreamingHandler('Only response streaming is supported.') + } + module.exports.load = async function (appRoot, fullHandlerString) { + _throwIfInvalidHandler(fullHandlerString) + const [moduleRoot, moduleAndHandler] = + _moduleRootAndHandler(fullHandlerString) + const [module2, handlerPath] = _splitHandlerString(moduleAndHandler) + const userApp = await _loadUserApp(appRoot, moduleRoot, module2) + const handlerFunc = _resolveHandler(userApp, handlerPath) + if (!handlerFunc) { + throw new HandlerNotFound( + `${fullHandlerString} is undefined or not exported`, + ) + } + if (typeof handlerFunc !== 'function') { + throw new HandlerNotFound(`${fullHandlerString} is not a function`) + } + return handlerFunc + } + module.exports.getHandlerMetadata = function (handlerFunc) { + return { + streaming: _isHandlerStreaming(handlerFunc), + } + } + module.exports.STREAM_RESPONSE = STREAM_RESPONSE +})() diff --git a/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/package.json b/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js index 52044948c..9e26bf2e4 100644 --- a/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +++ b/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js @@ -11,15 +11,15 @@ export default class WorkerThreadRunner { constructor(funOptions /* options */, env) { // this._options = options - const { functionKey, handlerName, handlerPath, timeout } = funOptions + const { functionKey, handler, servicePath, timeout } = funOptions this.#workerThread = new Worker(workerThreadHelperPath, { // don't pass process.env from the main process! env, workerData: { functionKey, - handlerName, - handlerPath, + handler, + servicePath, timeout, }, }) diff --git a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js index 153b18ab4..bb7f72ac8 100644 --- a/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +++ b/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js @@ -2,7 +2,7 @@ import { env } from 'node:process' import { parentPort, workerData } from 'node:worker_threads' import InProcessRunner from '../in-process-runner/index.js' -const { functionKey, handlerName, handlerPath, timeout } = workerData +const { functionKey, handler, servicePath, timeout } = workerData parentPort.on('message', async (messageData) => { const { context, event, port } = messageData @@ -10,10 +10,10 @@ parentPort.on('message', async (messageData) => { // TODO we could probably cache this in the module scope? const inProcessRunner = new InProcessRunner( functionKey, - handlerPath, - handlerName, env, timeout, + handler, + servicePath, ) let result diff --git a/tests/handler-module-formats/commonjs/cjs-extension/handler.cjs b/tests/handler-module-formats/commonjs/cjs-extension/handler.cjs new file mode 100644 index 000000000..0ff968b4c --- /dev/null +++ b/tests/handler-module-formats/commonjs/cjs-extension/handler.cjs @@ -0,0 +1,10 @@ +'use strict' + +const { stringify } = JSON + +exports.foo = async function foo() { + return { + body: stringify('foo'), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/commonjs/handlerModuleFormats.test.js b/tests/handler-module-formats/commonjs/handlerModuleFormats.test.js new file mode 100644 index 000000000..41935095d --- /dev/null +++ b/tests/handler-module-formats/commonjs/handlerModuleFormats.test.js @@ -0,0 +1,81 @@ +import assert from 'node:assert' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { setup, teardown } from '../../_testHelpers/index.js' +import { BASE_URL } from '../../config.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +describe('handler module format: commonjs', function desc() { + beforeEach(() => + setup({ + servicePath: resolve(__dirname), + }), + ) + + afterEach(() => teardown()) + + // + ;[ + { + description: '...', + expected: 'bar', + path: '/dev/namespace-export-1', + status: 200, + }, + + { + description: '...', + expected: 'foobar', + path: '/dev/namespace-export-2', + status: 200, + }, + + { + description: '...', + expected: 'static', + path: '/dev/namespace-export-3', + status: 200, + }, + + { + description: '...', + expected: 'prototype', + path: '/dev/namespace-export-4', + status: 200, + }, + + { + description: '...', + expected: 'foo', + path: '/dev/js-extension', + status: 200, + }, + + { + description: '...', + expected: 'foo', + path: '/dev/cjs-extension', + status: 200, + }, + + { + description: '...', + expected: 'foo', + path: '/dev/package-type', + status: 200, + }, + ].forEach(({ description, expected, path, status }) => { + it(description, async () => { + const url = new URL(path, BASE_URL) + + const response = await fetch(url) + assert.equal(response.status, status) + + if (expected) { + const json = await response.json() + assert.deepEqual(json, expected) + } + }) + }) +}) diff --git a/tests/handler-module-formats/commonjs/js-extension/handler.js b/tests/handler-module-formats/commonjs/js-extension/handler.js new file mode 100644 index 000000000..0ff968b4c --- /dev/null +++ b/tests/handler-module-formats/commonjs/js-extension/handler.js @@ -0,0 +1,10 @@ +'use strict' + +const { stringify } = JSON + +exports.foo = async function foo() { + return { + body: stringify('foo'), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/commonjs/js-extension/package.json b/tests/handler-module-formats/commonjs/js-extension/package.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/handler-module-formats/commonjs/js-extension/package.json @@ -0,0 +1 @@ +{} diff --git a/tests/handler-module-formats/commonjs/namespace-export/handler.js b/tests/handler-module-formats/commonjs/namespace-export/handler.js new file mode 100644 index 000000000..9bed67179 --- /dev/null +++ b/tests/handler-module-formats/commonjs/namespace-export/handler.js @@ -0,0 +1,41 @@ +/* eslint-disable max-classes-per-file */ + +'use strict' + +const { stringify } = JSON + +exports.namespaceFoo = { + async exportBar() { + return { + body: stringify('bar'), + statusCode: 200, + } + }, +} + +exports.namespaceFoo.namespaceBar = { + async exportFooBar() { + return { + body: stringify('foobar'), + statusCode: 200, + } + }, +} + +exports.namespaceClassStatic = class Foo { + static async exportStatic() { + return { + body: stringify('static'), + statusCode: 200, + } + } +} + +exports.namespaceClassPrototype = new (class Foo { + async exportPrototype() { + return { + body: stringify('prototype'), + statusCode: 200, + } + } +})() diff --git a/tests/handler-module-formats/commonjs/namespace-export/package.json b/tests/handler-module-formats/commonjs/namespace-export/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/handler-module-formats/commonjs/namespace-export/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/handler-module-formats/commonjs/package-type/handler.js b/tests/handler-module-formats/commonjs/package-type/handler.js new file mode 100644 index 000000000..0ff968b4c --- /dev/null +++ b/tests/handler-module-formats/commonjs/package-type/handler.js @@ -0,0 +1,10 @@ +'use strict' + +const { stringify } = JSON + +exports.foo = async function foo() { + return { + body: stringify('foo'), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/commonjs/package-type/package.json b/tests/handler-module-formats/commonjs/package-type/package.json new file mode 100644 index 000000000..5bbefffba --- /dev/null +++ b/tests/handler-module-formats/commonjs/package-type/package.json @@ -0,0 +1,3 @@ +{ + "type": "commonjs" +} diff --git a/tests/handler-module-formats/commonjs/serverless.yml b/tests/handler-module-formats/commonjs/serverless.yml new file mode 100644 index 000000000..fc4f1c641 --- /dev/null +++ b/tests/handler-module-formats/commonjs/serverless.yml @@ -0,0 +1,64 @@ +service: handler-module-formats-commonjs + +configValidationMode: error + +plugins: + - ../../../ + +provider: + memorySize: 128 + name: aws + region: us-east-1 # default + runtime: nodejs16.x + stage: dev + versionFunctions: false + +functions: + namespaceExport1: + events: + - http: + method: get + path: namespace-export-1 + handler: namespace-export/handler.namespaceFoo.exportBar + + namespaceExport2: + events: + - http: + method: get + path: namespace-export-2 + handler: namespace-export/handler.namespaceFoo.namespaceBar.exportFooBar + + namespaceExport3: + events: + - http: + method: get + path: namespace-export-3 + handler: namespace-export/handler.namespaceClassStatic.exportStatic + + namespaceExport4: + events: + - http: + method: get + path: namespace-export-4 + handler: namespace-export/handler.namespaceClassPrototype.exportPrototype + + js-extension: + events: + - http: + method: get + path: js-extension + handler: js-extension/handler.foo + + cjs-extension: + events: + - http: + method: get + path: cjs-extension + handler: cjs-extension/handler.foo + + package-type: + events: + - http: + method: get + path: package-type + handler: package-type/handler.foo diff --git a/tests/handler-module-formats/module/handlerModuleFormats.test.js b/tests/handler-module-formats/module/handlerModuleFormats.test.js new file mode 100644 index 000000000..7ef0f31c3 --- /dev/null +++ b/tests/handler-module-formats/module/handlerModuleFormats.test.js @@ -0,0 +1,81 @@ +import assert from 'node:assert' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { setup, teardown } from '../../_testHelpers/index.js' +import { BASE_URL } from '../../config.js' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +describe('handler module format: module', function desc() { + beforeEach(() => + setup({ + servicePath: resolve(__dirname), + }), + ) + + afterEach(() => teardown()) + + // + ;[ + { + description: '...', + expected: 'bar', + path: '/dev/namespace-export-1', + status: 200, + }, + + { + description: '...', + expected: 'foobar', + path: '/dev/namespace-export-2', + status: 200, + }, + + { + description: '...', + expected: 'static', + path: '/dev/namespace-export-3', + status: 200, + }, + + { + description: '...', + expected: 'prototype', + path: '/dev/namespace-export-4', + status: 200, + }, + + { + description: '...', + expected: 'foo', + path: '/dev/mjs-extension', + status: 200, + }, + + { + description: '...', + expected: 'foo', + path: '/dev/package-type', + status: 200, + }, + + { + description: 'top-level-await', + expected: 'foo', + path: '/dev/top-level-await', + status: 200, + }, + ].forEach(({ description, expected, path, status }) => { + it(description, async () => { + const url = new URL(path, BASE_URL) + + const response = await fetch(url) + assert.equal(response.status, status) + + if (expected) { + const json = await response.json() + assert.deepEqual(json, expected) + } + }) + }) +}) diff --git a/tests/handler-module-formats/module/mjs-extension/handler.mjs b/tests/handler-module-formats/module/mjs-extension/handler.mjs new file mode 100644 index 000000000..c7e2365f7 --- /dev/null +++ b/tests/handler-module-formats/module/mjs-extension/handler.mjs @@ -0,0 +1,9 @@ +const { stringify } = JSON + +// eslint-disable-next-line import/prefer-default-export +export async function foo() { + return { + body: stringify('foo'), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/module/namespace-export/handler.js b/tests/handler-module-formats/module/namespace-export/handler.js new file mode 100644 index 000000000..9411475d8 --- /dev/null +++ b/tests/handler-module-formats/module/namespace-export/handler.js @@ -0,0 +1,31 @@ +// eslint-disable-next-line max-classes-per-file +const { stringify } = JSON + +export * as namespaceFoo from './handlers.js' + +export const namespaceBar = { + async exportFooBar() { + return { + body: stringify('foobar'), + statusCode: 200, + } + }, +} + +export const namespaceClassStatic = class Foo { + static async exportStatic() { + return { + body: stringify('static'), + statusCode: 200, + } + } +} + +export const namespaceClassPrototype = new (class Foo { + async exportPrototype() { + return { + body: stringify('prototype'), + statusCode: 200, + } + } +})() diff --git a/tests/handler-module-formats/module/namespace-export/handlers.js b/tests/handler-module-formats/module/namespace-export/handlers.js new file mode 100644 index 000000000..e4f895ab5 --- /dev/null +++ b/tests/handler-module-formats/module/namespace-export/handlers.js @@ -0,0 +1,16 @@ +const { stringify } = JSON + +// eslint-disable-next-line import/prefer-default-export +export async function foo() { + return { + body: stringify('bar'), + statusCode: 200, + } +} + +// export async function bar() { +// return { +// body: stringify('foobar'), +// statusCode: 200, +// } +// } diff --git a/tests/handler-module-formats/module/namespace-export/package.json b/tests/handler-module-formats/module/namespace-export/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/handler-module-formats/module/namespace-export/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/handler-module-formats/module/package-type/handler.js b/tests/handler-module-formats/module/package-type/handler.js new file mode 100644 index 000000000..c7e2365f7 --- /dev/null +++ b/tests/handler-module-formats/module/package-type/handler.js @@ -0,0 +1,9 @@ +const { stringify } = JSON + +// eslint-disable-next-line import/prefer-default-export +export async function foo() { + return { + body: stringify('foo'), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/module/package-type/package.json b/tests/handler-module-formats/module/package-type/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/handler-module-formats/module/package-type/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +} diff --git a/tests/handler-module-formats/module/serverless.yml b/tests/handler-module-formats/module/serverless.yml new file mode 100644 index 000000000..0cbbfb525 --- /dev/null +++ b/tests/handler-module-formats/module/serverless.yml @@ -0,0 +1,64 @@ +service: handler-module-formats-module + +configValidationMode: error + +plugins: + - ../../../ + +provider: + memorySize: 128 + name: aws + region: us-east-1 # default + runtime: nodejs16.x + stage: dev + versionFunctions: false + +functions: + namespaceExport1: + events: + - http: + method: get + path: namespace-export-1 + handler: namespace-export/handler.namespaceFoo.foo + + namespaceExport2: + events: + - http: + method: get + path: namespace-export-2 + handler: namespace-export/handler.namespaceBar.exportFooBar + + namespaceExport3: + events: + - http: + method: get + path: namespace-export-3 + handler: namespace-export/handler.namespaceClassStatic.exportStatic + + namespaceExport4: + events: + - http: + method: get + path: namespace-export-4 + handler: namespace-export/handler.namespaceClassPrototype.exportPrototype + + mjs-extension: + events: + - http: + method: get + path: mjs-extension + handler: mjs-extension/handler.foo + + package-type: + events: + - http: + method: get + path: package-type + handler: package-type/handler.foo + + top-level-await: + events: + - http: + method: get + path: top-level-await + handler: top-level-await/handler.bar diff --git a/tests/handler-module-formats/module/top-level-await/foo.js b/tests/handler-module-formats/module/top-level-await/foo.js new file mode 100644 index 000000000..e7544e318 --- /dev/null +++ b/tests/handler-module-formats/module/top-level-await/foo.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export const foo = 'foo' diff --git a/tests/handler-module-formats/module/top-level-await/handler.js b/tests/handler-module-formats/module/top-level-await/handler.js new file mode 100644 index 000000000..2496e7d80 --- /dev/null +++ b/tests/handler-module-formats/module/top-level-await/handler.js @@ -0,0 +1,11 @@ +const { stringify } = JSON + +const { foo } = await import('./foo.js') + +// eslint-disable-next-line import/prefer-default-export +export async function bar() { + return { + body: stringify(foo), + statusCode: 200, + } +} diff --git a/tests/handler-module-formats/module/top-level-await/package.json b/tests/handler-module-formats/module/top-level-await/package.json new file mode 100644 index 000000000..3dbc1ca59 --- /dev/null +++ b/tests/handler-module-formats/module/top-level-await/package.json @@ -0,0 +1,3 @@ +{ + "type": "module" +}