bertui 1.1.0 β 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/README.md +424 -435
- package/package.json +26 -6
- package/src/build/compiler/file-transpiler.js +171 -0
- package/src/build/compiler/index.js +45 -0
- package/src/build/compiler/route-discoverer.js +46 -0
- package/src/build/compiler/router-generator.js +104 -0
- package/src/build/generators/html-generator.js +293 -0
- package/src/build/generators/robots-generator.js +58 -0
- package/src/build/generators/sitemap-generator.js +63 -0
- package/src/build/processors/asset-processor.js +19 -0
- package/src/build/processors/css-builder.js +102 -0
- package/src/build.js +100 -680
- package/src/client/compiler.js +23 -42
- package/src/config/defaultConfig.js +26 -6
- package/src/config/loadConfig.js +21 -5
- package/src/pagebuilder/core.js +191 -0
- package/src/server/dev-server.js +134 -36
- package/src/utils/env.js +59 -39
- package/src/utils/meta-extractor.js +127 -0
- package/types/config.d.ts +80 -0
- package/types/index.d.ts +116 -0
- package/types/react.d.ts +13 -0
- package/types/router.d.ts +79 -0
package/src/build.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
// bertui/src/build.js -
|
|
2
|
-
import { join
|
|
3
|
-
import { existsSync, mkdirSync, rmSync
|
|
1
|
+
// bertui/src/build.js - FIXED BUNDLING
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
4
4
|
import logger from './logger/logger.js';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { loadEnvVariables } from './utils/env.js';
|
|
6
|
+
import { runPageBuilder } from './pagebuilder/core.js';
|
|
7
|
+
|
|
8
|
+
// Import modular components
|
|
9
|
+
import { compileForBuild } from './build/compiler/index.js';
|
|
10
|
+
import { buildAllCSS } from './build/processors/css-builder.js';
|
|
11
|
+
import { copyAllStaticAssets } from './build/processors/asset-processor.js';
|
|
12
|
+
import { generateProductionHTML } from './build/generators/html-generator.js';
|
|
13
|
+
import { generateSitemap } from './build/generators/sitemap-generator.js';
|
|
14
|
+
import { generateRobots } from './build/generators/robots-generator.js';
|
|
8
15
|
|
|
9
16
|
export async function buildProduction(options = {}) {
|
|
10
17
|
const root = options.root || process.cwd();
|
|
@@ -14,18 +21,29 @@ export async function buildProduction(options = {}) {
|
|
|
14
21
|
logger.bigLog('BUILDING WITH SERVER ISLANDS ποΈ', { color: 'green' });
|
|
15
22
|
logger.info('π₯ OPTIONAL SERVER CONTENT - THE GAME CHANGER');
|
|
16
23
|
|
|
24
|
+
// Clean directories
|
|
17
25
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
18
26
|
if (existsSync(outDir)) rmSync(outDir, { recursive: true });
|
|
19
|
-
|
|
20
27
|
mkdirSync(buildDir, { recursive: true });
|
|
21
28
|
mkdirSync(outDir, { recursive: true });
|
|
22
29
|
|
|
23
30
|
const startTime = Date.now();
|
|
24
31
|
|
|
25
32
|
try {
|
|
33
|
+
// Step 0: Environment
|
|
26
34
|
logger.info('Step 0: Loading environment variables...');
|
|
27
35
|
const envVars = loadEnvVariables(root);
|
|
28
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
|
+
|
|
46
|
+
// Step 1: Compilation
|
|
29
47
|
logger.info('Step 1: Compiling and detecting Server Islands...');
|
|
30
48
|
const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
|
|
31
49
|
|
|
@@ -38,19 +56,57 @@ export async function buildProduction(options = {}) {
|
|
|
38
56
|
})));
|
|
39
57
|
}
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
logger.info(`Client-only routes: ${clientRoutes.length}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
59
|
+
// Step 2: CSS Processing
|
|
45
60
|
logger.info('Step 2: Combining CSS...');
|
|
46
61
|
await buildAllCSS(root, outDir);
|
|
47
62
|
|
|
63
|
+
// Step 3: Assets
|
|
48
64
|
logger.info('Step 3: Copying static assets...');
|
|
49
65
|
await copyAllStaticAssets(root, outDir);
|
|
50
66
|
|
|
67
|
+
// Step 4: JavaScript Bundling
|
|
51
68
|
logger.info('Step 4: Bundling JavaScript...');
|
|
52
69
|
const buildEntry = join(buildDir, 'main.js');
|
|
53
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
|
+
|
|
78
|
+
const result = await bundleJavaScript(buildEntry, outDir, envVars);
|
|
79
|
+
|
|
80
|
+
// Step 5: HTML Generation
|
|
81
|
+
logger.info('Step 5: Generating HTML with Server Islands...');
|
|
82
|
+
await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
|
|
83
|
+
|
|
84
|
+
// Step 6: Sitemap
|
|
85
|
+
logger.info('Step 6: Generating sitemap.xml...');
|
|
86
|
+
await generateSitemap(routes, config, outDir);
|
|
87
|
+
|
|
88
|
+
// Step 7: Robots.txt
|
|
89
|
+
logger.info('Step 7: Generating robots.txt...');
|
|
90
|
+
await generateRobots(config, outDir, routes);
|
|
91
|
+
|
|
92
|
+
// Cleanup
|
|
93
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
94
|
+
|
|
95
|
+
// Summary
|
|
96
|
+
const duration = Date.now() - startTime;
|
|
97
|
+
showBuildSummary(routes, serverIslands, clientRoutes, duration);
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error(`Build failed: ${error.message}`);
|
|
101
|
+
if (error.stack) logger.error(error.stack);
|
|
102
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function bundleJavaScript(buildEntry, outDir, envVars) {
|
|
108
|
+
try {
|
|
109
|
+
// β
CRITICAL FIX: Better error handling and clearer external configuration
|
|
54
110
|
const result = await Bun.build({
|
|
55
111
|
entrypoints: [buildEntry],
|
|
56
112
|
outdir: join(outDir, 'assets'),
|
|
@@ -63,6 +119,7 @@ export async function buildProduction(options = {}) {
|
|
|
63
119
|
chunk: 'chunks/[name]-[hash].js',
|
|
64
120
|
asset: '[name]-[hash].[ext]'
|
|
65
121
|
},
|
|
122
|
+
// β
FIXED: Externalize React to use CDN (reduces bundle size)
|
|
66
123
|
external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
|
|
67
124
|
define: {
|
|
68
125
|
'process.env.NODE_ENV': '"production"',
|
|
@@ -76,689 +133,52 @@ export async function buildProduction(options = {}) {
|
|
|
76
133
|
});
|
|
77
134
|
|
|
78
135
|
if (!result.success) {
|
|
79
|
-
logger.error('JavaScript build failed!');
|
|
80
|
-
result.logs.forEach(log => logger.error(log.message));
|
|
81
|
-
process.exit(1);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
logger.success('JavaScript bundled');
|
|
85
|
-
|
|
86
|
-
logger.info('Step 5: Generating HTML with Server Islands...');
|
|
87
|
-
await generateProductionHTML(root, outDir, result, routes, serverIslands);
|
|
88
|
-
|
|
89
|
-
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
90
|
-
|
|
91
|
-
const duration = Date.now() - startTime;
|
|
92
|
-
logger.success(`β¨ Build complete in ${duration}ms`);
|
|
93
|
-
|
|
94
|
-
// Show summary
|
|
95
|
-
logger.bigLog('BUILD SUMMARY', { color: 'green' });
|
|
96
|
-
logger.info(`π Total routes: ${routes.length}`);
|
|
97
|
-
logger.info(`ποΈ Server Islands (SSG): ${serverIslands.length}`);
|
|
98
|
-
logger.info(`β‘ Client-only: ${clientRoutes.length}`);
|
|
99
|
-
|
|
100
|
-
if (serverIslands.length > 0) {
|
|
101
|
-
logger.success('β
Server Islands enabled - INSTANT content delivery!');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
logger.bigLog('READY TO DEPLOY π', { color: 'green' });
|
|
105
|
-
|
|
106
|
-
} catch (error) {
|
|
107
|
-
logger.error(`Build failed: ${error.message}`);
|
|
108
|
-
if (error.stack) logger.error(error.stack);
|
|
109
|
-
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
110
|
-
process.exit(1);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async function compileForBuild(root, buildDir, envVars) {
|
|
115
|
-
const srcDir = join(root, 'src');
|
|
116
|
-
const pagesDir = join(srcDir, 'pages');
|
|
117
|
-
|
|
118
|
-
if (!existsSync(srcDir)) {
|
|
119
|
-
throw new Error('src/ directory not found!');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let routes = [];
|
|
123
|
-
let serverIslands = [];
|
|
124
|
-
let clientRoutes = [];
|
|
125
|
-
|
|
126
|
-
if (existsSync(pagesDir)) {
|
|
127
|
-
routes = await discoverRoutes(pagesDir);
|
|
128
|
-
|
|
129
|
-
// ποΈ DETECT SERVER ISLANDS
|
|
130
|
-
for (const route of routes) {
|
|
131
|
-
const sourceCode = await Bun.file(route.path).text();
|
|
132
|
-
const isServerIsland = sourceCode.includes('export const render = "server"');
|
|
136
|
+
logger.error('β JavaScript build failed!');
|
|
133
137
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
logger.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await compileBuildDirectory(srcDir, buildDir, root, envVars);
|
|
144
|
-
|
|
145
|
-
if (routes.length > 0) {
|
|
146
|
-
await generateBuildRouter(routes, buildDir);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return { routes, serverIslands, clientRoutes };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async function generateProductionHTML(root, outDir, buildResult, routes, serverIslands) {
|
|
153
|
-
const mainBundle = buildResult.outputs.find(o =>
|
|
154
|
-
o.path.includes('main') && o.kind === 'entry-point'
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (!mainBundle) {
|
|
158
|
-
logger.error('β Could not find main bundle');
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
|
|
163
|
-
|
|
164
|
-
const { loadConfig } = await import('./config/loadConfig.js');
|
|
165
|
-
const config = await loadConfig(root);
|
|
166
|
-
const defaultMeta = config.meta || {};
|
|
167
|
-
|
|
168
|
-
for (const route of routes) {
|
|
169
|
-
try {
|
|
170
|
-
const sourceCode = await Bun.file(route.path).text();
|
|
171
|
-
const pageMeta = extractMetaFromSource(sourceCode);
|
|
172
|
-
const meta = { ...defaultMeta, ...pageMeta };
|
|
173
|
-
|
|
174
|
-
// ποΈ CHECK IF THIS IS A SERVER ISLAND
|
|
175
|
-
const isServerIsland = serverIslands.find(si => si.route === route.route);
|
|
176
|
-
|
|
177
|
-
let staticHTML = '';
|
|
178
|
-
|
|
179
|
-
if (isServerIsland) {
|
|
180
|
-
logger.info(`ποΈ Extracting static content: ${route.route}`);
|
|
181
|
-
|
|
182
|
-
// ποΈ CRITICAL: Server Islands are PURE HTML
|
|
183
|
-
// We extract the return statement and convert JSX to HTML
|
|
184
|
-
// NO react-dom/server needed - this is the beauty of it!
|
|
185
|
-
|
|
186
|
-
staticHTML = await extractStaticHTMLFromComponent(sourceCode, route.path);
|
|
187
|
-
|
|
188
|
-
if (staticHTML) {
|
|
189
|
-
logger.success(`β
Server Island rendered: ${route.route}`);
|
|
190
|
-
} else {
|
|
191
|
-
logger.warn(`β οΈ Could not extract HTML, falling back to client-only`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland);
|
|
196
|
-
|
|
197
|
-
let htmlPath;
|
|
198
|
-
if (route.route === '/') {
|
|
199
|
-
htmlPath = join(outDir, 'index.html');
|
|
200
|
-
} else {
|
|
201
|
-
const routeDir = join(outDir, route.route.replace(/^\//, ''));
|
|
202
|
-
mkdirSync(routeDir, { recursive: true });
|
|
203
|
-
htmlPath = join(routeDir, 'index.html');
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
await Bun.write(htmlPath, html);
|
|
207
|
-
|
|
208
|
-
if (isServerIsland) {
|
|
209
|
-
logger.success(`β
Server Island: ${route.route} (instant content!)`);
|
|
210
|
-
} else {
|
|
211
|
-
logger.success(`β
Client-only: ${route.route}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
} catch (error) {
|
|
215
|
-
logger.error(`Failed HTML for ${route.route}: ${error.message}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ποΈ NEW: Extract static HTML from Server Island component
|
|
221
|
-
// This converts JSX to HTML WITHOUT using react-dom/server
|
|
222
|
-
// ποΈ SMARTER VALIDATOR - Ignores strings in JSX content
|
|
223
|
-
async function extractStaticHTMLFromComponent(sourceCode, filePath) {
|
|
224
|
-
try {
|
|
225
|
-
// STEP 1: Extract only the ACTUAL CODE (before the return statement)
|
|
226
|
-
// This is where imports and hooks would be
|
|
227
|
-
const returnMatch = sourceCode.match(/return\s*\(/);
|
|
228
|
-
if (!returnMatch) {
|
|
229
|
-
logger.warn(`β οΈ Could not find return statement in ${filePath}`);
|
|
230
|
-
return null;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const codeBeforeReturn = sourceCode.substring(0, returnMatch.index);
|
|
234
|
-
const jsxContent = sourceCode.substring(returnMatch.index);
|
|
235
|
-
|
|
236
|
-
// VALIDATE: Check only the CODE part (not JSX/text content)
|
|
237
|
-
|
|
238
|
-
// Rule 1: No React hooks (in actual code only)
|
|
239
|
-
const hookPatterns = [
|
|
240
|
-
'useState', 'useEffect', 'useContext', 'useReducer',
|
|
241
|
-
'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
|
|
242
|
-
'useLayoutEffect', 'useDebugValue'
|
|
243
|
-
];
|
|
244
|
-
|
|
245
|
-
let hasHooks = false;
|
|
246
|
-
for (const hook of hookPatterns) {
|
|
247
|
-
// Only check the code BEFORE the JSX return
|
|
248
|
-
const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
|
|
249
|
-
if (regex.test(codeBeforeReturn)) {
|
|
250
|
-
logger.error(`β Server Island at ${filePath} contains React hooks!`);
|
|
251
|
-
logger.error(` Server Islands must be pure HTML - no ${hook}, etc.`);
|
|
252
|
-
hasHooks = true;
|
|
253
|
-
break;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (hasHooks) return null;
|
|
258
|
-
|
|
259
|
-
// Rule 2: No bertui/router imports (in actual code only)
|
|
260
|
-
// Only check ACTUAL imports at the top of the file, not in template literals
|
|
261
|
-
// Match: import X from 'bertui/router'
|
|
262
|
-
// Don't match: {`import X from 'bertui/router'`} (inside backticks)
|
|
263
|
-
const importLines = codeBeforeReturn.split('\n')
|
|
264
|
-
.filter(line => line.trim().startsWith('import'))
|
|
265
|
-
.join('\n');
|
|
266
|
-
|
|
267
|
-
const hasRouterImport = /from\s+['"]bertui\/router['"]/m.test(importLines);
|
|
268
|
-
|
|
269
|
-
if (hasRouterImport) {
|
|
270
|
-
logger.error(`β Server Island at ${filePath} imports from 'bertui/router'!`);
|
|
271
|
-
logger.error(` Server Islands cannot use Link - use <a> tags instead.`);
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Rule 3: No event handlers in JSX (these are actual attributes)
|
|
276
|
-
const eventHandlers = [
|
|
277
|
-
'onClick=',
|
|
278
|
-
'onChange=',
|
|
279
|
-
'onSubmit=',
|
|
280
|
-
'onInput=',
|
|
281
|
-
'onFocus=',
|
|
282
|
-
'onBlur=',
|
|
283
|
-
'onMouseEnter=',
|
|
284
|
-
'onMouseLeave=',
|
|
285
|
-
'onKeyDown=',
|
|
286
|
-
'onKeyUp=',
|
|
287
|
-
'onScroll='
|
|
288
|
-
];
|
|
289
|
-
|
|
290
|
-
for (const handler of eventHandlers) {
|
|
291
|
-
if (jsxContent.includes(handler)) {
|
|
292
|
-
logger.error(`β Server Island uses event handler: ${handler.replace('=', '')}`);
|
|
293
|
-
logger.error(` Server Islands are static HTML - no interactivity allowed`);
|
|
294
|
-
return null;
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// NOW EXTRACT THE JSX
|
|
299
|
-
const fullReturnMatch = sourceCode.match(/return\s*\(([\s\S]*?)\);?\s*}/);
|
|
300
|
-
if (!fullReturnMatch) {
|
|
301
|
-
logger.warn(`β οΈ Could not extract JSX from ${filePath}`);
|
|
302
|
-
return null;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
let html = fullReturnMatch[1].trim();
|
|
306
|
-
|
|
307
|
-
// STEP 2: Remove JSX comments {/* ... */}
|
|
308
|
-
html = html.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
|
309
|
-
|
|
310
|
-
// STEP 3: Convert className to class
|
|
311
|
-
html = html.replace(/className=/g, 'class=');
|
|
312
|
-
|
|
313
|
-
// STEP 4: Convert style objects to inline styles
|
|
314
|
-
// Match style={{...}} and convert to style="..."
|
|
315
|
-
html = html.replace(/style=\{\{([^}]+)\}\}/g, (match, styleObj) => {
|
|
316
|
-
// Split by comma, but be careful of commas inside values like rgba()
|
|
317
|
-
const props = [];
|
|
318
|
-
let currentProp = '';
|
|
319
|
-
let depth = 0;
|
|
320
|
-
|
|
321
|
-
for (let i = 0; i < styleObj.length; i++) {
|
|
322
|
-
const char = styleObj[i];
|
|
323
|
-
if (char === '(') depth++;
|
|
324
|
-
if (char === ')') depth--;
|
|
325
|
-
|
|
326
|
-
if (char === ',' && depth === 0) {
|
|
327
|
-
props.push(currentProp.trim());
|
|
328
|
-
currentProp = '';
|
|
329
|
-
} else {
|
|
330
|
-
currentProp += char;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
if (currentProp.trim()) props.push(currentProp.trim());
|
|
334
|
-
|
|
335
|
-
// Convert each property
|
|
336
|
-
const cssString = props
|
|
337
|
-
.map(prop => {
|
|
338
|
-
const colonIndex = prop.indexOf(':');
|
|
339
|
-
if (colonIndex === -1) return '';
|
|
340
|
-
|
|
341
|
-
const key = prop.substring(0, colonIndex).trim();
|
|
342
|
-
const value = prop.substring(colonIndex + 1).trim();
|
|
343
|
-
|
|
344
|
-
if (!key || !value) return '';
|
|
345
|
-
|
|
346
|
-
// Convert camelCase to kebab-case
|
|
347
|
-
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
348
|
-
// Remove quotes from value
|
|
349
|
-
const cssValue = value.replace(/['"]/g, '');
|
|
350
|
-
|
|
351
|
-
return `${cssKey}: ${cssValue}`;
|
|
352
|
-
})
|
|
353
|
-
.filter(Boolean)
|
|
354
|
-
.join('; ');
|
|
355
|
-
|
|
356
|
-
return `style="${cssString}"`;
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// STEP 5: Handle self-closing tags
|
|
360
|
-
const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
|
|
361
|
-
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
362
|
-
|
|
363
|
-
html = html.replace(/<(\w+)([^>]*)\s*\/>/g, (match, tag, attrs) => {
|
|
364
|
-
if (voidElements.includes(tag.toLowerCase())) {
|
|
365
|
-
return match; // Keep void elements self-closing
|
|
366
|
-
} else {
|
|
367
|
-
return `<${tag}${attrs}></${tag}>`; // Convert to opening + closing
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// STEP 6: Clean up JSX expressions
|
|
372
|
-
// Template literals: {`text`} -> text
|
|
373
|
-
html = html.replace(/\{`([^`]*)`\}/g, '$1');
|
|
374
|
-
// String literals: {'text'} or {"text"} -> text
|
|
375
|
-
html = html.replace(/\{(['"])(.*?)\1\}/g, '$2');
|
|
376
|
-
// Numbers: {123} -> 123
|
|
377
|
-
html = html.replace(/\{(\d+)\}/g, '$1');
|
|
378
|
-
|
|
379
|
-
logger.info(` Extracted ${html.length} chars of static HTML`);
|
|
380
|
-
return html;
|
|
381
|
-
|
|
382
|
-
} catch (error) {
|
|
383
|
-
logger.error(`Failed to extract HTML: ${error.message}`);
|
|
384
|
-
return null;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
// Example of how the style regex should work:
|
|
388
|
-
// Input: style={{ background: 'rgba(0,0,0,0.05)', padding: '1.5rem', borderRadius: '8px' }}
|
|
389
|
-
// Output: style="background: rgba(0,0,0,0.05); padding: 1.5rem; border-radius: 8px"
|
|
390
|
-
function generateHTML(meta, route, bundlePath, staticHTML = '', isServerIsland = false) {
|
|
391
|
-
const rootContent = staticHTML
|
|
392
|
-
? `<div id="root">${staticHTML}</div>`
|
|
393
|
-
: '<div id="root"></div>';
|
|
394
|
-
|
|
395
|
-
const comment = isServerIsland
|
|
396
|
-
? '<!-- ποΈ Server Island: Static content rendered at build time -->'
|
|
397
|
-
: '<!-- β‘ Client-only: Content rendered by JavaScript -->';
|
|
398
|
-
|
|
399
|
-
return `<!DOCTYPE html>
|
|
400
|
-
<html lang="${meta.lang || 'en'}">
|
|
401
|
-
<head>
|
|
402
|
-
<meta charset="UTF-8">
|
|
403
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
404
|
-
<title>${meta.title || 'BertUI App'}</title>
|
|
405
|
-
|
|
406
|
-
<meta name="description" content="${meta.description || 'Built with BertUI'}">
|
|
407
|
-
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
408
|
-
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
409
|
-
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
410
|
-
|
|
411
|
-
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
412
|
-
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
413
|
-
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
414
|
-
|
|
415
|
-
<link rel="stylesheet" href="/styles/bertui.min.css">
|
|
416
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
417
|
-
|
|
418
|
-
<script type="importmap">
|
|
419
|
-
{
|
|
420
|
-
"imports": {
|
|
421
|
-
"react": "https://esm.sh/react@18.2.0",
|
|
422
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0",
|
|
423
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
|
424
|
-
"react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
</script>
|
|
428
|
-
</head>
|
|
429
|
-
<body>
|
|
430
|
-
${comment}
|
|
431
|
-
${rootContent}
|
|
432
|
-
<script type="module" src="/${bundlePath}"></script>
|
|
433
|
-
</body>
|
|
434
|
-
</html>`;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Helper functions from original build.js
|
|
438
|
-
async function copyAllStaticAssets(root, outDir) {
|
|
439
|
-
const publicDir = join(root, 'public');
|
|
440
|
-
const srcImagesDir = join(root, 'src', 'images');
|
|
441
|
-
|
|
442
|
-
if (existsSync(publicDir)) {
|
|
443
|
-
copyImages(publicDir, outDir);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
if (existsSync(srcImagesDir)) {
|
|
447
|
-
const distImagesDir = join(outDir, 'images');
|
|
448
|
-
mkdirSync(distImagesDir, { recursive: true });
|
|
449
|
-
copyImages(srcImagesDir, distImagesDir);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
async function buildAllCSS(root, outDir) {
|
|
454
|
-
const srcStylesDir = join(root, 'src', 'styles');
|
|
455
|
-
const stylesOutDir = join(outDir, 'styles');
|
|
456
|
-
|
|
457
|
-
mkdirSync(stylesOutDir, { recursive: true });
|
|
458
|
-
|
|
459
|
-
if (existsSync(srcStylesDir)) {
|
|
460
|
-
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
461
|
-
|
|
462
|
-
if (cssFiles.length === 0) {
|
|
463
|
-
await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
let combinedCSS = '';
|
|
468
|
-
for (const cssFile of cssFiles) {
|
|
469
|
-
const srcPath = join(srcStylesDir, cssFile);
|
|
470
|
-
const file = Bun.file(srcPath);
|
|
471
|
-
const cssContent = await file.text();
|
|
472
|
-
combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
const combinedPath = join(stylesOutDir, 'bertui.min.css');
|
|
476
|
-
await Bun.write(combinedPath, combinedCSS);
|
|
477
|
-
await buildCSS(combinedPath, combinedPath);
|
|
478
|
-
|
|
479
|
-
logger.success(`β
Combined ${cssFiles.length} CSS files`);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async function discoverRoutes(pagesDir) {
|
|
484
|
-
const routes = [];
|
|
485
|
-
|
|
486
|
-
async function scanDirectory(dir, basePath = '') {
|
|
487
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
488
|
-
|
|
489
|
-
for (const entry of entries) {
|
|
490
|
-
const fullPath = join(dir, entry.name);
|
|
491
|
-
const relativePath = join(basePath, entry.name);
|
|
492
|
-
|
|
493
|
-
if (entry.isDirectory()) {
|
|
494
|
-
await scanDirectory(fullPath, relativePath);
|
|
495
|
-
} else if (entry.isFile()) {
|
|
496
|
-
const ext = extname(entry.name);
|
|
497
|
-
if (ext === '.css') continue;
|
|
498
|
-
|
|
499
|
-
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
500
|
-
const fileName = entry.name.replace(ext, '');
|
|
501
|
-
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
502
|
-
|
|
503
|
-
if (fileName === 'index') {
|
|
504
|
-
route = route.replace('/index', '') || '/';
|
|
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'}`);
|
|
505
146
|
}
|
|
506
|
-
|
|
507
|
-
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
508
|
-
|
|
509
|
-
routes.push({
|
|
510
|
-
route: route === '' ? '/' : route,
|
|
511
|
-
file: relativePath.replace(/\\/g, '/'),
|
|
512
|
-
path: fullPath,
|
|
513
|
-
type: isDynamic ? 'dynamic' : 'static'
|
|
514
|
-
});
|
|
515
|
-
}
|
|
147
|
+
});
|
|
516
148
|
}
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
await scanDirectory(pagesDir);
|
|
521
|
-
routes.sort((a, b) => a.type === b.type ? a.route.localeCompare(b.route) : a.type === 'static' ? -1 : 1);
|
|
522
|
-
|
|
523
|
-
return routes;
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
|
|
527
|
-
const files = readdirSync(srcDir);
|
|
528
|
-
|
|
529
|
-
for (const file of files) {
|
|
530
|
-
const srcPath = join(srcDir, file);
|
|
531
|
-
const stat = statSync(srcPath);
|
|
532
|
-
|
|
533
|
-
if (stat.isDirectory()) {
|
|
534
|
-
const subBuildDir = join(buildDir, file);
|
|
535
|
-
mkdirSync(subBuildDir, { recursive: true });
|
|
536
|
-
await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
|
|
537
|
-
} else {
|
|
538
|
-
const ext = extname(file);
|
|
539
|
-
if (ext === '.css') continue;
|
|
540
149
|
|
|
541
|
-
|
|
542
|
-
await compileBuildFile(srcPath, buildDir, file, root, envVars);
|
|
543
|
-
} else if (ext === '.js') {
|
|
544
|
-
const outPath = join(buildDir, file);
|
|
545
|
-
let code = await Bun.file(srcPath).text();
|
|
546
|
-
code = removeCSSImports(code);
|
|
547
|
-
code = replaceEnvInCode(code, envVars);
|
|
548
|
-
code = fixBuildImports(code, srcPath, outPath, root);
|
|
549
|
-
if (usesJSX(code) && !code.includes('import React')) {
|
|
550
|
-
code = `import React from 'react';\n${code}`;
|
|
551
|
-
}
|
|
552
|
-
await Bun.write(outPath, code);
|
|
553
|
-
}
|
|
150
|
+
throw new Error('JavaScript bundling failed - check errors above');
|
|
554
151
|
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
|
|
559
|
-
const ext = extname(filename);
|
|
560
|
-
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
561
|
-
|
|
562
|
-
try {
|
|
563
|
-
let code = await Bun.file(srcPath).text();
|
|
564
|
-
code = removeCSSImports(code);
|
|
565
|
-
code = replaceEnvInCode(code, envVars);
|
|
566
152
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
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}`);
|
|
570
157
|
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
tsconfig: {
|
|
574
|
-
compilerOptions: {
|
|
575
|
-
jsx: 'react',
|
|
576
|
-
jsxFactory: 'React.createElement',
|
|
577
|
-
jsxFragmentFactory: 'React.Fragment'
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
let compiled = await transpiler.transform(code);
|
|
583
|
-
if (usesJSX(compiled) && !compiled.includes('import React')) {
|
|
584
|
-
compiled = `import React from 'react';\n${compiled}`;
|
|
585
|
-
}
|
|
586
|
-
compiled = fixRelativeImports(compiled);
|
|
587
|
-
await Bun.write(outPath, compiled);
|
|
588
|
-
} catch (error) {
|
|
589
|
-
logger.error(`Failed to compile ${filename}: ${error.message}`);
|
|
590
|
-
throw error;
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
function usesJSX(code) {
|
|
595
|
-
return code.includes('React.createElement') ||
|
|
596
|
-
code.includes('React.Fragment') ||
|
|
597
|
-
/<[A-Z]/.test(code) ||
|
|
598
|
-
code.includes('jsx(') ||
|
|
599
|
-
code.includes('jsxs(');
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
function removeCSSImports(code) {
|
|
603
|
-
code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
|
|
604
|
-
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
605
|
-
return code;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function fixBuildImports(code, srcPath, outPath, root) {
|
|
609
|
-
const buildDir = join(root, '.bertuibuild');
|
|
610
|
-
const routerPath = join(buildDir, 'router.js');
|
|
611
|
-
const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
612
|
-
const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
|
|
613
|
-
|
|
614
|
-
code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
|
|
615
|
-
return code;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function fixRelativeImports(code) {
|
|
619
|
-
const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
620
|
-
code = code.replace(importRegex, (match, prefix, path) => {
|
|
621
|
-
if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
|
|
622
|
-
return `from '${prefix}${path}.js';`;
|
|
623
|
-
});
|
|
624
|
-
return code;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
function extractMetaFromSource(code) {
|
|
628
|
-
try {
|
|
629
|
-
const metaMatch = code.match(/export\s+const\s+meta\s*=\s*\{/);
|
|
630
|
-
if (!metaMatch) return null;
|
|
631
|
-
|
|
632
|
-
const startIndex = metaMatch.index + metaMatch[0].length - 1;
|
|
633
|
-
let braceCount = 0;
|
|
634
|
-
let endIndex = startIndex;
|
|
635
|
-
|
|
636
|
-
for (let i = startIndex; i < code.length; i++) {
|
|
637
|
-
if (code[i] === '{') braceCount++;
|
|
638
|
-
if (code[i] === '}') {
|
|
639
|
-
braceCount--;
|
|
640
|
-
if (braceCount === 0) {
|
|
641
|
-
endIndex = i;
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
158
|
+
const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
|
|
159
|
+
logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
|
|
646
160
|
|
|
647
|
-
|
|
161
|
+
return result;
|
|
648
162
|
|
|
649
|
-
const metaString = code.substring(startIndex, endIndex + 1);
|
|
650
|
-
const meta = {};
|
|
651
|
-
const pairRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g;
|
|
652
|
-
let match;
|
|
653
|
-
|
|
654
|
-
while ((match = pairRegex.exec(metaString)) !== null) {
|
|
655
|
-
meta[match[1]] = match[3];
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
return Object.keys(meta).length > 0 ? meta : null;
|
|
659
163
|
} catch (error) {
|
|
660
|
-
|
|
164
|
+
logger.error('β Bundling error: ' + error.message);
|
|
165
|
+
if (error.stack) logger.error(error.stack);
|
|
166
|
+
throw error;
|
|
661
167
|
}
|
|
662
168
|
}
|
|
663
169
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const componentName = `Page${i}`;
|
|
673
|
-
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
674
|
-
}).join(',\n');
|
|
170
|
+
function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
|
|
171
|
+
logger.success(`β¨ Build complete in ${duration}ms`);
|
|
172
|
+
logger.bigLog('BUILD SUMMARY', { color: 'green' });
|
|
173
|
+
logger.info(`π Total routes: ${routes.length}`);
|
|
174
|
+
logger.info(`ποΈ Server Islands (SSG): ${serverIslands.length}`);
|
|
175
|
+
logger.info(`β‘ Client-only: ${clientRoutes.length}`);
|
|
176
|
+
logger.info(`πΊοΈ Sitemap: dist/sitemap.xml`);
|
|
177
|
+
logger.info(`π€ robots.txt: dist/robots.txt`);
|
|
675
178
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
const RouterContext = createContext(null);
|
|
679
|
-
|
|
680
|
-
export function useRouter() {
|
|
681
|
-
const context = useContext(RouterContext);
|
|
682
|
-
if (!context) throw new Error('useRouter must be used within a Router');
|
|
683
|
-
return context;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
export function Router({ routes }) {
|
|
687
|
-
const [currentRoute, setCurrentRoute] = useState(null);
|
|
688
|
-
const [params, setParams] = useState({});
|
|
689
|
-
|
|
690
|
-
useEffect(() => {
|
|
691
|
-
matchAndSetRoute(window.location.pathname);
|
|
692
|
-
const handlePopState = () => matchAndSetRoute(window.location.pathname);
|
|
693
|
-
window.addEventListener('popstate', handlePopState);
|
|
694
|
-
return () => window.removeEventListener('popstate', handlePopState);
|
|
695
|
-
}, [routes]);
|
|
696
|
-
|
|
697
|
-
function matchAndSetRoute(pathname) {
|
|
698
|
-
for (const route of routes) {
|
|
699
|
-
if (route.type === 'static' && route.path === pathname) {
|
|
700
|
-
setCurrentRoute(route);
|
|
701
|
-
setParams({});
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
for (const route of routes) {
|
|
706
|
-
if (route.type === 'dynamic') {
|
|
707
|
-
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
708
|
-
const regex = new RegExp('^' + pattern + '$');
|
|
709
|
-
const match = pathname.match(regex);
|
|
710
|
-
if (match) {
|
|
711
|
-
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
712
|
-
const extractedParams = {};
|
|
713
|
-
paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
|
|
714
|
-
setCurrentRoute(route);
|
|
715
|
-
setParams(extractedParams);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}
|
|
720
|
-
setCurrentRoute(null);
|
|
721
|
-
setParams({});
|
|
179
|
+
if (serverIslands.length > 0) {
|
|
180
|
+
logger.success('β
Server Islands enabled - INSTANT content delivery!');
|
|
722
181
|
}
|
|
723
|
-
|
|
724
|
-
function navigate(path) {
|
|
725
|
-
window.history.pushState({}, '', path);
|
|
726
|
-
matchAndSetRoute(path);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
const Component = currentRoute?.component;
|
|
730
|
-
return React.createElement(
|
|
731
|
-
RouterContext.Provider,
|
|
732
|
-
{ value: { currentRoute, params, navigate, pathname: window.location.pathname } },
|
|
733
|
-
Component ? React.createElement(Component, { params }) : React.createElement(NotFound)
|
|
734
|
-
);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export function Link({ to, children, ...props }) {
|
|
738
|
-
const { navigate } = useRouter();
|
|
739
|
-
return React.createElement('a', {
|
|
740
|
-
href: to,
|
|
741
|
-
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
742
|
-
...props
|
|
743
|
-
}, children);
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function NotFound() {
|
|
747
|
-
return React.createElement('div', {
|
|
748
|
-
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
749
|
-
justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
|
|
750
|
-
},
|
|
751
|
-
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
752
|
-
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
753
|
-
React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
${imports}
|
|
758
|
-
|
|
759
|
-
export const routes = [
|
|
760
|
-
${routeConfigs}
|
|
761
|
-
];`;
|
|
762
182
|
|
|
763
|
-
|
|
183
|
+
logger.bigLog('READY TO DEPLOY π', { color: 'green' });
|
|
764
184
|
}
|