'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.



Solution 1:[1]

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.

Solution 2:[2]

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}

Solution 3:[3]

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)

Solution 4:[4]

try with semicolons like:

=INDEX(E2:J3; 2; 3)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 user3180997
Solution 2 Dan Kantor
Solution 3
Solution 4 player0