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.
- package/README.md +152 -197
- package/index.js +16 -8
- package/package.json +1 -1
- package/src/build/compiler/file-transpiler.js +119 -76
- package/src/build/compiler/index.js +23 -15
- package/src/build/compiler/route-discoverer.js +4 -3
- package/src/build/generators/sitemap-generator.js +1 -1
- package/src/build/processors/css-builder.js +45 -41
- package/src/build.js +147 -90
- 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 +222 -49
- 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
package/src/config/loadConfig.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/config/loadConfig.js
|
|
1
|
+
// bertui/src/config/loadConfig.js
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { defaultConfig } from './defaultConfig.js';
|
|
@@ -6,44 +6,59 @@ import logger from '../logger/logger.js';
|
|
|
6
6
|
|
|
7
7
|
export async function loadConfig(root) {
|
|
8
8
|
const configPath = join(root, 'bertui.config.js');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
9
|
+
|
|
10
|
+
if (!existsSync(configPath)) {
|
|
11
|
+
logger.info('No config found, using defaults');
|
|
12
|
+
return defaultConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
// Read and transpile the config file manually —
|
|
17
|
+
// avoids Bun's dynamic import() build step which errors on plain JS configs
|
|
18
|
+
const source = await Bun.file(configPath).text();
|
|
19
|
+
|
|
20
|
+
const transpiler = new Bun.Transpiler({
|
|
21
|
+
loader: 'js',
|
|
22
|
+
target: 'bun',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
let code = await transpiler.transform(source);
|
|
26
|
+
|
|
27
|
+
// Strip any leftover 'export default' so we can eval it
|
|
28
|
+
// and grab the value directly
|
|
29
|
+
code = code.replace(/export\s+default\s+/, 'globalThis.__bertuiConfig = ');
|
|
30
|
+
|
|
31
|
+
// Run it in the current context
|
|
32
|
+
const fn = new Function('globalThis', code);
|
|
33
|
+
fn(globalThis);
|
|
34
|
+
|
|
35
|
+
const userConfig = globalThis.__bertuiConfig;
|
|
36
|
+
delete globalThis.__bertuiConfig;
|
|
37
|
+
|
|
38
|
+
if (!userConfig) {
|
|
39
|
+
logger.warn('bertui.config.js did not export a default value, using defaults');
|
|
27
40
|
return defaultConfig;
|
|
28
41
|
}
|
|
42
|
+
|
|
43
|
+
logger.success('Loaded bertui.config.js');
|
|
44
|
+
|
|
45
|
+
logger.info(`📋 Config: importhow=${JSON.stringify(Object.keys(userConfig.importhow || {}))}`);
|
|
46
|
+
|
|
47
|
+
return mergeConfig(defaultConfig, userConfig);
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.error(`Failed to load bertui.config.js: ${error.message}`);
|
|
51
|
+
return defaultConfig;
|
|
29
52
|
}
|
|
30
|
-
|
|
31
|
-
logger.info('No config found, using defaults');
|
|
32
|
-
return defaultConfig;
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
function mergeConfig(defaults, user) {
|
|
36
|
-
// Start with user config (so user values override defaults)
|
|
37
56
|
const merged = { ...user };
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
merged.
|
|
41
|
-
merged.
|
|
42
|
-
merged.robots = { ...defaults.robots, ...(user.robots || {}) };
|
|
43
|
-
|
|
44
|
-
// Ensure we have required top-level fields
|
|
57
|
+
merged.meta = { ...defaults.meta, ...(user.meta || {}) };
|
|
58
|
+
merged.appShell = { ...defaults.appShell, ...(user.appShell || {}) };
|
|
59
|
+
merged.robots = { ...defaults.robots, ...(user.robots || {}) };
|
|
60
|
+
merged.importhow = { ...(defaults.importhow || {}), ...(user.importhow || {}) };
|
|
45
61
|
if (!merged.siteName) merged.siteName = defaults.siteName;
|
|
46
|
-
if (!merged.baseUrl)
|
|
47
|
-
|
|
62
|
+
if (!merged.baseUrl) merged.baseUrl = defaults.baseUrl;
|
|
48
63
|
return merged;
|
|
49
64
|
}
|
package/src/dev.js
CHANGED
|
@@ -1,64 +1,237 @@
|
|
|
1
|
-
// bertui/src/
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
1
|
+
// bertui/src/build.js
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from 'fs';
|
|
4
|
+
import logger from './logger/logger.js';
|
|
5
|
+
import { loadEnvVariables } from './utils/env.js';
|
|
6
|
+
import { globalCache } from './utils/cache.js';
|
|
7
|
+
|
|
8
|
+
import { compileForBuild } from './build/compiler/index.js';
|
|
9
|
+
import { buildAllCSS } from './build/processors/css-builder.js';
|
|
10
|
+
import { copyAllStaticAssets } from './build/processors/asset-processor.js';
|
|
11
|
+
import { generateProductionHTML } from './build/generators/html-generator.js';
|
|
12
|
+
import { generateSitemap } from './build/generators/sitemap-generator.js';
|
|
13
|
+
import { generateRobots } from './build/generators/robots-generator.js';
|
|
14
|
+
import { compileLayouts } from './layouts/index.js';
|
|
6
15
|
import { compileLoadingComponents } from './loading/index.js';
|
|
7
16
|
import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
|
|
8
|
-
import
|
|
9
|
-
import {
|
|
17
|
+
import { analyzeBuild } from './analyzer/index.js';
|
|
18
|
+
import { buildAliasMap } from './utils/importhow.js';
|
|
19
|
+
|
|
20
|
+
const TOTAL_STEPS = 10;
|
|
21
|
+
|
|
22
|
+
export async function buildProduction(options = {}) {
|
|
23
|
+
const root = options.root || process.cwd();
|
|
24
|
+
const buildDir = join(root, '.bertuibuild');
|
|
25
|
+
const outDir = join(root, 'dist');
|
|
26
|
+
|
|
27
|
+
process.env.NODE_ENV = 'production';
|
|
10
28
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
logger.printHeader('BUILD');
|
|
30
|
+
|
|
31
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
32
|
+
if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
|
|
33
|
+
mkdirSync(buildDir, { recursive: true });
|
|
34
|
+
mkdirSync(outDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
let totalKB = '0';
|
|
14
37
|
|
|
15
38
|
try {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
39
|
+
// ── Step 1: Env ──────────────────────────────────────────────────────────
|
|
40
|
+
logger.step(1, TOTAL_STEPS, 'Loading env');
|
|
41
|
+
const envVars = loadEnvVariables(root);
|
|
42
|
+
const { loadConfig } = await import('./config/loadConfig.js');
|
|
43
|
+
const config = await loadConfig(root);
|
|
44
|
+
const importhow = config.importhow || {};
|
|
45
|
+
logger.stepDone('Loading env', `${Object.keys(envVars).length} vars`);
|
|
46
|
+
|
|
47
|
+
// ── Step 2: Compile ──────────────────────────────────────────────────────
|
|
48
|
+
logger.step(2, TOTAL_STEPS, 'Compiling');
|
|
49
|
+
const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars, config);
|
|
50
|
+
logger.stepDone('Compiling', `${routes.length} routes · ${serverIslands.length} islands`);
|
|
51
|
+
|
|
52
|
+
// ── Step 3: Layouts ──────────────────────────────────────────────────────
|
|
53
|
+
logger.step(3, TOTAL_STEPS, 'Layouts');
|
|
54
|
+
const layouts = await compileLayouts(root, buildDir);
|
|
55
|
+
logger.stepDone('Layouts', `${Object.keys(layouts).length} found`);
|
|
56
|
+
|
|
57
|
+
// ── Step 4: Loading states ───────────────────────────────────────────────
|
|
58
|
+
logger.step(4, TOTAL_STEPS, 'Loading states');
|
|
59
|
+
await compileLoadingComponents(root, buildDir);
|
|
60
|
+
logger.stepDone('Loading states');
|
|
31
61
|
|
|
32
|
-
// Step
|
|
33
|
-
logger.
|
|
34
|
-
const
|
|
62
|
+
// ── Step 5: Hydration analysis ───────────────────────────────────────────
|
|
63
|
+
logger.step(5, TOTAL_STEPS, 'Hydration analysis');
|
|
64
|
+
const analyzedRoutes = await analyzeRoutes(routes);
|
|
65
|
+
logger.stepDone('Hydration analysis',
|
|
66
|
+
`${analyzedRoutes.interactive.length} interactive · ${analyzedRoutes.static.length} static`);
|
|
35
67
|
|
|
36
|
-
// Step
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
68
|
+
// ── Step 6: CSS ──────────────────────────────────────────────────────────
|
|
69
|
+
logger.step(6, TOTAL_STEPS, 'Processing CSS');
|
|
70
|
+
await buildAllCSS(root, outDir);
|
|
71
|
+
logger.stepDone('Processing CSS');
|
|
72
|
+
|
|
73
|
+
// ── Step 7: Static assets ────────────────────────────────────────────────
|
|
74
|
+
logger.step(7, TOTAL_STEPS, 'Static assets');
|
|
75
|
+
await copyAllStaticAssets(root, outDir);
|
|
76
|
+
logger.stepDone('Static assets');
|
|
77
|
+
|
|
78
|
+
// ── Step 8: Bundle JS ────────────────────────────────────────────────────
|
|
79
|
+
logger.step(8, TOTAL_STEPS, 'Bundling JS');
|
|
80
|
+
const buildEntry = join(buildDir, 'main.js');
|
|
81
|
+
const routerPath = join(buildDir, 'router.js');
|
|
82
|
+
if (!existsSync(buildEntry)) throw new Error('main.js not found in build dir');
|
|
83
|
+
const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config);
|
|
84
|
+
totalKB = (result.outputs.reduce((a, o) => a + (o.size || 0), 0) / 1024).toFixed(1);
|
|
85
|
+
logger.stepDone('Bundling JS', `${totalKB} KB · tree-shaken`);
|
|
86
|
+
|
|
87
|
+
// ── Step 9: HTML ─────────────────────────────────────────────────────────
|
|
88
|
+
logger.step(9, TOTAL_STEPS, 'Generating HTML');
|
|
89
|
+
await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
|
|
90
|
+
logger.stepDone('Generating HTML', `${routes.length} pages`);
|
|
91
|
+
|
|
92
|
+
// ── Step 10: Sitemap + robots ────────────────────────────────────────────
|
|
93
|
+
logger.step(10, TOTAL_STEPS, 'Sitemap & robots');
|
|
94
|
+
await generateSitemap(routes, config, outDir);
|
|
95
|
+
await generateRobots(config, outDir, routes);
|
|
96
|
+
logger.stepDone('Sitemap & robots');
|
|
97
|
+
|
|
98
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
99
|
+
|
|
100
|
+
// Fire-and-forget — don't let the report generator block process exit
|
|
101
|
+
analyzeBuild(outDir, { outputFile: join(outDir, 'bundle-report.html') }).catch(() => {});
|
|
102
|
+
|
|
103
|
+
// ── Summary ──────────────────────────────────────────────────────────────
|
|
104
|
+
logger.printSummary({
|
|
105
|
+
routes: routes.length,
|
|
106
|
+
serverIslands: serverIslands.length,
|
|
107
|
+
interactive: analyzedRoutes.interactive.length,
|
|
108
|
+
staticRoutes: analyzedRoutes.static.length,
|
|
109
|
+
jsSize: `${totalKB} KB`,
|
|
110
|
+
outDir: 'dist/',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return { success: true };
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
logger.stepFail('Build', error.message);
|
|
117
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
async function generateProductionImportMap(root, config) {
|
|
125
|
+
const importMap = {
|
|
126
|
+
'react': 'https://esm.sh/react@18.2.0',
|
|
127
|
+
'react-dom': 'https://esm.sh/react-dom@18.2.0',
|
|
128
|
+
'react-dom/client': 'https://esm.sh/react-dom@18.2.0/client',
|
|
129
|
+
'react/jsx-runtime': 'https://esm.sh/react@18.2.0/jsx-runtime',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const nodeModulesDir = join(root, 'node_modules');
|
|
133
|
+
if (!existsSync(nodeModulesDir)) return importMap;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
for (const pkg of readdirSync(nodeModulesDir)) {
|
|
137
|
+
if (!pkg.startsWith('bertui-') || pkg.startsWith('.')) continue;
|
|
138
|
+
const pkgDir = join(nodeModulesDir, pkg);
|
|
139
|
+
const pkgJson = join(pkgDir, 'package.json');
|
|
140
|
+
if (!existsSync(pkgJson)) continue;
|
|
141
|
+
try {
|
|
142
|
+
const p = JSON.parse(await Bun.file(pkgJson).text());
|
|
143
|
+
for (const entry of [p.browser, p.module, p.main, 'dist/index.js', 'index.js'].filter(Boolean)) {
|
|
144
|
+
if (existsSync(join(pkgDir, entry))) {
|
|
145
|
+
importMap[pkg] = `/assets/node_modules/${pkg}/${entry}`;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch { continue; }
|
|
41
150
|
}
|
|
151
|
+
} catch { /* ignore */ }
|
|
152
|
+
|
|
153
|
+
return importMap;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function copyNodeModulesToDist(root, outDir, importMap) {
|
|
157
|
+
const dest = join(outDir, 'assets', 'node_modules');
|
|
158
|
+
mkdirSync(dest, { recursive: true });
|
|
159
|
+
const src = join(root, 'node_modules');
|
|
160
|
+
|
|
161
|
+
for (const [, assetPath] of Object.entries(importMap)) {
|
|
162
|
+
if (assetPath.startsWith('https://')) continue;
|
|
163
|
+
const match = assetPath.match(/\/assets\/node_modules\/(.+)$/);
|
|
164
|
+
if (!match) continue;
|
|
165
|
+
const parts = match[1].split('/');
|
|
166
|
+
const pkgName = parts[0];
|
|
167
|
+
const subPath = parts.slice(1);
|
|
168
|
+
const srcFile = join(src, pkgName, ...subPath);
|
|
169
|
+
const destFile = join(dest, pkgName, ...subPath);
|
|
170
|
+
mkdirSync(join(dest, pkgName, ...subPath.slice(0, -1)), { recursive: true });
|
|
171
|
+
if (existsSync(srcFile)) await Bun.write(destFile, Bun.file(srcFile));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
|
|
176
|
+
const originalCwd = process.cwd();
|
|
177
|
+
process.chdir(buildDir);
|
|
42
178
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
179
|
+
try {
|
|
180
|
+
const entrypoints = [buildEntry];
|
|
181
|
+
if (existsSync(routerPath)) entrypoints.push(routerPath);
|
|
182
|
+
|
|
183
|
+
const importMap = await generateProductionImportMap(root, config);
|
|
184
|
+
await Bun.write(join(outDir, 'import-map.json'), JSON.stringify({ imports: importMap }, null, 2));
|
|
185
|
+
await copyNodeModulesToDist(root, outDir, importMap);
|
|
186
|
+
|
|
187
|
+
const result = await Bun.build({
|
|
188
|
+
entrypoints,
|
|
189
|
+
outdir: join(outDir, 'assets'),
|
|
190
|
+
target: 'browser',
|
|
191
|
+
format: 'esm',
|
|
192
|
+
minify: {
|
|
193
|
+
whitespace: true,
|
|
194
|
+
syntax: true,
|
|
195
|
+
identifiers: true,
|
|
196
|
+
},
|
|
197
|
+
splitting: true,
|
|
198
|
+
sourcemap: 'external',
|
|
199
|
+
metafile: true,
|
|
200
|
+
naming: {
|
|
201
|
+
entry: 'js/[name]-[hash].js',
|
|
202
|
+
chunk: 'js/chunks/[name]-[hash].js',
|
|
203
|
+
asset: 'assets/[name]-[hash].[ext]',
|
|
204
|
+
},
|
|
205
|
+
external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
|
|
206
|
+
define: {
|
|
207
|
+
'process.env.NODE_ENV': '"production"',
|
|
208
|
+
...Object.fromEntries(
|
|
209
|
+
Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
|
|
210
|
+
),
|
|
211
|
+
},
|
|
57
212
|
});
|
|
58
213
|
|
|
214
|
+
if (!result.success) {
|
|
215
|
+
throw new Error(`Bundle failed\n${result.logs?.map(l => l.message).join('\n') || 'Unknown error'}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (result.metafile) {
|
|
219
|
+
await Bun.write(join(outDir, 'metafile.json'), JSON.stringify(result.metafile, null, 2));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return result;
|
|
223
|
+
|
|
224
|
+
} finally {
|
|
225
|
+
process.chdir(originalCwd);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export async function build(options = {}) {
|
|
230
|
+
try {
|
|
231
|
+
await buildProduction(options);
|
|
59
232
|
} catch (error) {
|
|
60
|
-
|
|
61
|
-
if (error.stack) logger.error(error.stack);
|
|
233
|
+
console.error(error);
|
|
62
234
|
process.exit(1);
|
|
63
235
|
}
|
|
236
|
+
process.exit(0);
|
|
64
237
|
}
|