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