bertui 0.1.5 → 0.1.6

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
@@ -32,5 +32,5 @@ export default {
32
32
  buildCSS,
33
33
  copyCSS,
34
34
  program,
35
- version: "0.1.5"
35
+ version: "0.1.6"
36
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -10,7 +10,8 @@
10
10
  "exports": {
11
11
  ".": "./index.js",
12
12
  "./styles": "./src/styles/bertui.css",
13
- "./logger": "./src/logger/logger.js"
13
+ "./logger": "./src/logger/logger.js",
14
+ "./router": "./src/router/Router.jsx"
14
15
  },
15
16
  "files": [
16
17
  "bin",
@@ -1,91 +1,226 @@
1
1
  // src/client/compiler.js
2
- import { join } from 'path';
3
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
3
+ import { join, extname, relative, sep } from 'path';
4
4
  import logger from '../logger/logger.js';
5
- import { generateRoutes, generateRouterCode, generateMainWithRouter, logRoutes } from '../router/router.js';
6
5
 
7
6
  export async function compileProject(root) {
8
- const compiledDir = join(root, '.bertui', 'compiled');
9
- const routerDir = join(root, '.bertui');
7
+ logger.bigLog('COMPILING PROJECT', { color: 'blue' });
10
8
 
11
- // Ensure compiled directory exists
12
- if (!existsSync(compiledDir)) {
13
- mkdirSync(compiledDir, { recursive: true });
9
+ const srcDir = join(root, 'src');
10
+ const pagesDir = join(srcDir, 'pages');
11
+ const outDir = join(root, '.bertui', 'compiled');
12
+
13
+ // Check if src exists
14
+ if (!existsSync(srcDir)) {
15
+ logger.error('src/ directory not found!');
16
+ process.exit(1);
14
17
  }
15
18
 
16
- const startTime = Date.now();
19
+ // Create output directory
20
+ if (!existsSync(outDir)) {
21
+ mkdirSync(outDir, { recursive: true });
22
+ logger.info('Created .bertui/compiled/');
23
+ }
17
24
 
18
- try {
19
- // Check if routing is enabled (pages directory exists)
20
- const pagesDir = join(root, 'src', 'pages');
21
- const useRouting = existsSync(pagesDir);
25
+ // Discover routes if pages directory exists
26
+ let routes = [];
27
+ if (existsSync(pagesDir)) {
28
+ routes = await discoverRoutes(pagesDir);
29
+ logger.info(`Discovered ${routes.length} routes`);
22
30
 
23
- let entryPoint;
24
- let routes = [];
25
-
26
- if (useRouting) {
27
- logger.info('📁 File-based routing enabled');
28
-
29
- // Generate routes
30
- routes = generateRoutes(root);
31
- logRoutes(routes);
31
+ // Display routes table
32
+ if (routes.length > 0) {
33
+ logger.bigLog('ROUTES DISCOVERED', { color: 'blue' });
34
+ logger.table(routes.map((r, i) => ({
35
+ '': i,
36
+ route: r.route,
37
+ file: r.file,
38
+ type: r.type
39
+ })));
32
40
 
33
- // Generate router code
34
- const routerCode = generateRouterCode(routes);
35
- const routerPath = join(routerDir, 'router.js');
36
- writeFileSync(routerPath, routerCode);
41
+ // Generate router file
42
+ await generateRouter(routes, outDir);
37
43
  logger.info('Generated router.js');
44
+ }
45
+ }
46
+
47
+ // Compile all files
48
+ const startTime = Date.now();
49
+ const stats = await compileDirectory(srcDir, outDir, root);
50
+ const duration = Date.now() - startTime;
51
+
52
+ logger.success(`Compiled ${stats.files} files in ${duration}ms`);
53
+ logger.info(`Output: ${outDir}`);
54
+
55
+ return { outDir, stats, routes };
56
+ }
57
+
58
+ async function discoverRoutes(pagesDir) {
59
+ const routes = [];
60
+
61
+ async function scanDirectory(dir, basePath = '') {
62
+ const entries = readdirSync(dir, { withFileTypes: true });
63
+
64
+ for (const entry of entries) {
65
+ const fullPath = join(dir, entry.name);
66
+ const relativePath = join(basePath, entry.name);
38
67
 
39
- // Generate main entry with router
40
- const mainCode = generateMainWithRouter(routes);
41
- const mainPath = join(routerDir, 'main-entry.js');
42
- writeFileSync(mainPath, mainCode);
43
-
44
- entryPoint = mainPath;
45
- } else {
46
- // Use regular main.jsx if no pages directory
47
- entryPoint = join(root, 'src/main.jsx');
48
-
49
- if (!existsSync(entryPoint)) {
50
- throw new Error('src/main.jsx not found. Create either src/main.jsx or src/pages/ directory.');
68
+ if (entry.isDirectory()) {
69
+ // Recursively scan subdirectories
70
+ await scanDirectory(fullPath, relativePath);
71
+ } else if (entry.isFile()) {
72
+ const ext = extname(entry.name);
73
+ if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
74
+ const fileName = entry.name.replace(ext, '');
75
+
76
+ // Generate route path
77
+ let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
78
+
79
+ // Handle index files
80
+ if (fileName === 'index') {
81
+ route = route.replace('/index', '') || '/';
82
+ }
83
+
84
+ // Determine route type
85
+ const isDynamic = fileName.includes('[') && fileName.includes(']');
86
+ const type = isDynamic ? 'dynamic' : 'static';
87
+
88
+ routes.push({
89
+ route: route === '' ? '/' : route,
90
+ file: relativePath,
91
+ path: fullPath,
92
+ type
93
+ });
94
+ }
51
95
  }
52
96
  }
53
-
54
- // Update the Bun.build section in buildProduction function
97
+ }
98
+
99
+ await scanDirectory(pagesDir);
100
+
101
+ // Sort routes: static routes first, then dynamic
102
+ routes.sort((a, b) => {
103
+ if (a.type === b.type) {
104
+ return a.route.localeCompare(b.route);
105
+ }
106
+ return a.type === 'static' ? -1 : 1;
107
+ });
108
+
109
+ return routes;
110
+ }
55
111
 
56
- // Build with Bun's bundler
57
- const result = await Bun.build({
58
- entrypoints: [join(root, 'src/main.jsx')],
59
- outdir: outDir,
60
- target: 'browser',
61
- minify: true,
62
- splitting: true,
63
- sourcemap: 'external',
64
- naming: {
65
- entry: '[name]-[hash].js',
66
- chunk: 'chunks/[name]-[hash].js',
67
- asset: 'assets/[name]-[hash].[ext]'
112
+ async function generateRouter(routes, outDir) {
113
+ const imports = routes.map((route, i) => {
114
+ const componentName = `Page${i}`;
115
+ const importPath = `./pages/${route.file.replace(/\\/g, '/')}`;
116
+ return `import ${componentName} from '${importPath}';`;
117
+ }).join('\n');
118
+
119
+ const routeConfigs = routes.map((route, i) => {
120
+ const componentName = `Page${i}`;
121
+ return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
122
+ }).join(',\n');
123
+
124
+ const routerCode = `// Auto-generated router - DO NOT EDIT
125
+ ${imports}
126
+
127
+ export const routes = [
128
+ ${routeConfigs}
129
+ ];
130
+
131
+ export function matchRoute(pathname) {
132
+ // Try exact match first
133
+ for (const route of routes) {
134
+ if (route.type === 'static' && route.path === pathname) {
135
+ return route;
136
+ }
68
137
  }
69
- });
138
+
139
+ // Try dynamic routes
140
+ for (const route of routes) {
141
+ if (route.type === 'dynamic') {
142
+ const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
143
+ const regex = new RegExp('^' + pattern + '$');
144
+ const match = pathname.match(regex);
145
+
146
+ if (match) {
147
+ // Extract params
148
+ const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
149
+ const params = {};
150
+ paramNames.forEach((name, i) => {
151
+ params[name] = match[i + 1];
152
+ });
153
+
154
+ return { ...route, params };
155
+ }
156
+ }
157
+ }
158
+
159
+ return null;
160
+ }
161
+ `;
162
+
163
+ const routerPath = join(outDir, 'router.js');
164
+ await Bun.write(routerPath, routerCode);
165
+ }
70
166
 
71
- if (!result.success) {
72
- logger.error('Build failed!');
73
-
74
- // Log detailed errors
75
- if (result.logs && result.logs.length > 0) {
76
- logger.error('Detailed errors:');
77
- result.logs.forEach((log, index) => {
78
- logger.error(` [${index + 1}] ${log.message}`);
79
- if (log.location) {
80
- logger.error(` at ${log.location.file}:${log.location.line}:${log.location.column}`);
167
+ async function compileDirectory(srcDir, outDir, root) {
168
+ const stats = { files: 0, skipped: 0 };
169
+
170
+ const files = readdirSync(srcDir);
171
+
172
+ for (const file of files) {
173
+ const srcPath = join(srcDir, file);
174
+ const stat = statSync(srcPath);
175
+
176
+ if (stat.isDirectory()) {
177
+ // Recursively compile subdirectories
178
+ const subOutDir = join(outDir, file);
179
+ mkdirSync(subOutDir, { recursive: true });
180
+ const subStats = await compileDirectory(srcPath, subOutDir, root);
181
+ stats.files += subStats.files;
182
+ stats.skipped += subStats.skipped;
183
+ } else {
184
+ // Compile file
185
+ const ext = extname(file);
186
+ const relativePath = relative(join(root, 'src'), srcPath);
187
+
188
+ if (['.jsx', '.tsx', '.ts'].includes(ext)) {
189
+ await compileFile(srcPath, outDir, file, relativePath);
190
+ stats.files++;
191
+ } else if (ext === '.js' || ext === '.css') {
192
+ // Copy as-is
193
+ const outPath = join(outDir, file);
194
+ await Bun.write(outPath, Bun.file(srcPath));
195
+ logger.debug(`Copied: ${relativePath}`);
196
+ stats.files++;
197
+ } else {
198
+ logger.debug(`Skipped: ${relativePath}`);
199
+ stats.skipped++;
81
200
  }
82
- });
201
+ }
83
202
  }
84
203
 
85
- process.exit(1);
204
+ return stats;
86
205
  }
206
+
207
+ async function compileFile(srcPath, outDir, filename, relativePath) {
208
+ const ext = extname(filename);
209
+ const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
210
+
211
+ try {
212
+ const transpiler = new Bun.Transpiler({ loader });
213
+ const code = await Bun.file(srcPath).text();
214
+ const compiled = await transpiler.transform(code);
215
+
216
+ // Change extension to .js
217
+ const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
218
+ const outPath = join(outDir, outFilename);
219
+
220
+ await Bun.write(outPath, compiled);
221
+ logger.debug(`Compiled: ${relativePath} → ${outFilename}`);
87
222
  } catch (error) {
88
- logger.error(`Compilation error: ${error.message}`);
223
+ logger.error(`Failed to compile ${relativePath}: ${error.message}`);
89
224
  throw error;
90
225
  }
91
226
  }
@@ -1,216 +1,129 @@
1
- // src/router/router.js
2
- import { join, relative, parse } from 'path';
3
- import { readdirSync, statSync, existsSync } from 'fs';
4
- import logger from 'ernest-logger'; // Assuming Ernest Logger is used
5
-
6
- /**
7
- * Scans the pages directory and generates route definitions
8
- */
9
- export function generateRoutes(root) {
10
- const pagesDir = join(root, 'src', 'pages');
11
-
12
- if (!existsSync(pagesDir)) {
13
- return [];
14
- }
15
-
16
- const routes = [];
17
-
18
- function scanDirectory(dir, basePath = '') {
19
- const entries = readdirSync(dir);
20
-
21
- for (const entry of entries) {
22
- const fullPath = join(dir, entry);
23
- const stat = statSync(fullPath);
24
-
25
- if (stat.isDirectory()) {
26
- // Recursively scan subdirectories
27
- scanDirectory(fullPath, join(basePath, entry));
28
- } else if (stat.isFile() && /\.(jsx?|tsx?)$/.test(entry)) {
29
- const parsed = parse(entry);
30
- let fileName = parsed.name;
31
-
32
- // Skip non-page files (e.g., those starting with a dot)
33
- if (fileName.startsWith('.') || fileName.startsWith('~')) {
34
- continue;
35
- }
36
-
37
- // --- ROUTE PATH GENERATION ---
38
-
39
- // Handle dynamic routes: _filename -> :filename
40
- let isDynamic = false;
41
- if (fileName.startsWith('_')) {
42
- fileName = fileName.slice(1); // Remove the underscore
43
- isDynamic = true;
44
- }
1
+ // src/router/Router.jsx
2
+ import { useState, useEffect, createContext, useContext } from 'react';
45
3
 
46
- // Generate route path
47
- let routePath = join(basePath, fileName === 'index' ? '' : fileName);
48
- routePath = '/' + routePath.replace(/\\/g, '/'); // Use forward slashes
49
-
50
- // Apply dynamic parameter if detected
51
- if (isDynamic) {
52
- // Replace the last part of the route path with the dynamic parameter (e.g., /blog/slug -> /blog/:slug)
53
- routePath = routePath.replace(new RegExp(`/${fileName}$`), `/:${fileName}`);
54
- }
55
-
56
- // Handle the root path, ensuring it's just '/'
57
- if (routePath === '//') {
58
- routePath = '/';
59
- }
4
+ // Router context
5
+ const RouterContext = createContext(null);
60
6
 
61
- // Get relative path from pages dir (used for imports in generated code)
62
- const relativePath = relative(pagesDir, fullPath);
63
-
64
- routes.push({
65
- path: routePath,
66
- file: relativePath,
67
- component: fullPath,
68
- isDynamic: isDynamic || routePath.includes(':')
69
- });
70
- }
71
- }
7
+ export function useRouter() {
8
+ const context = useContext(RouterContext);
9
+ if (!context) {
10
+ throw new Error('useRouter must be used within a Router component');
72
11
  }
73
-
74
- scanDirectory(pagesDir);
75
-
76
- // Sort routes: Root path first, then static, then dynamic
77
- routes.sort((a, b) => {
78
- if (a.path === '/') return -1;
79
- if (b.path === '/') return 1;
80
- if (a.isDynamic && !b.isDynamic) return 1;
81
- if (!a.isDynamic && b.isDynamic) return -1;
82
- return a.path.localeCompare(b.path);
83
- });
84
-
85
- return routes;
12
+ return context;
86
13
  }
87
14
 
88
- /**
89
- * Generates the router client code
90
- */
91
- export function generateRouterCode(routes) {
92
- const imports = routes.map((route, index) =>
93
- `import Page${index} from '../src/pages/${route.file.replace(/\\/g, '/')}';`
94
- ).join('\n');
95
-
96
- const routeConfigs = routes.map((route, index) => ({
97
- path: route.path,
98
- component: `Page${index}`
99
- }));
100
-
101
- return `
102
- // Auto-generated router code - DO NOT EDIT MANUALLY
103
- import React, { useState, useEffect } from 'react';
104
- ${imports}
105
-
106
- const routes = ${JSON.stringify(routeConfigs, null, 2).replace(/"Page(\d+)"/g, 'Page$1')};
107
-
108
- export function Router() {
109
- const [currentPath, setCurrentPath] = useState(window.location.pathname);
110
-
15
+ export function useParams() {
16
+ const { params } = useRouter();
17
+ return params;
18
+ }
19
+
20
+ export function Router({ routes, children }) {
21
+ const [currentRoute, setCurrentRoute] = useState(null);
22
+ const [params, setParams] = useState({});
23
+
111
24
  useEffect(() => {
25
+ // Match initial route
26
+ matchAndSetRoute(window.location.pathname);
27
+
28
+ // Handle browser navigation
112
29
  const handlePopState = () => {
113
- setCurrentPath(window.location.pathname);
30
+ matchAndSetRoute(window.location.pathname);
114
31
  };
115
-
32
+
116
33
  window.addEventListener('popstate', handlePopState);
117
34
  return () => window.removeEventListener('popstate', handlePopState);
118
35
  }, []);
119
-
120
- // Match current path to route
121
- const matchedRoute = routes.find(route => {
122
- if (route.path === currentPath) return true;
123
-
124
- // Handle dynamic routes (simple static matching for now)
125
- const routeParts = route.path.split('/');
126
- const pathParts = currentPath.split('/');
127
-
128
- if (routeParts.length !== pathParts.length) return false;
129
-
130
- return routeParts.every((part, i) => {
131
- if (part.startsWith(':')) return true; // Match any value for dynamic part
132
- return part === pathParts[i];
133
- });
134
- });
135
-
136
- if (!matchedRoute) {
137
- return <div style={{ padding: '2rem', textAlign: 'center' }}>
138
- <h1>404 - Page Not Found</h1>
139
- <p>The page "{currentPath}" does not exist.</p>
140
- <a href="/" onClick={(e) => { e.preventDefault(); navigate('/'); }}>Go Home</a>
141
- </div>;
142
- }
143
-
144
- // Extract params from dynamic routes
145
- const params = {};
146
- if (matchedRoute.path.includes(':')) {
147
- const routeParts = matchedRoute.path.split('/');
148
- const pathParts = currentPath.split('/');
149
-
150
- routeParts.forEach((part, i) => {
151
- if (part.startsWith(':')) {
152
- const paramName = part.slice(1);
153
- params[paramName] = pathParts[i];
36
+
37
+ function matchAndSetRoute(pathname) {
38
+ // Try exact match first (static routes)
39
+ for (const route of routes) {
40
+ if (route.type === 'static' && route.path === pathname) {
41
+ setCurrentRoute(route);
42
+ setParams({});
43
+ return;
44
+ }
45
+ }
46
+
47
+ // Try dynamic routes
48
+ for (const route of routes) {
49
+ if (route.type === 'dynamic') {
50
+ const pattern = route.path.replace(/\[([^\]]+)\]/g, '([^/]+)');
51
+ const regex = new RegExp('^' + pattern + '$');
52
+ const match = pathname.match(regex);
53
+
54
+ if (match) {
55
+ // Extract params
56
+ const paramNames = [...route.path.matchAll(/\[([^\]]+)\]/g)].map(m => m[1]);
57
+ const extractedParams = {};
58
+ paramNames.forEach((name, i) => {
59
+ extractedParams[name] = match[i + 1];
60
+ });
61
+
62
+ setCurrentRoute(route);
63
+ setParams(extractedParams);
64
+ return;
65
+ }
154
66
  }
155
- });
67
+ }
68
+
69
+ // No match found - 404
70
+ setCurrentRoute(null);
71
+ setParams({});
72
+ }
73
+
74
+ function navigate(path) {
75
+ window.history.pushState({}, '', path);
76
+ matchAndSetRoute(path);
156
77
  }
157
-
158
- const Component = matchedRoute.component;
159
- return <Component params={params} />;
160
- }
161
78
 
162
- // Client-side navigation
163
- export function navigate(path) {
164
- window.history.pushState({}, '', path);
165
- window.dispatchEvent(new PopStateEvent('popstate'));
79
+ const routerValue = {
80
+ currentRoute,
81
+ params,
82
+ navigate,
83
+ pathname: window.location.pathname
84
+ };
85
+
86
+ return (
87
+ <RouterContext.Provider value={routerValue}>
88
+ {currentRoute ? (
89
+ <currentRoute.component />
90
+ ) : (
91
+ children || <NotFound />
92
+ )}
93
+ </RouterContext.Provider>
94
+ );
166
95
  }
167
96
 
168
- // Link component for navigation
169
- export function Link({ href, children, ...props }) {
170
- const handleClick = (e) => {
97
+ export function Link({ to, children, className, ...props }) {
98
+ const { navigate } = useRouter();
99
+
100
+ function handleClick(e) {
171
101
  e.preventDefault();
172
- navigate(href);
173
- };
174
-
102
+ navigate(to);
103
+ }
104
+
175
105
  return (
176
- <a href={href} onClick={handleClick} {...props}>
106
+ <a href={to} onClick={handleClick} className={className} {...props}>
177
107
  {children}
178
108
  </a>
179
109
  );
180
110
  }
181
- `;
182
- }
183
111
 
184
- /**
185
- * Generates the main entry point with router
186
- */
187
- export function generateMainWithRouter(routes) {
188
- return `
189
- import 'bertui/styles';
190
- import React from 'react';
191
- import ReactDOM from 'react-dom/client';
192
- import { Router } from './.bertui/router.js';
193
-
194
- ReactDOM.createRoot(document.getElementById('root')).render(
195
- <Router />
196
- );
197
- `;
198
- }
199
-
200
- export function logRoutes(routes) {
201
- if (routes.length === 0) {
202
- logger.warn('No routes found in src/pages/');
203
- logger.info('Create files in src/pages/ to define routes:');
204
- logger.info('  src/pages/index.jsx     → /');
205
- logger.info('  src/pages/about.jsx     → /about');
206
- logger.info('  src/pages/user/_id.jsx  → /user/:id'); // Updated tip
207
- return;
208
- }
209
-
210
- logger.bigLog('ROUTES DISCOVERED', { color: 'cyan' });
211
- logger.table(routes.map(r => ({
212
- route: r.path,
213
- file: r.file,
214
- type: r.isDynamic ? 'dynamic' : 'static'
215
- })));
112
+ function NotFound() {
113
+ return (
114
+ <div style={{
115
+ display: 'flex',
116
+ flexDirection: 'column',
117
+ alignItems: 'center',
118
+ justifyContent: 'center',
119
+ minHeight: '100vh',
120
+ fontFamily: 'system-ui, sans-serif'
121
+ }}>
122
+ <h1 style={{ fontSize: '6rem', margin: 0 }}>404</h1>
123
+ <p style={{ fontSize: '1.5rem', color: '#666' }}>Page not found</p>
124
+ <a href="/" style={{ color: '#10b981', textDecoration: 'none', fontSize: '1.2rem' }}>
125
+ Go home
126
+ </a>
127
+ </div>
128
+ );
216
129
  }
@@ -12,28 +12,49 @@ export async function startDevServer(options = {}) {
12
12
  const compiledDir = join(root, '.bertui', 'compiled');
13
13
 
14
14
  const clients = new Set();
15
+ let hasRouter = false;
16
+
17
+ // Check if router exists
18
+ const routerPath = join(compiledDir, 'router.js');
19
+ if (existsSync(routerPath)) {
20
+ hasRouter = true;
21
+ logger.info('Router-based routing enabled');
22
+ }
15
23
 
16
24
  const app = new Elysia()
25
+ // Main HTML route - serves all pages
17
26
  .get('/', async () => {
18
- const html = `
19
- <!DOCTYPE html>
20
- <html lang="en">
21
- <head>
22
- <meta charset="UTF-8">
23
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
24
- <title>BertUI App - Dev</title>
25
- <link rel="stylesheet" href="/styles/bertui.css">
26
- </head>
27
- <body>
28
- <div id="root"></div>
29
- <script type="module" src="/hmr-client.js"></script>
30
- <script type="module" src="/compiled/main.js"></script>
31
- </body>
32
- </html>`;
27
+ return serveHTML(root, hasRouter);
28
+ })
29
+
30
+ // Catch-all route for SPA routing
31
+ .get('/*', async ({ params, set }) => {
32
+ const path = params['*'];
33
33
 
34
- return new Response(html, {
35
- headers: { 'Content-Type': 'text/html' }
36
- });
34
+ // Check if it's a file request
35
+ if (path.includes('.')) {
36
+ // Try to serve as static file
37
+ const filePath = join(compiledDir, path);
38
+ const file = Bun.file(filePath);
39
+
40
+ if (await file.exists()) {
41
+ const ext = extname(path);
42
+ const contentType = getContentType(ext);
43
+
44
+ return new Response(await file.text(), {
45
+ headers: {
46
+ 'Content-Type': contentType,
47
+ 'Cache-Control': 'no-store'
48
+ }
49
+ });
50
+ }
51
+
52
+ set.status = 404;
53
+ return 'File not found';
54
+ }
55
+
56
+ // For non-file routes, serve the main HTML (SPA mode)
57
+ return serveHTML(root, hasRouter);
37
58
  })
38
59
 
39
60
  .get('/hmr-client.js', () => {
@@ -111,9 +132,7 @@ ws.onclose = () => {
111
132
  }
112
133
 
113
134
  const ext = extname(filepath);
114
- const contentType = ext === '.js' ? 'application/javascript' :
115
- ext === '.css' ? 'text/css' :
116
- 'text/plain';
135
+ const contentType = getContentType(ext);
117
136
 
118
137
  return new Response(await file.text(), {
119
138
  headers: {
@@ -148,12 +167,62 @@ ws.onclose = () => {
148
167
  logger.info(`📁 Serving: ${root}`);
149
168
 
150
169
  // Watch for file changes
151
- setupWatcher(root, compiledDir, clients);
170
+ setupWatcher(root, compiledDir, clients, () => {
171
+ // Check router status on recompile
172
+ hasRouter = existsSync(join(compiledDir, 'router.js'));
173
+ });
152
174
 
153
175
  return app;
154
176
  }
155
177
 
156
- function setupWatcher(root, compiledDir, clients) {
178
+ function serveHTML(root, hasRouter) {
179
+ const html = `
180
+ <!DOCTYPE html>
181
+ <html lang="en">
182
+ <head>
183
+ <meta charset="UTF-8">
184
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
185
+ <title>BertUI App - Dev</title>
186
+ <link rel="stylesheet" href="/styles/bertui.css">
187
+ </head>
188
+ <body>
189
+ <div id="root"></div>
190
+ <script type="module" src="/hmr-client.js"></script>
191
+ ${hasRouter
192
+ ? '<script type="module" src="/compiled/router.js"></script>'
193
+ : ''
194
+ }
195
+ <script type="module" src="/compiled/main.js"></script>
196
+ </body>
197
+ </html>`;
198
+
199
+ return new Response(html, {
200
+ headers: { 'Content-Type': 'text/html' }
201
+ });
202
+ }
203
+
204
+ function getContentType(ext) {
205
+ const types = {
206
+ '.js': 'application/javascript',
207
+ '.css': 'text/css',
208
+ '.html': 'text/html',
209
+ '.json': 'application/json',
210
+ '.png': 'image/png',
211
+ '.jpg': 'image/jpeg',
212
+ '.jpeg': 'image/jpeg',
213
+ '.gif': 'image/gif',
214
+ '.svg': 'image/svg+xml',
215
+ '.ico': 'image/x-icon',
216
+ '.woff': 'font/woff',
217
+ '.woff2': 'font/woff2',
218
+ '.ttf': 'font/ttf',
219
+ '.eot': 'application/vnd.ms-fontobject'
220
+ };
221
+
222
+ return types[ext] || 'text/plain';
223
+ }
224
+
225
+ function setupWatcher(root, compiledDir, clients, onRecompile) {
157
226
  const srcDir = join(root, 'src');
158
227
 
159
228
  if (!existsSync(srcDir)) {
@@ -183,6 +252,11 @@ function setupWatcher(root, compiledDir, clients) {
183
252
  try {
184
253
  await compileProject(root);
185
254
 
255
+ // Call callback to update router status
256
+ if (onRecompile) {
257
+ onRecompile();
258
+ }
259
+
186
260
  // Notify clients to reload
187
261
  for (const client of clients) {
188
262
  try {