Skip to content

Instantly share code, notes, and snippets.

@heitorlessa
Last active April 25, 2023 09:01
Show Gist options
  • Save heitorlessa/a087f4394b38562e1a0aa128386b38b8 to your computer and use it in GitHub Desktop.
Save heitorlessa/a087f4394b38562e1a0aa128386b38b8 to your computer and use it in GitHub Desktop.
Webpack+SAM+Typescript example

Thread: https://twitter.com/heitor_lessa/status/1137361365818060800

  • Local prototyping with hot-reloading and step through debugging: npm run watch
  • Bundle, minify and valid structure for SAM package/deploy: npm run build

TL;DR

  • Webpack is instructed to dynamically look up for index.ts within src folder -- (src/function1/index.ts, src/function2/index.ts)
  • Webpack builds a bundle per function to build/function1/index.js, build/function2/index.js...
  • SAM CodeUri is set to the build folder build/function1
  • VS Code Debugging uses one entry per function pointing to the local bundle ("localRoot": "${workspaceRoot}/build/ingest")
  • VS Code uses both Source Map and the outFiles directives to map breakpoints back to the original index.ts file

With this structure, it allows an organic growth in the number of functions within a project without bothering with the tooling boilerplate. It auto discovers, minifies, bundles and transpiles every function that has index.ts -- <new_function>/index.ts

Current structure

.
├── build
│   ├── get
│   │   ├── index.js
│   │   └── index.js.map
│   └── ingest
│       ├── index.js
│       └── index.js.map
├── jest.config.js
├── local-env-vars.json
├── package-lock.json
├── package.json
├── src
│   ├── get
│   │   ├── event.json
│   │   ├── index.ts
│   │   └── lib
│   │       ├── document_client.js
│   │       └── document_client.ts
│   └── ingest
│       ├── event.json
│       ├── index.ts
│       └── lib
│           ├── document_client.js
│           └── document_client.ts
├── template.yaml
├── tests
│   ├── get.test.ts
│   └── ingest.test.ts
├── tsconfig.json
└── webpack.config.js

Step through debugging

  1. Open up VS Code at aws-serverless-airline-booking/src/backend/loyalty: cd aws-serverless-airline-booking/src/backend/loyalty && code .

  2. That should ensure ${workspaceRoot} is correctly set. If SAM TS Webpack isn't shown in the Debugger drop down menu, create a new one by copying/pasting launch.json contents.

  3. Add a breakpoint within src/ingest/index.ts

  4. Run sam local invoke IngestFunc -e src/ingest/event.json -d 5858

  5. Switch to index.ts within VS Code and hit the debugger - Profit!

Troubleshooting

Reasons why Step through debugging wasn't working:

Missing libraryTarget: commonjs2 instruction for Webpack

Default bundle output wasn't compatible with what Lambda NodeJS Runtime expects, and therefore the error: {"errorMessage":"Handler 'handler' missing on module 'index'"}

Missing outFiles VSCode Debug option

outFiles instructs VSCode that code being debugged has been transpilled to a separate location. That alongside with sourceMaps: true makes it possible for VSCode to know where to circle back for the correct line in the breakpoint, and what file to find the source code now transpiled.

// content of src/ingest/index.ts
import { Context, SNSEvent } from 'aws-lambda';
import { DefaultDocumentClient, DocumentClientInterface } from './lib/document_client';
import uuidv4 from 'uuid/v4';
const client = DefaultDocumentClient;
const table = process.env.TABLE_NAME
/**
* Result interface
*/
interface Result {
/**
* Message
*/
message: string;
}
/**
* LoyaltyPoints interface
*/
interface LoyaltyPoints {
/**
* Identifier
*/
Id: string;
/**
* Customer ID
*/
CustomerId: string;
/**
* Points
*/
Points: number;
/**
* DAte
*/
Date: string;
/**
* Flag
*/
Flag: LoyaltyStatus;
}
/**
* Loyalty Status
*/
enum LoyaltyStatus {
/**
* Active
*/
Active = "active",
/**
* Revoked
*/
Revoked = "revoked",
/**
* Expired
*/
Expired = "expired"
}
/**
* Add loyalty points to a given customerID
*
* @param {string} customerId - customer unique identifier
* @param {number} points - points that should be added to the customer
* @param {DocumentClient} dynamo - AWS DynamoDB DocumentClient
*/
export const addPoints = async (customerId: string, points: number, client: DocumentClientInterface, tableName: string) => {
const item: LoyaltyPoints = {
Id: uuidv4(),
CustomerId: customerId,
Points: points,
Flag: LoyaltyStatus.Active,
Date: new Date().toISOString()
};
let params = {
TableName: tableName,
Item: item as Object
}
try {
await client.put(params).promise();
} catch (e) {
throw new Error(`Unable to write to DynamoDB: ${e}`);
}
}
/**
* Lambda Function handler that takes one SNS message at a time and add loyalty points to a customer
* While SNS does send records in an Array it only has one event
* That means we're safe to only select the first one (event.records[0])
*
* @param {SNSEvent} event
* @param {Context} context
* @returns {Promise<Result>}
*/
export async function handler(event: SNSEvent, context: Context): Promise<Result> {
let message: string = "debugging...watch mode 3"
console.log(message)
if (!table) {
throw new Error(`Table name not defined`);
}
try {
const record = JSON.parse(event.Records[0].Sns.Message);
const customerId = record['customerId'];
const points = record['price'];
await addPoints(customerId, points, client, table)
} catch (error) {
throw error
}
return {
message: "ok!",
}
}
module.exports = {
roots: [
"./tests",
"./src"
],
transform: {
"^.+\\.tsx?$": "ts-jest"
}
}
{
"version": "0.2.0",
"configurations": [
{
"name": "SAM TS Webpack",
"type": "node",
"request": "attach",
"address": "localhost",
"port": 5858,
// Location to where the transpiled JS file is: follows CodeUri
"localRoot": "${workspaceRoot}/build/ingest",
"remoteRoot": "/var/task",
"protocol": "inspector",
"stopOnEntry": false,
// Same as LocalRoot given we run on a docker container
// outFiles allows VSCode debugger to know where the source code is after finding its sourceMap
"outFiles": [
"${workspaceRoot}/build/ingest/index.js"
],
// instructs debugger to use sourceMap to identify correct breakpoint line
// and more importantly expand line/column numbers correctly as code is minified
"sourceMaps": true
}
]
}
{
"name": "loyalty",
"version": "1.0.0",
"description": "Loyalty service to collection airmiles",
"dependencies": {
"@types/aws-lambda": "^8.10.26",
"@types/node": "^12.0.2",
"@types/uuid": "^3.4.4",
"aws-sdk": "^2.464.0",
"aws-sdk-mock": "^4.4.0",
"typescript": "^3.4.5",
"uuid": "^3.3.2"
},
"devDependencies": {
"@types/jest": "^24.0.13",
"jest": "^24.8.0",
"ts-jest": "^24.0.2",
"ts-loader": "^6.0.2",
"tslint": "^5.17.0",
"typescript": "^3.5.1",
"webpack": "^4.33.0",
"webpack-cli": "^3.3.3"
},
"scripts": {
"test": "jest",
"watch": "NODE_ENV=dev webpack --watch --mode=development",
"build": "NODE_ENV=prod webpack --mode=production",
"lint": "tslint --project tsconfig.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aws-samples/aws-serverless-airline-booking.git"
},
"author": "Simon Thulbourn <thulsimo@amazon.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/aws-samples/aws-serverless-airline-booking/issues"
},
"homepage": "https://github.com/aws-samples/aws-serverless-airline-booking#readme"
}
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
loyalty service example
Globals:
Function:
Timeout: 100
Parameters:
BookingSNSTopic:
Type: AWS::SSM::Parameter::Value<String>
Description: Booking SVC SNS Topic
Resources:
LoyaltyDataTable:
Type: AWS::DynamoDB::Table
Properties:
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: CustomerId
AttributeType: S
- AttributeName: Flag
AttributeType: S
- AttributeName: Id
AttributeType: S
KeySchema:
- AttributeName: Id
KeyType: HASH
GlobalSecondaryIndexes:
- IndexName: customer-flag
KeySchema:
- AttributeName: CustomerId
KeyType: HASH
- AttributeName: Flag
KeyType: RANGE
Projection:
ProjectionType: ALL
SSESpecification:
SSEEnabled: yes
IngestFunc:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/ingest
Handler: index.handler
Runtime: nodejs8.10
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref LoyaltyDataTable
Environment:
Variables:
DATA_TABLE_NAME: !Ref LoyaltyDataTable
Events:
Listener:
Type: SNS
Properties:
Topic: !Ref BookingSNSTopic
{
"include": [
"./src/**/*.ts"
],
"compilerOptions": {
"target": "es2017",
"module": "commonjs",
"strict": true,
"baseUrl": "./",
"typeRoots": [
"node_modules/@types"
],
"types": [
"node",
"jest"
],
"esModuleInterop": true,
"inlineSourceMap": true,
"resolveJsonModule": true
}
}
const path = require('path');
const glob = require('glob');
// Credits: https://hackernoon.com/webpack-creating-dynamically-named-outputs-for-wildcarded-entry-files-9241f596b065
const entryArray = glob.sync('./src/**/index.ts');
const entryObject = entryArray.reduce((acc, item) => {
let name = path.dirname(item.replace('./src/', ''))
// conforms with Webpack entry API
// Example: { ingest: './src/ingest/index.ts' }
acc[name] = item
return acc;
}, {});
module.exports = {
entry: entryObject,
devtool: 'source-map',
target: "node",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
// Output directive will generate build/<function-name>/index.js
output: {
filename: '[name]/index.js',
path: path.resolve(__dirname, 'build'),
devtoolModuleFilenameTemplate: '[absolute-resource-path]',
// credits to Rich Buggy!!!
libraryTarget: 'commonjs2'
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment