Introduction Link to heading

One of the things that’s worth noting about the hugo-cdk project is how much, well… boilerplate is involved.

We have:

  • .gitignore
  • .npmignore
  • .prettierrc.js
  • jest.config.js
  • package-lock.json
  • package.json
  • tsconfig.json

And none of these are really central to the actual project code. And… they’re also pretty important files. Which… I don’t really understand as well as I’d like to. For example, I’d like to do a really great job of linting, but I don’t want to spend a bunch of time learning the intricacies of eslint. Surely there must be a better way than googling around the interwebs for someone else’s configs?

Projen Link to heading

This is one of the problems that Projen was invented to solve. Projen is another of the family started by aws-cdk. The commonality is the idea of construct driven programming. Projen’s focus is to “define and maintain complex project configuration through code”. Sounds great, let’s give it a shot.

Projenification Link to heading

We’ll start by installing projen globally.

yarn global all projen

Projen’s native approach is to start a new project, but we have git so I can’t see any harm in running new inside my existing hugo-cdk project.

cd hugo-cdk
# Clean up tsc compile outputs
npx tsc --build --clean
projen new awscdk-app-ts
git status
[andrew.hammond@andrew-hammond-C02XKGFMJG5H-SM master hugo-cdk (projenify *$%)]$ git status
On branch projenify
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   .gitignore
        modified:   .npmignore
        modified:   cdk.json
        modified:   package.json
        modified:   tsconfig.json
        modified:   yarn.lock

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        .eslintrc.json
        .gitattributes
        .github/
        .mergify.yml
        .prettierrc.js
        .projen/
        .projenrc.js
        .versionrc.json
        LICENSE
        src/
        tsconfig.eslint.json
        tsconfig.jest.json

Looks like we’ve both cleaned up and added some boilerplate. Exciting! However it also looks like the default location of our source code has moved from /bin and /lib to /src. Fine by me, let’s move our code.

git mv lib/*.ts src/
git mv bin/app.ts src/main.ts
# tweak the imports in src/main.ts

And… that’s got us started?! Yup, really that easy. Next step is to take a look a the new .projenrc.js file. This single file is used to programmatically generate a whole pile of other stuff. First up is getting our package.json back into shape.

AWS-CDK v2 Link to heading

Since I’m pretty lazy, this seems like a great time to throw in a migration to CDK v2. I saw an example of v2 and it looked super clean and easier to manage than the v1 “bunch of imports” approach. v2 is already at rc.4, so clearly it’s ready for a pet project like this.

// in projenrc.js
  cdkDependencies: ['aws-cdk-lib'],
  cdkVersionPinning: true,

and then run the following

# regenerate package.json
projen
# clean up imports
rm -rf node_modules/*
yarn

and… yup, I now have CDKv2 pulled by my library. Next step is to go through my code and switch around all the imports.

-import cdk = require('@aws-cdk/core');
-import codebuild = require('@aws-cdk/aws-codebuild');
-import iam = require('@aws-cdk/aws-iam');
+import { aws_codebuild, aws_iam, Stack, StackProps } from 'aws-cdk-lib';
+import { Construct } from 'constructs';

You can quickly see how this it a lot cleaner an import pattern. We don’t have to manage a bunch of different aws-cdk libraries, but instead import what we need from the single lib. I could have aliased these imports on import with an as but instead I decided to just propagate the name changes throughout the code. It’s not a complicated change, and I’m a big fan of avoiding aliases where possible.

While I was at it, I decided to use the new aws_cloudfront.Distribution to replace the aging and deprecated aws_cloudfront.CloudFrontWebDistribution. Feel free to check out the diff to see the change.

More Projen Goodness Link to heading

Projen provides a default scripts target for eslint, but… I literally never use that. I instead always want to use eslint --fix. Because, of course I want the computer to clean whitespace for me? Sure enough, it’s super easy to add an eslint:fix target. In .projenrc.js,

project.addTask('eslint:fix', { exec: 'eslint --fix' });

This renders into the package.json file with the following.

    "eslint:fix": "npx projen eslint:fix",

That’s a little strange to me. Turns out that projen will, by default, configure yarn scripts to run things through projen. Initially I was confused about why it would want to do this, and I see that there’s an option to disable it. Eventually I got not only comfortable with the idea, but instead started really liking the way it pushed for consistency.

Conclusion Link to heading

This is kind of a no-brainer. projen eliminates an enormous amount of toil around the setup and, more importantly, maintenance of project boilerplate. The migration is only about 10 to 15 minutes work. If you want your mind properly blown check out projen’s creator, Elad Ben-Isreal demonstrating meta-programming with projen.

JOIN THE #TemplatesAreEvil MOVEMENT!