bertui 1.1.6 → 1.1.7
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/README.md +164 -292
- package/index.js +53 -31
- package/package.json +49 -31
- package/src/build/image-optimizer.js +102 -51
- package/src/client/fast-refresh.js +72 -0
- package/src/client/hmr-runtime.js +59 -0
- package/src/compiler/index.js +25 -0
- package/src/compiler/router-generator-pure.js +104 -0
- package/src/compiler/transform.js +149 -0
- package/src/config/index.js +2 -0
- package/src/css/index.js +46 -0
- package/src/css/processor.js +127 -0
- package/src/image-optimizer/index.js +76 -0
- package/src/images/index.js +102 -0
- package/src/images/processor.js +169 -0
- package/src/router/index.js +3 -0
- package/src/server/dev-handler.js +254 -0
- package/src/server/dev-server-utils.js +289 -0
- package/src/server/dev-server.js +10 -456
- package/src/server/hmr-handler.js +148 -0
- package/src/server/index.js +3 -0
- package/src/server/request-handler.js +36 -0
- package/src/server-islands/extractor.js +198 -0
- package/src/server-islands/index.js +59 -0
- package/src/utils/index.js +11 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bertui",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "Lightning-fast React dev server powered by Bun
|
|
3
|
+
"version": "1.1.7",
|
|
4
|
+
"description": "Lightning-fast React dev server powered by Bun - Now with Rust image optimization (WASM, no Rust required for users)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -11,22 +11,32 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
13
|
"types": "./types/index.d.ts",
|
|
14
|
-
"
|
|
14
|
+
"import": "./src/index.js",
|
|
15
|
+
"default": "./src/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./hmr": {
|
|
18
|
+
"import": "./src/client/hmr-runtime.js"
|
|
19
|
+
},
|
|
20
|
+
"./image-optimizer": {
|
|
21
|
+
"import": "./src/image-optimizer/index.js"
|
|
15
22
|
},
|
|
16
|
-
"./styles": "./src/styles/bertui.css",
|
|
17
|
-
"./logger": "./src/logger/logger.js",
|
|
18
23
|
"./router": {
|
|
19
24
|
"types": "./types/router.d.ts",
|
|
20
|
-
"
|
|
25
|
+
"import": "./src/router/index.js"
|
|
21
26
|
},
|
|
22
27
|
"./config": {
|
|
23
28
|
"types": "./types/config.d.ts",
|
|
24
|
-
"
|
|
25
|
-
}
|
|
29
|
+
"import": "./src/config/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./logger": {
|
|
32
|
+
"import": "./src/logger/logger.js"
|
|
33
|
+
},
|
|
34
|
+
"./styles": "./src/styles/bertui.css"
|
|
26
35
|
},
|
|
27
36
|
"files": [
|
|
28
37
|
"bin",
|
|
29
38
|
"src",
|
|
39
|
+
"dist",
|
|
30
40
|
"types",
|
|
31
41
|
"index.js",
|
|
32
42
|
"README.md",
|
|
@@ -35,23 +45,45 @@
|
|
|
35
45
|
"scripts": {
|
|
36
46
|
"dev": "bun bin/bertui.js dev",
|
|
37
47
|
"build": "bun bin/bertui.js build",
|
|
48
|
+
"build:wasm": "cd src/image-optimizer-rust && wasm-pack build --target web --out-dir ../../dist/image-optimizer/wasm && bun run fix:wasm",
|
|
49
|
+
"fix:wasm": "node scripts/fix-wasm-exports.js",
|
|
50
|
+
"prepublishOnly": "echo 'Skipping wasm build'",
|
|
38
51
|
"test": "cd test-app && bun run dev"
|
|
39
52
|
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"elysia": "^1.0.0",
|
|
55
|
+
"ernest-logger": "^2.0.0",
|
|
56
|
+
"lightningcss": "^1.30.2",
|
|
57
|
+
"react-refresh": "^0.14.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"bun": ">=1.0.0",
|
|
61
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
62
|
+
"react-dom": "^19.2.3"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@types/react": "^18.2.0",
|
|
66
|
+
"@types/react-dom": "^18.2.0",
|
|
67
|
+
"oxipng": "^1.0.1",
|
|
68
|
+
"wasm-pack": "^0.12.1"
|
|
69
|
+
},
|
|
70
|
+
"optionalDependencies": {
|
|
71
|
+
"@bertui/image-optimizer-wasm": "0.1.0",
|
|
72
|
+
"oxipng": "^8.0.0"
|
|
73
|
+
},
|
|
40
74
|
"keywords": [
|
|
41
75
|
"react",
|
|
42
76
|
"bun",
|
|
43
|
-
"dev-server",
|
|
44
|
-
"vite-alternative",
|
|
45
77
|
"elysia",
|
|
46
|
-
"build-tool",
|
|
47
|
-
"bundler",
|
|
48
|
-
"fast",
|
|
49
78
|
"hmr",
|
|
50
|
-
"
|
|
51
|
-
"
|
|
79
|
+
"fast-refresh",
|
|
80
|
+
"image-optimization",
|
|
81
|
+
"rust",
|
|
82
|
+
"wasm",
|
|
83
|
+
"server-islands",
|
|
52
84
|
"seo",
|
|
53
85
|
"sitemap",
|
|
54
|
-
"
|
|
86
|
+
"typescript"
|
|
55
87
|
],
|
|
56
88
|
"author": "Pease Ernest",
|
|
57
89
|
"license": "MIT",
|
|
@@ -59,21 +91,7 @@
|
|
|
59
91
|
"type": "git",
|
|
60
92
|
"url": "https://github.com/BunElysiaReact/BERTUI.git"
|
|
61
93
|
},
|
|
62
|
-
"dependencies": {
|
|
63
|
-
"elysia": "^1.0.0",
|
|
64
|
-
"ernest-logger": "^2.0.0",
|
|
65
|
-
"lightningcss": "^1.30.2"
|
|
66
|
-
},
|
|
67
|
-
"peerDependencies": {
|
|
68
|
-
"react": "^18.0.0 || ^19.0.0",
|
|
69
|
-
"react-dom": "^19.2.3",
|
|
70
|
-
"bun": ">=1.0.0"
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"@types/react": "^18.2.0",
|
|
74
|
-
"@types/react-dom": "^18.2.0"
|
|
75
|
-
},
|
|
76
94
|
"engines": {
|
|
77
95
|
"bun": ">=1.0.0"
|
|
78
96
|
}
|
|
79
|
-
}
|
|
97
|
+
}
|
|
@@ -1,86 +1,137 @@
|
|
|
1
|
-
// bertui/src/build/image-optimizer.js -
|
|
1
|
+
// bertui/src/build/image-optimizer.js - UPDATED WITH WASM
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync } from 'fs';
|
|
4
4
|
import logger from '../logger/logger.js';
|
|
5
|
+
import { optimizeImage, hasWasm } from '../image-optimizer/index.js';
|
|
6
|
+
import { copyImagesSync } from '../images/index.js';
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export async function optimizeImages(srcDir, outDir, options = {}) {
|
|
9
|
+
const {
|
|
10
|
+
quality = 80,
|
|
11
|
+
webpQuality = 75,
|
|
12
|
+
verbose = false
|
|
13
|
+
} = options;
|
|
10
14
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// Always return empty array to disable optimization
|
|
20
|
-
return [];
|
|
21
|
-
}
|
|
15
|
+
// Check if WASM is available
|
|
16
|
+
const wasmAvailable = await hasWasm();
|
|
17
|
+
|
|
18
|
+
if (wasmAvailable) {
|
|
19
|
+
logger.info(`🦀 Optimizing images with Rust WASM (quality: ${quality})...`);
|
|
20
|
+
} else {
|
|
21
|
+
logger.info('📋 Copying images (WASM optimizer not available)...');
|
|
22
|
+
}
|
|
22
23
|
|
|
23
|
-
export function copyImages(srcDir, outDir) {
|
|
24
|
-
// All common image formats
|
|
25
24
|
const imageExtensions = [
|
|
26
25
|
'.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg',
|
|
27
26
|
'.avif', '.ico', '.bmp', '.tiff', '.tif'
|
|
28
27
|
];
|
|
29
28
|
|
|
29
|
+
let optimized = 0;
|
|
30
30
|
let copied = 0;
|
|
31
31
|
let skipped = 0;
|
|
32
|
+
let totalSaved = 0;
|
|
33
|
+
const results = [];
|
|
32
34
|
|
|
33
35
|
if (!existsSync(srcDir)) {
|
|
34
36
|
logger.warn(`⚠️ Source not found: ${srcDir}`);
|
|
35
|
-
return 0;
|
|
37
|
+
return { optimized: 0, copied: 0, saved: 0, results: [] };
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
// Ensure output directory exists
|
|
39
40
|
mkdirSync(outDir, { recursive: true });
|
|
40
41
|
|
|
41
|
-
function processDirectory(dir, targetDir) {
|
|
42
|
-
|
|
43
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
42
|
+
async function processDirectory(dir, targetDir) {
|
|
43
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
const srcPath = join(dir, entry.name);
|
|
47
|
+
const destPath = join(targetDir, entry.name);
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
const subDestPath = join(targetDir, entry.name);
|
|
51
|
+
mkdirSync(subDestPath, { recursive: true });
|
|
52
|
+
await processDirectory(srcPath, subDestPath);
|
|
53
|
+
} else if (entry.isFile()) {
|
|
54
|
+
const ext = extname(entry.name).toLowerCase();
|
|
55
|
+
|
|
56
|
+
if (imageExtensions.includes(ext)) {
|
|
57
|
+
try {
|
|
58
|
+
const file = Bun.file(srcPath);
|
|
59
|
+
const buffer = await file.arrayBuffer();
|
|
60
|
+
const originalSize = buffer.byteLength;
|
|
61
|
+
|
|
62
|
+
// Try WASM optimization first, fallback to copy
|
|
63
|
+
if (wasmAvailable && ['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) {
|
|
64
|
+
const format = ext.slice(1);
|
|
65
|
+
const result = await optimizeImage(buffer, {
|
|
66
|
+
format,
|
|
67
|
+
quality,
|
|
68
|
+
webpQuality
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await Bun.write(destPath, new Uint8Array(result.data));
|
|
72
|
+
|
|
73
|
+
const saved = originalSize - result.optimized_size;
|
|
74
|
+
totalSaved += saved;
|
|
75
|
+
optimized++;
|
|
76
|
+
|
|
77
|
+
results.push({
|
|
78
|
+
file: entry.name,
|
|
79
|
+
original: formatBytes(originalSize),
|
|
80
|
+
optimized: formatBytes(result.optimized_size),
|
|
81
|
+
saved: formatBytes(saved),
|
|
82
|
+
percent: result.savings_percent.toFixed(1) + '%'
|
|
83
|
+
});
|
|
84
|
+
} else {
|
|
85
|
+
// Just copy unsupported formats
|
|
86
|
+
await Bun.write(destPath, file);
|
|
60
87
|
copied++;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
results.push({
|
|
89
|
+
file: entry.name,
|
|
90
|
+
status: 'copied'
|
|
91
|
+
});
|
|
64
92
|
}
|
|
65
|
-
}
|
|
66
|
-
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger.warn(` Failed to process ${entry.name}: ${error.message}`);
|
|
95
|
+
// Fallback: copy original
|
|
96
|
+
await Bun.write(destPath, Bun.file(srcPath));
|
|
97
|
+
copied++;
|
|
67
98
|
}
|
|
99
|
+
} else {
|
|
100
|
+
// Copy non-image files
|
|
101
|
+
await Bun.write(destPath, Bun.file(srcPath));
|
|
102
|
+
skipped++;
|
|
68
103
|
}
|
|
69
104
|
}
|
|
70
|
-
} catch (error) {
|
|
71
|
-
logger.error(`Error processing ${dir}: ${error.message}`);
|
|
72
105
|
}
|
|
73
106
|
}
|
|
74
107
|
|
|
75
|
-
processDirectory(srcDir, outDir);
|
|
108
|
+
await processDirectory(srcDir, outDir);
|
|
76
109
|
|
|
77
|
-
|
|
78
|
-
|
|
110
|
+
// Show summary
|
|
111
|
+
if (optimized > 0) {
|
|
112
|
+
logger.success(`✅ Optimized ${optimized} images with Rust WASM`);
|
|
113
|
+
logger.table(results.slice(0, 10));
|
|
114
|
+
if (results.length > 10) {
|
|
115
|
+
logger.info(` ... and ${results.length - 10} more images`);
|
|
116
|
+
}
|
|
117
|
+
logger.info(`📊 Total saved: ${formatBytes(totalSaved)}`);
|
|
79
118
|
}
|
|
80
119
|
|
|
81
|
-
if (
|
|
82
|
-
logger.info(
|
|
120
|
+
if (copied > 0) {
|
|
121
|
+
logger.info(`📋 Copied ${copied} images (fallback)`);
|
|
83
122
|
}
|
|
84
123
|
|
|
85
|
-
return copied;
|
|
124
|
+
return { optimized, copied, saved: totalSaved, results };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function copyImages(srcDir, outDir) {
|
|
128
|
+
return copyImagesSync(srcDir, outDir);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function formatBytes(bytes) {
|
|
132
|
+
if (bytes === 0) return '0 B';
|
|
133
|
+
const k = 1024;
|
|
134
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
135
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
136
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
|
86
137
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// bertui/src/client/fast-refresh.js
|
|
2
|
+
// React Fast Refresh integration
|
|
3
|
+
|
|
4
|
+
import RefreshRuntime from 'react-refresh';
|
|
5
|
+
|
|
6
|
+
// Inject into global scope
|
|
7
|
+
if (typeof window !== 'undefined') {
|
|
8
|
+
// Setup Fast Refresh globals
|
|
9
|
+
window.$RefreshReg$ = (type, id) => {
|
|
10
|
+
RefreshRuntime.register(type, id);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform;
|
|
14
|
+
|
|
15
|
+
// Store runtime in global
|
|
16
|
+
window.$RefreshRuntime$ = RefreshRuntime;
|
|
17
|
+
|
|
18
|
+
// Inject into global hook
|
|
19
|
+
if (!window.__REACT_DEVTOOLS_GLOBAL_HOOK__) {
|
|
20
|
+
window.__REACT_DEVTOOLS_GLOBAL_HOOK__ = {
|
|
21
|
+
supportsFiber: true,
|
|
22
|
+
inject: (fiber) => {},
|
|
23
|
+
onCommitFiberRoot: (rendererId, root) => {},
|
|
24
|
+
onCommitFiberUnmount: () => {}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Setup refresh handler
|
|
29
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
30
|
+
|
|
31
|
+
// Create queue for batched updates
|
|
32
|
+
let updateQueue = [];
|
|
33
|
+
let scheduled = false;
|
|
34
|
+
|
|
35
|
+
const scheduleUpdate = () => {
|
|
36
|
+
if (scheduled) return;
|
|
37
|
+
scheduled = true;
|
|
38
|
+
|
|
39
|
+
queueMicrotask(() => {
|
|
40
|
+
scheduled = false;
|
|
41
|
+
const queue = updateQueue;
|
|
42
|
+
updateQueue = [];
|
|
43
|
+
|
|
44
|
+
if (queue.length > 0 && window.$RefreshRuntime$) {
|
|
45
|
+
window.$RefreshRuntime$.performReactRefresh();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Listen for HMR updates
|
|
51
|
+
window.addEventListener('hmr-module-updated', (event) => {
|
|
52
|
+
const { moduleId } = event.detail;
|
|
53
|
+
updateQueue.push(moduleId);
|
|
54
|
+
scheduleUpdate();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log('%c⚡ React Fast Refresh enabled', 'color: #3b82f6; font-weight: bold');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Export for module usage
|
|
61
|
+
export const setupFastRefresh = () => {
|
|
62
|
+
if (typeof window !== 'undefined' && window.$RefreshRuntime$) {
|
|
63
|
+
return window.$RefreshRuntime$;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const performReactRefresh = () => {
|
|
69
|
+
if (typeof window !== 'undefined' && window.$RefreshRuntime$) {
|
|
70
|
+
window.$RefreshRuntime$.performReactRefresh();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// bertui/src/client/hmr-runtime.js
|
|
2
|
+
// ONE SOLUTION - Fast Refresh HMR
|
|
3
|
+
|
|
4
|
+
(function(global) {
|
|
5
|
+
let socket = null;
|
|
6
|
+
let reconnectTimer = null;
|
|
7
|
+
|
|
8
|
+
function connect() {
|
|
9
|
+
const protocol = global.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
10
|
+
socket = new WebSocket(`${protocol}//${global.location.host}/__hmr`);
|
|
11
|
+
|
|
12
|
+
socket.onopen = () => {
|
|
13
|
+
console.log('%c🔥 HMR connected', 'color: #10b981; font-weight: bold');
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
socket.onmessage = async (event) => {
|
|
17
|
+
const data = JSON.parse(event.data);
|
|
18
|
+
|
|
19
|
+
if (data.type === 'hmr-update') {
|
|
20
|
+
console.log(`%c🔥 HMR: ${data.module} (${data.time}ms)`, 'color: #10b981');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(data.module, global.location.origin);
|
|
24
|
+
url.searchParams.set('t', Date.now());
|
|
25
|
+
await import(url.toString());
|
|
26
|
+
|
|
27
|
+
if (global.$RefreshRuntime$) {
|
|
28
|
+
global.$RefreshRuntime$.performReactRefresh();
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
console.error('HMR update failed:', err);
|
|
32
|
+
global.location.reload();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (data.type === 'full-reload') {
|
|
37
|
+
global.location.reload();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
socket.onclose = () => {
|
|
42
|
+
console.log('%c⚠️ HMR disconnected, reconnecting...', 'color: #f59e0b');
|
|
43
|
+
reconnectTimer = setTimeout(connect, 2000);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (global.document) {
|
|
48
|
+
global.addEventListener('load', connect);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Fast Refresh setup
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
window.$RefreshReg$ = (type, id) => {};
|
|
54
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
})(typeof window !== 'undefined' ? window : global);
|
|
58
|
+
|
|
59
|
+
export const hmr = { connect: () => {} };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// bertui/src/compiler/index.js - NEW FILE (PURE, NO SERVER)
|
|
2
|
+
export { compileProject } from '../client/compiler.js';
|
|
3
|
+
export { compileForBuild } from '../build/compiler/index.js';
|
|
4
|
+
export { discoverRoutes } from '../build/compiler/route-discoverer.js';
|
|
5
|
+
export { validateServerIsland } from '../build/server-island-validator.js';
|
|
6
|
+
|
|
7
|
+
// PURE JSX→JS TRANSFORMATION
|
|
8
|
+
export async function transformJSX(sourceCode, options = {}) {
|
|
9
|
+
const transpiler = new Bun.Transpiler({
|
|
10
|
+
loader: options.loader || 'tsx',
|
|
11
|
+
target: 'browser',
|
|
12
|
+
define: {
|
|
13
|
+
'process.env.NODE_ENV': JSON.stringify(options.env || 'development')
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
return await transpiler.transform(sourceCode);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Export everything from this directory
|
|
20
|
+
export { transformJSX, transformJSXSync, containsJSX, removeCSSImports, removeDotenvImports, fixRelativeImports } from './transform.js';
|
|
21
|
+
export { generateRouterCode } from './router-generator-pure.js';
|
|
22
|
+
// Re-export existing
|
|
23
|
+
export { compileProject } from '../client/compiler.js';
|
|
24
|
+
export { compileForBuild } from '../build/compiler/index.js';
|
|
25
|
+
export { discoverRoutes } from '../build/compiler/route-discoverer.js';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// bertui/src/compiler/router-generator-pure.js - NEW FILE
|
|
2
|
+
// PURE function - no file system, no server
|
|
3
|
+
|
|
4
|
+
export function generateRouterCode(routes) {
|
|
5
|
+
const imports = routes.map((route, i) => {
|
|
6
|
+
const componentName = `Page${i}`;
|
|
7
|
+
const importPath = route.importPath || `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
8
|
+
return `import ${componentName} from '${importPath}';`;
|
|
9
|
+
}).join('\n');
|
|
10
|
+
|
|
11
|
+
const routeConfigs = routes.map((route, i) => {
|
|
12
|
+
const componentName = `Page${i}`;
|
|
13
|
+
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
14
|
+
}).join(',\n');
|
|
15
|
+
|
|
16
|
+
return `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
17
|
+
|
|
18
|
+
const RouterContext = createContext(null);
|
|
19
|
+
|
|
20
|
+
export function useRouter() {
|
|
21
|
+
const context = useContext(RouterContext);
|
|
22
|
+
if (!context) throw new Error('useRouter must be used within a Router');
|
|
23
|
+
return context;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function Router({ routes }) {
|
|
27
|
+
const [currentRoute, setCurrentRoute] = useState(null);
|
|
28
|
+
const [params, setParams] = useState({});
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
matchAndSetRoute(window.location.pathname);
|
|
32
|
+
const handlePopState = () => matchAndSetRoute(window.location.pathname);
|
|
33
|
+
window.addEventListener('popstate', handlePopState);
|
|
34
|
+
return () => window.removeEventListener('popstate', handlePopState);
|
|
35
|
+
}, [routes]);
|
|
36
|
+
|
|
37
|
+
function matchAndSetRoute(pathname) {
|
|
38
|
+
// 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
|
+
// Dynamic routes
|
|
47
|
+
for (const route of routes) {
|
|
48
|
+
if (route.type === 'dynamic') {
|
|
49
|
+
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
50
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
51
|
+
const match = pathname.match(regex);
|
|
52
|
+
if (match) {
|
|
53
|
+
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
54
|
+
const extractedParams = {};
|
|
55
|
+
paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
|
|
56
|
+
setCurrentRoute(route);
|
|
57
|
+
setParams(extractedParams);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
setCurrentRoute(null);
|
|
63
|
+
setParams({});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function navigate(path) {
|
|
67
|
+
window.history.pushState({}, '', path);
|
|
68
|
+
matchAndSetRoute(path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const Component = currentRoute?.component;
|
|
72
|
+
return React.createElement(
|
|
73
|
+
RouterContext.Provider,
|
|
74
|
+
{ value: { currentRoute, params, navigate, pathname: window.location.pathname } },
|
|
75
|
+
Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function Link({ to, children, ...props }) {
|
|
80
|
+
const { navigate } = useRouter();
|
|
81
|
+
return React.createElement('a', {
|
|
82
|
+
href: to,
|
|
83
|
+
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
84
|
+
...props
|
|
85
|
+
}, children);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function NotFound() {
|
|
89
|
+
return React.createElement('div', {
|
|
90
|
+
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
91
|
+
justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
|
|
92
|
+
},
|
|
93
|
+
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
94
|
+
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
95
|
+
React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
${imports}
|
|
100
|
+
|
|
101
|
+
export const routes = [
|
|
102
|
+
${routeConfigs}
|
|
103
|
+
];`;
|
|
104
|
+
}
|