bertui 1.2.7 → 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.7",
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",
@@ -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,14 +7,13 @@ 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
- import { createRoot } from 'react-dom/client';
18
17
 
19
18
  const RouterContext = createContext(null);
20
19
 
@@ -77,16 +76,16 @@ export function Router({ routes }) {
77
76
 
78
77
  export function Link({ to, children, ...props }) {
79
78
  const { navigate } = useRouter();
80
- return React.createElement('a', {
81
- href: to,
82
- onClick: (e) => { e.preventDefault(); navigate(to); },
83
- ...props
79
+ return React.createElement('a', {
80
+ href: to,
81
+ onClick: (e) => { e.preventDefault(); navigate(to); },
82
+ ...props
84
83
  }, children);
85
84
  }
86
85
 
87
86
  function NotFound() {
88
87
  return React.createElement('div', {
89
- style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
88
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
90
89
  justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
91
90
  },
92
91
  React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
@@ -100,18 +99,7 @@ ${imports}
100
99
  export const routes = [
101
100
  ${routeConfigs}
102
101
  ];
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
104
  await Bun.write(join(buildDir, 'router.js'), routerCode);
117
105
  }
@@ -4,34 +4,27 @@ 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, buildDir, buildResult, routes, serverIslands, config) {
7
+ export async function generateProductionHTML(root, outDir, buildResult, routes, config) {
8
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
20
 
22
- logger.info(`📄 Generating HTML for ${routes.length} routes...`);
23
-
24
- const BATCH_SIZE = 5;
21
+ logger.info(`Generating HTML for ${routes.length} routes...`);
25
22
 
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
- for (const route of batch) {
30
- await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
31
- }
23
+ for (const route of routes) {
24
+ await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
32
25
  }
33
26
 
34
- logger.success(`✅ HTML generation complete for ${routes.length} routes`);
27
+ logger.success(`HTML generation complete for ${routes.length} routes`);
35
28
  }
36
29
 
37
30
  async function copyBertuiPackagesToProduction(root, outDir) {
@@ -46,7 +39,6 @@ async function copyBertuiPackagesToProduction(root, outDir) {
46
39
  const bertuiIconsDest = join(outDir, 'node_modules', 'bertui-icons');
47
40
  mkdirSync(join(outDir, 'node_modules'), { recursive: true });
48
41
  cpSync(bertuiIconsSource, bertuiIconsDest, { recursive: true });
49
- logger.success('✅ Copied bertui-icons to dist/node_modules/');
50
42
  packages.bertuiIcons = true;
51
43
  } catch (error) {
52
44
  logger.error(`Failed to copy bertui-icons: ${error.message}`);
@@ -61,7 +53,6 @@ async function copyBertuiPackagesToProduction(root, outDir) {
61
53
  const minCSSPath = join(bertuiAnimateSource, 'bertui-animate.min.css');
62
54
  if (existsSync(minCSSPath)) {
63
55
  cpSync(minCSSPath, join(bertuiAnimateDest, 'bertui-animate.min.css'));
64
- logger.success('✅ Copied bertui-animate.min.css to dist/css/');
65
56
  packages.bertuiAnimate = true;
66
57
  }
67
58
  } catch (error) {
@@ -75,7 +66,6 @@ async function copyBertuiPackagesToProduction(root, outDir) {
75
66
  const elysiaEdenDest = join(outDir, 'node_modules', '@elysiajs', 'eden');
76
67
  mkdirSync(join(outDir, 'node_modules', '@elysiajs'), { recursive: true });
77
68
  cpSync(elysiaEdenSource, elysiaEdenDest, { recursive: true });
78
- logger.success('✅ Copied @elysiajs/eden to dist/node_modules/');
79
69
  packages.elysiaEden = true;
80
70
  } catch (error) {
81
71
  logger.error(`Failed to copy @elysiajs/eden: ${error.message}`);
@@ -103,11 +93,10 @@ async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir
103
93
  }
104
94
 
105
95
  await Bun.write(htmlPath, html);
106
- logger.success(`✅ ${route.route}`);
96
+ logger.success(`${route.route}`);
107
97
 
108
98
  } catch (error) {
109
99
  logger.error(`Failed HTML for ${route.route}: ${error.message}`);
110
- console.error(error);
111
100
  }
112
101
  }
113
102
 
@@ -153,7 +142,6 @@ ${bertuiAnimateCSS}
153
142
  }
154
143
  }
155
144
  </script>
156
- <script>window.__BERTUI_HYDRATE__ = false;</script>
157
145
  </head>
158
146
  <body>
159
147
  <div id="root"></div>
@@ -1,175 +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
- // 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)
40
- const hookPatterns = [
41
- 'useState', 'useEffect', 'useContext', 'useReducer',
42
- 'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
43
- 'useLayoutEffect', 'useDebugValue', 'useId', 'useDeferredValue',
44
- 'useTransition', 'useSyncExternalStore'
45
- ];
46
-
47
- for (const hook of hookPatterns) {
48
- // Look for the hook as a function call, but only in the cleaned code
49
- const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
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
- }
61
- }
62
- }
63
-
64
- // Rule 2: No bertui/router imports
65
- if (sourceCode.includes('from \'bertui/router\'') ||
66
- sourceCode.includes('from "bertui/router"')) {
67
- errors.push('❌ Imports from \'bertui/router\' (use <a> tags instead of Link)');
68
- }
69
-
70
- // Rule 3: No browser APIs (check cleaned code)
71
- const browserAPIs = [
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' },
80
- { pattern: '\\.addEventListener\\s*\\(', name: 'addEventListener' },
81
- { pattern: '\\.removeEventListener\\s*\\(', name: 'removeEventListener' },
82
- { pattern: '\\bsetTimeout\\s*\\(', name: 'setTimeout' },
83
- { pattern: '\\bsetInterval\\s*\\(', name: 'setInterval' },
84
- { pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' },
85
- { pattern: '\\bconsole\\.', name: 'console' }
86
- ];
87
-
88
- for (const api of browserAPIs) {
89
- const regex = new RegExp(api.pattern, 'g');
90
- if (regex.test(cleanedCode)) {
91
- if (api.name === 'console') {
92
- logger.warn(`⚠️ ${filePath} uses console.log (will not work in static HTML)`);
93
- } else {
94
- errors.push(`❌ Uses browser API: ${api.name}`);
95
- }
96
- }
97
- }
98
-
99
- // Rule 4: No event handlers (check cleaned code)
100
- const eventHandlers = [
101
- 'onClick=', 'onChange=', 'onSubmit=', 'onInput=', 'onFocus=',
102
- 'onBlur=', 'onMouseEnter=', 'onMouseLeave=', 'onKeyDown=',
103
- 'onKeyUp=', 'onScroll='
104
- ];
105
-
106
- for (const handler of eventHandlers) {
107
- const escapedHandler = handler.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
108
- const regex = new RegExp(`\\b${escapedHandler}\\s*{`, 'g');
109
- if (regex.test(cleanedCode)) {
110
- errors.push(`❌ Uses event handler: ${handler.replace('=', '')} (Server Islands are static HTML)`);
111
- }
112
- }
113
-
114
- // Rule 5: Check for dynamic imports
115
- if (/import\s*\(/.test(cleanedCode)) {
116
- errors.push('❌ Uses dynamic import() (not supported in Server Islands)');
117
- }
118
-
119
- // Rule 6: Check for async/await
120
- if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(cleanedCode)) {
121
- errors.push('❌ Uses async/await (Server Islands must be synchronous)');
122
- }
123
-
124
- const valid = errors.length === 0;
125
-
126
- return { valid, errors };
5
+ return { valid: true, errors: [] };
127
6
  }
128
7
 
8
+ export function displayValidationErrors(filePath, errors) {}
129
9
 
130
-
131
- /**
132
- * Display validation errors in a clear format
133
- */
134
- export function displayValidationErrors(filePath, errors) {
135
- logger.error(`\n🏝️ Server Island validation failed: ${filePath}`);
136
- logger.error('\nViolations:');
137
- errors.forEach(error => logger.error(` ${error}`));
138
- logger.error('\n📖 Server Island Rules:');
139
- logger.error(' ✅ Pure static JSX only');
140
- logger.error(' ❌ No React hooks (useState, useEffect, etc.)');
141
- logger.error(' ❌ No Link component (use <a> tags)');
142
- logger.error(' ❌ No browser APIs (window, document, fetch)');
143
- logger.error(' ❌ No event handlers (onClick, onChange, etc.)');
144
- logger.error('\n💡 Tip: Remove the "export const render = \\"server\\"" line');
145
- logger.error(' if you need these features (page will be client-only).\n');
146
- }
147
-
148
- /**
149
- * Extract and validate all Server Islands in a project
150
- */
151
10
  export async function validateAllServerIslands(routes) {
152
- const serverIslands = [];
153
- const validationResults = [];
154
-
155
- for (const route of routes) {
156
- const sourceCode = await Bun.file(route.path).text();
157
- const isServerIsland = sourceCode.includes('export const render = "server"');
158
-
159
- if (isServerIsland) {
160
- const validation = validateServerIsland(sourceCode, route.path);
161
-
162
- validationResults.push({
163
- route: route.route,
164
- path: route.path,
165
- ...validation
166
- });
167
-
168
- if (validation.valid) {
169
- serverIslands.push(route);
170
- }
171
- }
172
- }
173
-
174
- return { serverIslands, validationResults };
11
+ return { serverIslands: [], validationResults: [] };
175
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,14 +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
- if (!existsSync(buildEntry)) throw new Error('main.js not found in build dir');
82
- const result = await bundleJavaScript(buildEntry, 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);
83
84
  totalKB = (result.outputs.reduce((a, o) => a + (o.size || 0), 0) / 1024).toFixed(1);
84
85
  logger.stepDone('Bundling JS', `${totalKB} KB · tree-shaken`);
85
86
 
86
87
  // ── Step 9: HTML ─────────────────────────────────────────────────────────
87
88
  logger.step(9, TOTAL_STEPS, 'Generating HTML');
88
- await generateProductionHTML(root, outDir, buildDir, result, routes, serverIslands, config);
89
+ await generateProductionHTML(root, outDir, result, routes, config);
89
90
  logger.stepDone('Generating HTML', `${routes.length} pages`);
90
91
 
91
92
  // ── Step 10: Sitemap + robots ────────────────────────────────────────────
@@ -94,32 +95,27 @@ export async function buildProduction(options = {}) {
94
95
  await generateRobots(config, outDir, routes);
95
96
  logger.stepDone('Sitemap & robots');
96
97
 
97
- // Delete build dir AFTER HTML generation
98
98
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
99
99
 
100
- // Generate bundle report
101
100
  try {
102
101
  await analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') });
103
102
  } catch (reportErr) {
104
103
  logger.debug(`Bundle report generation skipped: ${reportErr.message}`);
105
104
  }
106
105
 
107
- // ── Summary ──────────────────────────────────────────────────────────────
108
106
  logger.printSummary({
109
- routes: routes.length,
110
- serverIslands: serverIslands.length,
111
- interactive: analyzedRoutes.interactive.length,
112
- staticRoutes: analyzedRoutes.static.length,
113
- jsSize: `${totalKB} KB`,
114
- outDir: 'dist/',
107
+ routes: routes.length,
108
+ interactive: analyzedRoutes.interactive.length,
109
+ staticRoutes: analyzedRoutes.static.length,
110
+ jsSize: `${totalKB} KB`,
111
+ outDir: 'dist/',
115
112
  });
116
113
 
117
114
  logger.cleanup();
118
-
119
115
  return { success: true };
120
116
 
121
117
  } catch (error) {
122
- logger.stepFail('Build', error.message);
118
+ logger.stepFail('Build', error?.message || String(error));
123
119
  if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
124
120
  throw error;
125
121
  }
@@ -161,6 +157,7 @@ async function generateProductionImportMap(root, config) {
161
157
  }
162
158
 
163
159
  async function copyNodeModulesToDist(root, outDir, importMap) {
160
+ const { mkdirSync } = await import('fs');
164
161
  const dest = join(outDir, 'assets', 'node_modules');
165
162
  mkdirSync(dest, { recursive: true });
166
163
  const src = join(root, 'node_modules');
@@ -179,23 +176,18 @@ async function copyNodeModulesToDist(root, outDir, importMap) {
179
176
  }
180
177
  }
181
178
 
182
- async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
179
+ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, root, config) {
183
180
  const originalCwd = process.cwd();
184
181
  process.chdir(buildDir);
185
182
 
186
183
  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.
190
- const entrypoints = [buildEntry];
191
-
192
184
  const importMap = await generateProductionImportMap(root, config);
193
185
  await Bun.write(join(outDir, 'import-map.json'), JSON.stringify({ imports: importMap }, null, 2));
194
186
  await copyNodeModulesToDist(root, outDir, importMap);
195
187
 
196
- // Copy @bunnyx/api client to dist so the importmap entry resolves
197
188
  const bunnyxSrc = join(root, 'bunnyx-api', 'api-client.js');
198
189
  if (existsSync(bunnyxSrc)) {
190
+ const { mkdirSync } = await import('fs');
199
191
  mkdirSync(join(outDir, 'bunnyx-api'), { recursive: true });
200
192
  await Bun.write(join(outDir, 'bunnyx-api', 'api-client.js'), Bun.file(bunnyxSrc));
201
193
  }
@@ -214,36 +206,42 @@ async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, analyzedR
214
206
  },
215
207
  };
216
208
 
217
- const result = await Bun.build({
218
- entrypoints,
219
- outdir: join(outDir, 'assets'),
220
- target: 'browser',
221
- format: 'esm',
222
- plugins: [cssModulePlugin],
223
- minify: {
224
- whitespace: true,
225
- syntax: true,
226
- identifiers: true,
227
- },
228
- splitting: true,
229
- sourcemap: 'external',
230
- metafile: true,
231
- naming: {
232
- entry: 'js/[name]-[hash].js',
233
- chunk: 'js/chunks/[name]-[hash].js',
234
- asset: 'assets/[name]-[hash].[ext]',
235
- },
236
- external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime', '@bunnyx/api'],
237
- define: {
238
- 'process.env.NODE_ENV': '"production"',
239
- ...Object.fromEntries(
240
- Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
241
- ),
242
- },
243
- });
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
+ }
244
241
 
245
242
  if (!result.success) {
246
- 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'}`);
247
245
  }
248
246
 
249
247
  if (result.metafile) {
@@ -262,7 +260,7 @@ export async function build(options = {}) {
262
260
  await buildProduction(options);
263
261
  process.exit(0);
264
262
  } catch (error) {
265
- console.error(error);
263
+ console.error('Build error:', error?.message || String(error));
266
264
  process.exit(1);
267
265
  }
268
266
  }