Hidden Sam CLI features
2023-01-26
⚠️ Everything in this post is subject to change. I'm also not employed by AWS and don't work on the Sam CLI source code.
Recently I've been in a position of using the AWS Sam CLI a lot. As a result of digging around the source code and the Lambda builder, I've found some undocumented features. By the phrase undocumented, I mean it doesn't appear on the documentation as of 2023/01/26.
A short bit of context, I'm currently working on NodeJS functions written in Typescript deployed on AWS Lambda using the AWS Sam CLI which features the bundler esbuild.
Feature 1: Format
If you want to use something like Top level await in Lambda you'll need to output your code in the esModule (esm) format. You might want to use this to utilise the init stage of the Lambda function to execute a task.
According to AWS, top-level await is available for NodeJS Lambdas as of 2022. However, if you use Typescript, esbuild and Sam CLI, as suggested by AWS and the default target field of es2020
, you will come across issues in the build stage. The error will be something like
Top-level await is not available in the configured target environment
To get around these build issues you can set your target to be Node18
. This is documented.
You can also set a new property - Format: esm
. This will ensure the output of esbuild is in the esm format as needed top-level await as there is no current support for CommonJS.
Solution? After digging through the source code on GitHub I found that you can set the format
output for esbuild. Here's an example CloudFormation template for a Lambda function: full example here
myFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handler.lambdaHandler
CodeUri: ./
Runtime: nodejs18.x
FunctionName: "MyTestFunction"
Metadata: # Manage esbuild properties
BuildMethod: esbuild
BuildProperties:
Target: "node18"
Format: esm # Undocumented
EntryPoints:
- src/handler.ts
Feature 2: OutExtension
Setting the format type for esbuild to use is only 50% of the problem. The code compiles now. Deploy it and you may see it still doesn't work. The error might look something like this:
await is only valid in async functions and the top level bodies of modules
But the code compiled and we've explicitly set the output to be a module. Javascript is ... funny. The problem here is that the run time hasn't been told it's using esmodules and it will usually find this information out one of two ways.
- Does the
package.json
have atype: module
JSON property? - Is the file extension a
.mjs
type?
We could change the package.json
but alas, doesn't fix our issue. This is because the Sam CLI omits the package.json
from the deployment. You don't need it to run a Lambda. This is where you can add an OutExtension
property to your CloudFormation template.
Take the example before and add the OutExtension
property.
Resources:
myFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handler.lambdaHandler
CodeUri: ./
Runtime: nodejs18.x
FunctionName: "MyTestFunction"
Metadata: # Manage esbuild properties
BuildMethod: esbuild
BuildProperties:
Target: "node18"
Format: esm # Undocumented
OutExtension: # Undocumented
- .js=.mjs
Sourcemap: true
EntryPoints:
- src/handler.ts
Esbuild will now output the .mjs
files instead of js
.
Feature 3: npm ci
npm ci
is a way to do a clean install of your dependencies.
To get this to run you need to add the following to your BuildProperties
like the following
BuildProperties:
Target: "node18"
Format: esm # Undocumented
UseNpmCi: true # Undocumented
OutExtension: # Undocumented
- .js=.mjs
Sourcemap: true
EntryPoints:
- src/handler.ts