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
package/src/build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// bertui/src/build.js
|
|
1
|
+
// bertui/src/build.js
|
|
2
2
|
import { join } from 'path';
|
|
3
|
-
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
3
|
+
import { existsSync, mkdirSync, rmSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import logger from './logger/logger.js';
|
|
5
5
|
import { loadEnvVariables } from './utils/env.js';
|
|
6
6
|
import { globalCache } from './utils/cache.js';
|
|
@@ -15,135 +15,194 @@ import { compileLayouts } from './layouts/index.js';
|
|
|
15
15
|
import { compileLoadingComponents } from './loading/index.js';
|
|
16
16
|
import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
|
|
17
17
|
import { analyzeBuild } from './analyzer/index.js';
|
|
18
|
+
import { buildAliasMap } from './utils/importhow.js';
|
|
19
|
+
|
|
20
|
+
const TOTAL_STEPS = 10;
|
|
18
21
|
|
|
19
22
|
export async function buildProduction(options = {}) {
|
|
20
|
-
const root
|
|
23
|
+
const root = options.root || process.cwd();
|
|
21
24
|
const buildDir = join(root, '.bertuibuild');
|
|
22
|
-
const outDir
|
|
25
|
+
const outDir = join(root, 'dist');
|
|
23
26
|
|
|
24
27
|
process.env.NODE_ENV = 'production';
|
|
25
28
|
|
|
26
|
-
logger.
|
|
29
|
+
logger.printHeader('BUILD');
|
|
27
30
|
|
|
28
31
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
29
|
-
if (existsSync(outDir))
|
|
32
|
+
if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
|
|
30
33
|
mkdirSync(buildDir, { recursive: true });
|
|
31
|
-
mkdirSync(outDir,
|
|
34
|
+
mkdirSync(outDir, { recursive: true });
|
|
32
35
|
|
|
33
|
-
|
|
36
|
+
let totalKB = '0';
|
|
34
37
|
|
|
35
38
|
try {
|
|
36
|
-
|
|
39
|
+
// ββ Step 1: Env ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
40
|
+
logger.step(1, TOTAL_STEPS, 'Loading env');
|
|
37
41
|
const envVars = loadEnvVariables(root);
|
|
38
|
-
|
|
39
42
|
const { loadConfig } = await import('./config/loadConfig.js');
|
|
40
|
-
const config
|
|
41
|
-
|
|
42
|
-
logger.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
route: r.route,
|
|
49
|
-
file: r.file,
|
|
50
|
-
mode: 'ποΈ Server Island (SSG)',
|
|
51
|
-
})));
|
|
52
|
-
}
|
|
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`);
|
|
53
51
|
|
|
54
|
-
//
|
|
55
|
-
logger.
|
|
52
|
+
// ββ Step 3: Layouts ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
53
|
+
logger.step(3, TOTAL_STEPS, 'Layouts');
|
|
56
54
|
const layouts = await compileLayouts(root, buildDir);
|
|
57
|
-
|
|
58
|
-
if (layoutCount > 0) {
|
|
59
|
-
logger.success(`π ${layoutCount} layout(s) compiled`);
|
|
60
|
-
}
|
|
55
|
+
logger.stepDone('Layouts', `${Object.keys(layouts).length} found`);
|
|
61
56
|
|
|
62
|
-
//
|
|
63
|
-
logger.
|
|
64
|
-
|
|
57
|
+
// ββ Step 4: Loading states βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
58
|
+
logger.step(4, TOTAL_STEPS, 'Loading states');
|
|
59
|
+
await compileLoadingComponents(root, buildDir);
|
|
60
|
+
logger.stepDone('Loading states');
|
|
65
61
|
|
|
66
|
-
//
|
|
67
|
-
logger.
|
|
62
|
+
// ββ Step 5: Hydration analysis βββββββββββββββββββββββββββββββββββββββββββ
|
|
63
|
+
logger.step(5, TOTAL_STEPS, 'Hydration analysis');
|
|
68
64
|
const analyzedRoutes = await analyzeRoutes(routes);
|
|
69
|
-
|
|
65
|
+
logger.stepDone('Hydration analysis',
|
|
66
|
+
`${analyzedRoutes.interactive.length} interactive Β· ${analyzedRoutes.static.length} static`);
|
|
70
67
|
|
|
71
|
-
|
|
68
|
+
// ββ Step 6: CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
69
|
+
logger.step(6, TOTAL_STEPS, 'Processing CSS');
|
|
72
70
|
await buildAllCSS(root, outDir);
|
|
71
|
+
logger.stepDone('Processing CSS');
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
// ββ Step 7: Static assets ββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
74
|
+
logger.step(7, TOTAL_STEPS, 'Static assets');
|
|
75
75
|
await copyAllStaticAssets(root, outDir);
|
|
76
|
+
logger.stepDone('Static assets');
|
|
76
77
|
|
|
77
|
-
|
|
78
|
+
// ββ Step 8: Bundle JS ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
79
|
+
logger.step(8, TOTAL_STEPS, 'Bundling JS');
|
|
78
80
|
const buildEntry = join(buildDir, 'main.js');
|
|
79
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`);
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes);
|
|
86
|
-
|
|
87
|
-
logger.info('Step 8: Generating HTML...');
|
|
87
|
+
// ββ Step 9: HTML βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
88
|
+
logger.step(9, TOTAL_STEPS, 'Generating HTML');
|
|
88
89
|
await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
|
|
90
|
+
logger.stepDone('Generating HTML', `${routes.length} pages`);
|
|
89
91
|
|
|
90
|
-
|
|
92
|
+
// ββ Step 10: Sitemap + robots ββββββββββββββββββββββββββββββββββββββββββββ
|
|
93
|
+
logger.step(10, TOTAL_STEPS, 'Sitemap & robots');
|
|
91
94
|
await generateSitemap(routes, config, outDir);
|
|
92
|
-
|
|
93
|
-
logger.info('Step 10: Generating robots.txt...');
|
|
94
95
|
await generateRobots(config, outDir, routes);
|
|
96
|
+
logger.stepDone('Sitemap & robots');
|
|
95
97
|
|
|
96
|
-
// Cleanup build directory
|
|
97
98
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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/',
|
|
108
111
|
});
|
|
109
112
|
|
|
110
|
-
|
|
111
|
-
logger.info('β
Build complete');
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}, 100);
|
|
113
|
+
return { success: true };
|
|
114
114
|
|
|
115
115
|
} catch (error) {
|
|
116
|
-
logger.
|
|
117
|
-
if (error.stack) logger.error(error.stack);
|
|
116
|
+
logger.stepFail('Build', error.message);
|
|
118
117
|
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
119
123
|
|
|
120
|
-
|
|
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; }
|
|
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));
|
|
121
172
|
}
|
|
122
173
|
}
|
|
123
174
|
|
|
124
|
-
async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes) {
|
|
175
|
+
async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes, importhow, root, config) {
|
|
125
176
|
const originalCwd = process.cwd();
|
|
126
177
|
process.chdir(buildDir);
|
|
127
178
|
|
|
128
179
|
try {
|
|
129
|
-
const hasRouter = existsSync(routerPath);
|
|
130
180
|
const entrypoints = [buildEntry];
|
|
131
|
-
if (
|
|
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);
|
|
132
186
|
|
|
133
187
|
const result = await Bun.build({
|
|
134
188
|
entrypoints,
|
|
135
|
-
outdir:
|
|
136
|
-
target:
|
|
137
|
-
|
|
138
|
-
|
|
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,
|
|
139
198
|
sourcemap: 'external',
|
|
140
|
-
metafile:
|
|
199
|
+
metafile: true,
|
|
141
200
|
naming: {
|
|
142
|
-
entry: '[name]-[hash].js',
|
|
143
|
-
chunk: 'chunks/[name]-[hash].js',
|
|
144
|
-
asset: '[name]-[hash].[ext]',
|
|
201
|
+
entry: 'js/[name]-[hash].js',
|
|
202
|
+
chunk: 'js/chunks/[name]-[hash].js',
|
|
203
|
+
asset: 'assets/[name]-[hash].[ext]',
|
|
145
204
|
},
|
|
146
|
-
external: ['react', 'react-dom', 'react-dom/client'],
|
|
205
|
+
external: ['react', 'react-dom', 'react-dom/client', 'react/jsx-runtime'],
|
|
147
206
|
define: {
|
|
148
207
|
'process.env.NODE_ENV': '"production"',
|
|
149
208
|
...Object.fromEntries(
|
|
@@ -153,13 +212,13 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
|
|
|
153
212
|
});
|
|
154
213
|
|
|
155
214
|
if (!result.success) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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));
|
|
160
220
|
}
|
|
161
221
|
|
|
162
|
-
logger.success(`β
Bundled ${result.outputs.length} files`);
|
|
163
222
|
return result;
|
|
164
223
|
|
|
165
224
|
} finally {
|
|
@@ -167,15 +226,12 @@ async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDi
|
|
|
167
226
|
}
|
|
168
227
|
}
|
|
169
228
|
|
|
170
|
-
function
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
logger.info(`π€ robots.txt: dist/robots.txt`);
|
|
179
|
-
logger.info(`π Bundle report: dist/bundle-report.html`);
|
|
180
|
-
logger.bigLog('READY TO DEPLOY π', { color: 'green' });
|
|
229
|
+
export async function build(options = {}) {
|
|
230
|
+
try {
|
|
231
|
+
await buildProduction(options);
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(error);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
236
|
+
process.exit(0);
|
|
181
237
|
}
|