@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 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,3 @@
1
+ export interface UpdateTailwindGlobsGeneratorSchema {
2
+ additionalStylePaths?: string[];
3
+ }
@@ -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;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }