bertui 1.1.8 → 1.2.0

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,9 +1,6 @@
1
- // ============================================
2
- // FILE: bertui/src/client/compiler.js (UPDATED - Skip templates/)
3
- // ============================================
4
-
5
1
  import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
6
2
  import { join, extname, relative, dirname } from 'path';
3
+ import { transform } from 'lightningcss';
7
4
  import logger from '../logger/logger.js';
8
5
  import { loadEnvVariables, generateEnvCode, replaceEnvInCode } from '../utils/env.js';
9
6
 
@@ -63,17 +60,18 @@ export async function compileProject(root) {
63
60
  return { outDir, stats, routes };
64
61
  }
65
62
 
66
- // NEW EXPORT - Single file compilation for HMR
67
63
  export async function compileFile(srcPath, root) {
68
64
  const srcDir = join(root, 'src');
69
65
  const outDir = join(root, '.bertui', 'compiled');
70
66
  const envVars = loadEnvVariables(root);
71
-
72
67
  const relativePath = relative(srcDir, srcPath);
73
68
  const ext = extname(srcPath);
74
69
 
75
- if (!existsSync(outDir)) {
76
- mkdirSync(outDir, { recursive: true });
70
+ if (!existsSync(outDir)) mkdirSync(outDir, { recursive: true });
71
+
72
+ if (srcPath.endsWith('.module.css')) {
73
+ await compileCSSModule(srcPath, root);
74
+ return { success: true };
77
75
  }
78
76
 
79
77
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
@@ -83,29 +81,30 @@ export async function compileFile(srcPath, root) {
83
81
  outputPath: relativePath.replace(/\.(jsx|tsx|ts)$/, '.js'),
84
82
  success: true
85
83
  };
86
- } else if (ext === '.js') {
84
+ }
85
+
86
+ if (ext === '.js') {
87
87
  const fileName = srcPath.split('/').pop();
88
88
  const outPath = join(outDir, fileName);
89
89
  let code = await Bun.file(srcPath).text();
90
-
91
- code = removeCSSImports(code);
90
+ code = transformCSSModuleImports(code, srcPath, root);
91
+ code = removePlainCSSImports(code);
92
92
  code = replaceEnvInCode(code, envVars);
93
93
  code = fixRouterImports(code, outPath, root);
94
-
95
94
  if (usesJSX(code) && !code.includes('import React')) {
96
95
  code = `import React from 'react';\n${code}`;
97
96
  }
98
-
99
97
  await Bun.write(outPath, code);
100
- return {
101
- outputPath: relativePath,
102
- success: true
103
- };
98
+ return { outputPath: relativePath, success: true };
104
99
  }
105
100
 
106
101
  return { success: false };
107
102
  }
108
103
 
104
+ // ============================================
105
+ // ROUTE DISCOVERY
106
+ // ============================================
107
+
109
108
  async function discoverRoutes(pagesDir) {
110
109
  const routes = [];
111
110
 
@@ -124,20 +123,22 @@ async function discoverRoutes(pagesDir) {
124
123
 
125
124
  if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
126
125
  const fileName = entry.name.replace(ext, '');
126
+
127
+ // ✅ Only loading is reserved — index is a valid route (renamed to /)
128
+ if (fileName === 'loading') continue;
129
+
127
130
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
128
-
129
131
  if (fileName === 'index') {
130
132
  route = route.replace('/index', '') || '/';
131
133
  }
132
134
 
133
135
  const isDynamic = fileName.includes('[') && fileName.includes(']');
134
- const type = isDynamic ? 'dynamic' : 'static';
135
136
 
136
137
  routes.push({
137
138
  route: route === '' ? '/' : route,
138
139
  file: relativePath.replace(/\\/g, '/'),
139
140
  path: fullPath,
140
- type
141
+ type: isDynamic ? 'dynamic' : 'static'
141
142
  });
142
143
  }
143
144
  }
@@ -146,15 +147,17 @@ async function discoverRoutes(pagesDir) {
146
147
 
147
148
  await scanDirectory(pagesDir);
148
149
  routes.sort((a, b) => {
149
- if (a.type === b.type) {
150
- return a.route.localeCompare(b.route);
151
- }
150
+ if (a.type === b.type) return a.route.localeCompare(b.route);
152
151
  return a.type === 'static' ? -1 : 1;
153
152
  });
154
153
 
155
154
  return routes;
156
155
  }
157
156
 
157
+ // ============================================
158
+ // ROUTER GENERATION
159
+ // ============================================
160
+
158
161
  async function generateRouter(routes, outDir, root) {
159
162
  const imports = routes.map((route, i) => {
160
163
  const componentName = `Page${i}`;
@@ -167,7 +170,7 @@ async function generateRouter(routes, outDir, root) {
167
170
  return ` { path: '${route.route}', component: ${componentName}, type: '${route.type}' }`;
168
171
  }).join(',\n');
169
172
 
170
- const routerComponentCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
173
+ const routerCode = `import React, { useState, useEffect, createContext, useContext } from 'react';
171
174
 
172
175
  const RouterContext = createContext(null);
173
176
 
@@ -230,16 +233,16 @@ export function Router({ routes }) {
230
233
 
231
234
  export function Link({ to, children, ...props }) {
232
235
  const { navigate } = useRouter();
233
- return React.createElement('a', {
234
- href: to,
235
- onClick: (e) => { e.preventDefault(); navigate(to); },
236
- ...props
236
+ return React.createElement('a', {
237
+ href: to,
238
+ onClick: (e) => { e.preventDefault(); navigate(to); },
239
+ ...props
237
240
  }, children);
238
241
  }
239
242
 
240
243
  function NotFound() {
241
244
  return React.createElement('div', {
242
- style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
245
+ style: { display: 'flex', flexDirection: 'column', alignItems: 'center',
243
246
  justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }
244
247
  },
245
248
  React.createElement('h1', { style: { fontSize: '6rem', margin: 0 } }, '404'),
@@ -254,9 +257,13 @@ export const routes = [
254
257
  ${routeConfigs}
255
258
  ];`;
256
259
 
257
- await Bun.write(join(outDir, 'router.js'), routerComponentCode);
260
+ await Bun.write(join(outDir, 'router.js'), routerCode);
258
261
  }
259
262
 
263
+ // ============================================
264
+ // DIRECTORY COMPILATION
265
+ // ============================================
266
+
260
267
  async function compileDirectory(srcDir, outDir, root, envVars) {
261
268
  const stats = { files: 0, skipped: 0 };
262
269
  const files = readdirSync(srcDir);
@@ -266,12 +273,10 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
266
273
  const stat = statSync(srcPath);
267
274
 
268
275
  if (stat.isDirectory()) {
269
- // ✅ NEW: Skip templates directory
270
276
  if (file === 'templates') {
271
- logger.debug('⏭️ Skipping src/templates/ (PageBuilder templates only)');
277
+ logger.debug('⏭️ Skipping src/templates/');
272
278
  continue;
273
279
  }
274
-
275
280
  const subOutDir = join(outDir, file);
276
281
  mkdirSync(subOutDir, { recursive: true });
277
282
  const subStats = await compileDirectory(srcPath, subOutDir, root, envVars);
@@ -280,14 +285,16 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
280
285
  } else {
281
286
  const ext = extname(file);
282
287
  const relativePath = relative(join(root, 'src'), srcPath);
283
-
284
- if (ext === '.css') {
288
+
289
+ // MUST check .module.css BEFORE plain .css
290
+ if (file.endsWith('.module.css')) {
291
+ await compileCSSModule(srcPath, root);
292
+ stats.files++;
293
+ } else if (ext === '.css') {
294
+ // Plain CSS → copy to .bertui/styles/ for <link> injection
285
295
  const stylesOutDir = join(root, '.bertui', 'styles');
286
- if (!existsSync(stylesOutDir)) {
287
- mkdirSync(stylesOutDir, { recursive: true });
288
- }
289
- const cssOutPath = join(stylesOutDir, file);
290
- await Bun.write(cssOutPath, Bun.file(srcPath));
296
+ if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
297
+ await Bun.write(join(stylesOutDir, file), Bun.file(srcPath));
291
298
  logger.debug(`Copied CSS: ${relativePath}`);
292
299
  stats.files++;
293
300
  } else if (['.jsx', '.tsx', '.ts'].includes(ext)) {
@@ -296,20 +303,17 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
296
303
  } else if (ext === '.js') {
297
304
  const outPath = join(outDir, file);
298
305
  let code = await Bun.file(srcPath).text();
299
-
300
- code = removeCSSImports(code);
306
+ code = transformCSSModuleImports(code, srcPath, root);
307
+ code = removePlainCSSImports(code);
301
308
  code = replaceEnvInCode(code, envVars);
302
309
  code = fixRouterImports(code, outPath, root);
303
-
304
310
  if (usesJSX(code) && !code.includes('import React')) {
305
311
  code = `import React from 'react';\n${code}`;
306
312
  }
307
-
308
313
  await Bun.write(outPath, code);
309
314
  logger.debug(`Copied: ${relativePath}`);
310
315
  stats.files++;
311
316
  } else {
312
- logger.debug(`Skipped: ${relativePath}`);
313
317
  stats.skipped++;
314
318
  }
315
319
  }
@@ -318,6 +322,114 @@ async function compileDirectory(srcDir, outDir, root, envVars) {
318
322
  return stats;
319
323
  }
320
324
 
325
+ // ============================================
326
+ // CSS MODULES
327
+ // ============================================
328
+
329
+ function hashClassName(filename, className) {
330
+ const str = filename + className;
331
+ let hash = 0;
332
+ for (let i = 0; i < str.length; i++) {
333
+ hash = (hash << 5) - hash + str.charCodeAt(i);
334
+ hash |= 0;
335
+ }
336
+ return Math.abs(hash).toString(36).slice(0, 5);
337
+ }
338
+
339
+ function scopeCSSModule(cssText, filename) {
340
+ const classNames = new Set();
341
+ const classRegex = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*[{,\s:]/g;
342
+ let match;
343
+ while ((match = classRegex.exec(cssText)) !== null) {
344
+ classNames.add(match[1]);
345
+ }
346
+
347
+ const mapping = {};
348
+ for (const cls of classNames) {
349
+ mapping[cls] = `${cls}_${hashClassName(filename, cls)}`;
350
+ }
351
+
352
+ let scopedCSS = cssText;
353
+ for (const [original, scoped] of Object.entries(mapping)) {
354
+ scopedCSS = scopedCSS.replace(
355
+ new RegExp(`\\.${original}(?=[\\s{,:\\[#.>+~)\\]])`, 'g'),
356
+ `.${scoped}`
357
+ );
358
+ }
359
+
360
+ return { mapping, scopedCSS };
361
+ }
362
+
363
+ 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
+
367
+ const { mapping, scopedCSS } = scopeCSSModule(cssText, filename);
368
+
369
+ // Run through LightningCSS for nesting support
370
+ let finalCSS = scopedCSS;
371
+ try {
372
+ const { code } = transform({
373
+ filename,
374
+ code: Buffer.from(scopedCSS),
375
+ minify: false,
376
+ drafts: { nesting: true },
377
+ targets: { chrome: 90 << 16 }
378
+ });
379
+ finalCSS = code.toString();
380
+ } catch (e) {
381
+ logger.warn(`LightningCSS failed for ${filename}: ${e.message}`);
382
+ }
383
+
384
+ // ✅ Scoped CSS → .bertui/styles/ (served as <link> tags)
385
+ const stylesOutDir = join(root, '.bertui', 'styles');
386
+ if (!existsSync(stylesOutDir)) mkdirSync(stylesOutDir, { recursive: true });
387
+ await Bun.write(join(stylesOutDir, filename), finalCSS);
388
+
389
+ // ✅ JS mapping → .bertui/compiled/styles/ (flat, imported by pages)
390
+ const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
391
+ 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`;
393
+ await Bun.write(join(compiledStylesDir, filename + '.js'), jsContent);
394
+
395
+ logger.debug(`CSS Module: ${filename} → ${Object.keys(mapping).length} classes scoped`);
396
+ }
397
+
398
+ // Rewrite: import styles from '../styles/home.module.css'
399
+ // → import styles from '../../styles/home.module.css.js' (relative to compiled output)
400
+ function transformCSSModuleImports(code, srcPath, root) {
401
+ 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
+
409
+ const compiledStylesDir = join(root, '.bertui', 'compiled', 'styles');
410
+
411
+ 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, '/');
415
+ if (!rel.startsWith('.')) rel = './' + rel;
416
+ return `import ${varName} from '${rel}'`;
417
+ });
418
+
419
+ return code;
420
+ }
421
+
422
+ // Remove plain CSS imports only — leave .module.css for transformCSSModuleImports
423
+ function removePlainCSSImports(code) {
424
+ code = code.replace(/import\s+['"][^'"]*(?<!\.module)\.css['"];?\s*/g, '');
425
+ code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
426
+ return code;
427
+ }
428
+
429
+ // ============================================
430
+ // FILE COMPILATION
431
+ // ============================================
432
+
321
433
  async function compileFileInternal(srcPath, outDir, filename, relativePath, root, envVars) {
322
434
  const ext = extname(filename);
323
435
  const loader = ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx';
@@ -325,14 +437,16 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
325
437
  try {
326
438
  let code = await Bun.file(srcPath).text();
327
439
 
328
- code = removeCSSImports(code);
440
+ // Transform module imports BEFORE stripping plain CSS
441
+ code = transformCSSModuleImports(code, srcPath, root);
442
+ code = removePlainCSSImports(code);
329
443
  code = removeDotenvImports(code);
330
444
  code = replaceEnvInCode(code, envVars);
331
445
 
332
446
  const outPath = join(outDir, filename.replace(/\.(jsx|tsx|ts)$/, '.js'));
333
447
  code = fixRouterImports(code, outPath, root);
334
448
 
335
- const transpiler = new Bun.Transpiler({
449
+ const transpiler = new Bun.Transpiler({
336
450
  loader,
337
451
  tsconfig: {
338
452
  compilerOptions: {
@@ -349,7 +463,6 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
349
463
  }
350
464
 
351
465
  compiled = fixRelativeImports(compiled);
352
-
353
466
  await Bun.write(outPath, compiled);
354
467
  logger.debug(`Compiled: ${relativePath} → ${filename.replace(/\.(jsx|tsx|ts)$/, '.js')}`);
355
468
  } catch (error) {
@@ -358,20 +471,18 @@ async function compileFileInternal(srcPath, outDir, filename, relativePath, root
358
471
  }
359
472
  }
360
473
 
474
+ // ============================================
475
+ // HELPERS
476
+ // ============================================
477
+
361
478
  function usesJSX(code) {
362
- return code.includes('React.createElement') ||
479
+ return code.includes('React.createElement') ||
363
480
  code.includes('React.Fragment') ||
364
481
  /<[A-Z]/.test(code) ||
365
482
  code.includes('jsx(') ||
366
483
  code.includes('jsxs(');
367
484
  }
368
485
 
369
- function removeCSSImports(code) {
370
- code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
371
- code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
372
- return code;
373
- }
374
-
375
486
  function removeDotenvImports(code) {
376
487
  code = code.replace(/import\s+\w+\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
377
488
  code = code.replace(/import\s+\{[^}]+\}\s+from\s+['"]dotenv['"]\s*;?\s*/g, '');
@@ -382,27 +493,17 @@ function removeDotenvImports(code) {
382
493
  function fixRouterImports(code, outPath, root) {
383
494
  const buildDir = join(root, '.bertui', 'compiled');
384
495
  const routerPath = join(buildDir, 'router.js');
385
-
386
496
  const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
387
497
  const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
388
-
389
- code = code.replace(
390
- /from\s+['"]bertui\/router['"]/g,
391
- `from '${routerImport}'`
392
- );
393
-
498
+ code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
394
499
  return code;
395
500
  }
396
501
 
397
502
  function fixRelativeImports(code) {
398
503
  const importRegex = /from\s+['"](\.\.?\/[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g;
399
-
400
504
  code = code.replace(importRegex, (match, path) => {
401
- if (path.endsWith('/') || /\.\w+$/.test(path)) {
402
- return match;
403
- }
505
+ if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
404
506
  return `from '${path}.js'`;
405
507
  });
406
-
407
508
  return code;
408
509
  }
@@ -124,4 +124,49 @@ export function extractCSSImports(code) {
124
124
  */
125
125
  export function isCSSFile(filename) {
126
126
  return filename.toLowerCase().endsWith('.css');
127
- }
127
+ }
128
+
129
+ // ============================================
130
+ // OPTIONAL SCSS SUPPORT - COMMENTED OUT BY DEFAULT
131
+ // Uncomment only if you install 'sass' package
132
+ // ============================================
133
+
134
+
135
+ export async function processSCSS(scssCode, options = {}) {
136
+ try {
137
+ // Dynamic import so it doesn't fail if sass isn't installed
138
+ const sass = await import('sass').catch(() => {
139
+ throw new Error('sass package not installed. Run: bun add sass');
140
+ });
141
+
142
+ const result = sass.compileString(scssCode, {
143
+ style: options.compressed ? 'compressed' : 'expanded',
144
+ sourceMap: false,
145
+ loadPaths: options.loadPaths || []
146
+ });
147
+
148
+ return result.css;
149
+ } catch (error) {
150
+ logger.error(`SCSS compilation failed: ${error.message}`);
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ export async function compileSCSSFile(filePath, options = {}) {
156
+ try {
157
+ const sass = await import('sass').catch(() => {
158
+ throw new Error('sass package not installed. Run: bun add sass');
159
+ });
160
+
161
+ const result = sass.compile(filePath, {
162
+ style: options.compressed ? 'compressed' : 'expanded',
163
+ sourceMap: false,
164
+ loadPaths: options.loadPaths || [require('path').dirname(filePath)]
165
+ });
166
+
167
+ return result.css;
168
+ } catch (error) {
169
+ logger.error(`SCSS file compilation failed: ${error.message}`);
170
+ throw error;
171
+ }
172
+ }
package/src/dev.js CHANGED
@@ -1,26 +1,64 @@
1
- // bertui/src/dev.js - CLEANED (No PageBuilder)
1
+ // bertui/src/dev.js - WITH MIDDLEWARE + LAYOUTS + LOADING + PARTIAL HYDRATION
2
2
  import { compileProject } from './client/compiler.js';
3
3
  import { startDevServer } from './server/dev-server.js';
4
+ import { MiddlewareManager } from './middleware/index.js';
5
+ import { compileLayouts, discoverLayouts } from './layouts/index.js';
6
+ import { compileLoadingComponents } from './loading/index.js';
7
+ import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
4
8
  import logger from './logger/logger.js';
5
9
  import { loadConfig } from './config/loadConfig.js';
6
10
 
7
11
  export async function startDev(options = {}) {
8
12
  const root = options.root || process.cwd();
9
13
  const port = options.port || 3000;
10
-
14
+
11
15
  try {
12
16
  const config = await loadConfig(root);
13
-
17
+
14
18
  // Step 1: Compile project
15
19
  logger.info('Step 1: Compiling project...');
16
- await compileProject(root);
17
-
18
- // Step 2: Start dev server
19
- logger.info('Step 2: Starting dev server...');
20
- await startDevServer({ root, port });
21
-
20
+ const { routes, outDir } = await compileProject(root);
21
+
22
+ // Step 2: Compile layouts
23
+ logger.info('Step 2: Loading layouts...');
24
+ const layouts = await compileLayouts(root, outDir);
25
+ const layoutCount = Object.keys(layouts).length;
26
+ if (layoutCount > 0) {
27
+ logger.success(`📐 ${layoutCount} layout(s) active`);
28
+ } else {
29
+ logger.info('No layouts found (create src/layouts/default.tsx to wrap all pages)');
30
+ }
31
+
32
+ // Step 3: Compile loading states
33
+ logger.info('Step 3: Loading per-route loading states...');
34
+ const loadingComponents = await compileLoadingComponents(root, outDir);
35
+
36
+ // Step 4: Analyze routes for partial hydration
37
+ if (routes && routes.length > 0) {
38
+ logger.info('Step 4: Analyzing routes for partial hydration...');
39
+ const analyzedRoutes = await analyzeRoutes(routes);
40
+ logHydrationReport(analyzedRoutes);
41
+ }
42
+
43
+ // Step 5: Load middleware
44
+ logger.info('Step 5: Loading middleware...');
45
+ const middlewareManager = new MiddlewareManager(root);
46
+ await middlewareManager.load();
47
+ middlewareManager.watch(); // Hot-reload middleware on change
48
+
49
+ // Step 6: Start dev server with all features
50
+ logger.info('Step 6: Starting dev server...');
51
+ await startDevServer({
52
+ root,
53
+ port,
54
+ middleware: middlewareManager,
55
+ layouts,
56
+ loadingComponents,
57
+ });
58
+
22
59
  } catch (error) {
23
60
  logger.error(`Dev server failed: ${error.message}`);
61
+ if (error.stack) logger.error(error.stack);
24
62
  process.exit(1);
25
63
  }
26
64
  }