Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
blake-regalia opened this issue Feb 25, 2021 · 27 comments · Fixed by #18346
Closed

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

blake-regalia opened this issue Feb 25, 2021 · 27 comments · Fixed by #18346
Labels
@aws-cdk/aws-lambda-nodejs bug This issue is a bug. effort/small Small work item – less than a day of effort p2

Comments

@blake-regalia
Copy link

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:

    if (!/\.(jsx?|tsx?)$/.test(entry)) {

  2. Allow passing in the format option to esbuild:

    const esbuildCommand: string = [
    npx, 'esbuild',
    '--bundle', pathJoin(inputDir, this.relativeEntryPath),
    `--target=${this.props.target ?? toTarget(this.props.runtime)}`,
    '--platform=node',
    `--outfile=${pathJoin(outputDir, 'index.js')}`,
    ...this.props.minify ? ['--minify'] : [],
    ...this.props.sourceMap ? ['--sourcemap'] : [],
    ...this.externals.map(external => `--external:${external}`),
    ...loaders.map(([ext, name]) => `--loader:${ext}=${name}`),
    ...defines.map(([key, value]) => `--define:${key}=${value}`),
    ...this.props.logLevel ? [`--log-level=${this.props.logLevel}`] : [],
    ...this.props.keepNames ? ['--keep-names'] : [],
    ...this.relativeTsconfigPath ? [`--tsconfig=${pathJoin(inputDir, this.relativeTsconfigPath)}`] : [],
    ...this.props.metafile ? [`--metafile=${pathJoin(outputDir, 'index.meta.json')}`] : [],
    ...this.props.banner ? [`--banner='${this.props.banner}'`] : [],
    ...this.props.footer ? [`--footer='${this.props.footer}'`] : [],
    ].join(' ');


This is 🐛 Bug Report

@blake-regalia blake-regalia added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Feb 25, 2021
@jogold
Copy link
Contributor

jogold commented Feb 25, 2021

Hi @blake-regalia, does Lambda support mjs files? How? AFAIK it only supports CJS.

@blake-regalia
Copy link
Author

Yes in node.js >= v14.13 environment you can use esmodules.

@eladb eladb added effort/small Small work item – less than a day of effort p2 labels Feb 25, 2021
@eladb eladb removed their assignment Feb 25, 2021
@jogold
Copy link
Contributor

jogold commented Feb 25, 2021

Yes in node.js >= v14.13 environment you can use esmodules.

I understand that you want to use .mjs files as input and it's easy to update the regex there.

But regarding the output of esbuild I don't think Lambda knows how to deal with .mjs files so we'll always have to transpile to CJS, no?

@blake-regalia
Copy link
Author

blake-regalia commented Feb 25, 2021

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.

And more important than the file extension is getting away from transpilation entirely since this breaks the use of import.meta.

@jogold
Copy link
Contributor

jogold commented Feb 26, 2021

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.

@ronaldocpontes
Copy link

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

Could someone publish a working example of using .mjs?

@ronaldocpontes
Copy link

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

@ronaldocpontes
Copy link

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)"
  ]
}

@revmischa
Copy link
Contributor

revmischa commented Apr 24, 2021

I would like to load a "type": "module" package in CDK when building my aws-lambda-nodejs Lambda, even if it's being eventually output as commonJS. There should be some way to set it up so esbuild is doing an import instead of require() somewhere

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/index.js
require() of ES modules is not supported.
require() of /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/index.js from /Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/lib/cdk-stack.ts 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 /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/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 Object.<anonymous> (/Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/lib/cdk-stack.ts:9:1)
    at Module._compile (internal/modules/cjs/loader.js:1063:30)
    at Module.m._compile (/Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/node_modules/ts-node/src/index.ts:1056:23)
    at Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Object.require.extensions.<computed> [as .ts] (/Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/node_modules/ts-node/src/index.ts:1059:12)

My motivation is to use esbuild's tree-shaking. Right now my function is very large and appears to contain all of @aws-cdk and typeorm and others, so I am very sure tree shaking is not working correctly.
The esbuild docs say:

Note that esbuild's tree shaking implementation relies on the use of ECMAScript module import and export statements. It does not work with CommonJS modules.

But I can see that aws-lambda-nodejs is not possibly doing this because of the above error and the require stack here is using the cjs not ejs version of the @jetkit/cdk module:

Require stack:
- /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/cjs/cdk/lambda/node14func.js
- /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/cjs/cdk/api/api.js
- /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/cjs/cdk/api/crud.js
- /Users/cyber/dev/jb/jkv2-demo/cdk/node_modules/@jetkit/cdk/build/cjs/index.js
- /Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/lib/cdk-stack.ts
- /Users/cyber/dev/jb/jkv2-demo/cdk/packages/infra/bin/cdk.ts
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:880:15)

I believe if esbuild was configured to support ES-modules then we could get much much smaller function bundles. As it is now it appears to bundle entire packages:
Screen Shot 2021-04-24 at 9 09 36 PM

@jogold
Copy link
Contributor

jogold commented Apr 26, 2021

Right now my function is very large and appears to contain all of @aws-cdk and typeorm and others,

This is really strange... can you share a repro? Are those dependencies imported in your handler? If yes, why? If not, they shouldn't be bundled.

@revmischa
Copy link
Contributor

Right now my function is very large and appears to contain all of @aws-cdk and typeorm and others,

This is really strange... can you share a repro? Are those dependencies imported in your handler? If yes, why? If not, they shouldn't be bundled.

My handler includes @jetkit/cdk which is what I'm trying to build right now. The construct I'm using is https://github.com/jetbridge/jetkit-cdk/blob/implement-view-routing/src/cdk/generator.ts and the app code is https://github.com/jetbridge/jetkit-cdk/blob/implement-view-routing/src/test/sampleApp.ts

@jogold
Copy link
Contributor

jogold commented Apr 26, 2021

My handler includes @jetkit/cdk which is what I'm trying to build right now

Can you show your handler? Is it a .ts file?

@revmischa
Copy link
Contributor

Yes, https://github.com/jetbridge/jetkit-cdk/blob/implement-view-routing/src/test/sampleApp.ts is the handler file. It's the handler export

export const handler = apiViewHandler(__filename, AlbumApi)

apiViewHandler is defined here: https://github.com/jetbridge/jetkit-cdk/blob/5d3adb8dbf732fdc1d968b5d5486ad2ec433a0fd/src/api/base.ts#L219

I'm trying to make some decorators that a CDK construct can make use of to generate NodeJsLambda functions and APIGW routes automatically

@Sparticuz
Copy link

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

@Mystogab
Copy link

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?

@groner
Copy link

groner commented Sep 22, 2021

Here's a possible approach to running ES modules in lambda today (without the benefit of NodejsFunction)

The following stackoverflow answer claims success running ES modules with a shim using dynamic import().

From https://stackoverflow.com/a/69136438/91927

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}

@groner
Copy link

groner commented Sep 22, 2021

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;
    }
  }
}

@intptr-t
Copy link

intptr-t commented Nov 9, 2021

Since there seems to be no progress, I will give my personal opinion.
There are two issues, which I think should be resolved independently of each other.

1. .mjs is not supported.

It should be legitimate for esbuild to output *.mjs and *.cjs as format=cjs.
I think it is reasonable that this be resolved by adding a feature (label=feature-request) to aws-lambda-nodejs in the CDK.

  1. add a regular expression to \. (c|m)?jsx?|(c|m)?tsx?$
  2. Allow passing in the format option to esbuild:, as shown by blake-regalia.

2. import.meta is not supported

The CDK is not concerned about the AWS Lambda runtime fully supporting ES2020 features (e.g. import.meta).
So it's better to wait until AWS Lambda supports it, or build it yourself using a custom runtime.

@jogold
Copy link
Contributor

jogold commented Jan 7, 2022

https://aws.amazon.com/about-aws/whats-new/2022/01/aws-lambda-es-modules-top-level-await-node-js-14/

https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/

@adamdottv
Copy link
Contributor

@jogold
Copy link
Contributor

jogold commented Jan 9, 2022

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.

jogold added a commit to jogold/aws-cdk that referenced this issue Jan 10, 2022
Add a `format` option to choose the output format (CommonJS or
ECMAScript module).

Generate a `index.mjs` file when the ECMAScript module output format
is chosen so that AWS Lambda treats it correctly.

Closes aws#13274
@jogold
Copy link
Contributor

jogold commented Jan 10, 2022

See #18346

@mergify mergify bot closed this as completed in #18346 Jan 11, 2022
mergify bot pushed a commit that referenced this issue Jan 11, 2022
Add a `format` option to choose the output format (CommonJS or
ECMAScript module).

Generate a `index.mjs` file when the ECMAScript module output format
is chosen so that AWS Lambda treats it correctly.

See https://aws.amazon.com/about-aws/whats-new/2022/01/aws-lambda-es-modules-top-level-await-node-js-14/
See https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/

Closes #13274

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@github-actions
Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@pfried
Copy link

pfried commented Jan 27, 2022

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

TikiTDO pushed a commit to TikiTDO/aws-cdk that referenced this issue Feb 21, 2022
Add a `format` option to choose the output format (CommonJS or
ECMAScript module).

Generate a `index.mjs` file when the ECMAScript module output format
is chosen so that AWS Lambda treats it correctly.

See https://aws.amazon.com/about-aws/whats-new/2022/01/aws-lambda-es-modules-top-level-await-node-js-14/
See https://aws.amazon.com/blogs/compute/using-node-js-es-modules-and-top-level-await-in-aws-lambda/

Closes aws#13274

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@paul-uz
Copy link

paul-uz commented Mar 31, 2022

@pfried damn, thats exactly the issue I hit. aws/aws-sdk-js-v3#3386

@pfried
Copy link

pfried commented Dec 14, 2022

@paul-uz I opened an issue here: #23333

@paul-uz
Copy link

paul-uz commented Dec 14, 2022

@paul-uz I opened an issue here: #23333

I believe this now works in node 18 on lambda

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-lambda-nodejs bug This issue is a bug. effort/small Small work item – less than a day of effort p2
Projects
None yet
Development

Successfully merging a pull request may close this issue.