Building Apps with SAM, TypeScript and VS Code Debugging

Building Apps with SAM, TypeScript and VS Code Debugging

I've been wanting to switch from the Serverless Framework to SAM for a long time now. While the Serverless Framework has been an excellent tool I only use AWS and they have good tooling/support for SAM that will only get better. Until now the major roadblock has been the lack of TypeScript support.

Over the last few day I've spent a lot of time reading the documentation for NPM, Webpack, TypeScript, SAM; looking at the SAM source code and messaging with Heitor Lessa who was also trying to solve the same problem. Between the two of us we've managed to solve the problem in slightly different ways.

This article describes my solution.

My number one requirement was to have something that worked with SAM build/package/deploy. TypeScript support for the Serverless Framework uses the serverless-webpack plugin so my first thought was writing a Node.js/Webpack builder for SAM.

Step 1: Building the app with Webpack

My first problem was building the app with Webpack. To do this I removed the existing devDependencies from the package.json in my functions folder (hello-world) then I added all of the packages I would need to compile a project using Webpack to the devDependencies.

npm install @babel/core @babel/preset-env @types/aws-lambda babel-loader ts-loader typescript webpack webpack-cli webpack-node-externals --save-dev

Knowing that I wanted source map support I also added a dependency for

npm install source-map-support

With that done I added a webpack.config.js to build the project.

const nodeExternals = require("webpack-node-externals");

module.exports = {
  devtool: "source-map",
  resolve: {
    extensions: [".js", ".ts"],
  },
  output: {
    libraryTarget: "commonjs2",
  },
  target: "node",
  externals: [nodeExternals()],
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
          },
        },
      },
      {
        test: /\.ts?$/,
        loader: "ts-loader",
      },
    ],
  },

  mode: "development",
};

This will use Babel to build .js files and TypeScript for .ts files.

You may have noticed that I'm only using Webpack to compile my code and that all of the NPM dependencies remain in the node_modules folder because the webpack-node-externals plugin is declaring them all as external. There were a few things that went into this decision and it's the single biggest difference between the two solutions that Heitor and I currently have.

The main reason I'm doing this is that compatibility with the sam build/package/deploy process was high on my wish list. When you run sam build it performs an npm pack to move the files into a new build folder then it runs npm install because npm pack doesn't copy the node_modules folder. This is very unforunate because it undoes all of the advantages of using tree shaking in Webpack by adding all of the dependencies back into the deployment package. It would have been much better if SAM told people to use bundledDependencies for any dependencies you want to include in the deployment package as npm pack does copy those. This would have removed the need to perform an npm install.

A secondary reason for doing this is that not all NPM packages are compatible with Webpack.

I also added a tsconfig.json to the hello-world folder with the configuration for TypeScript.

{
  "compilerOptions": {
    "target": "es2015",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": true,
    "sourceMap": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*"]
}

Next I created a src folder inside the hello-world folder then moved the existing app.js into it, converting it to a TypeScript file along the way.

With that done I could now compile my code into a build folder by running Webpack from inside the hello-world folder.

npx webpack-cli src/app.ts -c webpack.config.js -o build/app.js

After confirm it worked I added two scripts to my package.json.

"watch": "webpack-cli src/app.ts -c webpack.config.js -o build/app.js -w",
"webpack": "webpack-cli src/app.ts -c webpack.config.js -o build/app.js",

Now go to your template.yaml file and change the Handler for your function from app.lambdaHandler to build/app.lambdaHandler.

Step 2: What about tests?

Jest is my preferred test framework and I know it works with Webpack. The first step was to add a few more devDependencies to my package.json.

npm install @types/jest jest ts-jest --save-dev

Then I added a jest.config.js into the hello-world folder.

module.exports = {
  preset: "ts-jest",
  testEnvironment: "node",
};

Finally I moved the old test folder into src, renaming it to __tests__, and converted the file in it to TypeScript using Jest.

After confirming the tests worked when I ran npx jest I updated the "test" script in my package.json to execute jest.

Step 3: Making it work with SAM build

SAM uses the npm pack command to build the package. Running Webpack to build the project should have been as simple as renaming the "webpack" script to "prepack" so that NPM runs it before performing the pack. While this approach works if you're running npm pack manually it fails when you run sam build. At some point I'll investigate this problem further but for now I've added a build.sh script into the root of the project which executes npm run-script webpack in any folder immediately below the project root if it contains both a package.json and webpack.config.js to build all of my functions using Webpack.

#!/bin/sh

ROOT_DIR=$PWD

for dir in *; do
  if [ -d $ROOT_DIR/$dir ] && [ -f $ROOT_DIR/$dir/package.json ] && [ -f $ROOT_DIR/$dir/webpack.config.js ]
  then
    echo $dir
    cd $ROOT_DIR/$dir
    npm run-script webpack
  fi
done

To keep the source, tests and config files out of the final Lambda package I also modified the .npmignore in the hello-world folder to exclude those

src/*
jest.config.js
tsconfig.json
webpack.config.js

Finally I could now build the project using

./build.sh && sam build

Step 4: Debugging in VS Code

I've previously written about debugging Node.js Lambda with AWS SAM local and VS Code which covers how to use the VS Code debugger. Instead of repeating how to use the VS Code debugger I'm going to focus on the differences you need to know for TypeScript.

If you didn't include source-map-support as a dependency in step 1 you'll need to add it. You'll also need to include it at the top of your handler file.

import "source-map-support/register";

Note: I had to increase the memory for my Lambda after doing this or my Lambda would fail without reporting an error when something went wrong due to running out of memory when generating new stack traces.

The only difference in my launch.json is the addition of the sourceMapPathOverrides.

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "hello-world",
      "type": "node",
      "request": "attach",
      "address": "localhost",
      "port": 5858,
      "localRoot": "${workspaceRoot}/hello-world",
      "remoteRoot": "/var/task",
      "protocol": "inspector",
      "stopOnEntry": false,
      "sourceMapPathOverrides": {
        "webpack:///./~/*": "${workspaceRoot}/hello-world/node_modules/*",
        "webpack:///./*": "${workspaceRoot}/hello-world/*",
        "webpack:///*": "*"
      }
    }
  ]
}

Adding that allows setting breakpoints in the .ts file.

What next?

If you followed the article you should have

  • SAM + TypeScript + VS Code debugging working.

  • SAM build/package/deploy almost working exactly the same as plain Node (remember to run build.sh).

  • You can debug using both sam invoke and sam start-api.

  • If you run npm run-script watch inside your function folder it will automatically recompile when you make changes to the source and the API reloading will work too.

There are some areas I would like to improve over the coming weeks.

  • The development dependencies are duplicated in each function which is inefficient.

  • You need to run npm run-script watch for every function you want to rebuild automatically.

Beyond that I've created an issue to add a Webpack Lambda builder for SAM. Hopefully with a dedicated builder the remaining issues can be fixed including full support for tree shaking.

The full source for this as available on GitHub. I want to give a big thanks to Heitor Lessa for his help.

If you want updates to this then follow me on Twitter and join the mailing list.