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,4 +1,4 @@
|
|
|
1
|
-
// bertui/src/build/compiler/index.js
|
|
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
|
|
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
|
|
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,
|
|
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('
|
|
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(
|
|
23
|
-
|
|
24
|
-
const BATCH_SIZE = 5;
|
|
21
|
+
logger.info(`Generating HTML for ${routes.length} routes...`);
|
|
25
22
|
|
|
26
|
-
for (
|
|
27
|
-
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
50
|
-
logger.stepDone('Compiling', `${routes.length} routes
|
|
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))
|
|
82
|
-
|
|
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,
|
|
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:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
'
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
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
|
}
|