07/14/2025
9 min read

It’s been a while!

After a long hiatus from these devblogs you might assume the Package Analyzer project is stagnant:

Package Linter The lint functionality in action

While my attention has been on other projects, the Package Analyzer has remained a passion project. I still believe that the topic of security in the JavaScript ecosystem is very underexplored. Supply chain attacks remain a viable threat vector, especially with the rise of vibe coding, where novices now let AI generate code, pulling in all kinds of dependencies without fully understanding the implications from a security point of view.

In this blog post I want to reflect on the current state of the project, what I’ve worked on since the last update and what my plans are for the future.

As said, it’s as much a passion project as it’s a learning project and with almost 6 years of learning, there’s a lot to reflect.

From a simple question…

Believe it or not, the project is almost exactly 6 years old (🎂), and it all started with a simple question: “Do I use this dependency in my project?”.

Back then I was absolutely mesmerized by the absurd download numbers of trivial packages like isEven or is-string. So I’ve built a tool that I can point to any NPM package or Node.js project and it would tell me whether any of them use those dependencies and how they are introduced.

But then I asked myself, what if instead of just checking for a specific package name, I could provide my own check altogether? What if I could include custom data to use in that check as well? What if I could specify multiple checks and set their severity, like a linter?

So step by step the project grew over the years, with a lot of learnings along the way but always with the following goal in mind: Being a framework to introspect Node.js projects. Whether it be for security analysis or ensuring correctness of metadata or anything in between, all in an easy to use package.

However with that many learnings and a clear picture of what the project should be, I was faced with a classic developer dilemma:

To rewrite or not…

Should I continue with the existing project, or should I take the “good” parts and start with a new project from the ground up?

The project in its current state didn’t arrive in a straight line, as said it was as much of a learning project as it was a passion project. Overall I’m quite happy with the current code but there are some parts that could be more streamlined.

Naturally the new project approach made the most sense, take the “good” parts, remove or simplify the “bad” parts and voila, a nice clean base to build upon. However in practice it didn’t work out that way and I quickly ran into 3 issues:

  1. Too many changes at once: My plan for a rewrite involved moving to a monorepo, to have a clean separation between the individual components. Right now there’s the package analyzer itself, the linter functionality, which builds upon the package analyzer and the CLI connecting everything. I also want to have a browser compatible version in the future so a monorepo with distinct boundaries makes a lot of sense. However this meant moving a lot of files around. In addition whenever I tried to improve the “bad” parts, I realized that the existing solution isn’t that far off from the new improved version. Pairing with AI confirmed that the existing solution is almost there.

  2. Tests The project is quite well tested, but with that much code shuffling, I’d also need to move the tests around, resulting in even more code shuffling. In addition I would need to write new tests for the new code, resulting in even more changes. I’m not a fan of big bang releases, I like to work in small steps, and all this was detrimental to my “small steps” philosophy.

  3. CI Changes A new project would also mean setting up the CI infrastructure all over again, something I really dreaded to do.

So continuing with the current project, doing small steps at a time, turned out to be the most sensible approach.

To prepare for the upcoming work, I’ve done 2 changes to the project:

  • Moved the project to a ES Modules
  • Migrated the test framework from jest to vitest

Moving to ES Modules

While the projects source code has been written in ES Module syntax since day one, the project itself however was still interpreted as a CommonJS project due to the settings in the package.json. I felt it was time now to fully move to ES Modules. The Node.js ecosystem should be more than capable by now to handle ES Modules and frankly it’s the way forward.

Apart from setting the type field in the package.json to module to indicate that this is a ES Module type project, I also updated the tsconfig.json config to reflect current best practices based on Matt Pocock’s work.

Updated TypeScript Config

verbatimModuleSyntax was set to true, while this helps the TypeScript compiler decide which import statements to drop, from a developers point of view, it is now required to prefix type imports with the type keyword, e.g.

// with `verbatimModuleSyntax` set to `false`
import { MyType } from "./my-type";
// with `verbatimModuleSyntax` set to `true`
import type { MyType } from "./my-type";

But by far the biggest and most tedious change was setting module to NodeNext alongside introducing verbatimModuleSyntax. The latter made sure that TypeScript doesn’t modify the import statements too much. The former required that import statements now use the full path to the file, which means including the file extension, e.g.

./src/report.ts
// before
import { Report } from "./report";
// after
import { Report } from "./report.js";

This is so that Node can decide whether a file is a CommonJS file (.cjs) or a ES Module file (.js).

EVERY import needed to be updated to include the .js extension, but thankfully this was only a 1 time change 🙏

Updating the webpack config

The project, in the long run, should be able to run in the browser. While not used right now, there is already its own export file, that should only contain browser compatible code, aka no Node.js specific code. To ensure that, I’m using webpack to parse this file (in memory). If it finds any Node.js specific code, it will error.

However with the changes mentioned above, webpack would now parse the entry file, see imports with the .js extension and go looking for those files, which don’t exist in the source code folder. To fix this, I needed to tell webpack to resolve the .js extensions to the equivalent .ts files in the same folder.

In the resolve section I needed to add a new extensionAlias setting:

const compiler = webpack({
//...
resolve: {
extensions: [".tsx", ".ts", ".js"],
extensionAlias: {
// Given a .js file, tell webpack to first look for a .ts file, then a .js file
".js": [".ts", ".js"]
}
}
//...
});

As the webpack config was written in CommonJS syntax and I didn’t bother to convert it to ES Module syntax at this time, I also needed to rename the file from webpack.config.js to webpack.config.cjs so it gets interpreted correctly again, due to moving the project to ES Modules.

Migrating to Vitest

Motivation

I’ve always wanted to move to vitest. While there wasn’t anything wrong with jest, I felt that vitest is the way forward.

But I kept postponing the migration as I expected a lot of work. As said the project contains quite some tests, with snapshots, timers, module mocking etc. But the switch to ES Modules forced my hand, as it inadvertently broke the jest setup. So now I either had to fix the jest setup or use this as an opportunity to move to vitest.

I chose vitest.

The migration

The migration couldn’t have been easier, literally all I needed to do was swap the jest setup file with a standard vitest setup file, no special config was needed. Then it was just a matter of replacing all jest. instances in the code with vi., like jest.fn() to vi.fn() etc. Basically everything was API compatible, great developer experience by the vitest team!

I then ran the tests and to my surprise they nearly all passed right away, only one file didn’t and this was due to how I loaded my own config files for the lint functionality. ES Modules need full paths, I was doing work to make it relative. Anyhow I fixed it but I expect to replace this part with a battle tested config loading library like cosmiconfig or jiti.

After this, all tests passed 🎉

Coverage and CI updates

I noticed that the code coverage differed by 5%, I didn’t look too much into it, as all the tests passed, including the snapshot tests which looked identical etc. I chalk it up to differences in the code coverage calculation between jest and vitest.

Lastly, due to vitest I had to update the Azure pipeline from Node.js 18 to 22 but then everything was working again, just like it did before, but now in a pure ES Module project 🎉

What’s next?

Going forward I’ll focus on moving the project to a monorepo setup, likely with turborepo. Right now the projects basically consists of 2 projects, the core package analyzer and the linter which builds upon the core functionality.

This makes a good split in and of itself, but I also want to have a browser compatible version, which would likely be another package. The rules for the linter should also move to their own package.

And of course do small improvements to streamline the code after 6 years, as said in the beginning, it’s as much of a passion project as it is a learning project.