webpro-nl / knip

✂️ Find unused files, dependencies and exports in your JavaScript and TypeScript projects. Knip it before you ship it!
https://knip.dev
ISC License
7.11k stars 180 forks source link

đź’ˇ Consider Tailwind, PostCSS and AutoPrefixer production dependencies #847

Closed essenmitsosse closed 5 days ago

essenmitsosse commented 1 week ago

Suggest an idea for this project

Currently some things like Tailwind, PostCSS and AutoPrefixer are not considered production dependencies as per the default settings. Their plugins are removed when running in production mode, this that those tools as well as things like Tailwind plugins are supposed to be moved to devDependencies. But from my understanding, they shouldn't. Tailwind is something that explicitly ends up in the production bundle in the same was as something like React.

Is there anything that I am missing here?

webpro commented 1 week ago

Let me try to break it down, because there's a few things to your question. Please feel free to ask follow-up questions if you have any, as I might be off.

The thing here with (dev)dependencies is that once there was only Node.js and npm and all was clear: running the code in Node.js after npm install --production in CI would fail if one of the dependencies were missing in the runtime.

Tooling like test runners, linters, compilers and their plugins (including PostCSS and Autoprefixer) are usual suspects to me when it comes to this division: they're used during author/dev and build time for QA and to build production code. Yet they're not consumed directly in production. There's probably no require("autoprefixer") or import * from "postcss" in your source code.

Today we also have all sorts of frontend/client-focused and "hybrid" packages in the npm registry, and one could even argue that in certain cases React is not a production dependency, because during build time the React package is consumed but becomes part of the production bundle. Thus, the React package itself is no longer used in production and React could just as well be listed in devDependencies as long as it's available during the build process.

Either way, I think there's usually consensus about this split: React is a production dependency, even though it might be consumed out of the package files into optimized bundles during build time (not runtime). Everything else mentioned so far I'd consider a dev dependency.

That said, for Knip it's not always clear or possible to do the right thing:

Recently I did a refactoring for plugins that should make improving cases like this a lot easier to implement in the plugins themselves.

So I think the question is: how to decide whether any dependency should in either dependencies or devDependencies? For that we'd need more specific pointers like official documentation or even better: a reproduction of case(s) where you think Knip fails to report correctly.

As for Tailwind specifically, I'm not sure about all the possible ways to use it and I'm not sure if/when it should be available in production. Feel free to list dependencies and cases where Knip is not behaving as expected.

essenmitsosse commented 1 week ago

@webpro, thanks for the quick and detailed response. I see your point, and as you mentioned, it ultimately comes down to the definition of "production dependencies." While I can’t provide a definitive answer either, I do have some thoughts on this, that might make it more clear where I am coming from:

Difference Between Stand-alone Apps and Released Packages

The original distinction between dependencies, devDependencies, and peerDependencies likely stems from the idea that an NPM package is designed to be consumed. In that context:

However, here’s the twist: Many apps today aren’t intended to be consumed by other apps or released as NPM packages. Instead, they use NPM as a package manager. For these kinds of apps, the above definition doesn’t entirely hold.

In such cases, I usually simplify things by ignoring devDependencies entirely and placing everything into dependencies. This approach avoids unnecessary debates during pull requests, as the distinction can sometimes feel arbitrary. Plus, running npm install without a flag installs only regular dependencies, making it more straightforward.

My hope now is that Knip could help enforce some structure here and better organize these lists. While it’s a minor detail, it would still be nice to have. By splitting dependencies this way:

One advantage of this method is that it encourages closer scrutiny of dependencies included in or used to generate production code.

For example:


I hope this clarifies where I’m coming from. I assume your focus is mainly on released packages, for which I agree with your points. What I am looking for was a tool for stand-alone apps. Perhaps this is a very personal perspective, or maybe it goes beyond the scope and intended use case of Knip. However, if there’s a way to make this approach feasible, that would be fantastic.

Either way Knip is already a great tool! This would just be the cherry on top 🍒

webpro commented 1 week ago

Many apps today aren’t intended to be consumed by other apps or released as NPM packages. Instead, they use NPM as a package manager. For these kinds of apps, the above definition doesn’t entirely hold.

We're talking about the same scenario, I'm talking apps too.

  • Anything [...] used to create the production bundle would go into dependencies.

This is maybe where we might disagree. According to this guide e.g. Vite or webpack falls in this category too.

Tailwind CSS, which often ends up in the production bundle, would also be categorized as a dependency.

My question remains, as by now I feel like we're discussing Tailwind only and specifically. This is the crux: Tailwind is largely a dev/build tool and does not itself end up in the production bundle at all, just your CSS. Unless I'm mistaken, yet the website seems to indicate this too: npm install -D tailwindcss. Tailwind is a zero-runtime lib (unlike CSS-in-JS solutions like styled-components and Emotion). There are no import "tailwind" statements in your code (except for types maybe, which are stripped in production JS).

So if I had to enforce some structure then Tailwind ends up in devDependencies - this is the line that Knip currently draws, and tbh so far I'm not very convinced that should change :)

essenmitsosse commented 1 week ago

There are no import "tailwind" statements in your code (except for types maybe, which are stripped in production JS).

This might be more philosophical then I first thought. For me Tailwind delivers a lot of default CSS (even though you can heavily modify and treeshake it), which for me made it end up in production. Also things like this are added to the CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

Which to me signal the explicit import of CSS from the tailwind library. But I did some research you the internet seems to quite heavily support your POV.


Another way to cut it would be to consider building your app like this (especially when you do it in CI):

npm ci --production
npm run build

I'm not sure that is a usage someone would consider, but it would at least be a very clear cut. Can the above code run or will dependencies be missing?

As I said before this might be a very person preference and I don't want to push Knip in a highly opinionated direction. But for standalone apps being able to do this might be one of the main benefits of separating dependencies and devDependencies to begin with.

webpro commented 1 week ago

Which to me signal the explicit import of CSS from the tailwind library.

That's a good point, one could consider CSS imports equal to JS imports and argue that minified chunks of e.g. both Tailwind and React code end up in production...

Tailwind is something that explicitly ends up in the production bundle in the same was as something like React.

Wait.. that was your point :)

But for standalone apps being able to do this might be one of the main benefits of separating dependencies and devDependencies to begin with.

What exactly do you mean by "this" - that Knip detects lib vs app and adapts behavior, or that there would be a new configuration option/flag?

The clear cut as described means that simply everything needed to build the app must be in dependencies, also build tooling. But not test runners? And what about e.g. Vite plugins?

Btw, this would go against all the documentation, examples, boilerplates, etc. etc. So even if philosophically interesting this likely won't happen anytime soon just because of such major practical hurdles. Breaking changes and going against the grain just makes no sense, Knip is basically designed to go with the grain and follow defaults and standards as much as possible.

essenmitsosse commented 5 days ago

What exactly do you mean by "this" - that Knip detects lib vs app and adapts behavior, or that there would be a new configuration option/flag?

This was referencing being able to install and build production like this:

npm ci --production
npm run build

The clear cut as described means that simply everything needed to build the app must be in dependencies, also build tooling. But not test runners? And what about e.g. Vite plugins?

So given aboves assumption the answer would be: Vite plugins yes, test runners no. Basically anything that would be necessary to run npm run build.

Btw, this would go against all the documentation, examples, boilerplates, etc. etc. So even if philosophically interesting this likely won't happen anytime soon just because of such major practical hurdles. Breaking changes and going against the grain just makes no sense, Knip is basically designed to go with the grain and follow defaults and standards as much as possible.

I don't want to argue with that. It's what I mentioned earlier: I don't want to push Knip in a highly opinionated direction. For me React Knip already considers React a production dependency because it gets imported directly all over the place — I assume that should be the same for a lot of people. And as I said before: I acknowledge that things like vite would be very philosophical discussions and you got a strong point, about this goes against all established conventions.


I might have gotten a bit hung up on the Tailwind example. There might still be a case to argue there. Technically Tailwind gets imported via @tailwind base; etc. in the CSS file, and this CSS file usually gets directly imported for a lot of build systems. But I would also understand the position, that this would greatly blow open the scope of Knip. I can open a more specific issue for this, but I am also fine with letting this rest. Thanks either way for taking the time to walking me through your thought process. It's greatly appreciated!

webpro commented 5 days ago

Separate from this discussion, you might be able to achieve what you want:

Feel free to hit me up for this specific case.

Going to close this one, thanks!