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.
@@ -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
+ }