bertui 1.2.8 → 1.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,14 +2,13 @@
2
2
 
3
3
  **Zero-config React framework powered by Bun. File-based routing, Server Islands, and a build system that gets out of your way.**
4
4
 
5
- [![Version](https://img.shields.io/badge/version-1.2.2-blue)](https://www.npmjs.com/package/bertui)
5
+ [![Version](https://img.shields.io/badge/version-1.2.9-blue)](https://www.npmjs.com/package/bertui)
6
6
  [![Bun Powered](https://img.shields.io/badge/runtime-Bun-f472b6)](https://bun.sh)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
 
9
9
  ---
10
10
 
11
11
  ## Quick Start
12
-
13
12
  ```bash
14
13
  bunx create-bertui my-app
15
14
  cd my-app
@@ -18,11 +17,51 @@ bun run dev
18
17
 
19
18
  ---
20
19
 
20
+ ## What's New in v1.2.9
21
+
22
+ ### Server Islands — Rebuilt from the Ground Up
23
+
24
+ Server Islands are back and fully working. One export, zero config, pure HTML at build time.
25
+ ```jsx
26
+ // src/pages/about.jsx
27
+ export const render = "static"
28
+
29
+ export default function About() {
30
+ return (
31
+ <div>
32
+ <h1>About Us</h1>
33
+ <p>Rendered at build time. Zero JS in the browser.</p>
34
+ </div>
35
+ )
36
+ }
37
+ ```
38
+
39
+ BertUI runs `renderToString` on your page at build time and writes pure HTML to `dist/`. No React runtime ships to the browser. No hydration. Just HTML.
40
+
41
+ Three render modes, one export:
42
+ ```jsx
43
+ // Pure HTML — zero JS, zero React in the browser
44
+ export const render = "static"
45
+
46
+ // SSR HTML + JS bundle — pre-rendered for instant load, hydrated for interactivity
47
+ export const render = "server"
48
+
49
+ // Default — client-only React SPA (no export needed)
50
+ export default function Page() {}
51
+ ```
52
+
53
+ **Rules for static and server pages:**
54
+ - No React hooks (`useState`, `useEffect`, etc.)
55
+ - No event handlers (`onClick`, `onChange`, etc.)
56
+ - No browser APIs (`window`, `document`, `localStorage`)
57
+ - Violations are caught at build time with a clear error
58
+
59
+ ---
60
+
21
61
  ## What's New in v1.2.2
22
62
 
23
63
  ### Import Aliases (`importhow`)
24
64
  No more `../../../` chains. Define aliases in your config and import cleanly from anywhere.
25
-
26
65
  ```javascript
27
66
  // bertui.config.js
28
67
  export default {
@@ -32,7 +71,6 @@ export default {
32
71
  }
33
72
  }
34
73
  ```
35
-
36
74
  ```javascript
37
75
  // anywhere in your project
38
76
  import Button from 'amani/button';
@@ -43,7 +81,6 @@ Aliases are resolved at compile time — zero runtime overhead.
43
81
 
44
82
  ### Node Modules — Just Work
45
83
  Install a package, import it. That's it.
46
-
47
84
  ```javascript
48
85
  import { format } from 'date-fns';
49
86
  import confetti from 'canvas-confetti';
@@ -53,7 +90,6 @@ In dev, packages are served from your local filesystem. In production, only the
53
90
 
54
91
  ### CLI — New Look
55
92
  The build and dev output is now compact and step-based instead of verbose line-by-line logs.
56
-
57
93
  ```
58
94
  ██████╗ ███████╗██████╗ ████████╗██╗ ██╗██╗
59
95
  ██╔══██╗██╔════╝██╔══██╗╚══██╔══╝██║ ██║██║
@@ -64,7 +100,7 @@ The build and dev output is now compact and step-based instead of verbose line-b
64
100
  by Pease Ernest · BUILD
65
101
 
66
102
  [ 1/10] ✓ Loading env
67
- [ 2/10] ✓ Compiling 5 routes · 1 islands
103
+ [ 2/10] ✓ Compiling 5 routes
68
104
  [ 3/10] ⠸ Layouts ...
69
105
  ...
70
106
  ✓ Done 0.54s
@@ -82,7 +118,6 @@ Run `bun add some-package` and the dev server picks it up automatically. The imp
82
118
  ## Features
83
119
 
84
120
  ### File-Based Routing
85
-
86
121
  ```
87
122
  src/pages/index.jsx → /
88
123
  src/pages/about.jsx → /about
@@ -92,31 +127,45 @@ src/pages/blog/[slug].jsx → /blog/:slug
92
127
 
93
128
  ### Server Islands
94
129
 
95
- Add one line to opt a page into static generation at build time.
96
-
130
+ Three render modes. One export at the top of your page.
97
131
  ```jsx
98
- // src/pages/about.jsx
99
- export const render = "server";
100
- export const title = "About Us";
132
+ // render = "static" — pure HTML, zero JS
133
+ // Perfect for: marketing pages, blog posts, docs, any page without interactivity
134
+ export const render = "static"
135
+ export const title = "About Us"
101
136
 
102
137
  export default function About() {
103
138
  return (
104
139
  <div>
105
140
  <h1>About Us</h1>
106
- <p>Pre-rendered as static HTML at build time.</p>
141
+ <p>Pre-rendered at build time. Instant load, perfect SEO.</p>
107
142
  </div>
108
- );
143
+ )
144
+ }
145
+ ```
146
+ ```jsx
147
+ // render = "server" — SSR HTML in the body + JS bundle attached
148
+ // Perfect for: pages that need fast first paint AND interactivity after load
149
+ export const render = "server"
150
+
151
+ export default function Dashboard() {
152
+ return <div><h1>Dashboard</h1></div>
153
+ }
154
+ ```
155
+ ```jsx
156
+ // default — client-only React (no export needed)
157
+ // Perfect for: highly interactive pages, apps, anything with lots of state
158
+ export default function Editor() {
159
+ const [value, setValue] = useState('')
160
+ return <textarea onChange={e => setValue(e.target.value)} />
109
161
  }
110
162
  ```
111
163
 
112
- - Static HTML embedded in the output for instant load and perfect SEO
113
- - Pages with hooks or event handlers are rejected at build time with a clear error
114
- - All other pages are client-only by default
164
+ BertUI automatically detects the render mode and generates the right HTML for each page. Static pages get zero JS. Server pages get pre-rendered HTML with hydration. Default pages get the full SPA treatment.
115
165
 
116
166
  ### TypeScript
117
167
 
118
168
  `.tsx` and `.ts` files work with no setup. Mix them freely with `.jsx`.
119
-
120
169
  ```typescript
121
170
  // src/pages/blog/[slug].tsx
122
171
  import { useRouter } from 'bertui/router';
@@ -130,7 +179,6 @@ export default function BlogPost() {
130
179
  ### SEO
131
180
 
132
181
  `sitemap.xml` and `robots.txt` are generated automatically from your routes. Requires `baseUrl` in config.
133
-
134
182
  ```javascript
135
183
  export default {
136
184
  baseUrl: 'https://example.com',
@@ -148,7 +196,6 @@ Put your styles in `src/styles/`. They are combined and minified with LightningC
148
196
  ---
149
197
 
150
198
  ## Project Structure
151
-
152
199
  ```
153
200
  my-app/
154
201
  ├── src/
@@ -166,7 +213,6 @@ my-app/
166
213
  ---
167
214
 
168
215
  ## Configuration
169
-
170
216
  ```javascript
171
217
  // bertui.config.js
172
218
  export default {
@@ -209,6 +255,7 @@ Benchmarks on an Intel i3-2348M, 7.6GB RAM.
209
255
 
210
256
  - `bertui-elysia` — API routes, auth, database
211
257
  - `bertui-animate` — GPU-accelerated animations
258
+ - Partial hydration — `<Island>` component for mixed static/interactive pages
212
259
 
213
260
  ---
214
261
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
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",
@@ -1,117 +1,170 @@
1
1
  // bertui/src/build/generators/html-generator.js
2
- import { join, relative } from 'path';
3
- import { mkdirSync, existsSync, cpSync } from 'fs';
4
- import logger from '../../logger/logger.js';
5
- import { extractMetaFromSource } from '../../utils/meta-extractor.js';
2
+ import { join, relative } from 'path'
3
+ import { mkdirSync, existsSync, cpSync } from 'fs'
4
+ import logger from '../../logger/logger.js'
5
+ import { extractMetaFromSource } from '../../utils/meta-extractor.js'
6
+ import { renderPageToHTML, getPageRenderMode } from '../ssr-renderer.js'
6
7
 
7
- export async function generateProductionHTML(root, outDir, buildResult, routes, config) {
8
+ export async function generateProductionHTML(root, outDir, buildResult, routes, config, buildDir) {
8
9
  const mainBundle = buildResult.outputs.find(o =>
9
10
  o.path.includes('main') && o.kind === 'entry-point'
10
- );
11
+ )
11
12
 
12
13
  if (!mainBundle) {
13
- logger.error('Could not find main bundle');
14
- return;
14
+ logger.error('Could not find main bundle')
15
+ return
15
16
  }
16
17
 
17
- const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
18
- const defaultMeta = config.meta || {};
19
- const bertuiPackages = await copyBertuiPackagesToProduction(root, outDir);
18
+ const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/')
19
+ const defaultMeta = config.meta || {}
20
+ const bertuiPackages = await copyBertuiPackagesToProduction(root, outDir)
20
21
 
21
- logger.info(`Generating HTML for ${routes.length} routes...`);
22
+
23
+ logger.info(`Generating HTML for ${routes.length} routes...`)
22
24
 
23
25
  for (const route of routes) {
24
- await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
26
+ await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages, buildDir)
25
27
  }
26
28
 
27
- logger.success(`HTML generation complete for ${routes.length} routes`);
29
+ logger.success(`HTML generation complete for ${routes.length} routes`)
28
30
  }
29
31
 
30
- async function copyBertuiPackagesToProduction(root, outDir) {
31
- const nodeModulesDir = join(root, 'node_modules');
32
- const packages = { bertuiIcons: false, bertuiAnimate: false, elysiaEden: false };
32
+ async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages, buildDir) {
33
+ try {
34
+ const sourceCode = await Bun.file(route.path).text()
35
+ const pageMeta = extractMetaFromSource(sourceCode)
36
+ const meta = { ...defaultMeta, ...pageMeta }
33
37
 
34
- if (!existsSync(nodeModulesDir)) return packages;
38
+ // Determine render mode from source
39
+ const renderMode = await getPageRenderMode(route.path)
35
40
 
36
- const bertuiIconsSource = join(nodeModulesDir, 'bertui-icons');
37
- if (existsSync(bertuiIconsSource)) {
38
- try {
39
- const bertuiIconsDest = join(outDir, 'node_modules', 'bertui-icons');
40
- mkdirSync(join(outDir, 'node_modules'), { recursive: true });
41
- cpSync(bertuiIconsSource, bertuiIconsDest, { recursive: true });
42
- packages.bertuiIcons = true;
43
- } catch (error) {
44
- logger.error(`Failed to copy bertui-icons: ${error.message}`);
45
- }
46
- }
41
+ let html
47
42
 
48
- const bertuiAnimateSource = join(nodeModulesDir, 'bertui-animate', 'dist');
49
- if (existsSync(bertuiAnimateSource)) {
50
- try {
51
- const bertuiAnimateDest = join(outDir, 'css');
52
- mkdirSync(bertuiAnimateDest, { recursive: true });
53
- const minCSSPath = join(bertuiAnimateSource, 'bertui-animate.min.css');
54
- if (existsSync(minCSSPath)) {
55
- cpSync(minCSSPath, join(bertuiAnimateDest, 'bertui-animate.min.css'));
56
- packages.bertuiAnimate = true;
57
- }
58
- } catch (error) {
59
- logger.error(`Failed to copy bertui-animate: ${error.message}`);
60
- }
61
- }
43
+ if (renderMode === 'server' || renderMode === 'static') {
44
+ // Find the compiled version of this page in buildDir
45
+ const compiledPath = findCompiledPath(route, buildDir)
62
46
 
63
- const elysiaEdenSource = join(nodeModulesDir, '@elysiajs', 'eden');
64
- if (existsSync(elysiaEdenSource)) {
65
- try {
66
- const elysiaEdenDest = join(outDir, 'node_modules', '@elysiajs', 'eden');
67
- mkdirSync(join(outDir, 'node_modules', '@elysiajs'), { recursive: true });
68
- cpSync(elysiaEdenSource, elysiaEdenDest, { recursive: true });
69
- packages.elysiaEden = true;
70
- } catch (error) {
71
- logger.error(`Failed to copy @elysiajs/eden: ${error.message}`);
72
- }
73
- }
74
-
75
- return packages;
76
- }
47
+ if (compiledPath && existsSync(compiledPath)) {
48
+ logger.info(` SSR rendering: ${route.route}`)
49
+ const ssrHTML = await renderPageToHTML(compiledPath, buildDir)
77
50
 
78
- async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
79
- try {
80
- const sourceCode = await Bun.file(route.path).text();
81
- const pageMeta = extractMetaFromSource(sourceCode);
82
- const meta = { ...defaultMeta, ...pageMeta };
83
-
84
- const html = generateHTML(meta, bundlePath, bertuiPackages);
51
+ if (ssrHTML) {
52
+ if (renderMode === 'static') {
53
+ // Pure static no JS at all
54
+ html = generateStaticHTML({ ssrHTML, meta, bertuiPackages })
55
+ } else {
56
+ // Server island — SSR HTML + JS bundle for hydration
57
+ html = generateServerIslandHTML({ ssrHTML, meta, bundlePath, bertuiPackages })
58
+ }
59
+ logger.success(` ✓ SSR: ${route.route} (${renderMode})`)
60
+ } else {
61
+ // SSR failed — fall back to client render with a warning
62
+ logger.warn(` SSR failed for ${route.route}, falling back to client render`)
63
+ html = generateClientHTML({ meta, bundlePath, bertuiPackages })
64
+ }
65
+ } else {
66
+ logger.warn(` Compiled path not found for ${route.route}, using client render`)
67
+ html = generateClientHTML({ meta, bundlePath, bertuiPackages })
68
+ }
69
+ } else {
70
+ // Default: client-only SPA
71
+ html = generateClientHTML({ meta, bundlePath, bertuiPackages })
72
+ }
85
73
 
86
- let htmlPath;
74
+ // Write to dist/
75
+ let htmlPath
87
76
  if (route.route === '/') {
88
- htmlPath = join(outDir, 'index.html');
77
+ htmlPath = join(outDir, 'index.html')
89
78
  } else {
90
- const routeDir = join(outDir, route.route.replace(/^\//, ''));
91
- mkdirSync(routeDir, { recursive: true });
92
- htmlPath = join(routeDir, 'index.html');
79
+ const routeDir = join(outDir, route.route.replace(/^\//, ''))
80
+ mkdirSync(routeDir, { recursive: true })
81
+ htmlPath = join(routeDir, 'index.html')
93
82
  }
94
83
 
95
- await Bun.write(htmlPath, html);
96
- logger.success(`${route.route}`);
84
+ await Bun.write(htmlPath, html)
85
+ logger.success(` ${route.route}`)
97
86
 
98
87
  } catch (error) {
99
- logger.error(`Failed HTML for ${route.route}: ${error.message}`);
88
+ logger.error(`Failed HTML for ${route.route}: ${error.message}`)
100
89
  }
101
90
  }
102
91
 
103
- function generateHTML(meta, bundlePath, bertuiPackages = {}) {
104
- const bertuiIconsImport = bertuiPackages.bertuiIcons
105
- ? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
106
- : '';
92
+ /**
93
+ * Find the compiled .js path for a route's source file
94
+ */
95
+ function findCompiledPath(route, buildDir) {
96
+ const compiledFile = route.file.replace(/\.(jsx|tsx|ts)$/, '.js')
97
+ return join(buildDir, 'pages', compiledFile)
98
+ }
99
+ // ─── HTML generators ──────────────────────────────────────────────────────────
107
100
 
101
+ /**
102
+ * render = "static" → pure HTML, zero JS
103
+ */
104
+ function generateStaticHTML({ ssrHTML, meta, bertuiPackages }) {
108
105
  const bertuiAnimateCSS = bertuiPackages.bertuiAnimate
109
- ? ' <link rel="stylesheet" href="/css/bertui-animate.min.css">'
110
- : '';
106
+ ? '<link rel="stylesheet" href="/css/bertui-animate.min.css">'
107
+ : ''
111
108
 
112
- const elysiaEdenImport = bertuiPackages.elysiaEden
113
- ? ',\n "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"'
114
- : '';
109
+ return `<!DOCTYPE html>
110
+ <html lang="${meta.lang || 'en'}">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>${meta.title || 'BertUI App'}</title>
115
+ <meta name="description" content="${meta.description || ''}">
116
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
117
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
118
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
119
+ <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
120
+ <meta property="og:description" content="${meta.ogDescription || meta.description || ''}">
121
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
122
+ <link rel="stylesheet" href="/styles/bertui.min.css">
123
+ ${bertuiAnimateCSS}
124
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
125
+ </head>
126
+ <body>
127
+ ${ssrHTML}
128
+ </body>
129
+ </html>`
130
+ }
131
+
132
+ /**
133
+ * render = "server" → SSR HTML + JS bundle for hydration
134
+ */
135
+ function generateServerIslandHTML({ ssrHTML, meta, bundlePath, bertuiPackages }) {
136
+ const { importMapScript, bertuiAnimateCSS } = buildSharedAssets(meta, bertuiPackages)
137
+
138
+ return `<!DOCTYPE html>
139
+ <html lang="${meta.lang || 'en'}">
140
+ <head>
141
+ <meta charset="UTF-8">
142
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
143
+ <title>${meta.title || 'BertUI App'}</title>
144
+ <meta name="description" content="${meta.description || ''}">
145
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
146
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
147
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
148
+ <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
149
+ <meta property="og:description" content="${meta.ogDescription || meta.description || ''}">
150
+ ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
151
+ <link rel="stylesheet" href="/styles/bertui.min.css">
152
+ ${bertuiAnimateCSS}
153
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg">
154
+ ${importMapScript}
155
+ </head>
156
+ <body>
157
+ <div id="root">${ssrHTML}</div>
158
+ <script type="module" src="/${bundlePath}"></script>
159
+ </body>
160
+ </html>`
161
+ }
162
+
163
+ /**
164
+ * default → client-only SPA (existing behavior)
165
+ */
166
+ function generateClientHTML({ meta, bundlePath, bertuiPackages }) {
167
+ const { importMapScript, bertuiAnimateCSS } = buildSharedAssets(meta, bertuiPackages)
115
168
 
116
169
  return `<!DOCTYPE html>
117
170
  <html lang="${meta.lang || 'en'}">
@@ -119,17 +172,39 @@ function generateHTML(meta, bundlePath, bertuiPackages = {}) {
119
172
  <meta charset="UTF-8">
120
173
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
121
174
  <title>${meta.title || 'BertUI App'}</title>
122
- <meta name="description" content="${meta.description || 'Built with BertUI'}">
123
- ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
124
- ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
125
- ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
175
+ <meta name="description" content="${meta.description || ''}">
176
+ ${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
177
+ ${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
178
+ ${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
126
179
  <meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
127
- <meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
180
+ <meta property="og:description" content="${meta.ogDescription || meta.description || ''}">
128
181
  ${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
129
182
  <link rel="stylesheet" href="/styles/bertui.min.css">
130
- ${bertuiAnimateCSS}
183
+ ${bertuiAnimateCSS}
131
184
  <link rel="icon" type="image/svg+xml" href="/favicon.svg">
132
- <script type="importmap">
185
+ ${importMapScript}
186
+ </head>
187
+ <body>
188
+ <div id="root"></div>
189
+ <script type="module" src="/${bundlePath}"></script>
190
+ </body>
191
+ </html>`
192
+ }
193
+
194
+ // ─── Shared helpers ───────────────────────────────────────────────────────────
195
+
196
+ function buildSharedAssets(meta, bertuiPackages) {
197
+ const bertuiIconsImport = bertuiPackages.bertuiIcons
198
+ ? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
199
+ : ''
200
+ const elysiaEdenImport = bertuiPackages.elysiaEden
201
+ ? ',\n "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"'
202
+ : ''
203
+ const bertuiAnimateCSS = bertuiPackages.bertuiAnimate
204
+ ? '<link rel="stylesheet" href="/css/bertui-animate.min.css">'
205
+ : ''
206
+
207
+ const importMapScript = `<script type="importmap">
133
208
  {
134
209
  "imports": {
135
210
  "react": "https://esm.sh/react@18.2.0",
@@ -137,15 +212,52 @@ ${bertuiAnimateCSS}
137
212
  "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
138
213
  "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
139
214
  "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}
215
+ "@bunnyx/api": "/bunnyx-api/api-client.js"${bertuiIconsImport}${elysiaEdenImport}
142
216
  }
143
217
  }
144
- </script>
145
- </head>
146
- <body>
147
- <div id="root"></div>
148
- <script type="module" src="/${bundlePath}"></script>
149
- </body>
150
- </html>`;
218
+ </script>`
219
+
220
+ return { importMapScript, bertuiAnimateCSS }
221
+ }
222
+
223
+ async function copyBertuiPackagesToProduction(root, outDir) {
224
+ const nodeModulesDir = join(root, 'node_modules')
225
+ const packages = { bertuiIcons: false, bertuiAnimate: false, elysiaEden: false }
226
+
227
+ if (!existsSync(nodeModulesDir)) return packages
228
+
229
+ const bertuiIconsSrc = join(nodeModulesDir, 'bertui-icons')
230
+ if (existsSync(bertuiIconsSrc)) {
231
+ try {
232
+ const dest = join(outDir, 'node_modules', 'bertui-icons')
233
+ mkdirSync(join(outDir, 'node_modules'), { recursive: true })
234
+ cpSync(bertuiIconsSrc, dest, { recursive: true })
235
+ packages.bertuiIcons = true
236
+ } catch {}
237
+ }
238
+
239
+ const bertuiAnimateSrc = join(nodeModulesDir, 'bertui-animate', 'dist')
240
+ if (existsSync(bertuiAnimateSrc)) {
241
+ try {
242
+ const dest = join(outDir, 'css')
243
+ mkdirSync(dest, { recursive: true })
244
+ const minCSS = join(bertuiAnimateSrc, 'bertui-animate.min.css')
245
+ if (existsSync(minCSS)) {
246
+ cpSync(minCSS, join(dest, 'bertui-animate.min.css'))
247
+ packages.bertuiAnimate = true
248
+ }
249
+ } catch {}
250
+ }
251
+
252
+ const elysiaEdenSrc = join(nodeModulesDir, '@elysiajs', 'eden')
253
+ if (existsSync(elysiaEdenSrc)) {
254
+ try {
255
+ const dest = join(outDir, 'node_modules', '@elysiajs', 'eden')
256
+ mkdirSync(join(outDir, 'node_modules', '@elysiajs'), { recursive: true })
257
+ cpSync(elysiaEdenSrc, dest, { recursive: true })
258
+ packages.elysiaEden = true
259
+ } catch {}
260
+ }
261
+
262
+ return packages
151
263
  }
@@ -1,12 +1,67 @@
1
1
  // bertui/src/build/server-island-validator.js
2
- // Server Islands removed — all pages are client-rendered.
2
+ import logger from '../logger/logger.js'
3
+
4
+ const BANNED_HOOKS = [
5
+ 'useState', 'useEffect', 'useReducer', 'useCallback', 'useMemo',
6
+ 'useRef', 'useContext', 'useLayoutEffect', 'useId',
7
+ 'useImperativeHandle', 'useDebugValue', 'useDeferredValue',
8
+ 'useTransition', 'useSyncExternalStore',
9
+ ]
10
+
11
+ const BANNED_EVENTS = [
12
+ 'onClick', 'onChange', 'onSubmit', 'onInput', 'onFocus',
13
+ 'onBlur', 'onMouseEnter', 'onMouseLeave', 'onKeyDown', 'onKeyUp',
14
+ ]
3
15
 
4
16
  export function validateServerIsland(sourceCode, filePath) {
5
- return { valid: true, errors: [] };
17
+ const errors = []
18
+
19
+ for (const hook of BANNED_HOOKS) {
20
+ if (new RegExp(`\\b${hook}\\s*\\(`).test(sourceCode)) {
21
+ errors.push(`Cannot use React hook "${hook}" in a static/server page — hooks need a browser runtime`)
22
+ }
23
+ }
24
+
25
+ for (const event of BANNED_EVENTS) {
26
+ if (sourceCode.includes(`${event}=`)) {
27
+ errors.push(`Cannot use event handler "${event}" in a static/server page — events need a browser runtime`)
28
+ }
29
+ }
30
+
31
+ if (/window\.|document\.|localStorage\.|sessionStorage\./.test(sourceCode)) {
32
+ errors.push('Cannot access browser APIs (window/document/localStorage) in a static/server page')
33
+ }
34
+
35
+ return { valid: errors.length === 0, errors }
6
36
  }
7
37
 
8
- export function displayValidationErrors(filePath, errors) {}
38
+ export function displayValidationErrors(filePath, errors) {
39
+ logger.error(`\n❌ Static/Server page validation failed: ${filePath}`)
40
+ for (const err of errors) {
41
+ logger.error(` · ${err}`)
42
+ }
43
+ logger.error(` → Remove the above or switch to default render mode\n`)
44
+ }
9
45
 
10
46
  export async function validateAllServerIslands(routes) {
11
- return { serverIslands: [], validationResults: [] };
47
+ const serverIslands = []
48
+ const validationResults = []
49
+
50
+ for (const route of routes) {
51
+ try {
52
+ const src = await Bun.file(route.path).text()
53
+ const isStatic = /export\s+const\s+render\s*=\s*["'](server|static)["']/.test(src)
54
+ if (!isStatic) continue
55
+
56
+ const result = validateServerIsland(src, route.path)
57
+ serverIslands.push(route)
58
+ validationResults.push({ ...result, route: route.route, path: route.path })
59
+
60
+ if (!result.valid) {
61
+ displayValidationErrors(route.path, result.errors)
62
+ }
63
+ } catch {}
64
+ }
65
+
66
+ return { serverIslands, validationResults }
12
67
  }
@@ -0,0 +1,64 @@
1
+ // bertui/src/build/ssr-renderer.js
2
+ import { join } from 'path'
3
+ import { existsSync, mkdirSync, rmSync } from 'fs'
4
+ import logger from '../logger/logger.js'
5
+
6
+ /**
7
+ * Render a static page to HTML string using renderToString.
8
+ * Forces both the page and renderToString to use the same React
9
+ * instance from the project's own node_modules.
10
+ */
11
+ export async function renderPageToHTML(compiledPagePath, buildDir) {
12
+ try {
13
+ // Extract project root from compiled path
14
+ // e.g. /project/.bertuibuild/pages/about.js → /project/
15
+ const projectRoot = compiledPagePath.split('.bertuibuild')[0]
16
+
17
+ // Explicitly resolve React from the PROJECT's node_modules
18
+ // This prevents the two-React-instances problem when bertui is bun linked
19
+ const reactPath = join(projectRoot, 'node_modules', 'react', 'index.js')
20
+ const reactDomServerPath = join(projectRoot, 'node_modules', 'react-dom', 'server.js')
21
+
22
+ if (!existsSync(reactPath)) {
23
+ logger.warn(`React not found in project node_modules: ${reactPath}`)
24
+ return null
25
+ }
26
+
27
+ if (!existsSync(reactDomServerPath)) {
28
+ logger.warn(`react-dom/server not found in project node_modules: ${reactDomServerPath}`)
29
+ return null
30
+ }
31
+
32
+ const React = await import(reactPath)
33
+ const { renderToString } = await import(reactDomServerPath)
34
+
35
+ // Import the compiled page
36
+ const mod = await import(`${compiledPagePath}?t=${Date.now()}`)
37
+ const Component = mod.default
38
+
39
+ if (typeof Component !== 'function') {
40
+ logger.warn(`No default export found in ${compiledPagePath}`)
41
+ return null
42
+ }
43
+
44
+ return renderToString(React.createElement(Component))
45
+
46
+ } catch (err) {
47
+ logger.warn(`renderToString failed for ${compiledPagePath}: ${err.message}`)
48
+ return null
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Check if a route's source file has render = "server" or render = "static"
54
+ */
55
+ export async function getPageRenderMode(sourcePath) {
56
+ try {
57
+ const src = await Bun.file(sourcePath).text()
58
+ if (/export\s+const\s+render\s*=\s*["']server["']/.test(src)) return 'server'
59
+ if (/export\s+const\s+render\s*=\s*["']static["']/.test(src)) return 'static'
60
+ return 'default'
61
+ } catch {
62
+ return 'default'
63
+ }
64
+ }
package/src/build.js CHANGED
@@ -43,11 +43,18 @@ export async function buildProduction(options = {}) {
43
43
  const importhow = config.importhow || {};
44
44
  logger.stepDone('Loading env', `${Object.keys(envVars).length} vars`);
45
45
 
46
+ // ── Step 2: Compile ──────────────────────────────────────────────────────
46
47
  // ── Step 2: Compile ──────────────────────────────────────────────────────
47
48
  logger.step(2, TOTAL_STEPS, 'Compiling');
48
49
  const { routes } = await compileForBuild(root, buildDir, envVars, config);
49
50
  logger.stepDone('Compiling', `${routes.length} routes`);
50
51
 
52
+ // TEMP DEBUG - remove after
53
+ const aboutPath = join(buildDir, 'pages', 'about.js')
54
+ if (existsSync(aboutPath)) {
55
+ const src = await Bun.file(aboutPath).text()
56
+ console.log('\n--- about.js compiled output ---\n', src.slice(0, 500), '\n---\n')
57
+ }
51
58
  // ── Step 3: Layouts ──────────────────────────────────────────────────────
52
59
  logger.step(3, TOTAL_STEPS, 'Layouts');
53
60
  const layouts = await compileLayouts(root, buildDir);
@@ -86,7 +93,7 @@ export async function buildProduction(options = {}) {
86
93
 
87
94
  // ── Step 9: HTML ─────────────────────────────────────────────────────────
88
95
  logger.step(9, TOTAL_STEPS, 'Generating HTML');
89
- await generateProductionHTML(root, outDir, result, routes, config);
96
+ await generateProductionHTML(root, outDir, result, routes, config, buildDir)
90
97
  logger.stepDone('Generating HTML', `${routes.length} pages`);
91
98
 
92
99
  // ── Step 10: Sitemap + robots ────────────────────────────────────────────