Tips and Tricks for Developing CDK Construct using Projen

Projen is an essential tool for creating CDK constructs. But it is opinionated. We will look into tips and tricks on how to overcome its constraints. Tuesday, October 10, 2023

This article is part of a series that looks at solutions for improving CloudWatch Alarms for Lambda errors:

  1. Improving CloudWatch Alarms for Lambda Errors
  2. Tips and Tricks for Developing CDK Construct using Projen (here)
  3. CDK Escape Hatches + How to Export CDK to CloudFormation

You can find a full solution here.

In the first part of this series, we looked into what the solution does. Now, we will look at how we build a CDK construct.

Projen

Projen is a multipurpose project generator. Among other things, it can be used for building a CDK construct.

Why would you need a special tool like Projen for creating a CDK construct?

Creating a CDK construct is as easy as extending a Construct class.

This is a sample of code from my previous article:

export class LambdaErrorSnsSender extends Construct {
  constructor(scope: Construct, id: string, props: LambdaErrorSnsSenderProps) {
    super(scope, id);
    …
  }
}

If you are building a CDK construct that you intend to use in the same solution, you do not need anything else. But if you want to ship the construct to open source repos: Npm, Maven, Pypi, Nuget, or Go, you must transpile your code construct from TypeScript to other languages, package them, and deploy them to the desired repo. And you would need to do that in CI/CD. This is where Projen comes in. It sets up the whole environment with all the features and bells and whistles that you may need.

The Projen provides the following features when building a CDK construct:

  1. Compile, lint, and test the code.
  2. Use JSII to produce library artifacts for all target languages.
  3. Determine the next minor/patch version based on conventional commits.
  4. A changelog entry is generated based on the commit history.
  5. Packages are published to all target package managers.

Implementing those functionalities would take a lot of time. I am not a fan of code generators, but I do not want to build the whole thing on my own, either.

Projen is used for more than just building CDK construct. There is a whole set of projects that you can generate with it. Projen is very opinionated. If you are doing something unordinary, you will need some escape hatches. We will see them in this article.

There are many tutorials on how to build CDK construct with Projen. You can read them here, here, here, here, here, here and here. We will mainly concentrate on less-known tips and tricks.

Projen is an essential tool for creating CDK constructs. But it is opinionated. Luckily, there are many easy ways to overcome its constraints.

💡 Projen does a lot of magic in the background; sometimes, it is hard to determine the cause of an error. If you have an error on CI/CD, try running the process locally and observe if you get the same error. The CICD process also tracks if any generated file differs from the one in git. So if you run the process locally, you will see which files have changed, then inspect and commit them. One of those changes is also generated documentation. So, run “npm run docgen” before committing and push generated documentation.

💡 Projen automatically generates the package.json, .gitignore, .npmignore, eslint, jsii configuration, license files, etc., during project creation and continuously updates and maintains these settings. All configuration files become read-only. You should not modify them; even if you do, they will be overridden.

Important Parts of .projenrc.js

cdkVersion: '2.70.0'
Set the CDK version to the lowest⚠️ possible so your construct can be used in projects that do not use the latest CDK version.

jsiiVersion: '^5.1.9'
Set the latest version of JSII. JSII enables support for other languages, not just JavaScript/TypeScript.

releaseTrigger: ReleaseTrigger.manual()
If you do not automatically deploy the new version, set the release trigger to manual.

npmAccess: NpmAccess.PUBLIC
You need to set this if you use a scoped project like @me/my-package and want to publish a public package.

Using esbuild to Transpile TypeScript for Lambda

We could also use Projen to compile/transpile TypeScript. If you want to do that, put code under the subfolder src like src/functions. But you might not want to do that. It will compile code with CDK default tsconfig.json settings. If you want to use ECMAScript Modules, not old Common.js Modules, you need to transpile TypeScript yourself.

We will have our functions under the functions folder in the root folder. We will also add package.json in that folder where we can specify "type": "module".

We need to transpile TypesScript to JavaScript, which we can do using esbuild:

esbuild --bundle --minify --platform=node --external:@aws-sdk --format=esm functions/lambdaSnsError/index.ts --outfile=lib/functions/lambdaSnsError/index.mjs

cp functions/package.json lib/functions/lambdaSnsError

Adding New Tasks

If we want to do it via CI/CD, we need to add task in .projenrc.js.

const esbuilTask = project.addTask('esbuild', {
  exec: `
    esbuild --bundle --minify --platform=node --external:@aws-sdk --format=esm functions/lambdaSnsError/index.ts --outfile=lib/functions/lambdaSnsError/index.mjs

    cp functions/package.json lib/functions/lambdaSnsError
  `,
});

project.tasks.tryFind('post-compile')!.prependSpawn(esbuilTask);

Creating Monorepo

By adding a functions folder, the project becomes a monorepo, with one root project for CDK and an additional project in the function folder. We will take advantage of npm workspaces, which simplify working with monorepo. For that, we need to add a workspace node in project.json. But we cannot modify project.json. We need to use escape hatch to modify it with .projenrc.js:

project.package.addField('workspaces', ['functions', 'scripts']);

We must also modify the .gitinogre file because we want the function folder to have its own tsconfig.json. The root tsconfig.json is generated by Project and excluded from git:

project.gitignore.exclude('!functions/\*\*/tsconfig.json');

Overriding Any Generated Property

I have already listed all the escape hatches that we need for our purpose. But there are more. In fact, you can change any property in generated files through the addOverride, addDeletionOverride, addToArray, and patch methods accessible on file objects:

Here is a sample from the documentation on how you would modify package.json (although you do not need escape hatches for those).

// Get the ObjectFile
const packageJson = project.tryFindObjectFile('package.json');

// Use dot notation to address inside the object
packageJson.addOverride('description', 'the next generation of logging!');
packageJson.addDeletionOverride('author.organization');
packageJson.addToArray('keywords', 'logging', 'next-gen');

You can even use pure brute force:

packageJson.patch(JsonPatch.add('/author/name','A. Mused'));

Removing Files

You can remove a file from the project through the tryRemoveFile method on the Project class.

new TextFile(project, "hello.txt", { lines: "original" });
project.tryRemoveFile("hello.txt");
new TextFile(project, "hello.txt", { lines: "better" });

Eject

Your solution may be too complex and need too many escape hatches. Or you do not like the opinionated way the Projen offers, and you would still like to keep your work.

If you run projen eject, the Projen will be removed from your project, and existing functionality will be replaced with scripts. But be warned. There are quite a few bugs in the eject functionality, so you need to think twice if you want to do that.

Deploying with Semantic Versioning

The CI/CD pipeline that Projen builds with GitHub Actions supports semantic versioning based on Conventional Commits

For example, if you type in the commit message:

  • fix it bump path version (v0.0.1)
  • feat it bump minor version (v0.1.0)

The major version must be explicitly bumped by adding majorVersion: x to .projenrc.js to protect users against breaking changes.

Publishing to Construct Hub

Construct Hub is an aggregator of all CDK constructs. If you publish CDK construct to the npm registry, it will automatically be published to Construct Hub within 30 minutes. But it must match some conditions: It must be JSII compatible, open source, and needs the keyword "cdk" or similar.

Publish to All Open-Source Repos

Projen enables you to publish to all open source repos: Npm, Maven, Pypi, Nuget, Go.

Here are detailed instructions: https://www.taylorondrey.com/posts/projen-oss/

You will have to do quite some clicking to create accounts in all those repos and to grant permissions.