@juristr/nx-tailwind-sync 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/generators/schema.d.ts +3 -0
- package/dist/generators/schema.json +13 -0
- package/dist/generators/update-tailwind-globs.d.ts +6 -0
- package/dist/generators/update-tailwind-globs.d.ts.map +1 -0
- package/dist/generators/update-tailwind-globs.js +191 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/generators.json +9 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# tailwind-sync-plugin
|
|
2
|
+
|
|
3
|
+
Nx sync generator that auto-manages `@source` directives in CSS files for Tailwind v4 monorepos.
|
|
4
|
+
|
|
5
|
+
## Problem
|
|
6
|
+
|
|
7
|
+
Tailwind v4 [requires to define `@source` directives](https://tailwindcss.com/docs/detecting-classes-in-source-files#explicitly-registering-sources) for dependencies that might be outside its own project configuration. Something that is common in monorepos. Manually maintaining these in a monorepo is error-prone.
|
|
8
|
+
|
|
9
|
+
## Solution
|
|
10
|
+
|
|
11
|
+
This plugin traverses the Nx project graph and generates `@source` directives for all transitive dependencies.
|
|
12
|
+
|
|
13
|
+
## Detection
|
|
14
|
+
|
|
15
|
+
A project is detected as using Tailwind v4 if:
|
|
16
|
+
|
|
17
|
+
1. Has a CSS file with `@import 'tailwindcss'`
|
|
18
|
+
2. Has a Vite config using `tailwindcss()` from `@tailwindcss/vite`
|
|
19
|
+
|
|
20
|
+
## CSS File Search Paths
|
|
21
|
+
|
|
22
|
+
Default locations checked:
|
|
23
|
+
|
|
24
|
+
- `src/styles.css`
|
|
25
|
+
- `.storybook/styles.css`
|
|
26
|
+
|
|
27
|
+
Additional paths can be configured via the `additionalStylePaths` option.
|
|
28
|
+
|
|
29
|
+
## Output
|
|
30
|
+
|
|
31
|
+
The generator inserts/updates a managed block in CSS files:
|
|
32
|
+
|
|
33
|
+
```css
|
|
34
|
+
@import 'tailwindcss';
|
|
35
|
+
|
|
36
|
+
/* nx-tailwind-sources:start */
|
|
37
|
+
@source "../../../packages/shared/daypulse-ui";
|
|
38
|
+
@source "../../../packages/daypulse/styles";
|
|
39
|
+
/* nx-tailwind-sources:end */
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
Register the sync generator on tasks that need it (e.g., `build`, `dev`):
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"nx": {
|
|
49
|
+
"targets": {
|
|
50
|
+
"build": {
|
|
51
|
+
"syncGenerators": ["@aishop/tailwind-sync-plugin:update-tailwind-globs"]
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Run manually:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm nx sync
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Options
|
|
65
|
+
|
|
66
|
+
| Option | Type | Description |
|
|
67
|
+
| ---------------------- | ---------- | --------------------------------------------- |
|
|
68
|
+
| `additionalStylePaths` | `string[]` | Extra relative paths to search for styles.css |
|
|
69
|
+
|
|
70
|
+
## Building
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
nx build tailwind-sync-plugin
|
|
74
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/schema",
|
|
3
|
+
"$id": "UpdateTailwindGlobs",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"properties": {
|
|
6
|
+
"additionalStylePaths": {
|
|
7
|
+
"type": "array",
|
|
8
|
+
"items": { "type": "string" },
|
|
9
|
+
"description": "Additional relative paths (from project root) to search for styles.css files with @import 'tailwindcss'"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"required": []
|
|
13
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Tree } from '@nx/devkit';
|
|
2
|
+
import { SyncGeneratorResult } from 'nx/src/utils/sync-generators';
|
|
3
|
+
import { UpdateTailwindGlobsGeneratorSchema } from './schema';
|
|
4
|
+
export declare function updateTailwindGlobsGenerator(tree: Tree, options?: UpdateTailwindGlobsGeneratorSchema): Promise<SyncGeneratorResult>;
|
|
5
|
+
export default updateTailwindGlobsGenerator;
|
|
6
|
+
//# sourceMappingURL=update-tailwind-globs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-tailwind-globs.d.ts","sourceRoot":"","sources":["../../src/generators/update-tailwind-globs.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EAIL,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,OAAO,EAAE,kCAAkC,EAAE,MAAM,UAAU,CAAC;AA+O9D,wBAAsB,4BAA4B,CAChD,IAAI,EAAE,IAAI,EACV,OAAO,GAAE,kCAAuC,GAC/C,OAAO,CAAC,mBAAmB,CAAC,CAmC9B;AAED,eAAe,4BAA4B,CAAC"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.updateTailwindGlobsGenerator = updateTailwindGlobsGenerator;
|
|
4
|
+
const devkit_1 = require("@nx/devkit");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const START_MARKER = '/* nx-tailwind-sources:start */';
|
|
7
|
+
const END_MARKER = '/* nx-tailwind-sources:end */';
|
|
8
|
+
const DEFAULT_STYLE_PATHS = ['src/styles.css', '.storybook/styles.css'];
|
|
9
|
+
const VITE_CONFIG_PATTERNS = [
|
|
10
|
+
'vite.config.ts',
|
|
11
|
+
'vite.config.mts',
|
|
12
|
+
'vite.config.js',
|
|
13
|
+
'vite.config.mjs',
|
|
14
|
+
'vitest.config.ts',
|
|
15
|
+
'vitest.config.mts',
|
|
16
|
+
'vitest.config.storybook.ts',
|
|
17
|
+
];
|
|
18
|
+
/**
|
|
19
|
+
* Find CSS file with @import 'tailwindcss' in project
|
|
20
|
+
*/
|
|
21
|
+
function findTailwindCssFile(tree, projectRoot, additionalPaths = []) {
|
|
22
|
+
const searchPaths = [...DEFAULT_STYLE_PATHS, ...additionalPaths];
|
|
23
|
+
for (const relPath of searchPaths) {
|
|
24
|
+
const fullPath = (0, path_1.join)(projectRoot, relPath);
|
|
25
|
+
const content = tree.read(fullPath)?.toString();
|
|
26
|
+
if (content?.match(/@import\s+['"]tailwindcss['"]/)) {
|
|
27
|
+
return fullPath;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Find any styles.css file in project (for Vite plugin projects)
|
|
34
|
+
*/
|
|
35
|
+
function findAnyStylesFile(tree, projectRoot, additionalPaths = []) {
|
|
36
|
+
const searchPaths = [...DEFAULT_STYLE_PATHS, ...additionalPaths];
|
|
37
|
+
for (const relPath of searchPaths) {
|
|
38
|
+
const fullPath = (0, path_1.join)(projectRoot, relPath);
|
|
39
|
+
if (tree.exists(fullPath)) {
|
|
40
|
+
return fullPath;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Check if project uses @tailwindcss/vite plugin
|
|
47
|
+
*/
|
|
48
|
+
function projectUsesVitePlugin(tree, projectRoot) {
|
|
49
|
+
for (const configFile of VITE_CONFIG_PATTERNS) {
|
|
50
|
+
const configPath = (0, path_1.join)(projectRoot, configFile);
|
|
51
|
+
const content = tree.read(configPath)?.toString();
|
|
52
|
+
if (content) {
|
|
53
|
+
// Check for tailwindcss import from @tailwindcss/vite and usage
|
|
54
|
+
if (content.includes('@tailwindcss/vite') &&
|
|
55
|
+
content.match(/tailwindcss\s*\(\s*\)/)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Find all projects using Tailwind v4
|
|
64
|
+
*/
|
|
65
|
+
function findTailwindProjects(projectGraph, tree, additionalStylePaths = []) {
|
|
66
|
+
const results = [];
|
|
67
|
+
for (const project of Object.values(projectGraph.nodes)) {
|
|
68
|
+
if (!project.data.root)
|
|
69
|
+
continue;
|
|
70
|
+
// First check for CSS file with @import 'tailwindcss'
|
|
71
|
+
let cssFile = findTailwindCssFile(tree, project.data.root, additionalStylePaths);
|
|
72
|
+
const usesVitePlugin = projectUsesVitePlugin(tree, project.data.root);
|
|
73
|
+
// For Vite plugin projects without @import 'tailwindcss', look for any styles.css
|
|
74
|
+
if (!cssFile && usesVitePlugin) {
|
|
75
|
+
cssFile = findAnyStylesFile(tree, project.data.root, additionalStylePaths);
|
|
76
|
+
}
|
|
77
|
+
if (cssFile || usesVitePlugin) {
|
|
78
|
+
results.push({ project, cssFile, usesVitePlugin });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Collect all transitive dependencies for a project
|
|
85
|
+
*/
|
|
86
|
+
function collectDependencies(projectName, projectGraph) {
|
|
87
|
+
const dependencies = new Set();
|
|
88
|
+
const queue = [projectName];
|
|
89
|
+
const visited = new Set();
|
|
90
|
+
while (queue.length > 0) {
|
|
91
|
+
const current = queue.shift();
|
|
92
|
+
if (!current)
|
|
93
|
+
continue;
|
|
94
|
+
if (visited.has(current))
|
|
95
|
+
continue;
|
|
96
|
+
visited.add(current);
|
|
97
|
+
const deps = projectGraph.dependencies[current] || [];
|
|
98
|
+
deps.forEach((dep) => {
|
|
99
|
+
dependencies.add(dep.target);
|
|
100
|
+
queue.push(dep.target);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return dependencies;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Update @source directives in a CSS file
|
|
107
|
+
*/
|
|
108
|
+
function updateSourceDirectives(tree, projectName, cssFilePath, projectGraph) {
|
|
109
|
+
const dependencies = collectDependencies(projectName, projectGraph);
|
|
110
|
+
// Generate @source directives for each dependency
|
|
111
|
+
const sourceDirectives = [];
|
|
112
|
+
const cssDir = (0, path_1.dirname)(cssFilePath);
|
|
113
|
+
dependencies.forEach((dep) => {
|
|
114
|
+
const project = projectGraph.nodes[dep];
|
|
115
|
+
if (project && project.data.root) {
|
|
116
|
+
// Calculate relative path from CSS file directory to dependency root
|
|
117
|
+
const relativePath = (0, path_1.relative)(cssDir, project.data.root);
|
|
118
|
+
sourceDirectives.push(`@source "${relativePath}";`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Sort for consistency
|
|
122
|
+
sourceDirectives.sort();
|
|
123
|
+
// Read current CSS content
|
|
124
|
+
const currentContent = tree.read(cssFilePath)?.toString() || '';
|
|
125
|
+
// Build the managed block
|
|
126
|
+
const managedBlock = [START_MARKER, ...sourceDirectives, END_MARKER].join('\n');
|
|
127
|
+
// Check if markers already exist
|
|
128
|
+
const hasMarkers = currentContent.includes(START_MARKER) &&
|
|
129
|
+
currentContent.includes(END_MARKER);
|
|
130
|
+
if (hasMarkers) {
|
|
131
|
+
// Extract existing managed section content for comparison
|
|
132
|
+
const markerRegex = new RegExp(`${escapeRegex(START_MARKER)}[\\s\\S]*?${escapeRegex(END_MARKER)}`);
|
|
133
|
+
const existingBlock = currentContent.match(markerRegex)?.[0] || '';
|
|
134
|
+
if (existingBlock === managedBlock) {
|
|
135
|
+
return false; // No changes needed
|
|
136
|
+
}
|
|
137
|
+
// Replace content between markers
|
|
138
|
+
const newContent = currentContent.replace(markerRegex, managedBlock);
|
|
139
|
+
tree.write(cssFilePath, newContent);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// No markers yet - need to insert them
|
|
143
|
+
// Remove any existing bare @source directives (migration from old format)
|
|
144
|
+
const cleanedContent = currentContent.replace(/\n@source\s+["'][^"']*packages\/[^"']+["'];/g, '');
|
|
145
|
+
// Try to find @import 'tailwindcss' first
|
|
146
|
+
const tailwindImportRegex = /@import\s+['"]tailwindcss['"];/;
|
|
147
|
+
const tailwindImportMatch = cleanedContent.match(tailwindImportRegex);
|
|
148
|
+
// If not found, look for any @import statement
|
|
149
|
+
const anyImportRegex = /@import\s+['"][^'"]+['"];/;
|
|
150
|
+
const anyImportMatch = cleanedContent.match(anyImportRegex);
|
|
151
|
+
const importMatch = tailwindImportMatch || anyImportMatch;
|
|
152
|
+
let newContent;
|
|
153
|
+
if (importMatch && importMatch.index !== undefined) {
|
|
154
|
+
// Insert after the import line
|
|
155
|
+
const importEndIndex = cleanedContent.indexOf('\n', importMatch.index) + 1;
|
|
156
|
+
const beforeImport = cleanedContent.substring(0, importEndIndex);
|
|
157
|
+
const afterImport = cleanedContent.substring(importEndIndex);
|
|
158
|
+
newContent = beforeImport + '\n' + managedBlock + '\n' + afterImport;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// No imports found, prepend to file
|
|
162
|
+
newContent = managedBlock + '\n\n' + cleanedContent;
|
|
163
|
+
}
|
|
164
|
+
tree.write(cssFilePath, newContent);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
function escapeRegex(str) {
|
|
168
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
169
|
+
}
|
|
170
|
+
async function updateTailwindGlobsGenerator(tree, options = {}) {
|
|
171
|
+
const projectGraph = await (0, devkit_1.createProjectGraphAsync)();
|
|
172
|
+
const updatedProjects = [];
|
|
173
|
+
// Find all Tailwind v4 projects
|
|
174
|
+
const tailwindProjects = findTailwindProjects(projectGraph, tree, options.additionalStylePaths);
|
|
175
|
+
// Update @source directives for each project with a CSS file
|
|
176
|
+
for (const { project, cssFile } of tailwindProjects) {
|
|
177
|
+
if (cssFile) {
|
|
178
|
+
const updated = updateSourceDirectives(tree, project.name, cssFile, projectGraph);
|
|
179
|
+
if (updated) {
|
|
180
|
+
updatedProjects.push(project.name);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (updatedProjects.length === 0) {
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
outOfSyncMessage: `Tailwind @source directives updated for: ${updatedProjects.join(', ')}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
exports.default = updateTailwindGlobsGenerator;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
package/generators.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"generators": {
|
|
3
|
+
"update-tailwind-globs": {
|
|
4
|
+
"factory": "./dist/generators/update-tailwind-globs",
|
|
5
|
+
"schema": "./dist/generators/schema.json",
|
|
6
|
+
"description": "Nx Sync generator to update @source directives in CSS files for Tailwind v4 monorepos"
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@juristr/nx-tailwind-sync",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"module": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
"./package.json": "./package.json",
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"nx": {
|
|
16
|
+
"targets": {
|
|
17
|
+
"build": {
|
|
18
|
+
"executor": "@nx/js:tsc",
|
|
19
|
+
"outputs": [
|
|
20
|
+
"{options.outputPath}"
|
|
21
|
+
],
|
|
22
|
+
"options": {
|
|
23
|
+
"outputPath": "packages/tailwind-sync-plugin/dist",
|
|
24
|
+
"main": "packages/tailwind-sync-plugin/src/index.ts",
|
|
25
|
+
"tsConfig": "packages/tailwind-sync-plugin/tsconfig.lib.json",
|
|
26
|
+
"rootDir": "packages/tailwind-sync-plugin/src",
|
|
27
|
+
"generatePackageJson": false,
|
|
28
|
+
"assets": [
|
|
29
|
+
{
|
|
30
|
+
"input": "./packages/tailwind-sync-plugin/src",
|
|
31
|
+
"glob": "**/!(*.ts)",
|
|
32
|
+
"output": "."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"input": "./packages/tailwind-sync-plugin/src",
|
|
36
|
+
"glob": "**/*.d.ts",
|
|
37
|
+
"output": "."
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@nx/devkit": "21.1.2",
|
|
46
|
+
"tslib": "^2.3.0"
|
|
47
|
+
},
|
|
48
|
+
"generators": "./generators.json",
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"!**/*.tsbuildinfo",
|
|
52
|
+
"generators.json"
|
|
53
|
+
],
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|