bertui 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -197
- package/index.js +16 -8
- package/package.json +1 -1
- package/src/build/compiler/file-transpiler.js +119 -76
- package/src/build/compiler/index.js +23 -15
- package/src/build/compiler/route-discoverer.js +4 -3
- package/src/build/generators/sitemap-generator.js +1 -1
- package/src/build/processors/css-builder.js +45 -41
- package/src/build.js +147 -90
- package/src/client/compiler.js +169 -157
- package/src/config/defaultConfig.js +13 -4
- package/src/config/loadConfig.js +47 -32
- package/src/dev.js +222 -49
- package/src/logger/logger.js +294 -16
- package/src/server/dev-handler.js +11 -0
- package/src/server/dev-server-utils.js +262 -160
- package/src/utils/importhow.js +52 -0
package/src/client/compiler.js
CHANGED
|
@@ -1,175 +1,210 @@
|
|
|
1
|
+
// bertui/src/client/compiler.js - WITH IMPORTHOW ALIAS SUPPORT
|
|
1
2
|
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
2
3
|
import { join, extname, relative, dirname } from 'path';
|
|
3
4
|
import { transform } from 'lightningcss';
|
|
4
5
|
import logger from '../logger/logger.js';
|
|
5
6
|
import { loadEnvVariables, generateEnvCode, replaceEnvInCode } from '../utils/env.js';
|
|
7
|
+
import { buildAliasMap, rewriteAliasImports, getAliasDirs } from '../utils/importhow.js';
|
|
6
8
|
|
|
7
9
|
export async function compileProject(root) {
|
|
8
10
|
logger.bigLog('COMPILING PROJECT', { color: 'blue' });
|
|
9
|
-
|
|
10
|
-
const srcDir
|
|
11
|
+
|
|
12
|
+
const srcDir = join(root, 'src');
|
|
11
13
|
const pagesDir = join(srcDir, 'pages');
|
|
12
|
-
const outDir
|
|
13
|
-
|
|
14
|
+
const outDir = join(root, '.bertui', 'compiled');
|
|
15
|
+
|
|
14
16
|
if (!existsSync(srcDir)) {
|
|
15
17
|
logger.error('src/ directory not found!');
|
|
16
18
|
process.exit(1);
|
|
17
19
|
}
|
|
18
|
-
|
|
20
|
+
|
|
19
21
|
if (!existsSync(outDir)) {
|
|
20
22
|
mkdirSync(outDir, { recursive: true });
|
|
21
23
|
logger.info('Created .bertui/compiled/');
|
|
22
24
|
}
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
const envVars = loadEnvVariables(root);
|
|
25
27
|
if (Object.keys(envVars).length > 0) {
|
|
26
28
|
logger.info(`Loaded ${Object.keys(envVars).length} environment variables`);
|
|
27
29
|
}
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
const envCode = generateEnvCode(envVars);
|
|
30
32
|
await Bun.write(join(outDir, 'env.js'), envCode);
|
|
31
|
-
|
|
33
|
+
|
|
34
|
+
// ── Load config for importhow ────────────────────────────────────────────
|
|
35
|
+
let importhow = {};
|
|
36
|
+
try {
|
|
37
|
+
const { loadConfig } = await import('../config/loadConfig.js');
|
|
38
|
+
const config = await loadConfig(root);
|
|
39
|
+
importhow = config.importhow || {};
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
|
|
42
|
+
const aliasMap = buildAliasMap(importhow, root, outDir);
|
|
43
|
+
|
|
44
|
+
if (aliasMap.size > 0) {
|
|
45
|
+
logger.info(`🔗 importhow: ${aliasMap.size} alias(es) active`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Discover routes ──────────────────────────────────────────────────────
|
|
32
49
|
let routes = [];
|
|
33
50
|
if (existsSync(pagesDir)) {
|
|
34
51
|
routes = await discoverRoutes(pagesDir);
|
|
35
52
|
logger.info(`Discovered ${routes.length} routes`);
|
|
36
|
-
|
|
53
|
+
|
|
37
54
|
if (routes.length > 0) {
|
|
38
55
|
logger.bigLog('ROUTES DISCOVERED', { color: 'blue' });
|
|
39
56
|
logger.table(routes.map((r, i) => ({
|
|
40
|
-
'': i,
|
|
41
|
-
route: r.route,
|
|
42
|
-
file: r.file,
|
|
43
|
-
type: r.type
|
|
57
|
+
'': i, route: r.route, file: r.file, type: r.type
|
|
44
58
|
})));
|
|
45
59
|
}
|
|
46
60
|
}
|
|
47
|
-
|
|
61
|
+
|
|
62
|
+
// ── Compile src/ ─────────────────────────────────────────────────────────
|
|
48
63
|
const startTime = Date.now();
|
|
49
|
-
const stats = await compileDirectory(srcDir, outDir, root, envVars);
|
|
64
|
+
const stats = await compileDirectory(srcDir, outDir, root, envVars, aliasMap);
|
|
65
|
+
|
|
66
|
+
// ── Compile alias dirs (importhow targets) ───────────────────────────────
|
|
67
|
+
// NOTE: use raw importhow config here, NOT aliasMap
|
|
68
|
+
// aliasMap resolves to output dirs (for rewriting) — we need source dirs for compilation
|
|
69
|
+
for (const [alias, relPath] of Object.entries(importhow)) {
|
|
70
|
+
const absSrcDir = join(root, relPath);
|
|
71
|
+
if (!existsSync(absSrcDir)) {
|
|
72
|
+
logger.warn(`⚠️ importhow alias "${alias}" points to missing dir: ${absSrcDir}`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const aliasOutDir = join(outDir, alias);
|
|
76
|
+
mkdirSync(aliasOutDir, { recursive: true });
|
|
77
|
+
logger.info(`📦 Compiling alias [${alias}] → ${aliasOutDir}`);
|
|
78
|
+
const aliasStats = await compileDirectory(absSrcDir, aliasOutDir, root, envVars, aliasMap);
|
|
79
|
+
stats.files += aliasStats.files;
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
const duration = Date.now() - startTime;
|
|
51
|
-
|
|
83
|
+
|
|
52
84
|
if (routes.length > 0) {
|
|
53
85
|
await generateRouter(routes, outDir, root);
|
|
54
86
|
logger.info('Generated router.js');
|
|
55
87
|
}
|
|
56
|
-
|
|
88
|
+
|
|
57
89
|
logger.success(`Compiled ${stats.files} files in ${duration}ms`);
|
|
58
90
|
logger.info(`Output: ${outDir}`);
|
|
59
|
-
|
|
91
|
+
|
|
60
92
|
return { outDir, stats, routes };
|
|
61
93
|
}
|
|
62
94
|
|
|
63
95
|
export async function compileFile(srcPath, root) {
|
|
64
|
-
const srcDir
|
|
65
|
-
const outDir
|
|
96
|
+
const srcDir = join(root, 'src');
|
|
97
|
+
const outDir = join(root, '.bertui', 'compiled');
|
|
66
98
|
const envVars = loadEnvVariables(root);
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
99
|
+
const ext = extname(srcPath);
|
|
100
|
+
|
|
101
|
+
let importhow = {};
|
|
102
|
+
try {
|
|
103
|
+
const { loadConfig } = await import('../config/loadConfig.js');
|
|
104
|
+
const config = await loadConfig(root);
|
|
105
|
+
importhow = config.importhow || {};
|
|
106
|
+
} catch (_) {}
|
|
107
|
+
|
|
108
|
+
const aliasMap = buildAliasMap(importhow, root, outDir);
|
|
109
|
+
|
|
70
110
|
if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
|
|
71
|
-
|
|
111
|
+
|
|
72
112
|
if (srcPath.endsWith('.module.css')) {
|
|
73
113
|
await compileCSSModule(srcPath, root);
|
|
74
114
|
return { success: true };
|
|
75
115
|
}
|
|
76
|
-
|
|
116
|
+
|
|
77
117
|
if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
78
|
-
const fileName
|
|
79
|
-
|
|
118
|
+
const fileName = srcPath.split('/').pop();
|
|
119
|
+
const relativePath = relative(srcDir, srcPath);
|
|
120
|
+
await compileFileInternal(srcPath, outDir, fileName, relativePath, root, envVars, aliasMap);
|
|
80
121
|
return {
|
|
81
122
|
outputPath: relativePath.replace(/\.(jsx|tsx|ts)$/, '.js'),
|
|
82
123
|
success: true
|
|
83
124
|
};
|
|
84
125
|
}
|
|
85
|
-
|
|
126
|
+
|
|
86
127
|
if (ext === '.js') {
|
|
87
128
|
const fileName = srcPath.split('/').pop();
|
|
88
|
-
const outPath
|
|
129
|
+
const outPath = join(outDir, fileName);
|
|
89
130
|
let code = await Bun.file(srcPath).text();
|
|
90
131
|
code = transformCSSModuleImports(code, srcPath, root);
|
|
91
132
|
code = removePlainCSSImports(code);
|
|
92
133
|
code = replaceEnvInCode(code, envVars);
|
|
93
134
|
code = fixRouterImports(code, outPath, root);
|
|
135
|
+
code = rewriteAliasImports(code, outPath, aliasMap);
|
|
94
136
|
if (usesJSX(code) && !code.includes('import React')) {
|
|
95
137
|
code = `import React from 'react';\n${code}`;
|
|
96
138
|
}
|
|
97
139
|
await Bun.write(outPath, code);
|
|
98
|
-
return { outputPath:
|
|
140
|
+
return { outputPath: relative(srcDir, srcPath), success: true };
|
|
99
141
|
}
|
|
100
|
-
|
|
142
|
+
|
|
101
143
|
return { success: false };
|
|
102
144
|
}
|
|
103
145
|
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
147
|
+
// Route discovery
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
149
|
|
|
108
150
|
async function discoverRoutes(pagesDir) {
|
|
109
151
|
const routes = [];
|
|
110
|
-
|
|
152
|
+
|
|
111
153
|
async function scanDirectory(dir, basePath = '') {
|
|
112
154
|
const entries = readdirSync(dir, { withFileTypes: true });
|
|
113
|
-
|
|
155
|
+
|
|
114
156
|
for (const entry of entries) {
|
|
115
|
-
const fullPath
|
|
157
|
+
const fullPath = join(dir, entry.name);
|
|
116
158
|
const relativePath = join(basePath, entry.name);
|
|
117
|
-
|
|
159
|
+
|
|
118
160
|
if (entry.isDirectory()) {
|
|
119
161
|
await scanDirectory(fullPath, relativePath);
|
|
120
162
|
} else if (entry.isFile()) {
|
|
121
|
-
const ext
|
|
163
|
+
const ext = extname(entry.name);
|
|
122
164
|
if (ext === '.css') continue;
|
|
123
|
-
|
|
165
|
+
|
|
124
166
|
if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
|
|
125
167
|
const fileName = entry.name.replace(ext, '');
|
|
126
|
-
|
|
127
|
-
// ✅ Only loading is reserved — index is a valid route (renamed to /)
|
|
128
168
|
if (fileName === 'loading') continue;
|
|
129
169
|
|
|
130
170
|
let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
|
|
131
|
-
if (fileName === 'index')
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
171
|
+
if (fileName === 'index') route = route.replace('/index', '') || '/';
|
|
172
|
+
|
|
135
173
|
const isDynamic = fileName.includes('[') && fileName.includes(']');
|
|
136
|
-
|
|
137
174
|
routes.push({
|
|
138
|
-
route:
|
|
139
|
-
file:
|
|
140
|
-
path:
|
|
141
|
-
type:
|
|
175
|
+
route: route === '' ? '/' : route,
|
|
176
|
+
file: relativePath.replace(/\\/g, '/'),
|
|
177
|
+
path: fullPath,
|
|
178
|
+
type: isDynamic ? 'dynamic' : 'static'
|
|
142
179
|
});
|
|
143
180
|
}
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
183
|
}
|
|
147
|
-
|
|
184
|
+
|
|
148
185
|
await scanDirectory(pagesDir);
|
|
149
186
|
routes.sort((a, b) => {
|
|
150
187
|
if (a.type === b.type) return a.route.localeCompare(b.route);
|
|
151
188
|
return a.type === 'static' ? -1 : 1;
|
|
152
189
|
});
|
|
153
|
-
|
|
154
190
|
return routes;
|
|
155
191
|
}
|
|
156
192
|
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
193
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
194
|
+
// Router generation
|
|
195
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
160
196
|
|
|
161
197
|
async function generateRouter(routes, outDir, root) {
|
|
162
198
|
const imports = routes.map((route, i) => {
|
|
163
199
|
const componentName = `Page${i}`;
|
|
164
|
-
const importPath
|
|
200
|
+
const importPath = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
|
|
165
201
|
return `import ${componentName} from '${importPath}';`;
|
|
166
202
|
}).join('\n');
|
|
167
|
-
|
|
168
|
-
const routeConfigs = routes.map((route, i) =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
|
|
204
|
+
const routeConfigs = routes.map((route, i) =>
|
|
205
|
+
` { path: '${route.route}', component: Page${i}, type: '${route.type}' }`
|
|
206
|
+
).join(',\n');
|
|
207
|
+
|
|
173
208
|
const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
|
|
174
209
|
|
|
175
210
|
const RouterContext = createContext(null);
|
|
@@ -194,28 +229,23 @@ export function Router({ routes }) {
|
|
|
194
229
|
function matchAndSetRoute(pathname) {
|
|
195
230
|
for (const route of routes) {
|
|
196
231
|
if (route.type === 'static' && route.path === pathname) {
|
|
197
|
-
setCurrentRoute(route);
|
|
198
|
-
setParams({});
|
|
199
|
-
return;
|
|
232
|
+
setCurrentRoute(route); setParams({}); return;
|
|
200
233
|
}
|
|
201
234
|
}
|
|
202
235
|
for (const route of routes) {
|
|
203
236
|
if (route.type === 'dynamic') {
|
|
204
237
|
const pattern = route.path.replace(/\\[([^\\]]+)\\]/g, '([^/]+)');
|
|
205
|
-
const regex
|
|
206
|
-
const match
|
|
238
|
+
const regex = new RegExp('^' + pattern + '$');
|
|
239
|
+
const match = pathname.match(regex);
|
|
207
240
|
if (match) {
|
|
208
241
|
const paramNames = [...route.path.matchAll(/\\[([^\\]]+)\\]/g)].map(m => m[1]);
|
|
209
|
-
const
|
|
210
|
-
paramNames.forEach((name, i) => {
|
|
211
|
-
setCurrentRoute(route);
|
|
212
|
-
setParams(extractedParams);
|
|
213
|
-
return;
|
|
242
|
+
const extracted = {};
|
|
243
|
+
paramNames.forEach((name, i) => { extracted[name] = match[i + 1]; });
|
|
244
|
+
setCurrentRoute(route); setParams(extracted); return;
|
|
214
245
|
}
|
|
215
246
|
}
|
|
216
247
|
}
|
|
217
|
-
setCurrentRoute(null);
|
|
218
|
-
setParams({});
|
|
248
|
+
setCurrentRoute(null); setParams({});
|
|
219
249
|
}
|
|
220
250
|
|
|
221
251
|
function navigate(path) {
|
|
@@ -234,9 +264,7 @@ export function Router({ routes }) {
|
|
|
234
264
|
export function Link({ to, children, ...props }) {
|
|
235
265
|
const { navigate } = useRouter();
|
|
236
266
|
return React.createElement('a', {
|
|
237
|
-
href: to,
|
|
238
|
-
onClick: (e) => { e.preventDefault(); navigate(to); },
|
|
239
|
-
...props
|
|
267
|
+
href: to, onClick: (e) => { e.preventDefault(); navigate(to); }, ...props
|
|
240
268
|
}, children);
|
|
241
269
|
}
|
|
242
270
|
|
|
@@ -246,8 +274,8 @@ function NotFound() {
|
|
|
246
274
|
justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
|
|
247
275
|
},
|
|
248
276
|
React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
|
|
249
|
-
React.createElement('p',
|
|
250
|
-
React.createElement('a',
|
|
277
|
+
React.createElement('p', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
|
|
278
|
+
React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
|
|
251
279
|
);
|
|
252
280
|
}
|
|
253
281
|
|
|
@@ -256,49 +284,43 @@ ${imports}
|
|
|
256
284
|
export const routes = [
|
|
257
285
|
${routeConfigs}
|
|
258
286
|
];`;
|
|
259
|
-
|
|
287
|
+
|
|
260
288
|
await Bun.write(join(outDir, 'router.js'), routerCode);
|
|
261
289
|
}
|
|
262
290
|
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// Directory compilation
|
|
293
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
266
294
|
|
|
267
|
-
async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
295
|
+
async function compileDirectory(srcDir, outDir, root, envVars, aliasMap) {
|
|
268
296
|
const stats = { files: 0, skipped: 0 };
|
|
269
297
|
const files = readdirSync(srcDir);
|
|
270
|
-
|
|
298
|
+
|
|
271
299
|
for (const file of files) {
|
|
272
300
|
const srcPath = join(srcDir, file);
|
|
273
|
-
const stat
|
|
274
|
-
|
|
301
|
+
const stat = statSync(srcPath);
|
|
302
|
+
|
|
275
303
|
if (stat.isDirectory()) {
|
|
276
|
-
if (file === 'templates') {
|
|
277
|
-
logger.debug('⏭️ Skipping src/templates/');
|
|
278
|
-
continue;
|
|
279
|
-
}
|
|
304
|
+
if (file === 'templates') { logger.debug('⏭️ Skipping src/templates/'); continue; }
|
|
280
305
|
const subOutDir = join(outDir, file);
|
|
281
306
|
mkdirSync(subOutDir, { recursive: true });
|
|
282
|
-
const subStats = await compileDirectory(srcPath, subOutDir, root, envVars);
|
|
283
|
-
stats.files
|
|
307
|
+
const subStats = await compileDirectory(srcPath, subOutDir, root, envVars, aliasMap);
|
|
308
|
+
stats.files += subStats.files;
|
|
284
309
|
stats.skipped += subStats.skipped;
|
|
285
310
|
} else {
|
|
286
|
-
const ext
|
|
311
|
+
const ext = extname(file);
|
|
287
312
|
const relativePath = relative(join(root, 'src'), srcPath);
|
|
288
313
|
|
|
289
|
-
// ✅ MUST check .module.css BEFORE plain .css
|
|
290
314
|
if (file.endsWith('.module.css')) {
|
|
291
315
|
await compileCSSModule(srcPath, root);
|
|
292
316
|
stats.files++;
|
|
293
317
|
} else if (ext === '.css') {
|
|
294
|
-
// Plain CSS → copy to .bertui/styles/ for <link> injection
|
|
295
318
|
const stylesOutDir = join(root, '.bertui', 'styles');
|
|
296
319
|
if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
|
|
297
320
|
await Bun.write(join(stylesOutDir, file), Bun.file(srcPath));
|
|
298
|
-
logger.debug(`Copied CSS: ${relativePath}`);
|
|
299
321
|
stats.files++;
|
|
300
322
|
} else if (['.jsx', '.tsx', '.ts'].includes(ext)) {
|
|
301
|
-
await compileFileInternal(srcPath, outDir, file, relativePath, root, envVars);
|
|
323
|
+
await compileFileInternal(srcPath, outDir, file, relativePath, root, envVars, aliasMap);
|
|
302
324
|
stats.files++;
|
|
303
325
|
} else if (ext === '.js') {
|
|
304
326
|
const outPath = join(outDir, file);
|
|
@@ -310,21 +332,22 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
|
|
|
310
332
|
if (usesJSX(code) && !code.includes('import React')) {
|
|
311
333
|
code = `import React from 'react';\n${code}`;
|
|
312
334
|
}
|
|
335
|
+
// alias rewrite last — after all other transforms
|
|
336
|
+
code = rewriteAliasImports(code, outPath, aliasMap);
|
|
313
337
|
await Bun.write(outPath, code);
|
|
314
|
-
logger.debug(`Copied: ${relativePath}`);
|
|
315
338
|
stats.files++;
|
|
316
339
|
} else {
|
|
317
340
|
stats.skipped++;
|
|
318
341
|
}
|
|
319
342
|
}
|
|
320
343
|
}
|
|
321
|
-
|
|
344
|
+
|
|
322
345
|
return stats;
|
|
323
346
|
}
|
|
324
347
|
|
|
325
|
-
//
|
|
326
|
-
// CSS
|
|
327
|
-
//
|
|
348
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
349
|
+
// CSS Modules
|
|
350
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
328
351
|
|
|
329
352
|
function hashClassName(filename, className) {
|
|
330
353
|
const str = filename + className;
|
|
@@ -340,14 +363,10 @@ function scopeCSSModule(cssText, filename) {
|
|
|
340
363
|
const classNames = new Set();
|
|
341
364
|
const classRegex = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*[{,\s:]/g;
|
|
342
365
|
let match;
|
|
343
|
-
while ((match = classRegex.exec(cssText)) !== null)
|
|
344
|
-
classNames.add(match[1]);
|
|
345
|
-
}
|
|
366
|
+
while ((match = classRegex.exec(cssText)) !== null) classNames.add(match[1]);
|
|
346
367
|
|
|
347
368
|
const mapping = {};
|
|
348
|
-
for (const cls of classNames) {
|
|
349
|
-
mapping[cls] = `${cls}_${hashClassName(filename, cls)}`;
|
|
350
|
-
}
|
|
369
|
+
for (const cls of classNames) mapping[cls] = `${cls}_${hashClassName(filename, cls)}`;
|
|
351
370
|
|
|
352
371
|
let scopedCSS = cssText;
|
|
353
372
|
for (const [original, scoped] of Object.entries(mapping)) {
|
|
@@ -356,17 +375,14 @@ function scopeCSSModule(cssText, filename) {
|
|
|
356
375
|
`.${scoped}`
|
|
357
376
|
);
|
|
358
377
|
}
|
|
359
|
-
|
|
360
378
|
return { mapping, scopedCSS };
|
|
361
379
|
}
|
|
362
380
|
|
|
363
381
|
async function compileCSSModule(srcPath, root) {
|
|
364
|
-
const filename
|
|
365
|
-
const cssText
|
|
366
|
-
|
|
382
|
+
const filename = srcPath.split('/').pop();
|
|
383
|
+
const cssText = await Bun.file(srcPath).text();
|
|
367
384
|
const { mapping, scopedCSS } = scopeCSSModule(cssText, filename);
|
|
368
385
|
|
|
369
|
-
// Run through LightningCSS for nesting support
|
|
370
386
|
let finalCSS = scopedCSS;
|
|
371
387
|
try {
|
|
372
388
|
const { code } = transform({
|
|
@@ -381,71 +397,58 @@ async function compileCSSModule(srcPath, root) {
|
|
|
381
397
|
logger.warn(`LightningCSS failed for ${filename}: ${e.message}`);
|
|
382
398
|
}
|
|
383
399
|
|
|
384
|
-
// ✅ Scoped CSS → .bertui/styles/ (served as <link> tags)
|
|
385
400
|
const stylesOutDir = join(root, '.bertui', 'styles');
|
|
386
401
|
if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
|
|
387
402
|
await Bun.write(join(stylesOutDir, filename), finalCSS);
|
|
388
403
|
|
|
389
|
-
// ✅ JS mapping → .bertui/compiled/styles/ (flat, imported by pages)
|
|
390
404
|
const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
|
|
391
405
|
if (!existsSync(compiledStylesDir)) mkdirSync(compiledStylesDir, { recursive: true });
|
|
392
|
-
const jsContent = `// CSS Module: ${filename}
|
|
406
|
+
const jsContent = `// CSS Module: ${filename}\nconst styles = ${JSON.stringify(mapping, null, 2)};\nexport default styles;\n`;
|
|
393
407
|
await Bun.write(join(compiledStylesDir, filename + '.js'), jsContent);
|
|
394
|
-
|
|
395
|
-
logger.debug(`CSS Module: ${filename} → ${Object.keys(mapping).length} classes scoped`);
|
|
396
408
|
}
|
|
397
409
|
|
|
398
|
-
// Rewrite: import styles from '../styles/home.module.css'
|
|
399
|
-
// → import styles from '../../styles/home.module.css.js' (relative to compiled output)
|
|
400
410
|
function transformCSSModuleImports(code, srcPath, root) {
|
|
401
411
|
const moduleImportRegex = /import\s+(\w+)\s+from\s+['"]([^'"]*\.module\.css)['"]/g;
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const
|
|
405
|
-
const
|
|
406
|
-
const compiledFilePath = join(root, '.bertui', 'compiled', relativeFromSrc.replace(/\.(jsx|tsx|ts)$/, '.js'));
|
|
407
|
-
const compiledFileDir = dirname(compiledFilePath); // e.g. .bertui/compiled/pages/
|
|
408
|
-
|
|
412
|
+
const srcDir = join(root, 'src');
|
|
413
|
+
const relativeFromSrc = relative(srcDir, srcPath);
|
|
414
|
+
const compiledFilePath = join(root, '.bertui', 'compiled', relativeFromSrc.replace(/\.(jsx|tsx|ts)$/, '.js'));
|
|
415
|
+
const compiledFileDir = dirname(compiledFilePath);
|
|
409
416
|
const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
|
|
410
417
|
|
|
411
418
|
code = code.replace(moduleImportRegex, (match, varName, importPath) => {
|
|
412
|
-
const filename = importPath.split('/').pop();
|
|
413
|
-
const jsFile
|
|
414
|
-
let rel
|
|
419
|
+
const filename = importPath.split('/').pop();
|
|
420
|
+
const jsFile = join(compiledStylesDir, filename + '.js');
|
|
421
|
+
let rel = relative(compiledFileDir, jsFile).replace(/\\/g, '/');
|
|
415
422
|
if (!rel.startsWith('.')) rel = './' + rel;
|
|
416
423
|
return `import ${varName} from '${rel}'`;
|
|
417
424
|
});
|
|
418
|
-
|
|
419
425
|
return code;
|
|
420
426
|
}
|
|
421
427
|
|
|
422
|
-
// Remove plain CSS imports only — leave .module.css for transformCSSModuleImports
|
|
423
428
|
function removePlainCSSImports(code) {
|
|
424
429
|
code = code.replace(/import\s+['"][^'"]*(?<!\.module)\.css['"];?\s*/g, '');
|
|
425
430
|
code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
|
|
426
431
|
return code;
|
|
427
432
|
}
|
|
428
433
|
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
//
|
|
434
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
435
|
+
// File compilation
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
432
437
|
|
|
433
|
-
async function compileFileInternal(srcPath, outDir, filename, relativePath, root, envVars) {
|
|
434
|
-
const ext
|
|
438
|
+
async function compileFileInternal(srcPath, outDir, filename, relativePath, root, envVars, aliasMap) {
|
|
439
|
+
const ext = extname(filename);
|
|
435
440
|
const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
|
|
436
|
-
|
|
441
|
+
|
|
437
442
|
try {
|
|
438
443
|
let code = await Bun.file(srcPath).text();
|
|
439
|
-
|
|
440
|
-
// ✅ Transform module imports BEFORE stripping plain CSS
|
|
441
444
|
code = transformCSSModuleImports(code, srcPath, root);
|
|
442
445
|
code = removePlainCSSImports(code);
|
|
443
446
|
code = removeDotenvImports(code);
|
|
444
447
|
code = replaceEnvInCode(code, envVars);
|
|
445
|
-
|
|
448
|
+
|
|
446
449
|
const outPath = join(outDir, filename.replace(/\.(jsx|tsx|ts)$/, '.js'));
|
|
447
450
|
code = fixRouterImports(code, outPath, root);
|
|
448
|
-
|
|
451
|
+
|
|
449
452
|
const transpiler = new Bun.Transpiler({
|
|
450
453
|
loader,
|
|
451
454
|
tsconfig: {
|
|
@@ -457,29 +460,38 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
|
|
|
457
460
|
}
|
|
458
461
|
});
|
|
459
462
|
let compiled = await transpiler.transform(code);
|
|
460
|
-
|
|
463
|
+
|
|
461
464
|
if (usesJSX(compiled) && !compiled.includes('import React')) {
|
|
462
465
|
compiled = `import React from 'react';\n${compiled}`;
|
|
463
466
|
}
|
|
464
|
-
|
|
467
|
+
|
|
465
468
|
compiled = fixRelativeImports(compiled);
|
|
469
|
+
// ← alias rewrite MUST happen after transpiler — Bun normalizes specifiers during transform
|
|
470
|
+
compiled = rewriteAliasImports(compiled, outPath, aliasMap);
|
|
466
471
|
await Bun.write(outPath, compiled);
|
|
467
|
-
logger.debug(`Compiled: ${relativePath} → ${filename.replace(/\.(jsx|tsx|ts)$/, '.js')}`);
|
|
468
472
|
} catch (error) {
|
|
473
|
+
// Enrich error with file info so the watcher can forward it to the overlay
|
|
474
|
+
error.file = relativePath;
|
|
475
|
+
const detail = error.errors?.[0];
|
|
476
|
+
if (detail) {
|
|
477
|
+
error.message = detail.text || error.message;
|
|
478
|
+
error.line = detail.position?.line;
|
|
479
|
+
error.column = detail.position?.column;
|
|
480
|
+
}
|
|
469
481
|
logger.error(`Failed to compile ${relativePath}: ${error.message}`);
|
|
470
482
|
throw error;
|
|
471
483
|
}
|
|
472
484
|
}
|
|
473
485
|
|
|
474
|
-
//
|
|
475
|
-
//
|
|
476
|
-
//
|
|
486
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
487
|
+
// Helpers
|
|
488
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
477
489
|
|
|
478
490
|
function usesJSX(code) {
|
|
479
491
|
return code.includes('React.createElement') ||
|
|
480
|
-
code.includes('React.Fragment')
|
|
481
|
-
/<[A-Z]/.test(code)
|
|
482
|
-
code.includes('jsx(')
|
|
492
|
+
code.includes('React.Fragment') ||
|
|
493
|
+
/<[A-Z]/.test(code) ||
|
|
494
|
+
code.includes('jsx(') ||
|
|
483
495
|
code.includes('jsxs(');
|
|
484
496
|
}
|
|
485
497
|
|
|
@@ -491,10 +503,10 @@ function removeDotenvImports(code) {
|
|
|
491
503
|
}
|
|
492
504
|
|
|
493
505
|
function fixRouterImports(code, outPath, root) {
|
|
494
|
-
const buildDir
|
|
495
|
-
const routerPath
|
|
496
|
-
const
|
|
497
|
-
const routerImport =
|
|
506
|
+
const buildDir = join(root, '.bertui', 'compiled');
|
|
507
|
+
const routerPath = join(buildDir, 'router.js');
|
|
508
|
+
const rel = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
|
|
509
|
+
const routerImport = rel.startsWith('.') ? rel : './' + rel;
|
|
498
510
|
code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
|
|
499
511
|
return code;
|
|
500
512
|
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
// bertui/src/config/defaultConfig.js
|
|
1
|
+
// bertui/src/config/defaultConfig.js
|
|
2
2
|
export const defaultConfig = {
|
|
3
3
|
siteName: "BertUI App",
|
|
4
4
|
baseUrl: "http://localhost:3000",
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
// importhow: alias → relative path from project root
|
|
7
|
+
// Example:
|
|
8
|
+
// importhow: {
|
|
9
|
+
// amani: '../../components',
|
|
10
|
+
// ui: '../../components/ui',
|
|
11
|
+
// text: '../../utils/text',
|
|
12
|
+
// }
|
|
13
|
+
importhow: {},
|
|
14
|
+
|
|
6
15
|
meta: {
|
|
7
16
|
title: "BertUI - Lightning Fast React",
|
|
8
17
|
description: "Build lightning-fast React applications with file-based routing powered by Bun",
|
|
@@ -14,13 +23,13 @@ export const defaultConfig = {
|
|
|
14
23
|
ogDescription: "Build lightning-fast React apps with zero config",
|
|
15
24
|
ogImage: "/og-image.png"
|
|
16
25
|
},
|
|
17
|
-
|
|
26
|
+
|
|
18
27
|
appShell: {
|
|
19
28
|
loading: true,
|
|
20
29
|
loadingText: "Loading...",
|
|
21
30
|
backgroundColor: "#ffffff"
|
|
22
31
|
},
|
|
23
|
-
|
|
32
|
+
|
|
24
33
|
robots: {
|
|
25
34
|
disallow: [],
|
|
26
35
|
crawlDelay: null
|