bertui 1.2.1 → 1.2.3
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/README.md +152 -197
- package/index.js +16 -8
- package/package.json +1 -1
- package/src/build/compiler/file-transpiler.js +92 -141
- package/src/build/compiler/index.js +23 -15
- package/src/build.js +149 -93
- package/src/client/compiler.js +169 -157
- package/src/config/defaultConfig.js +13 -4
- package/src/config/loadConfig.js +47 -32
- package/src/dev.js +34 -30
- package/src/logger/logger.js +294 -16
- package/src/server/dev-handler.js +11 -0
- package/src/server/dev-server-utils.js +262 -160
- package/src/utils/importhow.js +52 -0
|
@@ -1,131 +1,60 @@
|
|
|
1
|
-
// bertui/src/build/compiler/file-transpiler.js
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
//
|
|
63
|
-
|
|
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
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
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
|
-
|
|
74
|
-
}
|
|
28
|
+
if (!existsSync(absAliasDir)) {
|
|
29
|
+
logger.warn(`⚠️ importhow alias "${alias}" points to missing dir: ${absAliasDir}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
75
32
|
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
74
|
+
await _compileTSXFile(file.path, file.dir, file.name, root, envVars, buildDir, aliasMap);
|
|
147
75
|
} else {
|
|
148
|
-
await
|
|
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
|
|
153
|
-
logger.info(` Progress: ${i + 1}/${filesToCompile.length} (${
|
|
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
|
-
|
|
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
|
|
114
|
+
const outPath = join(buildDir, outFilename);
|
|
173
115
|
|
|
174
|
-
code =
|
|
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
|
|
139
|
+
logger.warn(`⚠️ Dev JSX in ${filename}, fixing...`);
|
|
200
140
|
compiled = compiled.replace(/jsxDEV/g, 'jsx');
|
|
201
141
|
}
|
|
202
142
|
|
|
203
|
-
compiled =
|
|
204
|
-
await Bun.write(outPath, compiled);
|
|
143
|
+
compiled = _fixRelativeImports(compiled);
|
|
205
144
|
|
|
206
|
-
|
|
207
|
-
compiled =
|
|
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
|
|
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 =
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
238
|
-
code = code.replace(/import\s+['"][^'"]
|
|
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
|
|
244
|
-
const buildDir
|
|
245
|
-
const routerPath
|
|
246
|
-
const
|
|
247
|
-
const routerImport =
|
|
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
|
|
253
|
-
const importRegex = /from\s+['"](\.\.[
|
|
254
|
-
code = code.replace(importRegex, (match
|
|
255
|
-
if (
|
|
256
|
-
return
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|