bertui 1.0.3 β†’ 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/build.js CHANGED
@@ -1,678 +1,142 @@
1
- // src/build.js - COMPLETE v1.0.0 FIXED VERSION
2
- import { join, relative, basename, extname, dirname } from 'path';
3
- import { existsSync, mkdirSync, rmSync, cpSync, readdirSync, statSync } from 'fs';
1
+ // bertui/src/build.js - FINAL ORCHESTRATOR
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 { optimizeImages, checkOptimizationTools, copyImages } from './build/image-optimizer.js';
5
+ import { loadEnvVariables } from './utils/env.js';
6
+
7
+ // Import modular components
8
+ import { compileForBuild } from './build/compiler/index.js';
9
+ import { buildAllCSS } from './build/processors/css-builder.js';
10
+ import { copyAllStaticAssets } from './build/processors/asset-processor.js';
11
+ import { generateProductionHTML } from './build/generators/html-generator.js';
12
+ import { generateSitemap } from './build/generators/sitemap-generator.js';
13
+ import { generateRobots } from './build/generators/robots-generator.js';
14
+
15
+
8
16
 
9
17
  export async function buildProduction(options = {}) {
10
18
  const root = options.root || process.cwd();
11
19
  const buildDir = join(root, '.bertuibuild');
12
20
  const outDir = join(root, 'dist');
13
21
 
14
- logger.bigLog('BUILDING FOR PRODUCTION v1.0.0', { color: 'green' });
15
- logger.info('πŸ”₯ SINGLE CSS FILE SOLUTION - NO BULLSHIT');
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
- }
22
+ logger.bigLog('BUILDING WITH SERVER ISLANDS 🏝️', { color: 'green' });
23
+ logger.info('πŸ”₯ OPTIONAL SERVER CONTENT - THE GAME CHANGER');
25
24
 
25
+ // Clean directories
26
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
27
+ if (existsSync(outDir)) rmSync(outDir, { recursive: true });
26
28
  mkdirSync(buildDir, { recursive: true });
27
29
  mkdirSync(outDir, { recursive: true });
28
30
 
29
31
  const startTime = Date.now();
30
32
 
31
33
  try {
34
+ // Step 0: Environment
32
35
  logger.info('Step 0: Loading environment variables...');
33
36
  const envVars = loadEnvVariables(root);
34
- if (Object.keys(envVars).length > 0) {
35
- logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
36
- }
37
37
 
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`);
38
+ // Step 1: Compilation
39
+ logger.info('Step 1: Compiling and detecting Server Islands...');
40
+ const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
41
41
 
42
- logger.info('Step 2: Combining ALL CSS into ONE file...');
43
- await buildAllCSS(root, outDir);
42
+ if (serverIslands.length > 0) {
43
+ logger.bigLog('SERVER ISLANDS DETECTED 🏝️', { color: 'cyan' });
44
+ logger.table(serverIslands.map(r => ({
45
+ route: r.route,
46
+ file: r.file,
47
+ mode: '🏝️ Server Island (SSG)'
48
+ })));
49
+ }
44
50
 
45
- logger.info('Step 3: Checking image optimization tools...');
46
- const optimizationTools = await checkOptimizationTools();
51
+ // Step 2: CSS Processing
52
+ logger.info('Step 2: Combining CSS...');
53
+ await buildAllCSS(root, outDir);
47
54
 
48
- logger.info('Step 4: Copying and optimizing static assets...');
49
- await copyAllStaticAssets(root, outDir, false);
55
+ // Step 3: Assets
56
+ logger.info('Step 3: Copying static assets...');
57
+ await copyAllStaticAssets(root, outDir);
50
58
 
51
- logger.info('Step 5: Bundling JavaScript with Bun...');
59
+ // Step 4: JavaScript Bundling
60
+ logger.info('Step 4: Bundling JavaScript...');
52
61
  const buildEntry = join(buildDir, 'main.js');
62
+ const result = await bundleJavaScript(buildEntry, outDir, envVars);
53
63
 
54
- if (!existsSync(buildEntry)) {
55
- logger.error('Build entry point not found: .bertuibuild/main.js');
56
- process.exit(1);
57
- }
64
+ // Step 5: HTML Generation
65
+ logger.info('Step 5: Generating HTML with Server Islands...');
66
+ const { loadConfig } = await import('./config/loadConfig.js');
67
+ const config = await loadConfig(root);
68
+ await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
58
69
 
59
- const result = await Bun.build({
60
- entrypoints: [buildEntry],
61
- outdir: join(outDir, 'assets'),
62
- target: 'browser',
63
- minify: true,
64
- splitting: true,
65
- sourcemap: 'external',
66
- naming: {
67
- entry: '[name]-[hash].js',
68
- chunk: 'chunks/[name]-[hash].js',
69
- asset: '[name]-[hash].[ext]'
70
- },
71
- external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
72
- define: {
73
- 'process.env.NODE_ENV': '"production"',
74
- ...Object.fromEntries(
75
- Object.entries(envVars).map(([key, value]) => [
76
- `process.env.${key}`,
77
- JSON.stringify(value)
78
- ])
79
- )
80
- }
81
- });
70
+ // Step 6: Sitemap
71
+ logger.info('Step 6: Generating sitemap.xml...');
72
+ await generateSitemap(routes, config, outDir);
82
73
 
83
- if (!result.success) {
84
- logger.error('JavaScript build failed!');
85
- result.logs.forEach(log => logger.error(log.message));
86
- process.exit(1);
87
- }
88
-
89
- logger.success('JavaScript bundled successfully');
74
+ // Step 7: Robots.txt
75
+ logger.info('Step 7: Generating robots.txt...');
76
+ await generateRobots(config, outDir, routes);
90
77
 
91
- logger.info('Step 6: Generating HTML files with SINGLE CSS...');
92
- await generateProductionHTML(root, outDir, result, routes);
93
-
94
- // Clean up build directory
95
- if (existsSync(buildDir)) {
96
- rmSync(buildDir, { recursive: true });
97
- logger.info('Cleaned up .bertuibuild/');
98
- }
78
+ // Cleanup
79
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
99
80
 
81
+ // Summary
100
82
  const duration = Date.now() - startTime;
101
- logger.success(`✨ Build complete in ${duration}ms`);
102
- logger.info(`πŸ“¦ Output: ${outDir}`);
103
-
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
- })));
109
-
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');
83
+ showBuildSummary(routes, serverIslands, clientRoutes, duration);
116
84
 
117
85
  } catch (error) {
118
86
  logger.error(`Build failed: ${error.message}`);
119
- if (error.stack) {
120
- logger.error(error.stack);
121
- }
122
-
123
- if (existsSync(buildDir)) {
124
- rmSync(buildDir, { recursive: true });
125
- }
126
-
87
+ if (error.stack) logger.error(error.stack);
88
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
127
89
  process.exit(1);
128
90
  }
129
91
  }
130
92
 
131
- // SIMPLE asset copying
132
- async function copyAllStaticAssets(root, outDir, optimize = true) {
133
- const publicDir = join(root, 'public');
134
- const srcImagesDir = join(root, 'src', 'images');
135
-
136
- logger.info('πŸ“¦ Copying static assets...');
137
-
138
- // Copy from public/ to root of dist/
139
- if (existsSync(publicDir)) {
140
- logger.info(' Copying public/ directory...');
141
- copyImages(publicDir, outDir);
142
- } else {
143
- logger.info(' No public/ directory found');
144
- }
145
-
146
- // Copy from src/images/ to dist/images/
147
- if (existsSync(srcImagesDir)) {
148
- logger.info(' Copying src/images/ to dist/images/...');
149
- const distImagesDir = join(outDir, 'images');
150
- mkdirSync(distImagesDir, { recursive: true });
151
- copyImages(srcImagesDir, distImagesDir);
152
- } else {
153
- logger.info(' No src/images/ directory found');
154
- }
155
-
156
- logger.success('βœ… All assets copied');
157
- }
158
-
159
- // COMBINE ALL CSS INTO ONE FILE
160
- // COMBINE ALL CSS INTO ONE FILE - FIXED BUN API
161
- async function buildAllCSS(root, outDir) {
162
- const srcStylesDir = join(root, 'src', 'styles');
163
- const stylesOutDir = join(outDir, 'styles');
164
-
165
- mkdirSync(stylesOutDir, { recursive: true });
166
-
167
- if (existsSync(srcStylesDir)) {
168
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
169
- logger.info(`πŸ“¦ Found ${cssFiles.length} CSS files to combine`);
170
-
171
- if (cssFiles.length === 0) {
172
- logger.warn('⚠️ No CSS files found in src/styles/');
173
- return;
174
- }
175
-
176
- // COMBINE ALL CSS INTO ONE FILE
177
- let combinedCSS = '';
178
- let totalOriginalSize = 0;
179
-
180
- for (const cssFile of cssFiles) {
181
- const srcPath = join(srcStylesDir, cssFile);
182
- const file = Bun.file(srcPath);
183
- const cssContent = await file.text();
184
- totalOriginalSize += file.size;
185
- combinedCSS += `/* === ${cssFile} === */\n${cssContent}\n\n`;
186
- }
187
-
188
- // Write combined CSS
189
- const combinedPath = join(stylesOutDir, 'bertui.min.css');
190
- await Bun.write(combinedPath, combinedCSS);
191
-
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 */');
208
- }
209
- }
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
-
234
- async function discoverRoutes(pagesDir) {
235
- const routes = [];
236
-
237
- async function scanDirectory(dir, basePath = '') {
238
- const entries = readdirSync(dir, { withFileTypes: true });
239
-
240
- for (const entry of entries) {
241
- const fullPath = join(dir, entry.name);
242
- const relativePath = join(basePath, entry.name);
243
-
244
- if (entry.isDirectory()) {
245
- await scanDirectory(fullPath, relativePath);
246
- } else if (entry.isFile()) {
247
- const ext = extname(entry.name);
248
-
249
- if (ext === '.css') continue;
250
-
251
- if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
252
- const fileName = entry.name.replace(ext, '');
253
-
254
- let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
255
-
256
- if (fileName === 'index') {
257
- route = route.replace('/index', '') || '/';
258
- }
259
-
260
- const isDynamic = fileName.includes('[') && fileName.includes(']');
261
- const type = isDynamic ? 'dynamic' : 'static';
262
-
263
- routes.push({
264
- route: route === '' ? '/' : route,
265
- file: relativePath.replace(/\\/g, '/'),
266
- path: fullPath,
267
- type
268
- });
269
- }
270
- }
271
- }
272
- }
273
-
274
- 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
- });
282
-
283
- return routes;
284
- }
285
-
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
- }
93
+ async function bundleJavaScript(buildEntry, outDir, envVars) {
94
+ const result = await Bun.build({
95
+ entrypoints: [buildEntry],
96
+ outdir: join(outDir, 'assets'),
97
+ target: 'browser',
98
+ minify: true,
99
+ splitting: true,
100
+ sourcemap: 'external',
101
+ naming: {
102
+ entry: '[name]-[hash].js',
103
+ chunk: 'chunks/[name]-[hash].js',
104
+ asset: '[name]-[hash].[ext]'
402
105
  },
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
- async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
423
- const files = readdirSync(srcDir);
424
-
425
- for (const file of files) {
426
- const srcPath = join(srcDir, file);
427
- const stat = statSync(srcPath);
428
-
429
- if (stat.isDirectory()) {
430
- const subBuildDir = join(buildDir, file);
431
- mkdirSync(subBuildDir, { recursive: true });
432
- await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
433
- } else {
434
- const ext = extname(file);
435
-
436
- if (ext === '.css') continue;
437
-
438
- if (['.jsx', '.tsx', '.ts'].includes(ext)) {
439
- await compileBuildFile(srcPath, buildDir, file, root, envVars);
440
- } else if (ext === '.js') {
441
- const outPath = join(buildDir, file);
442
- let code = await Bun.file(srcPath).text();
443
-
444
- code = removeCSSImports(code);
445
- code = replaceEnvInCode(code, envVars);
446
- code = fixBuildImports(code, srcPath, outPath, root);
447
-
448
- if (usesJSX(code) && !code.includes('import React')) {
449
- code = `import React from 'react';\n${code}`;
450
- }
451
-
452
- await Bun.write(outPath, code);
453
- }
106
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
107
+ define: {
108
+ 'process.env.NODE_ENV': '"production"',
109
+ ...Object.fromEntries(
110
+ Object.entries(envVars).map(([key, value]) => [
111
+ `process.env.${key}`,
112
+ JSON.stringify(value)
113
+ ])
114
+ )
454
115
  }
455
- }
456
- }
457
-
458
- async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
459
- const ext = extname(filename);
460
- const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
461
-
462
- try {
463
- let code = await Bun.file(srcPath).text();
464
-
465
- code = removeCSSImports(code);
466
- code = replaceEnvInCode(code, envVars);
467
-
468
- const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
469
- const outPath = join(buildDir, outFilename);
470
-
471
- code = fixBuildImports(code, srcPath, outPath, root);
472
-
473
- const transpiler = new Bun.Transpiler({
474
- loader,
475
- tsconfig: {
476
- compilerOptions: {
477
- jsx: 'react',
478
- jsxFactory: 'React.createElement',
479
- jsxFragmentFactory: 'React.Fragment'
480
- }
481
- }
482
- });
483
-
484
- let compiled = await transpiler.transform(code);
485
-
486
- if (usesJSX(compiled) && !compiled.includes('import React')) {
487
- compiled = `import React from 'react';\n${compiled}`;
488
- }
489
-
490
- compiled = fixRelativeImports(compiled);
491
-
492
- await Bun.write(outPath, compiled);
493
- } catch (error) {
494
- logger.error(`Failed to compile ${filename}: ${error.message}`);
495
- throw error;
496
- }
497
- }
498
-
499
- function usesJSX(code) {
500
- return code.includes('React.createElement') ||
501
- code.includes('React.Fragment') ||
502
- /<[A-Z]/.test(code) ||
503
- code.includes('jsx(') ||
504
- code.includes('jsxs(');
505
- }
506
-
507
- function removeCSSImports(code) {
508
- code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
509
- code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
510
- return code;
511
- }
512
-
513
- function fixBuildImports(code, srcPath, outPath, root) {
514
- const buildDir = join(root, '.bertuibuild');
515
- const routerPath = join(buildDir, 'router.js');
516
-
517
- const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
518
- const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
519
-
520
- code = code.replace(
521
- /from\s+['"]bertui\/router['"]/g,
522
- `from '${routerImport}'`
523
- );
524
-
525
- return code;
526
- }
527
-
528
- function fixRelativeImports(code) {
529
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
530
-
531
- code = code.replace(importRegex, (match, prefix, path) => {
532
- if (path.endsWith('/') || /\.\w+$/.test(path)) {
533
- return match;
534
- }
535
- return `from '${prefix}${path}.js';`;
536
116
  });
537
117
 
538
- return code;
539
- }
540
-
541
- function extractMetaFromSource(code) {
542
- try {
543
- const metaMatch = code.match(/export\s+const\s+meta\s*=\s*\{/);
544
- if (!metaMatch) return null;
545
-
546
- const startIndex = metaMatch.index + metaMatch[0].length - 1;
547
- let braceCount = 0;
548
- let endIndex = startIndex;
549
-
550
- for (let i = startIndex; i < code.length; i++) {
551
- if (code[i] === '{') braceCount++;
552
- if (code[i] === '}') {
553
- braceCount--;
554
- if (braceCount === 0) {
555
- endIndex = i;
556
- break;
557
- }
558
- }
559
- }
560
-
561
- if (endIndex === startIndex) return null;
562
-
563
- const metaString = code.substring(startIndex, endIndex + 1);
564
- const meta = {};
565
- const pairRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g;
566
- let match;
567
-
568
- while ((match = pairRegex.exec(metaString)) !== null) {
569
- const key = match[1];
570
- const value = match[3];
571
- meta[key] = value;
572
- }
573
-
574
- return Object.keys(meta).length > 0 ? meta : null;
575
- } catch (error) {
576
- logger.warn(`Could not extract meta: ${error.message}`);
577
- return null;
578
- }
579
- }
580
-
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 || {};
601
-
602
- logger.info(`Generating HTML for ${routes.length} routes...`);
603
-
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');
619
- }
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
- }
118
+ if (!result.success) {
119
+ logger.error('JavaScript build failed!');
120
+ result.logs.forEach(log => logger.error(log.message));
121
+ process.exit(1);
627
122
  }
628
123
 
629
- logger.success('✨ All HTML files generated with SINGLE CSS file!');
124
+ logger.success('JavaScript bundled');
125
+ return result;
630
126
  }
631
127
 
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}">
128
+ function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
129
+ logger.success(`✨ Build complete in ${duration}ms`);
130
+ logger.bigLog('BUILD SUMMARY', { color: 'green' });
131
+ logger.info(`πŸ“„ Total routes: ${routes.length}`);
132
+ logger.info(`🏝️ Server Islands (SSG): ${serverIslands.length}`);
133
+ logger.info(`⚑ Client-only: ${clientRoutes.length}`);
134
+ logger.info(`πŸ—ΊοΈ Sitemap: dist/sitemap.xml`);
135
+ logger.info(`πŸ€– robots.txt: dist/robots.txt`);
650
136
 
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}">
661
-
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
- }
137
+ if (serverIslands.length > 0) {
138
+ logger.success('βœ… Server Islands enabled - INSTANT content delivery!');
670
139
  }
671
- </script>
672
- </head>
673
- <body>
674
- <div id="root"></div>
675
- <script type="module" src="/${bundlePath}"></script>
676
- </body>
677
- </html>`;
140
+
141
+ logger.bigLog('READY TO DEPLOY πŸš€', { color: 'green' });
678
142
  }