bertui 1.0.3 → 1.1.0

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/src/build.js CHANGED
@@ -1,27 +1,21 @@
1
- // src/build.js - COMPLETE v1.0.0 FIXED VERSION
1
+ // bertui/src/build.js - SERVER ISLANDS IMPLEMENTATION
2
2
  import { join, relative, basename, extname, dirname } from 'path';
3
3
  import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
5
  import { buildCSS } from './build/css-builder.js';
6
6
  import { loadEnvVariables, replaceEnvInCode } from './utils/env.js';
7
- import { optimizeImages, checkOptimizationTools, copyImages } from './build/image-optimizer.js';
7
+ import { copyImages } from './build/image-optimizer.js';
8
8
 
9
9
  export async function buildProduction(options = {}) {
10
10
  const root = options.root || process.cwd();
11
11
  const buildDir = join(root, '.bertuibuild');
12
12
  const outDir = join(root, 'dist');
13
13
 
14
- logger.bigLog('BUILDING FOR PRODUCTION v1.0.0', { color: 'green' });
15
- logger.info('šŸ”„ SINGLE CSS FILE SOLUTION - NO BULLSHIT');
14
+ logger.bigLog('BUILDING WITH SERVER ISLANDS šŸļø', { color: 'green' });
15
+ logger.info('šŸ”„ OPTIONAL SERVER CONTENT - THE GAME CHANGER');
16
16
 
17
- // Clean up old builds
18
- if (existsSync(buildDir)) {
19
- rmSync(buildDir, { recursive: true });
20
- }
21
- if (existsSync(outDir)) {
22
- rmSync(outDir, { recursive: true });
23
- logger.info('Cleaned dist/');
24
- }
17
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
18
+ if (existsSync(outDir)) rmSync(outDir, { recursive: true });
25
19
 
26
20
  mkdirSync(buildDir, { recursive: true });
27
21
  mkdirSync(outDir, { recursive: true });
@@ -31,31 +25,32 @@ export async function buildProduction(options = {}) {
31
25
  try {
32
26
  logger.info('Step 0: Loading environment variables...');
33
27
  const envVars = loadEnvVariables(root);
34
- if (Object.keys(envVars).length > 0) {
35
- logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
28
+
29
+ logger.info('Step 1: Compiling and detecting Server Islands...');
30
+ const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
31
+
32
+ if (serverIslands.length > 0) {
33
+ logger.bigLog('SERVER ISLANDS DETECTED šŸļø', { color: 'cyan' });
34
+ logger.table(serverIslands.map(r => ({
35
+ route: r.route,
36
+ file: r.file,
37
+ mode: 'šŸļø Server Island (SSG)'
38
+ })));
36
39
  }
37
40
 
38
- logger.info('Step 1: Compiling for production...');
39
- const { routes } = await compileForBuild(root, buildDir, envVars);
40
- logger.success(`Production compilation complete - ${routes.length} routes`);
41
+ if (clientRoutes.length > 0) {
42
+ logger.info(`Client-only routes: ${clientRoutes.length}`);
43
+ }
41
44
 
42
- logger.info('Step 2: Combining ALL CSS into ONE file...');
45
+ logger.info('Step 2: Combining CSS...');
43
46
  await buildAllCSS(root, outDir);
44
47
 
45
- logger.info('Step 3: Checking image optimization tools...');
46
- const optimizationTools = await checkOptimizationTools();
47
-
48
- logger.info('Step 4: Copying and optimizing static assets...');
49
- await copyAllStaticAssets(root, outDir, false);
48
+ logger.info('Step 3: Copying static assets...');
49
+ await copyAllStaticAssets(root, outDir);
50
50
 
51
- logger.info('Step 5: Bundling JavaScript with Bun...');
51
+ logger.info('Step 4: Bundling JavaScript...');
52
52
  const buildEntry = join(buildDir, 'main.js');
53
53
 
54
- if (!existsSync(buildEntry)) {
55
- logger.error('Build entry point not found: .bertuibuild/main.js');
56
- process.exit(1);
57
- }
58
-
59
54
  const result = await Bun.build({
60
55
  entrypoints: [buildEntry],
61
56
  outdir: join(outDir, 'assets'),
@@ -86,78 +81,375 @@ export async function buildProduction(options = {}) {
86
81
  process.exit(1);
87
82
  }
88
83
 
89
- logger.success('JavaScript bundled successfully');
84
+ logger.success('JavaScript bundled');
90
85
 
91
- logger.info('Step 6: Generating HTML files with SINGLE CSS...');
92
- await generateProductionHTML(root, outDir, result, routes);
86
+ logger.info('Step 5: Generating HTML with Server Islands...');
87
+ await generateProductionHTML(root, outDir, result, routes, serverIslands);
93
88
 
94
- // Clean up build directory
95
- if (existsSync(buildDir)) {
96
- rmSync(buildDir, { recursive: true });
97
- logger.info('Cleaned up .bertuibuild/');
98
- }
89
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
99
90
 
100
91
  const duration = Date.now() - startTime;
101
92
  logger.success(`✨ Build complete in ${duration}ms`);
102
- logger.info(`šŸ“¦ Output: ${outDir}`);
103
93
 
104
- logger.table(result.outputs.map(o => ({
105
- file: o.path.replace(outDir, ''),
106
- size: `${(o.size / 1024).toFixed(2)} KB`,
107
- type: o.kind
108
- })));
94
+ // Show summary
95
+ logger.bigLog('BUILD SUMMARY', { color: 'green' });
96
+ logger.info(`šŸ“„ Total routes: ${routes.length}`);
97
+ logger.info(`šŸļø Server Islands (SSG): ${serverIslands.length}`);
98
+ logger.info(`⚔ Client-only: ${clientRoutes.length}`);
99
+
100
+ if (serverIslands.length > 0) {
101
+ logger.success('āœ… Server Islands enabled - INSTANT content delivery!');
102
+ }
109
103
 
110
- logger.bigLog('READY TO DEPLOY', { color: 'green' });
111
- console.log('\nšŸ“¤ Deploy your app:\n');
112
- console.log(' Vercel: bunx vercel');
113
- console.log(' Netlify: bunx netlify deploy');
114
- console.log('\nšŸ” Preview locally:\n');
115
- console.log(' cd dist && bun run preview\n');
104
+ logger.bigLog('READY TO DEPLOY šŸš€', { color: 'green' });
116
105
 
117
106
  } catch (error) {
118
107
  logger.error(`Build failed: ${error.message}`);
119
- if (error.stack) {
120
- logger.error(error.stack);
108
+ if (error.stack) logger.error(error.stack);
109
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
110
+ process.exit(1);
111
+ }
112
+ }
113
+
114
+ async function compileForBuild(root, buildDir, envVars) {
115
+ const srcDir = join(root, 'src');
116
+ const pagesDir = join(srcDir, 'pages');
117
+
118
+ if (!existsSync(srcDir)) {
119
+ throw new Error('src/ directory not found!');
120
+ }
121
+
122
+ let routes = [];
123
+ let serverIslands = [];
124
+ let clientRoutes = [];
125
+
126
+ if (existsSync(pagesDir)) {
127
+ routes = await discoverRoutes(pagesDir);
128
+
129
+ // šŸļø DETECT SERVER ISLANDS
130
+ for (const route of routes) {
131
+ const sourceCode = await Bun.file(route.path).text();
132
+ const isServerIsland = sourceCode.includes('export const render = "server"');
133
+
134
+ if (isServerIsland) {
135
+ serverIslands.push(route);
136
+ logger.success(`šŸļø Server Island: ${route.route}`);
137
+ } else {
138
+ clientRoutes.push(route);
139
+ }
140
+ }
141
+ }
142
+
143
+ await compileBuildDirectory(srcDir, buildDir, root, envVars);
144
+
145
+ if (routes.length > 0) {
146
+ await generateBuildRouter(routes, buildDir);
147
+ }
148
+
149
+ return { routes, serverIslands, clientRoutes };
150
+ }
151
+
152
+ async function generateProductionHTML(root, outDir, buildResult, routes, serverIslands) {
153
+ const mainBundle = buildResult.outputs.find(o =>
154
+ o.path.includes('main') && o.kind === 'entry-point'
155
+ );
156
+
157
+ if (!mainBundle) {
158
+ logger.error('āŒ Could not find main bundle');
159
+ return;
160
+ }
161
+
162
+ const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
163
+
164
+ const { loadConfig } = await import('./config/loadConfig.js');
165
+ const config = await loadConfig(root);
166
+ const defaultMeta = config.meta || {};
167
+
168
+ for (const route of routes) {
169
+ try {
170
+ const sourceCode = await Bun.file(route.path).text();
171
+ const pageMeta = extractMetaFromSource(sourceCode);
172
+ const meta = { ...defaultMeta, ...pageMeta };
173
+
174
+ // šŸļø CHECK IF THIS IS A SERVER ISLAND
175
+ const isServerIsland = serverIslands.find(si => si.route === route.route);
176
+
177
+ let staticHTML = '';
178
+
179
+ if (isServerIsland) {
180
+ logger.info(`šŸļø Extracting static content: ${route.route}`);
181
+
182
+ // šŸļø CRITICAL: Server Islands are PURE HTML
183
+ // We extract the return statement and convert JSX to HTML
184
+ // NO react-dom/server needed - this is the beauty of it!
185
+
186
+ staticHTML = await extractStaticHTMLFromComponent(sourceCode, route.path);
187
+
188
+ if (staticHTML) {
189
+ logger.success(`āœ… Server Island rendered: ${route.route}`);
190
+ } else {
191
+ logger.warn(`āš ļø Could not extract HTML, falling back to client-only`);
192
+ }
193
+ }
194
+
195
+ const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland);
196
+
197
+ let htmlPath;
198
+ if (route.route === '/') {
199
+ htmlPath = join(outDir, 'index.html');
200
+ } else {
201
+ const routeDir = join(outDir, route.route.replace(/^\//, ''));
202
+ mkdirSync(routeDir, { recursive: true });
203
+ htmlPath = join(routeDir, 'index.html');
204
+ }
205
+
206
+ await Bun.write(htmlPath, html);
207
+
208
+ if (isServerIsland) {
209
+ logger.success(`āœ… Server Island: ${route.route} (instant content!)`);
210
+ } else {
211
+ logger.success(`āœ… Client-only: ${route.route}`);
212
+ }
213
+
214
+ } catch (error) {
215
+ logger.error(`Failed HTML for ${route.route}: ${error.message}`);
216
+ }
217
+ }
218
+ }
219
+
220
+ // šŸļø NEW: Extract static HTML from Server Island component
221
+ // This converts JSX to HTML WITHOUT using react-dom/server
222
+ // šŸļø SMARTER VALIDATOR - Ignores strings in JSX content
223
+ async function extractStaticHTMLFromComponent(sourceCode, filePath) {
224
+ try {
225
+ // STEP 1: Extract only the ACTUAL CODE (before the return statement)
226
+ // This is where imports and hooks would be
227
+ const returnMatch = sourceCode.match(/return\s*\(/);
228
+ if (!returnMatch) {
229
+ logger.warn(`āš ļø Could not find return statement in ${filePath}`);
230
+ return null;
121
231
  }
122
232
 
123
- if (existsSync(buildDir)) {
124
- rmSync(buildDir, { recursive: true });
233
+ const codeBeforeReturn = sourceCode.substring(0, returnMatch.index);
234
+ const jsxContent = sourceCode.substring(returnMatch.index);
235
+
236
+ // VALIDATE: Check only the CODE part (not JSX/text content)
237
+
238
+ // Rule 1: No React hooks (in actual code only)
239
+ const hookPatterns = [
240
+ 'useState', 'useEffect', 'useContext', 'useReducer',
241
+ 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
242
+ 'useLayoutEffect', 'useDebugValue'
243
+ ];
244
+
245
+ let hasHooks = false;
246
+ for (const hook of hookPatterns) {
247
+ // Only check the code BEFORE the JSX return
248
+ const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
249
+ if (regex.test(codeBeforeReturn)) {
250
+ logger.error(`āŒ Server Island at ${filePath} contains React hooks!`);
251
+ logger.error(` Server Islands must be pure HTML - no ${hook}, etc.`);
252
+ hasHooks = true;
253
+ break;
254
+ }
125
255
  }
126
256
 
127
- process.exit(1);
257
+ if (hasHooks) return null;
258
+
259
+ // Rule 2: No bertui/router imports (in actual code only)
260
+ // Only check ACTUAL imports at the top of the file, not in template literals
261
+ // Match: import X from 'bertui/router'
262
+ // Don't match: {`import X from 'bertui/router'`} (inside backticks)
263
+ const importLines = codeBeforeReturn.split('\n')
264
+ .filter(line => line.trim().startsWith('import'))
265
+ .join('\n');
266
+
267
+ const hasRouterImport = /from\s+['"]bertui\/router['"]/m.test(importLines);
268
+
269
+ if (hasRouterImport) {
270
+ logger.error(`āŒ Server Island at ${filePath} imports from 'bertui/router'!`);
271
+ logger.error(` Server Islands cannot use Link - use <a> tags instead.`);
272
+ return null;
273
+ }
274
+
275
+ // Rule 3: No event handlers in JSX (these are actual attributes)
276
+ const eventHandlers = [
277
+ 'onClick=',
278
+ 'onChange=',
279
+ 'onSubmit=',
280
+ 'onInput=',
281
+ 'onFocus=',
282
+ 'onBlur=',
283
+ 'onMouseEnter=',
284
+ 'onMouseLeave=',
285
+ 'onKeyDown=',
286
+ 'onKeyUp=',
287
+ 'onScroll='
288
+ ];
289
+
290
+ for (const handler of eventHandlers) {
291
+ if (jsxContent.includes(handler)) {
292
+ logger.error(`āŒ Server Island uses event handler: ${handler.replace('=', '')}`);
293
+ logger.error(` Server Islands are static HTML - no interactivity allowed`);
294
+ return null;
295
+ }
296
+ }
297
+
298
+ // NOW EXTRACT THE JSX
299
+ const fullReturnMatch = sourceCode.match(/return\s*\(([\s\S]*?)\);?\s*}/);
300
+ if (!fullReturnMatch) {
301
+ logger.warn(`āš ļø Could not extract JSX from ${filePath}`);
302
+ return null;
303
+ }
304
+
305
+ let html = fullReturnMatch[1].trim();
306
+
307
+ // STEP 2: Remove JSX comments {/* ... */}
308
+ html = html.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
309
+
310
+ // STEP 3: Convert className to class
311
+ html = html.replace(/className=/g, 'class=');
312
+
313
+ // STEP 4: Convert style objects to inline styles
314
+ // Match style={{...}} and convert to style="..."
315
+ html = html.replace(/style=\{\{([^}]+)\}\}/g, (match, styleObj) => {
316
+ // Split by comma, but be careful of commas inside values like rgba()
317
+ const props = [];
318
+ let currentProp = '';
319
+ let depth = 0;
320
+
321
+ for (let i = 0; i < styleObj.length; i++) {
322
+ const char = styleObj[i];
323
+ if (char === '(') depth++;
324
+ if (char === ')') depth--;
325
+
326
+ if (char === ',' && depth === 0) {
327
+ props.push(currentProp.trim());
328
+ currentProp = '';
329
+ } else {
330
+ currentProp += char;
331
+ }
332
+ }
333
+ if (currentProp.trim()) props.push(currentProp.trim());
334
+
335
+ // Convert each property
336
+ const cssString = props
337
+ .map(prop => {
338
+ const colonIndex = prop.indexOf(':');
339
+ if (colonIndex === -1) return '';
340
+
341
+ const key = prop.substring(0, colonIndex).trim();
342
+ const value = prop.substring(colonIndex + 1).trim();
343
+
344
+ if (!key || !value) return '';
345
+
346
+ // Convert camelCase to kebab-case
347
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
348
+ // Remove quotes from value
349
+ const cssValue = value.replace(/['"]/g, '');
350
+
351
+ return `${cssKey}: ${cssValue}`;
352
+ })
353
+ .filter(Boolean)
354
+ .join('; ');
355
+
356
+ return `style="${cssString}"`;
357
+ });
358
+
359
+ // STEP 5: Handle self-closing tags
360
+ const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
361
+ 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
362
+
363
+ html = html.replace(/<(\w+)([^>]*)\s*\/>/g, (match, tag, attrs) => {
364
+ if (voidElements.includes(tag.toLowerCase())) {
365
+ return match; // Keep void elements self-closing
366
+ } else {
367
+ return `<${tag}${attrs}></${tag}>`; // Convert to opening + closing
368
+ }
369
+ });
370
+
371
+ // STEP 6: Clean up JSX expressions
372
+ // Template literals: {`text`} -> text
373
+ html = html.replace(/\{`([^`]*)`\}/g, '$1');
374
+ // String literals: {'text'} or {"text"} -> text
375
+ html = html.replace(/\{(['"])(.*?)\1\}/g, '$2');
376
+ // Numbers: {123} -> 123
377
+ html = html.replace(/\{(\d+)\}/g, '$1');
378
+
379
+ logger.info(` Extracted ${html.length} chars of static HTML`);
380
+ return html;
381
+
382
+ } catch (error) {
383
+ logger.error(`Failed to extract HTML: ${error.message}`);
384
+ return null;
128
385
  }
129
386
  }
387
+ // Example of how the style regex should work:
388
+ // Input: style={{ background: 'rgba(0,0,0,0.05)', padding: '1.5rem', borderRadius: '8px' }}
389
+ // Output: style="background: rgba(0,0,0,0.05); padding: 1.5rem; border-radius: 8px"
390
+ function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false) {
391
+ const rootContent = staticHTML
392
+ ? `<div id="root">${staticHTML}</div>`
393
+ : '<div id="root"></div>';
394
+
395
+ const comment = isServerIsland
396
+ ? '<!-- šŸļø Server Island: Static content rendered at build time -->'
397
+ : '<!-- ⚔ Client-only: Content rendered by JavaScript -->';
398
+
399
+ return `<!DOCTYPE html>
400
+ <html lang="${meta.lang || 'en'}">
401
+ <head>
402
+ <meta charset="UTF-8">
403
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
404
+ <title>${meta.title || 'BertUI App'}</title>
405
+
406
+ <meta name="description" content="${meta.description || 'Built with BertUI'}">
407
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
408
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
409
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
410
+
411
+ <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
412
+ <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
413
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
414
+
415
+ <link rel="stylesheet" href="/styles/bertui.min.css">
416
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
417
+
418
+ <script type="importmap">
419
+ {
420
+ "imports": {
421
+ "react": "https://esm.sh/react@18.2.0",
422
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
423
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
424
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
425
+ }
426
+ }
427
+ </script>
428
+ </head>
429
+ <body>
430
+ ${comment}
431
+ ${rootContent}
432
+ <script type="module" src="/${bundlePath}"></script>
433
+ </body>
434
+ </html>`;
435
+ }
130
436
 
131
- // SIMPLE asset copying
132
- async function copyAllStaticAssets(root, outDir, optimize = true) {
437
+ // Helper functions from original build.js
438
+ async function copyAllStaticAssets(root, outDir) {
133
439
  const publicDir = join(root, 'public');
134
440
  const srcImagesDir = join(root, 'src', 'images');
135
441
 
136
- logger.info('šŸ“¦ Copying static assets...');
137
-
138
- // Copy from public/ to root of dist/
139
442
  if (existsSync(publicDir)) {
140
- logger.info(' Copying public/ directory...');
141
443
  copyImages(publicDir, outDir);
142
- } else {
143
- logger.info(' No public/ directory found');
144
444
  }
145
445
 
146
- // Copy from src/images/ to dist/images/
147
446
  if (existsSync(srcImagesDir)) {
148
- logger.info(' Copying src/images/ to dist/images/...');
149
447
  const distImagesDir = join(outDir, 'images');
150
448
  mkdirSync(distImagesDir, { recursive: true });
151
449
  copyImages(srcImagesDir, distImagesDir);
152
- } else {
153
- logger.info(' No src/images/ directory found');
154
450
  }
155
-
156
- logger.success('āœ… All assets copied');
157
451
  }
158
452
 
159
- // COMBINE ALL CSS INTO ONE FILE
160
- // COMBINE ALL CSS INTO ONE FILE - FIXED BUN API
161
453
  async function buildAllCSS(root, outDir) {
162
454
  const srcStylesDir = join(root, 'src', 'styles');
163
455
  const stylesOutDir = join(outDir, 'styles');
@@ -166,70 +458,27 @@ async function buildAllCSS(root, outDir) {
166
458
 
167
459
  if (existsSync(srcStylesDir)) {
168
460
  const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
169
- logger.info(`šŸ“¦ Found ${cssFiles.length} CSS files to combine`);
170
461
 
171
462
  if (cssFiles.length === 0) {
172
- logger.warn('āš ļø No CSS files found in src/styles/');
463
+ await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
173
464
  return;
174
465
  }
175
466
 
176
- // COMBINE ALL CSS INTO ONE FILE
177
467
  let combinedCSS = '';
178
- let totalOriginalSize = 0;
179
-
180
468
  for (const cssFile of cssFiles) {
181
469
  const srcPath = join(srcStylesDir, cssFile);
182
470
  const file = Bun.file(srcPath);
183
471
  const cssContent = await file.text();
184
- totalOriginalSize += file.size;
185
- combinedCSS += `/* === ${cssFile} === */\n${cssContent}\n\n`;
472
+ combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
186
473
  }
187
474
 
188
- // Write combined CSS
189
475
  const combinedPath = join(stylesOutDir, 'bertui.min.css');
190
476
  await Bun.write(combinedPath, combinedCSS);
477
+ await buildCSS(combinedPath, combinedPath);
191
478
 
192
- // Minify it
193
- const minified = await buildCSS(combinedPath, combinedPath);
194
-
195
- // Get final size
196
- const finalFile = Bun.file(combinedPath);
197
- const finalSize = finalFile.size / 1024;
198
- const originalSize = totalOriginalSize / 1024;
199
- const savings = ((originalSize - finalSize) / originalSize * 100).toFixed(1);
200
-
201
- logger.success(`āœ… Combined ${cssFiles.length} CSS files (${originalSize.toFixed(1)}KB) → bertui.min.css (${finalSize.toFixed(1)}KB, -${savings}%)`);
202
-
203
- } else {
204
- logger.warn('āš ļø No src/styles/ directory found');
205
- // Create empty CSS file so build doesn't fail
206
- const emptyPath = join(stylesOutDir, 'bertui.min.css');
207
- await Bun.write(emptyPath, '/* No CSS files found */');
479
+ logger.success(`āœ… Combined ${cssFiles.length} CSS files`);
208
480
  }
209
481
  }
210
- async function compileForBuild(root, buildDir, envVars) {
211
- const srcDir = join(root, 'src');
212
- const pagesDir = join(srcDir, 'pages');
213
-
214
- if (!existsSync(srcDir)) {
215
- throw new Error('src/ directory not found!');
216
- }
217
-
218
- let routes = [];
219
- if (existsSync(pagesDir)) {
220
- routes = await discoverRoutes(pagesDir);
221
- logger.info(`Found ${routes.length} routes`);
222
- }
223
-
224
- await compileBuildDirectory(srcDir, buildDir, root, envVars);
225
-
226
- if (routes.length > 0) {
227
- await generateBuildRouter(routes, buildDir);
228
- logger.info('Generated router for build');
229
- }
230
-
231
- return { routes };
232
- }
233
482
 
234
483
  async function discoverRoutes(pagesDir) {
235
484
  const routes = [];
@@ -245,12 +494,10 @@ async function discoverRoutes(pagesDir) {
245
494
  await scanDirectory(fullPath, relativePath);
246
495
  } else if (entry.isFile()) {
247
496
  const ext = extname(entry.name);
248
-
249
497
  if (ext === '.css') continue;
250
498
 
251
499
  if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
252
500
  const fileName = entry.name.replace(ext, '');
253
-
254
501
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
255
502
 
256
503
  if (fileName === 'index') {
@@ -258,13 +505,12 @@ async function discoverRoutes(pagesDir) {
258
505
  }
259
506
 
260
507
  const isDynamic = fileName.includes('[') && fileName.includes(']');
261
- const type = isDynamic ? 'dynamic' : 'static';
262
508
 
263
509
  routes.push({
264
510
  route: route === '' ? '/' : route,
265
511
  file: relativePath.replace(/\\/g, '/'),
266
512
  path: fullPath,
267
- type
513
+ type: isDynamic ? 'dynamic' : 'static'
268
514
  });
269
515
  }
270
516
  }
@@ -272,153 +518,11 @@ async function discoverRoutes(pagesDir) {
272
518
  }
273
519
 
274
520
  await scanDirectory(pagesDir);
275
-
276
- routes.sort((a, b) => {
277
- if (a.type === b.type) {
278
- return a.route.localeCompare(b.route);
279
- }
280
- return a.type === 'static' ? -1 : 1;
281
- });
521
+ routes.sort((a, b) => a.type === b.type ? a.route.localeCompare(b.route) : a.type === 'static' ? -1 : 1);
282
522
 
283
523
  return routes;
284
524
  }
285
525
 
286
- async function generateBuildRouter(routes, buildDir) {
287
- const imports = routes.map((route, i) => {
288
- const componentName = `Page${i}`;
289
- const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
290
- return `import ${componentName} from '${importPath}';`;
291
- }).join('\n');
292
-
293
- const routeConfigs = routes.map((route, i) => {
294
- const componentName = `Page${i}`;
295
- return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
296
- }).join(',\n');
297
-
298
- const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
299
-
300
- const RouterContext = createContext(null);
301
-
302
- export function useRouter() {
303
- const context = useContext(RouterContext);
304
- if (!context) {
305
- throw new Error('useRouter must be used within a Router component');
306
- }
307
- return context;
308
- }
309
-
310
- export function Router({ routes }) {
311
- const [currentRoute, setCurrentRoute] = useState(null);
312
- const [params, setParams] = useState({});
313
-
314
- useEffect(() => {
315
- matchAndSetRoute(window.location.pathname);
316
-
317
- const handlePopState = () => {
318
- matchAndSetRoute(window.location.pathname);
319
- };
320
-
321
- window.addEventListener('popstate', handlePopState);
322
- return () => window.removeEventListener('popstate', handlePopState);
323
- }, [routes]);
324
-
325
- function matchAndSetRoute(pathname) {
326
- for (const route of routes) {
327
- if (route.type === 'static' && route.path === pathname) {
328
- setCurrentRoute(route);
329
- setParams({});
330
- return;
331
- }
332
- }
333
-
334
- for (const route of routes) {
335
- if (route.type === 'dynamic') {
336
- const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
337
- const regex = new RegExp('^' + pattern + '$');
338
- const match = pathname.match(regex);
339
-
340
- if (match) {
341
- const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
342
- const extractedParams = {};
343
- paramNames.forEach((name, i) => {
344
- extractedParams[name] = match[i + 1];
345
- });
346
-
347
- setCurrentRoute(route);
348
- setParams(extractedParams);
349
- return;
350
- }
351
- }
352
- }
353
-
354
- setCurrentRoute(null);
355
- setParams({});
356
- }
357
-
358
- function navigate(path) {
359
- window.history.pushState({}, '', path);
360
- matchAndSetRoute(path);
361
- }
362
-
363
- const routerValue = {
364
- currentRoute,
365
- params,
366
- navigate,
367
- pathname: window.location.pathname
368
- };
369
-
370
- const Component = currentRoute?.component;
371
-
372
- return React.createElement(
373
- RouterContext.Provider,
374
- { value: routerValue },
375
- Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
376
- );
377
- }
378
-
379
- export function Link({ to, children, ...props }) {
380
- const { navigate } = useRouter();
381
-
382
- function handleClick(e) {
383
- e.preventDefault();
384
- navigate(to);
385
- }
386
-
387
- return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
388
- }
389
-
390
- function NotFound() {
391
- return React.createElement(
392
- 'div',
393
- {
394
- style: {
395
- display: 'flex',
396
- flexDirection: 'column',
397
- alignItems: 'center',
398
- justifyContent: 'center',
399
- minHeight: '100vh',
400
- fontFamily: 'system-ui'
401
- }
402
- },
403
- React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
404
- React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
405
- React.createElement('a', {
406
- href: '/',
407
- style: { color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }
408
- }, 'Go home')
409
- );
410
- }
411
-
412
- ${imports}
413
-
414
- export const routes = [
415
- ${routeConfigs}
416
- ];
417
- `;
418
-
419
- await Bun.write(join(buildDir, 'router.js'), routerCode);
420
- }
421
-
422
526
  async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
423
527
  const files = readdirSync(srcDir);
424
528
 
@@ -432,7 +536,6 @@ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
432
536
  await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
433
537
  } else {
434
538
  const ext = extname(file);
435
-
436
539
  if (ext === '.css') continue;
437
540
 
438
541
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
@@ -440,15 +543,12 @@ async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
440
543
  } else if (ext === '.js') {
441
544
  const outPath = join(buildDir, file);
442
545
  let code = await Bun.file(srcPath).text();
443
-
444
546
  code = removeCSSImports(code);
445
547
  code = replaceEnvInCode(code, envVars);
446
548
  code = fixBuildImports(code, srcPath, outPath, root);
447
-
448
549
  if (usesJSX(code) && !code.includes('import React')) {
449
550
  code = `import React from 'react';\n${code}`;
450
551
  }
451
-
452
552
  await Bun.write(outPath, code);
453
553
  }
454
554
  }
@@ -461,13 +561,11 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
461
561
 
462
562
  try {
463
563
  let code = await Bun.file(srcPath).text();
464
-
465
564
  code = removeCSSImports(code);
466
565
  code = replaceEnvInCode(code, envVars);
467
566
 
468
567
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
469
568
  const outPath = join(buildDir, outFilename);
470
-
471
569
  code = fixBuildImports(code, srcPath, outPath, root);
472
570
 
473
571
  const transpiler = new Bun.Transpiler({
@@ -482,13 +580,10 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
482
580
  });
483
581
 
484
582
  let compiled = await transpiler.transform(code);
485
-
486
583
  if (usesJSX(compiled) && !compiled.includes('import React')) {
487
584
  compiled = `import React from 'react';\n${compiled}`;
488
585
  }
489
-
490
586
  compiled = fixRelativeImports(compiled);
491
-
492
587
  await Bun.write(outPath, compiled);
493
588
  } catch (error) {
494
589
  logger.error(`Failed to compile ${filename}: ${error.message}`);
@@ -513,28 +608,19 @@ function removeCSSImports(code) {
513
608
  function fixBuildImports(code, srcPath, outPath, root) {
514
609
  const buildDir = join(root, '.bertuibuild');
515
610
  const routerPath = join(buildDir, 'router.js');
516
-
517
611
  const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
518
612
  const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
519
613
 
520
- code = code.replace(
521
- /from\s+['"]bertui\/router['"]/g,
522
- `from '${routerImport}'`
523
- );
524
-
614
+ code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
525
615
  return code;
526
616
  }
527
617
 
528
618
  function fixRelativeImports(code) {
529
619
  const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
530
-
531
620
  code = code.replace(importRegex, (match, prefix, path) => {
532
- if (path.endsWith('/') || /\.\w+$/.test(path)) {
533
- return match;
534
- }
621
+ if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
535
622
  return `from '${prefix}${path}.js';`;
536
623
  });
537
-
538
624
  return code;
539
625
  }
540
626
 
@@ -566,113 +652,113 @@ function extractMetaFromSource(code) {
566
652
  let match;
567
653
 
568
654
  while ((match = pairRegex.exec(metaString)) !== null) {
569
- const key = match[1];
570
- const value = match[3];
571
- meta[key] = value;
655
+ meta[match[1]] = match[3];
572
656
  }
573
657
 
574
658
  return Object.keys(meta).length > 0 ? meta : null;
575
659
  } catch (error) {
576
- logger.warn(`Could not extract meta: ${error.message}`);
577
660
  return null;
578
661
  }
579
662
  }
580
663
 
581
- // GENERATE HTML WITH SINGLE CSS FILE
582
- async function generateProductionHTML(root, outDir, buildResult, routes) {
583
- logger.info('Step 6: Generating HTML files with SINGLE CSS...');
584
-
585
- const mainBundle = buildResult.outputs.find(o =>
586
- o.path.includes('main') && o.kind === 'entry-point'
587
- );
588
-
589
- if (!mainBundle) {
590
- logger.error('āŒ Could not find main bundle');
591
- return;
592
- }
593
-
594
- const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
595
- logger.info(`Main JS bundle: /${bundlePath}`);
596
-
597
- // Load config
598
- const { loadConfig } = await import('./config/loadConfig.js');
599
- const config = await loadConfig(root);
600
- const defaultMeta = config.meta || {};
664
+ async function generateBuildRouter(routes, buildDir) {
665
+ const imports = routes.map((route, i) => {
666
+ const componentName = `Page${i}`;
667
+ const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
668
+ return `import ${componentName} from '${importPath}';`;
669
+ }).join('\n');
601
670
 
602
- logger.info(`Generating HTML for ${routes.length} routes...`);
671
+ const routeConfigs = routes.map((route, i) => {
672
+ const componentName = `Page${i}`;
673
+ return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
674
+ }).join(',\n');
603
675
 
604
- for (const route of routes) {
605
- try {
606
- const sourceCode = await Bun.file(route.path).text();
607
- const pageMeta = extractMetaFromSource(sourceCode);
608
- const meta = { ...defaultMeta, ...pageMeta };
609
-
610
- const html = generateHTML(meta, route, bundlePath);
611
-
612
- let htmlPath;
613
- if (route.route === '/') {
614
- htmlPath = join(outDir, 'index.html');
615
- } else {
616
- const routeDir = join(outDir, route.route.replace(/^\//, ''));
617
- mkdirSync(routeDir, { recursive: true });
618
- htmlPath = join(routeDir, 'index.html');
676
+ const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
677
+
678
+ const RouterContext = createContext(null);
679
+
680
+ export function useRouter() {
681
+ const context = useContext(RouterContext);
682
+ if (!context) throw new Error('useRouter must be used within a Router');
683
+ return context;
684
+ }
685
+
686
+ export function Router({ routes }) {
687
+ const [currentRoute, setCurrentRoute] = useState(null);
688
+ const [params, setParams] = useState({});
689
+
690
+ useEffect(() => {
691
+ matchAndSetRoute(window.location.pathname);
692
+ const handlePopState = () => matchAndSetRoute(window.location.pathname);
693
+ window.addEventListener('popstate', handlePopState);
694
+ return () => window.removeEventListener('popstate', handlePopState);
695
+ }, [routes]);
696
+
697
+ function matchAndSetRoute(pathname) {
698
+ for (const route of routes) {
699
+ if (route.type === 'static' && route.path === pathname) {
700
+ setCurrentRoute(route);
701
+ setParams({});
702
+ return;
703
+ }
704
+ }
705
+ for (const route of routes) {
706
+ if (route.type === 'dynamic') {
707
+ const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
708
+ const regex = new RegExp('^' + pattern + '$');
709
+ const match = pathname.match(regex);
710
+ if (match) {
711
+ const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
712
+ const extractedParams = {};
713
+ paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
714
+ setCurrentRoute(route);
715
+ setParams(extractedParams);
716
+ return;
717
+ }
619
718
  }
620
-
621
- await Bun.write(htmlPath, html);
622
- logger.success(`āœ… Generated: ${route.route === '/' ? '/' : route.route}`);
623
-
624
- } catch (error) {
625
- logger.error(`Failed to generate HTML for ${route.route}: ${error.message}`);
626
719
  }
720
+ setCurrentRoute(null);
721
+ setParams({});
627
722
  }
628
-
629
- logger.success('✨ All HTML files generated with SINGLE CSS file!');
723
+
724
+ function navigate(path) {
725
+ window.history.pushState({}, '', path);
726
+ matchAndSetRoute(path);
727
+ }
728
+
729
+ const Component = currentRoute?.component;
730
+ return React.createElement(
731
+ RouterContext.Provider,
732
+ { value: { currentRoute, params, navigate, pathname: window.location.pathname } },
733
+ Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
734
+ );
630
735
  }
631
736
 
632
- function generateHTML(meta, route, bundlePath) {
633
- return `<!DOCTYPE html>
634
- <html lang="${meta.lang || 'en'}">
635
- <head>
636
- <meta charset="UTF-8">
637
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
638
- <title>${meta.title || 'BertUI App'}</title>
639
-
640
- <meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
641
- ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
642
- ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
643
- ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
644
-
645
- <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
646
- <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
647
- ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
648
- <meta property="og:type" content="website">
649
- <meta property="og:url" content="${route.route}">
650
-
651
- <meta name="twitter:card" content="summary_large_image">
652
- <meta name="twitter:title" content="${meta.twitterTitle || meta.ogTitle || meta.title || 'BertUI App'}">
653
- <meta name="twitter:description" content="${meta.twitterDescription || meta.ogDescription || meta.description || 'Built with BertUI'}">
654
- ${meta.twitterImage || meta.ogImage ? `<meta name="twitter:image" content="${meta.twitterImage || meta.ogImage}">` : ''}
655
-
656
- <!-- šŸ”„ ONE CSS FILE FOR ALL PAGES - NO BULLSHIT -->
657
- <link rel="stylesheet" href="/styles/bertui.min.css">
658
-
659
- <link rel="icon" type="image/svg+xml" href="/favicon.svg">
660
- <link rel="canonical" href="${route.route}">
737
+ export function Link({ to, children, ...props }) {
738
+ const { navigate } = useRouter();
739
+ return React.createElement('a', {
740
+ href: to,
741
+ onClick: (e) => { e.preventDefault(); navigate(to); },
742
+ ...props
743
+ }, children);
744
+ }
745
+
746
+ function NotFound() {
747
+ return React.createElement('div', {
748
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
749
+ justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
750
+ },
751
+ React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
752
+ React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
753
+ React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
754
+ );
755
+ }
756
+
757
+ ${imports}
758
+
759
+ export const routes = [
760
+ ${routeConfigs}
761
+ ];`;
661
762
 
662
- <script type="importmap">
663
- {
664
- "imports": {
665
- "react": "https://esm.sh/react@18.2.0",
666
- "react-dom": "https://esm.sh/react-dom@18.2.0",
667
- "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
668
- "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
669
- }
670
- }
671
- </script>
672
- </head>
673
- <body>
674
- <div id="root"></div>
675
- <script type="module" src="/${bundlePath}"></script>
676
- </body>
677
- </html>`;
763
+ await Bun.write(join(buildDir, 'router.js'), routerCode);
678
764
  }