bertui 1.1.8 → 1.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.1.8",
3
+ "version": "1.1.9",
4
4
  "description": "Lightning-fast React dev server powered by Bun - Now with Rust image optimization (WASM, no Rust required for users)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -45,6 +45,8 @@
45
45
  "scripts": {
46
46
  "dev": "bun bin/bertui.js dev",
47
47
  "build": "bun bin/bertui.js build",
48
+ "serve": "bun bin/bertui.js serve",
49
+ "preview": "bun bin/bertui.js serve --port 5000",
48
50
  "build:wasm": "cd src/image-optimizer-rust && wasm-pack build --target web --out-dir ../../dist/image-optimizer/wasm && bun run fix:wasm",
49
51
  "fix:wasm": "node scripts/fix-wasm-exports.js",
50
52
  "prepublishOnly": "echo 'Note: Ensure WASM is built via build:wasm before publishing'",
@@ -69,7 +71,8 @@
69
71
  },
70
72
  "optionalDependencies": {
71
73
  "@bertui/image-optimizer-wasm": "0.1.0",
72
- "oxipng": "^8.0.0"
74
+ "oxipng": "^8.0.0",
75
+ "sass": "^1.69.5"
73
76
  },
74
77
  "keywords": [
75
78
  "react",
@@ -1,102 +1,138 @@
1
- // bertui/src/build/processors/css-builder.js - SAFE VERSION
1
+ // bertui/src/build/processors/css-builder.js - WITH SCSS + CACHING
2
2
  import { join } from 'path';
3
3
  import { existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
5
+ import { globalCache } from '../../utils/cache.js';
6
+ import { minifyCSS, processSCSS } from '../../css/processor.js';
5
7
 
6
8
  export async function buildAllCSS(root, outDir) {
9
+ const startTime = process.hrtime.bigint();
10
+
7
11
  const srcStylesDir = join(root, 'src', 'styles');
8
12
  const stylesOutDir = join(outDir, 'styles');
9
13
 
10
14
  mkdirSync(stylesOutDir, { recursive: true });
11
15
 
12
- if (existsSync(srcStylesDir)) {
13
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
14
-
15
- if (cssFiles.length === 0) {
16
- await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
17
- return;
18
- }
19
-
20
- logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
21
-
22
- let combinedCSS = '';
23
- for (const cssFile of cssFiles) {
24
- const srcPath = join(srcStylesDir, cssFile);
25
- const file = Bun.file(srcPath);
26
- const cssContent = await file.text();
27
- combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
28
- }
29
-
30
- const combinedPath = join(stylesOutDir, 'bertui.min.css');
31
-
32
- // ✅ SAFE: Try Lightning CSS, fallback to simple minification
33
- try {
34
- const minified = await minifyCSSSafe(combinedCSS);
35
- await Bun.write(combinedPath, minified);
36
-
37
- const originalSize = Buffer.byteLength(combinedCSS);
38
- const minifiedSize = Buffer.byteLength(minified);
39
- const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
40
-
41
- logger.success(`CSS minified: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
42
- } catch (error) {
43
- logger.warn(`CSS minification failed: ${error.message}`);
44
- logger.info('Falling back to unminified CSS...');
45
- await Bun.write(combinedPath, combinedCSS);
46
- }
47
-
48
- logger.success(`✅ Combined ${cssFiles.length} CSS files`);
49
- } else {
50
- // No styles directory, create empty CSS
16
+ // Check cache for entire CSS build
17
+ const cacheKey = `css-build:${root}:${Date.now()}`;
18
+ const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache
19
+
20
+ if (cached) {
21
+ logger.info(`⚡ Using cached CSS (${cached.files} files)`);
22
+ await Bun.write(join(stylesOutDir, 'bertui.min.css'), cached.content);
23
+ return;
24
+ }
25
+
26
+ if (!existsSync(srcStylesDir)) {
51
27
  await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No custom styles */');
52
28
  logger.info('No styles directory found, created empty CSS');
29
+ return;
30
+ }
31
+
32
+ // Process SCSS files first
33
+ await processSCSSDirectory(srcStylesDir, root);
34
+
35
+ // Read all CSS files (including compiled SCSS)
36
+ const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
37
+
38
+ if (cssFiles.length === 0) {
39
+ await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
40
+ return;
53
41
  }
42
+
43
+ logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
44
+
45
+ let combinedCSS = '';
46
+ const fileContents = [];
47
+
48
+ for (const cssFile of cssFiles) {
49
+ const srcPath = join(srcStylesDir, cssFile);
50
+
51
+ // Use file cache
52
+ const fileBuffer = await globalCache.getFile(srcPath, { logSpeed: true });
53
+ if (fileBuffer) {
54
+ const content = fileBuffer.toString('utf-8');
55
+ fileContents.push({ filename: cssFile, content });
56
+ combinedCSS += `/* ${cssFile} */\n${content}\n\n`;
57
+ }
58
+ }
59
+
60
+ const combinedPath = join(stylesOutDir, 'bertui.min.css');
61
+
62
+ // Minify with caching
63
+ const minifyCacheKey = `minify:${Buffer.from(combinedCSS).length}:${combinedCSS.substring(0, 100)}`;
64
+ let minified = globalCache.get(minifyCacheKey);
65
+
66
+ if (!minified) {
67
+ minified = await minifyCSS(combinedCSS, {
68
+ filename: 'bertui.min.css',
69
+ sourceMap: false
70
+ });
71
+ globalCache.set(minifyCacheKey, minified, { ttl: 60000 }); // Cache for 60 seconds
72
+ }
73
+
74
+ await Bun.write(combinedPath, minified);
75
+
76
+ const originalSize = Buffer.byteLength(combinedCSS);
77
+ const minifiedSize = Buffer.byteLength(minified);
78
+ const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
79
+
80
+ const endTime = process.hrtime.bigint();
81
+ const duration = Number(endTime - startTime) / 1000; // Microseconds
82
+
83
+ logger.success(`CSS optimized: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
84
+ logger.info(`⚡ Processing time: ${duration.toFixed(3)}µs`);
85
+
86
+ // Cache the final result
87
+ globalCache.set(cacheKey, {
88
+ files: cssFiles.length,
89
+ content: minified,
90
+ size: minifiedSize
91
+ }, { ttl: 5000 });
54
92
  }
55
93
 
56
- /**
57
- * Safe CSS minification with fallback
58
- */
59
- async function minifyCSSSafe(css) {
60
- // Try Lightning CSS first
94
+ // NEW: Process SCSS directory
95
+ async function processSCSSDirectory(stylesDir, root) {
61
96
  try {
62
- const { transform } = await import('lightningcss');
97
+ // Check if sass is installed
98
+ const sass = await import('sass').catch(() => null);
99
+ if (!sass) return;
63
100
 
64
- const { code } = transform({
65
- filename: 'styles.css',
66
- code: Buffer.from(css),
67
- minify: true,
68
- sourceMap: false,
69
- targets: {
70
- chrome: 90 << 16,
71
- firefox: 88 << 16,
72
- safari: 14 << 16,
73
- edge: 90 << 16
74
- }
75
- });
101
+ const files = readdirSync(stylesDir);
102
+ const scssFiles = files.filter(f => f.endsWith('.scss') || f.endsWith('.sass'));
76
103
 
77
- return code.toString();
104
+ if (scssFiles.length === 0) return;
78
105
 
79
- } catch (lightningError) {
80
- logger.warn('Lightning CSS failed, using simple minification');
106
+ logger.info(`📝 Compiling ${scssFiles.length} SCSS files...`);
81
107
 
82
- // Fallback: Simple manual minification
83
- return simpleMinifyCSS(css);
108
+ for (const file of scssFiles) {
109
+ const srcPath = join(stylesDir, file);
110
+ const cssPath = join(stylesDir, file.replace(/\.(scss|sass)$/, '.css'));
111
+
112
+ // Check cache
113
+ const fileBuffer = await globalCache.getFile(srcPath);
114
+ const cacheKey = `scss:${file}:${Buffer.from(fileBuffer).length}`;
115
+ const cached = globalCache.get(cacheKey);
116
+
117
+ if (cached && existsSync(cssPath)) {
118
+ logger.debug(`⚡ Cached SCSS: ${file} → ${file.replace(/\.(scss|sass)$/, '.css')}`);
119
+ continue;
120
+ }
121
+
122
+ const result = sass.compile(srcPath, {
123
+ style: 'expanded',
124
+ sourceMap: false,
125
+ loadPaths: [stylesDir, join(root, 'node_modules')]
126
+ });
127
+
128
+ await Bun.write(cssPath, result.css);
129
+ globalCache.set(cacheKey, true, { ttl: 30000 });
130
+
131
+ logger.debug(` ${file} → ${file.replace(/\.(scss|sass)$/, '.css')}`);
132
+ }
133
+
134
+ logger.success(`✅ SCSS compilation complete`);
135
+ } catch (error) {
136
+ logger.warn(`SCSS processing skipped: ${error.message}`);
84
137
  }
85
- }
86
-
87
- /**
88
- * Simple CSS minification without dependencies
89
- */
90
- function simpleMinifyCSS(css) {
91
- return css
92
- // Remove comments
93
- .replace(/\/\*[\s\S]*?\*\//g, '')
94
- // Remove extra whitespace
95
- .replace(/\s+/g, ' ')
96
- // Remove space around { } : ; ,
97
- .replace(/\s*([{}:;,])\s*/g, '$1')
98
- // Remove trailing semicolons before }
99
- .replace(/;}/g, '}')
100
- // Remove leading/trailing whitespace
101
- .trim();
102
138
  }
package/src/build.js CHANGED
@@ -1,8 +1,9 @@
1
- // bertui/src/build.js - CLEANED (No PageBuilder)
1
+ // bertui/src/build.js - COMPLETE FIXED VERSION WITH PROPER EXIT
2
2
  import { join } from 'path';
3
3
  import { existsSync, mkdirSync, rmSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
5
  import { loadEnvVariables } from './utils/env.js';
6
+ import { globalCache } from './utils/cache.js';
6
7
 
7
8
  import { compileForBuild } from './build/compiler/index.js';
8
9
  import { buildAllCSS } from './build/processors/css-builder.js';
@@ -22,12 +23,12 @@ export async function buildProduction(options = {}) {
22
23
  logger.bigLog('BUILDING WITH SERVER ISLANDS 🏝️', { color: 'green' });
23
24
  logger.info('🔥 OPTIONAL SERVER CONTENT - THE GAME CHANGER');
24
25
 
25
- if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
26
- if (existsSync(outDir)) rmSync(outDir, { recursive: true });
26
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
27
+ if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
27
28
  mkdirSync(buildDir, { recursive: true });
28
29
  mkdirSync(outDir, { recursive: true });
29
30
 
30
- const startTime = Date.now();
31
+ const startTime = process.hrtime.bigint(); // Microsecond precision
31
32
 
32
33
  try {
33
34
  logger.info('Step 0: Loading environment variables...');
@@ -48,53 +49,82 @@ export async function buildProduction(options = {}) {
48
49
  })));
49
50
  }
50
51
 
51
- logger.info('Step 2: Combining CSS...');
52
+ logger.info('Step 2: Processing SCSS/SASS...');
53
+ await processSCSS(root, buildDir);
54
+
55
+ logger.info('Step 3: Combining CSS...');
52
56
  await buildAllCSS(root, outDir);
53
57
 
54
- logger.info('Step 3: Copying static assets...');
58
+ logger.info('Step 4: Copying static assets...');
55
59
  await copyAllStaticAssets(root, outDir);
56
60
 
57
- logger.info('Step 4: Bundling JavaScript...');
61
+ logger.info('Step 5: Bundling JavaScript with Router...');
58
62
  const buildEntry = join(buildDir, 'main.js');
63
+ const routerPath = join(buildDir, 'router.js');
59
64
 
60
65
  if (!existsSync(buildEntry)) {
61
66
  logger.error('❌ main.js not found in build directory!');
62
67
  throw new Error('Build entry point missing');
63
68
  }
64
69
 
65
- const result = await bundleJavaScript(buildEntry, outDir, envVars, buildDir);
70
+ const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir);
66
71
 
67
- logger.info('Step 5: Generating HTML with Server Islands...');
72
+ logger.info('Step 6: Generating HTML with Server Islands...');
68
73
  await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
69
74
 
70
- logger.info('Step 6: Generating sitemap.xml...');
75
+ logger.info('Step 7: Generating sitemap.xml...');
71
76
  await generateSitemap(routes, config, outDir);
72
77
 
73
- logger.info('Step 7: Generating robots.txt...');
78
+ logger.info('Step 8: Generating robots.txt...');
74
79
  await generateRobots(config, outDir, routes);
75
80
 
76
- if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
81
+ // Clean up build directory
82
+ if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
83
+
84
+ const endTime = process.hrtime.bigint();
85
+ const durationMicro = Number(endTime - startTime) / 1000;
86
+ const durationMs = durationMicro / 1000;
77
87
 
78
- const duration = Date.now() - startTime;
79
- showBuildSummary(routes, serverIslands, clientRoutes, duration);
88
+ showBuildSummary(routes, serverIslands, clientRoutes, durationMs, durationMicro);
89
+
90
+ // ✅ FIX: Force exit after successful build
91
+ // Small delay to ensure all logs are flushed
92
+ setTimeout(() => {
93
+ logger.info('✅ Build process complete, exiting...');
94
+ process.exit(0);
95
+ }, 100);
80
96
 
81
97
  } catch (error) {
82
98
  logger.error(`Build failed: ${error.message}`);
83
99
  if (error.stack) logger.error(error.stack);
84
- if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
85
- process.exit(1);
100
+ 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);
86
106
  }
87
107
  }
88
108
 
89
- async function bundleJavaScript(buildEntry, outDir, envVars, buildDir) {
109
+ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir) {
90
110
  try {
111
+ const hasRouter = existsSync(routerPath);
112
+
91
113
  const originalCwd = process.cwd();
92
114
  process.chdir(buildDir);
93
115
 
94
116
  logger.info('🔧 Bundling with production JSX...');
95
117
 
118
+ 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
+
96
126
  const result = await Bun.build({
97
- entrypoints: [buildEntry],
127
+ entrypoints,
98
128
  outdir: join(outDir, 'assets'),
99
129
  target: 'browser',
100
130
  minify: true,
@@ -129,16 +159,38 @@ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir) {
129
159
  if (log.position) {
130
160
  logger.error(` File: ${log.position.file || 'unknown'}`);
131
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) {}
132
174
  }
133
175
  });
176
+ } else {
177
+ logger.error('No detailed logs available');
134
178
  }
135
179
 
136
180
  throw new Error('JavaScript bundling failed');
137
181
  }
138
182
 
183
+ const entryPoints = result.outputs.filter(o => o.kind === 'entry-point');
184
+ const chunks = result.outputs.filter(o => o.kind === 'chunk');
185
+
139
186
  logger.success('✅ JavaScript bundled successfully');
140
- logger.info(` Entry points: ${result.outputs.filter(o => o.kind === 'entry-point').length}`);
141
- logger.info(` Chunks: ${result.outputs.filter(o => o.kind === 'chunk').length}`);
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
+ });
142
194
 
143
195
  const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
144
196
  logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
@@ -147,12 +199,59 @@ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir) {
147
199
 
148
200
  } catch (error) {
149
201
  logger.error('❌ Bundling error: ' + error.message);
202
+ if (error.stack) {
203
+ logger.error('Stack trace:');
204
+ logger.error(error.stack);
205
+ }
150
206
  throw error;
151
207
  }
152
208
  }
153
209
 
154
- function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
155
- logger.success(`✨ Build complete in ${duration}ms`);
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}`);
250
+ }
251
+ }
252
+
253
+ function showBuildSummary(routes, serverIslands, clientRoutes, durationMs, durationMicro) {
254
+ logger.success(`✨ Build complete in ${durationMs.toFixed(3)}ms (${durationMicro.toFixed(0)}µs)`);
156
255
  logger.bigLog('BUILD SUMMARY', { color: 'green' });
157
256
  logger.info(`📄 Total routes: ${routes.length}`);
158
257
  logger.info(`🏝️ Server Islands (SSG): ${serverIslands.length}`);
@@ -160,9 +259,15 @@ function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
160
259
  logger.info(`🗺️ Sitemap: dist/sitemap.xml`);
161
260
  logger.info(`🤖 robots.txt: dist/robots.txt`);
162
261
 
262
+ const cacheStats = globalCache.getStats();
263
+ logger.info(`📊 Cache: ${cacheStats.hitRate} hit rate (${cacheStats.hits}/${cacheStats.hits + cacheStats.misses})`);
264
+
163
265
  if (serverIslands.length > 0) {
164
266
  logger.success('✅ Server Islands enabled - INSTANT content delivery!');
165
267
  }
166
268
 
167
269
  logger.bigLog('READY TO DEPLOY 🚀', { color: 'green' });
270
+
271
+ // ✅ Force log flush
272
+ logger.debug('Build complete, exiting...');
168
273
  }
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
- // src/cli.js
1
+ // bertui/src/cli.js - WITH SERVE COMMAND
2
2
  import { startDev } from './dev.js';
3
3
  import { buildProduction } from './build.js';
4
+ import { startPreviewServer } from './serve.js'; // NEW
4
5
  import logger from './logger/logger.js';
5
6
 
6
7
  export function program() {
@@ -22,9 +23,20 @@ export function program() {
22
23
  });
23
24
  break;
24
25
 
26
+ // ✅ NEW: Serve command for production preview
27
+ case 'serve':
28
+ case 'preview':
29
+ const previewPort = getArg('--port', '-p') || 5000;
30
+ startPreviewServer({
31
+ port: parseInt(previewPort),
32
+ root: process.cwd(),
33
+ dir: 'dist' // Default to dist folder
34
+ });
35
+ break;
36
+
25
37
  case '--version':
26
38
  case '-v':
27
- console.log('bertui v0.1.0');
39
+ console.log('bertui v1.1.9');
28
40
  break;
29
41
 
30
42
  case '--help':
@@ -50,17 +62,20 @@ function showHelp() {
50
62
  logger.bigLog('BERTUI CLI', { color: 'blue' });
51
63
  console.log(`
52
64
  Commands:
53
- bertui dev Start development server
65
+ bertui dev [--port] Start development server (default: 3000)
54
66
  bertui build Build for production
67
+ bertui serve [--port] Preview production build (default: 5000)
55
68
  bertui --version Show version
56
69
  bertui --help Show help
57
70
 
58
71
  Options:
59
- --port, -p <number> Port for dev server (default: 3000)
72
+ --port, -p <number> Port for server (dev: 3000, serve: 5000)
60
73
 
61
74
  Examples:
62
75
  bertui dev
63
76
  bertui dev --port 8080
64
77
  bertui build
78
+ bertui serve
79
+ bertui serve --port 4000
65
80
  `);
66
81
  }
@@ -124,4 +124,49 @@ export function extractCSSImports(code) {
124
124
  */
125
125
  export function isCSSFile(filename) {
126
126
  return filename.toLowerCase().endsWith('.css');
127
- }
127
+ }
128
+
129
+ // ============================================
130
+ // OPTIONAL SCSS SUPPORT - COMMENTED OUT BY DEFAULT
131
+ // Uncomment only if you install 'sass' package
132
+ // ============================================
133
+
134
+
135
+ export async function processSCSS(scssCode, options = {}) {
136
+ try {
137
+ // Dynamic import so it doesn't fail if sass isn't installed
138
+ const sass = await import('sass').catch(() => {
139
+ throw new Error('sass package not installed. Run: bun add sass');
140
+ });
141
+
142
+ const result = sass.compileString(scssCode, {
143
+ style: options.compressed ? 'compressed' : 'expanded',
144
+ sourceMap: false,
145
+ loadPaths: options.loadPaths || []
146
+ });
147
+
148
+ return result.css;
149
+ } catch (error) {
150
+ logger.error(`SCSS compilation failed: ${error.message}`);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ export async function compileSCSSFile(filePath, options = {}) {
156
+ try {
157
+ const sass = await import('sass').catch(() => {
158
+ throw new Error('sass package not installed. Run: bun add sass');
159
+ });
160
+
161
+ const result = sass.compile(filePath, {
162
+ style: options.compressed ? 'compressed' : 'expanded',
163
+ sourceMap: false,
164
+ loadPaths: options.loadPaths || [require('path').dirname(filePath)]
165
+ });
166
+
167
+ return result.css;
168
+ } catch (error) {
169
+ logger.error(`SCSS file compilation failed: ${error.message}`);
170
+ throw error;
171
+ }
172
+ }
package/src/serve.js ADDED
@@ -0,0 +1,195 @@
1
+ // bertui/src/serve.js - ULTRA-FAST PRODUCTION PREVIEW SERVER
2
+ import { join, extname } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import logger from './logger/logger.js';
5
+ import { globalCache } from './utils/cache.js';
6
+
7
+ // MIME types for fast serving
8
+ const MIME_TYPES = {
9
+ '.html': 'text/html',
10
+ '.css': 'text/css',
11
+ '.js': 'application/javascript',
12
+ '.mjs': 'application/javascript',
13
+ '.json': 'application/json',
14
+ '.png': 'image/png',
15
+ '.jpg': 'image/jpeg',
16
+ '.jpeg': 'image/jpeg',
17
+ '.gif': 'image/gif',
18
+ '.svg': 'image/svg+xml',
19
+ '.webp': 'image/webp',
20
+ '.avif': 'image/avif',
21
+ '.ico': 'image/x-icon',
22
+ '.woff': 'font/woff',
23
+ '.woff2': 'font/woff2',
24
+ '.ttf': 'font/ttf',
25
+ '.otf': 'font/otf',
26
+ '.txt': 'text/plain',
27
+ '.xml': 'application/xml',
28
+ '.pdf': 'application/pdf',
29
+ '.map': 'application/json'
30
+ };
31
+
32
+ export async function startPreviewServer(options = {}) {
33
+ const root = options.root || process.cwd();
34
+ const port = options.port || 5000;
35
+ const distDir = options.dir || 'dist';
36
+ const publicPath = join(root, distDir);
37
+
38
+ // Check if dist folder exists
39
+ if (!existsSync(publicPath)) {
40
+ logger.error(`❌ ${distDir}/ folder not found!`);
41
+ logger.info(` Run 'bertui build' first to generate production files.`);
42
+ process.exit(1);
43
+ }
44
+
45
+ logger.bigLog(`🚀 PREVIEW SERVER`, { color: 'green' });
46
+ logger.info(`📁 Serving: ${publicPath}`);
47
+ logger.info(`🌐 URL: http://localhost:${port}`);
48
+ logger.info(`⚡ Press Ctrl+C to stop`);
49
+
50
+ // Track connections for graceful shutdown
51
+ const connections = new Set();
52
+
53
+ // Create ultra-fast static server
54
+ const server = Bun.serve({
55
+ port,
56
+ async fetch(req) {
57
+ const url = new URL(req.url);
58
+ let filePath = join(publicPath, url.pathname);
59
+
60
+ // Handle root path - serve index.html
61
+ if (url.pathname === '/') {
62
+ filePath = join(publicPath, 'index.html');
63
+ }
64
+
65
+ // Handle directory requests - serve index.html
66
+ if (!extname(filePath)) {
67
+ const indexPath = join(filePath, 'index.html');
68
+ if (existsSync(indexPath)) {
69
+ filePath = indexPath;
70
+ }
71
+ }
72
+
73
+ // Check if file exists
74
+ if (!existsSync(filePath)) {
75
+ // Try fallback to index.html for SPA routing
76
+ if (!url.pathname.includes('.')) {
77
+ const spaPath = join(publicPath, 'index.html');
78
+ if (existsSync(spaPath)) {
79
+ const file = Bun.file(spaPath);
80
+ return new Response(file, {
81
+ headers: {
82
+ 'Content-Type': 'text/html',
83
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
84
+ 'X-BertUI-Preview': 'spa-fallback'
85
+ }
86
+ });
87
+ }
88
+ }
89
+
90
+ return new Response('Not Found', { status: 404 });
91
+ }
92
+
93
+ // Get file stats for caching
94
+ const stats = await Bun.file(filePath).stat();
95
+ const ext = extname(filePath).toLowerCase();
96
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
97
+
98
+ // Set cache headers based on file type
99
+ const isStaticAsset = ['.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.avif', '.ico'].includes(ext);
100
+ const cacheControl = isStaticAsset
101
+ ? 'public, max-age=31536000, immutable' // 1 year for assets with hash in name
102
+ : 'no-cache, no-store, must-revalidate'; // No cache for HTML
103
+
104
+ // Serve file with proper headers
105
+ const file = Bun.file(filePath);
106
+
107
+ return new Response(file, {
108
+ headers: {
109
+ 'Content-Type': contentType,
110
+ 'Content-Length': stats.size,
111
+ 'Cache-Control': cacheControl,
112
+ 'X-BertUI-Preview': 'static'
113
+ }
114
+ });
115
+ },
116
+
117
+ // Track connections for graceful shutdown
118
+ websocket: {
119
+ open(ws) {
120
+ connections.add(ws);
121
+ },
122
+ close(ws) {
123
+ connections.delete(ws);
124
+ }
125
+ }
126
+ });
127
+
128
+ // Handle graceful shutdown
129
+ process.on('SIGINT', () => {
130
+ logger.info('\n👋 Shutting down preview server...');
131
+
132
+ // Close all WebSocket connections
133
+ for (const ws of connections) {
134
+ try {
135
+ ws.close();
136
+ } catch (e) {}
137
+ }
138
+
139
+ server.stop();
140
+ process.exit(0);
141
+ });
142
+
143
+ return server;
144
+ }
145
+
146
+ // FAST FILE LISTING FOR DEBUGGING
147
+ export async function listDistContents(distPath) {
148
+ try {
149
+ const { readdirSync, statSync } = await import('fs');
150
+ const { join, relative } = await import('path');
151
+
152
+ function scan(dir, level = 0) {
153
+ const files = readdirSync(dir);
154
+ const result = [];
155
+
156
+ for (const file of files) {
157
+ const fullPath = join(dir, file);
158
+ const stat = statSync(fullPath);
159
+ const relPath = relative(distPath, fullPath);
160
+
161
+ if (stat.isDirectory()) {
162
+ result.push({
163
+ name: file,
164
+ path: relPath,
165
+ type: 'directory',
166
+ children: scan(fullPath, level + 1)
167
+ });
168
+ } else {
169
+ result.push({
170
+ name: file,
171
+ path: relPath,
172
+ type: 'file',
173
+ size: stat.size,
174
+ sizeFormatted: formatBytes(stat.size)
175
+ });
176
+ }
177
+ }
178
+
179
+ return result;
180
+ }
181
+
182
+ return scan(distPath);
183
+ } catch (error) {
184
+ logger.error(`Failed to list dist contents: ${error.message}`);
185
+ return [];
186
+ }
187
+ }
188
+
189
+ function formatBytes(bytes) {
190
+ if (bytes === 0) return '0 B';
191
+ const k = 1024;
192
+ const sizes = ['B', 'KB', 'MB', 'GB'];
193
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
194
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
195
+ }
@@ -1,10 +1,9 @@
1
- // bertui/src/server/dev-server-utils.js - NEW FILE
2
- // Shared utilities for dev server (extracted from dev-server.js)
3
-
1
+ // bertui/src/server/dev-server-utils.js - WITH CACHE IMPORT
4
2
  import { join, extname } from 'path';
5
3
  import { existsSync, readdirSync, watch } from 'fs';
6
4
  import logger from '../logger/logger.js';
7
5
  import { compileProject } from '../client/compiler.js';
6
+ import { globalCache } from '../utils/cache.js'; // ✅ Now this works!
8
7
 
9
8
  // Image content type mapping
10
9
  export function getImageContentType(ext) {
@@ -48,8 +47,17 @@ export function getContentType(ext) {
48
47
  return types[ext] || 'text/plain';
49
48
  }
50
49
 
51
- // HTML generator
50
+ // HTML generator with caching
52
51
  export async function serveHTML(root, hasRouter, config, port) {
52
+ const cacheKey = `html:${root}:${port}`;
53
+
54
+ // Try cache first
55
+ const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache during dev
56
+ if (cached) {
57
+ logger.debug('⚡ Serving cached HTML');
58
+ return cached;
59
+ }
60
+
53
61
  const meta = config.meta || {};
54
62
 
55
63
  const srcStylesDir = join(root, 'src', 'styles');
@@ -203,10 +211,13 @@ ${bertuiAnimateStylesheet}
203
211
  </body>
204
212
  </html>`;
205
213
 
214
+ // Cache the HTML
215
+ globalCache.set(cacheKey, html, { ttl: 1000 });
216
+
206
217
  return html;
207
218
  }
208
219
 
209
- // File watcher setup
220
+ // File watcher setup (unchanged)
210
221
  export function setupFileWatcher(root, compiledDir, clients, onRecompile) {
211
222
  const srcDir = join(root, 'src');
212
223
  const configPath = join(root, 'bertui.config.js');
@@ -0,0 +1,297 @@
1
+ // bertui/src/utils/cache.js - ULTRA FAST CACHING (Microsecond precision)
2
+ import { createHash } from 'crypto';
3
+ import logger from '../logger/logger.js';
4
+
5
+ export class BertuiCache {
6
+ constructor(options = {}) {
7
+ this.maxSize = options.maxSize || 5000;
8
+ this.ttl = options.ttl || 30000; // 30 seconds default
9
+ this.stats = { hits: 0, misses: 0, sets: 0, evictions: 0 };
10
+
11
+ // Main cache store
12
+ this.store = new Map();
13
+
14
+ // File content cache with timestamps
15
+ this.fileCache = new Map();
16
+ this.fileTimestamps = new Map();
17
+
18
+ // Compiled code cache (keyed by content hash)
19
+ this.codeCache = new Map();
20
+
21
+ // CSS processing cache
22
+ this.cssCache = new Map();
23
+
24
+ // Image optimization cache
25
+ this.imageCache = new Map();
26
+
27
+ // Weak reference cache for DOM objects (if in browser)
28
+ if (typeof WeakRef !== 'undefined') {
29
+ this.weakCache = new Map();
30
+ }
31
+
32
+ // Start periodic cleanup
33
+ this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
34
+ }
35
+
36
+ // ULTRA FAST GET with microsecond timing
37
+ get(key, options = {}) {
38
+ const start = process.hrtime.bigint();
39
+
40
+ const item = this.store.get(key);
41
+ if (!item) {
42
+ this.stats.misses++;
43
+ return null;
44
+ }
45
+
46
+ // Check TTL
47
+ const ttl = options.ttl || item.ttl || this.ttl;
48
+ if (Date.now() - item.timestamp > ttl) {
49
+ this.store.delete(key);
50
+ this.stats.misses++;
51
+ this.stats.evictions++;
52
+ return null;
53
+ }
54
+
55
+ this.stats.hits++;
56
+
57
+ // Update access time for LRU
58
+ item.lastAccessed = Date.now();
59
+
60
+ if (options.logSpeed) {
61
+ const end = process.hrtime.bigint();
62
+ const duration = Number(end - start) / 1000; // Microseconds
63
+ logger.debug(`⚡ Cache hit: ${duration.toFixed(3)}µs for ${key.substring(0, 30)}...`);
64
+ }
65
+
66
+ return item.value;
67
+ }
68
+
69
+ set(key, value, options = {}) {
70
+ // Generate hash for large values to save memory
71
+ const valueHash = typeof value === 'string' && value.length > 10000
72
+ ? createHash('md5').update(value).digest('hex')
73
+ : null;
74
+
75
+ this.store.set(key, {
76
+ value: valueHash ? { __hash: valueHash, __original: null } : value,
77
+ valueHash,
78
+ timestamp: Date.now(),
79
+ lastAccessed: Date.now(),
80
+ ttl: options.ttl || this.ttl,
81
+ size: this.getSize(value)
82
+ });
83
+
84
+ this.stats.sets++;
85
+
86
+ // Store original separately if hashed
87
+ if (valueHash) {
88
+ this.codeCache.set(valueHash, value);
89
+ }
90
+
91
+ // LRU cleanup if needed
92
+ if (this.store.size > this.maxSize) {
93
+ this.evictLRU();
94
+ }
95
+ }
96
+
97
+ // FILE CACHE: Zero-copy file reading with mtime validation
98
+ async getFile(filePath, options = {}) {
99
+ const cacheKey = `file:${filePath}`;
100
+
101
+ try {
102
+ const file = Bun.file(filePath);
103
+ const exists = await file.exists();
104
+ if (!exists) return null;
105
+
106
+ const stats = await file.stat();
107
+ const mtimeMs = stats.mtimeMs;
108
+
109
+ // Check cache
110
+ const cached = this.fileCache.get(cacheKey);
111
+ const cachedTime = this.fileTimestamps.get(cacheKey);
112
+
113
+ if (cached && cachedTime === mtimeMs) {
114
+ if (options.logSpeed) {
115
+ logger.debug(`📄 File cache hit: ${filePath.split('/').pop()}`);
116
+ }
117
+ return cached;
118
+ }
119
+
120
+ // Read file (single operation)
121
+ const start = process.hrtime.bigint();
122
+ const content = await file.arrayBuffer();
123
+ const buffer = Buffer.from(content);
124
+ const end = process.hrtime.bigint();
125
+
126
+ // Store in cache
127
+ this.fileCache.set(cacheKey, buffer);
128
+ this.fileTimestamps.set(cacheKey, mtimeMs);
129
+
130
+ if (options.logSpeed) {
131
+ const duration = Number(end - start) / 1000;
132
+ logger.debug(`📄 File read: ${duration.toFixed(3)}µs - ${filePath.split('/').pop()}`);
133
+ }
134
+
135
+ return buffer;
136
+
137
+ } catch (error) {
138
+ logger.error(`File cache error: ${filePath} - ${error.message}`);
139
+ return null;
140
+ }
141
+ }
142
+
143
+ // CODE TRANSFORMATION CACHE
144
+ getTransformed(sourceCode, options = {}) {
145
+ const hash = createHash('md5')
146
+ .update(sourceCode)
147
+ .update(JSON.stringify(options))
148
+ .digest('hex');
149
+
150
+ const cacheKey = `transform:${hash}`;
151
+ return this.get(cacheKey, options);
152
+ }
153
+
154
+ setTransformed(sourceCode, result, options = {}) {
155
+ const hash = createHash('md5')
156
+ .update(sourceCode)
157
+ .update(JSON.stringify(options))
158
+ .digest('hex');
159
+
160
+ const cacheKey = `transform:${hash}`;
161
+ this.set(cacheKey, result, options);
162
+ }
163
+
164
+ // CSS PROCESSING CACHE
165
+ getCSS(css, options = {}) {
166
+ const hash = createHash('md5')
167
+ .update(css)
168
+ .update(JSON.stringify(options))
169
+ .digest('hex');
170
+
171
+ return this.cssCache.get(hash);
172
+ }
173
+
174
+ setCSS(css, result, options = {}) {
175
+ const hash = createHash('md5')
176
+ .update(css)
177
+ .update(JSON.stringify(options))
178
+ .digest('hex');
179
+
180
+ this.cssCache.set(hash, result);
181
+ }
182
+
183
+ // BATCH OPERATIONS
184
+ mget(keys) {
185
+ const results = [];
186
+ for (const key of keys) {
187
+ results.push(this.get(key));
188
+ }
189
+ return results;
190
+ }
191
+
192
+ mset(entries) {
193
+ for (const [key, value] of entries) {
194
+ this.set(key, value);
195
+ }
196
+ }
197
+
198
+ // STATS with microsecond precision
199
+ getStats() {
200
+ const total = this.stats.hits + this.stats.misses;
201
+ const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(2) : 0;
202
+
203
+ // Memory usage
204
+ const memUsage = process.memoryUsage();
205
+
206
+ return {
207
+ hits: this.stats.hits,
208
+ misses: this.stats.misses,
209
+ sets: this.stats.sets,
210
+ evictions: this.stats.evictions,
211
+ hitRate: `${hitRate}%`,
212
+ size: this.store.size,
213
+ fileCacheSize: this.fileCache.size,
214
+ codeCacheSize: this.codeCache.size,
215
+ cssCacheSize: this.cssCache.size,
216
+ imageCacheSize: this.imageCache.size,
217
+ memory: {
218
+ heapUsed: `${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB`,
219
+ heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
220
+ rss: `${(memUsage.rss / 1024 / 1024).toFixed(2)} MB`
221
+ }
222
+ };
223
+ }
224
+
225
+ // PRIVATE METHODS
226
+ getSize(value) {
227
+ if (typeof value === 'string') return value.length;
228
+ if (Buffer.isBuffer(value)) return value.length;
229
+ if (typeof value === 'object') return JSON.stringify(value).length;
230
+ return 0;
231
+ }
232
+
233
+ evictLRU() {
234
+ const entries = Array.from(this.store.entries());
235
+ entries.sort((a, b) => a[1].lastAccessed - b[1].lastAccessed);
236
+
237
+ const removeCount = Math.floor(this.maxSize * 0.2); // Remove 20%
238
+ for (let i = 0; i < removeCount && i < entries.length; i++) {
239
+ this.store.delete(entries[i][0]);
240
+ this.stats.evictions++;
241
+ }
242
+ }
243
+
244
+ cleanup() {
245
+ const now = Date.now();
246
+ let evicted = 0;
247
+
248
+ for (const [key, item] of this.store.entries()) {
249
+ if (now - item.timestamp > item.ttl) {
250
+ this.store.delete(key);
251
+ evicted++;
252
+ }
253
+ }
254
+
255
+ if (evicted > 0) {
256
+ logger.debug(`🧹 Cache cleanup: removed ${evicted} expired items`);
257
+ this.stats.evictions += evicted;
258
+ }
259
+ }
260
+
261
+ dispose() {
262
+ if (this.cleanupInterval) {
263
+ clearInterval(this.cleanupInterval);
264
+ }
265
+ this.store.clear();
266
+ this.fileCache.clear();
267
+ this.fileTimestamps.clear();
268
+ this.codeCache.clear();
269
+ this.cssCache.clear();
270
+ this.imageCache.clear();
271
+ }
272
+ }
273
+
274
+ // Singleton instance
275
+ export const globalCache = new BertuiCache();
276
+
277
+ // Decorator for automatic caching of async functions
278
+ export function cached(options = {}) {
279
+ return function(target, propertyKey, descriptor) {
280
+ const originalMethod = descriptor.value;
281
+
282
+ descriptor.value = async function(...args) {
283
+ const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
284
+ const cached = globalCache.get(cacheKey, options);
285
+
286
+ if (cached !== null) {
287
+ return cached;
288
+ }
289
+
290
+ const result = await originalMethod.apply(this, args);
291
+ globalCache.set(cacheKey, result, options);
292
+ return result;
293
+ };
294
+
295
+ return descriptor;
296
+ };
297
+ }