project with Lerna and NPM workspaces


3 min read

Hello. I'm writing a Lambda "tick" function to simulate the universe for my game. It's a different architecture than my previous idea of maintaining a 24/7 server, which simulates a busy real-time game loop. I wanted to give it a try.

In this post, I want to share the project structure I used, which consists of Lerna, and two packages: one for the game's logic and one for the framework for deploying the Lambda. I had some fun with dependencies and Lambda not finding the game logic's dependency.

Project Structure

Here's what my project looks like:

├── lerna.json
├── package-lock.json
├── package.json
└── packages
    ├── logic
    │   ├── build
    │   │   ├── index.d.ts
    │   │   └── index.js
    │   ├── package-lock.json
    │   ├── package.json
    │   ├── src
    │   │   └── index.ts
    │   └── tsconfig.json
    └── serverless
        ├── functions
        │   └── handler.ts
        ├── package.json
        ├── serverless.yml
        └── tsconfig.json

Game Logic Package Setup

Nothing too interesting here. I decided to understand CJS modules and ES modules' output this time. Targeting the Node 18.x runtime by AWS Lambda, I saw that ES modules are supported and decided to go with the future of everything by making TypeScript emit an ES module. It looks something like this:

# PARTIAL tsconfig.json compiler options
  "target": "ES2016",
  "module": "Node16",
  "declaration": true,
  "outDir": "./build"
# PARTIAL package.json, scripts/license/boring parts omitted
  "name": "@ags9/logic",
  "exports": {
    "import": "./build/index.js"
  "types": "./build/index.d.ts",
  "type": "module",
  "scripts": {
    "test": "jest",
    "build": "tsc -p .",
    "start": "node build/index.js",
    "dev": "tsc -w -p ."

Note the "type": "module" which tells Node to run .js files as ES modules. "types" for letting the importer of this package know it has nice types ("declaration": true in tsconfig.json, important for type-safety in the Lambda handler). The exports.import field is for telling ES module importers of @ags9/logic what to load. Notice the matching "outDir" path in tsconfig.json.

Serverless Setup

I decided against using the aws-nodejs-typescript serverless template as it was too complicated and outdated. Instead, I used the regular aws-nodejs template and installed this serverless-typescript plugin for TypeScript support.

The interesting setup here would be two key aspects. First, the tsconfig.json which references the projects' packages. Second, the patterns for including/excluding the right files from the Lambda package by Serverless.

How did I find out nothing works and why?

I used sls package, which gave me a zip file under the .serverless output folder. Examining the contents of the zip file, I noticed that no node_modules existed there, especially my @ags9/logic package. I then tried some combinations for the package.patterns in serverless.yml and eventually nailed the correct configuration, which looks like this:

# part of serverless.yml

  individually: true
    - ../../node_modules/@ags9/**
    - "!../../node_modules/@ags9/*/src/**"
    - "!../../node_modules/@ags9/serverless/**"

It's a bit confusing that in my filesystem I omitted @ags9/ from the path, but inside node_modules, the correct package name from the package.json is used, e.g. ags9/logic.

Next Steps For Me

  1. Install regular NPM packages in both logic and serverless and ensure they are packaged correctly by Serverless.

  2. Build a game.