@webmate-studio/builder 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmate-studio/builder",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
@@ -24,6 +24,8 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
+ "@tailwindcss/cli": "^4.1.0",
28
+ "@webmate-studio/builder": "^0.1.5",
27
29
  "@webmate-studio/core": "^0.1.0",
28
30
  "@webmate-studio/parser": "^0.1.0",
29
31
  "alpinejs": "^3.15.0",
@@ -38,7 +40,6 @@
38
40
  "react-dom": "^19.2.0",
39
41
  "svelte": "^5.41.2",
40
42
  "tailwindcss": "^4.1.0",
41
- "@tailwindcss/cli": "^4.1.0",
42
43
  "vue": "^3.5.22"
43
44
  }
44
45
  }
package/src/build.js CHANGED
@@ -5,7 +5,7 @@ import { parseComponent } from '@webmate-studio/parser';
5
5
  import { loadConfig, logger } from '@webmate-studio/core';
6
6
  import { cleanComponentHTML, extractStyles } from './html-cleaner.js';
7
7
  import { generateManifest } from './manifest.js';
8
- import { generateComponentCSS } from './tailwind-generator-v4.js';
8
+ import { generateComponentCSS } from './tailwind-generator.js';
9
9
  import { bundleComponentIslands } from './bundler.js';
10
10
  import { parseDocument } from 'htmlparser2';
11
11
  import { DomUtils } from 'htmlparser2';
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Tailwind CSS Generator for Components
3
- * Generates scoped Tailwind CSS for each component based on used classes
2
+ * Tailwind CSS v4 Generator for Components
3
+ * Generates scoped Tailwind CSS using Tailwind v4's new CSS-based configuration
4
4
  */
5
5
 
6
6
  import { exec } from 'child_process';
7
7
  import { promisify } from 'util';
8
- import { writeFileSync, readFileSync, unlinkSync, mkdtempSync } from 'fs';
8
+ import { writeFileSync, unlinkSync, mkdtempSync } from 'fs';
9
9
  import { join, dirname } from 'path';
10
10
  import { tmpdir } from 'os';
11
11
  import { fileURLToPath } from 'url';
@@ -21,27 +21,25 @@ import { createRequire } from 'module';
21
21
  const require = createRequire(import.meta.url);
22
22
 
23
23
  /**
24
- * Get tailwindcss command for execution
25
- * @returns {string} Command to run tailwindcss
24
+ * Get tailwindcss CLI path for execution
25
+ * @returns {string} Path to tailwindcss CLI
26
26
  */
27
27
  function getTailwindCommand() {
28
28
  try {
29
- // Try to resolve tailwindcss package
30
- const tailwindPackagePath = require.resolve('tailwindcss/package.json');
31
- const tailwindDir = dirname(tailwindPackagePath);
32
-
33
- // Check if we're on Windows
34
- const isWindows = process.platform === 'win32';
35
- const binName = isWindows ? 'tailwindcss.cmd' : 'tailwindcss';
36
-
37
- // Build path to bin
38
- const binPath = join(tailwindDir, '..', '.bin', binName);
39
-
40
- // Return just npx command - will be resolved at runtime
41
- return 'npx tailwindcss';
29
+ // Try to resolve @tailwindcss/cli package which contains the actual binary
30
+ const cliPath = require.resolve('@tailwindcss/cli');
31
+ return cliPath;
42
32
  } catch (e) {
43
- // Fallback to npx
44
- return 'npx tailwindcss';
33
+ // Fallback: try to find tailwindcss package and use its CLI
34
+ try {
35
+ const tailwindPackagePath = require.resolve('tailwindcss/package.json');
36
+ const tailwindDir = dirname(tailwindPackagePath);
37
+ // Tailwind v4 uses @tailwindcss/cli
38
+ const cliPath = join(tailwindDir, '..', '@tailwindcss', 'cli', 'dist', 'index.js');
39
+ return cliPath;
40
+ } catch (e2) {
41
+ throw new Error('Could not find @tailwindcss/cli. Please install it: npm install @tailwindcss/cli');
42
+ }
45
43
  }
46
44
  }
47
45
 
@@ -71,7 +69,50 @@ export function extractTailwindClasses(html) {
71
69
  }
72
70
 
73
71
  /**
74
- * Generate Tailwind CSS for specific classes
72
+ * Generate CSS with custom theme colors and design tokens for Tailwind v4
73
+ * @param {Object} colors - Color definitions
74
+ * @param {Object} designTokens - Complete design tokens (optional)
75
+ * @returns {string} CSS with @theme directive
76
+ */
77
+ function generateThemeCSS(colors, designTokens = null) {
78
+ let themeCSS = `@theme {\n`;
79
+
80
+ // Helper to flatten nested colors
81
+ function flattenColors(obj, prefix = '') {
82
+ let css = '';
83
+ for (const [key, value] of Object.entries(obj)) {
84
+ const colorKey = prefix ? `${prefix}-${key}` : key;
85
+
86
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
87
+ // Nested color (e.g., primary.light)
88
+ if (value.DEFAULT) {
89
+ css += ` --color-${colorKey}: ${value.DEFAULT};\n`;
90
+ }
91
+ css += flattenColors(value, colorKey);
92
+ } else {
93
+ // Simple color value
94
+ css += ` --color-${colorKey}: ${value};\n`;
95
+ }
96
+ }
97
+ return css;
98
+ }
99
+
100
+ themeCSS += flattenColors(colors);
101
+
102
+ // Add spacing if provided in design tokens
103
+ if (designTokens?.spacing) {
104
+ if (typeof designTokens.spacing === 'string') {
105
+ themeCSS += ` --spacing: ${designTokens.spacing};\n`;
106
+ }
107
+ }
108
+
109
+ themeCSS += `}\n`;
110
+
111
+ return themeCSS;
112
+ }
113
+
114
+ /**
115
+ * Generate Tailwind CSS for specific classes (Tailwind v4)
75
116
  * @param {Set<string>|Array<string>} classes - Tailwind classes to include
76
117
  * @param {Object} options - Generation options
77
118
  * @param {Object} options.designTokens - Custom design tokens (optional)
@@ -92,14 +133,10 @@ export async function generateTailwindCSS(classes, options = {}) {
92
133
 
93
134
  // Create temporary directory
94
135
  const tempDir = mkdtempSync(join(tmpdir(), 'tailwind-gen-'));
95
- const configPath = join(tempDir, 'tailwind.config.cjs');
96
136
  const inputPath = join(tempDir, 'input.css');
97
137
  const htmlPath = join(tempDir, 'content.html');
98
138
 
99
139
  try {
100
- // Tailwind v4 uses CSS-based configuration
101
- // Instead of config file, we generate CSS with custom properties and @theme
102
-
103
140
  // Default theme colors (complete design tokens)
104
141
  const defaultColors = {
105
142
  // Brand colors with variants
@@ -150,13 +187,13 @@ export async function generateTailwindCSS(classes, options = {}) {
150
187
  900: '#111827'
151
188
  },
152
189
 
153
- // Semantic text colors (direct utilities)
190
+ // Semantic text colors
154
191
  'text-body': '#374151',
155
192
  'text-heading': '#111827',
156
193
  'text-muted': '#6b7280',
157
194
  'text-inverse': '#ffffff',
158
195
 
159
- // Semantic background colors (direct utilities)
196
+ // Semantic background colors
160
197
  'bg-default': '#ffffff',
161
198
  'bg-subtle': '#f9fafb',
162
199
  'bg-elevated': '#ffffff',
@@ -170,11 +207,9 @@ export async function generateTailwindCSS(classes, options = {}) {
170
207
  // Merge with provided design tokens
171
208
  let themeColors = defaultColors;
172
209
  if (designTokens?.colors) {
173
- // Convert flat design tokens to nested structure
174
210
  const tokens = designTokens.colors;
175
211
  themeColors = {
176
212
  ...defaultColors,
177
- // Override with provided tokens
178
213
  primary: {
179
214
  DEFAULT: tokens.primary || defaultColors.primary.DEFAULT,
180
215
  light: tokens.primaryLight || defaultColors.primary.light,
@@ -232,54 +267,45 @@ export async function generateTailwindCSS(classes, options = {}) {
232
267
  };
233
268
  }
234
269
 
235
- // Create Tailwind config with safelist
236
- const config = {
237
- content: [htmlPath],
238
- safelist: classArray,
239
- theme: {
240
- extend: {
241
- colors: themeColors,
242
- fontFamily: designTokens?.typography?.fontFamily || {},
243
- fontSize: designTokens?.typography?.fontSize || {},
244
- spacing: designTokens?.spacing || {},
245
- borderRadius: designTokens?.borderRadius || {},
246
- boxShadow: designTokens?.boxShadow || {}
247
- }
248
- },
249
- plugins: []
250
- };
251
-
252
- writeFileSync(configPath, `module.exports = ${JSON.stringify(config, null, 2)};`);
253
-
254
- // Create input CSS with Tailwind directives
270
+ // Create input CSS with Tailwind v4 syntax
271
+ const themeCSS = generateThemeCSS(themeColors);
255
272
  const inputCSS = `
256
- @tailwind base;
257
- @tailwind components;
258
- @tailwind utilities;
273
+ @import "tailwindcss";
274
+
275
+ ${themeCSS}
259
276
  `.trim();
277
+
260
278
  writeFileSync(inputPath, inputCSS);
261
279
 
262
280
  // Create dummy HTML file with all classes (for content scanning)
263
281
  const dummyHTML = `<div class="${classArray.join(' ')}"></div>`;
264
282
  writeFileSync(htmlPath, dummyHTML);
265
283
 
266
- // Run Tailwind CLI
267
- const tailwindCmd = getTailwindCommand();
268
- const command = `${tailwindCmd} -c "${configPath}" -i "${inputPath}" ${minify ? '--minify' : ''}`;
284
+ // Run Tailwind v4 CLI
285
+ // Tailwind v4 auto-detects HTML files in the same directory
286
+ const tailwindCliPath = getTailwindCommand();
287
+ const command = `node "${tailwindCliPath}" -i "${inputPath}" ${minify ? '--minify' : ''}`;
288
+
289
+ // Set NODE_PATH so tailwindcss module can be resolved
290
+ const builderNodeModules = join(__dirname, "..", "node_modules");
269
291
 
270
292
  const { stdout } = await execAsync(command, {
271
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer
293
+ cwd: tempDir,
294
+ maxBuffer: 10 * 1024 * 1024,
295
+ env: {
296
+ ...process.env,
297
+ NODE_PATH: builderNodeModules
298
+ }
272
299
  });
273
300
 
274
301
  return stdout.trim();
275
302
 
276
303
  } catch (error) {
277
- console.error('[Tailwind Generator] Error:', error);
304
+ console.error('[Tailwind Generator v4] Error:', error);
278
305
  throw new Error(`Failed to generate Tailwind CSS: ${error.message}`);
279
306
  } finally {
280
307
  // Cleanup temp files
281
308
  try {
282
- unlinkSync(configPath);
283
309
  unlinkSync(inputPath);
284
310
  unlinkSync(htmlPath);
285
311
  } catch (e) {
@@ -1,343 +0,0 @@
1
- /**
2
- * Tailwind CSS v4 Generator for Components
3
- * Generates scoped Tailwind CSS using Tailwind v4's new CSS-based configuration
4
- */
5
-
6
- import { exec } from 'child_process';
7
- import { promisify } from 'util';
8
- import { writeFileSync, unlinkSync, mkdtempSync } from 'fs';
9
- import { join, dirname } from 'path';
10
- import { tmpdir } from 'os';
11
- import { fileURLToPath } from 'url';
12
-
13
- const execAsync = promisify(exec);
14
-
15
- // Get the directory where this module is located
16
- const __filename = fileURLToPath(import.meta.url);
17
- const __dirname = dirname(__filename);
18
-
19
- // Import createRequire for resolving packages
20
- import { createRequire } from 'module';
21
- const require = createRequire(import.meta.url);
22
-
23
- /**
24
- * Get tailwindcss command for execution
25
- * @returns {string} Command to run tailwindcss
26
- */
27
- function getTailwindCommand() {
28
- try {
29
- // Try to resolve tailwindcss package
30
- const tailwindPackagePath = require.resolve('tailwindcss/package.json');
31
- const tailwindDir = dirname(tailwindPackagePath);
32
-
33
- // Check if we're on Windows
34
- const isWindows = process.platform === 'win32';
35
- const binName = isWindows ? 'tailwindcss.cmd' : 'tailwindcss';
36
-
37
- // Build path to bin
38
- const binPath = join(tailwindDir, '..', '.bin', binName);
39
-
40
- // Return just npx command - will be resolved at runtime
41
- return 'npx tailwindcss';
42
- } catch (e) {
43
- // Fallback to npx
44
- return 'npx tailwindcss';
45
- }
46
- }
47
-
48
- /**
49
- * Extract Tailwind classes from HTML content
50
- * @param {string} html - HTML content
51
- * @returns {Set<string>} Set of unique Tailwind classes
52
- */
53
- export function extractTailwindClasses(html) {
54
- const classes = new Set();
55
-
56
- // Match class="..." and class='...'
57
- const classRegex = /class=["']([^"']+)["']/g;
58
- let match;
59
-
60
- while ((match = classRegex.exec(html)) !== null) {
61
- const classString = match[1];
62
- // Split by whitespace and add each class
63
- classString.split(/\s+/).forEach(cls => {
64
- if (cls.trim()) {
65
- classes.add(cls.trim());
66
- }
67
- });
68
- }
69
-
70
- return classes;
71
- }
72
-
73
- /**
74
- * Generate CSS with custom theme colors and design tokens for Tailwind v4
75
- * @param {Object} colors - Color definitions
76
- * @param {Object} designTokens - Complete design tokens (optional)
77
- * @returns {string} CSS with @theme directive
78
- */
79
- function generateThemeCSS(colors, designTokens = null) {
80
- let themeCSS = `@theme {\n`;
81
-
82
- // Helper to flatten nested colors
83
- function flattenColors(obj, prefix = '') {
84
- let css = '';
85
- for (const [key, value] of Object.entries(obj)) {
86
- const colorKey = prefix ? `${prefix}-${key}` : key;
87
-
88
- if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
89
- // Nested color (e.g., primary.light)
90
- if (value.DEFAULT) {
91
- css += ` --color-${colorKey}: ${value.DEFAULT};\n`;
92
- }
93
- css += flattenColors(value, colorKey);
94
- } else {
95
- // Simple color value
96
- css += ` --color-${colorKey}: ${value};\n`;
97
- }
98
- }
99
- return css;
100
- }
101
-
102
- themeCSS += flattenColors(colors);
103
-
104
- // Add spacing if provided in design tokens
105
- if (designTokens?.spacing) {
106
- if (typeof designTokens.spacing === 'string') {
107
- themeCSS += ` --spacing: ${designTokens.spacing};\n`;
108
- }
109
- }
110
-
111
- themeCSS += `}\n`;
112
-
113
- return themeCSS;
114
- }
115
-
116
- /**
117
- * Generate Tailwind CSS for specific classes (Tailwind v4)
118
- * @param {Set<string>|Array<string>} classes - Tailwind classes to include
119
- * @param {Object} options - Generation options
120
- * @param {Object} options.designTokens - Custom design tokens (optional)
121
- * @param {boolean} options.minify - Minify output CSS
122
- * @returns {Promise<string>} Generated CSS
123
- */
124
- export async function generateTailwindCSS(classes, options = {}) {
125
- const {
126
- designTokens = null,
127
- minify = true
128
- } = options;
129
-
130
- const classArray = Array.isArray(classes) ? classes : Array.from(classes);
131
-
132
- if (classArray.length === 0) {
133
- return '/* No Tailwind classes found */';
134
- }
135
-
136
- // Create temporary directory
137
- const tempDir = mkdtempSync(join(tmpdir(), 'tailwind-gen-'));
138
- const inputPath = join(tempDir, 'input.css');
139
- const htmlPath = join(tempDir, 'content.html');
140
-
141
- try {
142
- // Default theme colors (complete design tokens)
143
- const defaultColors = {
144
- // Brand colors with variants
145
- primary: {
146
- DEFAULT: '#cf0ce9',
147
- light: '#fb923c',
148
- dark: '#c2410c'
149
- },
150
- secondary: {
151
- DEFAULT: '#6366f1',
152
- light: '#818cf8',
153
- dark: '#4f46e5'
154
- },
155
-
156
- // Feedback colors with variants
157
- success: {
158
- DEFAULT: '#10b981',
159
- light: '#34d399',
160
- dark: '#059669'
161
- },
162
- error: {
163
- DEFAULT: '#ef4444',
164
- light: '#f87171',
165
- dark: '#dc2626'
166
- },
167
- warning: {
168
- DEFAULT: '#f59e0b',
169
- light: '#fbbf24',
170
- dark: '#d97706'
171
- },
172
- info: {
173
- DEFAULT: '#3b82f6',
174
- light: '#60a5fa',
175
- dark: '#2563eb'
176
- },
177
-
178
- // Grays
179
- gray: {
180
- 50: '#f9fafb',
181
- 100: '#f3f4f6',
182
- 200: '#e5e7eb',
183
- 300: '#d1d5db',
184
- 400: '#9ca3af',
185
- 500: '#6b7280',
186
- 600: '#4b5563',
187
- 700: '#374151',
188
- 800: '#1f2937',
189
- 900: '#111827'
190
- },
191
-
192
- // Semantic text colors
193
- 'text-body': '#374151',
194
- 'text-heading': '#111827',
195
- 'text-muted': '#6b7280',
196
- 'text-inverse': '#ffffff',
197
-
198
- // Semantic background colors
199
- 'bg-default': '#ffffff',
200
- 'bg-subtle': '#f9fafb',
201
- 'bg-elevated': '#ffffff',
202
- 'bg-inverse': '#111827',
203
-
204
- // Basic colors
205
- black: '#000000',
206
- white: '#ffffff'
207
- };
208
-
209
- // Merge with provided design tokens
210
- let themeColors = defaultColors;
211
- if (designTokens?.colors) {
212
- const tokens = designTokens.colors;
213
- themeColors = {
214
- ...defaultColors,
215
- primary: {
216
- DEFAULT: tokens.primary || defaultColors.primary.DEFAULT,
217
- light: tokens.primaryLight || defaultColors.primary.light,
218
- dark: tokens.primaryDark || defaultColors.primary.dark
219
- },
220
- secondary: {
221
- DEFAULT: tokens.secondary || defaultColors.secondary.DEFAULT,
222
- light: tokens.secondaryLight || defaultColors.secondary.light,
223
- dark: tokens.secondaryDark || defaultColors.secondary.dark
224
- },
225
- success: {
226
- DEFAULT: tokens.success || defaultColors.success.DEFAULT,
227
- light: tokens.successLight || defaultColors.success.light,
228
- dark: tokens.successDark || defaultColors.success.dark
229
- },
230
- error: {
231
- DEFAULT: tokens.error || defaultColors.error.DEFAULT,
232
- light: tokens.errorLight || defaultColors.error.light,
233
- dark: tokens.errorDark || defaultColors.error.dark
234
- },
235
- warning: {
236
- DEFAULT: tokens.warning || defaultColors.warning.DEFAULT,
237
- light: tokens.warningLight || defaultColors.warning.light,
238
- dark: tokens.warningDark || defaultColors.warning.dark
239
- },
240
- info: {
241
- DEFAULT: tokens.info || defaultColors.info.DEFAULT,
242
- light: tokens.infoLight || defaultColors.info.light,
243
- dark: tokens.infoDark || defaultColors.info.dark
244
- },
245
- // Semantic colors
246
- 'text-body': tokens.textBody || defaultColors['text-body'],
247
- 'text-heading': tokens.textHeading || defaultColors['text-heading'],
248
- 'text-muted': tokens.textMuted || defaultColors['text-muted'],
249
- 'text-inverse': tokens.textInverse || defaultColors['text-inverse'],
250
- 'bg-default': tokens.bgDefault || defaultColors['bg-default'],
251
- 'bg-subtle': tokens.bgSubtle || defaultColors['bg-subtle'],
252
- 'bg-elevated': tokens.bgElevated || defaultColors['bg-elevated'],
253
- 'bg-inverse': tokens.bgInverse || defaultColors['bg-inverse'],
254
- // Grays
255
- gray: {
256
- 50: tokens.gray50 || defaultColors.gray[50],
257
- 100: tokens.gray100 || defaultColors.gray[100],
258
- 200: tokens.gray200 || defaultColors.gray[200],
259
- 300: tokens.gray300 || defaultColors.gray[300],
260
- 400: tokens.gray400 || defaultColors.gray[400],
261
- 500: tokens.gray500 || defaultColors.gray[500],
262
- 600: tokens.gray600 || defaultColors.gray[600],
263
- 700: tokens.gray700 || defaultColors.gray[700],
264
- 800: tokens.gray800 || defaultColors.gray[800],
265
- 900: tokens.gray900 || defaultColors.gray[900]
266
- },
267
- black: tokens.black || defaultColors.black,
268
- white: tokens.white || defaultColors.white
269
- };
270
- }
271
-
272
- // Create input CSS with Tailwind v4 syntax
273
- const themeCSS = generateThemeCSS(themeColors);
274
- const inputCSS = `
275
- @import "tailwindcss";
276
-
277
- ${themeCSS}
278
- `.trim();
279
-
280
- writeFileSync(inputPath, inputCSS);
281
-
282
- // Create dummy HTML file with all classes (for content scanning)
283
- const dummyHTML = `<div class="${classArray.join(' ')}"></div>`;
284
- writeFileSync(htmlPath, dummyHTML);
285
-
286
- // Run Tailwind v4 CLI
287
- // Tailwind v4 auto-detects HTML files in the same directory
288
- const tailwindCmd = getTailwindCommand();
289
- const command = `${tailwindCmd} -i "${inputPath}" ${minify ? '--minify' : ''}`;
290
-
291
- // Set NODE_PATH so tailwindcss module can be resolved
292
- const builderNodeModules = join(__dirname, "..", "node_modules");
293
-
294
- const { stdout } = await execAsync(command, {
295
- cwd: tempDir,
296
- maxBuffer: 10 * 1024 * 1024,
297
- env: {
298
- ...process.env,
299
- NODE_PATH: builderNodeModules
300
- }
301
- });
302
-
303
- return stdout.trim();
304
-
305
- } catch (error) {
306
- console.error('[Tailwind Generator v4] Error:', error);
307
- throw new Error(`Failed to generate Tailwind CSS: ${error.message}`);
308
- } finally {
309
- // Cleanup temp files
310
- try {
311
- unlinkSync(inputPath);
312
- unlinkSync(htmlPath);
313
- } catch (e) {
314
- // Ignore cleanup errors
315
- }
316
- }
317
- }
318
-
319
- /**
320
- * Generate Tailwind CSS for a component HTML file
321
- * @param {string} html - Component HTML content
322
- * @param {Object} options - Generation options
323
- * @returns {Promise<{css: string, classes: string[]}>} Generated CSS and extracted classes
324
- */
325
- export async function generateComponentCSS(html, options = {}) {
326
- // Extract classes from HTML
327
- const classes = extractTailwindClasses(html);
328
-
329
- if (classes.size === 0) {
330
- return {
331
- css: '/* No Tailwind classes found in component */\n',
332
- classes: []
333
- };
334
- }
335
-
336
- // Generate CSS
337
- const css = await generateTailwindCSS(classes, options);
338
-
339
- return {
340
- css,
341
- classes: Array.from(classes)
342
- };
343
- }