bertui 1.2.6 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
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');
@@ -14,6 +14,7 @@ export async function generateBuildRouter(routes, buildDir) {
14
14
  }).join(',\n');
15
15
 
16
16
  const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
17
+ import { createRoot } from 'react-dom/client';
17
18
 
18
19
  const RouterContext = createContext(null);
19
20
 
@@ -98,7 +99,19 @@ ${imports}
98
99
 
99
100
  export const routes = [
100
101
  ${routeConfigs}
101
- ];`;
102
+ ];
103
+
104
+ // Guard against double-mount. router.js is imported by main.js — if it ever
105
+ // gets evaluated more than once (e.g. duplicate script tags, HMR quirks),
106
+ // this ensures React never calls createRoot on the same container twice,
107
+ // which causes "Node.removeChild: The node to be removed is not a child" crashes.
108
+ if (!window.__BERTUI_MOUNTED__) {
109
+ window.__BERTUI_MOUNTED__ = true;
110
+ const container = document.getElementById('root');
111
+ const app = React.createElement(Router, { routes });
112
+ createRoot(container).render(app);
113
+ }
114
+ `;
102
115
 
103
116
  await Bun.write(join(buildDir, 'router.js'), routerCode);
104
117
  }
@@ -1,54 +1,45 @@
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, buildDir, buildResult, routes, serverIslands, 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
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
-
19
+
20
20
  const bertuiPackages = await copyBertuiPackagesToProduction(root, outDir);
21
-
21
+
22
22
  logger.info(`📄 Generating HTML for ${routes.length} routes...`);
23
-
23
+
24
24
  const BATCH_SIZE = 5;
25
-
25
+
26
26
  for (let i = 0; i < routes.length; i += BATCH_SIZE) {
27
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
-
28
+ logger.debug(`Processing batch ${Math.floor(i / BATCH_SIZE) + 1}/${Math.ceil(routes.length / BATCH_SIZE)}`);
30
29
  for (const route of batch) {
31
- await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiPackages);
30
+ await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
32
31
  }
33
32
  }
34
-
33
+
35
34
  logger.success(`✅ HTML generation complete for ${routes.length} routes`);
36
35
  }
37
36
 
38
37
  async function copyBertuiPackagesToProduction(root, outDir) {
39
38
  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
39
+ const packages = { bertuiIcons: false, bertuiAnimate: false, elysiaEden: false };
40
+
41
+ if (!existsSync(nodeModulesDir)) return packages;
42
+
52
43
  const bertuiIconsSource = join(nodeModulesDir, 'bertui-icons');
53
44
  if (existsSync(bertuiIconsSource)) {
54
45
  try {
@@ -61,14 +52,12 @@ async function copyBertuiPackagesToProduction(root, outDir) {
61
52
  logger.error(`Failed to copy bertui-icons: ${error.message}`);
62
53
  }
63
54
  }
64
-
65
- // Copy bertui-animate CSS files
55
+
66
56
  const bertuiAnimateSource = join(nodeModulesDir, 'bertui-animate', 'dist');
67
57
  if (existsSync(bertuiAnimateSource)) {
68
58
  try {
69
59
  const bertuiAnimateDest = join(outDir, 'css');
70
60
  mkdirSync(bertuiAnimateDest, { recursive: true });
71
-
72
61
  const minCSSPath = join(bertuiAnimateSource, 'bertui-animate.min.css');
73
62
  if (existsSync(minCSSPath)) {
74
63
  cpSync(minCSSPath, join(bertuiAnimateDest, 'bertui-animate.min.css'));
@@ -80,7 +69,6 @@ async function copyBertuiPackagesToProduction(root, outDir) {
80
69
  }
81
70
  }
82
71
 
83
- // Copy @elysiajs/eden
84
72
  const elysiaEdenSource = join(nodeModulesDir, '@elysiajs', 'eden');
85
73
  if (existsSync(elysiaEdenSource)) {
86
74
  try {
@@ -93,33 +81,18 @@ async function copyBertuiPackagesToProduction(root, outDir) {
93
81
  logger.error(`Failed to copy @elysiajs/eden: ${error.message}`);
94
82
  }
95
83
  }
96
-
84
+
97
85
  return packages;
98
86
  }
99
87
 
100
- async function processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
88
+ async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
101
89
  try {
102
90
  const sourceCode = await Bun.file(route.path).text();
103
91
  const pageMeta = extractMetaFromSource(sourceCode);
104
92
  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
-
93
+
94
+ const html = generateHTML(meta, bundlePath, bertuiPackages);
95
+
123
96
  let htmlPath;
124
97
  if (route.route === '/') {
125
98
  htmlPath = join(outDir, 'index.html');
@@ -128,210 +101,62 @@ async function processSingleRoute(route, serverIslands, config, defaultMeta, bun
128
101
  mkdirSync(routeDir, { recursive: true });
129
102
  htmlPath = join(routeDir, 'index.html');
130
103
  }
131
-
104
+
132
105
  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
- }
106
+ logger.success(`✅ ${route.route}`);
144
107
 
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
108
  } catch (error) {
271
- logger.error(`Failed to extract HTML: ${error.message}`);
272
- return null;
109
+ logger.error(`Failed HTML for ${route.route}: ${error.message}`);
110
+ console.error(error);
273
111
  }
274
112
  }
275
113
 
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
114
+ function generateHTML(meta, bundlePath, bertuiPackages = {}) {
115
+ const bertuiIconsImport = bertuiPackages.bertuiIcons
286
116
  ? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
287
117
  : '';
288
-
118
+
289
119
  const bertuiAnimateCSS = bertuiPackages.bertuiAnimate
290
120
  ? ' <link rel="stylesheet" href="/css/bertui-animate.min.css">'
291
121
  : '';
292
122
 
293
- // ✅ NEW: @elysiajs/eden local import map
294
123
  const elysiaEdenImport = bertuiPackages.elysiaEden
295
124
  ? ',\n "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"'
296
125
  : '';
297
-
126
+
298
127
  return `<!DOCTYPE html>
299
128
  <html lang="${meta.lang || 'en'}">
300
129
  <head>
301
130
  <meta charset="UTF-8">
302
131
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
303
132
  <title>${meta.title || 'BertUI App'}</title>
304
-
305
133
  <meta name="description" content="${meta.description || 'Built with BertUI'}">
306
134
  ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
307
135
  ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
308
136
  ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
309
-
310
137
  <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
311
138
  <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
312
139
  ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
313
-
314
140
  <link rel="stylesheet" href="/styles/bertui.min.css">
315
141
  ${bertuiAnimateCSS}
316
142
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
317
-
318
143
  <script type="importmap">
319
144
  {
320
145
  "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',
146
+ "react": "https://esm.sh/react@18.2.0",
147
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
148
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
149
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
150
+ "react/jsx-dev-runtime": "https://esm.sh/react@18.2.0/jsx-dev-runtime",
151
+ "@bunnyx/api": "/bunnyx-api/api-client.js",
152
+ "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"${bertuiIconsImport}${elysiaEdenImport}
328
153
  }
329
154
  }
330
155
  </script>
156
+ <script>window.__BERTUI_HYDRATE__ = false;</script>
331
157
  </head>
332
158
  <body>
333
- ${comment}
334
- ${rootContent}
159
+ <div id="root"></div>
335
160
  <script type="module" src="/${bundlePath}"></script>
336
161
  </body>
337
162
  </html>`;
@@ -12,30 +12,52 @@ import logger from '../logger/logger.js';
12
12
  export function validateServerIsland(sourceCode, filePath) {
13
13
  const errors = [];
14
14
 
15
- // Rule 1: No React hooks (FIXED: only match actual function calls)
15
+ // SUPER AGGRESSIVE STRIPPING: Remove EVERYTHING that could be a false positive
16
+
17
+ // First, remove all JSX prop values that contain code examples
18
+ let cleanedCode = sourceCode
19
+ // Remove the entire content of <Code> components (most common culprit)
20
+ .replace(/<Code[^>]*>[\s\S]*?<\/Code>/g, '')
21
+ // Remove the entire content of <InlineCode> components
22
+ .replace(/<InlineCode[^>]*>[\s\S]*?<\/InlineCode>/g, '')
23
+ // Remove any JSX expression that looks like it contains code
24
+ .replace(/\{`[\s\S]*?`\}/g, '{}')
25
+ .replace(/\{[\s\S]*?import[\s\S]*?\}/g, '{}')
26
+ .replace(/\{[\s\S]*?useState[\s\S]*?\}/g, '{}')
27
+ .replace(/\{[\s\S]*?useEffect[\s\S]*?\}/g, '{}')
28
+ .replace(/\{[\s\S]*?fetch\([\s\S]*?\}/g, '{}');
29
+
30
+ // Then strip all string literals
31
+ cleanedCode = cleanedCode
32
+ .replace(/`[\s\S]*?`/g, '""')
33
+ .replace(/"(?:[^"\\]|\\.)*"/g, '""')
34
+ .replace(/'(?:[^'\\]|\\.)*'/g, "''")
35
+ // Remove comments
36
+ .replace(/\/\/.*$/gm, '')
37
+ .replace(/\/\*[\s\S]*?\*\//g, '');
38
+
39
+ // Rule 1: No React hooks (check the cleaned code only)
16
40
  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'
41
+ 'useState', 'useEffect', 'useContext', 'useReducer',
42
+ 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
43
+ 'useLayoutEffect', 'useDebugValue', 'useId', 'useDeferredValue',
44
+ 'useTransition', 'useSyncExternalStore'
31
45
  ];
32
46
 
33
47
  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(
48
+ // Look for the hook as a function call, but only in the cleaned code
36
49
  const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
37
- if (regex.test(sourceCode)) {
38
- errors.push(`❌ Uses React hook: ${hook}`);
50
+
51
+ // Also check that it's not preceded by "import" or part of a comment
52
+ const matches = cleanedCode.match(regex);
53
+ if (matches) {
54
+ // Verify this isn't in an import statement by checking context
55
+ const hookIndex = cleanedCode.indexOf(matches[0]);
56
+ const contextBefore = cleanedCode.substring(Math.max(0, hookIndex - 50), hookIndex);
57
+
58
+ if (!contextBefore.includes('import')) {
59
+ errors.push(`❌ Uses React hook: ${hook}`);
60
+ }
39
61
  }
40
62
  }
41
63
 
@@ -45,26 +67,27 @@ export function validateServerIsland(sourceCode, filePath) {
45
67
  errors.push('❌ Imports from \'bertui/router\' (use <a> tags instead of Link)');
46
68
  }
47
69
 
48
- // Rule 3: No browser APIs (FIXED: only match actual usage, not in strings/comments)
70
+ // Rule 3: No browser APIs (check cleaned code)
49
71
  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' },
72
+ { pattern: '\\bwindow\\.(?!location)', name: 'window' },
73
+ { pattern: '\\bdocument\\.', name: 'document' },
74
+ { pattern: '\\blocalStorage\\.', name: 'localStorage' },
75
+ { pattern: '\\bsessionStorage\\.', name: 'sessionStorage' },
76
+ { pattern: '\\bnavigator\\.', name: 'navigator' },
77
+ { pattern: '\\blocation\\.(?!href)', name: 'location' },
78
+ { pattern: '\\bhistory\\.', name: 'history' },
79
+ { pattern: '\\bfetch\\s*\\(', name: 'fetch' },
58
80
  { pattern: '\\.addEventListener\\s*\\(', name: 'addEventListener' },
59
81
  { pattern: '\\.removeEventListener\\s*\\(', name: 'removeEventListener' },
60
82
  { pattern: '\\bsetTimeout\\s*\\(', name: 'setTimeout' },
61
83
  { pattern: '\\bsetInterval\\s*\\(', name: 'setInterval' },
62
- { pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' }
84
+ { pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' },
85
+ { pattern: '\\bconsole\\.', name: 'console' }
63
86
  ];
64
87
 
65
88
  for (const api of browserAPIs) {
66
89
  const regex = new RegExp(api.pattern, 'g');
67
- if (regex.test(sourceCode)) {
90
+ if (regex.test(cleanedCode)) {
68
91
  if (api.name === 'console') {
69
92
  logger.warn(`⚠️ ${filePath} uses console.log (will not work in static HTML)`);
70
93
  } else {
@@ -73,34 +96,28 @@ export function validateServerIsland(sourceCode, filePath) {
73
96
  }
74
97
  }
75
98
 
76
- // Rule 4: No event handlers (these won't work without JS)
99
+ // Rule 4: No event handlers (check cleaned code)
77
100
  const eventHandlers = [
78
- 'onClick=',
79
- 'onChange=',
80
- 'onSubmit=',
81
- 'onInput=',
82
- 'onFocus=',
83
- 'onBlur=',
84
- 'onMouseEnter=',
85
- 'onMouseLeave=',
86
- 'onKeyDown=',
87
- 'onKeyUp=',
88
- 'onScroll='
101
+ 'onClick=', 'onChange=', 'onSubmit=', 'onInput=', 'onFocus=',
102
+ 'onBlur=', 'onMouseEnter=', 'onMouseLeave=', 'onKeyDown=',
103
+ 'onKeyUp=', 'onScroll='
89
104
  ];
90
105
 
91
106
  for (const handler of eventHandlers) {
92
- if (sourceCode.includes(handler)) {
107
+ const escapedHandler = handler.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
108
+ const regex = new RegExp(`\\b${escapedHandler}\\s*{`, 'g');
109
+ if (regex.test(cleanedCode)) {
93
110
  errors.push(`❌ Uses event handler: ${handler.replace('=', '')} (Server Islands are static HTML)`);
94
111
  }
95
112
  }
96
113
 
97
114
  // Rule 5: Check for dynamic imports
98
- if (/import\s*\(/.test(sourceCode)) {
115
+ if (/import\s*\(/.test(cleanedCode)) {
99
116
  errors.push('❌ Uses dynamic import() (not supported in Server Islands)');
100
117
  }
101
118
 
102
- // Rule 6: Check for async/await (usually indicates API calls)
103
- if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(sourceCode)) {
119
+ // Rule 6: Check for async/await
120
+ if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(cleanedCode)) {
104
121
  errors.push('❌ Uses async/await (Server Islands must be synchronous)');
105
122
  }
106
123
 
@@ -109,6 +126,8 @@ export function validateServerIsland(sourceCode, filePath) {
109
126
  return { valid, errors };
110
127
  }
111
128
 
129
+
130
+
112
131
  /**
113
132
  * Display validation errors in a clear format
114
133
  */
package/src/build.js CHANGED
@@ -78,15 +78,14 @@ export async function buildProduction(options = {}) {
78
78
  // ── Step 8: Bundle JS ────────────────────────────────────────────────────
79
79
  logger.step(8, TOTAL_STEPS, 'Bundling JS');
80
80
  const buildEntry = join(buildDir, 'main.js');
81
- const routerPath = join(buildDir, 'router.js');
82
81
  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);
82
+ const result = await bundleJavaScript(buildEntry, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config);
84
83
  totalKB = (result.outputs.reduce((a, o) => a + (o.size || 0), 0) / 1024).toFixed(1);
85
84
  logger.stepDone('Bundling JS', `${totalKB} KB · tree-shaken`);
86
85
 
87
86
  // ── Step 9: HTML ─────────────────────────────────────────────────────────
88
87
  logger.step(9, TOTAL_STEPS, 'Generating HTML');
89
- await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
88
+ await generateProductionHTML(root, outDir, buildDir, result, routes, serverIslands, config);
90
89
  logger.stepDone('Generating HTML', `${routes.length} pages`);
91
90
 
92
91
  // ── Step 10: Sitemap + robots ────────────────────────────────────────────
@@ -95,10 +94,15 @@ export async function buildProduction(options = {}) {
95
94
  await generateRobots(config, outDir, routes);
96
95
  logger.stepDone('Sitemap & robots');
97
96
 
97
+ // Delete build dir AFTER HTML generation
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
+ // Generate bundle report
101
+ try {
102
+ await analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') });
103
+ } catch (reportErr) {
104
+ logger.debug(`Bundle report generation skipped: ${reportErr.message}`);
105
+ }
102
106
 
103
107
  // ── Summary ──────────────────────────────────────────────────────────────
104
108
  logger.printSummary({
@@ -110,6 +114,8 @@ export async function buildProduction(options = {}) {
110
114
  outDir: 'dist/',
111
115
  });
112
116
 
117
+ logger.cleanup();
118
+
113
119
  return { success: true };
114
120
 
115
121
  } catch (error) {
@@ -173,13 +179,15 @@ async function copyNodeModulesToDist(root, outDir, importMap) {
173
179
  }
174
180
  }
175
181
 
176
- async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
182
+ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
177
183
  const originalCwd = process.cwd();
178
184
  process.chdir(buildDir);
179
185
 
180
186
  try {
187
+ // Only main.js as entrypoint — router.js is imported by main.js already.
188
+ // Adding router.js as a second entrypoint causes it to mount twice on
189
+ // the same #root which triggers "Node.removeChild" DOM crashes in React.
181
190
  const entrypoints = [buildEntry];
182
- if (existsSync(routerPath)) entrypoints.push(routerPath);
183
191
 
184
192
  const importMap = await generateProductionImportMap(root, config);
185
193
  await Bun.write(join(outDir, 'import-map.json'), JSON.stringify({ imports: importMap }, null, 2));
@@ -192,11 +200,26 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
192
200
  await Bun.write(join(outDir, 'bunnyx-api', 'api-client.js'), Bun.file(bunnyxSrc));
193
201
  }
194
202
 
203
+ const cssModulePlugin = {
204
+ name: 'css-modules',
205
+ setup(build) {
206
+ build.onLoad({ filter: /\.module\.css$/ }, () => ({
207
+ contents: 'export default new Proxy({}, { get: (_, k) => k });',
208
+ loader: 'js',
209
+ }));
210
+ build.onLoad({ filter: /\.css$/ }, () => ({
211
+ contents: '',
212
+ loader: 'js',
213
+ }));
214
+ },
215
+ };
216
+
195
217
  const result = await Bun.build({
196
218
  entrypoints,
197
219
  outdir: join(outDir, 'assets'),
198
220
  target: 'browser',
199
221
  format: 'esm',
222
+ plugins: [cssModulePlugin],
200
223
  minify: {
201
224
  whitespace: true,
202
225
  syntax: true,
@@ -237,9 +260,9 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
237
260
  export async function build(options = {}) {
238
261
  try {
239
262
  await buildProduction(options);
263
+ process.exit(0);
240
264
  } catch (error) {
241
265
  console.error(error);
242
266
  process.exit(1);
243
267
  }
244
- process.exit(0);
245
268
  }
@@ -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)) {