bertui 1.2.6 → 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 +1 -1
- package/src/build/compiler/file-transpiler.js +6 -1
- package/src/build/compiler/index.js +4 -26
- package/src/build/compiler/router-generator.js +10 -9
- package/src/build/generators/html-generator.js +38 -225
- package/src/build/server-island-validator.js +4 -148
- package/src/build.js +73 -52
- package/src/logger/logger.js +29 -5
- package/src/serve.js +2 -4
- package/src/server/dev-server-utils.js +11 -7
package/package.json
CHANGED
|
@@ -187,11 +187,16 @@ function _usesJSX(code) {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
function _removeCSSImports(code) {
|
|
190
|
+
// Replace CSS module imports with a Proxy so styles.foo = 'foo' at runtime
|
|
191
|
+
code = code.replace(
|
|
192
|
+
/import\s+(\w+)\s+from\s+['"][^'"]*\.module\.css['"];?\s*/g,
|
|
193
|
+
(_, varName) => `const ${varName} = new Proxy({}, { get: (_, k) => k });\n`
|
|
194
|
+
);
|
|
195
|
+
// Strip plain CSS imports entirely
|
|
190
196
|
code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
|
|
191
197
|
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
192
198
|
return code;
|
|
193
199
|
}
|
|
194
|
-
|
|
195
200
|
function _fixBuildImports(code, srcPath, outPath, root) {
|
|
196
201
|
const buildDir = join(root, '.bertuibuild');
|
|
197
202
|
const routerPath = join(buildDir, 'router.js');
|
|
@@ -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,12 +7,12 @@ 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
17
|
|
|
18
18
|
const RouterContext = createContext(null);
|
|
@@ -76,16 +76,16 @@ export function Router({ routes }) {
|
|
|
76
76
|
|
|
77
77
|
export function Link({ to, children, ...props }) {
|
|
78
78
|
const { navigate } = useRouter();
|
|
79
|
-
return React.createElement('a', {
|
|
80
|
-
href: to,
|
|
81
|
-
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
82
|
-
...props
|
|
79
|
+
return React.createElement('a', {
|
|
80
|
+
href: to,
|
|
81
|
+
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
82
|
+
...props
|
|
83
83
|
}, children);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function NotFound() {
|
|
87
87
|
return React.createElement('div', {
|
|
88
|
-
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
88
|
+
style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
|
|
89
89
|
justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
|
|
90
90
|
},
|
|
91
91
|
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
@@ -98,7 +98,8 @@ ${imports}
|
|
|
98
98
|
|
|
99
99
|
export const routes = [
|
|
100
100
|
${routeConfigs}
|
|
101
|
-
]
|
|
102
|
-
|
|
101
|
+
];
|
|
102
|
+
`;
|
|
103
|
+
|
|
103
104
|
await Bun.write(join(buildDir, 'router.js'), routerCode);
|
|
104
105
|
}
|
|
@@ -1,78 +1,58 @@
|
|
|
1
|
-
// bertui/src/build/generators/html-generator.js
|
|
1
|
+
// bertui/src/build/generators/html-generator.js
|
|
2
2
|
import { join, relative } from 'path';
|
|
3
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
|
|
|
7
|
-
export async function generateProductionHTML(root, outDir, buildResult, routes,
|
|
8
|
-
const mainBundle = buildResult.outputs.find(o =>
|
|
7
|
+
export async function generateProductionHTML(root, outDir, buildResult, routes, config) {
|
|
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
|
-
|
|
22
|
-
logger.info(
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < routes.length; i += BATCH_SIZE) {
|
|
27
|
-
const batch = routes.slice(i, i + BATCH_SIZE);
|
|
28
|
-
logger.debug(`Processing batch ${Math.floor(i/BATCH_SIZE) + 1}/${Math.ceil(routes.length/BATCH_SIZE)}`);
|
|
29
|
-
|
|
30
|
-
for (const route of batch) {
|
|
31
|
-
await processSingleRoute(route, serverIslands, config, defaultMeta, bundlePath, outDir, bertuiPackages);
|
|
32
|
-
}
|
|
20
|
+
|
|
21
|
+
logger.info(`Generating HTML for ${routes.length} routes...`);
|
|
22
|
+
|
|
23
|
+
for (const route of routes) {
|
|
24
|
+
await processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages);
|
|
33
25
|
}
|
|
34
|
-
|
|
35
|
-
logger.success(
|
|
26
|
+
|
|
27
|
+
logger.success(`HTML generation complete for ${routes.length} routes`);
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
async function copyBertuiPackagesToProduction(root, outDir) {
|
|
39
31
|
const nodeModulesDir = join(root, 'node_modules');
|
|
40
|
-
const packages = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
if (!existsSync(nodeModulesDir)) {
|
|
47
|
-
logger.debug('node_modules not found, skipping package copy');
|
|
48
|
-
return packages;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Copy bertui-icons
|
|
32
|
+
const packages = { bertuiIcons: false, bertuiAnimate: false, elysiaEden: false };
|
|
33
|
+
|
|
34
|
+
if (!existsSync(nodeModulesDir)) return packages;
|
|
35
|
+
|
|
52
36
|
const bertuiIconsSource = join(nodeModulesDir, 'bertui-icons');
|
|
53
37
|
if (existsSync(bertuiIconsSource)) {
|
|
54
38
|
try {
|
|
55
39
|
const bertuiIconsDest = join(outDir, 'node_modules', 'bertui-icons');
|
|
56
40
|
mkdirSync(join(outDir, 'node_modules'), { recursive: true });
|
|
57
41
|
cpSync(bertuiIconsSource, bertuiIconsDest, { recursive: true });
|
|
58
|
-
logger.success('✅ Copied bertui-icons to dist/node_modules/');
|
|
59
42
|
packages.bertuiIcons = true;
|
|
60
43
|
} catch (error) {
|
|
61
44
|
logger.error(`Failed to copy bertui-icons: ${error.message}`);
|
|
62
45
|
}
|
|
63
46
|
}
|
|
64
|
-
|
|
65
|
-
// Copy bertui-animate CSS files
|
|
47
|
+
|
|
66
48
|
const bertuiAnimateSource = join(nodeModulesDir, 'bertui-animate', 'dist');
|
|
67
49
|
if (existsSync(bertuiAnimateSource)) {
|
|
68
50
|
try {
|
|
69
51
|
const bertuiAnimateDest = join(outDir, 'css');
|
|
70
52
|
mkdirSync(bertuiAnimateDest, { recursive: true });
|
|
71
|
-
|
|
72
53
|
const minCSSPath = join(bertuiAnimateSource, 'bertui-animate.min.css');
|
|
73
54
|
if (existsSync(minCSSPath)) {
|
|
74
55
|
cpSync(minCSSPath, join(bertuiAnimateDest, 'bertui-animate.min.css'));
|
|
75
|
-
logger.success('✅ Copied bertui-animate.min.css to dist/css/');
|
|
76
56
|
packages.bertuiAnimate = true;
|
|
77
57
|
}
|
|
78
58
|
} catch (error) {
|
|
@@ -80,46 +60,29 @@ async function copyBertuiPackagesToProduction(root, outDir) {
|
|
|
80
60
|
}
|
|
81
61
|
}
|
|
82
62
|
|
|
83
|
-
// Copy @elysiajs/eden
|
|
84
63
|
const elysiaEdenSource = join(nodeModulesDir, '@elysiajs', 'eden');
|
|
85
64
|
if (existsSync(elysiaEdenSource)) {
|
|
86
65
|
try {
|
|
87
66
|
const elysiaEdenDest = join(outDir, 'node_modules', '@elysiajs', 'eden');
|
|
88
67
|
mkdirSync(join(outDir, 'node_modules', '@elysiajs'), { recursive: true });
|
|
89
68
|
cpSync(elysiaEdenSource, elysiaEdenDest, { recursive: true });
|
|
90
|
-
logger.success('✅ Copied @elysiajs/eden to dist/node_modules/');
|
|
91
69
|
packages.elysiaEden = true;
|
|
92
70
|
} catch (error) {
|
|
93
71
|
logger.error(`Failed to copy @elysiajs/eden: ${error.message}`);
|
|
94
72
|
}
|
|
95
73
|
}
|
|
96
|
-
|
|
74
|
+
|
|
97
75
|
return packages;
|
|
98
76
|
}
|
|
99
77
|
|
|
100
|
-
async function processSingleRoute(route,
|
|
78
|
+
async function processSingleRoute(route, config, defaultMeta, bundlePath, outDir, bertuiPackages) {
|
|
101
79
|
try {
|
|
102
80
|
const sourceCode = await Bun.file(route.path).text();
|
|
103
81
|
const pageMeta = extractMetaFromSource(sourceCode);
|
|
104
82
|
const meta = { ...defaultMeta, ...pageMeta };
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
let staticHTML = '';
|
|
109
|
-
|
|
110
|
-
if (isServerIsland) {
|
|
111
|
-
logger.info(`🏝️ Extracting static content: ${route.route}`);
|
|
112
|
-
staticHTML = await extractStaticHTMLFromComponent(sourceCode, route.path);
|
|
113
|
-
|
|
114
|
-
if (staticHTML) {
|
|
115
|
-
logger.success(`✅ Server Island rendered: ${route.route}`);
|
|
116
|
-
} else {
|
|
117
|
-
logger.warn(`⚠️ Could not extract HTML, falling back to client-only`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const html = generateHTML(meta, route, bundlePath, staticHTML, isServerIsland, bertuiPackages);
|
|
122
|
-
|
|
83
|
+
|
|
84
|
+
const html = generateHTML(meta, bundlePath, bertuiPackages);
|
|
85
|
+
|
|
123
86
|
let htmlPath;
|
|
124
87
|
if (route.route === '/') {
|
|
125
88
|
htmlPath = join(outDir, 'index.html');
|
|
@@ -128,210 +91,60 @@ async function processSingleRoute(route, serverIslands, config, defaultMeta, bun
|
|
|
128
91
|
mkdirSync(routeDir, { recursive: true });
|
|
129
92
|
htmlPath = join(routeDir, 'index.html');
|
|
130
93
|
}
|
|
131
|
-
|
|
94
|
+
|
|
132
95
|
await Bun.write(htmlPath, html);
|
|
133
|
-
|
|
134
|
-
if (isServerIsland) {
|
|
135
|
-
logger.success(`✅ Server Island: ${route.route} (instant content!)`);
|
|
136
|
-
} else {
|
|
137
|
-
logger.success(`✅ Client-only: ${route.route}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
} catch (error) {
|
|
141
|
-
logger.error(`Failed HTML for ${route.route}: ${error.message}`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
96
|
+
logger.success(`${route.route}`);
|
|
144
97
|
|
|
145
|
-
async function extractStaticHTMLFromComponent(sourceCode, filePath) {
|
|
146
|
-
try {
|
|
147
|
-
const returnMatch = sourceCode.match(/return\s*\(/);
|
|
148
|
-
if (!returnMatch) {
|
|
149
|
-
logger.warn(`⚠️ Could not find return statement in ${filePath}`);
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const codeBeforeReturn = sourceCode.substring(0, returnMatch.index);
|
|
154
|
-
const jsxContent = sourceCode.substring(returnMatch.index);
|
|
155
|
-
|
|
156
|
-
const hookPatterns = [
|
|
157
|
-
'useState', 'useEffect', 'useContext', 'useReducer',
|
|
158
|
-
'useCallback', 'useMemo', 'useRef', 'useImperativeHandle',
|
|
159
|
-
'useLayoutEffect', 'useDebugValue'
|
|
160
|
-
];
|
|
161
|
-
|
|
162
|
-
let hasHooks = false;
|
|
163
|
-
for (const hook of hookPatterns) {
|
|
164
|
-
const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
|
|
165
|
-
if (regex.test(codeBeforeReturn)) {
|
|
166
|
-
logger.error(`❌ Server Island at ${filePath} contains React hooks!`);
|
|
167
|
-
logger.error(` Server Islands must be pure HTML - no ${hook}, etc.`);
|
|
168
|
-
hasHooks = true;
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (hasHooks) return null;
|
|
174
|
-
|
|
175
|
-
const importLines = codeBeforeReturn.split('\n')
|
|
176
|
-
.filter(line => line.trim().startsWith('import'))
|
|
177
|
-
.join('\n');
|
|
178
|
-
|
|
179
|
-
const hasRouterImport = /from\s+['"]bertui\/router['"]/m.test(importLines);
|
|
180
|
-
|
|
181
|
-
if (hasRouterImport) {
|
|
182
|
-
logger.error(`❌ Server Island at ${filePath} imports from 'bertui/router'!`);
|
|
183
|
-
logger.error(` Server Islands cannot use Link - use <a> tags instead.`);
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const eventHandlers = [
|
|
188
|
-
'onClick=', 'onChange=', 'onSubmit=', 'onInput=', 'onFocus=',
|
|
189
|
-
'onBlur=', 'onMouseEnter=', 'onMouseLeave=', 'onKeyDown=',
|
|
190
|
-
'onKeyUp=', 'onScroll='
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
for (const handler of eventHandlers) {
|
|
194
|
-
if (jsxContent.includes(handler)) {
|
|
195
|
-
logger.error(`❌ Server Island uses event handler: ${handler.replace('=', '')}`);
|
|
196
|
-
logger.error(` Server Islands are static HTML - no interactivity allowed`);
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const fullReturnMatch = sourceCode.match(/return\s*\(([\s\S]*?)\);?\s*}/);
|
|
202
|
-
if (!fullReturnMatch) {
|
|
203
|
-
logger.warn(`⚠️ Could not extract JSX from ${filePath}`);
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
let html = fullReturnMatch[1].trim();
|
|
208
|
-
|
|
209
|
-
html = html.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
|
|
210
|
-
html = html.replace(/className=/g, 'class=');
|
|
211
|
-
|
|
212
|
-
html = html.replace(/style=\{\{([^}]+)\}\}/g, (match, styleObj) => {
|
|
213
|
-
const props = [];
|
|
214
|
-
let currentProp = '';
|
|
215
|
-
let depth = 0;
|
|
216
|
-
|
|
217
|
-
for (let i = 0; i < styleObj.length; i++) {
|
|
218
|
-
const char = styleObj[i];
|
|
219
|
-
if (char === '(') depth++;
|
|
220
|
-
if (char === ')') depth--;
|
|
221
|
-
|
|
222
|
-
if (char === ',' && depth === 0) {
|
|
223
|
-
props.push(currentProp.trim());
|
|
224
|
-
currentProp = '';
|
|
225
|
-
} else {
|
|
226
|
-
currentProp += char;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (currentProp.trim()) props.push(currentProp.trim());
|
|
230
|
-
|
|
231
|
-
const cssString = props
|
|
232
|
-
.map(prop => {
|
|
233
|
-
const colonIndex = prop.indexOf(':');
|
|
234
|
-
if (colonIndex === -1) return '';
|
|
235
|
-
|
|
236
|
-
const key = prop.substring(0, colonIndex).trim();
|
|
237
|
-
const value = prop.substring(colonIndex + 1).trim();
|
|
238
|
-
|
|
239
|
-
if (!key || !value) return '';
|
|
240
|
-
|
|
241
|
-
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
242
|
-
const cssValue = value.replace(/['"]/g, '');
|
|
243
|
-
|
|
244
|
-
return `${cssKey}: ${cssValue}`;
|
|
245
|
-
})
|
|
246
|
-
.filter(Boolean)
|
|
247
|
-
.join('; ');
|
|
248
|
-
|
|
249
|
-
return `style="${cssString}"`;
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
const voidElements = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img',
|
|
253
|
-
'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
254
|
-
|
|
255
|
-
html = html.replace(/<(\w+)([^>]*)\s*\/>/g, (match, tag, attrs) => {
|
|
256
|
-
if (voidElements.includes(tag.toLowerCase())) {
|
|
257
|
-
return match;
|
|
258
|
-
} else {
|
|
259
|
-
return `<${tag}${attrs}></${tag}>`;
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
html = html.replace(/\{`([^`]*)`\}/g, '$1');
|
|
264
|
-
html = html.replace(/\{(['"])(.*?)\1\}/g, '$2');
|
|
265
|
-
html = html.replace(/\{(\d+)\}/g, '$1');
|
|
266
|
-
|
|
267
|
-
logger.info(` Extracted ${html.length} chars of static HTML`);
|
|
268
|
-
return html;
|
|
269
|
-
|
|
270
98
|
} catch (error) {
|
|
271
|
-
logger.error(`Failed
|
|
272
|
-
return null;
|
|
99
|
+
logger.error(`Failed HTML for ${route.route}: ${error.message}`);
|
|
273
100
|
}
|
|
274
101
|
}
|
|
275
102
|
|
|
276
|
-
function generateHTML(meta,
|
|
277
|
-
const
|
|
278
|
-
? `<div id="root">${staticHTML}</div>`
|
|
279
|
-
: '<div id="root"></div>';
|
|
280
|
-
|
|
281
|
-
const comment = isServerIsland
|
|
282
|
-
? '<!-- 🏝️ Server Island: Static content rendered at build time -->'
|
|
283
|
-
: '<!-- ⚡ Client-only: Content rendered by JavaScript -->';
|
|
284
|
-
|
|
285
|
-
const bertuiIconsImport = bertuiPackages.bertuiIcons
|
|
103
|
+
function generateHTML(meta, bundlePath, bertuiPackages = {}) {
|
|
104
|
+
const bertuiIconsImport = bertuiPackages.bertuiIcons
|
|
286
105
|
? ',\n "bertui-icons": "/node_modules/bertui-icons/generated/index.js"'
|
|
287
106
|
: '';
|
|
288
|
-
|
|
107
|
+
|
|
289
108
|
const bertuiAnimateCSS = bertuiPackages.bertuiAnimate
|
|
290
109
|
? ' <link rel="stylesheet" href="/css/bertui-animate.min.css">'
|
|
291
110
|
: '';
|
|
292
111
|
|
|
293
|
-
// ✅ NEW: @elysiajs/eden local import map
|
|
294
112
|
const elysiaEdenImport = bertuiPackages.elysiaEden
|
|
295
113
|
? ',\n "@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"'
|
|
296
114
|
: '';
|
|
297
|
-
|
|
115
|
+
|
|
298
116
|
return `<!DOCTYPE html>
|
|
299
117
|
<html lang="${meta.lang || 'en'}">
|
|
300
118
|
<head>
|
|
301
119
|
<meta charset="UTF-8">
|
|
302
120
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
303
121
|
<title>${meta.title || 'BertUI App'}</title>
|
|
304
|
-
|
|
305
122
|
<meta name="description" content="${meta.description || 'Built with BertUI'}">
|
|
306
123
|
${meta.keywords ? `<meta name="keywords" content="${meta.keywords}">` : ''}
|
|
307
124
|
${meta.author ? `<meta name="author" content="${meta.author}">` : ''}
|
|
308
125
|
${meta.themeColor ? `<meta name="theme-color" content="${meta.themeColor}">` : ''}
|
|
309
|
-
|
|
310
126
|
<meta property="og:title" content="${meta.ogTitle || meta.title || 'BertUI App'}">
|
|
311
127
|
<meta property="og:description" content="${meta.ogDescription || meta.description || 'Built with BertUI'}">
|
|
312
128
|
${meta.ogImage ? `<meta property="og:image" content="${meta.ogImage}">` : ''}
|
|
313
|
-
|
|
314
129
|
<link rel="stylesheet" href="/styles/bertui.min.css">
|
|
315
130
|
${bertuiAnimateCSS}
|
|
316
131
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
317
|
-
|
|
318
132
|
<script type="importmap">
|
|
319
133
|
{
|
|
320
134
|
"imports": {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
135
|
+
"react": "https://esm.sh/react@18.2.0",
|
|
136
|
+
"react-dom": "https://esm.sh/react-dom@18.2.0",
|
|
137
|
+
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
|
|
138
|
+
"react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
|
|
139
|
+
"react/jsx-dev-runtime": "https://esm.sh/react@18.2.0/jsx-dev-runtime",
|
|
140
|
+
"@bunnyx/api": "/bunnyx-api/api-client.js",
|
|
141
|
+
"@elysiajs/eden": "/node_modules/@elysiajs/eden/dist/index.mjs"${bertuiIconsImport}${elysiaEdenImport}
|
|
328
142
|
}
|
|
329
143
|
}
|
|
330
144
|
</script>
|
|
331
145
|
</head>
|
|
332
146
|
<body>
|
|
333
|
-
|
|
334
|
-
${rootContent}
|
|
147
|
+
<div id="root"></div>
|
|
335
148
|
<script type="module" src="/${bundlePath}"></script>
|
|
336
149
|
</body>
|
|
337
150
|
</html>`;
|
|
@@ -1,156 +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
|
-
// Rule 1: No React hooks (FIXED: only match actual function calls)
|
|
16
|
-
const hookPatterns = [
|
|
17
|
-
'useState',
|
|
18
|
-
'useEffect',
|
|
19
|
-
'useContext',
|
|
20
|
-
'useReducer',
|
|
21
|
-
'useCallback',
|
|
22
|
-
'useMemo',
|
|
23
|
-
'useRef',
|
|
24
|
-
'useImperativeHandle',
|
|
25
|
-
'useLayoutEffect',
|
|
26
|
-
'useDebugValue',
|
|
27
|
-
'useId',
|
|
28
|
-
'useDeferredValue',
|
|
29
|
-
'useTransition',
|
|
30
|
-
'useSyncExternalStore'
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
for (const hook of hookPatterns) {
|
|
34
|
-
// FIXED: Only match hooks as function calls, not in text/comments
|
|
35
|
-
// Looks for: useState( or const [x] = useState(
|
|
36
|
-
const regex = new RegExp(`\\b${hook}\\s*\\(`, 'g');
|
|
37
|
-
if (regex.test(sourceCode)) {
|
|
38
|
-
errors.push(`❌ Uses React hook: ${hook}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Rule 2: No bertui/router imports
|
|
43
|
-
if (sourceCode.includes('from \'bertui/router\'') ||
|
|
44
|
-
sourceCode.includes('from "bertui/router"')) {
|
|
45
|
-
errors.push('❌ Imports from \'bertui/router\' (use <a> tags instead of Link)');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Rule 3: No browser APIs (FIXED: only match actual usage, not in strings/comments)
|
|
49
|
-
const browserAPIs = [
|
|
50
|
-
{ pattern: 'window\\.', name: 'window' },
|
|
51
|
-
{ pattern: 'document\\.', name: 'document' },
|
|
52
|
-
{ pattern: 'localStorage\\.', name: 'localStorage' },
|
|
53
|
-
{ pattern: 'sessionStorage\\.', name: 'sessionStorage' },
|
|
54
|
-
{ pattern: 'navigator\\.', name: 'navigator' },
|
|
55
|
-
{ pattern: 'location\\.', name: 'location' },
|
|
56
|
-
{ pattern: 'history\\.', name: 'history' },
|
|
57
|
-
{ pattern: '(?<!//.*|/\\*.*|\\*)\\bfetch\\s*\\(', name: 'fetch' },
|
|
58
|
-
{ pattern: '\\.addEventListener\\s*\\(', name: 'addEventListener' },
|
|
59
|
-
{ pattern: '\\.removeEventListener\\s*\\(', name: 'removeEventListener' },
|
|
60
|
-
{ pattern: '\\bsetTimeout\\s*\\(', name: 'setTimeout' },
|
|
61
|
-
{ pattern: '\\bsetInterval\\s*\\(', name: 'setInterval' },
|
|
62
|
-
{ pattern: '\\brequestAnimationFrame\\s*\\(', name: 'requestAnimationFrame' }
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
for (const api of browserAPIs) {
|
|
66
|
-
const regex = new RegExp(api.pattern, 'g');
|
|
67
|
-
if (regex.test(sourceCode)) {
|
|
68
|
-
if (api.name === 'console') {
|
|
69
|
-
logger.warn(`⚠️ ${filePath} uses console.log (will not work in static HTML)`);
|
|
70
|
-
} else {
|
|
71
|
-
errors.push(`❌ Uses browser API: ${api.name}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Rule 4: No event handlers (these won't work without JS)
|
|
77
|
-
const eventHandlers = [
|
|
78
|
-
'onClick=',
|
|
79
|
-
'onChange=',
|
|
80
|
-
'onSubmit=',
|
|
81
|
-
'onInput=',
|
|
82
|
-
'onFocus=',
|
|
83
|
-
'onBlur=',
|
|
84
|
-
'onMouseEnter=',
|
|
85
|
-
'onMouseLeave=',
|
|
86
|
-
'onKeyDown=',
|
|
87
|
-
'onKeyUp=',
|
|
88
|
-
'onScroll='
|
|
89
|
-
];
|
|
90
|
-
|
|
91
|
-
for (const handler of eventHandlers) {
|
|
92
|
-
if (sourceCode.includes(handler)) {
|
|
93
|
-
errors.push(`❌ Uses event handler: ${handler.replace('=', '')} (Server Islands are static HTML)`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Rule 5: Check for dynamic imports
|
|
98
|
-
if (/import\s*\(/.test(sourceCode)) {
|
|
99
|
-
errors.push('❌ Uses dynamic import() (not supported in Server Islands)');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Rule 6: Check for async/await (usually indicates API calls)
|
|
103
|
-
if (/async\s+function|async\s*\(|async\s+\w+\s*\(/.test(sourceCode)) {
|
|
104
|
-
errors.push('❌ Uses async/await (Server Islands must be synchronous)');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const valid = errors.length === 0;
|
|
108
|
-
|
|
109
|
-
return { valid, errors };
|
|
5
|
+
return { valid: true, errors: [] };
|
|
110
6
|
}
|
|
111
7
|
|
|
112
|
-
|
|
113
|
-
* Display validation errors in a clear format
|
|
114
|
-
*/
|
|
115
|
-
export function displayValidationErrors(filePath, errors) {
|
|
116
|
-
logger.error(`\n🏝️ Server Island validation failed: ${filePath}`);
|
|
117
|
-
logger.error('\nViolations:');
|
|
118
|
-
errors.forEach(error => logger.error(` ${error}`));
|
|
119
|
-
logger.error('\n📖 Server Island Rules:');
|
|
120
|
-
logger.error(' ✅ Pure static JSX only');
|
|
121
|
-
logger.error(' ❌ No React hooks (useState, useEffect, etc.)');
|
|
122
|
-
logger.error(' ❌ No Link component (use <a> tags)');
|
|
123
|
-
logger.error(' ❌ No browser APIs (window, document, fetch)');
|
|
124
|
-
logger.error(' ❌ No event handlers (onClick, onChange, etc.)');
|
|
125
|
-
logger.error('\n💡 Tip: Remove the "export const render = \\"server\\"" line');
|
|
126
|
-
logger.error(' if you need these features (page will be client-only).\n');
|
|
127
|
-
}
|
|
8
|
+
export function displayValidationErrors(filePath, errors) {}
|
|
128
9
|
|
|
129
|
-
/**
|
|
130
|
-
* Extract and validate all Server Islands in a project
|
|
131
|
-
*/
|
|
132
10
|
export async function validateAllServerIslands(routes) {
|
|
133
|
-
|
|
134
|
-
const validationResults = [];
|
|
135
|
-
|
|
136
|
-
for (const route of routes) {
|
|
137
|
-
const sourceCode = await Bun.file(route.path).text();
|
|
138
|
-
const isServerIsland = sourceCode.includes('export const render = "server"');
|
|
139
|
-
|
|
140
|
-
if (isServerIsland) {
|
|
141
|
-
const validation = validateServerIsland(sourceCode, route.path);
|
|
142
|
-
|
|
143
|
-
validationResults.push({
|
|
144
|
-
route: route.route,
|
|
145
|
-
path: route.path,
|
|
146
|
-
...validation
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
if (validation.valid) {
|
|
150
|
-
serverIslands.push(route);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return { serverIslands, validationResults };
|
|
11
|
+
return { serverIslands: [], validationResults: [] };
|
|
156
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,15 +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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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);
|
|
84
84
|
totalKB = (result.outputs.reduce((a, o) => a + (o.size || 0), 0) / 1024).toFixed(1);
|
|
85
85
|
logger.stepDone('Bundling JS', `${totalKB} KB · tree-shaken`);
|
|
86
86
|
|
|
87
87
|
// ── Step 9: HTML ─────────────────────────────────────────────────────────
|
|
88
88
|
logger.step(9, TOTAL_STEPS, 'Generating HTML');
|
|
89
|
-
await generateProductionHTML(root, outDir, result, routes,
|
|
89
|
+
await generateProductionHTML(root, outDir, result, routes, config);
|
|
90
90
|
logger.stepDone('Generating HTML', `${routes.length} pages`);
|
|
91
91
|
|
|
92
92
|
// ── Step 10: Sitemap + robots ────────────────────────────────────────────
|
|
@@ -97,23 +97,25 @@ export async function buildProduction(options = {}) {
|
|
|
97
97
|
|
|
98
98
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
try {
|
|
101
|
+
await analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') });
|
|
102
|
+
} catch (reportErr) {
|
|
103
|
+
logger.debug(`Bundle report generation skipped: ${reportErr.message}`);
|
|
104
|
+
}
|
|
102
105
|
|
|
103
|
-
// ── Summary ──────────────────────────────────────────────────────────────
|
|
104
106
|
logger.printSummary({
|
|
105
|
-
routes:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
outDir: 'dist/',
|
|
107
|
+
routes: routes.length,
|
|
108
|
+
interactive: analyzedRoutes.interactive.length,
|
|
109
|
+
staticRoutes: analyzedRoutes.static.length,
|
|
110
|
+
jsSize: `${totalKB} KB`,
|
|
111
|
+
outDir: 'dist/',
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
logger.cleanup();
|
|
113
115
|
return { success: true };
|
|
114
116
|
|
|
115
117
|
} catch (error) {
|
|
116
|
-
logger.stepFail('Build', error
|
|
118
|
+
logger.stepFail('Build', error?.message || String(error));
|
|
117
119
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
118
120
|
throw error;
|
|
119
121
|
}
|
|
@@ -155,6 +157,7 @@ async function generateProductionImportMap(root, config) {
|
|
|
155
157
|
}
|
|
156
158
|
|
|
157
159
|
async function copyNodeModulesToDist(root, outDir, importMap) {
|
|
160
|
+
const { mkdirSync } = await import('fs');
|
|
158
161
|
const dest = join(outDir, 'assets', 'node_modules');
|
|
159
162
|
mkdirSync(dest, { recursive: true });
|
|
160
163
|
const src = join(root, 'node_modules');
|
|
@@ -173,54 +176,72 @@ async function copyNodeModulesToDist(root, outDir, importMap) {
|
|
|
173
176
|
}
|
|
174
177
|
}
|
|
175
178
|
|
|
176
|
-
async function bundleJavaScript(buildEntry,
|
|
179
|
+
async function bundleJavaScript(buildEntry, outDir, envVars, buildDir, root, config) {
|
|
177
180
|
const originalCwd = process.cwd();
|
|
178
181
|
process.chdir(buildDir);
|
|
179
182
|
|
|
180
183
|
try {
|
|
181
|
-
const entrypoints = [buildEntry];
|
|
182
|
-
if (existsSync(routerPath)) entrypoints.push(routerPath);
|
|
183
|
-
|
|
184
184
|
const importMap = await generateProductionImportMap(root, config);
|
|
185
185
|
await Bun.write(join(outDir, 'import-map.json'), JSON.stringify({ imports: importMap }, null, 2));
|
|
186
186
|
await copyNodeModulesToDist(root, outDir, importMap);
|
|
187
187
|
|
|
188
|
-
// Copy @bunnyx/api client to dist so the importmap entry resolves
|
|
189
188
|
const bunnyxSrc = join(root, 'bunnyx-api', 'api-client.js');
|
|
190
189
|
if (existsSync(bunnyxSrc)) {
|
|
190
|
+
const { mkdirSync } = await import('fs');
|
|
191
191
|
mkdirSync(join(outDir, 'bunnyx-api'), { recursive: true });
|
|
192
192
|
await Bun.write(join(outDir, 'bunnyx-api', 'api-client.js'), Bun.file(bunnyxSrc));
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
sourcemap: 'external',
|
|
207
|
-
metafile: true,
|
|
208
|
-
naming: {
|
|
209
|
-
entry: 'js/[name]-[hash].js',
|
|
210
|
-
chunk: 'js/chunks/[name]-[hash].js',
|
|
211
|
-
asset: 'assets/[name]-[hash].[ext]',
|
|
195
|
+
const cssModulePlugin = {
|
|
196
|
+
name: 'css-modules',
|
|
197
|
+
setup(build) {
|
|
198
|
+
build.onLoad({ filter: /\.module\.css$/ }, () => ({
|
|
199
|
+
contents: 'export default new Proxy({}, { get: (_, k) => k });',
|
|
200
|
+
loader: 'js',
|
|
201
|
+
}));
|
|
202
|
+
build.onLoad({ filter: /\.css$/ }, () => ({
|
|
203
|
+
contents: '',
|
|
204
|
+
loader: 'js',
|
|
205
|
+
}));
|
|
212
206
|
},
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
207
|
+
};
|
|
208
|
+
|
|
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
|
+
}
|
|
221
241
|
|
|
222
242
|
if (!result.success) {
|
|
223
|
-
|
|
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'}`);
|
|
224
245
|
}
|
|
225
246
|
|
|
226
247
|
if (result.metafile) {
|
|
@@ -237,9 +258,9 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
|
|
|
237
258
|
export async function build(options = {}) {
|
|
238
259
|
try {
|
|
239
260
|
await buildProduction(options);
|
|
261
|
+
process.exit(0);
|
|
240
262
|
} catch (error) {
|
|
241
|
-
console.error(error);
|
|
263
|
+
console.error('Build error:', error?.message || String(error));
|
|
242
264
|
process.exit(1);
|
|
243
265
|
}
|
|
244
|
-
process.exit(0);
|
|
245
266
|
}
|
package/src/logger/logger.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { createWriteStream } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
|
+
import { mkdirSync, existsSync } from 'fs';
|
|
6
7
|
|
|
7
8
|
// ── ANSI helpers ─────────────────────────────────────────────────────────────
|
|
8
9
|
const C = {
|
|
@@ -121,12 +122,10 @@ export function fileProgress(current, total, filename) {
|
|
|
121
122
|
|
|
122
123
|
// ── Simple log levels (used internally, suppressed in compact mode) ───────────
|
|
123
124
|
export function info(msg) {
|
|
124
|
-
// In compact mode swallow routine info — only pass through to debug log
|
|
125
125
|
_debugLog('INFO', msg);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
export function success(msg) {
|
|
129
|
-
// Swallow — stepDone() is the visual replacement
|
|
130
129
|
_debugLog('SUCCESS', msg);
|
|
131
130
|
}
|
|
132
131
|
|
|
@@ -185,6 +184,9 @@ export function bigLog(title, opts = {}) {
|
|
|
185
184
|
// ── Build/Dev summary ─────────────────────────────────────────────────────────
|
|
186
185
|
export function printSummary(stats = {}) {
|
|
187
186
|
_stopSpinner();
|
|
187
|
+
// Close the log stream before printing summary
|
|
188
|
+
_closeLogStream();
|
|
189
|
+
|
|
188
190
|
process.stdout.write('\n');
|
|
189
191
|
|
|
190
192
|
const dur = _startTime ? `${((Date.now() - _startTime) / 1000).toFixed(2)}s` : '';
|
|
@@ -268,17 +270,38 @@ let _logStream = null;
|
|
|
268
270
|
function _debugLog(level, msg) {
|
|
269
271
|
if (!_logStream) {
|
|
270
272
|
try {
|
|
273
|
+
const logDir = join(process.cwd(), '.bertui');
|
|
274
|
+
if (!existsSync(logDir)) {
|
|
275
|
+
mkdirSync(logDir, { recursive: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
271
278
|
_logStream = createWriteStream(
|
|
272
|
-
join(
|
|
279
|
+
join(logDir, 'dev.log'),
|
|
273
280
|
{ flags: 'a' }
|
|
274
281
|
);
|
|
275
|
-
} catch {
|
|
282
|
+
} catch (err) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
276
285
|
}
|
|
277
286
|
const ts = new Date().toISOString().substring(11, 23);
|
|
278
287
|
_logStream.write(`[${ts}] [${level}] ${msg}\n`);
|
|
279
288
|
}
|
|
280
289
|
|
|
281
|
-
//
|
|
290
|
+
// NEW: Function to close the log stream
|
|
291
|
+
function _closeLogStream() {
|
|
292
|
+
if (_logStream) {
|
|
293
|
+
_logStream.end();
|
|
294
|
+
_logStream = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// NEW: Cleanup function to be called when done
|
|
299
|
+
export function cleanup() {
|
|
300
|
+
_stopSpinner();
|
|
301
|
+
_closeLogStream();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── Default export ────────────────────────────────────────────────────────────
|
|
282
305
|
export default {
|
|
283
306
|
printHeader,
|
|
284
307
|
step,
|
|
@@ -293,4 +316,5 @@ export default {
|
|
|
293
316
|
table,
|
|
294
317
|
bigLog,
|
|
295
318
|
printSummary,
|
|
319
|
+
cleanup, // Export cleanup function
|
|
296
320
|
};
|
package/src/serve.js
CHANGED
|
@@ -42,10 +42,8 @@ export async function startPreviewServer(options = {}) {
|
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
logger.info(`🌐 URL: http://localhost:${port}`);
|
|
48
|
-
logger.info(`⚡ Press Ctrl+C to stop`);
|
|
45
|
+
console.log(`\n 🚀 Preview running at http://localhost:${port}`);
|
|
46
|
+
console.log(` Press Ctrl+C to stop\n`);
|
|
49
47
|
|
|
50
48
|
// Track connections for graceful shutdown
|
|
51
49
|
const connections = new Set();
|
|
@@ -72,13 +72,17 @@ export async function buildDevImportMap(root) {
|
|
|
72
72
|
|
|
73
73
|
logger.info('🔄 Rebuilding dev import map (new packages detected)...');
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
// Initialize importMap with default mappings
|
|
76
|
+
const importMap = {
|
|
77
|
+
'react': 'https://esm.sh/react@18.2.0',
|
|
78
|
+
'react-dom': 'https://esm.sh/react-dom@18.2.0',
|
|
79
|
+
'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
|
|
80
|
+
'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
|
|
81
|
+
'react/jsx-dev-runtime': 'https://esm.sh/react@18.2.0/jsx-dev-runtime',
|
|
82
|
+
'@bunnyx/api': '/bunnyx-api/api-client.js',
|
|
83
|
+
'@elysiajs/eden': '/node_modules/@elysiajs/eden/dist/index.mjs',
|
|
84
|
+
};
|
|
85
|
+
|
|
82
86
|
const SKIP = new Set(['react', 'react-dom', '.bin', '.cache', '.package-lock.json', '.yarn']);
|
|
83
87
|
|
|
84
88
|
if (existsSync(nodeModulesDir)) {
|