bertui 1.0.3 β 1.1.1
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 +551 -132
- package/package.json +27 -7
- 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 +259 -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 +35 -0
- package/src/build/server-island-validator.js +156 -0
- package/src/build.js +96 -632
- package/src/config/defaultConfig.js +26 -6
- package/src/config/loadConfig.js +21 -5
- package/src/router/Router.js +38 -5
- package/src/router/SSRRouter.js +156 -0
- package/src/utils/meta-extractor.js +61 -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,678 +1,142 @@
|
|
|
1
|
-
// src/build.js -
|
|
2
|
-
import { join
|
|
3
|
-
import { existsSync, mkdirSync, rmSync
|
|
1
|
+
// bertui/src/build.js - FINAL ORCHESTRATOR
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
4
4
|
import logger from './logger/logger.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
import { loadEnvVariables } from './utils/env.js';
|
|
6
|
+
|
|
7
|
+
// Import modular components
|
|
8
|
+
import { compileForBuild } from './build/compiler/index.js';
|
|
9
|
+
import { buildAllCSS } from './build/processors/css-builder.js';
|
|
10
|
+
import { copyAllStaticAssets } from './build/processors/asset-processor.js';
|
|
11
|
+
import { generateProductionHTML } from './build/generators/html-generator.js';
|
|
12
|
+
import { generateSitemap } from './build/generators/sitemap-generator.js';
|
|
13
|
+
import { generateRobots } from './build/generators/robots-generator.js';
|
|
14
|
+
|
|
15
|
+
|
|
8
16
|
|
|
9
17
|
export async function buildProduction(options = {}) {
|
|
10
18
|
const root = options.root || process.cwd();
|
|
11
19
|
const buildDir = join(root, '.bertuibuild');
|
|
12
20
|
const outDir = join(root, 'dist');
|
|
13
21
|
|
|
14
|
-
logger.bigLog('BUILDING
|
|
15
|
-
logger.info('π₯
|
|
16
|
-
|
|
17
|
-
// Clean up old builds
|
|
18
|
-
if (existsSync(buildDir)) {
|
|
19
|
-
rmSync(buildDir, { recursive: true });
|
|
20
|
-
}
|
|
21
|
-
if (existsSync(outDir)) {
|
|
22
|
-
rmSync(outDir, { recursive: true });
|
|
23
|
-
logger.info('Cleaned dist/');
|
|
24
|
-
}
|
|
22
|
+
logger.bigLog('BUILDING WITH SERVER ISLANDS ποΈ', { color: 'green' });
|
|
23
|
+
logger.info('π₯ OPTIONAL SERVER CONTENT - THE GAME CHANGER');
|
|
25
24
|
|
|
25
|
+
// Clean directories
|
|
26
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
27
|
+
if (existsSync(outDir)) rmSync(outDir, { recursive: true });
|
|
26
28
|
mkdirSync(buildDir, { recursive: true });
|
|
27
29
|
mkdirSync(outDir, { recursive: true });
|
|
28
30
|
|
|
29
31
|
const startTime = Date.now();
|
|
30
32
|
|
|
31
33
|
try {
|
|
34
|
+
// Step 0: Environment
|
|
32
35
|
logger.info('Step 0: Loading environment variables...');
|
|
33
36
|
const envVars = loadEnvVariables(root);
|
|
34
|
-
if (Object.keys(envVars).length > 0) {
|
|
35
|
-
logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
|
|
36
|
-
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
// Step 1: Compilation
|
|
39
|
+
logger.info('Step 1: Compiling and detecting Server Islands...');
|
|
40
|
+
const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
if (serverIslands.length > 0) {
|
|
43
|
+
logger.bigLog('SERVER ISLANDS DETECTED ποΈ', { color: 'cyan' });
|
|
44
|
+
logger.table(serverIslands.map(r => ({
|
|
45
|
+
route: r.route,
|
|
46
|
+
file: r.file,
|
|
47
|
+
mode: 'ποΈ Server Island (SSG)'
|
|
48
|
+
})));
|
|
49
|
+
}
|
|
44
50
|
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
// Step 2: CSS Processing
|
|
52
|
+
logger.info('Step 2: Combining CSS...');
|
|
53
|
+
await buildAllCSS(root, outDir);
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
// Step 3: Assets
|
|
56
|
+
logger.info('Step 3: Copying static assets...');
|
|
57
|
+
await copyAllStaticAssets(root, outDir);
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
// Step 4: JavaScript Bundling
|
|
60
|
+
logger.info('Step 4: Bundling JavaScript...');
|
|
52
61
|
const buildEntry = join(buildDir, 'main.js');
|
|
62
|
+
const result = await bundleJavaScript(buildEntry, outDir, envVars);
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
// Step 5: HTML Generation
|
|
65
|
+
logger.info('Step 5: Generating HTML with Server Islands...');
|
|
66
|
+
const { loadConfig } = await import('./config/loadConfig.js');
|
|
67
|
+
const config = await loadConfig(root);
|
|
68
|
+
await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
|
|
58
69
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
target: 'browser',
|
|
63
|
-
minify: true,
|
|
64
|
-
splitting: true,
|
|
65
|
-
sourcemap: 'external',
|
|
66
|
-
naming: {
|
|
67
|
-
entry: '[name]-[hash].js',
|
|
68
|
-
chunk: 'chunks/[name]-[hash].js',
|
|
69
|
-
asset: '[name]-[hash].[ext]'
|
|
70
|
-
},
|
|
71
|
-
external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
|
|
72
|
-
define: {
|
|
73
|
-
'process.env.NODE_ENV': '"production"',
|
|
74
|
-
...Object.fromEntries(
|
|
75
|
-
Object.entries(envVars).map(([key, value]) => [
|
|
76
|
-
`process.env.${key}`,
|
|
77
|
-
JSON.stringify(value)
|
|
78
|
-
])
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
});
|
|
70
|
+
// Step 6: Sitemap
|
|
71
|
+
logger.info('Step 6: Generating sitemap.xml...');
|
|
72
|
+
await generateSitemap(routes, config, outDir);
|
|
82
73
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
logger.success('JavaScript bundled successfully');
|
|
74
|
+
// Step 7: Robots.txt
|
|
75
|
+
logger.info('Step 7: Generating robots.txt...');
|
|
76
|
+
await generateRobots(config, outDir, routes);
|
|
90
77
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// Clean up build directory
|
|
95
|
-
if (existsSync(buildDir)) {
|
|
96
|
-
rmSync(buildDir, { recursive: true });
|
|
97
|
-
logger.info('Cleaned up .bertuibuild/');
|
|
98
|
-
}
|
|
78
|
+
// Cleanup
|
|
79
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
99
80
|
|
|
81
|
+
// Summary
|
|
100
82
|
const duration = Date.now() - startTime;
|
|
101
|
-
|
|
102
|
-
logger.info(`π¦ Output: ${outDir}`);
|
|
103
|
-
|
|
104
|
-
logger.table(result.outputs.map(o => ({
|
|
105
|
-
file: o.path.replace(outDir, ''),
|
|
106
|
-
size: `${(o.size / 1024).toFixed(2)} KB`,
|
|
107
|
-
type: o.kind
|
|
108
|
-
})));
|
|
109
|
-
|
|
110
|
-
logger.bigLog('READY TO DEPLOY', { color: 'green' });
|
|
111
|
-
console.log('\nπ€ Deploy your app:\n');
|
|
112
|
-
console.log(' Vercel: bunx vercel');
|
|
113
|
-
console.log(' Netlify: bunx netlify deploy');
|
|
114
|
-
console.log('\nπ Preview locally:\n');
|
|
115
|
-
console.log(' cd dist && bun run preview\n');
|
|
83
|
+
showBuildSummary(routes, serverIslands, clientRoutes, duration);
|
|
116
84
|
|
|
117
85
|
} catch (error) {
|
|
118
86
|
logger.error(`Build failed: ${error.message}`);
|
|
119
|
-
if (error.stack)
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (existsSync(buildDir)) {
|
|
124
|
-
rmSync(buildDir, { recursive: true });
|
|
125
|
-
}
|
|
126
|
-
|
|
87
|
+
if (error.stack) logger.error(error.stack);
|
|
88
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
127
89
|
process.exit(1);
|
|
128
90
|
}
|
|
129
91
|
}
|
|
130
92
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
logger.info(' No public/ directory found');
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Copy from src/images/ to dist/images/
|
|
147
|
-
if (existsSync(srcImagesDir)) {
|
|
148
|
-
logger.info(' Copying src/images/ to dist/images/...');
|
|
149
|
-
const distImagesDir = join(outDir, 'images');
|
|
150
|
-
mkdirSync(distImagesDir, { recursive: true });
|
|
151
|
-
copyImages(srcImagesDir, distImagesDir);
|
|
152
|
-
} else {
|
|
153
|
-
logger.info(' No src/images/ directory found');
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
logger.success('β
All assets copied');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// COMBINE ALL CSS INTO ONE FILE
|
|
160
|
-
// COMBINE ALL CSS INTO ONE FILE - FIXED BUN API
|
|
161
|
-
async function buildAllCSS(root, outDir) {
|
|
162
|
-
const srcStylesDir = join(root, 'src', 'styles');
|
|
163
|
-
const stylesOutDir = join(outDir, 'styles');
|
|
164
|
-
|
|
165
|
-
mkdirSync(stylesOutDir, { recursive: true });
|
|
166
|
-
|
|
167
|
-
if (existsSync(srcStylesDir)) {
|
|
168
|
-
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
169
|
-
logger.info(`π¦ Found ${cssFiles.length} CSS files to combine`);
|
|
170
|
-
|
|
171
|
-
if (cssFiles.length === 0) {
|
|
172
|
-
logger.warn('β οΈ No CSS files found in src/styles/');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// COMBINE ALL CSS INTO ONE FILE
|
|
177
|
-
let combinedCSS = '';
|
|
178
|
-
let totalOriginalSize = 0;
|
|
179
|
-
|
|
180
|
-
for (const cssFile of cssFiles) {
|
|
181
|
-
const srcPath = join(srcStylesDir, cssFile);
|
|
182
|
-
const file = Bun.file(srcPath);
|
|
183
|
-
const cssContent = await file.text();
|
|
184
|
-
totalOriginalSize += file.size;
|
|
185
|
-
combinedCSS += `/* === ${cssFile} === */\n${cssContent}\n\n`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Write combined CSS
|
|
189
|
-
const combinedPath = join(stylesOutDir, 'bertui.min.css');
|
|
190
|
-
await Bun.write(combinedPath, combinedCSS);
|
|
191
|
-
|
|
192
|
-
// Minify it
|
|
193
|
-
const minified = await buildCSS(combinedPath, combinedPath);
|
|
194
|
-
|
|
195
|
-
// Get final size
|
|
196
|
-
const finalFile = Bun.file(combinedPath);
|
|
197
|
-
const finalSize = finalFile.size / 1024;
|
|
198
|
-
const originalSize = totalOriginalSize / 1024;
|
|
199
|
-
const savings = ((originalSize - finalSize) / originalSize * 100).toFixed(1);
|
|
200
|
-
|
|
201
|
-
logger.success(`β
Combined ${cssFiles.length} CSS files (${originalSize.toFixed(1)}KB) β bertui.min.css (${finalSize.toFixed(1)}KB, -${savings}%)`);
|
|
202
|
-
|
|
203
|
-
} else {
|
|
204
|
-
logger.warn('β οΈ No src/styles/ directory found');
|
|
205
|
-
// Create empty CSS file so build doesn't fail
|
|
206
|
-
const emptyPath = join(stylesOutDir, 'bertui.min.css');
|
|
207
|
-
await Bun.write(emptyPath, '/* No CSS files found */');
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
async function compileForBuild(root, buildDir, envVars) {
|
|
211
|
-
const srcDir = join(root, 'src');
|
|
212
|
-
const pagesDir = join(srcDir, 'pages');
|
|
213
|
-
|
|
214
|
-
if (!existsSync(srcDir)) {
|
|
215
|
-
throw new Error('src/ directory not found!');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
let routes = [];
|
|
219
|
-
if (existsSync(pagesDir)) {
|
|
220
|
-
routes = await discoverRoutes(pagesDir);
|
|
221
|
-
logger.info(`Found ${routes.length} routes`);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
await compileBuildDirectory(srcDir, buildDir, root, envVars);
|
|
225
|
-
|
|
226
|
-
if (routes.length > 0) {
|
|
227
|
-
await generateBuildRouter(routes, buildDir);
|
|
228
|
-
logger.info('Generated router for build');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return { routes };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function discoverRoutes(pagesDir) {
|
|
235
|
-
const routes = [];
|
|
236
|
-
|
|
237
|
-
async function scanDirectory(dir, basePath = '') {
|
|
238
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
239
|
-
|
|
240
|
-
for (const entry of entries) {
|
|
241
|
-
const fullPath = join(dir, entry.name);
|
|
242
|
-
const relativePath = join(basePath, entry.name);
|
|
243
|
-
|
|
244
|
-
if (entry.isDirectory()) {
|
|
245
|
-
await scanDirectory(fullPath, relativePath);
|
|
246
|
-
} else if (entry.isFile()) {
|
|
247
|
-
const ext = extname(entry.name);
|
|
248
|
-
|
|
249
|
-
if (ext === '.css') continue;
|
|
250
|
-
|
|
251
|
-
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
252
|
-
const fileName = entry.name.replace(ext, '');
|
|
253
|
-
|
|
254
|
-
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
255
|
-
|
|
256
|
-
if (fileName === 'index') {
|
|
257
|
-
route = route.replace('/index', '') || '/';
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
261
|
-
const type = isDynamic ? 'dynamic' : 'static';
|
|
262
|
-
|
|
263
|
-
routes.push({
|
|
264
|
-
route: route === '' ? '/' : route,
|
|
265
|
-
file: relativePath.replace(/\\/g, '/'),
|
|
266
|
-
path: fullPath,
|
|
267
|
-
type
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
await scanDirectory(pagesDir);
|
|
275
|
-
|
|
276
|
-
routes.sort((a, b) => {
|
|
277
|
-
if (a.type === b.type) {
|
|
278
|
-
return a.route.localeCompare(b.route);
|
|
279
|
-
}
|
|
280
|
-
return a.type === 'static' ? -1 : 1;
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
return routes;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function generateBuildRouter(routes, buildDir) {
|
|
287
|
-
const imports = routes.map((route, i) => {
|
|
288
|
-
const componentName = `Page${i}`;
|
|
289
|
-
const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
290
|
-
return `import ${componentName} from '${importPath}';`;
|
|
291
|
-
}).join('\n');
|
|
292
|
-
|
|
293
|
-
const routeConfigs = routes.map((route, i) => {
|
|
294
|
-
const componentName = `Page${i}`;
|
|
295
|
-
return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
|
|
296
|
-
}).join(',\n');
|
|
297
|
-
|
|
298
|
-
const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
299
|
-
|
|
300
|
-
const RouterContext = createContext(null);
|
|
301
|
-
|
|
302
|
-
export function useRouter() {
|
|
303
|
-
const context = useContext(RouterContext);
|
|
304
|
-
if (!context) {
|
|
305
|
-
throw new Error('useRouter must be used within a Router component');
|
|
306
|
-
}
|
|
307
|
-
return context;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
export function Router({ routes }) {
|
|
311
|
-
const [currentRoute, setCurrentRoute] = useState(null);
|
|
312
|
-
const [params, setParams] = useState({});
|
|
313
|
-
|
|
314
|
-
useEffect(() => {
|
|
315
|
-
matchAndSetRoute(window.location.pathname);
|
|
316
|
-
|
|
317
|
-
const handlePopState = () => {
|
|
318
|
-
matchAndSetRoute(window.location.pathname);
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
window.addEventListener('popstate', handlePopState);
|
|
322
|
-
return () => window.removeEventListener('popstate', handlePopState);
|
|
323
|
-
}, [routes]);
|
|
324
|
-
|
|
325
|
-
function matchAndSetRoute(pathname) {
|
|
326
|
-
for (const route of routes) {
|
|
327
|
-
if (route.type === 'static' && route.path === pathname) {
|
|
328
|
-
setCurrentRoute(route);
|
|
329
|
-
setParams({});
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
for (const route of routes) {
|
|
335
|
-
if (route.type === 'dynamic') {
|
|
336
|
-
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
337
|
-
const regex = new RegExp('^' + pattern + '$');
|
|
338
|
-
const match = pathname.match(regex);
|
|
339
|
-
|
|
340
|
-
if (match) {
|
|
341
|
-
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
342
|
-
const extractedParams = {};
|
|
343
|
-
paramNames.forEach((name, i) => {
|
|
344
|
-
extractedParams[name] = match[i + 1];
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
setCurrentRoute(route);
|
|
348
|
-
setParams(extractedParams);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
setCurrentRoute(null);
|
|
355
|
-
setParams({});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function navigate(path) {
|
|
359
|
-
window.history.pushState({}, '', path);
|
|
360
|
-
matchAndSetRoute(path);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const routerValue = {
|
|
364
|
-
currentRoute,
|
|
365
|
-
params,
|
|
366
|
-
navigate,
|
|
367
|
-
pathname: window.location.pathname
|
|
368
|
-
};
|
|
369
|
-
|
|
370
|
-
const Component = currentRoute?.component;
|
|
371
|
-
|
|
372
|
-
return React.createElement(
|
|
373
|
-
RouterContext.Provider,
|
|
374
|
-
{ value: routerValue },
|
|
375
|
-
Component ? React.createElement(Component, { params }) : React.createElement(NotFound, null)
|
|
376
|
-
);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function Link({ to, children, ...props }) {
|
|
380
|
-
const { navigate } = useRouter();
|
|
381
|
-
|
|
382
|
-
function handleClick(e) {
|
|
383
|
-
e.preventDefault();
|
|
384
|
-
navigate(to);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return React.createElement('a', { href: to, onClick: handleClick, ...props }, children);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function NotFound() {
|
|
391
|
-
return React.createElement(
|
|
392
|
-
'div',
|
|
393
|
-
{
|
|
394
|
-
style: {
|
|
395
|
-
display: 'flex',
|
|
396
|
-
flexDirection: 'column',
|
|
397
|
-
alignItems: 'center',
|
|
398
|
-
justifyContent: 'center',
|
|
399
|
-
minHeight: '100vh',
|
|
400
|
-
fontFamily: 'system-ui'
|
|
401
|
-
}
|
|
93
|
+
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]'
|
|
402
105
|
},
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
${imports}
|
|
413
|
-
|
|
414
|
-
export const routes = [
|
|
415
|
-
${routeConfigs}
|
|
416
|
-
];
|
|
417
|
-
`;
|
|
418
|
-
|
|
419
|
-
await Bun.write(join(buildDir, 'router.js'), routerCode);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
|
|
423
|
-
const files = readdirSync(srcDir);
|
|
424
|
-
|
|
425
|
-
for (const file of files) {
|
|
426
|
-
const srcPath = join(srcDir, file);
|
|
427
|
-
const stat = statSync(srcPath);
|
|
428
|
-
|
|
429
|
-
if (stat.isDirectory()) {
|
|
430
|
-
const subBuildDir = join(buildDir, file);
|
|
431
|
-
mkdirSync(subBuildDir, { recursive: true });
|
|
432
|
-
await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
|
|
433
|
-
} else {
|
|
434
|
-
const ext = extname(file);
|
|
435
|
-
|
|
436
|
-
if (ext === '.css') continue;
|
|
437
|
-
|
|
438
|
-
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
439
|
-
await compileBuildFile(srcPath, buildDir, file, root, envVars);
|
|
440
|
-
} else if (ext === '.js') {
|
|
441
|
-
const outPath = join(buildDir, file);
|
|
442
|
-
let code = await Bun.file(srcPath).text();
|
|
443
|
-
|
|
444
|
-
code = removeCSSImports(code);
|
|
445
|
-
code = replaceEnvInCode(code, envVars);
|
|
446
|
-
code = fixBuildImports(code, srcPath, outPath, root);
|
|
447
|
-
|
|
448
|
-
if (usesJSX(code) && !code.includes('import React')) {
|
|
449
|
-
code = `import React from 'react';\n${code}`;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
await Bun.write(outPath, code);
|
|
453
|
-
}
|
|
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
|
+
)
|
|
454
115
|
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
async function compileBuildFile(srcPath, buildDir, filename, root, envVars) {
|
|
459
|
-
const ext = extname(filename);
|
|
460
|
-
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
461
|
-
|
|
462
|
-
try {
|
|
463
|
-
let code = await Bun.file(srcPath).text();
|
|
464
|
-
|
|
465
|
-
code = removeCSSImports(code);
|
|
466
|
-
code = replaceEnvInCode(code, envVars);
|
|
467
|
-
|
|
468
|
-
const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
|
|
469
|
-
const outPath = join(buildDir, outFilename);
|
|
470
|
-
|
|
471
|
-
code = fixBuildImports(code, srcPath, outPath, root);
|
|
472
|
-
|
|
473
|
-
const transpiler = new Bun.Transpiler({
|
|
474
|
-
loader,
|
|
475
|
-
tsconfig: {
|
|
476
|
-
compilerOptions: {
|
|
477
|
-
jsx: 'react',
|
|
478
|
-
jsxFactory: 'React.createElement',
|
|
479
|
-
jsxFragmentFactory: 'React.Fragment'
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
let compiled = await transpiler.transform(code);
|
|
485
|
-
|
|
486
|
-
if (usesJSX(compiled) && !compiled.includes('import React')) {
|
|
487
|
-
compiled = `import React from 'react';\n${compiled}`;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
compiled = fixRelativeImports(compiled);
|
|
491
|
-
|
|
492
|
-
await Bun.write(outPath, compiled);
|
|
493
|
-
} catch (error) {
|
|
494
|
-
logger.error(`Failed to compile ${filename}: ${error.message}`);
|
|
495
|
-
throw error;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function usesJSX(code) {
|
|
500
|
-
return code.includes('React.createElement') ||
|
|
501
|
-
code.includes('React.Fragment') ||
|
|
502
|
-
/<[A-Z]/.test(code) ||
|
|
503
|
-
code.includes('jsx(') ||
|
|
504
|
-
code.includes('jsxs(');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function removeCSSImports(code) {
|
|
508
|
-
code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
|
|
509
|
-
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
510
|
-
return code;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function fixBuildImports(code, srcPath, outPath, root) {
|
|
514
|
-
const buildDir = join(root, '.bertuibuild');
|
|
515
|
-
const routerPath = join(buildDir, 'router.js');
|
|
516
|
-
|
|
517
|
-
const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
518
|
-
const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
|
|
519
|
-
|
|
520
|
-
code = code.replace(
|
|
521
|
-
/from\s+['"]bertui\/router['"]/g,
|
|
522
|
-
`from '${routerImport}'`
|
|
523
|
-
);
|
|
524
|
-
|
|
525
|
-
return code;
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
function fixRelativeImports(code) {
|
|
529
|
-
const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
|
|
530
|
-
|
|
531
|
-
code = code.replace(importRegex, (match, prefix, path) => {
|
|
532
|
-
if (path.endsWith('/') || /\.\w+$/.test(path)) {
|
|
533
|
-
return match;
|
|
534
|
-
}
|
|
535
|
-
return `from '${prefix}${path}.js';`;
|
|
536
116
|
});
|
|
537
117
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
try {
|
|
543
|
-
const metaMatch = code.match(/export\s+const\s+meta\s*=\s*\{/);
|
|
544
|
-
if (!metaMatch) return null;
|
|
545
|
-
|
|
546
|
-
const startIndex = metaMatch.index + metaMatch[0].length - 1;
|
|
547
|
-
let braceCount = 0;
|
|
548
|
-
let endIndex = startIndex;
|
|
549
|
-
|
|
550
|
-
for (let i = startIndex; i < code.length; i++) {
|
|
551
|
-
if (code[i] === '{') braceCount++;
|
|
552
|
-
if (code[i] === '}') {
|
|
553
|
-
braceCount--;
|
|
554
|
-
if (braceCount === 0) {
|
|
555
|
-
endIndex = i;
|
|
556
|
-
break;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (endIndex === startIndex) return null;
|
|
562
|
-
|
|
563
|
-
const metaString = code.substring(startIndex, endIndex + 1);
|
|
564
|
-
const meta = {};
|
|
565
|
-
const pairRegex = /(\w+)\s*:\s*(['"`])((?:(?!\2).)*)\2/g;
|
|
566
|
-
let match;
|
|
567
|
-
|
|
568
|
-
while ((match = pairRegex.exec(metaString)) !== null) {
|
|
569
|
-
const key = match[1];
|
|
570
|
-
const value = match[3];
|
|
571
|
-
meta[key] = value;
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return Object.keys(meta).length > 0 ? meta : null;
|
|
575
|
-
} catch (error) {
|
|
576
|
-
logger.warn(`Could not extract meta: ${error.message}`);
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// GENERATE HTML WITH SINGLE CSS FILE
|
|
582
|
-
async function generateProductionHTML(root, outDir, buildResult, routes) {
|
|
583
|
-
logger.info('Step 6: Generating HTML files with SINGLE CSS...');
|
|
584
|
-
|
|
585
|
-
const mainBundle = buildResult.outputs.find(o =>
|
|
586
|
-
o.path.includes('main') && o.kind === 'entry-point'
|
|
587
|
-
);
|
|
588
|
-
|
|
589
|
-
if (!mainBundle) {
|
|
590
|
-
logger.error('β Could not find main bundle');
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
const bundlePath = relative(outDir, mainBundle.path).replace(/\\/g, '/');
|
|
595
|
-
logger.info(`Main JS bundle: /${bundlePath}`);
|
|
596
|
-
|
|
597
|
-
// Load config
|
|
598
|
-
const { loadConfig } = await import('./config/loadConfig.js');
|
|
599
|
-
const config = await loadConfig(root);
|
|
600
|
-
const defaultMeta = config.meta || {};
|
|
601
|
-
|
|
602
|
-
logger.info(`Generating HTML for ${routes.length} routes...`);
|
|
603
|
-
|
|
604
|
-
for (const route of routes) {
|
|
605
|
-
try {
|
|
606
|
-
const sourceCode = await Bun.file(route.path).text();
|
|
607
|
-
const pageMeta = extractMetaFromSource(sourceCode);
|
|
608
|
-
const meta = { ...defaultMeta, ...pageMeta };
|
|
609
|
-
|
|
610
|
-
const html = generateHTML(meta, route, bundlePath);
|
|
611
|
-
|
|
612
|
-
let htmlPath;
|
|
613
|
-
if (route.route === '/') {
|
|
614
|
-
htmlPath = join(outDir, 'index.html');
|
|
615
|
-
} else {
|
|
616
|
-
const routeDir = join(outDir, route.route.replace(/^\//, ''));
|
|
617
|
-
mkdirSync(routeDir, { recursive: true });
|
|
618
|
-
htmlPath = join(routeDir, 'index.html');
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
await Bun.write(htmlPath, html);
|
|
622
|
-
logger.success(`β
Generated: ${route.route === '/' ? '/' : route.route}`);
|
|
623
|
-
|
|
624
|
-
} catch (error) {
|
|
625
|
-
logger.error(`Failed to generate HTML for ${route.route}: ${error.message}`);
|
|
626
|
-
}
|
|
118
|
+
if (!result.success) {
|
|
119
|
+
logger.error('JavaScript build failed!');
|
|
120
|
+
result.logs.forEach(log => logger.error(log.message));
|
|
121
|
+
process.exit(1);
|
|
627
122
|
}
|
|
628
123
|
|
|
629
|
-
logger.success('
|
|
124
|
+
logger.success('JavaScript bundled');
|
|
125
|
+
return result;
|
|
630
126
|
}
|
|
631
127
|
|
|
632
|
-
function
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
<meta name="description" content="${meta.description || 'Built with BertUI - Lightning fast React development'}">
|
|
641
|
-
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
642
|
-
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
643
|
-
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
644
|
-
|
|
645
|
-
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
646
|
-
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
647
|
-
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
648
|
-
<meta property="og:type" content="website">
|
|
649
|
-
<meta property="og:url" content="${route.route}">
|
|
128
|
+
function showBuildSummary(routes, serverIslands, clientRoutes, duration) {
|
|
129
|
+
logger.success(`β¨ Build complete in ${duration}ms`);
|
|
130
|
+
logger.bigLog('BUILD SUMMARY', { color: 'green' });
|
|
131
|
+
logger.info(`π Total routes: ${routes.length}`);
|
|
132
|
+
logger.info(`ποΈ Server Islands (SSG): ${serverIslands.length}`);
|
|
133
|
+
logger.info(`β‘ Client-only: ${clientRoutes.length}`);
|
|
134
|
+
logger.info(`πΊοΈ Sitemap: dist/sitemap.xml`);
|
|
135
|
+
logger.info(`π€ robots.txt: dist/robots.txt`);
|
|
650
136
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
<meta name="twitter:description" content="${meta.twitterDescription || meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
654
|
-
${meta.twitterImage || meta.ogImage ? `<meta name="twitter:image" content="${meta.twitterImage || meta.ogImage}">` : ''}
|
|
655
|
-
|
|
656
|
-
<!-- π₯ ONE CSS FILE FOR ALL PAGES - NO BULLSHIT -->
|
|
657
|
-
<link rel="stylesheet" href="/styles/bertui.min.css">
|
|
658
|
-
|
|
659
|
-
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
660
|
-
<link rel="canonical" href="${route.route}">
|
|
661
|
-
|
|
662
|
-
<script type="importmap">
|
|
663
|
-
{
|
|
664
|
-
"imports": {
|
|
665
|
-
"react": "https://esm.sh/react@18.2.0",
|
|
666
|
-
"react-dom": "https://esm.sh/react-dom@18.2.0",
|
|
667
|
-
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
|
668
|
-
"react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime"
|
|
669
|
-
}
|
|
137
|
+
if (serverIslands.length > 0) {
|
|
138
|
+
logger.success('β
Server Islands enabled - INSTANT content delivery!');
|
|
670
139
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
<body>
|
|
674
|
-
<div id="root"></div>
|
|
675
|
-
<script type="module" src="/${bundlePath}"></script>
|
|
676
|
-
</body>
|
|
677
|
-
</html>`;
|
|
140
|
+
|
|
141
|
+
logger.bigLog('READY TO DEPLOY π', { color: 'green' });
|
|
678
142
|
}
|