@webmate-studio/builder 0.1.6 → 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.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "description": "Webmate Studio Component Builder",
6
6
  "keywords": [
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';
@@ -69,7 +69,50 @@ export function extractTailwindClasses(html) {
69
69
  }
70
70
 
71
71
  /**
72
- * 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)
73
116
  * @param {Set<string>|Array<string>} classes - Tailwind classes to include
74
117
  * @param {Object} options - Generation options
75
118
  * @param {Object} options.designTokens - Custom design tokens (optional)
@@ -90,14 +133,10 @@ export async function generateTailwindCSS(classes, options = {}) {
90
133
 
91
134
  // Create temporary directory
92
135
  const tempDir = mkdtempSync(join(tmpdir(), 'tailwind-gen-'));
93
- const configPath = join(tempDir, 'tailwind.config.cjs');
94
136
  const inputPath = join(tempDir, 'input.css');
95
137
  const htmlPath = join(tempDir, 'content.html');
96
138
 
97
139
  try {
98
- // Tailwind v4 uses CSS-based configuration
99
- // Instead of config file, we generate CSS with custom properties and @theme
100
-
101
140
  // Default theme colors (complete design tokens)
102
141
  const defaultColors = {
103
142
  // Brand colors with variants
@@ -148,13 +187,13 @@ export async function generateTailwindCSS(classes, options = {}) {
148
187
  900: '#111827'
149
188
  },
150
189
 
151
- // Semantic text colors (direct utilities)
190
+ // Semantic text colors
152
191
  'text-body': '#374151',
153
192
  'text-heading': '#111827',
154
193
  'text-muted': '#6b7280',
155
194
  'text-inverse': '#ffffff',
156
195
 
157
- // Semantic background colors (direct utilities)
196
+ // Semantic background colors
158
197
  'bg-default': '#ffffff',
159
198
  'bg-subtle': '#f9fafb',
160
199
  'bg-elevated': '#ffffff',
@@ -168,11 +207,9 @@ export async function generateTailwindCSS(classes, options = {}) {
168
207
  // Merge with provided design tokens
169
208
  let themeColors = defaultColors;
170
209
  if (designTokens?.colors) {
171
- // Convert flat design tokens to nested structure
172
210
  const tokens = designTokens.colors;
173
211
  themeColors = {
174
212
  ...defaultColors,
175
- // Override with provided tokens
176
213
  primary: {
177
214
  DEFAULT: tokens.primary || defaultColors.primary.DEFAULT,
178
215
  light: tokens.primaryLight || defaultColors.primary.light,
@@ -230,54 +267,45 @@ export async function generateTailwindCSS(classes, options = {}) {
230
267
  };
231
268
  }
232
269
 
233
- // Create Tailwind config with safelist
234
- const config = {
235
- content: [htmlPath],
236
- safelist: classArray,
237
- theme: {
238
- extend: {
239
- colors: themeColors,
240
- fontFamily: designTokens?.typography?.fontFamily || {},
241
- fontSize: designTokens?.typography?.fontSize || {},
242
- spacing: designTokens?.spacing || {},
243
- borderRadius: designTokens?.borderRadius || {},
244
- boxShadow: designTokens?.boxShadow || {}
245
- }
246
- },
247
- plugins: []
248
- };
249
-
250
- writeFileSync(configPath, `module.exports = ${JSON.stringify(config, null, 2)};`);
251
-
252
- // Create input CSS with Tailwind directives
270
+ // Create input CSS with Tailwind v4 syntax
271
+ const themeCSS = generateThemeCSS(themeColors);
253
272
  const inputCSS = `
254
- @tailwind base;
255
- @tailwind components;
256
- @tailwind utilities;
273
+ @import "tailwindcss";
274
+
275
+ ${themeCSS}
257
276
  `.trim();
277
+
258
278
  writeFileSync(inputPath, inputCSS);
259
279
 
260
280
  // Create dummy HTML file with all classes (for content scanning)
261
281
  const dummyHTML = `<div class="${classArray.join(' ')}"></div>`;
262
282
  writeFileSync(htmlPath, dummyHTML);
263
283
 
264
- // Run Tailwind CLI
284
+ // Run Tailwind v4 CLI
285
+ // Tailwind v4 auto-detects HTML files in the same directory
265
286
  const tailwindCliPath = getTailwindCommand();
266
- const command = `node "${tailwindCliPath}" -c "${configPath}" -i "${inputPath}" ${minify ? '--minify' : ''}`;
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");
267
291
 
268
292
  const { stdout } = await execAsync(command, {
269
- maxBuffer: 10 * 1024 * 1024 // 10MB buffer
293
+ cwd: tempDir,
294
+ maxBuffer: 10 * 1024 * 1024,
295
+ env: {
296
+ ...process.env,
297
+ NODE_PATH: builderNodeModules
298
+ }
270
299
  });
271
300
 
272
301
  return stdout.trim();
273
302
 
274
303
  } catch (error) {
275
- console.error('[Tailwind Generator] Error:', error);
304
+ console.error('[Tailwind Generator v4] Error:', error);
276
305
  throw new Error(`Failed to generate Tailwind CSS: ${error.message}`);
277
306
  } finally {
278
307
  // Cleanup temp files
279
308
  try {
280
- unlinkSync(configPath);
281
309
  unlinkSync(inputPath);
282
310
  unlinkSync(htmlPath);
283
311
  } catch (e) {
@@ -1,341 +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 CLI path for execution
25
- * @returns {string} Path to tailwindcss CLI
26
- */
27
- function getTailwindCommand() {
28
- try {
29
- // Try to resolve @tailwindcss/cli package which contains the actual binary
30
- const cliPath = require.resolve('@tailwindcss/cli');
31
- return cliPath;
32
- } catch (e) {
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
- }
43
- }
44
- }
45
-
46
- /**
47
- * Extract Tailwind classes from HTML content
48
- * @param {string} html - HTML content
49
- * @returns {Set<string>} Set of unique Tailwind classes
50
- */
51
- export function extractTailwindClasses(html) {
52
- const classes = new Set();
53
-
54
- // Match class="..." and class='...'
55
- const classRegex = /class=["']([^"']+)["']/g;
56
- let match;
57
-
58
- while ((match = classRegex.exec(html)) !== null) {
59
- const classString = match[1];
60
- // Split by whitespace and add each class
61
- classString.split(/\s+/).forEach(cls => {
62
- if (cls.trim()) {
63
- classes.add(cls.trim());
64
- }
65
- });
66
- }
67
-
68
- return classes;
69
- }
70
-
71
- /**
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)
116
- * @param {Set<string>|Array<string>} classes - Tailwind classes to include
117
- * @param {Object} options - Generation options
118
- * @param {Object} options.designTokens - Custom design tokens (optional)
119
- * @param {boolean} options.minify - Minify output CSS
120
- * @returns {Promise<string>} Generated CSS
121
- */
122
- export async function generateTailwindCSS(classes, options = {}) {
123
- const {
124
- designTokens = null,
125
- minify = true
126
- } = options;
127
-
128
- const classArray = Array.isArray(classes) ? classes : Array.from(classes);
129
-
130
- if (classArray.length === 0) {
131
- return '/* No Tailwind classes found */';
132
- }
133
-
134
- // Create temporary directory
135
- const tempDir = mkdtempSync(join(tmpdir(), 'tailwind-gen-'));
136
- const inputPath = join(tempDir, 'input.css');
137
- const htmlPath = join(tempDir, 'content.html');
138
-
139
- try {
140
- // Default theme colors (complete design tokens)
141
- const defaultColors = {
142
- // Brand colors with variants
143
- primary: {
144
- DEFAULT: '#cf0ce9',
145
- light: '#fb923c',
146
- dark: '#c2410c'
147
- },
148
- secondary: {
149
- DEFAULT: '#6366f1',
150
- light: '#818cf8',
151
- dark: '#4f46e5'
152
- },
153
-
154
- // Feedback colors with variants
155
- success: {
156
- DEFAULT: '#10b981',
157
- light: '#34d399',
158
- dark: '#059669'
159
- },
160
- error: {
161
- DEFAULT: '#ef4444',
162
- light: '#f87171',
163
- dark: '#dc2626'
164
- },
165
- warning: {
166
- DEFAULT: '#f59e0b',
167
- light: '#fbbf24',
168
- dark: '#d97706'
169
- },
170
- info: {
171
- DEFAULT: '#3b82f6',
172
- light: '#60a5fa',
173
- dark: '#2563eb'
174
- },
175
-
176
- // Grays
177
- gray: {
178
- 50: '#f9fafb',
179
- 100: '#f3f4f6',
180
- 200: '#e5e7eb',
181
- 300: '#d1d5db',
182
- 400: '#9ca3af',
183
- 500: '#6b7280',
184
- 600: '#4b5563',
185
- 700: '#374151',
186
- 800: '#1f2937',
187
- 900: '#111827'
188
- },
189
-
190
- // Semantic text colors
191
- 'text-body': '#374151',
192
- 'text-heading': '#111827',
193
- 'text-muted': '#6b7280',
194
- 'text-inverse': '#ffffff',
195
-
196
- // Semantic background colors
197
- 'bg-default': '#ffffff',
198
- 'bg-subtle': '#f9fafb',
199
- 'bg-elevated': '#ffffff',
200
- 'bg-inverse': '#111827',
201
-
202
- // Basic colors
203
- black: '#000000',
204
- white: '#ffffff'
205
- };
206
-
207
- // Merge with provided design tokens
208
- let themeColors = defaultColors;
209
- if (designTokens?.colors) {
210
- const tokens = designTokens.colors;
211
- themeColors = {
212
- ...defaultColors,
213
- primary: {
214
- DEFAULT: tokens.primary || defaultColors.primary.DEFAULT,
215
- light: tokens.primaryLight || defaultColors.primary.light,
216
- dark: tokens.primaryDark || defaultColors.primary.dark
217
- },
218
- secondary: {
219
- DEFAULT: tokens.secondary || defaultColors.secondary.DEFAULT,
220
- light: tokens.secondaryLight || defaultColors.secondary.light,
221
- dark: tokens.secondaryDark || defaultColors.secondary.dark
222
- },
223
- success: {
224
- DEFAULT: tokens.success || defaultColors.success.DEFAULT,
225
- light: tokens.successLight || defaultColors.success.light,
226
- dark: tokens.successDark || defaultColors.success.dark
227
- },
228
- error: {
229
- DEFAULT: tokens.error || defaultColors.error.DEFAULT,
230
- light: tokens.errorLight || defaultColors.error.light,
231
- dark: tokens.errorDark || defaultColors.error.dark
232
- },
233
- warning: {
234
- DEFAULT: tokens.warning || defaultColors.warning.DEFAULT,
235
- light: tokens.warningLight || defaultColors.warning.light,
236
- dark: tokens.warningDark || defaultColors.warning.dark
237
- },
238
- info: {
239
- DEFAULT: tokens.info || defaultColors.info.DEFAULT,
240
- light: tokens.infoLight || defaultColors.info.light,
241
- dark: tokens.infoDark || defaultColors.info.dark
242
- },
243
- // Semantic colors
244
- 'text-body': tokens.textBody || defaultColors['text-body'],
245
- 'text-heading': tokens.textHeading || defaultColors['text-heading'],
246
- 'text-muted': tokens.textMuted || defaultColors['text-muted'],
247
- 'text-inverse': tokens.textInverse || defaultColors['text-inverse'],
248
- 'bg-default': tokens.bgDefault || defaultColors['bg-default'],
249
- 'bg-subtle': tokens.bgSubtle || defaultColors['bg-subtle'],
250
- 'bg-elevated': tokens.bgElevated || defaultColors['bg-elevated'],
251
- 'bg-inverse': tokens.bgInverse || defaultColors['bg-inverse'],
252
- // Grays
253
- gray: {
254
- 50: tokens.gray50 || defaultColors.gray[50],
255
- 100: tokens.gray100 || defaultColors.gray[100],
256
- 200: tokens.gray200 || defaultColors.gray[200],
257
- 300: tokens.gray300 || defaultColors.gray[300],
258
- 400: tokens.gray400 || defaultColors.gray[400],
259
- 500: tokens.gray500 || defaultColors.gray[500],
260
- 600: tokens.gray600 || defaultColors.gray[600],
261
- 700: tokens.gray700 || defaultColors.gray[700],
262
- 800: tokens.gray800 || defaultColors.gray[800],
263
- 900: tokens.gray900 || defaultColors.gray[900]
264
- },
265
- black: tokens.black || defaultColors.black,
266
- white: tokens.white || defaultColors.white
267
- };
268
- }
269
-
270
- // Create input CSS with Tailwind v4 syntax
271
- const themeCSS = generateThemeCSS(themeColors);
272
- const inputCSS = `
273
- @import "tailwindcss";
274
-
275
- ${themeCSS}
276
- `.trim();
277
-
278
- writeFileSync(inputPath, inputCSS);
279
-
280
- // Create dummy HTML file with all classes (for content scanning)
281
- const dummyHTML = `<div class="${classArray.join(' ')}"></div>`;
282
- writeFileSync(htmlPath, dummyHTML);
283
-
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");
291
-
292
- const { stdout } = await execAsync(command, {
293
- cwd: tempDir,
294
- maxBuffer: 10 * 1024 * 1024,
295
- env: {
296
- ...process.env,
297
- NODE_PATH: builderNodeModules
298
- }
299
- });
300
-
301
- return stdout.trim();
302
-
303
- } catch (error) {
304
- console.error('[Tailwind Generator v4] Error:', error);
305
- throw new Error(`Failed to generate Tailwind CSS: ${error.message}`);
306
- } finally {
307
- // Cleanup temp files
308
- try {
309
- unlinkSync(inputPath);
310
- unlinkSync(htmlPath);
311
- } catch (e) {
312
- // Ignore cleanup errors
313
- }
314
- }
315
- }
316
-
317
- /**
318
- * Generate Tailwind CSS for a component HTML file
319
- * @param {string} html - Component HTML content
320
- * @param {Object} options - Generation options
321
- * @returns {Promise<{css: string, classes: string[]}>} Generated CSS and extracted classes
322
- */
323
- export async function generateComponentCSS(html, options = {}) {
324
- // Extract classes from HTML
325
- const classes = extractTailwindClasses(html);
326
-
327
- if (classes.size === 0) {
328
- return {
329
- css: '/* No Tailwind classes found in component */\n',
330
- classes: []
331
- };
332
- }
333
-
334
- // Generate CSS
335
- const css = await generateTailwindCSS(classes, options);
336
-
337
- return {
338
- css,
339
- classes: Array.from(classes)
340
- };
341
- }