Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NodeJS 14.x - Native AWS Lambda Import/Export Support

I am looking to make use of the native import/export that comes with ES6.

I am using Serverless Containers within AWS Lambda.

I have my Dockerfile which looks like this:

FROM public.ecr.aws/lambda/nodejs:14

COPY app ./

RUN npm install

CMD [ "app.handler" ]

I then have an app directory with my application code. The app.js code looks like this:

import { success } from './utils/log';

exports.handler = async () => {
  success('lambda invoked');
  const response = 'Hello World';
  return {
    statusCode: 200,
    body: JSON.stringify(response),
    isBase64Encoded: false,
  };
};

As you can see from this line import { success } from './utils/log'; I am making use of native imports.

In my package.json I specify this:

  "type": "module"

As I need to tell my application this is a module and I would like imports natively. If I don't specify this, I get:

{
    "errorType": "Runtime.UserCodeSyntaxError",
    "errorMessage": "SyntaxError: Cannot use import statement outside a module",
    "stack": [
        "Runtime.UserCodeSyntaxError: SyntaxError: Cannot use import statement outside a module",
        "    at _loadUserApp (/var/runtime/UserFunction.js:98:13)",
        "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)",
        "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
        "    at Module._compile (internal/modules/cjs/loader.js:1063:30)",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)",
        "    at Module.load (internal/modules/cjs/loader.js:928:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:769:14)",
        "    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:72:12)",
        "    at internal/main/run_main_module.js:17:47"
    ]
}

So, I specify it, telling Lambda this is a module. However, for the life of me I can't get it to work, I am seeing this error:

{
    "errorType": "Error",
    "errorMessage": "Must use import to load ES Module: /var/task/app.js\nrequire() of ES modules is not supported.\nrequire() of /var/task/app.js from /var/runtime/UserFunction.js is an ES module file as it is a .js file whose nearest parent package.json contains \"type\": \"module\" which defines all .js files in that package scope as ES modules.\nInstead rename app.js to end in .cjs, change the requiring code to use import(), or remove \"type\": \"module\" from /var/task/package.json.\n",
    "code": "ERR_REQUIRE_ESM",
    "stack": [
        "Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /var/task/app.js",
        "require() of ES modules is not supported.",
        "require() of /var/task/app.js from /var/runtime/UserFunction.js is an ES module file as it is a .js file whose nearest parent package.json contains \"type\": \"module\" which defines all .js files in that package scope as ES modules.",
        "Instead rename app.js to end in .cjs, change the requiring code to use import(), or remove \"type\": \"module\" from /var/task/package.json.",
        "",
        "    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1080:13)",
        "    at Module.load (internal/modules/cjs/loader.js:928:32)",
        "    at Function.Module._load (internal/modules/cjs/loader.js:769:14)",
        "    at Module.require (internal/modules/cjs/loader.js:952:19)",
        "    at require (internal/modules/cjs/helpers.js:88:18)",
        "    at _tryRequire (/var/runtime/UserFunction.js:75:12)",
        "    at _loadUserApp (/var/runtime/UserFunction.js:95:12)",
        "    at Object.module.exports.load (/var/runtime/UserFunction.js:140:17)",
        "    at Object.<anonymous> (/var/runtime/index.js:43:30)",
        "    at Module._compile (internal/modules/cjs/loader.js:1063:30)"
    ]
}

It looks like /var/runtime/UserFunction.js is calling my app handler as a require and a module. However, I have no control over /var/runtime/UserFunction.js (I don't believe?). In my Dockerfile I have specified Node14. I don't quite know where I have gone wrong?

What I am looking to do is run the latest and greatest of Node14 (such as imports) without Babel/Transpiler that "bloat" my code. If someone could point me in the right direction of where I have gone wrong, it would be appreciated.

like image 354
user3180997 Avatar asked Mar 17 '21 15:03

user3180997


People also ask

Does Lambda support node 14?

js 14. You can now author Amazon Lambda functions in Node. js 14 and use its new features, such as top-level-await, enhanced diagnostics, modifications of the streams APIs, and a revised JavaScript engine for better performance.

Does node 14 support import?

Node 14 introduces ECMAScript modules — no more transpiling, no more difficulties with import and export.

Is node js supported by AWS Lambda?

AWS Lambda now supports Node. js 16 as both a managed runtime and a container base image. Developers creating serverless applications in Lambda with Node. js 16 can take advantage of new features such as support for Apple silicon for local development, the timers promises API, and enhanced performance.

Does node support import export?

It's finally happened: nearly 4 years after the import keyword was introduced in ES6, Node. js introduced experimental support for ES6 imports and exports. In Node. js 12, you can use import and export in your project if you do both of the below items.

How do I deploy Lambda in Node JS 14?

To deploy Lambda functions using Node.js 14, upload the code through the Lambda console and select the Node.js 14.x runtime. You can also use AWS CLI, AWS Serverless Application Model (AWS SAM) and AWS CloudFormation to deploy and manage serverless applications authored in Node.js 14.

What version of NodeJS is used for AWS Lambda?

You can now develop AWS Lambda functions using the Node.js 14.x runtime. This is the current Long Term Support (LTS) version of Node.js. Start using this new version today by specifying a runtime parameter value of nodejs14.x when creating or updating functions or by using the appropriate managed runtime base image.

What is the difference between lambda and node 14?

Lambda will automatically apply updates to the Node.js 14 managed runtime and to the Node.js 14 AWS provided base image, as they become available from the Node.js community. The Node.js 14 runtime is available in all Regions where Lambda is available.

What's new in AWS Lambda 14?

You can now author AWS Lambda functions in Node.js 14 and use its new features, such as top-level-await, enhanced diagnostics, modifications of the streams APIs, and a revised JavaScript engine for better performance. Lambda functions written in Node.js 14 run on Amazon Linux 2, the latest generation of Amazon Linux.


4 Answers

If anyone sees this, running into the same problem. Please see the below from AWS Offical Technical Support:

"Your instruction to use package.json { "type": "module" } are correct but ECMAScript modules are not supported by Lambda Node.js 14 runtime at this moment".

I will post an update to this post when I hear more about when support is available. I am leaving this question here just in case other people run into the same problem.

like image 190
user3180997 Avatar answered Oct 23 '22 10:10

user3180997


It appears that since yesterday there finally is native support for the ES6 module syntax in Node 14 lambdas - see https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda

like image 44
Marces Engel Avatar answered Oct 23 '22 11:10

Marces Engel


This worked for me on Lambda Node 14.x -

in app.js

exports.lambdaHandler = async (event, context) => {
  const { App } = await import('./lib/app.mjs');
  return new App(event, context);
}

And then in lib/app.mjs -

class App {

  constructor(event, context) {
    return {
      'statusCode': 200,
      'body': JSON.stringify({
        'message': 'hello world'
      })
    }
  }
} 

export {App}
like image 22
Dan Kantor Avatar answered Oct 23 '22 12:10

Dan Kantor


AWS Lambda does not official support ESM, but with the following workarounds it works smoothly.

This answer is a sum up of different workarounds inspired by answers/comments of Evan Sosenko and Dan Kantor and some additional ideas by me. This includes some nicer handling for typescript projects, but parts of it can be used for plain javascript projects as well.

I assume the following:

  1. Node.js version 14 is used: FROM public.ecr.aws/lambda/nodejs:14
  2. Serverless containers are used: FROM public.ecr.aws/lambda/nodejs:14
  3. Local imports should work without file ending: import { success } from './utils/log';
  4. Imports from libs should use ESM syntax: import AWS from 'aws-sdk';
  5. The lambda is written in typescript: (.ts is file suffix).

(I also provide information for plain .js instead of typescript at the end)

Dockerfile

FROM public.ecr.aws/lambda/nodejs:14

# copy only package.json + package-lock.json 
COPY package*.json ./

# install all npm dependencies including dev dependencies
RUN npm install

# copy all files not excluded by .dockerignore of current directory to docker container
COPY .  ./

# build typescript
RUN tsc

# remove npm dev dependencies as they are not needed anymore
RUN npm prune --production

# remove typescript sources
# RUN rm -r src

# rename all .js files to .mjs except for handler.js
RUN find ./dist -type f -name -and -not -name "handler.js" "*.js" -exec sh -c 'mv "$0" "${0%.js}.mjs"' {} \;

# allow local imports without file ending - see: https://nodejs.org/api/esm.html
ENV NODE_OPTIONS="--experimental-specifier-resolution=node"

# set handler function
CMD ["dist/lambda/handler.handler"]

Explanation

As AWS Lambda only supports commonJS, the Lambda entrypoint is a commonJS file. This is specified by an empty package.json which overwrites the package.json from project root. As this file is empty it does not contain: "type":"module" and defaults all files in that folder and subfolders to commonJS. A commonJS file can access ESM files if the have .mjs extension, but as typescript compiles to .js I use some unix commands to rename all files mathching ".*js" after calling tsc. The handler.js has to stay ".js" so I rename it back from .mjs.
More about ".js/.mjs"

Folder structure

- src
- - lambda
- - - handler.ts (commonJS)
- - - package.json (contains only: `{}`)
- - app.ts (ESM)
- - services
- - - (other .ts files ESM)
- package.json (contains `{"type": "module"}`, but also other settings)
- Dockerfile
- tsconfig.json

handler.ts

exports.handler = async (event) => {
    const {App} = await import('../app.mjs');
    const app = new App();
    return await app.run(event);
};

app.ts

// example import library
import AWS from 'aws-sdk';
// example import local file
import {FileService} from './services/file.service';

class App {
  constructor() {
  }

  async run(event) {
     // write your logic here
     return {
       'statusCode': 200,
       'body': JSON.stringify({'message': 'hello world'})
     }
  }
}
export {App};

tsconfig.json

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "Node",
    "esModuleInterop": true,
    "declaration": true,
    "removeComments": true,
    "allowSyntheticDefaultImports": true,
    "target": "es6",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./src",
    "lib": [
      "es6",
      "dom"
    ]
  },
  "include": [
    "src/**/*"
  ]
}

Differences if not using typescript

Let's assume you are not using typescript, but javascript like mentioned in the question than change the following:

  • don't use tsc command in Dockerfile
  • don't have a tsconfig.json file
  • all files should be name *.js instead of *.ts
  • obviously don't use any typescript typing (in my example there are none)
like image 38
flohall Avatar answered Oct 23 '22 11:10

flohall