Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💡 Consider Tailwind, PostCSS and AutoPrefixer production dependencies #847

Open
essenmitsosse opened this issue Nov 23, 2024 · 5 comments
Labels
feature request Feature request

Comments

@essenmitsosse
Copy link

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?

@essenmitsosse essenmitsosse added the feature request Feature request label Nov 23, 2024
@webpro
Copy link
Collaborator

webpro commented Nov 25, 2024

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
Copy link
Author

@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:

  • 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.
  • On the other hand, anything your code requires or imports 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:

  • Anything consumed by production code, included in the production bundle, or used to create the production bundle would go into dependencies.
  • Everything else would go into devDependencies.

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

For example:

  • By this definition, react would definitely be a dependency because almost all React apps import parts of it into the production code.
  • Tailwind CSS, which often ends up in the production bundle, would also be categorized as a dependency.
  • Conversely, 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 🍒

@webpro
Copy link
Collaborator

webpro commented Nov 26, 2024

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
Copy link
Author

essenmitsosse commented Nov 26, 2024

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
Copy link
Collaborator

webpro commented Nov 26, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature request Feature request
Projects
None yet
Development

No branches or pull requests

2 participants