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 +65 -45
- package/package.json +5 -2
- package/src/analyzer/index.js +370 -0
- package/src/build/compiler/route-discoverer.js +2 -0
- package/src/build/processors/css-builder.js +116 -80
- package/src/build.js +104 -93
- package/src/cli.js +83 -18
- package/src/client/compiler.js +168 -67
- package/src/css/processor.js +46 -1
- package/src/dev.js +47 -9
- package/src/hydration/index.js +151 -0
- package/src/layouts/index.js +165 -0
- package/src/loading/index.js +210 -0
- package/src/middleware/index.js +182 -0
- package/src/scaffolder/index.js +310 -0
- package/src/serve.js +195 -0
- package/src/server/dev-handler.js +78 -148
- package/src/server/dev-server-utils.js +16 -5
- package/src/server-islands/index.js +1 -1
- package/src/utils/cache.js +297 -0
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.
|
|
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
|
|