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,131 +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 { 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);
6
+ import { buildAliasMap, rewriteAliasImports } from '../../utils/importhow.js';
7
+
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
+ );
16
+ logger.info('Created bunfig.toml for classic JSX');
47
17
 
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
- }
18
+ // Build mode: aliases resolve to buildDir/<alias> so relative paths inside dist/ are correct
19
+ const aliasMap = buildAliasMap(importhow, root, buildDir);
61
20
 
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);
21
+ // Compile src/
22
+ await _compileDir(srcDir, buildDir, root, envVars, aliasMap);
67
23
 
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);
24
+ // Compile each alias source dir buildDir/<alias>
25
+ for (const [alias, relPath] of Object.entries(importhow)) {
26
+ const absAliasDir = join(root, relPath);
72
27
 
73
- logger.debug(`CSS Module: ${filename} → ${Object.keys(mapping).length} classes scoped`);
74
- }
28
+ if (!existsSync(absAliasDir)) {
29
+ logger.warn(`⚠️ importhow alias "${alias}" points to missing dir: ${absAliasDir}`);
30
+ continue;
31
+ }
75
32
 
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
- });
33
+ const aliasOutDir = join(buildDir, alias);
34
+ mkdirSync(aliasOutDir, { recursive: true });
88
35
 
89
- return code;
36
+ logger.info(`📦 Compiling alias dir [${alias}] → ${aliasOutDir}`);
37
+ await _compileDir(absAliasDir, aliasOutDir, root, envVars, aliasMap);
38
+ }
90
39
  }
91
40
 
92
- // ============================================
93
- // MAIN COMPILER
94
- // ============================================
95
-
96
- export async function compileBuildDirectory(srcDir, buildDir, root, envVars) {
97
- const bunfigContent = `
98
- [build]
99
- jsx = "react"
100
- jsxFactory = "React.createElement"
101
- jsxFragment = "React.Fragment"
102
- `.trim();
103
-
104
- writeFileSync(join(buildDir, 'bunfig.toml'), bunfigContent);
105
- logger.info('Created bunfig.toml for classic JSX');
41
+ // ─────────────────────────────────────────────────────────────────────────────
106
42
 
43
+ async function _compileDir(srcDir, buildDir, root, envVars, aliasMap) {
107
44
  const files = readdirSync(srcDir);
108
45
  const filesToCompile = [];
109
- const rootBuildDir = join(root, '.bertuibuild');
110
46
 
111
47
  for (const file of files) {
112
48
  const srcPath = join(srcDir, file);
113
- const stat = statSync(srcPath);
49
+ const stat = statSync(srcPath);
114
50
 
115
51
  if (stat.isDirectory()) {
116
52
  const subBuildDir = join(buildDir, file);
117
53
  mkdirSync(subBuildDir, { recursive: true });
118
- await compileBuildDirectory(srcPath, subBuildDir, root, envVars);
54
+ await _compileDir(srcPath, subBuildDir, root, envVars, aliasMap);
119
55
  } else {
120
56
  const ext = extname(file);
121
-
122
- if (file.endsWith('.module.css')) {
123
- await compileCSSModule(srcPath, rootBuildDir);
124
- continue;
125
- }
126
-
127
57
  if (ext === '.css') continue;
128
-
129
58
  if (['.jsx', '.tsx', '.ts'].includes(ext)) {
130
59
  filesToCompile.push({ path: srcPath, dir: buildDir, name: file, type: 'tsx' });
131
60
  } else if (ext === '.js') {
@@ -136,23 +65,21 @@ jsxFragment = "React.Fragment"
136
65
 
137
66
  if (filesToCompile.length === 0) return;
138
67
 
139
- logger.info(`📦 Compiling ${filesToCompile.length} files...`);
68
+ logger.info(`📦 Compiling ${filesToCompile.length} files in ${srcDir.split('/').slice(-2).join('/')}...`);
140
69
 
141
70
  for (let i = 0; i < filesToCompile.length; i++) {
142
71
  const file = filesToCompile[i];
143
-
144
72
  try {
145
73
  if (file.type === 'tsx') {
146
- await compileBuildFile(file.path, file.dir, file.name, root, envVars, rootBuildDir);
74
+ await _compileTSXFile(file.path, file.dir, file.name, root, envVars, buildDir, aliasMap);
147
75
  } else {
148
- await compileJSFile(file.path, file.dir, file.name, root, envVars, rootBuildDir);
76
+ await _compileJSFile(file.path, file.dir, file.name, root, envVars, aliasMap);
149
77
  }
150
78
 
151
79
  if ((i + 1) % 10 === 0 || i === filesToCompile.length - 1) {
152
- const percent = (((i + 1) / filesToCompile.length) * 100).toFixed(0);
153
- 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}%)`);
154
82
  }
155
-
156
83
  } catch (error) {
157
84
  logger.error(`Failed to compile ${file.name}: ${error.message}`);
158
85
  }
@@ -161,19 +88,32 @@ jsxFragment = "React.Fragment"
161
88
  logger.success(`✅ Compiled ${filesToCompile.length} files`);
162
89
  }
163
90
 
164
- async function compileBuildFile(srcPath, buildDir, filename, root, envVars, rootBuildDir) {
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) {
165
106
  const ext = extname(filename);
166
107
 
167
108
  try {
168
109
  let code = await Bun.file(srcPath).text();
110
+ code = _removeCSSImports(code);
169
111
  code = replaceEnvInCode(code, envVars);
170
112
 
171
113
  const outFilename = filename.replace(/\.(jsx|tsx|ts)$/, '.js');
172
- const outPath = join(buildDir, outFilename);
114
+ const outPath = join(buildDir, outFilename);
173
115
 
174
- code = transformCSSModuleImports(code, outPath, rootBuildDir);
175
- code = removePlainCSSImports(code);
176
- code = fixBuildImports(code, srcPath, outPath, root);
116
+ code = _fixBuildImports(code, srcPath, outPath, root);
177
117
 
178
118
  if (!code.includes('import React')) {
179
119
  code = `import React from 'react';\n${code}`;
@@ -196,15 +136,20 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars, root
196
136
  let compiled = await transpiler.transform(code);
197
137
 
198
138
  if (compiled.includes('jsxDEV')) {
199
- logger.warn(`⚠️ Dev JSX detected in ${filename}, fixing...`);
139
+ logger.warn(`⚠️ Dev JSX in ${filename}, fixing...`);
200
140
  compiled = compiled.replace(/jsxDEV/g, 'jsx');
201
141
  }
202
142
 
203
- compiled = fixRelativeImports(compiled);
204
- await Bun.write(outPath, compiled);
143
+ compiled = _fixRelativeImports(compiled);
205
144
 
206
- code = null;
207
- compiled = null;
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
+
152
+ await Bun.write(outPath, compiled);
208
153
 
209
154
  } catch (error) {
210
155
  logger.error(`Failed to compile ${filename}: ${error.message}`);
@@ -212,48 +157,54 @@ async function compileBuildFile(srcPath, buildDir, filename, root, envVars, root
212
157
  }
213
158
  }
214
159
 
215
- async function compileJSFile(srcPath, buildDir, filename, root, envVars, rootBuildDir) {
160
+ async function _compileJSFile(srcPath, buildDir, filename, root, envVars, aliasMap) {
216
161
  const outPath = join(buildDir, filename);
217
162
  let code = await Bun.file(srcPath).text();
163
+ code = _removeCSSImports(code);
218
164
  code = replaceEnvInCode(code, envVars);
219
- code = transformCSSModuleImports(code, outPath, rootBuildDir);
220
- code = removePlainCSSImports(code);
221
- code = fixBuildImports(code, srcPath, outPath, root);
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);
222
169
 
223
- if (usesJSX(code) && !code.includes('import React')) {
170
+ // NOTE: _rewriteNodeModuleImports is intentionally a no-op — see above.
171
+ code = _rewriteNodeModuleImports(code);
172
+
173
+ if (_usesJSX(code) && !code.includes('import React')) {
224
174
  code = `import React from 'react';\n${code}`;
225
175
  }
226
176
 
227
177
  await Bun.write(outPath, code);
228
- code = null;
229
178
  }
230
179
 
231
- function usesJSX(code) {
180
+ // ─────────────────────────────────────────────────────────────────────────────
181
+
182
+ function _usesJSX(code) {
232
183
  return code.includes('React.createElement') ||
233
- code.includes('React.Fragment') ||
184
+ code.includes('React.Fragment') ||
234
185
  /<[A-Z]/.test(code);
235
186
  }
236
187
 
237
- function removePlainCSSImports(code) {
238
- code = code.replace(/import\s+['"][^'"]*(?<!\.module)\.css['"];?\s*/g, '');
188
+ function _removeCSSImports(code) {
189
+ code = code.replace(/import\s+['"][^'"]*\.css['"];?\s*/g, '');
239
190
  code = code.replace(/import\s+['"]bertui\/styles['"]\s*;?\s*/g, '');
240
191
  return code;
241
192
  }
242
193
 
243
- function fixBuildImports(code, srcPath, outPath, root) {
244
- const buildDir = join(root, '.bertuibuild');
245
- const routerPath = join(buildDir, 'router.js');
246
- const relativeToRouter = relative(dirname(outPath), routerPath).replace(/\\/g, '/');
247
- const routerImport = relativeToRouter.startsWith('.') ? relativeToRouter : './' + relativeToRouter;
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;
248
199
  code = code.replace(/from\s+['"]bertui\/router['"]/g, `from '${routerImport}'`);
249
200
  return code;
250
201
  }
251
202
 
252
- function fixRelativeImports(code) {
253
- const importRegex = /from\s+['"](\.\.[\/\\]|\.\/)((?:[^'"]+?)(?<!\.js|\.jsx|\.ts|\.tsx|\.json))['"];?/g;
254
- code = code.replace(importRegex, (match, prefix, path) => {
255
- if (path.endsWith('/') || /\.\w+$/.test(path)) return match;
256
- 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"');
257
208
  });
258
209
  return code;
259
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
  }