bertui 1.1.8 → 1.2.0
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/index.js +65 -45
- package/package.json +5 -2
- package/src/analyzer/index.js +370 -0
- package/src/build/compiler/route-discoverer.js +2 -0
- package/src/build/processors/css-builder.js +116 -80
- package/src/build.js +104 -93
- package/src/cli.js +83 -18
- package/src/client/compiler.js +168 -67
- package/src/css/processor.js +46 -1
- package/src/dev.js +47 -9
- package/src/hydration/index.js +151 -0
- package/src/layouts/index.js +165 -0
- package/src/loading/index.js +210 -0
- package/src/middleware/index.js +182 -0
- package/src/scaffolder/index.js +310 -0
- package/src/serve.js +195 -0
- package/src/server/dev-handler.js +78 -148
- package/src/server/dev-server-utils.js +16 -5
- package/src/server-islands/index.js +1 -1
- package/src/utils/cache.js +297 -0
|
@@ -1,102 +1,138 @@
|
|
|
1
|
-
// bertui/src/build/processors/css-builder.js -
|
|
1
|
+
// bertui/src/build/processors/css-builder.js - WITH SCSS + CACHING
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync, readdirSync, mkdirSync } from 'fs';
|
|
4
4
|
import logger from '../../logger/logger.js';
|
|
5
|
+
import { globalCache } from '../../utils/cache.js';
|
|
6
|
+
import { minifyCSS, processSCSS } from '../../css/processor.js';
|
|
5
7
|
|
|
6
8
|
export async function buildAllCSS(root, outDir) {
|
|
9
|
+
const startTime = process.hrtime.bigint();
|
|
10
|
+
|
|
7
11
|
const srcStylesDir = join(root, 'src', 'styles');
|
|
8
12
|
const stylesOutDir = join(outDir, 'styles');
|
|
9
13
|
|
|
10
14
|
mkdirSync(stylesOutDir, { recursive: true });
|
|
11
15
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
for (const cssFile of cssFiles) {
|
|
24
|
-
const srcPath = join(srcStylesDir, cssFile);
|
|
25
|
-
const file = Bun.file(srcPath);
|
|
26
|
-
const cssContent = await file.text();
|
|
27
|
-
combinedCSS += `/* ${cssFile} */\n${cssContent}\n\n`;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const combinedPath = join(stylesOutDir, 'bertui.min.css');
|
|
31
|
-
|
|
32
|
-
// ✅ SAFE: Try Lightning CSS, fallback to simple minification
|
|
33
|
-
try {
|
|
34
|
-
const minified = await minifyCSSSafe(combinedCSS);
|
|
35
|
-
await Bun.write(combinedPath, minified);
|
|
36
|
-
|
|
37
|
-
const originalSize = Buffer.byteLength(combinedCSS);
|
|
38
|
-
const minifiedSize = Buffer.byteLength(minified);
|
|
39
|
-
const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
|
|
40
|
-
|
|
41
|
-
logger.success(`CSS minified: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
logger.warn(`CSS minification failed: ${error.message}`);
|
|
44
|
-
logger.info('Falling back to unminified CSS...');
|
|
45
|
-
await Bun.write(combinedPath, combinedCSS);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
logger.success(`✅ Combined ${cssFiles.length} CSS files`);
|
|
49
|
-
} else {
|
|
50
|
-
// No styles directory, create empty CSS
|
|
16
|
+
// Check cache for entire CSS build
|
|
17
|
+
const cacheKey = `css-build:${root}:${Date.now()}`;
|
|
18
|
+
const cached = globalCache.get(cacheKey, { ttl: 1000 }); // 1 second cache
|
|
19
|
+
|
|
20
|
+
if (cached) {
|
|
21
|
+
logger.info(`⚡ Using cached CSS (${cached.files} files)`);
|
|
22
|
+
await Bun.write(join(stylesOutDir, 'bertui.min.css'), cached.content);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!existsSync(srcStylesDir)) {
|
|
51
27
|
await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No custom styles */');
|
|
52
28
|
logger.info('No styles directory found, created empty CSS');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Process SCSS files first
|
|
33
|
+
await processSCSSDirectory(srcStylesDir, root);
|
|
34
|
+
|
|
35
|
+
// Read all CSS files (including compiled SCSS)
|
|
36
|
+
const cssFiles = readdirSync(srcStylesDir).filter(f => f.endsWith('.css'));
|
|
37
|
+
|
|
38
|
+
if (cssFiles.length === 0) {
|
|
39
|
+
await Bun.write(join(stylesOutDir, 'bertui.min.css'), '/* No CSS */');
|
|
40
|
+
return;
|
|
53
41
|
}
|
|
42
|
+
|
|
43
|
+
logger.info(`Processing ${cssFiles.length} CSS file(s)...`);
|
|
44
|
+
|
|
45
|
+
let combinedCSS = '';
|
|
46
|
+
const fileContents = [];
|
|
47
|
+
|
|
48
|
+
for (const cssFile of cssFiles) {
|
|
49
|
+
const srcPath = join(srcStylesDir, cssFile);
|
|
50
|
+
|
|
51
|
+
// Use file cache
|
|
52
|
+
const fileBuffer = await globalCache.getFile(srcPath, { logSpeed: true });
|
|
53
|
+
if (fileBuffer) {
|
|
54
|
+
const content = fileBuffer.toString('utf-8');
|
|
55
|
+
fileContents.push({ filename: cssFile, content });
|
|
56
|
+
combinedCSS += `/* ${cssFile} */\n${content}\n\n`;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const combinedPath = join(stylesOutDir, 'bertui.min.css');
|
|
61
|
+
|
|
62
|
+
// Minify with caching
|
|
63
|
+
const minifyCacheKey = `minify:${Buffer.from(combinedCSS).length}:${combinedCSS.substring(0, 100)}`;
|
|
64
|
+
let minified = globalCache.get(minifyCacheKey);
|
|
65
|
+
|
|
66
|
+
if (!minified) {
|
|
67
|
+
minified = await minifyCSS(combinedCSS, {
|
|
68
|
+
filename: 'bertui.min.css',
|
|
69
|
+
sourceMap: false
|
|
70
|
+
});
|
|
71
|
+
globalCache.set(minifyCacheKey, minified, { ttl: 60000 }); // Cache for 60 seconds
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await Bun.write(combinedPath, minified);
|
|
75
|
+
|
|
76
|
+
const originalSize = Buffer.byteLength(combinedCSS);
|
|
77
|
+
const minifiedSize = Buffer.byteLength(minified);
|
|
78
|
+
const reduction = ((1 - minifiedSize / originalSize) * 100).toFixed(1);
|
|
79
|
+
|
|
80
|
+
const endTime = process.hrtime.bigint();
|
|
81
|
+
const duration = Number(endTime - startTime) / 1000; // Microseconds
|
|
82
|
+
|
|
83
|
+
logger.success(`CSS optimized: ${(originalSize/1024).toFixed(2)}KB → ${(minifiedSize/1024).toFixed(2)}KB (-${reduction}%)`);
|
|
84
|
+
logger.info(`⚡ Processing time: ${duration.toFixed(3)}µs`);
|
|
85
|
+
|
|
86
|
+
// Cache the final result
|
|
87
|
+
globalCache.set(cacheKey, {
|
|
88
|
+
files: cssFiles.length,
|
|
89
|
+
content: minified,
|
|
90
|
+
size: minifiedSize
|
|
91
|
+
}, { ttl: 5000 });
|
|
54
92
|
}
|
|
55
93
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
*/
|
|
59
|
-
async function minifyCSSSafe(css) {
|
|
60
|
-
// Try Lightning CSS first
|
|
94
|
+
// NEW: Process SCSS directory
|
|
95
|
+
async function processSCSSDirectory(stylesDir, root) {
|
|
61
96
|
try {
|
|
62
|
-
|
|
97
|
+
// Check if sass is installed
|
|
98
|
+
const sass = await import('sass').catch(() => null);
|
|
99
|
+
if (!sass) return;
|
|
63
100
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
code: Buffer.from(css),
|
|
67
|
-
minify: true,
|
|
68
|
-
sourceMap: false,
|
|
69
|
-
targets: {
|
|
70
|
-
chrome: 90 << 16,
|
|
71
|
-
firefox: 88 << 16,
|
|
72
|
-
safari: 14 << 16,
|
|
73
|
-
edge: 90 << 16
|
|
74
|
-
}
|
|
75
|
-
});
|
|
101
|
+
const files = readdirSync(stylesDir);
|
|
102
|
+
const scssFiles = files.filter(f => f.endsWith('.scss') || f.endsWith('.sass'));
|
|
76
103
|
|
|
77
|
-
|
|
104
|
+
if (scssFiles.length === 0) return;
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
logger.warn('Lightning CSS failed, using simple minification');
|
|
106
|
+
logger.info(`📝 Compiling ${scssFiles.length} SCSS files...`);
|
|
81
107
|
|
|
82
|
-
|
|
83
|
-
|
|
108
|
+
for (const file of scssFiles) {
|
|
109
|
+
const srcPath = join(stylesDir, file);
|
|
110
|
+
const cssPath = join(stylesDir, file.replace(/\.(scss|sass)$/, '.css'));
|
|
111
|
+
|
|
112
|
+
// Check cache
|
|
113
|
+
const fileBuffer = await globalCache.getFile(srcPath);
|
|
114
|
+
const cacheKey = `scss:${file}:${Buffer.from(fileBuffer).length}`;
|
|
115
|
+
const cached = globalCache.get(cacheKey);
|
|
116
|
+
|
|
117
|
+
if (cached && existsSync(cssPath)) {
|
|
118
|
+
logger.debug(`⚡ Cached SCSS: ${file} → ${file.replace(/\.(scss|sass)$/, '.css')}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = sass.compile(srcPath, {
|
|
123
|
+
style: 'expanded',
|
|
124
|
+
sourceMap: false,
|
|
125
|
+
loadPaths: [stylesDir, join(root, 'node_modules')]
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
await Bun.write(cssPath, result.css);
|
|
129
|
+
globalCache.set(cacheKey, true, { ttl: 30000 });
|
|
130
|
+
|
|
131
|
+
logger.debug(` ${file} → ${file.replace(/\.(scss|sass)$/, '.css')}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
logger.success(`✅ SCSS compilation complete`);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
logger.warn(`SCSS processing skipped: ${error.message}`);
|
|
84
137
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Simple CSS minification without dependencies
|
|
89
|
-
*/
|
|
90
|
-
function simpleMinifyCSS(css) {
|
|
91
|
-
return css
|
|
92
|
-
// Remove comments
|
|
93
|
-
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
94
|
-
// Remove extra whitespace
|
|
95
|
-
.replace(/\s+/g, ' ')
|
|
96
|
-
// Remove space around { } : ; ,
|
|
97
|
-
.replace(/\s*([{}:;,])\s*/g, '$1')
|
|
98
|
-
// Remove trailing semicolons before }
|
|
99
|
-
.replace(/;}/g, '}')
|
|
100
|
-
// Remove leading/trailing whitespace
|
|
101
|
-
.trim();
|
|
102
138
|
}
|
package/src/build.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// bertui/src/build.js -
|
|
1
|
+
// bertui/src/build.js - WITH LAYOUTS + LOADING + PARTIAL HYDRATION + ANALYZER
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync, mkdirSync, rmSync } from 'fs';
|
|
4
4
|
import logger from './logger/logger.js';
|
|
5
5
|
import { loadEnvVariables } from './utils/env.js';
|
|
6
|
+
import { globalCache } from './utils/cache.js';
|
|
6
7
|
|
|
7
8
|
import { compileForBuild } from './build/compiler/index.js';
|
|
8
9
|
import { buildAllCSS } from './build/processors/css-builder.js';
|
|
@@ -10,159 +11,169 @@ import { copyAllStaticAssets } from './build/processors/asset-processor.js';
|
|
|
10
11
|
import { generateProductionHTML } from './build/generators/html-generator.js';
|
|
11
12
|
import { generateSitemap } from './build/generators/sitemap-generator.js';
|
|
12
13
|
import { generateRobots } from './build/generators/robots-generator.js';
|
|
14
|
+
import { compileLayouts } from './layouts/index.js';
|
|
15
|
+
import { compileLoadingComponents } from './loading/index.js';
|
|
16
|
+
import { analyzeRoutes, logHydrationReport } from './hydration/index.js';
|
|
17
|
+
import { analyzeBuild } from './analyzer/index.js';
|
|
13
18
|
|
|
14
19
|
export async function buildProduction(options = {}) {
|
|
15
20
|
const root = options.root || process.cwd();
|
|
16
21
|
const buildDir = join(root, '.bertuibuild');
|
|
17
22
|
const outDir = join(root, 'dist');
|
|
18
|
-
|
|
19
|
-
// Force production environment
|
|
23
|
+
|
|
20
24
|
process.env.NODE_ENV = 'production';
|
|
21
|
-
|
|
25
|
+
|
|
22
26
|
logger.bigLog('BUILDING WITH SERVER ISLANDS 🏝️', { color: 'green' });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (existsSync(
|
|
26
|
-
if (existsSync(outDir)) rmSync(outDir, { recursive: true });
|
|
27
|
+
|
|
28
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
29
|
+
if (existsSync(outDir)) rmSync(outDir, { recursive: true, force: true });
|
|
27
30
|
mkdirSync(buildDir, { recursive: true });
|
|
28
31
|
mkdirSync(outDir, { recursive: true });
|
|
29
|
-
|
|
30
|
-
const startTime =
|
|
31
|
-
|
|
32
|
+
|
|
33
|
+
const startTime = process.hrtime.bigint();
|
|
34
|
+
|
|
32
35
|
try {
|
|
33
36
|
logger.info('Step 0: Loading environment variables...');
|
|
34
37
|
const envVars = loadEnvVariables(root);
|
|
35
|
-
|
|
38
|
+
|
|
36
39
|
const { loadConfig } = await import('./config/loadConfig.js');
|
|
37
40
|
const config = await loadConfig(root);
|
|
38
|
-
|
|
39
|
-
logger.info('Step 1: Compiling
|
|
41
|
+
|
|
42
|
+
logger.info('Step 1: Compiling project...');
|
|
40
43
|
const { routes, serverIslands, clientRoutes } = await compileForBuild(root, buildDir, envVars);
|
|
41
|
-
|
|
44
|
+
|
|
42
45
|
if (serverIslands.length > 0) {
|
|
43
46
|
logger.bigLog('SERVER ISLANDS DETECTED 🏝️', { color: 'cyan' });
|
|
44
47
|
logger.table(serverIslands.map(r => ({
|
|
45
48
|
route: r.route,
|
|
46
49
|
file: r.file,
|
|
47
|
-
mode: '🏝️ Server Island (SSG)'
|
|
50
|
+
mode: '🏝️ Server Island (SSG)',
|
|
48
51
|
})));
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
|
|
54
|
+
// ✅ NEW: Compile layouts
|
|
55
|
+
logger.info('Step 2: Compiling layouts...');
|
|
56
|
+
const layouts = await compileLayouts(root, buildDir);
|
|
57
|
+
const layoutCount = Object.keys(layouts).length;
|
|
58
|
+
if (layoutCount > 0) {
|
|
59
|
+
logger.success(`📐 ${layoutCount} layout(s) compiled`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ✅ NEW: Compile per-route loading states
|
|
63
|
+
logger.info('Step 3: Compiling loading states...');
|
|
64
|
+
const loadingComponents = await compileLoadingComponents(root, buildDir);
|
|
65
|
+
|
|
66
|
+
// ✅ NEW: Partial hydration analysis
|
|
67
|
+
logger.info('Step 4: Analyzing routes for partial hydration...');
|
|
68
|
+
const analyzedRoutes = await analyzeRoutes(routes);
|
|
69
|
+
logHydrationReport(analyzedRoutes);
|
|
70
|
+
|
|
71
|
+
logger.info('Step 5: Processing CSS...');
|
|
52
72
|
await buildAllCSS(root, outDir);
|
|
53
|
-
|
|
54
|
-
logger.info('Step
|
|
73
|
+
|
|
74
|
+
logger.info('Step 6: Copying static assets...');
|
|
55
75
|
await copyAllStaticAssets(root, outDir);
|
|
56
|
-
|
|
57
|
-
logger.info('Step
|
|
76
|
+
|
|
77
|
+
logger.info('Step 7: Bundling JavaScript...');
|
|
58
78
|
const buildEntry = join(buildDir, 'main.js');
|
|
59
|
-
|
|
79
|
+
const routerPath = join(buildDir, 'router.js');
|
|
80
|
+
|
|
60
81
|
if (!existsSync(buildEntry)) {
|
|
61
|
-
|
|
62
|
-
throw new Error('Build entry point missing');
|
|
82
|
+
throw new Error('Build entry point missing (main.js not found in build dir)');
|
|
63
83
|
}
|
|
64
|
-
|
|
65
|
-
const result = await bundleJavaScript(buildEntry, outDir, envVars, buildDir);
|
|
66
|
-
|
|
67
|
-
logger.info('Step
|
|
84
|
+
|
|
85
|
+
const result = await bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes);
|
|
86
|
+
|
|
87
|
+
logger.info('Step 8: Generating HTML...');
|
|
68
88
|
await generateProductionHTML(root, outDir, result, routes, serverIslands, config);
|
|
69
|
-
|
|
70
|
-
logger.info('Step
|
|
89
|
+
|
|
90
|
+
logger.info('Step 9: Generating sitemap.xml...');
|
|
71
91
|
await generateSitemap(routes, config, outDir);
|
|
72
|
-
|
|
73
|
-
logger.info('Step
|
|
92
|
+
|
|
93
|
+
logger.info('Step 10: Generating robots.txt...');
|
|
74
94
|
await generateRobots(config, outDir, routes);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
|
|
96
|
+
// Cleanup build directory
|
|
97
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
98
|
+
|
|
99
|
+
const endTime = process.hrtime.bigint();
|
|
100
|
+
const durationMs = Number(endTime - startTime) / 1_000_000;
|
|
101
|
+
|
|
102
|
+
showBuildSummary(routes, serverIslands, clientRoutes, analyzedRoutes, durationMs);
|
|
103
|
+
|
|
104
|
+
// ✅ NEW: Auto-generate bundle report
|
|
105
|
+
logger.info('Generating bundle report...');
|
|
106
|
+
await analyzeBuild(outDir, {
|
|
107
|
+
outputFile: join(outDir, 'bundle-report.html'),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
setTimeout(() => {
|
|
111
|
+
logger.info('✅ Build complete');
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}, 100);
|
|
114
|
+
|
|
81
115
|
} catch (error) {
|
|
82
116
|
logger.error(`Build failed: ${error.message}`);
|
|
83
117
|
if (error.stack) logger.error(error.stack);
|
|
84
|
-
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true });
|
|
85
|
-
|
|
118
|
+
if (existsSync(buildDir)) rmSync(buildDir, { recursive: true, force: true });
|
|
119
|
+
|
|
120
|
+
setTimeout(() => process.exit(1), 100);
|
|
86
121
|
}
|
|
87
122
|
}
|
|
88
123
|
|
|
89
|
-
async function bundleJavaScript(buildEntry, outDir, envVars, buildDir) {
|
|
124
|
+
async function bundleJavaScript(buildEntry, routerPath, outDir, envVars, buildDir, analyzedRoutes) {
|
|
125
|
+
const originalCwd = process.cwd();
|
|
126
|
+
process.chdir(buildDir);
|
|
127
|
+
|
|
90
128
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
129
|
+
const hasRouter = existsSync(routerPath);
|
|
130
|
+
const entrypoints = [buildEntry];
|
|
131
|
+
if (hasRouter) entrypoints.push(routerPath);
|
|
132
|
+
|
|
96
133
|
const result = await Bun.build({
|
|
97
|
-
entrypoints
|
|
134
|
+
entrypoints,
|
|
98
135
|
outdir: join(outDir, 'assets'),
|
|
99
136
|
target: 'browser',
|
|
100
137
|
minify: true,
|
|
101
|
-
splitting: true,
|
|
138
|
+
splitting: true, // Code splitting per route
|
|
102
139
|
sourcemap: 'external',
|
|
140
|
+
metafile: true, // ✅ Enable for analyzer
|
|
103
141
|
naming: {
|
|
104
142
|
entry: '[name]-[hash].js',
|
|
105
143
|
chunk: 'chunks/[name]-[hash].js',
|
|
106
|
-
asset: '[name]-[hash].[ext]'
|
|
144
|
+
asset: '[name]-[hash].[ext]',
|
|
107
145
|
},
|
|
108
146
|
external: ['react', 'react-dom', 'react-dom/client'],
|
|
109
147
|
define: {
|
|
110
148
|
'process.env.NODE_ENV': '"production"',
|
|
111
149
|
...Object.fromEntries(
|
|
112
|
-
Object.entries(envVars).map(([
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
])
|
|
116
|
-
)
|
|
117
|
-
}
|
|
150
|
+
Object.entries(envVars).map(([k, v]) => [`process.env.${k}`, JSON.stringify(v)])
|
|
151
|
+
),
|
|
152
|
+
},
|
|
118
153
|
});
|
|
119
|
-
|
|
120
|
-
process.chdir(originalCwd);
|
|
121
|
-
|
|
154
|
+
|
|
122
155
|
if (!result.success) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (result.logs && result.logs.length > 0) {
|
|
126
|
-
logger.error('\n📋 Build errors:');
|
|
127
|
-
result.logs.forEach((log, i) => {
|
|
128
|
-
logger.error(`\n${i + 1}. ${log.message}`);
|
|
129
|
-
if (log.position) {
|
|
130
|
-
logger.error(` File: ${log.position.file || 'unknown'}`);
|
|
131
|
-
logger.error(` Line: ${log.position.line || 'unknown'}`);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
throw new Error('JavaScript bundling failed');
|
|
156
|
+
const errors = result.logs?.map(l => l.message).join('\n') || 'Unknown error';
|
|
157
|
+
throw new Error(`JavaScript bundling failed:\n${errors}`);
|
|
137
158
|
}
|
|
138
|
-
|
|
139
|
-
logger.success(
|
|
140
|
-
logger.info(` Entry points: ${result.outputs.filter(o => o.kind === 'entry-point').length}`);
|
|
141
|
-
logger.info(` Chunks: ${result.outputs.filter(o => o.kind === 'chunk').length}`);
|
|
142
|
-
|
|
143
|
-
const totalSize = result.outputs.reduce((sum, o) => sum + (o.size || 0), 0);
|
|
144
|
-
logger.info(` Total size: ${(totalSize / 1024).toFixed(2)} KB`);
|
|
145
|
-
|
|
159
|
+
|
|
160
|
+
logger.success(`✅ Bundled ${result.outputs.length} files`);
|
|
146
161
|
return result;
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
throw error;
|
|
162
|
+
|
|
163
|
+
} finally {
|
|
164
|
+
process.chdir(originalCwd);
|
|
151
165
|
}
|
|
152
166
|
}
|
|
153
167
|
|
|
154
|
-
function showBuildSummary(routes, serverIslands, clientRoutes,
|
|
155
|
-
logger.success(`✨ Build complete in ${
|
|
168
|
+
function showBuildSummary(routes, serverIslands, clientRoutes, analyzedRoutes, durationMs) {
|
|
169
|
+
logger.success(`✨ Build complete in ${durationMs.toFixed(1)}ms`);
|
|
156
170
|
logger.bigLog('BUILD SUMMARY', { color: 'green' });
|
|
157
171
|
logger.info(`📄 Total routes: ${routes.length}`);
|
|
158
172
|
logger.info(`🏝️ Server Islands (SSG): ${serverIslands.length}`);
|
|
159
|
-
logger.info(`⚡
|
|
173
|
+
logger.info(`⚡ Interactive (hydrated): ${analyzedRoutes.interactive.length}`);
|
|
174
|
+
logger.info(`🧊 Static (no JS): ${analyzedRoutes.static.length}`);
|
|
160
175
|
logger.info(`🗺️ Sitemap: dist/sitemap.xml`);
|
|
161
176
|
logger.info(`🤖 robots.txt: dist/robots.txt`);
|
|
162
|
-
|
|
163
|
-
if (serverIslands.length > 0) {
|
|
164
|
-
logger.success('✅ Server Islands enabled - INSTANT content delivery!');
|
|
165
|
-
}
|
|
166
|
-
|
|
177
|
+
logger.info(`📊 Bundle report: dist/bundle-report.html`);
|
|
167
178
|
logger.bigLog('READY TO DEPLOY 🚀', { color: 'green' });
|
|
168
179
|
}
|
package/src/cli.js
CHANGED
|
@@ -1,37 +1,87 @@
|
|
|
1
|
-
// src/cli.js
|
|
1
|
+
// bertui/src/cli.js - WITH ALL COMMANDS
|
|
2
2
|
import { startDev } from './dev.js';
|
|
3
3
|
import { buildProduction } from './build.js';
|
|
4
|
+
import { startPreviewServer } from './serve.js';
|
|
5
|
+
import { scaffold, parseCreateArgs } from './scaffolder/index.js';
|
|
6
|
+
import { analyzeBuild } from './analyzer/index.js';
|
|
4
7
|
import logger from './logger/logger.js';
|
|
8
|
+
import { join } from 'path';
|
|
5
9
|
|
|
6
10
|
export function program() {
|
|
7
11
|
const args = process.argv.slice(2);
|
|
8
12
|
const command = args[0] || 'dev';
|
|
9
13
|
|
|
10
14
|
switch (command) {
|
|
11
|
-
case 'dev':
|
|
15
|
+
case 'dev': {
|
|
12
16
|
const devPort = getArg('--port', '-p') || 3000;
|
|
13
|
-
startDev({
|
|
14
|
-
port: parseInt(devPort),
|
|
15
|
-
root: process.cwd()
|
|
17
|
+
startDev({
|
|
18
|
+
port: parseInt(devPort),
|
|
19
|
+
root: process.cwd(),
|
|
16
20
|
});
|
|
17
21
|
break;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
case 'build': {
|
|
25
|
+
buildProduction({ root: process.cwd() });
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case 'serve':
|
|
30
|
+
case 'preview': {
|
|
31
|
+
const previewPort = getArg('--port', '-p') || 5000;
|
|
32
|
+
startPreviewServer({
|
|
33
|
+
port: parseInt(previewPort),
|
|
34
|
+
root: process.cwd(),
|
|
35
|
+
dir: 'dist',
|
|
22
36
|
});
|
|
23
37
|
break;
|
|
24
|
-
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ✅ NEW: Component/page/layout scaffolder
|
|
41
|
+
case 'create': {
|
|
42
|
+
const createArgs = args.slice(1);
|
|
43
|
+
const parsed = parseCreateArgs(createArgs);
|
|
44
|
+
if (parsed) {
|
|
45
|
+
scaffold(parsed.type, parsed.name, { root: process.cwd() })
|
|
46
|
+
.then(result => {
|
|
47
|
+
if (!result) process.exit(1);
|
|
48
|
+
})
|
|
49
|
+
.catch(err => {
|
|
50
|
+
logger.error(`Create failed: ${err.message}`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ✅ NEW: Bundle analyzer
|
|
58
|
+
case 'analyze': {
|
|
59
|
+
const outDir = join(process.cwd(), 'dist');
|
|
60
|
+
const open = args.includes('--open');
|
|
61
|
+
analyzeBuild(outDir, { open })
|
|
62
|
+
.then(result => {
|
|
63
|
+
if (!result) {
|
|
64
|
+
logger.error('No build found. Run: bertui build');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
.catch(err => {
|
|
69
|
+
logger.error(`Analyze failed: ${err.message}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
});
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
|
|
25
75
|
case '--version':
|
|
26
76
|
case '-v':
|
|
27
|
-
console.log('bertui
|
|
77
|
+
console.log('bertui v1.2.0');
|
|
28
78
|
break;
|
|
29
|
-
|
|
79
|
+
|
|
30
80
|
case '--help':
|
|
31
81
|
case '-h':
|
|
32
82
|
showHelp();
|
|
33
83
|
break;
|
|
34
|
-
|
|
84
|
+
|
|
35
85
|
default:
|
|
36
86
|
logger.error(`Unknown command: ${command}`);
|
|
37
87
|
showHelp();
|
|
@@ -50,17 +100,32 @@ function showHelp() {
|
|
|
50
100
|
logger.bigLog('BERTUI CLI', { color: 'blue' });
|
|
51
101
|
console.log(`
|
|
52
102
|
Commands:
|
|
53
|
-
bertui dev Start development server
|
|
54
|
-
bertui build
|
|
55
|
-
bertui --
|
|
56
|
-
bertui --
|
|
103
|
+
bertui dev [--port] Start development server (default: 3000)
|
|
104
|
+
bertui build Build for production
|
|
105
|
+
bertui serve [--port] Preview production build (default: 5000)
|
|
106
|
+
bertui analyze [--open] Analyze bundle size (opens report in browser)
|
|
107
|
+
|
|
108
|
+
bertui create component <Name> Scaffold a React component
|
|
109
|
+
bertui create page <name> Scaffold a page (adds to file-based routing)
|
|
110
|
+
bertui create layout <name> Scaffold a layout (default wraps all pages)
|
|
111
|
+
bertui create loading <route> Scaffold a per-route loading state
|
|
112
|
+
bertui create middleware Scaffold src/middleware.ts
|
|
57
113
|
|
|
58
114
|
Options:
|
|
59
|
-
--port, -p <number>
|
|
115
|
+
--port, -p <number> Port for server
|
|
116
|
+
--open Open browser after command
|
|
60
117
|
|
|
61
118
|
Examples:
|
|
62
119
|
bertui dev
|
|
63
120
|
bertui dev --port 8080
|
|
64
121
|
bertui build
|
|
122
|
+
bertui analyze --open
|
|
123
|
+
bertui create component Button
|
|
124
|
+
bertui create page About
|
|
125
|
+
bertui create page blog/[slug]
|
|
126
|
+
bertui create layout default
|
|
127
|
+
bertui create layout blog
|
|
128
|
+
bertui create loading blog
|
|
129
|
+
bertui create middleware
|
|
65
130
|
`);
|
|
66
131
|
}
|