Closed essenmitsosse closed 5 days 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:
dependencies
(https://knip.dev/reference/known-issues#definitely-typed-packages-in-dependencies)devDependencies
in one project should be production dependencies
in another.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.
@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:
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:
devDependencies
are any dependencies necessary for building and releasing the app but not for consuming it. For example, if you use a linter on your code, someone consuming your build code won’t need it. Following this logic, all linters, test runners, and build tools would go into devDependencies
.require
s or import
s but doesn’t get bundled with your package would fall under dependencies
or peerDependencies
(the distinction between these two isn't as relevant here).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:
dependencies
.devDependencies
.One advantage of this method is that it encourages closer scrutiny of dependencies included in or used to generate production code.
For example:
react
would definitely be a dependency
because almost all React apps import parts of it into the production code.dependency
.eslint
doesn’t end up in the production bundle or directly contribute to its creation, so it would belong in devDependencies
. The same goes for any type-only packages.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 🍒
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 :)
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.
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
anddevDependencies
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.
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!
Separate from this discussion, you might be able to achieve what you want:
.css
compiler: https://knip.dev/features/compilers#cssimport "tailwind";
entry: ["src/**/*.css!"]
Feel free to hit me up for this specific case.
Going to close this one, thanks!
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?