bertui 1.2.6 → 1.2.8

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,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
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",
@@ -187,11 +187,16 @@ function _usesJSX(code) {
187
187
  }
188
188
 
189
189
  function _removeCSSImports(code) {
190
+ // Replace CSS module imports with a Proxy so styles.foo = 'foo' at runtime
191
+ code = code.replace(
192
+ /import\s+(\w+)\s+from\s+['"][^'"]*\.module\.css['"];?\s*/g,
193
+ (_, varName) => `const ${varName} = new Proxy({}, { get: (_, k) => k });\n`
194
+ );
195
+ // Strip plain CSS imports entirely
190
196
  code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
191
197
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
192
198
  return code;
193
199
  }
194
-
195
200
  function _fixBuildImports(code, srcPath, outPath, root) {
196
201
  const buildDir = join(root, '.bertuibuild');
197
202
  const routerPath = join(buildDir, 'router.js');
@@ -1,4 +1,4 @@
1
- // bertui/src/build/compiler/index.js - WITH IMPORTHOW + NODE MODULE SUPPORT
1
+ // bertui/src/build/compiler/index.js
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
@@ -6,14 +6,8 @@ import { discoverRoutes } from './route-discoverer.js';
6
6
  import { compileBuildDirectory } from './file-transpiler.js';
7
7
  import { generateBuildRouter } from './router-generator.js';
8
8
 
9
- /**
10
- * @param {string} root
11
- * @param {string} buildDir
12
- * @param {Object} envVars
13
- * @param {Object} config - full bertui config (includes importhow)
14
- */
15
9
  export async function compileForBuild(root, buildDir, envVars, config = {}) {
16
- const srcDir = join(root, 'src');
10
+ const srcDir = join(root, 'src');
17
11
  const pagesDir = join(srcDir, 'pages');
18
12
 
19
13
  if (!existsSync(srcDir)) {
@@ -21,33 +15,17 @@ export async function compileForBuild(root, buildDir, envVars, config = {}) {
21
15
  }
22
16
 
23
17
  const importhow = config.importhow || {};
24
-
25
- let routes = [];
26
- let serverIslands = [];
27
- let clientRoutes = [];
18
+ let routes = [];
28
19
 
29
20
  if (existsSync(pagesDir)) {
30
21
  routes = await discoverRoutes(pagesDir);
31
-
32
- for (const route of routes) {
33
- const sourceCode = await Bun.file(route.path).text();
34
- const isServerIsland = sourceCode.includes('export const render = "server"');
35
-
36
- if (isServerIsland) {
37
- serverIslands.push(route);
38
- logger.success(`🏝️ Server Island: ${route.route}`);
39
- } else {
40
- clientRoutes.push(route);
41
- }
42
- }
43
22
  }
44
23
 
45
- // Pass importhow so alias dirs also get compiled
46
24
  await compileBuildDirectory(srcDir, buildDir, root, envVars, importhow);
47
25
 
48
26
  if (routes.length > 0) {
49
27
  await generateBuildRouter(routes, buildDir);
50
28
  }
51
29
 
52
- return { routes, serverIslands, clientRoutes };
30
+ return { routes };
53
31
  }
@@ -7,12 +7,12 @@ export async function generateBuildRouter(routes, buildDir) {
7
7
  const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
8
8
  return `import ${componentName} from '${importPath}';`;
9
9
  }).join('\n');
10
-
10
+
11
11
  const routeConfigs = routes.map((route, i) => {
12
12
  const componentName = `Page${i}`;
13
13
  return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
14
14
  }).join(',\n');
15
-
15
+
16
16
  const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
17
17
 
18
18
  const RouterContext = createContext(null);
@@ -76,16 +76,16 @@ export function Router({ routes }) {
76
76
 
77
77
  export function Link({ to, children, ...props }) {
78
78
  const { navigate } = useRouter();
79
- return React.createElement('a', {
80
- href: to,
81
- onClick: (e) => { e.preventDefault(); navigate(to); },
82
- ...props
79
+ return React.createElement('a', {
80
+ href: to,
81
+ onClick: (e) => { e.preventDefault(); navigate(to); },
82
+ ...props
83
83
  }, children);
84
84
  }
85
85
 
86
86
  function NotFound() {
87
87
  return React.createElement('div', {
88
- style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
88
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
89
89
  justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
90
90
  },
91
91
  React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
@@ -98,7 +98,8 @@ ${imports}
98
98
 
99
99
  export const routes = [
100
100
  ${routeConfigs}
101
- ];`;
102
-
101
+ ];
102
+ `;
103
+
103
104
  await Bun.write(join(buildDir, 'router.js'), routerCode);
104
105
  }
@@ -1,78 +1,58 @@
1
- // bertui/src/build/generators/html-generator.js - FIXED CSS BUILD
1
+ // bertui/src/build/generators/html-generator.js
2
2
  import { join, relative } from 'path';
3
3
  import { mkdirSync, existsSync, cpSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
5
5
  import { extractMetaFromSource } from '../../utils/meta-extractor.js';
6
6
 
7
- export async function generateProductionHTML(root, outDir, buildResult, routes, serverIslands, config) {
8
- const mainBundle = buildResult.outputs.find(o =>
7
+ export async function generateProductionHTML(root, outDir, buildResult, routes, config) {
8
+ const mainBundle = buildResult.outputs.find(o =>
9
9
  o.path.includes('main') && o.kind === 'entry-point'
10
10
  );
11
-
11
+
12
12
  if (!mainBundle) {
13
- logger.error('Could not find main bundle');
13
+ logger.error('Could not find main bundle');
14
14
  return;
15
15
  }
16
-
16
+
17
17
  const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
18
18
  const defaultMeta = config.meta || {};
19
-
20
19
  const bertuiPackages = await copyBertuiPackagesToProduction(root, outDir);
21
-
22
- logger.info(`📄 Generating HTML for ${routes.length} routes...`);
23
-
24
- const BATCH_SIZE = 5;
25
-
26
- for (let i = 0; i < routes.length; i += BATCH_SIZE) {
27
- const batch = routes.slice(i, i + BATCH_SIZE);
28
- logger.debug(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(routes.length/BATCH_SIZE)}`);
29
-
30
- for (const route of batch) {
31
- await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiPackages);
32
- }
20
+
21
+ logger.info(`Generating HTML for ${routes.length} routes...`);
22
+
23
+ for (const route of routes) {
24
+ await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
33
25
  }
34
-
35
- logger.success(`✅ HTML generation complete for ${routes.length} routes`);
26
+
27
+ logger.success(`HTML generation complete for ${routes.length} routes`);
36
28
  }
37
29
 
38
30
  async function copyBertuiPackagesToProduction(root, outDir) {
39
31
  const nodeModulesDir = join(root, 'node_modules');
40
- const packages = {
41
- bertuiIcons: false,
42
- bertuiAnimate: false,
43
- elysiaEden: false
44
- };
45
-
46
- if (!existsSync(nodeModulesDir)) {
47
- logger.debug('node_modules not found, skipping package copy');
48
- return packages;
49
- }
50
-
51
- // Copy bertui-icons
32
+ const packages = { bertuiIcons: false, bertuiAnimate: false, elysiaEden: false };
33
+
34
+ if (!existsSync(nodeModulesDir)) return packages;
35
+
52
36
  const bertuiIconsSource = join(nodeModulesDir, 'bertui-icons');
53
37
  if (existsSync(bertuiIconsSource)) {
54
38
  try {
55
39
  const bertuiIconsDest = join(outDir, 'node_modules', 'bertui-icons');
56
40
  mkdirSync(join(outDir, 'node_modules'), { recursive: true });
57
41
  cpSync(bertuiIconsSource, bertuiIconsDest, { recursive: true });
58
- logger.success('✅ Copied bertui-icons to dist/node_modules/');
59
42
  packages.bertuiIcons = true;
60
43
  } catch (error) {
61
44
  logger.error(`Failed to copy bertui-icons: ${error.message}`);
62
45
  }
63
46
  }
64
-
65
- // Copy bertui-animate CSS files
47
+
66
48
  const bertuiAnimateSource = join(nodeModulesDir, 'bertui-animate', 'dist');
67
49
  if (existsSync(bertuiAnimateSource)) {
68
50
  try {
69
51
  const bertuiAnimateDest = join(outDir, 'css');
70
52
  mkdirSync(bertuiAnimateDest, { recursive: true });
71
-
72
53
  const minCSSPath = join(bertuiAnimateSource, 'bertui-animate.min.css');
73
54
  if (existsSync(minCSSPath)) {
74
55
  cpSync(minCSSPath, join(bertuiAnimateDest, 'bertui-animate.min.css'));
75
- logger.success('✅ Copied bertui-animate.min.css to dist/css/');
76
56
  packages.bertuiAnimate = true;
77
57
  }
78
58
  } catch (error) {
@@ -80,46 +60,29 @@ async function copyBertuiPackagesToProduction(root, outDir) {
80
60
  }
81
61
  }
82
62
 
83
- // Copy @elysiajs/eden
84
63
  const elysiaEdenSource = join(nodeModulesDir, '@elysiajs', 'eden');
85
64
  if (existsSync(elysiaEdenSource)) {
86
65
  try {
87
66
  const elysiaEdenDest = join(outDir, 'node_modules', '@elysiajs', 'eden');
88
67
  mkdirSync(join(outDir, 'node_modules', '@elysiajs'), { recursive: true });
89
68
  cpSync(elysiaEdenSource, elysiaEdenDest, { recursive: true });
90
- logger.success('✅ Copied @elysiajs/eden to dist/node_modules/');
91
69
  packages.elysiaEden = true;
92
70
  } catch (error) {
93
71
  logger.error(`Failed to copy @elysiajs/eden: ${error.message}`);
94
72
  }
95
73
  }
96
-
74
+
97
75
  return packages;
98
76
  }
99
77
 
100
- async function processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
78
+ async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
101
79
  try {
102
80
  const sourceCode = await Bun.file(route.path).text();
103
81
  const pageMeta = extractMetaFromSource(sourceCode);
104
82
  const meta = { ...defaultMeta, ...pageMeta };
105
-
106
- const isServerIsland = serverIslands.find(si => si.route === route.route);
107
-
108
- let staticHTML = '';
109
-
110
- if (isServerIsland) {
111
- logger.info(`🏝️ Extracting static content: ${route.route}`);
112
- staticHTML = await extractStaticHTMLFromComponent(sourceCode, route.path);
113
-
114
- if (staticHTML) {
115
- logger.success(`✅ Server Island rendered: ${route.route}`);
116
- } else {
117
- logger.warn(`⚠️ Could not extract HTML, falling back to client-only`);
118
- }
119
- }
120
-
121
- const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland, bertuiPackages);
122
-
83
+
84
+ const html = generateHTML(meta, bundlePath, bertuiPackages);
85
+
123
86
  let htmlPath;
124
87
  if (route.route === '/') {
125
88
  htmlPath = join(outDir, 'index.html');
@@ -128,210 +91,60 @@ async function processSingleRoute(route, serverIslands, config, defaultMeta, bun
128
91
  mkdirSync(routeDir, { recursive: true });
129
92
  htmlPath = join(routeDir, 'index.html');
130
93
  }
131
-
94
+
132
95
  await Bun.write(htmlPath, html);
133
-
134
- if (isServerIsland) {
135
- logger.success(`✅ Server Island: ${route.route} (instant content!)`);
136
- } else {
137
- logger.success(`✅ Client-only: ${route.route}`);
138
- }
139
-
140
- } catch (error) {
141
- logger.error(`Failed HTML for ${route.route}: ${error.message}`);
142
- }
143
- }
96
+ logger.success(`${route.route}`);
144
97
 
145
- async function extractStaticHTMLFromComponent(sourceCode, filePath) {
146
- try {
147
- const returnMatch = sourceCode.match(/return\s*\(/);
148
- if (!returnMatch) {
149
- logger.warn(`⚠️ Could not find return statement in ${filePath}`);
150
- return null;
151
- }
152
-
153
- const codeBeforeReturn = sourceCode.substring(0, returnMatch.index);
154
- const jsxContent = sourceCode.substring(returnMatch.index);
155
-
156
- const hookPatterns = [
157
- 'useState', 'useEffect', 'useContext', 'useReducer',
158
- 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
159
- 'useLayoutEffect', 'useDebugValue'
160
- ];
161
-
162
- let hasHooks = false;
163
- for (const hook of hookPatterns) {
164
- const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
165
- if (regex.test(codeBeforeReturn)) {
166
- logger.error(`❌ Server Island at ${filePath} contains React hooks!`);
167
- logger.error(` Server Islands must be pure HTML - no ${hook}, etc.`);
168
- hasHooks = true;
169
- break;
170
- }
171
- }
172
-
173
- if (hasHooks) return null;
174
-
175
- const importLines = codeBeforeReturn.split('\n')
176
- .filter(line => line.trim().startsWith('import'))
177
- .join('\n');
178
-
179
- const hasRouterImport = /from\s+['"]bertui\/router['"]/m.test(importLines);
180
-
181
- if (hasRouterImport) {
182
- logger.error(`❌ Server Island at ${filePath} imports from 'bertui/router'!`);
183
- logger.error(` Server Islands cannot use Link - use <a> tags instead.`);
184
- return null;
185
- }
186
-
187
- const eventHandlers = [
188
- 'onClick=', 'onChange=', 'onSubmit=', 'onInput=', 'onFocus=',
189
- 'onBlur=', 'onMouseEnter=', 'onMouseLeave=', 'onKeyDown=',
190
- 'onKeyUp=', 'onScroll='
191
- ];
192
-
193
- for (const handler of eventHandlers) {
194
- if (jsxContent.includes(handler)) {
195
- logger.error(`❌ Server Island uses event handler: ${handler.replace('=', '')}`);
196
- logger.error(` Server Islands are static HTML - no interactivity allowed`);
197
- return null;
198
- }
199
- }
200
-
201
- const fullReturnMatch = sourceCode.match(/return\s*\(([\s\S]*?)\);?\s*}/);
202
- if (!fullReturnMatch) {
203
- logger.warn(`⚠️ Could not extract JSX from ${filePath}`);
204
- return null;
205
- }
206
-
207
- let html = fullReturnMatch[1].trim();
208
-
209
- html = html.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
210
- html = html.replace(/className=/g, 'class=');
211
-
212
- html = html.replace(/style=\{\{([^}]+)\}\}/g, (match, styleObj) => {
213
- const props = [];
214
- let currentProp = '';
215
- let depth = 0;
216
-
217
- for (let i = 0; i < styleObj.length; i++) {
218
- const char = styleObj[i];
219
- if (char === '(') depth++;
220
- if (char === ')') depth--;
221
-
222
- if (char === ',' && depth === 0) {
223
- props.push(currentProp.trim());
224
- currentProp = '';
225
- } else {
226
- currentProp += char;
227
- }
228
- }
229
- if (currentProp.trim()) props.push(currentProp.trim());
230
-
231
- const cssString = props
232
- .map(prop => {
233
- const colonIndex = prop.indexOf(':');
234
- if (colonIndex === -1) return '';
235
-
236
- const key = prop.substring(0, colonIndex).trim();
237
- const value = prop.substring(colonIndex + 1).trim();
238
-
239
- if (!key || !value) return '';
240
-
241
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
242
- const cssValue = value.replace(/['"]/g, '');
243
-
244
- return `${cssKey}: ${cssValue}`;
245
- })
246
- .filter(Boolean)
247
- .join('; ');
248
-
249
- return `style="${cssString}"`;
250
- });
251
-
252
- const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
253
- 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
254
-
255
- html = html.replace(/<(\w+)([^>]*)\s*\/>/g, (match, tag, attrs) => {
256
- if (voidElements.includes(tag.toLowerCase())) {
257
- return match;
258
- } else {
259
- return `<${tag}${attrs}></${tag}>`;
260
- }
261
- });
262
-
263
- html = html.replace(/\{`([^`]*)`\}/g, '$1');
264
- html = html.replace(/\{(['"])(.*?)\1\}/g, '$2');
265
- html = html.replace(/\{(\d+)\}/g, '$1');
266
-
267
- logger.info(` Extracted ${html.length} chars of static HTML`);
268
- return html;
269
-
270
98
  } catch (error) {
271
- logger.error(`Failed to extract HTML: ${error.message}`);
272
- return null;
99
+ logger.error(`Failed HTML for ${route.route}: ${error.message}`);
273
100
  }
274
101
  }
275
102
 
276
- function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false, bertuiPackages = {}) {
277
- const rootContent = staticHTML
278
- ? `<div id="root">${staticHTML}</div>`
279
- : '<div id="root"></div>';
280
-
281
- const comment = isServerIsland
282
- ? '<!-- 🏝️ Server Island: Static content rendered at build time -->'
283
- : '<!-- ⚡ Client-only: Content rendered by JavaScript -->';
284
-
285
- const bertuiIconsImport = bertuiPackages.bertuiIcons
103
+ function generateHTML(meta, bundlePath, bertuiPackages = {}) {
104
+ const bertuiIconsImport = bertuiPackages.bertuiIcons
286
105
  ? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
287
106
  : '';
288
-
107
+
289
108
  const bertuiAnimateCSS = bertuiPackages.bertuiAnimate
290
109
  ? ' <link rel="stylesheet" href="/css/bertui-animate.min.css">'
291
110
  : '';
292
111
 
293
- // ✅ NEW: @elysiajs/eden local import map
294
112
  const elysiaEdenImport = bertuiPackages.elysiaEden
295
113
  ? ',\n "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"'
296
114
  : '';
297
-
115
+
298
116
  return `<!DOCTYPE html>
299
117
  <html lang="${meta.lang || 'en'}">
300
118
  <head>
301
119
  <meta charset="UTF-8">
302
120
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
303
121
  <title>${meta.title || 'BertUI App'}</title>
304
-
305
122
  <meta name="description" content="${meta.description || 'Built with BertUI'}">
306
123
  ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
307
124
  ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
308
125
  ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
309
-
310
126
  <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
311
127
  <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
312
128
  ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
313
-
314
129
  <link rel="stylesheet" href="/styles/bertui.min.css">
315
130
  ${bertuiAnimateCSS}
316
131
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
317
-
318
132
  <script type="importmap">
319
133
  {
320
134
  "imports": {
321
- 'react': 'https://esm.sh/react@18.2.0',
322
- 'react-dom': 'https://esm.sh/react-dom@18.2.0',
323
- 'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
324
- 'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
325
- 'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0/jsx-dev-runtime', // ADD THIS
326
- '@bunnyx/api': '/bunnyx-api/api-client.js',
327
- '@elysiajs/eden': '/node_modules/@elysiajs/eden/dist/index.mjs',
135
+ "react": "https://esm.sh/react@18.2.0",
136
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
137
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
138
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
139
+ "react/jsx-dev-runtime": "https://esm.sh/react@18.2.0/jsx-dev-runtime",
140
+ "@bunnyx/api": "/bunnyx-api/api-client.js",
141
+ "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"${bertuiIconsImport}${elysiaEdenImport}
328
142
  }
329
143
  }
330
144
  </script>
331
145
  </head>
332
146
  <body>
333
- ${comment}
334
- ${rootContent}
147
+ <div id="root"></div>
335
148
  <script type="module" src="/${bundlePath}"></script>
336
149
  </body>
337
150
  </html>`;
@@ -1,156 +1,12 @@
1
1
  // bertui/src/build/server-island-validator.js
2
- // Fixed validation for Server Islands - no false positives!
2
+ // Server Islands removed all pages are client-rendered.
3
3
 
4
- import logger from '../logger/logger.js';
5
-
6
- /**
7
- * Validates that a Server Island component follows all rules
8
- * @param {string} sourceCode - The component source code
9
- * @param {string} filePath - Path to the file (for error messages)
10
- * @returns {{ valid: boolean, errors: string[] }}
11
- */
12
4
  export function validateServerIsland(sourceCode, filePath) {
13
- const errors = [];
14
-
15
- // Rule 1: No React hooks (FIXED: only match actual function calls)
16
- const hookPatterns = [
17
- 'useState',
18
- 'useEffect',
19
- 'useContext',
20
- 'useReducer',
21
- 'useCallback',
22
- 'useMemo',
23
- 'useRef',
24
- 'useImperativeHandle',
25
- 'useLayoutEffect',
26
- 'useDebugValue',
27
- 'useId',
28
- 'useDeferredValue',
29
- 'useTransition',
30
- 'useSyncExternalStore'
31
- ];
32
-
33
- for (const hook of hookPatterns) {
34
- // FIXED: Only match hooks as function calls, not in text/comments
35
- // Looks for: useState( or const [x] = useState(
36
- const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
37
- if (regex.test(sourceCode)) {
38
- errors.push(`❌ Uses React hook: ${hook}`);
39
- }
40
- }
41
-
42
- // Rule 2: No bertui/router imports
43
- if (sourceCode.includes('from \'bertui/router\'') ||
44
- sourceCode.includes('from "bertui/router"')) {
45
- errors.push('❌ Imports from \'bertui/router\' (use <a> tags instead of Link)');
46
- }
47
-
48
- // Rule 3: No browser APIs (FIXED: only match actual usage, not in strings/comments)
49
- const browserAPIs = [
50
- { pattern: 'window\\.', name: 'window' },
51
- { pattern: 'document\\.', name: 'document' },
52
- { pattern: 'localStorage\\.', name: 'localStorage' },
53
- { pattern: 'sessionStorage\\.', name: 'sessionStorage' },
54
- { pattern: 'navigator\\.', name: 'navigator' },
55
- { pattern: 'location\\.', name: 'location' },
56
- { pattern: 'history\\.', name: 'history' },
57
- { pattern: '(?<!//.*|/\\*.*|\\*)\\bfetch\\s*\\(', name: 'fetch' },
58
- { pattern: '\\.addEventListener\\s*\\(', name: 'addEventListener' },
59
- { pattern: '\\.removeEventListener\\s*\\(', name: 'removeEventListener' },
60
- { pattern: '\\bsetTimeout\\s*\\(', name: 'setTimeout' },
61
- { pattern: '\\bsetInterval\\s*\\(', name: 'setInterval' },
62
- { pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' }
63
- ];
64
-
65
- for (const api of browserAPIs) {
66
- const regex = new RegExp(api.pattern, 'g');
67
- if (regex.test(sourceCode)) {
68
- if (api.name === 'console') {
69
- logger.warn(`⚠️ ${filePath} uses console.log (will not work in static HTML)`);
70
- } else {
71
- errors.push(`❌ Uses browser API: ${api.name}`);
72
- }
73
- }
74
- }
75
-
76
- // Rule 4: No event handlers (these won't work without JS)
77
- const eventHandlers = [
78
- 'onClick=',
79
- 'onChange=',
80
- 'onSubmit=',
81
- 'onInput=',
82
- 'onFocus=',
83
- 'onBlur=',
84
- 'onMouseEnter=',
85
- 'onMouseLeave=',
86
- 'onKeyDown=',
87
- 'onKeyUp=',
88
- 'onScroll='
89
- ];
90
-
91
- for (const handler of eventHandlers) {
92
- if (sourceCode.includes(handler)) {
93
- errors.push(`❌ Uses event handler: ${handler.replace('=', '')} (Server Islands are static HTML)`);
94
- }
95
- }
96
-
97
- // Rule 5: Check for dynamic imports
98
- if (/import\s*\(/.test(sourceCode)) {
99
- errors.push('❌ Uses dynamic import() (not supported in Server Islands)');
100
- }
101
-
102
- // Rule 6: Check for async/await (usually indicates API calls)
103
- if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(sourceCode)) {
104
- errors.push('❌ Uses async/await (Server Islands must be synchronous)');
105
- }
106
-
107
- const valid = errors.length === 0;
108
-
109
- return { valid, errors };
5
+ return { valid: true, errors: [] };
110
6
  }
111
7
 
112
- /**
113
- * Display validation errors in a clear format
114
- */
115
- export function displayValidationErrors(filePath, errors) {
116
- logger.error(`\n🏝️ Server Island validation failed: ${filePath}`);
117
- logger.error('\nViolations:');
118
- errors.forEach(error => logger.error(` ${error}`));
119
- logger.error('\n📖 Server Island Rules:');
120
- logger.error(' ✅ Pure static JSX only');
121
- logger.error(' ❌ No React hooks (useState, useEffect, etc.)');
122
- logger.error(' ❌ No Link component (use <a> tags)');
123
- logger.error(' ❌ No browser APIs (window, document, fetch)');
124
- logger.error(' ❌ No event handlers (onClick, onChange, etc.)');
125
- logger.error('\n💡 Tip: Remove the "export const render = \\"server\\"" line');
126
- logger.error(' if you need these features (page will be client-only).\n');
127
- }
8
+ export function displayValidationErrors(filePath, errors) {}
128
9
 
129
- /**
130
- * Extract and validate all Server Islands in a project
131
- */
132
10
  export async function validateAllServerIslands(routes) {
133
- const serverIslands = [];
134
- const validationResults = [];
135
-
136
- for (const route of routes) {
137
- const sourceCode = await Bun.file(route.path).text();
138
- const isServerIsland = sourceCode.includes('export const render = "server"');
139
-
140
- if (isServerIsland) {
141
- const validation = validateServerIsland(sourceCode, route.path);
142
-
143
- validationResults.push({
144
- route: route.route,
145
- path: route.path,
146
- ...validation
147
- });
148
-
149
- if (validation.valid) {
150
- serverIslands.push(route);
151
- }
152
- }
153
- }
154
-
155
- return { serverIslands, validationResults };
11
+ return { serverIslands: [], validationResults: [] };
156
12
  }
package/src/build.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // bertui/src/build.js
2
2
  import { join } from 'path';
3
- import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from 'fs';
3
+ import { existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
5
  import { loadEnvVariables } from './utils/env.js';
6
6
  import { globalCache } from './utils/cache.js';
@@ -13,9 +13,8 @@ import { generateSitemap } from './build/generators/sitemap-generator.js';
13
13
  import { generateRobots } from './build/generators/robots-generator.js';
14
14
  import { compileLayouts } from './layouts/index.js';
15
15
  import { compileLoadingComponents } from './loading/index.js';
16
- import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
16
+ import { analyzeRoutes } from './hydration/index.js';
17
17
  import { analyzeBuild } from './analyzer/index.js';
18
- import { buildAliasMap } from './utils/importhow.js';
19
18
 
20
19
  const TOTAL_STEPS = 10;
21
20
 
@@ -46,8 +45,8 @@ export async function buildProduction(options = {}) {
46
45
 
47
46
  // ── Step 2: Compile ──────────────────────────────────────────────────────
48
47
  logger.step(2, TOTAL_STEPS, 'Compiling');
49
- const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars, config);
50
- logger.stepDone('Compiling', `${routes.length} routes · ${serverIslands.length} islands`);
48
+ const { routes } = await compileForBuild(root, buildDir, envVars, config);
49
+ logger.stepDone('Compiling', `${routes.length} routes`);
51
50
 
52
51
  // ── Step 3: Layouts ──────────────────────────────────────────────────────
53
52
  logger.step(3, TOTAL_STEPS, 'Layouts');
@@ -78,15 +77,16 @@ export async function buildProduction(options = {}) {
78
77
  // ── Step 8: Bundle JS ────────────────────────────────────────────────────
79
78
  logger.step(8, TOTAL_STEPS, 'Bundling JS');
80
79
  const buildEntry = join(buildDir, 'main.js');
81
- const routerPath = join(buildDir, 'router.js');
82
- if (!existsSync(buildEntry)) throw new Error('main.js not found in build dir');
83
- const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config);
80
+ if (!existsSync(buildEntry)) {
81
+ throw new Error('main.js not found in build dir — make sure src/main.jsx exists');
82
+ }
83
+ const result = await bundleJavaScript(buildEntry, outDir, envVars, buildDir, root, config);
84
84
  totalKB = (result.outputs.reduce((a, o) => a + (o.size || 0), 0) / 1024).toFixed(1);
85
85
  logger.stepDone('Bundling JS', `${totalKB} KB · tree-shaken`);
86
86
 
87
87
  // ── Step 9: HTML ─────────────────────────────────────────────────────────
88
88
  logger.step(9, TOTAL_STEPS, 'Generating HTML');
89
- await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
89
+ await generateProductionHTML(root, outDir, result, routes, config);
90
90
  logger.stepDone('Generating HTML', `${routes.length} pages`);
91
91
 
92
92
  // ── Step 10: Sitemap + robots ────────────────────────────────────────────
@@ -97,23 +97,25 @@ export async function buildProduction(options = {}) {
97
97
 
98
98
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
99
99
 
100
- // Fire-and-forget — don't let the report generator block process exit
101
- analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') }).catch(() => {});
100
+ try {
101
+ await analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') });
102
+ } catch (reportErr) {
103
+ logger.debug(`Bundle report generation skipped: ${reportErr.message}`);
104
+ }
102
105
 
103
- // ── Summary ──────────────────────────────────────────────────────────────
104
106
  logger.printSummary({
105
- routes: routes.length,
106
- serverIslands: serverIslands.length,
107
- interactive: analyzedRoutes.interactive.length,
108
- staticRoutes: analyzedRoutes.static.length,
109
- jsSize: `${totalKB} KB`,
110
- outDir: 'dist/',
107
+ routes: routes.length,
108
+ interactive: analyzedRoutes.interactive.length,
109
+ staticRoutes: analyzedRoutes.static.length,
110
+ jsSize: `${totalKB} KB`,
111
+ outDir: 'dist/',
111
112
  });
112
113
 
114
+ logger.cleanup();
113
115
  return { success: true };
114
116
 
115
117
  } catch (error) {
116
- logger.stepFail('Build', error.message);
118
+ logger.stepFail('Build', error?.message || String(error));
117
119
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
118
120
  throw error;
119
121
  }
@@ -155,6 +157,7 @@ async function generateProductionImportMap(root, config) {
155
157
  }
156
158
 
157
159
  async function copyNodeModulesToDist(root, outDir, importMap) {
160
+ const { mkdirSync } = await import('fs');
158
161
  const dest = join(outDir, 'assets', 'node_modules');
159
162
  mkdirSync(dest, { recursive: true });
160
163
  const src = join(root, 'node_modules');
@@ -173,54 +176,72 @@ async function copyNodeModulesToDist(root, outDir, importMap) {
173
176
  }
174
177
  }
175
178
 
176
- async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
179
+ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, root, config) {
177
180
  const originalCwd = process.cwd();
178
181
  process.chdir(buildDir);
179
182
 
180
183
  try {
181
- const entrypoints = [buildEntry];
182
- if (existsSync(routerPath)) entrypoints.push(routerPath);
183
-
184
184
  const importMap = await generateProductionImportMap(root, config);
185
185
  await Bun.write(join(outDir, 'import-map.json'), JSON.stringify({ imports: importMap }, null, 2));
186
186
  await copyNodeModulesToDist(root, outDir, importMap);
187
187
 
188
- // Copy @bunnyx/api client to dist so the importmap entry resolves
189
188
  const bunnyxSrc = join(root, 'bunnyx-api', 'api-client.js');
190
189
  if (existsSync(bunnyxSrc)) {
190
+ const { mkdirSync } = await import('fs');
191
191
  mkdirSync(join(outDir, 'bunnyx-api'), { recursive: true });
192
192
  await Bun.write(join(outDir, 'bunnyx-api', 'api-client.js'), Bun.file(bunnyxSrc));
193
193
  }
194
194
 
195
- const result = await Bun.build({
196
- entrypoints,
197
- outdir: join(outDir, 'assets'),
198
- target: 'browser',
199
- format: 'esm',
200
- minify: {
201
- whitespace: true,
202
- syntax: true,
203
- identifiers: true,
204
- },
205
- splitting: true,
206
- sourcemap: 'external',
207
- metafile: true,
208
- naming: {
209
- entry: 'js/[name]-[hash].js',
210
- chunk: 'js/chunks/[name]-[hash].js',
211
- asset: 'assets/[name]-[hash].[ext]',
195
+ const cssModulePlugin = {
196
+ name: 'css-modules',
197
+ setup(build) {
198
+ build.onLoad({ filter: /\.module\.css$/ }, () => ({
199
+ contents: 'export default new Proxy({}, { get: (_, k) => k });',
200
+ loader: 'js',
201
+ }));
202
+ build.onLoad({ filter: /\.css$/ }, () => ({
203
+ contents: '',
204
+ loader: 'js',
205
+ }));
212
206
  },
213
- external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@bunnyx/api'],
214
- define: {
215
- 'process.env.NODE_ENV': '"production"',
216
- ...Object.fromEntries(
217
- Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
218
- ),
219
- },
220
- });
207
+ };
208
+
209
+ let result;
210
+ try {
211
+ result = await Bun.build({
212
+ entrypoints: [buildEntry],
213
+ outdir: join(outDir, 'assets'),
214
+ target: 'browser',
215
+ format: 'esm',
216
+ plugins: [cssModulePlugin],
217
+ minify: {
218
+ whitespace: true,
219
+ syntax: true,
220
+ identifiers: true,
221
+ },
222
+ splitting: true,
223
+ sourcemap: 'external',
224
+ metafile: true,
225
+ naming: {
226
+ entry: 'js/[name]-[hash].js',
227
+ chunk: 'js/chunks/[name]-[hash].js',
228
+ asset: 'assets/[name]-[hash].[ext]',
229
+ },
230
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@bunnyx/api'],
231
+ define: {
232
+ 'process.env.NODE_ENV': '"production"',
233
+ ...Object.fromEntries(
234
+ Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
235
+ ),
236
+ },
237
+ });
238
+ } catch (err) {
239
+ throw new Error(`Bun.build failed: ${err?.message || String(err)}`);
240
+ }
221
241
 
222
242
  if (!result.success) {
223
- throw new Error(`Bundle failed\n${result.logs?.map(l => l.message).join('\n') || 'Unknown error'}`);
243
+ const msgs = (result.logs || []).map(l => l?.message || l?.text || JSON.stringify(l)).join('\n');
244
+ throw new Error(`Bundle failed\n${msgs || 'Check your imports for .jsx extensions or unresolvable paths'}`);
224
245
  }
225
246
 
226
247
  if (result.metafile) {
@@ -237,9 +258,9 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
237
258
  export async function build(options = {}) {
238
259
  try {
239
260
  await buildProduction(options);
261
+ process.exit(0);
240
262
  } catch (error) {
241
- console.error(error);
263
+ console.error('Build error:', error?.message || String(error));
242
264
  process.exit(1);
243
265
  }
244
- process.exit(0);
245
266
  }
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { createWriteStream } from 'fs';
5
5
  import { join } from 'path';
6
+ import { mkdirSync, existsSync } from 'fs';
6
7
 
7
8
  // ── ANSI helpers ─────────────────────────────────────────────────────────────
8
9
  const C = {
@@ -121,12 +122,10 @@ export function fileProgress(current, total, filename) {
121
122
 
122
123
  // ── Simple log levels (used internally, suppressed in compact mode) ───────────
123
124
  export function info(msg) {
124
- // In compact mode swallow routine info — only pass through to debug log
125
125
  _debugLog('INFO', msg);
126
126
  }
127
127
 
128
128
  export function success(msg) {
129
- // Swallow — stepDone() is the visual replacement
130
129
  _debugLog('SUCCESS', msg);
131
130
  }
132
131
 
@@ -185,6 +184,9 @@ export function bigLog(title, opts = {}) {
185
184
  // ── Build/Dev summary ─────────────────────────────────────────────────────────
186
185
  export function printSummary(stats = {}) {
187
186
  _stopSpinner();
187
+ // Close the log stream before printing summary
188
+ _closeLogStream();
189
+
188
190
  process.stdout.write('\n');
189
191
 
190
192
  const dur = _startTime ? `${((Date.now() - _startTime) / 1000).toFixed(2)}s` : '';
@@ -268,17 +270,38 @@ let _logStream = null;
268
270
  function _debugLog(level, msg) {
269
271
  if (!_logStream) {
270
272
  try {
273
+ const logDir = join(process.cwd(), '.bertui');
274
+ if (!existsSync(logDir)) {
275
+ mkdirSync(logDir, { recursive: true });
276
+ }
277
+
271
278
  _logStream = createWriteStream(
272
- join(process.cwd(), '.bertui', 'dev.log'),
279
+ join(logDir, 'dev.log'),
273
280
  { flags: 'a' }
274
281
  );
275
- } catch { return; }
282
+ } catch (err) {
283
+ return;
284
+ }
276
285
  }
277
286
  const ts = new Date().toISOString().substring(11, 23);
278
287
  _logStream.write(`[${ts}] [${level}] ${msg}\n`);
279
288
  }
280
289
 
281
- // ── Default export (matches existing logger.method() call sites) ──────────────
290
+ // NEW: Function to close the log stream
291
+ function _closeLogStream() {
292
+ if (_logStream) {
293
+ _logStream.end();
294
+ _logStream = null;
295
+ }
296
+ }
297
+
298
+ // NEW: Cleanup function to be called when done
299
+ export function cleanup() {
300
+ _stopSpinner();
301
+ _closeLogStream();
302
+ }
303
+
304
+ // ── Default export ────────────────────────────────────────────────────────────
282
305
  export default {
283
306
  printHeader,
284
307
  step,
@@ -293,4 +316,5 @@ export default {
293
316
  table,
294
317
  bigLog,
295
318
  printSummary,
319
+ cleanup, // Export cleanup function
296
320
  };
package/src/serve.js CHANGED
@@ -42,10 +42,8 @@ export async function startPreviewServer(options = {}) {
42
42
  process.exit(1);
43
43
  }
44
44
 
45
- logger.bigLog(`🚀 PREVIEW SERVER`, { color: 'green' });
46
- logger.info(`📁 Serving: ${publicPath}`);
47
- logger.info(`🌐 URL: http://localhost:${port}`);
48
- logger.info(`⚡ Press Ctrl+C to stop`);
45
+ console.log(`\n 🚀 Preview running at http://localhost:${port}`);
46
+ console.log(` Press Ctrl+C to stop\n`);
49
47
 
50
48
  // Track connections for graceful shutdown
51
49
  const connections = new Set();
@@ -72,13 +72,17 @@ export async function buildDevImportMap(root) {
72
72
 
73
73
  logger.info('🔄 Rebuilding dev import map (new packages detected)...');
74
74
 
75
- 'react': 'https://esm.sh/react@18.2.0',
76
- 'react-dom': 'https://esm.sh/react-dom@18.2.0',
77
- 'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
78
- 'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
79
- 'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0/jsx-dev-runtime', // ADD THIS
80
- '@bunnyx/api': '/bunnyx-api/api-client.js',
81
- '@elysiajs/eden': '/node_modules/@elysiajs/eden/dist/index.mjs',
75
+ // Initialize importMap with default mappings
76
+ const importMap = {
77
+ 'react': 'https://esm.sh/react@18.2.0',
78
+ 'react-dom': 'https://esm.sh/react-dom@18.2.0',
79
+ 'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
80
+ 'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
81
+ 'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0/jsx-dev-runtime',
82
+ '@bunnyx/api': '/bunnyx-api/api-client.js',
83
+ '@elysiajs/eden': '/node_modules/@elysiajs/eden/dist/index.mjs',
84
+ };
85
+
82
86
  const SKIP = new Set(['react', 'react-dom', '.bin', '.cache', '.package-lock.json', '.yarn']);
83
87
 
84
88
  if (existsSync(nodeModulesDir)) {