This lesson preview is part of the Bundling and Automation in Monorepos course and can be unlocked immediately with a \newline Pro subscription or a single-time purchase. Already have access to this course? Log in here.

This video is available to students only
Unlock This Course

Get unlimited access to Bundling and Automation in Monorepos, plus 90+ \newline books, guides and courses with the \newline Pro subscription.

Thumbnail for the \newline course Bundling and Automation in Monorepos
  • [00:00 - 00:17] In this lesson, we're going to set up Knip to identify and fix dependency issues in our Monorepo, and we're going to then integrate it as a pre-commit check. This will ensure that our repository will remain clean with no unused or missing dependencies and no dead code.

    [00:18 - 00:26] We're going to start by configuring Knip. I'm going to run "pnpm create @knip/config" and then run "pnpm install".

    [00:27 - 00:40] Now we're going to run "git diff" and see the changes that were made. We can see that the new script called "knip" was added to the list of our scripts, and several dependencies were added to our dev dependencies.

    [00:41 - 00:49] These are "@types/node", "knip", and "typescript". Finally, there are some lockfile changes that we don't care about.

    [00:50 - 00:59] So let's run Knip and see what it reports. It has identified a bunch of errors in our Monorepo.

    [01:00 - 01:12] I'm going to pipe this output into vim to make it easier to go through it one by one. To fix some of these, we're going to need to change Knip's configuration, so I'm going to create a knip.json config file.

    [01:13 - 01:42] I'm going to add the magic "$schema" property that allows our editor to give us autocomplete for the properties of this config file, and then I'm going to add the "workspaces" option, which allows us to configure Knip for each of the separate workspaces within our Monorepo. On the left side, the first complaint that Knip is giving us is that we have an unused file of packages/config/edlint8.base.mjs.

    [01:43 - 02:04] The reason for that is that we created this config as an example, but we don't actually have any workspaces in our example project that are using ESLint 8. The purpose of this file was just to show you if you must support both ESLint 8 and ESLint 9 at the same time. How you could do it?

    [02:05 - 02:19] However, I do want to keep this file even though it's currently unused in our Monorepo. To do that, I have to tell Knip to treat this file as an "entry" file in Knip terminology.

    [02:20 - 03:16] Entry files are the files that Knip is going to traverse and check for imports and dependencies usage, and based on that, decide if there is a unused dependency, unused code, or a missing dependency. So to fix the first error reported by Knip, the only thing I need to do is add a "packages/config" workspace config object and then add an "entry" that contains "eslint8.base.mjs". After this change, let me go back to the command line and run Knip again and we can see that Knip now reports several errors fewer because our ESLint 8 file is now being correctly treated as an entry point, the dependency at @eslint8/js is also being correctly identified as used, so it has been removed from the list of unused dependencies.

    [03:17 - 03:43] I'm going to again pipe this output into vim and in the right pane I'm going to open my knip.json config file and we can look at the other errors reported for packages/config. Knip is failing to correctly find that we're using the SWC dependencies for our Jest config. Indeed, it's saying that there is an unresolved import in our Jest config file.

    [03:44 - 03:54] Let's figure out why that is. The reason is that if we open our Jest config file, we are going to see that the "transform" property is just a string.

    [03:55 - 04:19] We're not actually importing "@swc/jest", and because we don't have an actual import statement, Knip cannot know for sure if this string, "@swc/jest", actually is referring to something that's being imported, or is just a random string. We need to explicitly say to Knip that yes, we are actually using this.

    [04:20 - 04:42] The easiest fix in this case is just to add our SWC dependencies into the list of ignored dependencies that Knip will never raise errors for when it finds them. Now, when I drop down to the command line and run Knip again, we're going to see that all errors related to packages/config have been resolved.

    [04:43 - 04:58] I'm going to again pipe the new output of Knip into vim, split it and open the config file on the right side. And then the next set of problems I want to fix are the ones related to our frontend-nextjs package.

    [04:59 - 05:15] The first of these is an unused dev dependency for "eslint-config-next". Why is that? Well, the problem with old ESLint is that you don't actually import configs, you just refer to them by name.

    [05:16 - 05:57] We can see that if we open apps/frontend-nextjs/eslint.config.mjs file, and we can see that we load the eslint-config-next configuration using the FlatCompat plugin and then referring to "next/core-web-vitals". What this line actually means is "Load eslint-config-next/slash-core-web-vitals", but again, because we don't have an explicit import statement, Knip cannot know this for sure.

    [05:58 - 06:03] The fix is simple enough. We're just going to add this to our ignored dependencies.

    [06:04 - 06:19] And then the next one is an unlisted dependency post. "postcss-load-config" is referred to in the file "postcss.config.mjs", but it's not in the list of dependencies in our package.json.

    [06:20 - 06:35] If we open that file, we're going to see that it actually is a JSDoc type import that gives us the typing of the config object. The reason to have this is to have better autocomplete in your editor.

    [06:36 - 06:50] This is part of the generated files that Next.js itself comes with, and unfortunately, if we check for autocomplete on this object, it actually does not work. Knip has correctly identified that this type import doesn't work.

    [06:51 - 06:56] It refers to a package that just does not exist. The fix is again straightforward enough.

    [06:58 - 07:47] I'm going to drop down to the command line go into apps/frontend-nextjs and I'm going to install this package with "pnpm add -D postcss-load-config". Then I'm going to return to the root folder of the Conorepo with "cd -" and I'm going to go back to my editor. And now that we have added the dependency, we can see that the type import works correctly and we have the type information for this variable. The last error is the same as what we had previously, but in this case, because it's being imported from frontend-nextjs it's being reported there. The fix is going to be the same, because the Jest config does not directly refer to "@swc/jest" with an import statement.

    [07:48 - 08:02] We need to add "@swc/jest" in the list of our ignored dependencies. With that, I'm going to drop down to the command line again and I'm going to run Knip and we can see that we just have a single error left to solve.

    [08:03 - 08:37] If I go to our apps/frontend-vite workspace and open our eslint.config.mjs next to our package.json, we can see that even though we have a dependency on @eslint/js, we never reference this dependency in our ESLint config. This means that it's safe to just remove it, which I'm going to do with "pnpm rm @eslint/js". I'm going to now return to the Monorepo root run Knip again.

    [08:38 - 08:54] And finally we have no more errors. In order to carefully inspect the changes that we've made, I'm going to use "git add -p", which means show me a patch for each change that's going to be added to the commit.

    [08:55 - 09:12] And we can go to them one by one. The first one is to add the missing postcss-load-config dependency in our frontend-nextjs app. The second one is to remove the unused dev dependency on at @eslint/js in frontend-vite.

    [09:13 - 09:25] The third one is to add the Knip script in the root package.json. And finally, there are the dependencies necessary for Knip to work that have been added to the root dev dependencies.

    [09:26 - 09:47] The rest are just going to be our pnpm-lockfile.yaml file changes that we're not going to inspect one by one, because we trust pnpm to be able to do that correctly. I'm going to commit this as "Add Knip", and then I'm going to open our lefthook.yaml file and do two changes. The first one is that I want to remove our "output" config.

    [09:48 - 10:09] We did make it shorter, but honestly it makes it harder to debug stuff, so it's better to just use the default output. That's a bit chattier, but much easier to understand what Lefthook is doing. So I'm just going to remove the "output" setting completely, and then I'm going to add another pre-commit command that I'm going to call "check for unused files, exports and dependencies".

    [10:10 - 10:26] That's going to run Knip. Now because we have two commands here, of which only one actually changes files - pretty-quick will format and save files with changes - it's safe for us to run these scripts in parallel.

    [10:27 - 10:35] This is just going to make the execution of the pre-commit hook faster. I'm going to commit this as "Run knip on pre-commit".

    [10:36 - 10:45] Oh, and I have an error in my lefthook.yaml file, let me quickly fix that. Yes, I'm missing a colon after "run".

    [10:46 - 10:59] Fix that, go back to the command line and write our commit and we can see that Lefthook run both our scripts as part of pre-commit. And then when everything passed, the commit was saved.

    [11:00 - 11:06] With that, we have successfully set up Knip and integrated it into our development workflow.