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 +5 -2
- package/src/build/processors/css-builder.js +116 -80
- package/src/build.js +127 -22
- package/src/cli.js +19 -4
- package/src/css/processor.js +46 -1
- package/src/serve.js +195 -0
- package/src/server/dev-server-utils.js +16 -5
- package/src/utils/cache.js +297 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "1.1.
|
|
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 -
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
// Check if sass is installed
|
|
98
|
+
const sass = await import('sass').catch(() => null);
|
|
99
|
+
if (!sass) return;
|
|
63
100
|
|
|
64
|
-
const
|
|
65
|
-
|
|
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
|
-
|
|
104
|
+
if (scssFiles.length === 0) return;
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
logger.warn('Lightning CSS failed, using simple minification');
|
|
106
|
+
logger.info(`📝 Compiling ${scssFiles.length} SCSS files...`);
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
|
|
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 -
|
|
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 =
|
|
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:
|
|
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
|
|
58
|
+
logger.info('Step 4: Copying static assets...');
|
|
55
59
|
await copyAllStaticAssets(root, outDir);
|
|
56
60
|
|
|
57
|
-
logger.info('Step
|
|
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
|
|
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
|
|
75
|
+
logger.info('Step 7: Generating sitemap.xml...');
|
|
71
76
|
await generateSitemap(routes, config, outDir);
|
|
72
77
|
|
|
73
|
-
logger.info('Step
|
|
78
|
+
logger.info('Step 8: Generating robots.txt...');
|
|
74
79
|
await generateRobots(config, outDir, routes);
|
|
75
80
|
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
|
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: ${
|
|
141
|
-
logger.info(` Chunks: ${
|
|
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
|
|
155
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/css/processor.js
CHANGED
|
@@ -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 -
|
|
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
|
+
}
|