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 +11 -10
- package/package.json +15 -14
- package/src/build/css-builder.js +84 -0
- package/src/build.js +50 -21
- package/src/client/compiler.js +202 -68
- package/src/config/loadConfig.js +5 -4
- package/src/router/router.js +105 -192
- package/src/server/dev-server.js +114 -94
package/index.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
// index.js
|
|
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
|
-
//
|
|
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.
|
|
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.
|
|
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
|
-
"
|
|
42
|
-
"
|
|
36
|
+
"fast",
|
|
37
|
+
"hmr"
|
|
43
38
|
],
|
|
44
|
-
"author": "
|
|
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
|
|
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: [
|
|
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: '
|
|
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('
|
|
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
|
-
|
|
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(
|
|
54
|
-
logger.info(
|
|
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
|
|
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="/${
|
|
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.
|
|
122
|
+
logger.success('Generated index.html');
|
|
94
123
|
}
|
package/src/client/compiler.js
CHANGED
|
@@ -1,92 +1,226 @@
|
|
|
1
1
|
// src/client/compiler.js
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
|
|
9
|
-
const routerDir = join(root, '.bertui');
|
|
7
|
+
logger.bigLog('COMPILING PROJECT', { color: 'blue' });
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
+
// Create output directory
|
|
20
|
+
if (!existsSync(outDir)) {
|
|
21
|
+
mkdirSync(outDir, { recursive: true });
|
|
22
|
+
logger.info('Created .bertui/compiled/');
|
|
23
|
+
}
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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(`
|
|
223
|
+
logger.error(`Failed to compile ${relativePath}: ${error.message}`);
|
|
90
224
|
throw error;
|
|
91
225
|
}
|
|
92
226
|
}
|
package/src/config/loadConfig.js
CHANGED
|
@@ -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 '../
|
|
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
|
|
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
|
}
|
package/src/router/router.js
CHANGED
|
@@ -1,216 +1,129 @@
|
|
|
1
|
-
// src/router/
|
|
2
|
-
import {
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
97
|
+
export function Link({ to, children, className, ...props }) {
|
|
98
|
+
const { navigate } = useRouter();
|
|
99
|
+
|
|
100
|
+
function handleClick(e) {
|
|
171
101
|
e.preventDefault();
|
|
172
|
-
navigate(
|
|
173
|
-
}
|
|
174
|
-
|
|
102
|
+
navigate(to);
|
|
103
|
+
}
|
|
104
|
+
|
|
175
105
|
return (
|
|
176
|
-
<a href={
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
}
|
package/src/server/dev-server.js
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
19
|
-
.get('
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
25
|
-
|
|
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
|
-
//
|
|
29
|
-
|
|
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🔄
|
|
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
|
|
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(
|
|
146
|
-
logger.info(
|
|
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, (
|
|
150
|
-
|
|
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
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
'.
|
|
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,
|
|
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
|
|
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
|
-
|
|
253
|
+
await compileProject(root);
|
|
221
254
|
|
|
222
|
-
//
|
|
223
|
-
if (
|
|
224
|
-
|
|
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
|
});
|