docs(nx-dev): new blog post on how to use sync generators for Tailwind globs
This commit is contained in:
parent
4c7586c82d
commit
9e9345b5e1
238
docs/blog/2025-06-19-setup-tailwind4-npm-workspace.md
Normal file
238
docs/blog/2025-06-19-setup-tailwind4-npm-workspace.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
---
|
||||||
|
title: 'Configure Tailwind 4 with Vite in an NPM Workspace: The Complete Guide'
|
||||||
|
slug: setup-tailwind-4-npm-workspace
|
||||||
|
authors: ['Juri Strumpflohner']
|
||||||
|
tags: ['nx', 'tailwind', 'vite', 'npm-workspaces', 'sync-generators']
|
||||||
|
cover_image: /blog/images/articles/bg-tailwind-4-guide.avif
|
||||||
|
description: 'Learn how to set up Tailwind CSS v4 with Vite in an NPM workspace monorepo, and automate your configuration with Nx Sync Generators for optimal performance.'
|
||||||
|
youtubeUrl: https://youtu.be/tg3LnqhNNws
|
||||||
|
---
|
||||||
|
|
||||||
|
Tailwind CSS v4 brings revolutionary changes to how we configure and use the popular utility-first framework. The simplified setup eliminates configuration files and complex PostCSS setups - you just install, import, and start building. But when working in NPM workspaces or monorepos, there's still one crucial challenge: **how do you tell Tailwind which packages to scan for classes?**
|
||||||
|
|
||||||
|
This guide walks you through setting up Tailwind v4 with Vite in an NPM workspace, then shows you how to automate the configuration using Nx Sync Generators to **eliminate manual maintenance**.
|
||||||
|
|
||||||
|
{% github-repository url="https://github.com/juristr/tailwind4-vite-npm-workspaces" /%}
|
||||||
|
|
||||||
|
{% toc /%}
|
||||||
|
|
||||||
|
## Setting up Tailwind v4
|
||||||
|
|
||||||
|
[Tailwind v4](https://tailwindcss.com/blog/tailwindcss-v4) introduced some nice simplifications when it comes to configuring Tailwind:
|
||||||
|
|
||||||
|
- **No more `tailwind.config.js`** - The framework works out of the box
|
||||||
|
- **Minimal dependencies** - Just `tailwindcss` and `@tailwindcss/vite` for Vite projects
|
||||||
|
- **Simple CSS import** - Add `@import "tailwindcss"` to your stylesheet and you're ready
|
||||||
|
|
||||||
|
Since we're using Vite in this workspace, we can leverage the dedicated Tailwind Vite plugin instead of PostCSS configuration. Here's what you need:
|
||||||
|
|
||||||
|
Install the required packages at your workspace root:
|
||||||
|
|
||||||
|
```json {% fileName="package.json" %}
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"tailwindcss": "^4.0.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure your Vite setup:
|
||||||
|
|
||||||
|
```typescript {% fileName="apps/shop/vite.config.ts" %}
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
// ... rest of your config
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the import to your main CSS file:
|
||||||
|
|
||||||
|
```css {% fileName="apps/shop/src/styles.css" %}
|
||||||
|
@import 'tailwindcss';
|
||||||
|
```
|
||||||
|
|
||||||
|
## The NPM workspace challenge
|
||||||
|
|
||||||
|
Consider a typical e-commerce application structured as an NPM workspace:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
apps/
|
||||||
|
shop/
|
||||||
|
src <<<< where tailwind is configured
|
||||||
|
packages/
|
||||||
|
products/
|
||||||
|
feat-product-list/
|
||||||
|
feat-product-detail/
|
||||||
|
data-access-products/
|
||||||
|
shared/
|
||||||
|
ui/
|
||||||
|
utils/
|
||||||
|
```
|
||||||
|
|
||||||
|
At this point, your application will build and serve, but you'll notice that styles from your `packages/` are missing. In this modular setup, your main application (`shop`) depends on various feature packages, but **Tailwind only scans the main app by default**. This means styles defined in your packages won't be included in the final bundle, leading to missing styles and broken layouts.
|
||||||
|
|
||||||
|
## Solving the scanning problem with @source directives
|
||||||
|
|
||||||
|
Tailwind v4 introduces the `@source` directive to address exactly this problem. You can explicitly tell Tailwind which directories to scan by adding these directives to your CSS file:
|
||||||
|
|
||||||
|
```css {% fileName="apps/shop/src/styles.css" %}
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
@source "../../../packages/products/feat-product-list";
|
||||||
|
@source "../../../packages/products/feat-product-detail";
|
||||||
|
@source "../../../packages/shared/ui";
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
With these directives in place, Tailwind will scan the specified packages and include any utility classes found there. Your application styles will now work correctly across all packages.
|
||||||
|
|
||||||
|
## Automating @source entries - enter Nx sync generators
|
||||||
|
|
||||||
|
While `@source` directives solve the technical problem, they introduce a maintenance challenge:
|
||||||
|
|
||||||
|
- **manual updates required** when adding or removing dependencies,
|
||||||
|
- **easy to forget updating** the directives,
|
||||||
|
- **hard-to-debug issues** since missing styles don't break builds (just cause visual problems), and
|
||||||
|
- **team coordination** since every developer needs to remember to update these paths.
|
||||||
|
|
||||||
|
This is where automation becomes crucial and where Nx can help. [Nx Sync Generators](/concepts/sync-generators) provide a powerful solution for **automating configuration that needs to stay in sync with your project structure**.
|
||||||
|
|
||||||
|
For our specific use case we can automate the generation of the `@source` directives by
|
||||||
|
|
||||||
|
- analyzing and traversing all of the `shop` application's dependencies (leveraging the [Nx project graph](/features/explore-graph))
|
||||||
|
- generating the `@source` entries into the correct `styles.css` file
|
||||||
|
|
||||||
|
You can follow [the guide on the Nx docs](/extending-nx/recipes/create-sync-generator) for all the details on how to implement your own Nx sync generator. At a high level these are the steps you'll need:
|
||||||
|
|
||||||
|
**Step 1: Add Nx Plugin development support**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx add @nx/plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Generate a new plugin into your workspace**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx g @nx/plugin:plugin tools/tailwind-sync-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
Note, you can choose whatever folder you like. I happen to use the `tools/` folder for this example.
|
||||||
|
|
||||||
|
**Step 3: Generate a sync generator**
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx nx g @nx/plugin:generator --name=update-tailwind-globs --path=tools/tailwind-sync-plugin/src/generators/update-tailwind-globs
|
||||||
|
```
|
||||||
|
|
||||||
|
With that you have the infrastructure in place and we can look at the actual implementation of the sync generator:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Tree, createProjectGraphAsync, joinPathFragments } from '@nx/devkit';
|
||||||
|
import { SyncGeneratorResult } from 'nx/src/utils/sync-generators';
|
||||||
|
|
||||||
|
export async function updateTailwindGlobsGenerator(
|
||||||
|
tree: Tree
|
||||||
|
): Promise<SyncGeneratorResult> {
|
||||||
|
const appName = '@aishop/shop';
|
||||||
|
const projectGraph = await createProjectGraphAsync();
|
||||||
|
|
||||||
|
// Traverse all dependencies of the shop app
|
||||||
|
const dependencies = new Set<string>();
|
||||||
|
const queue = [appName];
|
||||||
|
const visited = new Set<string>();
|
||||||
|
|
||||||
|
while (queue.length > 0) {
|
||||||
|
const current = queue.shift()!;
|
||||||
|
if (visited.has(current)) continue;
|
||||||
|
visited.add(current);
|
||||||
|
|
||||||
|
const deps = projectGraph.dependencies[current] || [];
|
||||||
|
deps.forEach((dep) => {
|
||||||
|
dependencies.add(dep.target);
|
||||||
|
queue.push(dep.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate @source directives for each dependency
|
||||||
|
const sourceDirectives: string[] = [];
|
||||||
|
dependencies.forEach((dep) => {
|
||||||
|
const project = projectGraph.nodes[dep];
|
||||||
|
if (project && project.data.root) {
|
||||||
|
const relativePath = joinPathFragments('../../../', project.data.root);
|
||||||
|
sourceDirectives.push(`@source "${relativePath}";`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the styles.css file
|
||||||
|
const stylesPath = 'apps/shop/src/styles.css';
|
||||||
|
const currentContent = tree.read(stylesPath)?.toString() || '';
|
||||||
|
|
||||||
|
// Insert the @source directives after @import "tailwindcss"
|
||||||
|
// ... (implementation details)
|
||||||
|
|
||||||
|
return {
|
||||||
|
outOfSyncMessage: 'Tailwind @source directives updated',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_(Check out the [Github repo for the full implementation](https://github.com/juristr/tailwind4-vite-npm-workspaces))_
|
||||||
|
|
||||||
|
You can manually run sync generators with `nx sync`, but we want this to run automatically whenever we build or serve our application. As such we can register the sync generator in the app's `package.json`:
|
||||||
|
|
||||||
|
```json {% fileName="apps/shop/package.json" %}
|
||||||
|
{
|
||||||
|
"name": "@aishop/shop",
|
||||||
|
...
|
||||||
|
"nx": {
|
||||||
|
"targets": {
|
||||||
|
"build": {
|
||||||
|
"syncGenerators": ["@aishop/tailwind-sync-plugin:update-tailwind-globs"]
|
||||||
|
},
|
||||||
|
"serve": {
|
||||||
|
"syncGenerators": ["@aishop/tailwind-sync-plugin:update-tailwind-globs"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nx sync generators in action
|
||||||
|
|
||||||
|
When you run your development server with `nx serve shop`, the sync generator automatically checks if your `@source` directives are up to date:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Your CSS file is automatically updated with the correct directives based on your actual project dependencies. If you add or remove dependencies later, the next build or serve will **detect the changes and update the configuration automatically**.
|
||||||
|
|
||||||
|
You can find the complete implementation in this [GitHub repository](https://github.com/juristr/tailwind4-vite-npm-workspaces).
|
||||||
|
|
||||||
|
## Using Tailwind v3?
|
||||||
|
|
||||||
|
If you're currently using Tailwind v3, the concept is similar but the implementation differs. Instead of updating `@source` directives, you'd modify the `tailwind.config.js` file with glob patterns.
|
||||||
|
|
||||||
|
Check out the following video which explains the same approach for Tailwind v3:
|
||||||
|
|
||||||
|
{% youtube src="https://www.youtube.com/watch?v=huTmV-F8c0A" title="Optimizing Tailwind with Nx Sync Generators (v3)" /%}
|
||||||
|
|
||||||
|
The [Tailwind v3 demo repository](https://github.com/juristr/tailwind-sync-demo) shows how to implement this approach for older versions.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
While Tailwind v4's simplified setup is a significant improvement, manually maintaining `@source` directives creates a maintenance burden in monorepos. Nx Sync Generators solve this by automatically keeping your Tailwind configuration in sync with your project dependencies, eliminating manual updates and preventing hard-to-debug styling issues.
|
||||||
|
|
||||||
|
This approach transforms configuration maintenance into a completely automated process, letting you focus on building features rather than managing paths.
|
||||||
|
|
||||||
|
## Learn more
|
||||||
|
|
||||||
|
- 🧠 [Nx Docs](/getting-started/intro)
|
||||||
|
- 👩💻 [Tailwind v4 Vite NPM Workspace Demo](https://github.com/juristr/tailwind4-vite-npm-workspaces)
|
||||||
|
- 📖 [Nx Sync Generators Documentation](/extending-nx/recipes/create-sync-generator)
|
||||||
|
- 📹 [Nx Youtube Channel](https://www.youtube.com/@nxdevtools)
|
||||||
|
- 💬 [Nx Official Discord Server](https://go.nx.dev/community)
|
||||||
|
- 🐦 [Follow me on Twitter/X](https://twitter.com/juristr)
|
||||||
BIN
docs/blog/images/articles/bg-tailwind-4-guide.avif
Normal file
BIN
docs/blog/images/articles/bg-tailwind-4-guide.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
docs/blog/images/articles/bg-tailwind-4-guide.png
Normal file
BIN
docs/blog/images/articles/bg-tailwind-4-guide.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 569 KiB |
BIN
docs/blog/images/articles/tailwind-sync-generator.avif
Normal file
BIN
docs/blog/images/articles/tailwind-sync-generator.avif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
Loading…
x
Reference in New Issue
Block a user