bertui 1.1.0 β†’ 1.1.2

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,10 +1,17 @@
1
- // bertui/src/build.js - SERVER ISLANDS IMPLEMENTATION
2
- import { join, relative, basename, extname, dirname } from 'path';
3
- import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
1
+ // bertui/src/build.js - FIXED BUNDLING
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync, rmSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
- import { buildCSS } from './build/css-builder.js';
6
- import { loadEnvVariables, replaceEnvInCode } from './utils/env.js';
7
- import { copyImages } from './build/image-optimizer.js';
5
+ import { loadEnvVariables } from './utils/env.js';
6
+ import { runPageBuilder } from './pagebuilder/core.js';
7
+
8
+ // Import modular components
9
+ import { compileForBuild } from './build/compiler/index.js';
10
+ import { buildAllCSS } from './build/processors/css-builder.js';
11
+ import { copyAllStaticAssets } from './build/processors/asset-processor.js';
12
+ import { generateProductionHTML } from './build/generators/html-generator.js';
13
+ import { generateSitemap } from './build/generators/sitemap-generator.js';
14
+ import { generateRobots } from './build/generators/robots-generator.js';
8
15
 
9
16
  export async function buildProduction(options = {}) {
10
17
  const root = options.root || process.cwd();
@@ -14,18 +21,29 @@ export async function buildProduction(options = {}) {
14
21
  logger.bigLog('BUILDING WITH SERVER ISLANDS 🏝️', { color: 'green' });
15
22
  logger.info('πŸ”₯ OPTIONAL SERVER CONTENT - THE GAME CHANGER');
16
23
 
24
+ // Clean directories
17
25
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
18
26
  if (existsSync(outDir)) rmSync(outDir, { recursive: true });
19
-
20
27
  mkdirSync(buildDir, { recursive: true });
21
28
  mkdirSync(outDir, { recursive: true });
22
29
 
23
30
  const startTime = Date.now();
24
31
 
25
32
  try {
33
+ // Step 0: Environment
26
34
  logger.info('Step 0: Loading environment variables...');
27
35
  const envVars = loadEnvVariables(root);
28
36
 
37
+ // Step 0.5: Load config and run Page Builder
38
+ const { loadConfig } = await import('./config/loadConfig.js');
39
+ const config = await loadConfig(root);
40
+
41
+ if (config.pageBuilder) {
42
+ logger.info('Step 0.5: Running Page Builder...');
43
+ await runPageBuilder(root, config);
44
+ }
45
+
46
+ // Step 1: Compilation
29
47
  logger.info('Step 1: Compiling and detecting Server Islands...');
30
48
  const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
31
49
 
@@ -38,19 +56,57 @@ export async function buildProduction(options = {}) {
38
56
  })));
39
57
  }
40
58
 
41
- if (clientRoutes.length > 0) {
42
- logger.info(`Client-only routes: ${clientRoutes.length}`);
43
- }
44
-
59
+ // Step 2: CSS Processing
45
60
  logger.info('Step 2: Combining CSS...');
46
61
  await buildAllCSS(root, outDir);
47
62
 
63
+ // Step 3: Assets
48
64
  logger.info('Step 3: Copying static assets...');
49
65
  await copyAllStaticAssets(root, outDir);
50
66
 
67
+ // Step 4: JavaScript Bundling
51
68
  logger.info('Step 4: Bundling JavaScript...');
52
69
  const buildEntry = join(buildDir, 'main.js');
53
70
 
71
+ // βœ… CRITICAL FIX: Check if main.js exists before bundling
72
+ if (!existsSync(buildEntry)) {
73
+ logger.error('❌ main.js not found in build directory!');
74
+ logger.error(' Expected: ' + buildEntry);
75
+ throw new Error('Build entry point missing. Compilation may have failed.');
76
+ }
77
+
78
+ const result = await bundleJavaScript(buildEntry, outDir, envVars);
79
+
80
+ // Step 5: HTML Generation
81
+ logger.info('Step 5: Generating HTML with Server Islands...');
82
+ await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
83
+
84
+ // Step 6: Sitemap
85
+ logger.info('Step 6: Generating sitemap.xml...');
86
+ await generateSitemap(routes, config, outDir);
87
+
88
+ // Step 7: Robots.txt
89
+ logger.info('Step 7: Generating robots.txt...');
90
+ await generateRobots(config, outDir, routes);
91
+
92
+ // Cleanup
93
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
94
+
95
+ // Summary
96
+ const duration = Date.now() - startTime;
97
+ showBuildSummary(routes, serverIslands, clientRoutes, duration);
98
+
99
+ } catch (error) {
100
+ logger.error(`Build failed: ${error.message}`);
101
+ if (error.stack) logger.error(error.stack);
102
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ async function bundleJavaScript(buildEntry, outDir, envVars) {
108
+ try {
109
+ // βœ… CRITICAL FIX: Better error handling and clearer external configuration
54
110
  const result = await Bun.build({
55
111
  entrypoints: [buildEntry],
56
112
  outdir: join(outDir, 'assets'),
@@ -63,6 +119,7 @@ export async function buildProduction(options = {}) {
63
119
  chunk: 'chunks/[name]-[hash].js',
64
120
  asset: '[name]-[hash].[ext]'
65
121
  },
122
+ // βœ… FIXED: Externalize React to use CDN (reduces bundle size)
66
123
  external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
67
124
  define: {
68
125
  'process.env.NODE_ENV': '"production"',
@@ -76,689 +133,52 @@ export async function buildProduction(options = {}) {
76
133
  });
77
134
 
78
135
  if (!result.success) {
79
- logger.error('JavaScript build failed!');
80
- result.logs.forEach(log => logger.error(log.message));
81
- process.exit(1);
82
- }
83
-
84
- logger.success('JavaScript bundled');
85
-
86
- logger.info('Step 5: Generating HTML with Server Islands...');
87
- await generateProductionHTML(root, outDir, result, routes, serverIslands);
88
-
89
- if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
90
-
91
- const duration = Date.now() - startTime;
92
- logger.success(`✨ Build complete in ${duration}ms`);
93
-
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
- }
103
-
104
- logger.bigLog('READY TO DEPLOY πŸš€', { color: 'green' });
105
-
106
- } catch (error) {
107
- logger.error(`Build failed: ${error.message}`);
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"');
136
+ logger.error('❌ JavaScript build failed!');
133
137
 
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;
231
- }
232
-
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
- }
255
- }
256
-
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;
385
- }
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
- }
436
-
437
- // Helper functions from original build.js
438
- async function copyAllStaticAssets(root, outDir) {
439
- const publicDir = join(root, 'public');
440
- const srcImagesDir = join(root, 'src', 'images');
441
-
442
- if (existsSync(publicDir)) {
443
- copyImages(publicDir, outDir);
444
- }
445
-
446
- if (existsSync(srcImagesDir)) {
447
- const distImagesDir = join(outDir, 'images');
448
- mkdirSync(distImagesDir, { recursive: true });
449
- copyImages(srcImagesDir, distImagesDir);
450
- }
451
- }
452
-
453
- async function buildAllCSS(root, outDir) {
454
- const srcStylesDir = join(root, 'src', 'styles');
455
- const stylesOutDir = join(outDir, 'styles');
456
-
457
- mkdirSync(stylesOutDir, { recursive: true });
458
-
459
- if (existsSync(srcStylesDir)) {
460
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
461
-
462
- if (cssFiles.length === 0) {
463
- await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
464
- return;
465
- }
466
-
467
- let combinedCSS = '';
468
- for (const cssFile of cssFiles) {
469
- const srcPath = join(srcStylesDir, cssFile);
470
- const file = Bun.file(srcPath);
471
- const cssContent = await file.text();
472
- combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
473
- }
474
-
475
- const combinedPath = join(stylesOutDir, 'bertui.min.css');
476
- await Bun.write(combinedPath, combinedCSS);
477
- await buildCSS(combinedPath, combinedPath);
478
-
479
- logger.success(`βœ… Combined ${cssFiles.length} CSS files`);
480
- }
481
- }
482
-
483
- async function discoverRoutes(pagesDir) {
484
- const routes = [];
485
-
486
- async function scanDirectory(dir, basePath = '') {
487
- const entries = readdirSync(dir, { withFileTypes: true });
488
-
489
- for (const entry of entries) {
490
- const fullPath = join(dir, entry.name);
491
- const relativePath = join(basePath, entry.name);
492
-
493
- if (entry.isDirectory()) {
494
- await scanDirectory(fullPath, relativePath);
495
- } else if (entry.isFile()) {
496
- const ext = extname(entry.name);
497
- if (ext === '.css') continue;
498
-
499
- if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
500
- const fileName = entry.name.replace(ext, '');
501
- let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
502
-
503
- if (fileName === 'index') {
504
- route = route.replace('/index', '') || '/';
138
+ // βœ… IMPROVED: Better error reporting
139
+ if (result.logs && result.logs.length > 0) {
140
+ logger.error('\nπŸ“‹ Build errors:');
141
+ result.logs.forEach((log, i) => {
142
+ logger.error(`\n${i + 1}. ${log.message}`);
143
+ if (log.position) {
144
+ logger.error(` File: ${log.position.file || 'unknown'}`);
145
+ logger.error(` Line: ${log.position.line || 'unknown'}`);
505
146
  }
506
-
507
- const isDynamic = fileName.includes('[') && fileName.includes(']');
508
-
509
- routes.push({
510
- route: route === '' ? '/' : route,
511
- file: relativePath.replace(/\\/g, '/'),
512
- path: fullPath,
513
- type: isDynamic ? 'dynamic' : 'static'
514
- });
515
- }
147
+ });
516
148
  }
517
- }
518
- }
519
-
520
- await scanDirectory(pagesDir);
521
- routes.sort((a, b) => a.type === b.type ? a.route.localeCompare(b.route) : a.type === 'static' ? -1 : 1);
522
-
523
- return routes;
524
- }
525
-
526
- async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
527
- const files = readdirSync(srcDir);
528
-
529
- for (const file of files) {
530
- const srcPath = join(srcDir, file);
531
- const stat = statSync(srcPath);
532
-
533
- if (stat.isDirectory()) {
534
- const subBuildDir = join(buildDir, file);
535
- mkdirSync(subBuildDir, { recursive: true });
536
- await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
537
- } else {
538
- const ext = extname(file);
539
- if (ext === '.css') continue;
540
149
 
541
- if (['.jsx', '.tsx', '.ts'].includes(ext)) {
542
- await compileBuildFile(srcPath, buildDir, file, root, envVars);
543
- } else if (ext === '.js') {
544
- const outPath = join(buildDir, file);
545
- let code = await Bun.file(srcPath).text();
546
- code = removeCSSImports(code);
547
- code = replaceEnvInCode(code, envVars);
548
- code = fixBuildImports(code, srcPath, outPath, root);
549
- if (usesJSX(code) && !code.includes('import React')) {
550
- code = `import React from 'react';\n${code}`;
551
- }
552
- await Bun.write(outPath, code);
553
- }
150
+ throw new Error('JavaScript bundling failed - check errors above');
554
151
  }
555
- }
556
- }
557
-
558
- async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
559
- const ext = extname(filename);
560
- const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
561
-
562
- try {
563
- let code = await Bun.file(srcPath).text();
564
- code = removeCSSImports(code);
565
- code = replaceEnvInCode(code, envVars);
566
152
 
567
- const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
568
- const outPath = join(buildDir, outFilename);
569
- code = fixBuildImports(code, srcPath, outPath, root);
153
+ // βœ… IMPROVED: Log successful bundle info
154
+ logger.success('βœ… JavaScript bundled successfully');
155
+ logger.info(` Entry points: ${result.outputs.filter(o => o.kind === 'entry-point').length}`);
156
+ logger.info(` Chunks: ${result.outputs.filter(o => o.kind === 'chunk').length}`);
570
157
 
571
- const transpiler = new Bun.Transpiler({
572
- loader,
573
- tsconfig: {
574
- compilerOptions: {
575
- jsx: 'react',
576
- jsxFactory: 'React.createElement',
577
- jsxFragmentFactory: 'React.Fragment'
578
- }
579
- }
580
- });
581
-
582
- let compiled = await transpiler.transform(code);
583
- if (usesJSX(compiled) && !compiled.includes('import React')) {
584
- compiled = `import React from 'react';\n${compiled}`;
585
- }
586
- compiled = fixRelativeImports(compiled);
587
- await Bun.write(outPath, compiled);
588
- } catch (error) {
589
- logger.error(`Failed to compile ${filename}: ${error.message}`);
590
- throw error;
591
- }
592
- }
593
-
594
- function usesJSX(code) {
595
- return code.includes('React.createElement') ||
596
- code.includes('React.Fragment') ||
597
- /<[A-Z]/.test(code) ||
598
- code.includes('jsx(') ||
599
- code.includes('jsxs(');
600
- }
601
-
602
- function removeCSSImports(code) {
603
- code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
604
- code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
605
- return code;
606
- }
607
-
608
- function fixBuildImports(code, srcPath, outPath, root) {
609
- const buildDir = join(root, '.bertuibuild');
610
- const routerPath = join(buildDir, 'router.js');
611
- const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
612
- const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
613
-
614
- code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
615
- return code;
616
- }
617
-
618
- function fixRelativeImports(code) {
619
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
620
- code = code.replace(importRegex, (match, prefix, path) => {
621
- if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
622
- return `from '${prefix}${path}.js';`;
623
- });
624
- return code;
625
- }
626
-
627
- function extractMetaFromSource(code) {
628
- try {
629
- const metaMatch = code.match(/export\s+const\s+meta\s*=\s*\{/);
630
- if (!metaMatch) return null;
631
-
632
- const startIndex = metaMatch.index + metaMatch[0].length - 1;
633
- let braceCount = 0;
634
- let endIndex = startIndex;
635
-
636
- for (let i = startIndex; i < code.length; i++) {
637
- if (code[i] === '{') braceCount++;
638
- if (code[i] === '}') {
639
- braceCount--;
640
- if (braceCount === 0) {
641
- endIndex = i;
642
- break;
643
- }
644
- }
645
- }
158
+ const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
159
+ logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
646
160
 
647
- if (endIndex === startIndex) return null;
161
+ return result;
648
162
 
649
- const metaString = code.substring(startIndex, endIndex + 1);
650
- const meta = {};
651
- const pairRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g;
652
- let match;
653
-
654
- while ((match = pairRegex.exec(metaString)) !== null) {
655
- meta[match[1]] = match[3];
656
- }
657
-
658
- return Object.keys(meta).length > 0 ? meta : null;
659
163
  } catch (error) {
660
- return null;
164
+ logger.error('❌ Bundling error: ' + error.message);
165
+ if (error.stack) logger.error(error.stack);
166
+ throw error;
661
167
  }
662
168
  }
663
169
 
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');
670
-
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');
170
+ function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
171
+ logger.success(`✨ Build complete in ${duration}ms`);
172
+ logger.bigLog('BUILD SUMMARY', { color: 'green' });
173
+ logger.info(`πŸ“„ Total routes: ${routes.length}`);
174
+ logger.info(`🏝️ Server Islands (SSG): ${serverIslands.length}`);
175
+ logger.info(`⚑ Client-only: ${clientRoutes.length}`);
176
+ logger.info(`πŸ—ΊοΈ Sitemap: dist/sitemap.xml`);
177
+ logger.info(`πŸ€– robots.txt: dist/robots.txt`);
675
178
 
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
- }
718
- }
719
- }
720
- setCurrentRoute(null);
721
- setParams({});
179
+ if (serverIslands.length > 0) {
180
+ logger.success('βœ… Server Islands enabled - INSTANT content delivery!');
722
181
  }
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
- );
735
- }
736
-
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
- ];`;
762
182
 
763
- await Bun.write(join(buildDir, 'router.js'), routerCode);
183
+ logger.bigLog('READY TO DEPLOY πŸš€', { color: 'green' });
764
184
  }