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,4 +1,4 @@
1
- // src/config/loadConfig.js - COMPLETE CORRECTED VERSION
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
- // Check if user created config
11
- if (existsSync(configPath)) {
12
- try {
13
- const userConfig = await import(configPath);
14
- logger.success('Loaded bertui.config.js');
15
-
16
- // DEBUG: Show what we loaded
17
- logger.info(`📋 Config loaded: ${JSON.stringify({
18
- hasSiteName: !!(userConfig.default?.siteName || userConfig.siteName),
19
- hasBaseUrl: !!(userConfig.default?.baseUrl || userConfig.baseUrl),
20
- hasRobots: !!(userConfig.default?.robots || userConfig.robots)
21
- })}`);
22
-
23
- // Merge user config with defaults
24
- return mergeConfig(defaultConfig, userConfig.default || userConfig);
25
- } catch (error) {
26
- logger.error(`Failed to load config. Make sure bertui.config.js is in the root directory: ${error.message}`);
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
- // Deep merge for nested objects
40
- merged.meta = { ...defaults.meta, ...(user.meta || {}) };
41
- merged.appShell = { ...defaults.appShell, ...(user.appShell || {}) };
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) merged.baseUrl = defaults.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/dev.js - WITH MIDDLEWARE + LAYOUTS + LOADING + PARTIAL HYDRATION
2
- import { compileProject } from './client/compiler.js';
3
- import { startDevServer } from './server/dev-server.js';
4
- import { MiddlewareManager } from './middleware/index.js';
5
- import { compileLayouts, discoverLayouts } from './layouts/index.js';
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 logger from './logger/logger.js';
9
- import { loadConfig } from './config/loadConfig.js';
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
- export async function startDev(options = {}) {
12
- const root = options.root || process.cwd();
13
- const port = options.port || 3000;
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
- const config = await loadConfig(root);
17
-
18
- // Step 1: Compile project
19
- logger.info('Step 1: Compiling project...');
20
- const { routes, outDir } = await compileProject(root);
21
-
22
- // Step 2: Compile layouts
23
- logger.info('Step 2: Loading layouts...');
24
- const layouts = await compileLayouts(root, outDir);
25
- const layoutCount = Object.keys(layouts).length;
26
- if (layoutCount > 0) {
27
- logger.success(`📐 ${layoutCount} layout(s) active`);
28
- } else {
29
- logger.info('No layouts found (create src/layouts/default.tsx to wrap all pages)');
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 3: Compile loading states
33
- logger.info('Step 3: Loading per-route loading states...');
34
- const loadingComponents = await compileLoadingComponents(root, outDir);
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 4: Analyze routes for partial hydration
37
- if (routes && routes.length > 0) {
38
- logger.info('Step 4: Analyzing routes for partial hydration...');
39
- const analyzedRoutes = await analyzeRoutes(routes);
40
- logHydrationReport(analyzedRoutes);
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
- // Step 5: Load middleware
44
- logger.info('Step 5: Loading middleware...');
45
- const middlewareManager = new MiddlewareManager(root);
46
- await middlewareManager.load();
47
- middlewareManager.watch(); // Hot-reload middleware on change
48
-
49
- // Step 6: Start dev server with all features
50
- logger.info('Step 6: Starting dev server...');
51
- await startDevServer({
52
- root,
53
- port,
54
- middleware: middlewareManager,
55
- layouts,
56
- loadingComponents,
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
- logger.error(`Dev server failed: ${error.message}`);
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
  }