bertui 0.1.4 → 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
@@ -1,25 +1,23 @@
1
- // index.js - Main BertUI package exports
1
+ // index.js
2
2
  import logger from "./src/logger/logger.js";
3
3
  import { defaultConfig } from "./src/config/defaultConfig.js";
4
+ import { loadConfig } from "./src/config/loadConfig.js";
4
5
  import { startDev } from "./src/dev.js";
5
6
  import { buildProduction } from "./src/build.js";
6
7
  import { compileProject } from "./src/client/compiler.js";
8
+ import { buildCSS, copyCSS } from "./src/build/css-builder.js";
7
9
  import { program } from "./src/cli.js";
8
10
 
9
- // Router exports - these will be available after compilation
10
- // Users import these from 'bertui/router'
11
- // export { Link, navigate, Router } from './src/router/router.js';
12
- // reason commnedt out is In JavaScript/TypeScript modules, if you define functions/components inside a file, you must explicitly use the export keyword for them to be available to other files that import them.
13
-
14
- // Your router.js file contains a function, generateRouterCode(routes), which internally defines the Link, Maps, and Router components/functions as strings within a template
15
-
16
- // Named exports for CLI and build tools
11
+ // Named exports
17
12
  export {
18
13
  logger,
19
14
  defaultConfig,
15
+ loadConfig,
20
16
  startDev,
21
17
  buildProduction,
22
18
  compileProject,
19
+ buildCSS,
20
+ copyCSS,
23
21
  program
24
22
  };
25
23
 
@@ -27,9 +25,12 @@ export {
27
25
  export default {
28
26
  logger,
29
27
  defaultConfig,
28
+ loadConfig,
30
29
  startDev,
31
30
  buildProduction,
32
31
  compileProject,
32
+ buildCSS,
33
+ copyCSS,
33
34
  program,
34
- version: "0.1.4"
35
+ version: "0.1.6"
35
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "0.1.4",
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",
@@ -8,22 +8,17 @@
8
8
  "bertui": "./bin/bertui.js"
9
9
  },
10
10
  "exports": {
11
- ".": {
12
- "import": "./index.js",
13
- "require": "./index.js"
14
- },
11
+ ".": "./index.js",
15
12
  "./styles": "./src/styles/bertui.css",
16
13
  "./logger": "./src/logger/logger.js",
17
- "./router": {
18
- "import": "./src/router/client-exports.js",
19
- "require": "./src/router/client-exports.js"
20
- }
14
+ "./router": "./src/router/Router.jsx"
21
15
  },
22
16
  "files": [
23
17
  "bin",
24
18
  "src",
25
19
  "index.js",
26
- "README.md"
20
+ "README.md",
21
+ "LICENSE"
27
22
  ],
28
23
  "scripts": {
29
24
  "dev": "bun bin/bertui.js dev",
@@ -38,10 +33,10 @@
38
33
  "elysia",
39
34
  "build-tool",
40
35
  "bundler",
41
- "routing",
42
- "file-based-routing"
36
+ "fast",
37
+ "hmr"
43
38
  ],
44
- "author": "Your Name",
39
+ "author": "Pease Ernest",
45
40
  "license": "MIT",
46
41
  "repository": {
47
42
  "type": "git",
@@ -49,11 +44,17 @@
49
44
  },
50
45
  "dependencies": {
51
46
  "elysia": "^1.0.0",
52
- "ernest-logger": "latest"
47
+ "ernest-logger": "latest",
48
+ "postcss": "^8.4.32",
49
+ "autoprefixer": "^10.4.16",
50
+ "cssnano": "^6.0.2"
53
51
  },
54
52
  "peerDependencies": {
55
53
  "react": "^18.0.0 || ^19.0.0",
56
54
  "react-dom": "^18.0.0 || ^19.0.0",
57
55
  "bun": ">=1.0.0"
56
+ },
57
+ "engines": {
58
+ "bun": ">=1.0.0"
58
59
  }
59
60
  }
@@ -0,0 +1,84 @@
1
+ // src/build/css-builder.js
2
+ import { join } from 'path';
3
+ import { existsSync, mkdirSync } from 'fs';
4
+ import postcss from 'postcss';
5
+ import autoprefixer from 'autoprefixer';
6
+ import cssnano from 'cssnano';
7
+ import logger from '../logger/logger.js';
8
+
9
+ /**
10
+ * Build and minify CSS for production
11
+ * @param {string} srcPath - Source CSS file path
12
+ * @param {string} destPath - Destination CSS file path
13
+ */
14
+ export async function buildCSS(srcPath, destPath) {
15
+ try {
16
+ logger.info('Processing CSS...');
17
+
18
+ // Ensure destination directory exists
19
+ const destDir = join(destPath, '..');
20
+ if (!existsSync(destDir)) {
21
+ mkdirSync(destDir, { recursive: true });
22
+ }
23
+
24
+ // Read source CSS
25
+ const css = await Bun.file(srcPath).text();
26
+
27
+ // Process with PostCSS
28
+ const result = await postcss([
29
+ autoprefixer(),
30
+ cssnano({
31
+ preset: ['default', {
32
+ discardComments: { removeAll: true },
33
+ normalizeWhitespace: true,
34
+ colormin: true,
35
+ minifyFontValues: true,
36
+ minifySelectors: true,
37
+ }]
38
+ })
39
+ ]).process(css, { from: srcPath, to: destPath });
40
+
41
+ // Write minified CSS
42
+ await Bun.write(destPath, result.css);
43
+
44
+ // Calculate size reduction
45
+ const originalSize = (Buffer.byteLength(css) / 1024).toFixed(2);
46
+ const minifiedSize = (Buffer.byteLength(result.css) / 1024).toFixed(2);
47
+ const reduction = ((1 - Buffer.byteLength(result.css) / Buffer.byteLength(css)) * 100).toFixed(1);
48
+
49
+ logger.success(`CSS minified: ${originalSize}KB → ${minifiedSize}KB (-${reduction}%)`);
50
+
51
+ if (result.warnings().length > 0) {
52
+ result.warnings().forEach(warn => {
53
+ logger.warn(warn.toString());
54
+ });
55
+ }
56
+
57
+ return { success: true, size: minifiedSize };
58
+ } catch (error) {
59
+ logger.error(`CSS build failed: ${error.message}`);
60
+ throw error;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Copy CSS without minification (for dev)
66
+ * @param {string} srcPath - Source CSS file path
67
+ * @param {string} destPath - Destination CSS file path
68
+ */
69
+ export async function copyCSS(srcPath, destPath) {
70
+ try {
71
+ const destDir = join(destPath, '..');
72
+ if (!existsSync(destDir)) {
73
+ mkdirSync(destDir, { recursive: true });
74
+ }
75
+
76
+ await Bun.write(destPath, Bun.file(srcPath));
77
+ logger.info('CSS copied for development');
78
+
79
+ return { success: true };
80
+ } catch (error) {
81
+ logger.error(`CSS copy failed: ${error.message}`);
82
+ throw error;
83
+ }
84
+ }
package/src/build.js CHANGED
@@ -1,7 +1,8 @@
1
1
  // src/build.js
2
2
  import { join } from 'path';
3
- import { existsSync, mkdirSync, rmSync } from 'fs';
3
+ import { existsSync, mkdirSync, rmSync, cpSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
+ import { buildCSS } from './build/css-builder.js';
5
6
 
6
7
  export async function buildProduction(options = {}) {
7
8
  const root = options.root || process.cwd();
@@ -19,10 +20,34 @@ export async function buildProduction(options = {}) {
19
20
  const startTime = Date.now();
20
21
 
21
22
  try {
22
- // Build with Bun's bundler
23
+ // Step 1: Build CSS from BertUI library
24
+ logger.info('Step 1: Building CSS...');
25
+ const bertuiCssSource = join(import.meta.dir, 'styles/bertui.css');
26
+ const bertuiCssDest = join(outDir, 'styles/bertui.min.css');
27
+ await buildCSS(bertuiCssSource, bertuiCssDest);
28
+
29
+ // Step 2: Copy public assets if they exist
30
+ const publicDir = join(root, 'public');
31
+ if (existsSync(publicDir)) {
32
+ logger.info('Step 2: Copying public assets...');
33
+ cpSync(publicDir, outDir, { recursive: true });
34
+ logger.success('Public assets copied');
35
+ } else {
36
+ logger.info('Step 2: No public directory found, skipping...');
37
+ }
38
+
39
+ // Step 3: Build JavaScript with Bun's bundler
40
+ logger.info('Step 3: Bundling JavaScript...');
41
+ const mainEntry = join(root, 'src/main.jsx');
42
+
43
+ if (!existsSync(mainEntry)) {
44
+ logger.error('Entry point not found: src/main.jsx');
45
+ process.exit(1);
46
+ }
47
+
23
48
  const result = await Bun.build({
24
- entrypoints: [join(root, 'src/main.jsx')],
25
- outdir: outDir,
49
+ entrypoints: [mainEntry],
50
+ outdir: join(outDir, 'assets'),
26
51
  target: 'browser',
27
52
  minify: true,
28
53
  splitting: true,
@@ -30,35 +55,38 @@ export async function buildProduction(options = {}) {
30
55
  naming: {
31
56
  entry: '[name]-[hash].js',
32
57
  chunk: 'chunks/[name]-[hash].js',
33
- asset: 'assets/[name]-[hash].[ext]'
34
- }
58
+ asset: '[name]-[hash].[ext]'
59
+ },
60
+ external: [] // Don't externalize anything for browser builds
35
61
  });
36
62
 
37
63
  if (!result.success) {
38
- logger.error('Build failed!');
64
+ logger.error('JavaScript build failed!');
39
65
  result.logs.forEach(log => logger.error(log.message));
40
66
  process.exit(1);
41
67
  }
42
68
 
43
- // Copy BertUI CSS to dist
44
- const bertuiCss = join(import.meta.dir, 'styles/bertui.css');
45
- const destCss = join(outDir, 'bertui.css');
46
- await Bun.write(destCss, Bun.file(bertuiCss));
47
- logger.info('Copied BertUI CSS');
69
+ logger.success('JavaScript bundled');
48
70
 
49
- // Generate index.html
71
+ // Step 4: Generate index.html
72
+ logger.info('Step 4: Generating index.html...');
50
73
  await generateProductionHTML(root, outDir, result);
51
74
 
52
75
  const duration = Date.now() - startTime;
53
- logger.success(`Build complete in ${duration}ms`);
54
- logger.info(`Output: ${outDir}`);
76
+ logger.success(`✨ Build complete in ${duration}ms`);
77
+ logger.info(`📦 Output: ${outDir}`);
78
+
79
+ // Display build stats
55
80
  logger.table(result.outputs.map(o => ({
56
- file: o.path,
81
+ file: o.path.replace(outDir, ''),
57
82
  size: `${(o.size / 1024).toFixed(2)} KB`
58
83
  })));
59
84
 
60
85
  } catch (error) {
61
86
  logger.error(`Build failed: ${error.message}`);
87
+ if (error.stack) {
88
+ logger.error(error.stack);
89
+ }
62
90
  process.exit(1);
63
91
  }
64
92
  }
@@ -70,25 +98,26 @@ async function generateProductionHTML(root, outDir, buildResult) {
70
98
  );
71
99
 
72
100
  if (!mainBundle) {
73
- throw new Error('Could not find main bundle');
101
+ throw new Error('Could not find main bundle in build output');
74
102
  }
75
103
 
76
- const bundleName = mainBundle.path.split('/').pop();
104
+ const bundlePath = mainBundle.path.replace(outDir, '').replace(/^\//, '');
77
105
 
78
106
  const html = `<!DOCTYPE html>
79
107
  <html lang="en">
80
108
  <head>
81
109
  <meta charset="UTF-8">
82
110
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
111
+ <meta name="description" content="Built with BertUI - Lightning fast React development">
83
112
  <title>BertUI App</title>
84
- <link rel="stylesheet" href="/bertui.css">
113
+ <link rel="stylesheet" href="/styles/bertui.min.css">
85
114
  </head>
86
115
  <body>
87
116
  <div id="root"></div>
88
- <script type="module" src="/${bundleName}"></script>
117
+ <script type="module" src="/${bundlePath}"></script>
89
118
  </body>
90
119
  </html>`;
91
120
 
92
121
  await Bun.write(join(outDir, 'index.html'), html);
93
- logger.info('Generated index.html');
122
+ logger.success('Generated index.html');
94
123
  }
@@ -1,92 +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
- // Transpile with Bun
55
- const result = await Bun.build({
56
- entrypoints: [entryPoint],
57
- outdir: compiledDir,
58
- target: 'browser',
59
- format: 'esm',
60
- splitting: true,
61
- naming: {
62
- entry: '[name].js',
63
- chunk: 'chunks/[name]-[hash].js'
64
- },
65
- external: ['react', 'react-dom'],
66
- define: {
67
- 'process.env.NODE_ENV': '"development"'
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
+ }
111
+
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
+ }
137
+ }
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 };
68
155
  }
69
- });
70
-
71
- if (!result.success) {
72
- logger.error('Compilation failed!');
73
- result.logs.forEach(log => logger.error(log.message));
74
- throw new Error('Build failed');
75
156
  }
157
+ }
158
+
159
+ return null;
160
+ }
161
+ `;
162
+
163
+ const routerPath = join(outDir, 'router.js');
164
+ await Bun.write(routerPath, routerCode);
165
+ }
166
+
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);
76
175
 
77
- // Copy BertUI CSS
78
- const bertuiCss = join(import.meta.dir, '../styles/bertui.css');
79
- if (existsSync(bertuiCss)) {
80
- await Bun.write(join(compiledDir, 'bertui.css'), Bun.file(bertuiCss));
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++;
200
+ }
81
201
  }
202
+ }
203
+
204
+ return stats;
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);
82
215
 
83
- const duration = Date.now() - startTime;
84
- logger.success(`Compiled in ${duration}ms`);
85
-
86
- return { success: true, routes };
216
+ // Change extension to .js
217
+ const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
218
+ const outPath = join(outDir, outFilename);
87
219
 
220
+ await Bun.write(outPath, compiled);
221
+ logger.debug(`Compiled: ${relativePath} → ${outFilename}`);
88
222
  } catch (error) {
89
- logger.error(`Compilation error: ${error.message}`);
223
+ logger.error(`Failed to compile ${relativePath}: ${error.message}`);
90
224
  throw error;
91
225
  }
92
226
  }
@@ -1,7 +1,8 @@
1
+ // src/config/loadConfig.js
1
2
  import { join } from 'path';
2
3
  import { existsSync } from 'fs';
3
4
  import { defaultConfig } from './defaultConfig.js';
4
- import logger from '../utils/logger.js';
5
+ import logger from '../logger/logger.js';
5
6
 
6
7
  export async function loadConfig(root) {
7
8
  const configPath = join(root, 'bertui.config.js');
@@ -15,7 +16,7 @@ export async function loadConfig(root) {
15
16
  // Merge user config with defaults
16
17
  return mergeConfig(defaultConfig, userConfig.default || userConfig);
17
18
  } catch (error) {
18
- logger.error(`Failed to load config make sure the file bertui.config.js is in the root directory of the app if not create it : ${error.message}`);
19
+ logger.error(`Failed to load config. Make sure bertui.config.js is in the root directory: ${error.message}`);
19
20
  return defaultConfig;
20
21
  }
21
22
  }
@@ -26,7 +27,7 @@ export async function loadConfig(root) {
26
27
 
27
28
  function mergeConfig(defaults, user) {
28
29
  return {
29
- meta: { ...defaults.meta, ...user.meta },
30
- appShell: { ...defaults.appShell, ...user.appShell }
30
+ meta: { ...defaults.meta, ...(user.meta || {}) },
31
+ appShell: { ...defaults.appShell, ...(user.appShell || {}) }
31
32
  };
32
33
  }
@@ -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,46 +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 currentRoutes = [];
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
+ }
16
23
 
17
24
  const app = new Elysia()
18
- // Serve index.html for all routes (SPA mode)
19
- .get('/*', async ({ params }) => {
20
- // Check if it's requesting a file
21
- const path = params['*'] || '';
25
+ // Main HTML route - serves all pages
26
+ .get('/', async () => {
27
+ return serveHTML(root, hasRouter);
28
+ })
29
+
30
+ // Catch-all route for SPA routing
31
+ .get('/*', async ({ params, set }) => {
32
+ const path = params['*'];
22
33
 
34
+ // Check if it's a file request
23
35
  if (path.includes('.')) {
24
- // It's a file request, handle it separately
25
- return await serveFile(compiledDir, path);
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';
26
54
  }
27
55
 
28
- // Serve the SPA HTML for all routes
29
- const html = `
30
- <!DOCTYPE html>
31
- <html lang="en">
32
- <head>
33
- <meta charset="UTF-8">
34
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
- <title>BertUI App</title>
36
- <link rel="stylesheet" href="/compiled/bertui.css">
37
- </head>
38
- <body>
39
- <div id="root"></div>
40
- <script type="module" src="/hmr-client.js"></script>
41
- <script type="module">
42
- // Provide React and ReactDOM from CDN for dev
43
- import React from 'https://esm.sh/react@18.2.0';
44
- import ReactDOM from 'https://esm.sh/react-dom@18.2.0';
45
- window.React = React;
46
- window.ReactDOM = ReactDOM;
47
- </script>
48
- <script type="module" src="/compiled/main-entry.js"></script>
49
- </body>
50
- </html>`;
51
-
52
- return new Response(html, {
53
- headers: { 'Content-Type': 'text/html' }
54
- });
56
+ // For non-file routes, serve the main HTML (SPA mode)
57
+ return serveHTML(root, hasRouter);
55
58
  })
56
59
 
57
60
  .get('/hmr-client.js', () => {
@@ -66,28 +69,21 @@ ws.onmessage = (event) => {
66
69
  const data = JSON.parse(event.data);
67
70
 
68
71
  if (data.type === 'reload') {
69
- console.log('%c🔄 Hot reloading...', 'color: #f59e0b');
72
+ console.log('%c🔄 Reloading...', 'color: #f59e0b');
70
73
  window.location.reload();
71
74
  }
72
75
 
73
76
  if (data.type === 'recompiling') {
74
77
  console.log('%c⚙️ Recompiling...', 'color: #3b82f6');
75
78
  }
76
-
77
- if (data.type === 'routes-updated') {
78
- console.log('%c📍 Routes updated:', 'color: #8b5cf6; font-weight: bold');
79
- data.routes.forEach(r => {
80
- console.log(\` \${r.path} → \${r.file}\`);
81
- });
82
- }
83
79
  };
84
80
 
85
81
  ws.onerror = (error) => {
86
- console.error('HMR connection error:', error);
82
+ console.error('%c❌ HMR connection error', 'color: #ef4444', error);
87
83
  };
88
84
 
89
85
  ws.onclose = () => {
90
- console.log('%c HMR disconnected. Refresh to reconnect.', 'color: #ef4444');
86
+ console.log('%c⚠️ HMR disconnected. Refresh to reconnect.', 'color: #f59e0b');
91
87
  };
92
88
  `;
93
89
 
@@ -100,20 +96,31 @@ ws.onclose = () => {
100
96
  open(ws) {
101
97
  clients.add(ws);
102
98
  logger.info('Client connected to HMR');
103
-
104
- // Send current routes on connection
105
- if (currentRoutes.length > 0) {
106
- ws.send(JSON.stringify({
107
- type: 'routes-updated',
108
- routes: currentRoutes
109
- }));
110
- }
111
99
  },
112
100
  close(ws) {
113
101
  clients.delete(ws);
102
+ logger.info('Client disconnected from HMR');
114
103
  }
115
104
  })
116
105
 
106
+ // Serve BertUI CSS
107
+ .get('/styles/bertui.css', async ({ set }) => {
108
+ const cssPath = join(import.meta.dir, '../styles/bertui.css');
109
+ const file = Bun.file(cssPath);
110
+
111
+ if (!await file.exists()) {
112
+ set.status = 404;
113
+ return 'CSS file not found';
114
+ }
115
+
116
+ return new Response(await file.text(), {
117
+ headers: {
118
+ 'Content-Type': 'text/css',
119
+ 'Cache-Control': 'no-store'
120
+ }
121
+ });
122
+ })
123
+
117
124
  // Serve compiled files
118
125
  .get('/compiled/*', async ({ params, set }) => {
119
126
  const filepath = join(compiledDir, params['*']);
@@ -135,6 +142,20 @@ ws.onclose = () => {
135
142
  });
136
143
  })
137
144
 
145
+ // Serve public assets
146
+ .get('/public/*', async ({ params, set }) => {
147
+ const publicDir = join(root, 'public');
148
+ const filepath = join(publicDir, params['*']);
149
+ const file = Bun.file(filepath);
150
+
151
+ if (!await file.exists()) {
152
+ set.status = 404;
153
+ return 'File not found';
154
+ }
155
+
156
+ return new Response(file);
157
+ })
158
+
138
159
  .listen(port);
139
160
 
140
161
  if (!app.server) {
@@ -142,33 +163,41 @@ ws.onclose = () => {
142
163
  process.exit(1);
143
164
  }
144
165
 
145
- logger.success(`Server running at http://localhost:${port}`);
146
- logger.info('Press Ctrl+C to stop');
166
+ logger.success(`🚀 Server running at http://localhost:${port}`);
167
+ logger.info(`📁 Serving: ${root}`);
147
168
 
148
169
  // Watch for file changes
149
- setupWatcher(root, clients, (routes) => {
150
- currentRoutes = routes;
170
+ setupWatcher(root, compiledDir, clients, () => {
171
+ // Check router status on recompile
172
+ hasRouter = existsSync(join(compiledDir, 'router.js'));
151
173
  });
152
174
 
153
175
  return app;
154
176
  }
155
177
 
156
- async function serveFile(compiledDir, path) {
157
- const filepath = join(compiledDir, path);
158
- const file = Bun.file(filepath);
159
-
160
- if (!await file.exists()) {
161
- return new Response('File not found', { status: 404 });
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
+ : ''
162
194
  }
195
+ <script type="module" src="/compiled/main.js"></script>
196
+ </body>
197
+ </html>`;
163
198
 
164
- const ext = extname(filepath);
165
- const contentType = getContentType(ext);
166
-
167
- return new Response(await file.text(), {
168
- headers: {
169
- 'Content-Type': contentType,
170
- 'Cache-Control': 'no-store'
171
- }
199
+ return new Response(html, {
200
+ headers: { 'Content-Type': 'text/html' }
172
201
  });
173
202
  }
174
203
 
@@ -180,12 +209,20 @@ function getContentType(ext) {
180
209
  '.json': 'application/json',
181
210
  '.png': 'image/png',
182
211
  '.jpg': 'image/jpeg',
183
- '.svg': 'image/svg+xml'
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'
184
220
  };
221
+
185
222
  return types[ext] || 'text/plain';
186
223
  }
187
224
 
188
- function setupWatcher(root, clients, onRoutesUpdate) {
225
+ function setupWatcher(root, compiledDir, clients, onRecompile) {
189
226
  const srcDir = join(root, 'src');
190
227
 
191
228
  if (!existsSync(srcDir)) {
@@ -195,15 +232,11 @@ function setupWatcher(root, clients, onRoutesUpdate) {
195
232
 
196
233
  logger.info(`👀 Watching: ${srcDir}`);
197
234
 
198
- let isRecompiling = false;
199
-
200
235
  watch(srcDir, { recursive: true }, async (eventType, filename) => {
201
- if (!filename || isRecompiling) return;
236
+ if (!filename) return;
202
237
 
203
238
  const ext = extname(filename);
204
239
  if (['.js', '.jsx', '.ts', '.tsx', '.css'].includes(ext)) {
205
- isRecompiling = true;
206
-
207
240
  logger.info(`📝 File changed: ${filename}`);
208
241
 
209
242
  // Notify clients that recompilation is starting
@@ -217,22 +250,11 @@ function setupWatcher(root, clients, onRoutesUpdate) {
217
250
 
218
251
  // Recompile the project
219
252
  try {
220
- const result = await compileProject(root);
253
+ await compileProject(root);
221
254
 
222
- // Notify about route changes
223
- if (result.routes && result.routes.length > 0) {
224
- onRoutesUpdate(result.routes);
225
-
226
- for (const client of clients) {
227
- try {
228
- client.send(JSON.stringify({
229
- type: 'routes-updated',
230
- routes: result.routes
231
- }));
232
- } catch (e) {
233
- clients.delete(client);
234
- }
235
- }
255
+ // Call callback to update router status
256
+ if (onRecompile) {
257
+ onRecompile();
236
258
  }
237
259
 
238
260
  // Notify clients to reload
@@ -245,8 +267,6 @@ function setupWatcher(root, clients, onRoutesUpdate) {
245
267
  }
246
268
  } catch (error) {
247
269
  logger.error(`Recompilation failed: ${error.message}`);
248
- } finally {
249
- isRecompiling = false;
250
270
  }
251
271
  }
252
272
  });