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
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// bertui/src/scaffolder/index.js
|
|
2
|
+
// CLI component/page/layout scaffolder
|
|
3
|
+
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { existsSync, mkdirSync } from 'fs';
|
|
6
|
+
import logger from '../logger/logger.js';
|
|
7
|
+
// ─── TEMPLATES ─────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const TEMPLATES = {
|
|
10
|
+
component: (name) => `import React, { useState } from 'react';
|
|
11
|
+
|
|
12
|
+
interface ${name}Props {
|
|
13
|
+
className?: string;
|
|
14
|
+
children?: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default function ${name}({ className = '', children }: ${name}Props) {
|
|
18
|
+
return (
|
|
19
|
+
<div className={className}>
|
|
20
|
+
{children ?? <p>${name} component</p>}
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
`,
|
|
25
|
+
|
|
26
|
+
page: (name, route) => `// Route: ${route}
|
|
27
|
+
import React from 'react';
|
|
28
|
+
import { Link } from 'bertui/router';
|
|
29
|
+
|
|
30
|
+
export const title = '${name}';
|
|
31
|
+
export const description = '${name} page';
|
|
32
|
+
|
|
33
|
+
export default function ${name}Page() {
|
|
34
|
+
return (
|
|
35
|
+
<main style={{ padding: '2rem', fontFamily: 'system-ui' }}>
|
|
36
|
+
<h1>${name}</h1>
|
|
37
|
+
<p>Welcome to ${name}</p>
|
|
38
|
+
<Link to="/">← Back home</Link>
|
|
39
|
+
</main>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
|
|
44
|
+
layout: (name) => `import React from 'react';
|
|
45
|
+
|
|
46
|
+
interface ${name}LayoutProps {
|
|
47
|
+
children: React.ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// This layout wraps all pages${name === 'default' ? '' : ` under /${name.toLowerCase()}/`}
|
|
51
|
+
export default function ${name}Layout({ children }: ${name}LayoutProps) {
|
|
52
|
+
return (
|
|
53
|
+
<div style={{ minHeight: '100vh', fontFamily: 'system-ui' }}>
|
|
54
|
+
<header style={{
|
|
55
|
+
padding: '1rem 2rem',
|
|
56
|
+
borderBottom: '1px solid #e5e7eb',
|
|
57
|
+
display: 'flex',
|
|
58
|
+
alignItems: 'center',
|
|
59
|
+
justifyContent: 'space-between',
|
|
60
|
+
}}>
|
|
61
|
+
<a href="/" style={{ fontWeight: 700, textDecoration: 'none', color: 'inherit' }}>
|
|
62
|
+
My App
|
|
63
|
+
</a>
|
|
64
|
+
<nav style={{ display: 'flex', gap: '1.5rem' }}>
|
|
65
|
+
<a href="/" style={{ color: '#4b5563', textDecoration: 'none' }}>Home</a>
|
|
66
|
+
<a href="/about" style={{ color: '#4b5563', textDecoration: 'none' }}>About</a>
|
|
67
|
+
</nav>
|
|
68
|
+
</header>
|
|
69
|
+
<main style={{ padding: '2rem' }}>
|
|
70
|
+
{children}
|
|
71
|
+
</main>
|
|
72
|
+
<footer style={{
|
|
73
|
+
padding: '1rem 2rem',
|
|
74
|
+
borderTop: '1px solid #e5e7eb',
|
|
75
|
+
color: '#9ca3af',
|
|
76
|
+
fontSize: '14px',
|
|
77
|
+
textAlign: 'center',
|
|
78
|
+
}}>
|
|
79
|
+
Built with BertUI ⚡
|
|
80
|
+
</footer>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
`,
|
|
85
|
+
|
|
86
|
+
loading: (name) => `import React from 'react';
|
|
87
|
+
|
|
88
|
+
// Loading state for ${name} route
|
|
89
|
+
// This shows while the page JavaScript loads
|
|
90
|
+
export default function ${name}Loading() {
|
|
91
|
+
return (
|
|
92
|
+
<div style={{
|
|
93
|
+
display: 'flex',
|
|
94
|
+
flexDirection: 'column',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
justifyContent: 'center',
|
|
97
|
+
minHeight: '60vh',
|
|
98
|
+
gap: '1rem',
|
|
99
|
+
fontFamily: 'system-ui',
|
|
100
|
+
}}>
|
|
101
|
+
<div style={{
|
|
102
|
+
width: '40px',
|
|
103
|
+
height: '40px',
|
|
104
|
+
border: '3px solid #e5e7eb',
|
|
105
|
+
borderTopColor: '#10b981',
|
|
106
|
+
borderRadius: '50%',
|
|
107
|
+
animation: 'spin 0.7s linear infinite',
|
|
108
|
+
}} />
|
|
109
|
+
<style>{'@keyframes spin { to { transform: rotate(360deg); } }'}</style>
|
|
110
|
+
<p style={{ color: '#6b7280', fontSize: '14px' }}>Loading ${name}...</p>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
`,
|
|
115
|
+
|
|
116
|
+
middleware: () => `import type { MiddlewareContext } from 'bertui/middleware';
|
|
117
|
+
|
|
118
|
+
// Runs before EVERY page request
|
|
119
|
+
// Use ctx.redirect(), ctx.respond(), or ctx.locals to pass data
|
|
120
|
+
|
|
121
|
+
export async function onRequest(ctx: MiddlewareContext) {
|
|
122
|
+
// Example: protect /dashboard
|
|
123
|
+
// if (ctx.pathname.startsWith('/dashboard')) {
|
|
124
|
+
// const token = ctx.headers['authorization'];
|
|
125
|
+
// if (!token) return ctx.redirect('/login');
|
|
126
|
+
// }
|
|
127
|
+
|
|
128
|
+
// Example: add custom headers
|
|
129
|
+
// ctx.setHeader('X-Powered-By', 'BertUI');
|
|
130
|
+
|
|
131
|
+
// Example: pass data to pages via locals
|
|
132
|
+
// ctx.locals.user = await getUser(ctx.headers.cookie);
|
|
133
|
+
|
|
134
|
+
console.log('[Middleware]', ctx.method, ctx.pathname);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Optional: handle middleware errors
|
|
138
|
+
export async function onError(ctx: MiddlewareContext, error: Error) {
|
|
139
|
+
console.error('[Middleware Error]', error.message);
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ─── SCAFFOLDER ─────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
export async function scaffold(type, name, options = {}) {
|
|
147
|
+
const root = options.root || process.cwd();
|
|
148
|
+
const ts = options.ts !== false; // Default: TypeScript
|
|
149
|
+
|
|
150
|
+
const ext = ts ? '.tsx' : '.jsx';
|
|
151
|
+
|
|
152
|
+
switch (type) {
|
|
153
|
+
case 'component':
|
|
154
|
+
return createComponent(name, root, ext);
|
|
155
|
+
case 'page':
|
|
156
|
+
return createPage(name, root, ext);
|
|
157
|
+
case 'layout':
|
|
158
|
+
return createLayout(name, root, ext);
|
|
159
|
+
case 'loading':
|
|
160
|
+
return createLoading(name, root, ext);
|
|
161
|
+
case 'middleware':
|
|
162
|
+
return createMiddleware(root, ts);
|
|
163
|
+
default:
|
|
164
|
+
logger.error(`Unknown scaffold type: ${type}`);
|
|
165
|
+
logger.info('Available types: component, page, layout, loading, middleware');
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function createComponent(name, root, ext) {
|
|
171
|
+
const pascal = toPascalCase(name);
|
|
172
|
+
const dir = join(root, 'src', 'components');
|
|
173
|
+
|
|
174
|
+
fsMkdir(dir, { recursive: true });
|
|
175
|
+
|
|
176
|
+
const filePath = join(dir, `${pascal}${ext}`);
|
|
177
|
+
|
|
178
|
+
if (fsExists(filePath)) {
|
|
179
|
+
logger.warn(`Component already exists: src/components/${pascal}${ext}`);
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await Bun.write(filePath, TEMPLATES.component(pascal));
|
|
184
|
+
logger.success(`✅ Created component: src/components/${pascal}${ext}`);
|
|
185
|
+
return filePath;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function createPage(name, root, ext) {
|
|
189
|
+
const pascal = toPascalCase(name);
|
|
190
|
+
const route = `/${name.toLowerCase().replace(/\s+/g, '-')}`;
|
|
191
|
+
|
|
192
|
+
// Handle nested routes like "blog/[slug]"
|
|
193
|
+
const parts = name.split('/');
|
|
194
|
+
const pageName = toPascalCase(parts[parts.length - 1]);
|
|
195
|
+
const dir = join(root, 'src', 'pages', ...parts.slice(0, -1));
|
|
196
|
+
|
|
197
|
+
fsMkdir(dir, { recursive: true });
|
|
198
|
+
|
|
199
|
+
// Handle index pages
|
|
200
|
+
const fileName = pageName.toLowerCase() === 'index' ? 'index' : pageName.toLowerCase();
|
|
201
|
+
const filePath = join(dir, `${fileName}${ext}`);
|
|
202
|
+
|
|
203
|
+
if (fsExists(filePath)) {
|
|
204
|
+
logger.warn(`Page already exists: src/pages/${name}${ext}`);
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
await Bun.write(filePath, TEMPLATES.page(pascal, route));
|
|
209
|
+
logger.success(`✅ Created page: src/pages/${name}${ext}`);
|
|
210
|
+
logger.info(` Route: ${route}`);
|
|
211
|
+
return filePath;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function createLayout(name, root, ext) {
|
|
215
|
+
const pascal = toPascalCase(name);
|
|
216
|
+
const dir = join(root, 'src', 'layouts');
|
|
217
|
+
|
|
218
|
+
fsMkdir(dir, { recursive: true });
|
|
219
|
+
|
|
220
|
+
const filePath = join(dir, `${name.toLowerCase()}${ext}`);
|
|
221
|
+
|
|
222
|
+
if (fsExists(filePath)) {
|
|
223
|
+
logger.warn(`Layout already exists: src/layouts/${name.toLowerCase()}${ext}`);
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
await Bun.write(filePath, TEMPLATES.layout(pascal));
|
|
228
|
+
logger.success(`✅ Created layout: src/layouts/${name.toLowerCase()}${ext}`);
|
|
229
|
+
|
|
230
|
+
if (name.toLowerCase() === 'default') {
|
|
231
|
+
logger.info(' → This layout will wrap ALL pages automatically');
|
|
232
|
+
} else {
|
|
233
|
+
logger.info(` → This layout will wrap pages under /${name.toLowerCase()}/`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return filePath;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function createLoading(name, root, ext) {
|
|
240
|
+
const pascal = toPascalCase(name);
|
|
241
|
+
|
|
242
|
+
// Place loading.tsx next to the relevant page/route
|
|
243
|
+
const isRoot = name.toLowerCase() === 'root' || name === '/';
|
|
244
|
+
const dir = isRoot
|
|
245
|
+
? join(root, 'src', 'pages')
|
|
246
|
+
: join(root, 'src', 'pages', name.toLowerCase());
|
|
247
|
+
|
|
248
|
+
fsMkdir(dir, { recursive: true });
|
|
249
|
+
|
|
250
|
+
const filePath = join(dir, `loading${ext}`);
|
|
251
|
+
|
|
252
|
+
if (fsExists(filePath)) {
|
|
253
|
+
logger.warn(`Loading component already exists at ${dir}/loading${ext}`);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
await Bun.write(filePath, TEMPLATES.loading(pascal));
|
|
258
|
+
logger.success(`✅ Created loading state: ${filePath.replace(root, '')}`);
|
|
259
|
+
logger.info(` → Shows while ${isRoot ? 'root' : `/${name.toLowerCase()}`} route loads`);
|
|
260
|
+
return filePath;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function createMiddleware(root, ts) {
|
|
264
|
+
const ext = ts ? '.ts' : '.js';
|
|
265
|
+
const filePath = join(root, 'src', `middleware${ext}`);
|
|
266
|
+
|
|
267
|
+
if (fsExists(filePath)) {
|
|
268
|
+
logger.warn(`Middleware already exists: src/middleware${ext}`);
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await Bun.write(filePath, TEMPLATES.middleware());
|
|
273
|
+
logger.success(`✅ Created middleware: src/middleware${ext}`);
|
|
274
|
+
logger.info(' → Runs before every page request');
|
|
275
|
+
return filePath;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ─── HELPERS ────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
function toPascalCase(str) {
|
|
281
|
+
return str
|
|
282
|
+
.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase())
|
|
283
|
+
.replace(/^(.)/, c => c.toUpperCase())
|
|
284
|
+
.replace(/[[\]]/g, ''); // Remove bracket from dynamic routes
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse CLI args for the create command
|
|
289
|
+
* Usage: bertui create component Button
|
|
290
|
+
* bertui create page About
|
|
291
|
+
* bertui create layout default
|
|
292
|
+
* bertui create loading blog
|
|
293
|
+
* bertui create middleware
|
|
294
|
+
*/
|
|
295
|
+
export function parseCreateArgs(args) {
|
|
296
|
+
const [type, name] = args;
|
|
297
|
+
|
|
298
|
+
if (!type) {
|
|
299
|
+
logger.error('Usage: bertui create <type> [name]');
|
|
300
|
+
logger.info('Types: component, page, layout, loading, middleware');
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (type !== 'middleware' && !name) {
|
|
305
|
+
logger.error(`Usage: bertui create ${type} <name>`);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return { type, name: name || type };
|
|
310
|
+
}
|
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
|
+
}
|