bertui 1.0.3 → 1.1.1

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/package.json CHANGED
@@ -1,21 +1,33 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.0.3",
4
- "description": "Lightning-fast React dev server powered by Bun and Elysia",
3
+ "version": "1.1.1",
4
+ "description": "Lightning-fast React dev server powered by Bun and Elysia - Now with TypeScript support!",
5
5
  "type": "module",
6
6
  "main": "./index.js",
7
+ "types": "./types/index.d.ts",
7
8
  "bin": {
8
9
  "bertui": "./bin/bertui.js"
9
10
  },
10
11
  "exports": {
11
- ".": "./index.js",
12
+ ".": {
13
+ "types": "./types/index.d.ts",
14
+ "default": "./index.js"
15
+ },
12
16
  "./styles": "./src/styles/bertui.css",
13
17
  "./logger": "./src/logger/logger.js",
14
- "./router": "./src/router/Router.jsx"
18
+ "./router": {
19
+ "types": "./types/router.d.ts",
20
+ "default": "./src/router/Router.jsx"
21
+ },
22
+ "./config": {
23
+ "types": "./types/config.d.ts",
24
+ "default": "./src/config/loadConfig.js"
25
+ }
15
26
  },
16
27
  "files": [
17
28
  "bin",
18
29
  "src",
30
+ "types",
19
31
  "index.js",
20
32
  "README.md",
21
33
  "LICENSE"
@@ -35,7 +47,11 @@
35
47
  "bundler",
36
48
  "fast",
37
49
  "hmr",
38
- "file-based-routing"
50
+ "file-based-routing",
51
+ "typescript",
52
+ "seo",
53
+ "sitemap",
54
+ "server-islands"
39
55
  ],
40
56
  "author": "Pease Ernest",
41
57
  "license": "MIT",
@@ -45,14 +61,18 @@
45
61
  },
46
62
  "dependencies": {
47
63
  "elysia": "^1.0.0",
48
- "ernest-logger": "latest",
64
+ "ernest-logger": "^2.0.0",
49
65
  "lightningcss": "^1.30.2"
50
66
  },
51
67
  "peerDependencies": {
52
68
  "react": "^18.0.0 || ^19.0.0",
53
- "react-dom": "^18.0.0 || ^19.0.0",
69
+ "react-dom": "^19.2.3",
54
70
  "bun": ">=1.0.0"
55
71
  },
72
+ "devDependencies": {
73
+ "@types/react": "^18.2.0",
74
+ "@types/react-dom": "^18.2.0"
75
+ },
56
76
  "engines": {
57
77
  "bun": ">=1.0.0"
58
78
  }
@@ -0,0 +1,171 @@
1
+ // bertui/src/build/compiler/file-transpiler.js - SINGLE TRANSPILER INSTANCE
2
+ import { join, relative, dirname, extname } from 'path';
3
+ import { readdirSync, statSync, mkdirSync } from 'fs';
4
+ import logger from '../../logger/logger.js';
5
+ import { replaceEnvInCode } from '../../utils/env.js';
6
+
7
+ // ✅ CRITICAL FIX: Create transpilers ONCE at module level
8
+ const jsxTranspiler = new Bun.Transpiler({ loader: 'jsx' });
9
+
10
+ const tsxTranspiler = new Bun.Transpiler({
11
+ loader: 'tsx',
12
+ tsconfig: {
13
+ compilerOptions: {
14
+ jsx: 'react',
15
+ jsxFactory: 'React.createElement',
16
+ jsxFragmentFactory: 'React.Fragment'
17
+ }
18
+ }
19
+ });
20
+
21
+ const tsTranspiler = new Bun.Transpiler({
22
+ loader: 'ts',
23
+ tsconfig: {
24
+ compilerOptions: {
25
+ jsx: 'preserve'
26
+ }
27
+ }
28
+ });
29
+
30
+ function getTranspiler(loader) {
31
+ // Just return the pre-created instances
32
+ return loader === 'tsx' ? tsxTranspiler :
33
+ loader === 'ts' ? tsTranspiler :
34
+ jsxTranspiler;
35
+ }
36
+
37
+ export async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
38
+ const files = readdirSync(srcDir);
39
+ const filesToCompile = [];
40
+
41
+ for (const file of files) {
42
+ const srcPath = join(srcDir, file);
43
+ const stat = statSync(srcPath);
44
+
45
+ if (stat.isDirectory()) {
46
+ const subBuildDir = join(buildDir, file);
47
+ mkdirSync(subBuildDir, { recursive: true });
48
+ await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
49
+ } else {
50
+ const ext = extname(file);
51
+ if (ext === '.css') continue;
52
+
53
+ if (['.jsx', '.tsx', '.ts'].includes(ext)) {
54
+ filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'tsx' });
55
+ } else if (ext === '.js') {
56
+ filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'js' });
57
+ }
58
+ }
59
+ }
60
+
61
+ if (filesToCompile.length === 0) return;
62
+
63
+ // ✅ Process sequentially with progress
64
+ logger.info(`📦 Compiling ${filesToCompile.length} files sequentially...`);
65
+
66
+ for (let i = 0; i < filesToCompile.length; i++) {
67
+ const file = filesToCompile[i];
68
+
69
+ try {
70
+ if (file.type === 'tsx') {
71
+ await compileBuildFile(file.path, file.dir, file.name, root, envVars);
72
+ } else {
73
+ await compileJSFile(file.path, file.dir, file.name, root, envVars);
74
+ }
75
+
76
+ // Progress every 10 files
77
+ if ((i + 1) % 10 === 0 || i === filesToCompile.length - 1) {
78
+ const percent = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
79
+ logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${percent}%)`);
80
+ }
81
+
82
+ } catch (error) {
83
+ logger.error(`Failed to compile ${file.name}: ${error.message}`);
84
+ // Continue with other files
85
+ }
86
+ }
87
+
88
+ logger.success(`✅ Compiled ${filesToCompile.length} files`);
89
+ }
90
+
91
+ async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
92
+ const ext = extname(filename);
93
+ const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
94
+
95
+ try {
96
+ let code = await Bun.file(srcPath).text();
97
+ code = removeCSSImports(code);
98
+ code = replaceEnvInCode(code, envVars);
99
+
100
+ const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
101
+ const outPath = join(buildDir, outFilename);
102
+ code = fixBuildImports(code, srcPath, outPath, root);
103
+
104
+ // ✅ Reuse the same transpiler instance
105
+ const transpiler = getTranspiler(loader);
106
+ let compiled = await transpiler.transform(code);
107
+
108
+ if (usesJSX(compiled) && !compiled.includes('import React')) {
109
+ compiled = `import React from 'react';\n${compiled}`;
110
+ }
111
+
112
+ compiled = fixRelativeImports(compiled);
113
+ await Bun.write(outPath, compiled);
114
+
115
+ // Help GC
116
+ code = null;
117
+ compiled = null;
118
+
119
+ } catch (error) {
120
+ logger.error(`Failed to compile ${filename}: ${error.message}`);
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ async function compileJSFile(srcPath, buildDir, filename, root, envVars) {
126
+ const outPath = join(buildDir, filename);
127
+ let code = await Bun.file(srcPath).text();
128
+ code = removeCSSImports(code);
129
+ code = replaceEnvInCode(code, envVars);
130
+ code = fixBuildImports(code, srcPath, outPath, root);
131
+
132
+ if (usesJSX(code) && !code.includes('import React')) {
133
+ code = `import React from 'react';\n${code}`;
134
+ }
135
+
136
+ await Bun.write(outPath, code);
137
+ code = null;
138
+ }
139
+
140
+ function usesJSX(code) {
141
+ return code.includes('React.createElement') ||
142
+ code.includes('React.Fragment') ||
143
+ /<[A-Z]/.test(code) ||
144
+ code.includes('jsx(') ||
145
+ code.includes('jsxs(');
146
+ }
147
+
148
+ function removeCSSImports(code) {
149
+ code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
150
+ code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
151
+ return code;
152
+ }
153
+
154
+ function fixBuildImports(code, srcPath, outPath, root) {
155
+ const buildDir = join(root, '.bertuibuild');
156
+ const routerPath = join(buildDir, 'router.js');
157
+ const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
158
+ const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
159
+
160
+ code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
161
+ return code;
162
+ }
163
+
164
+ function fixRelativeImports(code) {
165
+ const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
166
+ code = code.replace(importRegex, (match, prefix, path) => {
167
+ if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
168
+ return `from '${prefix}${path}.js';`;
169
+ });
170
+ return code;
171
+ }
@@ -0,0 +1,45 @@
1
+ // bertui/src/build/compiler/index.js
2
+ import { join } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import logger from '../../logger/logger.js';
5
+ import { discoverRoutes } from './route-discoverer.js';
6
+ import { compileBuildDirectory } from './file-transpiler.js';
7
+ import { generateBuildRouter } from './router-generator.js';
8
+
9
+
10
+ export async function compileForBuild(root, buildDir, envVars) {
11
+ const srcDir = join(root, 'src');
12
+ const pagesDir = join(srcDir, 'pages');
13
+
14
+ if (!existsSync(srcDir)) {
15
+ throw new Error('src/ directory not found!');
16
+ }
17
+
18
+ let routes = [];
19
+ let serverIslands = [];
20
+ let clientRoutes = [];
21
+
22
+ if (existsSync(pagesDir)) {
23
+ routes = await discoverRoutes(pagesDir);
24
+
25
+ for (const route of routes) {
26
+ const sourceCode = await Bun.file(route.path).text();
27
+ const isServerIsland = sourceCode.includes('export const render = "server"');
28
+
29
+ if (isServerIsland) {
30
+ serverIslands.push(route);
31
+ logger.success(`🏝️ Server Island: ${route.route}`);
32
+ } else {
33
+ clientRoutes.push(route);
34
+ }
35
+ }
36
+ }
37
+
38
+ await compileBuildDirectory(srcDir, buildDir, root, envVars);
39
+
40
+ if (routes.length > 0) {
41
+ await generateBuildRouter(routes, buildDir);
42
+ }
43
+
44
+ return { routes, serverIslands, clientRoutes };
45
+ }
@@ -0,0 +1,46 @@
1
+ // bertui/src/build/compiler/route-discoverer.js
2
+ import { join, extname } from 'path';
3
+ import { readdirSync, statSync } from 'fs';
4
+
5
+ export async function discoverRoutes(pagesDir) {
6
+ const routes = [];
7
+
8
+ async function scanDirectory(dir, basePath = '') {
9
+ const entries = readdirSync(dir, { withFileTypes: true });
10
+
11
+ for (const entry of entries) {
12
+ const fullPath = join(dir, entry.name);
13
+ const relativePath = join(basePath, entry.name);
14
+
15
+ if (entry.isDirectory()) {
16
+ await scanDirectory(fullPath, relativePath);
17
+ } else if (entry.isFile()) {
18
+ const ext = extname(entry.name);
19
+ if (ext === '.css') continue;
20
+
21
+ if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
22
+ const fileName = entry.name.replace(ext, '');
23
+ let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
24
+
25
+ if (fileName === 'index') {
26
+ route = route.replace('/index', '') || '/';
27
+ }
28
+
29
+ const isDynamic = fileName.includes('[') && fileName.includes(']');
30
+
31
+ routes.push({
32
+ route: route === '' ? '/' : route,
33
+ file: relativePath.replace(/\\/g, '/'),
34
+ path: fullPath,
35
+ type: isDynamic ? 'dynamic' : 'static'
36
+ });
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ await scanDirectory(pagesDir);
43
+ routes.sort((a, b) => a.type === b.type ? a.route.localeCompare(b.route) : a.type === 'static' ? -1 : 1);
44
+
45
+ return routes;
46
+ }
@@ -0,0 +1,104 @@
1
+ // bertui/src/build/compiler/router-generator.js
2
+ import { join } from 'path';
3
+
4
+ export async function generateBuildRouter(routes, buildDir) {
5
+ const imports = routes.map((route, i) => {
6
+ const componentName = `Page${i}`;
7
+ const 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
+ const routerCode = `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
+ for (const route of routes) {
39
+ if (route.type === 'static' && route.path === pathname) {
40
+ setCurrentRoute(route);
41
+ setParams({});
42
+ return;
43
+ }
44
+ }
45
+ for (const route of routes) {
46
+ if (route.type === 'dynamic') {
47
+ const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
48
+ const regex = new RegExp('^' + pattern + '$');
49
+ const match = pathname.match(regex);
50
+ if (match) {
51
+ const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
52
+ const extractedParams = {};
53
+ paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
54
+ setCurrentRoute(route);
55
+ setParams(extractedParams);
56
+ return;
57
+ }
58
+ }
59
+ }
60
+ setCurrentRoute(null);
61
+ setParams({});
62
+ }
63
+
64
+ function navigate(path) {
65
+ window.history.pushState({}, '', path);
66
+ matchAndSetRoute(path);
67
+ }
68
+
69
+ const Component = currentRoute?.component;
70
+ return React.createElement(
71
+ RouterContext.Provider,
72
+ { value: { currentRoute, params, navigate, pathname: window.location.pathname } },
73
+ Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
74
+ );
75
+ }
76
+
77
+ export function Link({ to, children, ...props }) {
78
+ const { navigate } = useRouter();
79
+ return React.createElement('a', {
80
+ href: to,
81
+ onClick: (e) => { e.preventDefault(); navigate(to); },
82
+ ...props
83
+ }, children);
84
+ }
85
+
86
+ function NotFound() {
87
+ return React.createElement('div', {
88
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
89
+ justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
90
+ },
91
+ React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
92
+ React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
93
+ React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
94
+ );
95
+ }
96
+
97
+ ${imports}
98
+
99
+ export const routes = [
100
+ ${routeConfigs}
101
+ ];`;
102
+
103
+ await Bun.write(join(buildDir, 'router.js'), routerCode);
104
+ }
@@ -0,0 +1,259 @@
1
+ // bertui/src/build/generators/html-generator.js
2
+ import { join, relative } from 'path';
3
+ import { mkdirSync } from 'fs';
4
+ import logger from '../../logger/logger.js';
5
+ import { extractMetaFromSource } from '../../utils/meta-extractor.js';
6
+
7
+ export async function generateProductionHTML(root, outDir, buildResult, routes, serverIslands, config) {
8
+ const mainBundle = buildResult.outputs.find(o =>
9
+ o.path.includes('main') && o.kind === 'entry-point'
10
+ );
11
+
12
+ if (!mainBundle) {
13
+ logger.error('❌ Could not find main bundle');
14
+ return;
15
+ }
16
+
17
+ const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
18
+ const defaultMeta = config.meta || {};
19
+
20
+ logger.info(`📄 Generating HTML for ${routes.length} routes...`);
21
+
22
+ // Process in batches to avoid Bun crashes
23
+ const BATCH_SIZE = 5;
24
+
25
+ for (let i = 0; i < routes.length; i += BATCH_SIZE) {
26
+ const batch = routes.slice(i, i + BATCH_SIZE);
27
+ logger.debug(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(routes.length/BATCH_SIZE)}`);
28
+
29
+ // Process batch sequentially
30
+ for (const route of batch) {
31
+ await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir);
32
+ }
33
+ }
34
+
35
+ logger.success(`✅ HTML generation complete for ${routes.length} routes`);
36
+ }
37
+
38
+ async function processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir) {
39
+ try {
40
+ const sourceCode = await Bun.file(route.path).text();
41
+ const pageMeta = extractMetaFromSource(sourceCode);
42
+ const meta = { ...defaultMeta, ...pageMeta };
43
+
44
+ const isServerIsland = serverIslands.find(si => si.route === route.route);
45
+
46
+ let staticHTML = '';
47
+
48
+ if (isServerIsland) {
49
+ logger.info(`🏝️ Extracting static content: ${route.route}`);
50
+ staticHTML = await extractStaticHTMLFromComponent(sourceCode, route.path);
51
+
52
+ if (staticHTML) {
53
+ logger.success(`✅ Server Island rendered: ${route.route}`);
54
+ } else {
55
+ logger.warn(`⚠️ Could not extract HTML, falling back to client-only`);
56
+ }
57
+ }
58
+
59
+ const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland);
60
+
61
+ let htmlPath;
62
+ if (route.route === '/') {
63
+ htmlPath = join(outDir, 'index.html');
64
+ } else {
65
+ const routeDir = join(outDir, route.route.replace(/^\//, ''));
66
+ mkdirSync(routeDir, { recursive: true });
67
+ htmlPath = join(routeDir, 'index.html');
68
+ }
69
+
70
+ await Bun.write(htmlPath, html);
71
+
72
+ if (isServerIsland) {
73
+ logger.success(`✅ Server Island: ${route.route} (instant content!)`);
74
+ } else {
75
+ logger.success(`✅ Client-only: ${route.route}`);
76
+ }
77
+
78
+ } catch (error) {
79
+ logger.error(`Failed HTML for ${route.route}: ${error.message}`);
80
+ }
81
+ }
82
+
83
+ async function extractStaticHTMLFromComponent(sourceCode, filePath) {
84
+ try {
85
+ const returnMatch = sourceCode.match(/return\s*\(/);
86
+ if (!returnMatch) {
87
+ logger.warn(`⚠️ Could not find return statement in ${filePath}`);
88
+ return null;
89
+ }
90
+
91
+ const codeBeforeReturn = sourceCode.substring(0, returnMatch.index);
92
+ const jsxContent = sourceCode.substring(returnMatch.index);
93
+
94
+ const hookPatterns = [
95
+ 'useState', 'useEffect', 'useContext', 'useReducer',
96
+ 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
97
+ 'useLayoutEffect', 'useDebugValue'
98
+ ];
99
+
100
+ let hasHooks = false;
101
+ for (const hook of hookPatterns) {
102
+ const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
103
+ if (regex.test(codeBeforeReturn)) {
104
+ logger.error(`❌ Server Island at ${filePath} contains React hooks!`);
105
+ logger.error(` Server Islands must be pure HTML - no ${hook}, etc.`);
106
+ hasHooks = true;
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (hasHooks) return null;
112
+
113
+ const importLines = codeBeforeReturn.split('\n')
114
+ .filter(line => line.trim().startsWith('import'))
115
+ .join('\n');
116
+
117
+ const hasRouterImport = /from\s+['"]bertui\/router['"]/m.test(importLines);
118
+
119
+ if (hasRouterImport) {
120
+ logger.error(`❌ Server Island at ${filePath} imports from 'bertui/router'!`);
121
+ logger.error(` Server Islands cannot use Link - use <a> tags instead.`);
122
+ return null;
123
+ }
124
+
125
+ const eventHandlers = [
126
+ 'onClick=', 'onChange=', 'onSubmit=', 'onInput=', 'onFocus=',
127
+ 'onBlur=', 'onMouseEnter=', 'onMouseLeave=', 'onKeyDown=',
128
+ 'onKeyUp=', 'onScroll='
129
+ ];
130
+
131
+ for (const handler of eventHandlers) {
132
+ if (jsxContent.includes(handler)) {
133
+ logger.error(`❌ Server Island uses event handler: ${handler.replace('=', '')}`);
134
+ logger.error(` Server Islands are static HTML - no interactivity allowed`);
135
+ return null;
136
+ }
137
+ }
138
+
139
+ const fullReturnMatch = sourceCode.match(/return\s*\(([\s\S]*?)\);?\s*}/);
140
+ if (!fullReturnMatch) {
141
+ logger.warn(`⚠️ Could not extract JSX from ${filePath}`);
142
+ return null;
143
+ }
144
+
145
+ let html = fullReturnMatch[1].trim();
146
+
147
+ html = html.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
148
+ html = html.replace(/className=/g, 'class=');
149
+
150
+ html = html.replace(/style=\{\{([^}]+)\}\}/g, (match, styleObj) => {
151
+ const props = [];
152
+ let currentProp = '';
153
+ let depth = 0;
154
+
155
+ for (let i = 0; i < styleObj.length; i++) {
156
+ const char = styleObj[i];
157
+ if (char === '(') depth++;
158
+ if (char === ')') depth--;
159
+
160
+ if (char === ',' && depth === 0) {
161
+ props.push(currentProp.trim());
162
+ currentProp = '';
163
+ } else {
164
+ currentProp += char;
165
+ }
166
+ }
167
+ if (currentProp.trim()) props.push(currentProp.trim());
168
+
169
+ const cssString = props
170
+ .map(prop => {
171
+ const colonIndex = prop.indexOf(':');
172
+ if (colonIndex === -1) return '';
173
+
174
+ const key = prop.substring(0, colonIndex).trim();
175
+ const value = prop.substring(colonIndex + 1).trim();
176
+
177
+ if (!key || !value) return '';
178
+
179
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
180
+ const cssValue = value.replace(/['"]/g, '');
181
+
182
+ return `${cssKey}: ${cssValue}`;
183
+ })
184
+ .filter(Boolean)
185
+ .join('; ');
186
+
187
+ return `style="${cssString}"`;
188
+ });
189
+
190
+ const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
191
+ 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
192
+
193
+ html = html.replace(/<(\w+)([^>]*)\s*\/>/g, (match, tag, attrs) => {
194
+ if (voidElements.includes(tag.toLowerCase())) {
195
+ return match;
196
+ } else {
197
+ return `<${tag}${attrs}></${tag}>`;
198
+ }
199
+ });
200
+
201
+ html = html.replace(/\{`([^`]*)`\}/g, '$1');
202
+ html = html.replace(/\{(['"])(.*?)\1\}/g, '$2');
203
+ html = html.replace(/\{(\d+)\}/g, '$1');
204
+
205
+ logger.info(` Extracted ${html.length} chars of static HTML`);
206
+ return html;
207
+
208
+ } catch (error) {
209
+ logger.error(`Failed to extract HTML: ${error.message}`);
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false) {
215
+ const rootContent = staticHTML
216
+ ? `<div id="root">${staticHTML}</div>`
217
+ : '<div id="root"></div>';
218
+
219
+ const comment = isServerIsland
220
+ ? '<!-- 🏝️ Server Island: Static content rendered at build time -->'
221
+ : '<!-- ⚡ Client-only: Content rendered by JavaScript -->';
222
+
223
+ return `<!DOCTYPE html>
224
+ <html lang="${meta.lang || 'en'}">
225
+ <head>
226
+ <meta charset="UTF-8">
227
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
228
+ <title>${meta.title || 'BertUI App'}</title>
229
+
230
+ <meta name="description" content="${meta.description || 'Built with BertUI'}">
231
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
232
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
233
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
234
+
235
+ <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
236
+ <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
237
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
238
+
239
+ <link rel="stylesheet" href="/styles/bertui.min.css">
240
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
241
+
242
+ <script type="importmap">
243
+ {
244
+ "imports": {
245
+ "react": "https://esm.sh/react@18.2.0",
246
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
247
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
248
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
249
+ }
250
+ }
251
+ </script>
252
+ </head>
253
+ <body>
254
+ ${comment}
255
+ ${rootContent}
256
+ <script type="module" src="/${bundlePath}"></script>
257
+ </body>
258
+ </html>`;
259
+ }