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.
@@ -1,36 +1,60 @@
1
- // bertui/src/build/compiler/file-transpiler.js - FORCE PRODUCTION JSX
1
+ // bertui/src/build/compiler/file-transpiler.js
2
2
  import { join, relative, dirname, extname } from 'path';
3
- import { readdirSync, statSync, mkdirSync, writeFileSync } from 'fs';
3
+ import { readdirSync, statSync, mkdirSync, writeFileSync, existsSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
5
5
  import { replaceEnvInCode } from '../../utils/env.js';
6
+ import { buildAliasMap, rewriteAliasImports } from '../../utils/importhow.js';
6
7
 
7
- export async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
8
- // Create bunfig.toml in build directory
9
- const bunfigContent = `
10
- [build]
11
- jsx = "react"
12
- jsxFactory = "React.createElement"
13
- jsxFragment = "React.Fragment"
14
- `.trim();
15
-
16
- writeFileSync(join(buildDir, 'bunfig.toml'), bunfigContent);
8
+ /**
9
+ * Compile src/ + alias dirs into buildDir.
10
+ */
11
+ export async function compileBuildDirectory(srcDir, buildDir, root, envVars, importhow = {}) {
12
+ writeFileSync(
13
+ join(buildDir, 'bunfig.toml'),
14
+ `[build]\njsx = "react"\njsxFactory = "React.createElement"\njsxFragment = "React.Fragment"`.trim()
15
+ );
17
16
  logger.info('Created bunfig.toml for classic JSX');
18
-
17
+
18
+ // Build mode: aliases resolve to buildDir/<alias> so relative paths inside dist/ are correct
19
+ const aliasMap = buildAliasMap(importhow, root, buildDir);
20
+
21
+ // Compile src/
22
+ await _compileDir(srcDir, buildDir, root, envVars, aliasMap);
23
+
24
+ // Compile each alias source dir → buildDir/<alias>
25
+ for (const [alias, relPath] of Object.entries(importhow)) {
26
+ const absAliasDir = join(root, relPath);
27
+
28
+ if (!existsSync(absAliasDir)) {
29
+ logger.warn(`⚠️ importhow alias "${alias}" points to missing dir: ${absAliasDir}`);
30
+ continue;
31
+ }
32
+
33
+ const aliasOutDir = join(buildDir, alias);
34
+ mkdirSync(aliasOutDir, { recursive: true });
35
+
36
+ logger.info(`📦 Compiling alias dir [${alias}] → ${aliasOutDir}`);
37
+ await _compileDir(absAliasDir, aliasOutDir, root, envVars, aliasMap);
38
+ }
39
+ }
40
+
41
+ // ─────────────────────────────────────────────────────────────────────────────
42
+
43
+ async function _compileDir(srcDir, buildDir, root, envVars, aliasMap) {
19
44
  const files = readdirSync(srcDir);
20
45
  const filesToCompile = [];
21
-
46
+
22
47
  for (const file of files) {
23
48
  const srcPath = join(srcDir, file);
24
- const stat = statSync(srcPath);
25
-
49
+ const stat = statSync(srcPath);
50
+
26
51
  if (stat.isDirectory()) {
27
52
  const subBuildDir = join(buildDir, file);
28
53
  mkdirSync(subBuildDir, { recursive: true });
29
- await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
54
+ await _compileDir(srcPath, subBuildDir, root, envVars, aliasMap);
30
55
  } else {
31
56
  const ext = extname(file);
32
57
  if (ext === '.css') continue;
33
-
34
58
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
35
59
  filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'tsx' });
36
60
  } else if (ext === '.js') {
@@ -38,58 +62,67 @@ jsxFragment = "React.Fragment"
38
62
  }
39
63
  }
40
64
  }
41
-
65
+
42
66
  if (filesToCompile.length === 0) return;
43
-
44
- logger.info(`📦 Compiling ${filesToCompile.length} files...`);
45
-
67
+
68
+ logger.info(`📦 Compiling ${filesToCompile.length} files in ${srcDir.split('/').slice(-2).join('/')}...`);
69
+
46
70
  for (let i = 0; i < filesToCompile.length; i++) {
47
71
  const file = filesToCompile[i];
48
-
49
72
  try {
50
73
  if (file.type === 'tsx') {
51
- await compileBuildFile(file.path, file.dir, file.name, root, envVars, buildDir);
74
+ await _compileTSXFile(file.path, file.dir, file.name, root, envVars, buildDir, aliasMap);
52
75
  } else {
53
- await compileJSFile(file.path, file.dir, file.name, root, envVars);
76
+ await _compileJSFile(file.path, file.dir, file.name, root, envVars, aliasMap);
54
77
  }
55
-
78
+
56
79
  if ((i + 1) % 10 === 0 || i === filesToCompile.length - 1) {
57
- const percent = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
58
- logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${percent}%)`);
80
+ const pct = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
81
+ logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${pct}%)`);
59
82
  }
60
-
61
83
  } catch (error) {
62
84
  logger.error(`Failed to compile ${file.name}: ${error.message}`);
63
85
  }
64
86
  }
65
-
87
+
66
88
  logger.success(`✅ Compiled ${filesToCompile.length} files`);
67
89
  }
68
90
 
69
- async function compileBuildFile(srcPath, buildDir, filename, root, envVars, configDir) {
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+ // _rewriteNodeModuleImports — intentionally a no-op.
93
+ //
94
+ // Previously this rewrote bare specifiers like 'react' → '/node_modules/react/index.js'
95
+ // which caused "Could not resolve" errors during Bun.build because:
96
+ // 1. 'react' is marked `external` in Bun.build and expected as a bare specifier.
97
+ // 2. Other npm packages are better handled by Bun.build natively (tree-shaken + minified).
98
+ //
99
+ // Leaving bare specifiers untouched lets Bun.build do the right thing for both cases.
100
+ // ─────────────────────────────────────────────────────────────────────────────
101
+ function _rewriteNodeModuleImports(code) {
102
+ return code;
103
+ }
104
+
105
+ async function _compileTSXFile(srcPath, buildDir, filename, root, envVars, configDir, aliasMap) {
70
106
  const ext = extname(filename);
71
-
107
+
72
108
  try {
73
109
  let code = await Bun.file(srcPath).text();
74
- code = removeCSSImports(code);
110
+ code = _removeCSSImports(code);
75
111
  code = replaceEnvInCode(code, envVars);
76
-
112
+
77
113
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
78
- const outPath = join(buildDir, outFilename);
79
- code = fixBuildImports(code, srcPath, outPath, root);
80
-
81
- // Add React import BEFORE transpiling
114
+ const outPath = join(buildDir, outFilename);
115
+
116
+ code = _fixBuildImports(code, srcPath, outPath, root);
117
+
82
118
  if (!code.includes('import React')) {
83
119
  code = `import React from 'react';\n${code}`;
84
120
  }
85
-
86
- // Use Bun.Transpiler with explicit production settings
121
+
87
122
  const transpiler = new Bun.Transpiler({
88
123
  loader: ext === '.tsx' ? 'tsx' : ext === '.ts' ? 'ts' : 'jsx',
89
124
  target: 'browser',
90
- define: {
91
- 'process.env.NODE_ENV': '"production"'
92
- },
125
+ define: { 'process.env.NODE_ENV': '"production"' },
93
126
  tsconfig: {
94
127
  compilerOptions: {
95
128
  jsx: 'react',
@@ -99,69 +132,79 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars, conf
99
132
  }
100
133
  }
101
134
  });
102
-
135
+
103
136
  let compiled = await transpiler.transform(code);
104
-
105
- // Verify no dev JSX leaked through
137
+
106
138
  if (compiled.includes('jsxDEV')) {
107
- logger.warn(`⚠️ Dev JSX detected in ${filename}, fixing...`);
139
+ logger.warn(`⚠️ Dev JSX in ${filename}, fixing...`);
108
140
  compiled = compiled.replace(/jsxDEV/g, 'jsx');
109
141
  }
110
-
111
- compiled = fixRelativeImports(compiled);
142
+
143
+ compiled = _fixRelativeImports(compiled);
144
+
145
+ // ✅ Alias rewrite AFTER transpile — Bun won't undo it
146
+ compiled = rewriteAliasImports(compiled, outPath, aliasMap);
147
+
148
+ // NOTE: _rewriteNodeModuleImports is intentionally a no-op — bare specifiers
149
+ // are left for Bun.build to handle natively (tree-shaking + bundling).
150
+ compiled = _rewriteNodeModuleImports(compiled);
151
+
112
152
  await Bun.write(outPath, compiled);
113
-
114
- code = null;
115
- compiled = null;
116
-
153
+
117
154
  } catch (error) {
118
155
  logger.error(`Failed to compile ${filename}: ${error.message}`);
119
156
  throw error;
120
157
  }
121
158
  }
122
159
 
123
- async function compileJSFile(srcPath, buildDir, filename, root, envVars) {
160
+ async function _compileJSFile(srcPath, buildDir, filename, root, envVars, aliasMap) {
124
161
  const outPath = join(buildDir, filename);
125
162
  let code = await Bun.file(srcPath).text();
126
- code = removeCSSImports(code);
163
+ code = _removeCSSImports(code);
127
164
  code = replaceEnvInCode(code, envVars);
128
- code = fixBuildImports(code, srcPath, outPath, root);
129
-
130
- if (usesJSX(code) && !code.includes('import React')) {
165
+ code = _fixBuildImports(code, srcPath, outPath, root);
166
+
167
+ // JS files don't go through Bun.Transpiler so rewrite is safe here
168
+ code = rewriteAliasImports(code, outPath, aliasMap);
169
+
170
+ // NOTE: _rewriteNodeModuleImports is intentionally a no-op — see above.
171
+ code = _rewriteNodeModuleImports(code);
172
+
173
+ if (_usesJSX(code) && !code.includes('import React')) {
131
174
  code = `import React from 'react';\n${code}`;
132
175
  }
133
-
176
+
134
177
  await Bun.write(outPath, code);
135
- code = null;
136
178
  }
137
179
 
138
- function usesJSX(code) {
139
- return code.includes('React.createElement') ||
140
- code.includes('React.Fragment') ||
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+
182
+ function _usesJSX(code) {
183
+ return code.includes('React.createElement') ||
184
+ code.includes('React.Fragment') ||
141
185
  /<[A-Z]/.test(code);
142
186
  }
143
187
 
144
- function removeCSSImports(code) {
188
+ function _removeCSSImports(code) {
145
189
  code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
146
190
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
147
191
  return code;
148
192
  }
149
193
 
150
- function fixBuildImports(code, srcPath, outPath, root) {
151
- const buildDir = join(root, '.bertuibuild');
152
- const routerPath = join(buildDir, 'router.js');
153
- const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
154
- const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
155
-
194
+ function _fixBuildImports(code, srcPath, outPath, root) {
195
+ const buildDir = join(root, '.bertuibuild');
196
+ const routerPath = join(buildDir, 'router.js');
197
+ const rel = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
198
+ const routerImport = rel.startsWith('.') ? rel : './' + rel;
156
199
  code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
157
200
  return code;
158
201
  }
159
202
 
160
- function fixRelativeImports(code) {
161
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
162
- code = code.replace(importRegex, (match, prefix, path) => {
163
- if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
164
- return `from '${prefix}${path}.js';`;
203
+ function _fixRelativeImports(code) {
204
+ const importRegex = /from\s+['"](\.\.[\\/]|\.\/)(?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json)['"]/g;
205
+ code = code.replace(importRegex, (match) => {
206
+ if (/\.\w+['"]/.test(match)) return match;
207
+ return match.replace(/['"]$/, '.js"');
165
208
  });
166
209
  return code;
167
- }
210
+ }
@@ -1,4 +1,4 @@
1
- // bertui/src/build/compiler/index.js
1
+ // bertui/src/build/compiler/index.js - WITH IMPORTHOW + NODE MODULE SUPPORT
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
4
  import logger from '../../logger/logger.js';
@@ -6,26 +6,33 @@ import { discoverRoutes } from './route-discoverer.js';
6
6
  import { compileBuildDirectory } from './file-transpiler.js';
7
7
  import { generateBuildRouter } from './router-generator.js';
8
8
 
9
-
10
- export async function compileForBuild(root, buildDir, envVars) {
11
- const srcDir = join(root, 'src');
9
+ /**
10
+ * @param {string} root
11
+ * @param {string} buildDir
12
+ * @param {Object} envVars
13
+ * @param {Object} config - full bertui config (includes importhow)
14
+ */
15
+ export async function compileForBuild(root, buildDir, envVars, config = {}) {
16
+ const srcDir = join(root, 'src');
12
17
  const pagesDir = join(srcDir, 'pages');
13
-
18
+
14
19
  if (!existsSync(srcDir)) {
15
20
  throw new Error('src/ directory not found!');
16
21
  }
17
-
18
- let routes = [];
22
+
23
+ const importhow = config.importhow || {};
24
+
25
+ let routes = [];
19
26
  let serverIslands = [];
20
- let clientRoutes = [];
21
-
27
+ let clientRoutes = [];
28
+
22
29
  if (existsSync(pagesDir)) {
23
30
  routes = await discoverRoutes(pagesDir);
24
-
31
+
25
32
  for (const route of routes) {
26
33
  const sourceCode = await Bun.file(route.path).text();
27
34
  const isServerIsland = sourceCode.includes('export const render = "server"');
28
-
35
+
29
36
  if (isServerIsland) {
30
37
  serverIslands.push(route);
31
38
  logger.success(`🏝️ Server Island: ${route.route}`);
@@ -34,12 +41,13 @@ export async function compileForBuild(root, buildDir, envVars) {
34
41
  }
35
42
  }
36
43
  }
37
-
38
- await compileBuildDirectory(srcDir, buildDir, root, envVars);
39
-
44
+
45
+ // Pass importhow so alias dirs also get compiled
46
+ await compileBuildDirectory(srcDir, buildDir, root, envVars, importhow);
47
+
40
48
  if (routes.length > 0) {
41
49
  await generateBuildRouter(routes, buildDir);
42
50
  }
43
-
51
+
44
52
  return { routes, serverIslands, clientRoutes };
45
53
  }
@@ -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);