bertui 1.2.1 → 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.
@@ -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 = join(root, 'src');
11
+
12
+ const srcDir = join(root, 'src');
11
13
  const pagesDir = join(srcDir, 'pages');
12
- const outDir = join(root, '.bertui', 'compiled');
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 = join(root, 'src');
65
- const outDir = join(root, '.bertui', 'compiled');
96
+ const srcDir = join(root, 'src');
97
+ const outDir = join(root, '.bertui', 'compiled');
66
98
  const envVars = loadEnvVariables(root);
67
- const relativePath = relative(srcDir, srcPath);
68
- const ext = extname(srcPath);
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 = srcPath.split('/').pop();
79
- await compileFileInternal(srcPath, outDir, fileName, relativePath, root, envVars);
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 = join(outDir, fileName);
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: relativePath, success: true };
140
+ return { outputPath: relative(srcDir, srcPath), success: true };
99
141
  }
100
-
142
+
101
143
  return { success: false };
102
144
  }
103
145
 
104
- // ============================================
105
- // ROUTE DISCOVERY
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 = join(dir, entry.name);
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 = extname(entry.name);
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
- route = route.replace('/index', '') || '/';
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: route === '' ? '/' : route,
139
- file: relativePath.replace(/\\/g, '/'),
140
- path: fullPath,
141
- type: isDynamic ? 'dynamic' : 'static'
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
- // ROUTER GENERATION
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 = `./pages/${route.file.replace(/\.(jsx|tsx|ts)$/, '.js')}`;
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
- const componentName = `Page${i}`;
170
- return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
171
- }).join(',\n');
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 = new RegExp('^' + pattern + '$');
206
- const match = pathname.match(regex);
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 extractedParams = {};
210
- paramNames.forEach((name, i) => { extractedParams[name] = match[i + 1]; });
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', { style: { fontSize: '1.5rem', color: '#666' } }, 'Page not found'),
250
- React.createElement('a', { href: '/', style: { color: '#10b981', textDecoration: 'none' } }, 'Go home')
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
- // DIRECTORY COMPILATION
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 = statSync(srcPath);
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 += subStats.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 = extname(file);
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 MODULES
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 = srcPath.split('/').pop(); // e.g. home.module.css
365
- const cssText = await Bun.file(srcPath).text();
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} — auto-generated by BertUI\nconst styles = ${JSON.stringify(mapping, null, 2)};\nexport default styles;\n`;
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
- // The compiled output of this file lives in .bertui/compiled/ + relative path from src/
404
- const srcDir = join(root, 'src');
405
- const relativeFromSrc = relative(srcDir, srcPath); // e.g. pages/about.jsx
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(); // e.g. home.module.css
413
- const jsFile = join(compiledStylesDir, filename + '.js'); // absolute target
414
- let rel = relative(compiledFileDir, jsFile).replace(/\\/g, '/');
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
- // FILE COMPILATION
431
- // ============================================
434
+ // ─────────────────────────────────────────────────────────────────────────────
435
+ // File compilation
436
+ // ─────────────────────────────────────────────────────────────────────────────
432
437
 
433
- async function compileFileInternal(srcPath, outDir, filename, relativePath, root, envVars) {
434
- const ext = extname(filename);
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
- // HELPERS
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 = join(root, '.bertui', 'compiled');
495
- const routerPath = join(buildDir, 'router.js');
496
- const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
497
- const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
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 - CLEANED
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