bertui 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bertui",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Lightning-fast React dev server powered by Bun - Now with Rust image optimization (WASM, no Rust required for users)",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -3,34 +3,129 @@ import { join, relative, dirname, extname } from 'path';
3
3
  import { readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
5
5
  import { replaceEnvInCode } from '../../utils/env.js';
6
+ import { transform } from 'lightningcss';
7
+
8
+ // ============================================
9
+ // CSS MODULES
10
+ // ============================================
11
+
12
+ function hashClassName(filename, className) {
13
+ const str = filename + className;
14
+ let hash = 0;
15
+ for (let i = 0; i < str.length; i++) {
16
+ hash = (hash << 5) - hash + str.charCodeAt(i);
17
+ hash |= 0;
18
+ }
19
+ return Math.abs(hash).toString(36).slice(0, 5);
20
+ }
21
+
22
+ function scopeCSSModule(cssText, filename) {
23
+ const classNames = new Set();
24
+ const classRegex = /\.([a-zA-Z_][a-zA-Z0-9_-]*)\s*[{,\s:]/g;
25
+ let match;
26
+ while ((match = classRegex.exec(cssText)) !== null) {
27
+ classNames.add(match[1]);
28
+ }
29
+ const mapping = {};
30
+ for (const cls of classNames) {
31
+ mapping[cls] = `${cls}_${hashClassName(filename, cls)}`;
32
+ }
33
+ let scopedCSS = cssText;
34
+ for (const [original, scoped] of Object.entries(mapping)) {
35
+ scopedCSS = scopedCSS.replace(
36
+ new RegExp(`\\.${original}(?=[\\s{,:\\[#.>+~)\\]])`, 'g'),
37
+ `.${scoped}`
38
+ );
39
+ }
40
+ return { mapping, scopedCSS };
41
+ }
42
+
43
+ async function compileCSSModule(srcPath, rootBuildDir) {
44
+ const filename = srcPath.split('/').pop();
45
+ const cssText = await Bun.file(srcPath).text();
46
+ const { mapping, scopedCSS } = scopeCSSModule(cssText, filename);
47
+
48
+ let finalCSS = scopedCSS;
49
+ try {
50
+ const { code } = transform({
51
+ filename,
52
+ code: Buffer.from(scopedCSS),
53
+ minify: false,
54
+ drafts: { nesting: true },
55
+ targets: { chrome: 90 << 16 }
56
+ });
57
+ finalCSS = code.toString();
58
+ } catch (e) {
59
+ logger.warn(`LightningCSS failed for ${filename}: ${e.message}`);
60
+ }
61
+
62
+ // JS mapping → .bertuibuild/styles/home.module.css.js (for JS imports)
63
+ const stylesDir = join(rootBuildDir, 'styles');
64
+ mkdirSync(stylesDir, { recursive: true });
65
+ const jsContent = `// CSS Module: ${filename} — auto-generated by BertUI\nconst styles = ${JSON.stringify(mapping, null, 2)};\nexport default styles;\n`;
66
+ writeFileSync(join(stylesDir, filename + '.js'), jsContent);
67
+
68
+ // Scoped CSS → .bertuibuild/styles-staged/home.module.css (for css-builder.js to pick up)
69
+ const stagingDir = join(rootBuildDir, 'styles-staged');
70
+ mkdirSync(stagingDir, { recursive: true });
71
+ writeFileSync(join(stagingDir, filename), finalCSS);
72
+
73
+ logger.debug(`CSS Module: ${filename} → ${Object.keys(mapping).length} classes scoped`);
74
+ }
75
+
76
+ function transformCSSModuleImports(code, outPath, rootBuildDir) {
77
+ const moduleImportRegex = /import\s+(\w+)\s+from\s+['"]([^'"]*\.module\.css)['"]/g;
78
+ const outFileDir = dirname(outPath);
79
+ const stylesDir = join(rootBuildDir, 'styles');
80
+
81
+ code = code.replace(moduleImportRegex, (match, varName, importPath) => {
82
+ const filename = importPath.split('/').pop();
83
+ const jsFile = join(stylesDir, filename + '.js');
84
+ let rel = relative(outFileDir, jsFile).replace(/\\/g, '/');
85
+ if (!rel.startsWith('.')) rel = './' + rel;
86
+ return `import ${varName} from '${rel}'`;
87
+ });
88
+
89
+ return code;
90
+ }
91
+
92
+ // ============================================
93
+ // MAIN COMPILER
94
+ // ============================================
6
95
 
7
96
  export async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
8
- // Create bunfig.toml in build directory
9
97
  const bunfigContent = `
10
98
  [build]
11
99
  jsx = "react"
12
100
  jsxFactory = "React.createElement"
13
101
  jsxFragment = "React.Fragment"
14
102
  `.trim();
15
-
103
+
16
104
  writeFileSync(join(buildDir, 'bunfig.toml'), bunfigContent);
17
105
  logger.info('Created bunfig.toml for classic JSX');
18
-
106
+
19
107
  const files = readdirSync(srcDir);
20
108
  const filesToCompile = [];
21
-
109
+ const rootBuildDir = join(root, '.bertuibuild');
110
+
22
111
  for (const file of files) {
23
112
  const srcPath = join(srcDir, file);
24
113
  const stat = statSync(srcPath);
25
-
114
+
26
115
  if (stat.isDirectory()) {
27
116
  const subBuildDir = join(buildDir, file);
28
117
  mkdirSync(subBuildDir, { recursive: true });
29
118
  await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
30
119
  } else {
31
120
  const ext = extname(file);
121
+
122
+ if (file.endsWith('.module.css')) {
123
+ await compileCSSModule(srcPath, rootBuildDir);
124
+ continue;
125
+ }
126
+
32
127
  if (ext === '.css') continue;
33
-
128
+
34
129
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
35
130
  filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'tsx' });
36
131
  } else if (ext === '.js') {
@@ -38,58 +133,56 @@ jsxFragment = "React.Fragment"
38
133
  }
39
134
  }
40
135
  }
41
-
136
+
42
137
  if (filesToCompile.length === 0) return;
43
-
138
+
44
139
  logger.info(`📦 Compiling ${filesToCompile.length} files...`);
45
-
140
+
46
141
  for (let i = 0; i < filesToCompile.length; i++) {
47
142
  const file = filesToCompile[i];
48
-
143
+
49
144
  try {
50
145
  if (file.type === 'tsx') {
51
- await compileBuildFile(file.path, file.dir, file.name, root, envVars, buildDir);
146
+ await compileBuildFile(file.path, file.dir, file.name, root, envVars, rootBuildDir);
52
147
  } else {
53
- await compileJSFile(file.path, file.dir, file.name, root, envVars);
148
+ await compileJSFile(file.path, file.dir, file.name, root, envVars, rootBuildDir);
54
149
  }
55
-
150
+
56
151
  if ((i + 1) % 10 === 0 || i === filesToCompile.length - 1) {
57
152
  const percent = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
58
153
  logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${percent}%)`);
59
154
  }
60
-
155
+
61
156
  } catch (error) {
62
157
  logger.error(`Failed to compile ${file.name}: ${error.message}`);
63
158
  }
64
159
  }
65
-
160
+
66
161
  logger.success(`✅ Compiled ${filesToCompile.length} files`);
67
162
  }
68
163
 
69
- async function compileBuildFile(srcPath, buildDir, filename, root, envVars, configDir) {
164
+ async function compileBuildFile(srcPath, buildDir, filename, root, envVars, rootBuildDir) {
70
165
  const ext = extname(filename);
71
-
166
+
72
167
  try {
73
168
  let code = await Bun.file(srcPath).text();
74
- code = removeCSSImports(code);
75
169
  code = replaceEnvInCode(code, envVars);
76
-
170
+
77
171
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
78
172
  const outPath = join(buildDir, outFilename);
173
+
174
+ code = transformCSSModuleImports(code, outPath, rootBuildDir);
175
+ code = removePlainCSSImports(code);
79
176
  code = fixBuildImports(code, srcPath, outPath, root);
80
-
81
- // Add React import BEFORE transpiling
177
+
82
178
  if (!code.includes('import React')) {
83
179
  code = `import React from 'react';\n${code}`;
84
180
  }
85
-
86
- // Use Bun.Transpiler with explicit production settings
181
+
87
182
  const transpiler = new Bun.Transpiler({
88
183
  loader: ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx',
89
184
  target: 'browser',
90
- define: {
91
- 'process.env.NODE_ENV': '"production"'
92
- },
185
+ define: { 'process.env.NODE_ENV': '"production"' },
93
186
  tsconfig: {
94
187
  compilerOptions: {
95
188
  jsx: 'react',
@@ -99,50 +192,50 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars, conf
99
192
  }
100
193
  }
101
194
  });
102
-
195
+
103
196
  let compiled = await transpiler.transform(code);
104
-
105
- // Verify no dev JSX leaked through
197
+
106
198
  if (compiled.includes('jsxDEV')) {
107
199
  logger.warn(`⚠️ Dev JSX detected in ${filename}, fixing...`);
108
200
  compiled = compiled.replace(/jsxDEV/g, 'jsx');
109
201
  }
110
-
202
+
111
203
  compiled = fixRelativeImports(compiled);
112
204
  await Bun.write(outPath, compiled);
113
-
205
+
114
206
  code = null;
115
207
  compiled = null;
116
-
208
+
117
209
  } catch (error) {
118
210
  logger.error(`Failed to compile ${filename}: ${error.message}`);
119
211
  throw error;
120
212
  }
121
213
  }
122
214
 
123
- async function compileJSFile(srcPath, buildDir, filename, root, envVars) {
215
+ async function compileJSFile(srcPath, buildDir, filename, root, envVars, rootBuildDir) {
124
216
  const outPath = join(buildDir, filename);
125
217
  let code = await Bun.file(srcPath).text();
126
- code = removeCSSImports(code);
127
218
  code = replaceEnvInCode(code, envVars);
219
+ code = transformCSSModuleImports(code, outPath, rootBuildDir);
220
+ code = removePlainCSSImports(code);
128
221
  code = fixBuildImports(code, srcPath, outPath, root);
129
-
222
+
130
223
  if (usesJSX(code) && !code.includes('import React')) {
131
224
  code = `import React from 'react';\n${code}`;
132
225
  }
133
-
226
+
134
227
  await Bun.write(outPath, code);
135
228
  code = null;
136
229
  }
137
230
 
138
231
  function usesJSX(code) {
139
- return code.includes('React.createElement') ||
232
+ return code.includes('React.createElement') ||
140
233
  code.includes('React.Fragment') ||
141
234
  /<[A-Z]/.test(code);
142
235
  }
143
236
 
144
- function removeCSSImports(code) {
145
- code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
237
+ function removePlainCSSImports(code) {
238
+ code = code.replace(/import\s+['"][^'"]*(?<!\.module)\.css['"];?\s*/g, '');
146
239
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
147
240
  return code;
148
241
  }
@@ -152,7 +245,6 @@ function fixBuildImports(code, srcPath, outPath, root) {
152
245
  const routerPath = join(buildDir, 'router.js');
153
246
  const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
154
247
  const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
155
-
156
248
  code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
157
249
  return code;
158
250
  }
@@ -164,4 +256,4 @@ function fixRelativeImports(code) {
164
256
  return `from '${prefix}${path}.js';`;
165
257
  });
166
258
  return code;
167
- }
259
+ }
@@ -20,13 +20,14 @@ export async function discoverRoutes(pagesDir) {
20
20
 
21
21
  if (['.jsx', '.tsx', '.js', '.ts'].includes(ext)) {
22
22
  const fileName = entry.name.replace(ext, '');
23
+
24
+ // Only loading is reserved - index is a valid route (becomes /)
25
+ if (fileName === 'loading') continue;
26
+
23
27
  let route = '/' + relativePath.replace(/\\/g, '/').replace(ext, '');
24
-
25
- const RESERVED = ['index', 'loading'];
26
28
  if (fileName === 'index') {
27
29
  route = route.replace('/index', '') || '/';
28
30
  }
29
- if (RESERVED.includes(fileName)) continue;
30
31
 
31
32
  const isDynamic = fileName.includes('[') && fileName.includes(']');
32
33
 
@@ -1,6 +1,6 @@
1
1
  // bertui/src/build/sitemap-generator.js - SIMPLIFIED
2
2
  import { join } from 'path';
3
- import logger from '../../logger/logger';
3
+ import logger from '../../logger/logger.js';
4
4
 
5
5
  function calculatePriority(route) {
6
6
  if (route === '/') return 1.0;
@@ -1,4 +1,4 @@
1
- // bertui/src/build/processors/css-builder.js - WITH SCSS + CACHING
1
+ // bertui/src/build/processors/css-builder.js - WITH SCSS + CACHING + CSS MODULES
2
2
  import { join } from 'path';
3
3
  import { existsSync, readdirSync, mkdirSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
@@ -9,13 +9,14 @@ export async function buildAllCSS(root, outDir) {
9
9
  const startTime = process.hrtime.bigint();
10
10
 
11
11
  const srcStylesDir = join(root, 'src', 'styles');
12
+ // CSS modules scoped CSS is staged here by file-transpiler.js
13
+ const modulesStagingDir = join(root, '.bertuibuild', 'styles-staged');
12
14
  const stylesOutDir = join(outDir, 'styles');
13
15
 
14
16
  mkdirSync(stylesOutDir, { recursive: true });
15
17
 
16
- // Check cache for entire CSS build
17
18
  const cacheKey = `css-build:${root}:${Date.now()}`;
18
- const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache
19
+ const cached = globalCache.get(cacheKey, { ttl: 1000 });
19
20
 
20
21
  if (cached) {
21
22
  logger.info(`⚡ Using cached CSS (${cached.files} files)`);
@@ -23,43 +24,50 @@ export async function buildAllCSS(root, outDir) {
23
24
  return;
24
25
  }
25
26
 
26
- if (!existsSync(srcStylesDir)) {
27
- await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No custom styles */');
28
- logger.info('No styles directory found, created empty CSS');
29
- return;
27
+ let combinedCSS = '';
28
+ let fileCount = 0;
29
+
30
+ // 1. Process src/styles/ (plain CSS + SCSS)
31
+ if (existsSync(srcStylesDir)) {
32
+ await processSCSSDirectory(srcStylesDir, root);
33
+ const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css') && !f.endsWith('.module.css'));
34
+
35
+ logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
36
+
37
+ for (const cssFile of cssFiles) {
38
+ const srcPath = join(srcStylesDir, cssFile);
39
+ const fileBuffer = await globalCache.getFile(srcPath, { logSpeed: true });
40
+ if (fileBuffer) {
41
+ const content = fileBuffer.toString('utf-8');
42
+ combinedCSS += `/* ${cssFile} */\n${content}\n\n`;
43
+ fileCount++;
44
+ }
45
+ }
46
+ } else {
47
+ logger.info('No styles directory found');
30
48
  }
31
-
32
- // Process SCSS files first
33
- await processSCSSDirectory(srcStylesDir, root);
34
-
35
- // Read all CSS files (including compiled SCSS)
36
- const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
37
-
38
- if (cssFiles.length === 0) {
49
+
50
+ // 2. Include scoped CSS from CSS modules (staged by file-transpiler.js)
51
+ if (existsSync(modulesStagingDir)) {
52
+ const moduleFiles = readdirSync(modulesStagingDir).filter(f => f.endsWith('.css'));
53
+ if (moduleFiles.length > 0) {
54
+ logger.info(`Including ${moduleFiles.length} CSS module(s)...`);
55
+ for (const cssFile of moduleFiles) {
56
+ const srcPath = join(modulesStagingDir, cssFile);
57
+ const content = await Bun.file(srcPath).text();
58
+ combinedCSS += `/* module: ${cssFile} */\n${content}\n\n`;
59
+ fileCount++;
60
+ }
61
+ }
62
+ }
63
+
64
+ if (!combinedCSS.trim()) {
39
65
  await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
40
66
  return;
41
67
  }
42
-
43
- logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
44
-
45
- let combinedCSS = '';
46
- const fileContents = [];
47
-
48
- for (const cssFile of cssFiles) {
49
- const srcPath = join(srcStylesDir, cssFile);
50
-
51
- // Use file cache
52
- const fileBuffer = await globalCache.getFile(srcPath, { logSpeed: true });
53
- if (fileBuffer) {
54
- const content = fileBuffer.toString('utf-8');
55
- fileContents.push({ filename: cssFile, content });
56
- combinedCSS += `/* ${cssFile} */\n${content}\n\n`;
57
- }
58
- }
59
-
68
+
60
69
  const combinedPath = join(stylesOutDir, 'bertui.min.css');
61
70
 
62
- // Minify with caching
63
71
  const minifyCacheKey = `minify:${Buffer.from(combinedCSS).length}:${combinedCSS.substring(0, 100)}`;
64
72
  let minified = globalCache.get(minifyCacheKey);
65
73
 
@@ -68,7 +76,7 @@ export async function buildAllCSS(root, outDir) {
68
76
  filename: 'bertui.min.css',
69
77
  sourceMap: false
70
78
  });
71
- globalCache.set(minifyCacheKey, minified, { ttl: 60000 }); // Cache for 60 seconds
79
+ globalCache.set(minifyCacheKey, minified, { ttl: 60000 });
72
80
  }
73
81
 
74
82
  await Bun.write(combinedPath, minified);
@@ -78,23 +86,20 @@ export async function buildAllCSS(root, outDir) {
78
86
  const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
79
87
 
80
88
  const endTime = process.hrtime.bigint();
81
- const duration = Number(endTime - startTime) / 1000; // Microseconds
89
+ const duration = Number(endTime - startTime) / 1000;
82
90
 
83
91
  logger.success(`CSS optimized: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
84
92
  logger.info(`⚡ Processing time: ${duration.toFixed(3)}µs`);
85
93
 
86
- // Cache the final result
87
94
  globalCache.set(cacheKey, {
88
- files: cssFiles.length,
95
+ files: fileCount,
89
96
  content: minified,
90
97
  size: minifiedSize
91
98
  }, { ttl: 5000 });
92
99
  }
93
100
 
94
- // NEW: Process SCSS directory
95
101
  async function processSCSSDirectory(stylesDir, root) {
96
102
  try {
97
- // Check if sass is installed
98
103
  const sass = await import('sass').catch(() => null);
99
104
  if (!sass) return;
100
105
 
@@ -109,7 +114,6 @@ async function processSCSSDirectory(stylesDir, root) {
109
114
  const srcPath = join(stylesDir, file);
110
115
  const cssPath = join(stylesDir, file.replace(/\.(scss|sass)$/, '.css'));
111
116
 
112
- // Check cache
113
117
  const fileBuffer = await globalCache.getFile(srcPath);
114
118
  const cacheKey = `scss:${file}:${Buffer.from(fileBuffer).length}`;
115
119
  const cached = globalCache.get(cacheKey);
package/src/build.js CHANGED
@@ -153,7 +153,9 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
153
153
  });
154
154
 
155
155
  if (!result.success) {
156
- const errors = result.logs?.map(l => l.message).join('\n') || 'Unknown error';
156
+ console.log('RAW LOGS:', JSON.stringify(result.logs, null, 2));
157
+ console.log('OUTPUTS:', JSON.stringify(result.outputs?.map(o => o.path), null, 2));
158
+ const errors = result.logs?.map(l => l.text || l.message || JSON.stringify(l)).join('\n') || 'Unknown error';
157
159
  throw new Error(`JavaScript bundling failed:\n${errors}`);
158
160
  }
159
161