Repository: motdotla/node-lambda (js)

Issue:
Missing support for .mjs files
We'd like to move to newer nodejs 18 and 20 which is supported by AWS now. Our setup:

package.json :
```json
{
  "name": "fk-demo-connector",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "index.mjs",
}
```

index.mjs :
```javascript
console.log('Loading function');

export const handler = async (event, context) => {
    console.log('Received event:', JSON.stringify(event, null, 2));
   
    return {};  // Echo back the first key value
    // throw new Error('Something went wrong');
};
```

Running node-lambda:
```
> node-lambda run --contextFile ../context.json -f ../.env -j event-init.json

node:internal/errors:496
    ErrorCaptureStackTrace(err);
    ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/home/vlad/workspace/lambda/fk-demo-connector/index.js' imported from /usr/local/lib/node_modules/node-lambda/lib/main.js
    at new NodeError (node:internal/errors:405:5)
    at finalizeResolution (node:internal/modules/esm/resolve:327:11)
    at moduleResolve (node:internal/modules/esm/resolve:946:10)
    at defaultResolve (node:internal/modules/esm/resolve:1132:11)
    at nextResolve (node:internal/modules/esm/loader:163:28)
    at ESMLoader.resolve (node:internal/modules/esm/loader:835:30)
    at ESMLoader.getModuleJob (node:internal/modules/esm/loader:424:18)
    at ESMLoader.import (node:internal/modules/esm/loader:524:22)
    at importModuleDynamically (node:internal/modules/cjs/loader:1188:29)
    at importModuleDynamicallyWrapper (node:internal/vm/module:429:21) {
  code: 'ERR_MODULE_NOT_FOUND'
}

Node.js v18.18.0
```

Suggestions:
* Allow to parameterize extension index.js -> index.mjs
* Take index.mjs from package.json main key

Pull request:
Support .mjs
fixes #700

Patch:
diff --git a/README.md b/README.md
index d3a794c..9f7cf5e 100644
--- a/README.md
+++ b/README.md
@@ -230,7 +230,7 @@ AWS Lambda will let you set environment variables for your function. Use the sam
 
 ## Node.js Runtime Configuration
 
-AWS Lambda now supports Node.js 18, Node.js 16 and Node.js 14. Please also check the [Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) page.
+AWS Lambda now supports Node.js 20, Node.js 18 and Node.js 16. Please also check the [Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) page.
 
 ## Use S3 to deploy
 
diff --git a/index_mjs.mjs b/index_mjs.mjs
new file mode 100644
index 0000000..529deb2
--- /dev/null
+++ b/index_mjs.mjs
@@ -0,0 +1,9 @@
+// For development/testing purposes
+export const handler = (event, context, callback) => {
+  console.log('Running index.handler (mjs)')
+  console.log('==================================')
+  console.log('event', event)
+  console.log('==================================')
+  console.log('Stopping index.handler (mjs)')
+  callback(null)
+}
diff --git a/lib/.env.example b/lib/.env.example
index 4dfd135..fd20d26 100644
--- a/lib/.env.example
+++ b/lib/.env.example
@@ -10,7 +10,7 @@ AWS_HANDLER=index.handler
 AWS_MEMORY_SIZE=128
 AWS_TIMEOUT=3
 AWS_DESCRIPTION=
-AWS_RUNTIME=nodejs18.x
+AWS_RUNTIME=nodejs20.x
 AWS_VPC_SUBNETS=
 AWS_VPC_SECURITY_GROUPS=
 AWS_TRACING_CONFIG=
diff --git a/lib/main.js b/lib/main.js
index b9e5108..fe14185 100644
--- a/lib/main.js
+++ b/lib/main.js
@@ -52,14 +52,25 @@ event_sources.json and ${program.eventFile} files as needed.`)
   }
 
   async run (program) {
-    if (!['nodejs14.x', 'nodejs16.x', 'nodejs18.x'].includes(program.runtime)) {
+    if (!['nodejs16.x', 'nodejs18.x', 'nodejs20.x'].includes(program.runtime)) {
       console.error(`Runtime [${program.runtime}] is not supported.`)
       process.exit(254)
     }
 
     this._createSampleFile(program.eventFile, 'event.json')
     const splitHandler = program.handler.split('.')
-    const filename = splitHandler[0] + '.js'
+    const filename = (() => {
+      for (const extension of ['.js', '.mjs']) {
+        if (fs.existsSync(splitHandler[0] + extension)) {
+          return splitHandler[0] + extension
+        }
+      }
+    })()
+    if (filename == null) {
+      console.error('Handler file not found.')
+      process.exitCode = 255
+      return
+    }
     const handlername = splitHandler[1]
 
     // Set custom environment variables if program.configFile is defined


Test patch:
diff --git a/test/main.js b/test/main.js
index d4bec29..d91c870 100644
--- a/test/main.js
+++ b/test/main.js
@@ -23,7 +23,7 @@ const originalProgram = {
   memorySize: 128,
   timeout: 3,
   description: '',
-  runtime: 'nodejs18.x',
+  runtime: 'nodejs20.x',
   deadLetterConfigTargetArn: '',
   tracingConfig: '',
   Layers: '',
diff --git a/test/node-lambda.js b/test/node-lambda.js
index 834e344..23b00f0 100644
--- a/test/node-lambda.js
+++ b/test/node-lambda.js
@@ -157,7 +157,7 @@ describe('bin/node-lambda', () => {
         process.env.AWS_RUNTIME = 'test'
       })
       after(() => {
-        process.env.AWS_RUNTIME = 'nodejs18.x'
+        process.env.AWS_RUNTIME = 'nodejs20.x'
       })
 
       it('`node-lambda run` exitCode is `254` (callback(null))', (done) => {
@@ -171,14 +171,14 @@ describe('bin/node-lambda', () => {
       })
     })
 
-    describe('node-lambda run (Runtime nodejs18.x is supported)', () => {
+    describe('node-lambda run (Runtime nodejs20.x is supported)', () => {
       const eventObj = {
         asyncTest: false,
         callbackWaitsForEmptyEventLoop: true // True is the default value of Lambda
       }
 
       before(() => {
-        process.env.AWS_RUNTIME = 'nodejs18.x'
+        process.env.AWS_RUNTIME = 'nodejs20.x'
       })
 
       it('`node-lambda run` exitCode is not `254` (callback(null))', (done) => {
@@ -189,7 +189,7 @@ describe('bin/node-lambda', () => {
       })
     })
 
-    describe('node-lambda run (Multiple events))', () => {
+    describe('node-lambda run (Multiple events)', () => {
       const eventObj = [{
         asyncTest: false,
         callbackWaitsForEmptyEventLoop: true,
@@ -217,7 +217,7 @@ describe('bin/node-lambda', () => {
       })
     })
 
-    describe('node-lambda run (disable Multiple events))', () => {
+    describe('node-lambda run (disable Multiple events)', () => {
       const eventObj = [{
         asyncTest: false,
         callbackWaitsForEmptyEventLoop: true,
@@ -264,7 +264,7 @@ describe('bin/node-lambda', () => {
       })
     })
 
-    describe('node-lambda run (API Gateway events))', () => {
+    describe('node-lambda run (API Gateway events)', () => {
       const eventObj = {
         asyncTest: false,
         callbackWaitsForEmptyEventLoop: true,
@@ -295,6 +295,54 @@ describe('bin/node-lambda', () => {
         })
       })
     })
+
+    describe('node-lambda run (by *.mjs)', () => {
+      it('`node-lambda run` by index.mjs', function (done) {
+        const run = spawn('node', [
+          nodeLambdaPath, 'run',
+          '--handler', 'index_mjs.handler'
+        ])
+        let stdoutString = ''
+        run.stdout.on('data', (data) => {
+          stdoutString += data.toString().replace(/\r|\n|\s/g, '')
+        })
+
+        run.on('exit', (code) => {
+          const expected = 'Runningindex.handler(mjs)' +
+            '==================================' +
+            'event{asyncTest:false,callbackWaitsForEmptyEventLoop:true,callbackCode:\'callback(null);\'}' +
+            '==================================' +
+            'Stoppingindex.handler(mjs)Success:'
+          assert.equal(stdoutString, expected)
+          assert.equal(code, 0)
+          done()
+        })
+      })
+    })
+
+    describe('node-lambda run (handler file not found)', () => {
+      it('`node-lambda run` Invalid handler specification.', function (done) {
+        const run = spawn('node', [
+          nodeLambdaPath, 'run',
+          '--handler', 'not_found.handler'
+        ])
+        let stdoutString = ''
+        run.stdout.on('data', (data) => {
+          stdoutString += data.toString().replace(/\r|\n|\s/g, '')
+        })
+        let stderrString = ''
+        run.stderr.on('data', (data) => {
+          stderrString += data.toString()
+        })
+
+        run.on('exit', (code) => {
+          assert.equal(stdoutString, '')
+          assert.match(stderrString, /Handler file not found\./)
+          assert.equal(code, 255)
+          done()
+        })
+      })
+    })
   })
 
   describe('node-lambda duplicate check of short option', () => {