aws-cdk: (aws-lambda-nodejs): `.mjs` file extension and `import.meta` not supported

Although Lambda now supports Node v14:

  1. .mjs entry files are rejected by a regex pattern in aws-lambda-nodejs
  2. esbuild is transpiling the scripts to cjs format (commonjs) but BundlingOptions provides no means to specify format: "esm", and consequently esbuild polyfills import.meta which breaks all of its uses in the scripts

Reproduction Steps

lib/my-stack.mjs:

export class MyStack extends Stack {
    constructor(scope, id, props) {
        super(scope, id, props);

        // ...

        new NodejsFunction(this, 'Example', {
            runtime: Runtime.NODEJS_14_X,
            entry: 'src/entry-file.mjs',
            bundling: {
                target: 'es2020',
                // format: 'esm',   <-- should be able to pass this option here
            },
        };

        // ...
    }
}

src/entry-file.mjs:

export async function Example() {
    return import.meta.url;
}

What did you expect to happen?

  1. entry-file.mjs to be allowed to be used as an entry file.
  2. import.meta to be defined and Example() to return a string.

What actually happened?

  1. Error: Only JavaScript or TypeScript entry files are supported.
  2. Example() returns undefined since import.meta is polyfilled with an empty plain object.

Environment

  • CDK CLI Version : n/a
  • Framework Version:
  • Node.js Version: 14.13.0
  • OS : n/a
  • Language (Version):

Other

  1. For the .mjs extension: https://github.com/aws/aws-cdk/blob/5d71d8e815529ccde7ffe13bb72f57ae88e896dc/packages/%40aws-cdk/aws-lambda-nodejs/lib/function.ts#L140

  2. Allow passing in the format option to esbuild: https://github.com/aws/aws-cdk/blob/5d71d8e815529ccde7ffe13bb72f57ae88e896dc/packages/%40aws-cdk/aws-lambda-nodejs/lib/bundling.ts#L141-L158


This is šŸ› Bug Report

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 6
  • Comments: 27 (12 by maintainers)

Commits related to this issue

Most upvoted comments

This is what I get when using ā€œtypeā€: ā€œmoduleā€ on package.json. As you can see on the error: require() of ES modules is not supported. require() of /var/task/index.js…

The lambda engine is using require() to load the code on index.js.

# package.json
{
"name": "myLambda",
"type": "module",
"main": "index.js",
}
# index.js
export const handler = async (event) => {  
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda!'),
    };
    return response;
};
Response
{
  "errorType": "Error",
  "errorMessage": "Must use import to load ES Module: /var/task/index.js\nrequire() of ES modules is not supported.\nrequire() of /var/task/index.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 index.js to end in .cjs, change the requiring code to use import(), or remove \"type\": \"module\" from /var/task/package.json.\n",
  "trace": [
    "Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /var/task/index.js",
    "require() of ES modules is not supported.",
    "require() of /var/task/index.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 index.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)"
  ]
}

I believe the problem is that the lambda engine is using CommonJS require to load the handler method. This won’t work if the lambda handler is an ES6 module or a .mjs file as it will fall in the error quadrant on the table below.

So the question is: how do we use nodejs 14.x out-of-the-box ES6 modules on AWS Lambdas?

https://pencilflip.medium.com/using-es-modules-with-commonjs-modules-in-node-js-1015786dab03 image

I don’t know where you are getting this assumption about Lambda not knowing how to deal with .mjs files; I must admit I don’t know exactly how it is done currently but I don’t see why this would cause any issues. As long as the environment is the correct version of node.js, it should be able to run mjs files in module mode.

Tried in the Lambda console and I was not able to do this. If you manage to successfully run files in module mode in the Lambda environment, let me know how you did it.

According to this SO question, the Node on Lambda doesn’t support ecmascript modules yet. I would love to see this happen ASAP. All of Sindre Sorhus’ packages are currently moving to ESM only! https://blog.sindresorhus.com/hello-modules-d1010b4e777b

I am also getting errors on a new lambda with node 14.x.

Could someone publish a working example of using .mjs?

ame here to see if this was in the works; @jogold are you planning to add support for native esm?

I will try to have a look at it this week or next week.

Not an aws-cdk Issue, but for everyone who ends up here: Using modules form layers is currently not supported in lambda when using ESM

The following code in aws/aws-lambda-nodejs-runtime-interface-client looks like it could be adapted to use import() and import.meta.resolve() without much trouble.

From https://github.com/aws/aws-lambda-nodejs-runtime-interface-client/blob/f9373b3e7cb153fe2895db446e21f3241eb156c6/src/utils/UserFunction.ts#L70-L111

/**
 * Attempt to load the user's module.
 * Attempts to directly resolve the module relative to the application root,
 * then falls back to the more general require().
 */
function _tryRequire(appRoot: string, moduleRoot: string, module: string): any {
  const lambdaStylePath = path.resolve(appRoot, moduleRoot, module);
  if (_canLoadAsFile(lambdaStylePath)) {
    return require(lambdaStylePath);
  } else {
    // Why not just require(module)?
    // Because require() is relative to __dirname, not process.cwd()
    const nodeStylePath = require.resolve(module, {
      paths: [appRoot, moduleRoot],
    });
    return require(nodeStylePath);
  }
}


/**
 * Load the user's application or throw a descriptive error.
 * @throws Runtime errors in two cases
 *   1 - UserCodeSyntaxError if there's a syntax error while loading the module
 *   2 - ImportModuleError if the module cannot be found
 */
function _loadUserApp(
  appRoot: string,
  moduleRoot: string,
  module: string
): any {
  try {
    return _tryRequire(appRoot, moduleRoot, module);
  } catch (e) {
    if (e instanceof SyntaxError) {
      throw new UserCodeSyntaxError(<any>e);
    } else if (e.code !== undefined && e.code === "MODULE_NOT_FOUND") {
      throw new ImportModuleError(e);
    } else {
      throw e;
    }
  }
}

Any updates on this? I’m on a mayor refactor on a curtial step of a pure JS project ( I wanted to switch to TS but was too agressive to suggest all changes at once). So if I use nodejs 14.x runtime, then export const handler wont work?