bertui 1.1.9 → 1.2.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,4 +1,4 @@
1
- // bertui/src/build.js - COMPLETE FIXED VERSION WITH PROPER EXIT
1
+ // bertui/src/build.js - WITH LAYOUTS + LOADING + PARTIAL HYDRATION + ANALYZER
2
2
  import { join } from 'path';
3
3
  import { existsSync, mkdirSync, rmSync } from 'fs';
4
4
  import logger from './logger/logger.js';
@@ -11,263 +11,169 @@ import { copyAllStaticAssets } from './build/processors/asset-processor.js';
11
11
  import { generateProductionHTML } from './build/generators/html-generator.js';
12
12
  import { generateSitemap } from './build/generators/sitemap-generator.js';
13
13
  import { generateRobots } from './build/generators/robots-generator.js';
14
+ import { compileLayouts } from './layouts/index.js';
15
+ import { compileLoadingComponents } from './loading/index.js';
16
+ import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
17
+ import { analyzeBuild } from './analyzer/index.js';
14
18
 
15
19
  export async function buildProduction(options = {}) {
16
20
  const root = options.root || process.cwd();
17
21
  const buildDir = join(root, '.bertuibuild');
18
22
  const outDir = join(root, 'dist');
19
-
20
- // Force production environment
23
+
21
24
  process.env.NODE_ENV = 'production';
22
-
25
+
23
26
  logger.bigLog('BUILDING WITH SERVER ISLANDS 🏝️', { color: 'green' });
24
- logger.info('🔥 OPTIONAL SERVER CONTENT - THE GAME CHANGER');
25
-
27
+
26
28
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
27
29
  if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
28
30
  mkdirSync(buildDir, { recursive: true });
29
31
  mkdirSync(outDir, { recursive: true });
30
-
31
- const startTime = process.hrtime.bigint(); // Microsecond precision
32
-
32
+
33
+ const startTime = process.hrtime.bigint();
34
+
33
35
  try {
34
36
  logger.info('Step 0: Loading environment variables...');
35
37
  const envVars = loadEnvVariables(root);
36
-
38
+
37
39
  const { loadConfig } = await import('./config/loadConfig.js');
38
40
  const config = await loadConfig(root);
39
-
40
- logger.info('Step 1: Compiling and detecting Server Islands...');
41
+
42
+ logger.info('Step 1: Compiling project...');
41
43
  const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
42
-
44
+
43
45
  if (serverIslands.length > 0) {
44
46
  logger.bigLog('SERVER ISLANDS DETECTED 🏝️', { color: 'cyan' });
45
47
  logger.table(serverIslands.map(r => ({
46
48
  route: r.route,
47
49
  file: r.file,
48
- mode: '🏝️ Server Island (SSG)'
50
+ mode: '🏝️ Server Island (SSG)',
49
51
  })));
50
52
  }
51
-
52
- logger.info('Step 2: Processing SCSS/SASS...');
53
- await processSCSS(root, buildDir);
54
-
55
- logger.info('Step 3: Combining CSS...');
53
+
54
+ // ✅ NEW: Compile layouts
55
+ logger.info('Step 2: Compiling layouts...');
56
+ const layouts = await compileLayouts(root, buildDir);
57
+ const layoutCount = Object.keys(layouts).length;
58
+ if (layoutCount > 0) {
59
+ logger.success(`📐 ${layoutCount} layout(s) compiled`);
60
+ }
61
+
62
+ // ✅ NEW: Compile per-route loading states
63
+ logger.info('Step 3: Compiling loading states...');
64
+ const loadingComponents = await compileLoadingComponents(root, buildDir);
65
+
66
+ // ✅ NEW: Partial hydration analysis
67
+ logger.info('Step 4: Analyzing routes for partial hydration...');
68
+ const analyzedRoutes = await analyzeRoutes(routes);
69
+ logHydrationReport(analyzedRoutes);
70
+
71
+ logger.info('Step 5: Processing CSS...');
56
72
  await buildAllCSS(root, outDir);
57
-
58
- logger.info('Step 4: Copying static assets...');
73
+
74
+ logger.info('Step 6: Copying static assets...');
59
75
  await copyAllStaticAssets(root, outDir);
60
-
61
- logger.info('Step 5: Bundling JavaScript with Router...');
76
+
77
+ logger.info('Step 7: Bundling JavaScript...');
62
78
  const buildEntry = join(buildDir, 'main.js');
63
79
  const routerPath = join(buildDir, 'router.js');
64
-
80
+
65
81
  if (!existsSync(buildEntry)) {
66
- logger.error(' main.js not found in build directory!');
67
- throw new Error('Build entry point missing');
82
+ throw new Error('Build entry point missing (main.js not found in build dir)');
68
83
  }
69
-
70
- const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir);
71
-
72
- logger.info('Step 6: Generating HTML with Server Islands...');
84
+
85
+ const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes);
86
+
87
+ logger.info('Step 8: Generating HTML...');
73
88
  await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
74
-
75
- logger.info('Step 7: Generating sitemap.xml...');
89
+
90
+ logger.info('Step 9: Generating sitemap.xml...');
76
91
  await generateSitemap(routes, config, outDir);
77
-
78
- logger.info('Step 8: Generating robots.txt...');
92
+
93
+ logger.info('Step 10: Generating robots.txt...');
79
94
  await generateRobots(config, outDir, routes);
80
-
81
- // Clean up build directory
95
+
96
+ // Cleanup build directory
82
97
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
83
-
98
+
84
99
  const endTime = process.hrtime.bigint();
85
- const durationMicro = Number(endTime - startTime) / 1000;
86
- const durationMs = durationMicro / 1000;
87
-
88
- showBuildSummary(routes, serverIslands, clientRoutes, durationMs, durationMicro);
89
-
90
- // FIX: Force exit after successful build
91
- // Small delay to ensure all logs are flushed
100
+ const durationMs = Number(endTime - startTime) / 1_000_000;
101
+
102
+ showBuildSummary(routes, serverIslands, clientRoutes, analyzedRoutes, durationMs);
103
+
104
+ // ✅ NEW: Auto-generate bundle report
105
+ logger.info('Generating bundle report...');
106
+ await analyzeBuild(outDir, {
107
+ outputFile: join(outDir, 'bundle-report.html'),
108
+ });
109
+
92
110
  setTimeout(() => {
93
- logger.info('✅ Build process complete, exiting...');
111
+ logger.info('✅ Build complete');
94
112
  process.exit(0);
95
113
  }, 100);
96
-
114
+
97
115
  } catch (error) {
98
116
  logger.error(`Build failed: ${error.message}`);
99
117
  if (error.stack) logger.error(error.stack);
100
118
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
101
-
102
- // FIX: Force exit with error code
103
- setTimeout(() => {
104
- process.exit(1);
105
- }, 100);
119
+
120
+ setTimeout(() => process.exit(1), 100);
106
121
  }
107
122
  }
108
123
 
109
- async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir) {
124
+ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes) {
125
+ const originalCwd = process.cwd();
126
+ process.chdir(buildDir);
127
+
110
128
  try {
111
129
  const hasRouter = existsSync(routerPath);
112
-
113
- const originalCwd = process.cwd();
114
- process.chdir(buildDir);
115
-
116
- logger.info('🔧 Bundling with production JSX...');
117
-
118
130
  const entrypoints = [buildEntry];
119
- if (hasRouter) {
120
- entrypoints.push(routerPath);
121
- logger.success('✅ Router included in bundle');
122
- }
123
-
124
- logger.info(`📦 Entry points: ${entrypoints.map(e => e.split('/').pop()).join(', ')}`);
125
-
131
+ if (hasRouter) entrypoints.push(routerPath);
132
+
126
133
  const result = await Bun.build({
127
134
  entrypoints,
128
135
  outdir: join(outDir, 'assets'),
129
136
  target: 'browser',
130
137
  minify: true,
131
- splitting: true,
138
+ splitting: true, // Code splitting per route
132
139
  sourcemap: 'external',
140
+ metafile: true, // ✅ Enable for analyzer
133
141
  naming: {
134
142
  entry: '[name]-[hash].js',
135
143
  chunk: 'chunks/[name]-[hash].js',
136
- asset: '[name]-[hash].[ext]'
144
+ asset: '[name]-[hash].[ext]',
137
145
  },
138
146
  external: ['react', 'react-dom', 'react-dom/client'],
139
147
  define: {
140
148
  'process.env.NODE_ENV': '"production"',
141
149
  ...Object.fromEntries(
142
- Object.entries(envVars).map(([key, value]) => [
143
- `process.env.${key}`,
144
- JSON.stringify(value)
145
- ])
146
- )
147
- }
150
+ Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
151
+ ),
152
+ },
148
153
  });
149
-
150
- process.chdir(originalCwd);
151
-
154
+
152
155
  if (!result.success) {
153
- logger.error(' JavaScript build failed!');
154
-
155
- if (result.logs && result.logs.length > 0) {
156
- logger.error('\n📋 Build errors:');
157
- result.logs.forEach((log, i) => {
158
- logger.error(`\n${i + 1}. ${log.message}`);
159
- if (log.position) {
160
- logger.error(` File: ${log.position.file || 'unknown'}`);
161
- logger.error(` Line: ${log.position.line || 'unknown'}`);
162
- logger.error(` Column: ${log.position.column || 'unknown'}`);
163
- }
164
- if (log.position && log.position.file && existsSync(log.position.file)) {
165
- try {
166
- const fileContent = Bun.file(log.position.file).text();
167
- const lines = fileContent.split('\n');
168
- const line = lines[log.position.line - 1];
169
- if (line) {
170
- logger.error(` Code: ${line.trim()}`);
171
- logger.error(` ${' '.repeat(log.position.column - 1)}^`);
172
- }
173
- } catch (e) {}
174
- }
175
- });
176
- } else {
177
- logger.error('No detailed logs available');
178
- }
179
-
180
- throw new Error('JavaScript bundling failed');
156
+ const errors = result.logs?.map(l => l.message).join('\n') || 'Unknown error';
157
+ throw new Error(`JavaScript bundling failed:\n${errors}`);
181
158
  }
182
-
183
- const entryPoints = result.outputs.filter(o => o.kind === 'entry-point');
184
- const chunks = result.outputs.filter(o => o.kind === 'chunk');
185
-
186
- logger.success('✅ JavaScript bundled successfully');
187
- logger.info(` Entry points: ${entryPoints.length}`);
188
- logger.info(` Chunks: ${chunks.length}`);
189
-
190
- result.outputs.forEach(output => {
191
- const size = (output.size / 1024).toFixed(2);
192
- logger.debug(` 📄 ${output.path.split('/').pop()} (${size} KB)`);
193
- });
194
-
195
- const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
196
- logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
197
-
159
+
160
+ logger.success(`✅ Bundled ${result.outputs.length} files`);
198
161
  return result;
199
-
200
- } catch (error) {
201
- logger.error('❌ Bundling error: ' + error.message);
202
- if (error.stack) {
203
- logger.error('Stack trace:');
204
- logger.error(error.stack);
205
- }
206
- throw error;
207
- }
208
- }
209
162
 
210
- async function processSCSS(root, buildDir) {
211
- const srcStylesDir = join(root, 'src', 'styles');
212
- if (!existsSync(srcStylesDir)) return;
213
-
214
- try {
215
- const sass = await import('sass').catch(() => {
216
- logger.warn('⚠️ sass package not installed. Install with: bun add sass');
217
- return null;
218
- });
219
-
220
- if (!sass) return;
221
-
222
- const { readdirSync } = await import('fs');
223
- const scssFiles = readdirSync(srcStylesDir).filter(f =>
224
- f.endsWith('.scss') || f.endsWith('.sass')
225
- );
226
-
227
- if (scssFiles.length === 0) return;
228
-
229
- logger.info(`📝 Processing ${scssFiles.length} SCSS/SASS files...`);
230
-
231
- for (const file of scssFiles) {
232
- const srcPath = join(srcStylesDir, file);
233
- const cssPath = join(buildDir, 'styles', file.replace(/\.(scss|sass)$/, '.css'));
234
-
235
- mkdirSync(join(buildDir, 'styles'), { recursive: true });
236
-
237
- const result = sass.compile(srcPath, {
238
- style: 'compressed',
239
- sourceMap: false,
240
- loadPaths: [srcStylesDir, join(root, 'node_modules')]
241
- });
242
-
243
- await Bun.write(cssPath, result.css);
244
- logger.debug(` ${file} → ${file.replace(/\.(scss|sass)$/, '.css')}`);
245
- }
246
-
247
- logger.success(`✅ Processed ${scssFiles.length} SCSS files`);
248
- } catch (error) {
249
- logger.error(`SCSS processing failed: ${error.message}`);
163
+ } finally {
164
+ process.chdir(originalCwd);
250
165
  }
251
166
  }
252
167
 
253
- function showBuildSummary(routes, serverIslands, clientRoutes, durationMs, durationMicro) {
254
- logger.success(`✨ Build complete in ${durationMs.toFixed(3)}ms (${durationMicro.toFixed(0)}µs)`);
168
+ function showBuildSummary(routes, serverIslands, clientRoutes, analyzedRoutes, durationMs) {
169
+ logger.success(`✨ Build complete in ${durationMs.toFixed(1)}ms`);
255
170
  logger.bigLog('BUILD SUMMARY', { color: 'green' });
256
171
  logger.info(`📄 Total routes: ${routes.length}`);
257
172
  logger.info(`🏝️ Server Islands (SSG): ${serverIslands.length}`);
258
- logger.info(`⚡ Client-only: ${clientRoutes.length}`);
173
+ logger.info(`⚡ Interactive (hydrated): ${analyzedRoutes.interactive.length}`);
174
+ logger.info(`🧊 Static (no JS): ${analyzedRoutes.static.length}`);
259
175
  logger.info(`🗺️ Sitemap: dist/sitemap.xml`);
260
176
  logger.info(`🤖 robots.txt: dist/robots.txt`);
261
-
262
- const cacheStats = globalCache.getStats();
263
- logger.info(`📊 Cache: ${cacheStats.hitRate} hit rate (${cacheStats.hits}/${cacheStats.hits + cacheStats.misses})`);
264
-
265
- if (serverIslands.length > 0) {
266
- logger.success('✅ Server Islands enabled - INSTANT content delivery!');
267
- }
268
-
177
+ logger.info(`📊 Bundle report: dist/bundle-report.html`);
269
178
  logger.bigLog('READY TO DEPLOY 🚀', { color: 'green' });
270
-
271
- // ✅ Force log flush
272
- logger.debug('Build complete, exiting...');
273
179
  }
package/src/cli.js CHANGED
@@ -1,49 +1,87 @@
1
- // bertui/src/cli.js - WITH SERVE COMMAND
1
+ // bertui/src/cli.js - WITH ALL COMMANDS
2
2
  import { startDev } from './dev.js';
3
3
  import { buildProduction } from './build.js';
4
- import { startPreviewServer } from './serve.js'; // NEW
4
+ import { startPreviewServer } from './serve.js';
5
+ import { scaffold, parseCreateArgs } from './scaffolder/index.js';
6
+ import { analyzeBuild } from './analyzer/index.js';
5
7
  import logger from './logger/logger.js';
8
+ import { join } from 'path';
6
9
 
7
10
  export function program() {
8
11
  const args = process.argv.slice(2);
9
12
  const command = args[0] || 'dev';
10
13
 
11
14
  switch (command) {
12
- case 'dev':
15
+ case 'dev': {
13
16
  const devPort = getArg('--port', '-p') || 3000;
14
- startDev({
15
- port: parseInt(devPort),
16
- root: process.cwd()
17
+ startDev({
18
+ port: parseInt(devPort),
19
+ root: process.cwd(),
17
20
  });
18
21
  break;
19
-
20
- case 'build':
21
- buildProduction({
22
- root: process.cwd()
23
- });
22
+ }
23
+
24
+ case 'build': {
25
+ buildProduction({ root: process.cwd() });
24
26
  break;
25
-
26
- // ✅ NEW: Serve command for production preview
27
+ }
28
+
27
29
  case 'serve':
28
- case 'preview':
30
+ case 'preview': {
29
31
  const previewPort = getArg('--port', '-p') || 5000;
30
32
  startPreviewServer({
31
33
  port: parseInt(previewPort),
32
34
  root: process.cwd(),
33
- dir: 'dist' // Default to dist folder
35
+ dir: 'dist',
34
36
  });
35
37
  break;
36
-
38
+ }
39
+
40
+ // ✅ NEW: Component/page/layout scaffolder
41
+ case 'create': {
42
+ const createArgs = args.slice(1);
43
+ const parsed = parseCreateArgs(createArgs);
44
+ if (parsed) {
45
+ scaffold(parsed.type, parsed.name, { root: process.cwd() })
46
+ .then(result => {
47
+ if (!result) process.exit(1);
48
+ })
49
+ .catch(err => {
50
+ logger.error(`Create failed: ${err.message}`);
51
+ process.exit(1);
52
+ });
53
+ }
54
+ break;
55
+ }
56
+
57
+ // ✅ NEW: Bundle analyzer
58
+ case 'analyze': {
59
+ const outDir = join(process.cwd(), 'dist');
60
+ const open = args.includes('--open');
61
+ analyzeBuild(outDir, { open })
62
+ .then(result => {
63
+ if (!result) {
64
+ logger.error('No build found. Run: bertui build');
65
+ process.exit(1);
66
+ }
67
+ })
68
+ .catch(err => {
69
+ logger.error(`Analyze failed: ${err.message}`);
70
+ process.exit(1);
71
+ });
72
+ break;
73
+ }
74
+
37
75
  case '--version':
38
76
  case '-v':
39
- console.log('bertui v1.1.9');
77
+ console.log('bertui v1.2.0');
40
78
  break;
41
-
79
+
42
80
  case '--help':
43
81
  case '-h':
44
82
  showHelp();
45
83
  break;
46
-
84
+
47
85
  default:
48
86
  logger.error(`Unknown command: ${command}`);
49
87
  showHelp();
@@ -62,20 +100,32 @@ function showHelp() {
62
100
  logger.bigLog('BERTUI CLI', { color: 'blue' });
63
101
  console.log(`
64
102
  Commands:
65
- bertui dev [--port] Start development server (default: 3000)
66
- bertui build Build for production
67
- bertui serve [--port] Preview production build (default: 5000)
68
- bertui --version Show version
69
- bertui --help Show help
103
+ bertui dev [--port] Start development server (default: 3000)
104
+ bertui build Build for production
105
+ bertui serve [--port] Preview production build (default: 5000)
106
+ bertui analyze [--open] Analyze bundle size (opens report in browser)
107
+
108
+ bertui create component <Name> Scaffold a React component
109
+ bertui create page <name> Scaffold a page (adds to file-based routing)
110
+ bertui create layout <name> Scaffold a layout (default wraps all pages)
111
+ bertui create loading <route> Scaffold a per-route loading state
112
+ bertui create middleware Scaffold src/middleware.ts
70
113
 
71
114
  Options:
72
- --port, -p <number> Port for server (dev: 3000, serve: 5000)
115
+ --port, -p <number> Port for server
116
+ --open Open browser after command
73
117
 
74
118
  Examples:
75
119
  bertui dev
76
120
  bertui dev --port 8080
77
121
  bertui build
78
- bertui serve
79
- bertui serve --port 4000
122
+ bertui analyze --open
123
+ bertui create component Button
124
+ bertui create page About
125
+ bertui create page blog/[slug]
126
+ bertui create layout default
127
+ bertui create layout blog
128
+ bertui create loading blog
129
+ bertui create middleware
80
130
  `);
81
131
  }