bertui 1.1.1 → 1.1.2

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.1.1",
3
+ "version": "1.1.2",
4
4
  "description": "Lightning-fast React dev server powered by Bun and Elysia - Now with TypeScript support!",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -1,6 +1,6 @@
1
- // bertui/src/build/generators/html-generator.js
1
+ // bertui/src/build/generators/html-generator.js - FIXED PRODUCTION IMPORT MAP
2
2
  import { join, relative } from 'path';
3
- import { mkdirSync } from 'fs';
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
 
@@ -17,6 +17,9 @@ export async function generateProductionHTML(root, outDir, buildResult, routes,
17
17
  const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
18
18
  const defaultMeta = config.meta || {};
19
19
 
20
+ // ✅ FIX: Check if bertui-icons is installed and copy to dist/
21
+ const bertuiIconsInstalled = await copyBertuiIconsToProduction(root, outDir);
22
+
20
23
  logger.info(`📄 Generating HTML for ${routes.length} routes...`);
21
24
 
22
25
  // Process in batches to avoid Bun crashes
@@ -28,14 +31,39 @@ export async function generateProductionHTML(root, outDir, buildResult, routes,
28
31
 
29
32
  // Process batch sequentially
30
33
  for (const route of batch) {
31
- await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir);
34
+ await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiIconsInstalled);
32
35
  }
33
36
  }
34
37
 
35
38
  logger.success(`✅ HTML generation complete for ${routes.length} routes`);
36
39
  }
37
40
 
38
- async function processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir) {
41
+ // NEW: Copy bertui-icons to dist/ for production
42
+ async function copyBertuiIconsToProduction(root, outDir) {
43
+ const nodeModulesDir = join(root, 'node_modules');
44
+ const bertuiIconsSource = join(nodeModulesDir, 'bertui-icons');
45
+
46
+ if (!existsSync(bertuiIconsSource)) {
47
+ logger.debug('bertui-icons not installed, skipping...');
48
+ return false;
49
+ }
50
+
51
+ try {
52
+ const bertuiIconsDest = join(outDir, 'node_modules', 'bertui-icons');
53
+ mkdirSync(join(outDir, 'node_modules'), { recursive: true });
54
+
55
+ // Copy the entire bertui-icons package
56
+ cpSync(bertuiIconsSource, bertuiIconsDest, { recursive: true });
57
+
58
+ logger.success('✅ Copied bertui-icons to dist/node_modules/');
59
+ return true;
60
+ } catch (error) {
61
+ logger.error(`Failed to copy bertui-icons: ${error.message}`);
62
+ return false;
63
+ }
64
+ }
65
+
66
+ async function processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiIconsInstalled) {
39
67
  try {
40
68
  const sourceCode = await Bun.file(route.path).text();
41
69
  const pageMeta = extractMetaFromSource(sourceCode);
@@ -56,7 +84,7 @@ async function processSingleRoute(route, serverIslands, config, defaultMeta, bun
56
84
  }
57
85
  }
58
86
 
59
- const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland);
87
+ const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland, bertuiIconsInstalled);
60
88
 
61
89
  let htmlPath;
62
90
  if (route.route === '/') {
@@ -211,7 +239,8 @@ async function extractStaticHTMLFromComponent(sourceCode, filePath) {
211
239
  }
212
240
  }
213
241
 
214
- function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false) {
242
+ // FIXED: Add bertuiIconsInstalled parameter
243
+ function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false, bertuiIconsInstalled = false) {
215
244
  const rootContent = staticHTML
216
245
  ? `<div id="root">${staticHTML}</div>`
217
246
  : '<div id="root"></div>';
@@ -220,6 +249,11 @@ function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland =
220
249
  ? '<!-- 🏝️ Server Island: Static content rendered at build time -->'
221
250
  : '<!-- ⚡ Client-only: Content rendered by JavaScript -->';
222
251
 
252
+ // ✅ FIX: Add bertui-icons to production import map if installed
253
+ const bertuiIconsImport = bertuiIconsInstalled
254
+ ? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
255
+ : '';
256
+
223
257
  return `<!DOCTYPE html>
224
258
  <html lang="${meta.lang || 'en'}">
225
259
  <head>
@@ -245,7 +279,7 @@ function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland =
245
279
  "react": "https://esm.sh/react@18.2.0",
246
280
  "react-dom": "https://esm.sh/react-dom@18.2.0",
247
281
  "react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
248
- "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
282
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"${bertuiIconsImport}
249
283
  }
250
284
  }
251
285
  </script>
@@ -1,8 +1,7 @@
1
- // bertui/src/build/processors/css-builder.js
1
+ // bertui/src/build/processors/css-builder.js - SAFE VERSION
2
2
  import { join } from 'path';
3
3
  import { existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
5
- import { buildCSS } from '../css-builder.js';
6
5
 
7
6
  export async function buildAllCSS(root, outDir) {
8
7
  const srcStylesDir = join(root, 'src', 'styles');
@@ -18,6 +17,8 @@ export async function buildAllCSS(root, outDir) {
18
17
  return;
19
18
  }
20
19
 
20
+ logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
21
+
21
22
  let combinedCSS = '';
22
23
  for (const cssFile of cssFiles) {
23
24
  const srcPath = join(srcStylesDir, cssFile);
@@ -27,9 +28,75 @@ export async function buildAllCSS(root, outDir) {
27
28
  }
28
29
 
29
30
  const combinedPath = join(stylesOutDir, 'bertui.min.css');
30
- await Bun.write(combinedPath, combinedCSS);
31
- await buildCSS(combinedPath, combinedPath);
31
+
32
+ // ✅ SAFE: Try Lightning CSS, fallback to simple minification
33
+ try {
34
+ const minified = await minifyCSSSafe(combinedCSS);
35
+ await Bun.write(combinedPath, minified);
36
+
37
+ const originalSize = Buffer.byteLength(combinedCSS);
38
+ const minifiedSize = Buffer.byteLength(minified);
39
+ const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
40
+
41
+ logger.success(`CSS minified: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
42
+ } catch (error) {
43
+ logger.warn(`CSS minification failed: ${error.message}`);
44
+ logger.info('Falling back to unminified CSS...');
45
+ await Bun.write(combinedPath, combinedCSS);
46
+ }
32
47
 
33
48
  logger.success(`✅ Combined ${cssFiles.length} CSS files`);
49
+ } else {
50
+ // No styles directory, create empty CSS
51
+ await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No custom styles */');
52
+ logger.info('No styles directory found, created empty CSS');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Safe CSS minification with fallback
58
+ */
59
+ async function minifyCSSSafe(css) {
60
+ // Try Lightning CSS first
61
+ try {
62
+ const { transform } = await import('lightningcss');
63
+
64
+ const { code } = transform({
65
+ filename: 'styles.css',
66
+ code: Buffer.from(css),
67
+ minify: true,
68
+ sourceMap: false,
69
+ targets: {
70
+ chrome: 90 << 16,
71
+ firefox: 88 << 16,
72
+ safari: 14 << 16,
73
+ edge: 90 << 16
74
+ }
75
+ });
76
+
77
+ return code.toString();
78
+
79
+ } catch (lightningError) {
80
+ logger.warn('Lightning CSS failed, using simple minification');
81
+
82
+ // Fallback: Simple manual minification
83
+ return simpleMinifyCSS(css);
34
84
  }
85
+ }
86
+
87
+ /**
88
+ * Simple CSS minification without dependencies
89
+ */
90
+ function simpleMinifyCSS(css) {
91
+ return css
92
+ // Remove comments
93
+ .replace(/\/\*[\s\S]*?\*\//g, '')
94
+ // Remove extra whitespace
95
+ .replace(/\s+/g, ' ')
96
+ // Remove space around { } : ; ,
97
+ .replace(/\s*([{}:;,])\s*/g, '$1')
98
+ // Remove trailing semicolons before }
99
+ .replace(/;}/g, '}')
100
+ // Remove leading/trailing whitespace
101
+ .trim();
35
102
  }
package/src/build.js CHANGED
@@ -1,8 +1,9 @@
1
- // bertui/src/build.js - FINAL ORCHESTRATOR
1
+ // bertui/src/build.js - FIXED BUNDLING
2
2
  import { join } from 'path';
3
3
  import { existsSync, mkdirSync, rmSync } from 'fs';
4
4
  import logger from './logger/logger.js';
5
5
  import { loadEnvVariables } from './utils/env.js';
6
+ import { runPageBuilder } from './pagebuilder/core.js';
6
7
 
7
8
  // Import modular components
8
9
  import { compileForBuild } from './build/compiler/index.js';
@@ -12,8 +13,6 @@ import { generateProductionHTML } from './build/generators/html-generator.js';
12
13
  import { generateSitemap } from './build/generators/sitemap-generator.js';
13
14
  import { generateRobots } from './build/generators/robots-generator.js';
14
15
 
15
-
16
-
17
16
  export async function buildProduction(options = {}) {
18
17
  const root = options.root || process.cwd();
19
18
  const buildDir = join(root, '.bertuibuild');
@@ -35,6 +34,15 @@ export async function buildProduction(options = {}) {
35
34
  logger.info('Step 0: Loading environment variables...');
36
35
  const envVars = loadEnvVariables(root);
37
36
 
37
+ // Step 0.5: Load config and run Page Builder
38
+ const { loadConfig } = await import('./config/loadConfig.js');
39
+ const config = await loadConfig(root);
40
+
41
+ if (config.pageBuilder) {
42
+ logger.info('Step 0.5: Running Page Builder...');
43
+ await runPageBuilder(root, config);
44
+ }
45
+
38
46
  // Step 1: Compilation
39
47
  logger.info('Step 1: Compiling and detecting Server Islands...');
40
48
  const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
@@ -59,12 +67,18 @@ export async function buildProduction(options = {}) {
59
67
  // Step 4: JavaScript Bundling
60
68
  logger.info('Step 4: Bundling JavaScript...');
61
69
  const buildEntry = join(buildDir, 'main.js');
70
+
71
+ // ✅ CRITICAL FIX: Check if main.js exists before bundling
72
+ if (!existsSync(buildEntry)) {
73
+ logger.error('❌ main.js not found in build directory!');
74
+ logger.error(' Expected: ' + buildEntry);
75
+ throw new Error('Build entry point missing. Compilation may have failed.');
76
+ }
77
+
62
78
  const result = await bundleJavaScript(buildEntry, outDir, envVars);
63
79
 
64
80
  // Step 5: HTML Generation
65
81
  logger.info('Step 5: Generating HTML with Server Islands...');
66
- const { loadConfig } = await import('./config/loadConfig.js');
67
- const config = await loadConfig(root);
68
82
  await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
69
83
 
70
84
  // Step 6: Sitemap
@@ -91,38 +105,66 @@ export async function buildProduction(options = {}) {
91
105
  }
92
106
 
93
107
  async function bundleJavaScript(buildEntry, outDir, envVars) {
94
- const result = await Bun.build({
95
- entrypoints: [buildEntry],
96
- outdir: join(outDir, 'assets'),
97
- target: 'browser',
98
- minify: true,
99
- splitting: true,
100
- sourcemap: 'external',
101
- naming: {
102
- entry: '[name]-[hash].js',
103
- chunk: 'chunks/[name]-[hash].js',
104
- asset: '[name]-[hash].[ext]'
105
- },
106
- external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
107
- define: {
108
- 'process.env.NODE_ENV': '"production"',
109
- ...Object.fromEntries(
110
- Object.entries(envVars).map(([key, value]) => [
111
- `process.env.${key}`,
112
- JSON.stringify(value)
113
- ])
114
- )
108
+ try {
109
+ // ✅ CRITICAL FIX: Better error handling and clearer external configuration
110
+ const result = await Bun.build({
111
+ entrypoints: [buildEntry],
112
+ outdir: join(outDir, 'assets'),
113
+ target: 'browser',
114
+ minify: true,
115
+ splitting: true,
116
+ sourcemap: 'external',
117
+ naming: {
118
+ entry: '[name]-[hash].js',
119
+ chunk: 'chunks/[name]-[hash].js',
120
+ asset: '[name]-[hash].[ext]'
121
+ },
122
+ // ✅ FIXED: Externalize React to use CDN (reduces bundle size)
123
+ external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
124
+ define: {
125
+ 'process.env.NODE_ENV': '"production"',
126
+ ...Object.fromEntries(
127
+ Object.entries(envVars).map(([key, value]) => [
128
+ `process.env.${key}`,
129
+ JSON.stringify(value)
130
+ ])
131
+ )
132
+ }
133
+ });
134
+
135
+ if (!result.success) {
136
+ logger.error('❌ JavaScript build failed!');
137
+
138
+ // ✅ IMPROVED: Better error reporting
139
+ if (result.logs && result.logs.length > 0) {
140
+ logger.error('\n📋 Build errors:');
141
+ result.logs.forEach((log, i) => {
142
+ logger.error(`\n${i + 1}. ${log.message}`);
143
+ if (log.position) {
144
+ logger.error(` File: ${log.position.file || 'unknown'}`);
145
+ logger.error(` Line: ${log.position.line || 'unknown'}`);
146
+ }
147
+ });
148
+ }
149
+
150
+ throw new Error('JavaScript bundling failed - check errors above');
115
151
  }
116
- });
117
-
118
- if (!result.success) {
119
- logger.error('JavaScript build failed!');
120
- result.logs.forEach(log => logger.error(log.message));
121
- process.exit(1);
152
+
153
+ // ✅ IMPROVED: Log successful bundle info
154
+ logger.success('✅ JavaScript bundled successfully');
155
+ logger.info(` Entry points: ${result.outputs.filter(o => o.kind === 'entry-point').length}`);
156
+ logger.info(` Chunks: ${result.outputs.filter(o => o.kind === 'chunk').length}`);
157
+
158
+ const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
159
+ logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
160
+
161
+ return result;
162
+
163
+ } catch (error) {
164
+ logger.error('❌ Bundling error: ' + error.message);
165
+ if (error.stack) logger.error(error.stack);
166
+ throw error;
122
167
  }
123
-
124
- logger.success('JavaScript bundled');
125
- return result;
126
168
  }
127
169
 
128
170
  function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
@@ -1,3 +1,4 @@
1
+ // bertui/src/client/compiler.js - FIXED NODE_MODULES IMPORTS
1
2
  import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
2
3
  import { join, extname, relative, dirname } from 'path';
3
4
  import logger from '../logger/logger.js';
@@ -73,12 +74,10 @@ async function discoverRoutes(pagesDir) {
73
74
  await scanDirectory(fullPath, relativePath);
74
75
  } else if (entry.isFile()) {
75
76
  const ext = extname(entry.name);
76
-
77
77
  if (ext === '.css') continue;
78
78
 
79
79
  if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
80
80
  const fileName = entry.name.replace(ext, '');
81
-
82
81
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
83
82
 
84
83
  if (fileName === 'index') {
@@ -100,7 +99,6 @@ async function discoverRoutes(pagesDir) {
100
99
  }
101
100
 
102
101
  await scanDirectory(pagesDir);
103
-
104
102
  routes.sort((a, b) => {
105
103
  if (a.type === b.type) {
106
104
  return a.route.localeCompare(b.route);
@@ -141,11 +139,7 @@ export function Router({ routes }) {
141
139
 
142
140
  useEffect(() => {
143
141
  matchAndSetRoute(window.location.pathname);
144
-
145
- const handlePopState = () => {
146
- matchAndSetRoute(window.location.pathname);
147
- };
148
-
142
+ const handlePopState = () => matchAndSetRoute(window.location.pathname);
149
143
  window.addEventListener('popstate', handlePopState);
150
144
  return () => window.removeEventListener('popstate', handlePopState);
151
145
  }, [routes]);
@@ -158,27 +152,21 @@ export function Router({ routes }) {
158
152
  return;
159
153
  }
160
154
  }
161
-
162
155
  for (const route of routes) {
163
156
  if (route.type === 'dynamic') {
164
157
  const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
165
158
  const regex = new RegExp('^' + pattern + '$');
166
159
  const match = pathname.match(regex);
167
-
168
160
  if (match) {
169
161
  const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
170
162
  const extractedParams = {};
171
- paramNames.forEach((name, i) => {
172
- extractedParams[name] = match[i + 1];
173
- });
174
-
163
+ paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
175
164
  setCurrentRoute(route);
176
165
  setParams(extractedParams);
177
166
  return;
178
167
  }
179
168
  }
180
169
  }
181
-
182
170
  setCurrentRoute(null);
183
171
  setParams({});
184
172
  }
@@ -188,31 +176,21 @@ export function Router({ routes }) {
188
176
  matchAndSetRoute(path);
189
177
  }
190
178
 
191
- const routerValue = {
192
- currentRoute,
193
- params,
194
- navigate,
195
- pathname: window.location.pathname
196
- };
197
-
198
179
  const Component = currentRoute?.component;
199
-
200
180
  return React.createElement(
201
181
  RouterContext.Provider,
202
- { value: routerValue },
203
- Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
182
+ { value: { currentRoute, params, navigate, pathname: window.location.pathname } },
183
+ Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
204
184
  );
205
185
  }
206
186
 
207
187
  export function Link({ to, children, ...props }) {
208
188
  const { navigate } = useRouter();
209
-
210
- function handleClick(e) {
211
- e.preventDefault();
212
- navigate(to);
213
- }
214
-
215
- return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
189
+ return React.createElement('a', {
190
+ href: to,
191
+ onClick: (e) => { e.preventDefault(); navigate(to); },
192
+ ...props
193
+ }, children);
216
194
  }
217
195
 
218
196
  function NotFound() {
@@ -250,7 +228,6 @@ ${routeConfigs}
250
228
 
251
229
  async function compileDirectory(srcDir, outDir, root, envVars) {
252
230
  const stats = { files: 0, skipped: 0 };
253
-
254
231
  const files = readdirSync(srcDir);
255
232
 
256
233
  for (const file of files) {
@@ -287,7 +264,6 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
287
264
  code = replaceEnvInCode(code, envVars);
288
265
  code = fixRouterImports(code, outPath, root);
289
266
 
290
- // ✅ CRITICAL FIX: Ensure React import for .js files with JSX
291
267
  if (usesJSX(code) && !code.includes('import React')) {
292
268
  code = `import React from 'react';\n${code}`;
293
269
  }
@@ -331,11 +307,11 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
331
307
  });
332
308
  let compiled = await transpiler.transform(code);
333
309
 
334
- // ✅ CRITICAL FIX: Always add React import if JSX is present
335
310
  if (usesJSX(compiled) && !compiled.includes('import React')) {
336
311
  compiled = `import React from 'react';\n${compiled}`;
337
312
  }
338
313
 
314
+ // ✅ CRITICAL FIX: Don't touch node_modules imports
339
315
  compiled = fixRelativeImports(compiled);
340
316
 
341
317
  await Bun.write(outPath, compiled);
@@ -346,13 +322,12 @@ async function compileFile(srcPath, outDir, filename, relativePath, root, envVar
346
322
  }
347
323
  }
348
324
 
349
- // ✅ NEW: Detect if code uses JSX
350
325
  function usesJSX(code) {
351
326
  return code.includes('React.createElement') ||
352
327
  code.includes('React.Fragment') ||
353
- /<[A-Z]/.test(code) || // Detects JSX tags like <Component>
354
- code.includes('jsx(') || // Runtime JSX
355
- code.includes('jsxs('); // Runtime JSX
328
+ /<[A-Z]/.test(code) ||
329
+ code.includes('jsx(') ||
330
+ code.includes('jsxs(');
356
331
  }
357
332
 
358
333
  function removeCSSImports(code) {
@@ -384,13 +359,19 @@ function fixRouterImports(code, outPath, root) {
384
359
  }
385
360
 
386
361
  function fixRelativeImports(code) {
387
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
362
+ // CRITICAL FIX: Only fix relative imports, NOT bare specifiers like 'bertui-icons'
363
+ // Regex explanation:
364
+ // - Match: from './file' or from '../file'
365
+ // - DON'T match: from 'bertui-icons' or from 'react'
366
+
367
+ const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g;
388
368
 
389
- code = code.replace(importRegex, (match, prefix, path) => {
369
+ code = code.replace(importRegex, (match, path) => {
370
+ // Don't add .js if path already has an extension or ends with /
390
371
  if (path.endsWith('/') || /\.\w+$/.test(path)) {
391
372
  return match;
392
373
  }
393
- return `from '${prefix}${path}.js';`;
374
+ return `from '${path}.js'`;
394
375
  });
395
376
 
396
377
  return code;