bertui 1.1.8 → 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/index.js CHANGED
@@ -1,6 +1,4 @@
1
- // ============================================
2
- // FILE: bertui/index.js (Located in root)
3
- // ============================================
1
+ // bertui/index.js - v1.2.0 with all features
4
2
 
5
3
  // Compiler
6
4
  export { compileProject, compileFile } from './src/client/compiler.js';
@@ -11,11 +9,11 @@ export { discoverRoutes } from './src/build/compiler/route-discoverer.js';
11
9
  export { hmr } from './src/client/hmr-runtime.js';
12
10
 
13
11
  // Image Optimizer
14
- export {
15
- optimizeImage,
16
- optimizeImagesBatch,
17
- hasWasm,
18
- version as optimizerVersion
12
+ export {
13
+ optimizeImage,
14
+ optimizeImagesBatch,
15
+ hasWasm,
16
+ version as optimizerVersion,
19
17
  } from './src/image-optimizer/index.js';
20
18
 
21
19
  // Build
@@ -35,41 +33,63 @@ export { default as logger } from './src/logger/logger.js';
35
33
  // CLI
36
34
  export { program } from './src/cli.js';
37
35
 
36
+ // ✅ NEW: Middleware system
37
+ export {
38
+ MiddlewareManager,
39
+ loadMiddleware,
40
+ runMiddleware,
41
+ MiddlewareContext,
42
+ } from './src/middleware/index.js';
43
+
44
+ // ✅ NEW: Layout system
45
+ export {
46
+ discoverLayouts,
47
+ compileLayouts,
48
+ matchLayout,
49
+ generateLayoutWrapper,
50
+ injectLayoutsIntoRouter,
51
+ } from './src/layouts/index.js';
52
+
53
+ // ✅ NEW: Loading states
54
+ export {
55
+ discoverLoadingComponents,
56
+ compileLoadingComponents,
57
+ generateLoadingAwareRouter,
58
+ getLoadingScript,
59
+ DEFAULT_LOADING_HTML,
60
+ } from './src/loading/index.js';
61
+
62
+ // ✅ NEW: Partial hydration
63
+ export {
64
+ needsHydration,
65
+ getInteractiveFeatures,
66
+ analyzeRoutes,
67
+ generatePartialHydrationCode,
68
+ logHydrationReport,
69
+ } from './src/hydration/index.js';
70
+
71
+ // ✅ NEW: Bundle analyzer
72
+ export { analyzeBuild } from './src/analyzer/index.js';
73
+
74
+ // ✅ NEW: CLI scaffolder
75
+ export { scaffold, parseCreateArgs } from './src/scaffolder/index.js';
76
+
77
+ // Server
78
+ export { createDevHandler } from './src/server/dev-handler.js';
79
+ export { startDevServer } from './src/server/dev-server.js';
80
+
81
+ // CSS
82
+ export { minifyCSS, combineCSS } from './src/css/processor.js';
83
+
84
+ // Images
85
+ export { copyImagesSync, isImageFile } from './src/images/index.js';
86
+
87
+ // Server Islands
88
+ export {
89
+ extractStaticHTML,
90
+ isServerIsland,
91
+ validateServerIsland,
92
+ } from './src/server-islands/index.js';
93
+
38
94
  // Version
39
- export const version = '1.2.0';
40
-
41
- // Import for default export
42
- import { compileProject, compileFile } from './src/client/compiler.js';
43
- import { compileForBuild } from './src/build/compiler/index.js';
44
- import { discoverRoutes } from './src/build/compiler/route-discoverer.js';
45
- import { hmr } from './src/client/hmr-runtime.js';
46
- import { optimizeImage, optimizeImagesBatch } from './src/image-optimizer/index.js';
47
- import { optimizeImages } from './src/build/image-optimizer.js';
48
- import { buildProduction } from './src/build.js';
49
- import { Router, Link, useRouter } from './src/router/index.js';
50
- import { SSRRouter } from './src/router/SSRRouter.js';
51
- import { loadConfig, defaultConfig } from './src/config/index.js';
52
- import logger from './src/logger/logger.js';
53
- import { program } from './src/cli.js';
54
-
55
- // Default export
56
- export default {
57
- compileProject,
58
- compileFile,
59
- compileForBuild,
60
- discoverRoutes,
61
- hmr,
62
- optimizeImage,
63
- optimizeImagesBatch,
64
- optimizeImages,
65
- buildProduction,
66
- Router,
67
- Link,
68
- useRouter,
69
- SSRRouter,
70
- loadConfig,
71
- defaultConfig,
72
- logger,
73
- program,
74
- version: '1.2.0'
75
- };
95
+ export const version = '1.2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.1.8",
3
+ "version": "1.2.0",
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",
@@ -0,0 +1,370 @@
1
+ // bertui/src/analyzer/index.js
2
+ // Bundle analyzer - reads Bun metafile, generates HTML report
3
+
4
+ import { join, relative } from 'path';
5
+ import { existsSync } from 'fs';
6
+ import logger from '../logger/logger.js';
7
+
8
+ /**
9
+ * Analyze build output and generate HTML report
10
+ */
11
+ export async function analyzeBuild(outDir, options = {}) {
12
+ const {
13
+ open = false,
14
+ outputFile = join(outDir, 'bundle-report.html'),
15
+ title = 'BertUI Bundle Report',
16
+ } = options;
17
+
18
+ // Collect all JS files from dist/assets
19
+ const assetsDir = join(outDir, 'assets');
20
+ if (!existsSync(assetsDir)) {
21
+ logger.warn('No assets directory found. Run bun run build first.');
22
+ return null;
23
+ }
24
+
25
+ const files = await collectFiles(assetsDir, outDir);
26
+ const report = generateReport(files, title, outDir);
27
+
28
+ await Bun.write(outputFile, report);
29
+ logger.success(`📊 Bundle report: ${outputFile}`);
30
+
31
+ if (open) {
32
+ try {
33
+ const { exec } = await import('child_process');
34
+ const cmd = process.platform === 'darwin' ? 'open' :
35
+ process.platform === 'win32' ? 'start' : 'xdg-open';
36
+ exec(`${cmd} ${outputFile}`);
37
+ } catch (e) {}
38
+ }
39
+
40
+ return { outputFile, files };
41
+ }
42
+
43
+ async function collectFiles(assetsDir, outDir) {
44
+ const { readdirSync, statSync } = await import('fs');
45
+ const files = [];
46
+
47
+ function scan(dir) {
48
+ const entries = readdirSync(dir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ const fullPath = join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ scan(fullPath);
53
+ } else if (entry.isFile()) {
54
+ const stat = statSync(fullPath);
55
+ const relPath = relative(outDir, fullPath);
56
+ const ext = entry.name.split('.').pop();
57
+
58
+ files.push({
59
+ name: entry.name,
60
+ path: relPath,
61
+ size: stat.size,
62
+ sizeKB: (stat.size / 1024).toFixed(2),
63
+ type: getFileType(ext),
64
+ ext,
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ scan(assetsDir);
71
+ files.sort((a, b) => b.size - a.size);
72
+ return files;
73
+ }
74
+
75
+ function getFileType(ext) {
76
+ if (['js', 'mjs'].includes(ext)) return 'javascript';
77
+ if (ext === 'css') return 'css';
78
+ if (['map'].includes(ext)) return 'sourcemap';
79
+ if (['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif'].includes(ext)) return 'image';
80
+ return 'other';
81
+ }
82
+
83
+ function formatBytes(bytes) {
84
+ if (bytes === 0) return '0 B';
85
+ const k = 1024;
86
+ const sizes = ['B', 'KB', 'MB', 'GB'];
87
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
88
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
89
+ }
90
+
91
+ function getColor(type) {
92
+ return {
93
+ javascript: '#3b82f6',
94
+ css: '#8b5cf6',
95
+ sourcemap: '#6b7280',
96
+ image: '#f59e0b',
97
+ other: '#10b981',
98
+ }[type] || '#6b7280';
99
+ }
100
+
101
+ function generateReport(files, title, outDir) {
102
+ const totalSize = files.reduce((s, f) => s + f.size, 0);
103
+ const jsFiles = files.filter(f => f.type === 'javascript');
104
+ const cssFiles = files.filter(f => f.type === 'css');
105
+ const imageFiles = files.filter(f => f.type === 'image');
106
+ const otherFiles = files.filter(f => !['javascript','css','image'].includes(f.type));
107
+
108
+ const jsTotal = jsFiles.reduce((s, f) => s + f.size, 0);
109
+ const cssTotal = cssFiles.reduce((s, f) => s + f.size, 0);
110
+ const imageTotal = imageFiles.reduce((s, f) => s + f.size, 0);
111
+
112
+ const fileRows = files.map(f => {
113
+ const pct = totalSize > 0 ? ((f.size / totalSize) * 100).toFixed(1) : 0;
114
+ const barWidth = Math.max(2, Math.round((f.size / (files[0]?.size || 1)) * 200));
115
+ const color = getColor(f.type);
116
+ return `
117
+ <tr>
118
+ <td class="file-name">
119
+ <span class="dot" style="background:${color}"></span>
120
+ ${f.name}
121
+ </td>
122
+ <td class="file-path">${f.path}</td>
123
+ <td class="file-type">${f.type}</td>
124
+ <td class="file-size">${formatBytes(f.size)}</td>
125
+ <td class="file-pct">
126
+ <div class="bar-wrap">
127
+ <div class="bar" style="width:${barWidth}px;background:${color}"></div>
128
+ <span>${pct}%</span>
129
+ </div>
130
+ </td>
131
+ </tr>`;
132
+ }).join('');
133
+
134
+ const now = new Date().toLocaleString();
135
+
136
+ return `<!DOCTYPE html>
137
+ <html lang="en">
138
+ <head>
139
+ <meta charset="UTF-8">
140
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
141
+ <title>${title}</title>
142
+ <style>
143
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
144
+ body {
145
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
146
+ background: #0f172a;
147
+ color: #e2e8f0;
148
+ min-height: 100vh;
149
+ padding: 32px;
150
+ }
151
+ .header {
152
+ display: flex;
153
+ align-items: center;
154
+ gap: 16px;
155
+ margin-bottom: 32px;
156
+ }
157
+ .header h1 { font-size: 28px; font-weight: 700; color: #f8fafc; }
158
+ .header .badge {
159
+ background: #10b981;
160
+ color: white;
161
+ padding: 4px 12px;
162
+ border-radius: 20px;
163
+ font-size: 12px;
164
+ font-weight: 600;
165
+ }
166
+ .meta { color: #64748b; font-size: 13px; margin-top: 4px; }
167
+ .cards {
168
+ display: grid;
169
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
170
+ gap: 16px;
171
+ margin-bottom: 32px;
172
+ }
173
+ .card {
174
+ background: #1e293b;
175
+ border: 1px solid #334155;
176
+ border-radius: 12px;
177
+ padding: 20px;
178
+ }
179
+ .card .label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }
180
+ .card .value { font-size: 28px; font-weight: 700; margin-top: 4px; }
181
+ .card .sub { font-size: 12px; color: #64748b; margin-top: 2px; }
182
+ .section {
183
+ background: #1e293b;
184
+ border: 1px solid #334155;
185
+ border-radius: 12px;
186
+ overflow: hidden;
187
+ margin-bottom: 24px;
188
+ }
189
+ .section-header {
190
+ padding: 16px 24px;
191
+ border-bottom: 1px solid #334155;
192
+ display: flex;
193
+ align-items: center;
194
+ gap: 8px;
195
+ }
196
+ .section-header h2 { font-size: 15px; font-weight: 600; color: #f1f5f9; }
197
+ .section-header .count {
198
+ background: #334155;
199
+ color: #94a3b8;
200
+ padding: 2px 8px;
201
+ border-radius: 10px;
202
+ font-size: 12px;
203
+ }
204
+ table { width: 100%; border-collapse: collapse; }
205
+ th {
206
+ text-align: left;
207
+ padding: 12px 24px;
208
+ font-size: 11px;
209
+ text-transform: uppercase;
210
+ letter-spacing: 0.05em;
211
+ color: #64748b;
212
+ border-bottom: 1px solid #334155;
213
+ }
214
+ td {
215
+ padding: 12px 24px;
216
+ font-size: 13px;
217
+ border-bottom: 1px solid #1e293b;
218
+ vertical-align: middle;
219
+ }
220
+ tr:last-child td { border-bottom: none; }
221
+ tr:hover td { background: #263348; }
222
+ .file-name { font-weight: 600; color: #f1f5f9; white-space: nowrap; }
223
+ .file-path { color: #64748b; font-size: 12px; font-family: monospace; }
224
+ .file-type { color: #94a3b8; font-size: 12px; text-transform: uppercase; }
225
+ .file-size { font-weight: 600; color: #10b981; white-space: nowrap; }
226
+ .dot {
227
+ display: inline-block;
228
+ width: 8px;
229
+ height: 8px;
230
+ border-radius: 50%;
231
+ margin-right: 8px;
232
+ vertical-align: middle;
233
+ }
234
+ .bar-wrap {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 8px;
238
+ }
239
+ .bar {
240
+ height: 6px;
241
+ border-radius: 3px;
242
+ min-width: 2px;
243
+ }
244
+ .bar-wrap span { color: #64748b; font-size: 12px; white-space: nowrap; }
245
+ .filter-bar {
246
+ display: flex;
247
+ gap: 8px;
248
+ padding: 16px 24px;
249
+ border-bottom: 1px solid #334155;
250
+ flex-wrap: wrap;
251
+ }
252
+ .filter-btn {
253
+ background: #334155;
254
+ color: #94a3b8;
255
+ border: none;
256
+ padding: 6px 14px;
257
+ border-radius: 8px;
258
+ font-size: 12px;
259
+ cursor: pointer;
260
+ transition: all 0.15s;
261
+ }
262
+ .filter-btn:hover, .filter-btn.active {
263
+ background: #10b981;
264
+ color: white;
265
+ }
266
+ input[type="search"] {
267
+ background: #334155;
268
+ border: 1px solid #475569;
269
+ color: #f1f5f9;
270
+ padding: 6px 14px;
271
+ border-radius: 8px;
272
+ font-size: 13px;
273
+ outline: none;
274
+ width: 240px;
275
+ }
276
+ input[type="search"]::placeholder { color: #64748b; }
277
+ input[type="search"]:focus { border-color: #10b981; }
278
+ .footer { color: #64748b; font-size: 12px; text-align: center; margin-top: 24px; }
279
+ </style>
280
+ </head>
281
+ <body>
282
+ <div class="header">
283
+ <div>
284
+ <div style="display:flex;align-items:center;gap:12px">
285
+ <h1>📊 ${title}</h1>
286
+ <span class="badge">⚡ BertUI</span>
287
+ </div>
288
+ <p class="meta">Generated ${now} · ${outDir}</p>
289
+ </div>
290
+ </div>
291
+
292
+ <div class="cards">
293
+ <div class="card">
294
+ <div class="label">Total Size</div>
295
+ <div class="value" style="color:#10b981">${formatBytes(totalSize)}</div>
296
+ <div class="sub">${files.length} files</div>
297
+ </div>
298
+ <div class="card">
299
+ <div class="label">JavaScript</div>
300
+ <div class="value" style="color:#3b82f6">${formatBytes(jsTotal)}</div>
301
+ <div class="sub">${jsFiles.length} files</div>
302
+ </div>
303
+ <div class="card">
304
+ <div class="label">CSS</div>
305
+ <div class="value" style="color:#8b5cf6">${formatBytes(cssTotal)}</div>
306
+ <div class="sub">${cssFiles.length} files</div>
307
+ </div>
308
+ <div class="card">
309
+ <div class="label">Images</div>
310
+ <div class="value" style="color:#f59e0b">${formatBytes(imageTotal)}</div>
311
+ <div class="sub">${imageFiles.length} files</div>
312
+ </div>
313
+ </div>
314
+
315
+ <div class="section">
316
+ <div class="section-header">
317
+ <h2>All Files</h2>
318
+ <span class="count">${files.length}</span>
319
+ </div>
320
+ <div class="filter-bar">
321
+ <button class="filter-btn active" onclick="filterFiles('all', this)">All</button>
322
+ <button class="filter-btn" onclick="filterFiles('javascript', this)">JS</button>
323
+ <button class="filter-btn" onclick="filterFiles('css', this)">CSS</button>
324
+ <button class="filter-btn" onclick="filterFiles('image', this)">Images</button>
325
+ <input type="search" id="search" placeholder="Search files..." oninput="searchFiles(this.value)">
326
+ </div>
327
+ <table id="files-table">
328
+ <thead>
329
+ <tr>
330
+ <th>File</th>
331
+ <th>Path</th>
332
+ <th>Type</th>
333
+ <th>Size</th>
334
+ <th>% of Total</th>
335
+ </tr>
336
+ </thead>
337
+ <tbody>${fileRows}</tbody>
338
+ </table>
339
+ </div>
340
+
341
+ <p class="footer">Built with BertUI · bundle-report.html</p>
342
+
343
+ <script>
344
+ let currentFilter = 'all';
345
+ const rows = Array.from(document.querySelectorAll('#files-table tbody tr'));
346
+
347
+ function filterFiles(type, btn) {
348
+ currentFilter = type;
349
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
350
+ btn.classList.add('active');
351
+ applyFilters();
352
+ }
353
+
354
+ function searchFiles(query) {
355
+ applyFilters(query);
356
+ }
357
+
358
+ function applyFilters(query = document.getElementById('search').value) {
359
+ rows.forEach(row => {
360
+ const typeCell = row.cells[2].textContent.trim();
361
+ const nameCell = row.cells[0].textContent.trim();
362
+ const matchesType = currentFilter === 'all' || typeCell === currentFilter;
363
+ const matchesSearch = !query || nameCell.toLowerCase().includes(query.toLowerCase());
364
+ row.style.display = matchesType && matchesSearch ? '' : 'none';
365
+ });
366
+ }
367
+ </script>
368
+ </body>
369
+ </html>`;
370
+ }
@@ -22,9 +22,11 @@ export async function discoverRoutes(pagesDir) {
22
22
  const fileName = entry.name.replace(ext, '');
23
23
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
24
24
 
25
+ const RESERVED = ['index', 'loading'];
25
26
  if (fileName === 'index') {
26
27
  route = route.replace('/index', '') || '/';
27
28
  }
29
+ if (RESERVED.includes(fileName)) continue;
28
30
 
29
31
  const isDynamic = fileName.includes('[') && fileName.includes(']');
30
32