blumenjs 0.2.1 → 0.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/dist/cli/blumen.js +875 -62
- package/dist/cli/commands/audit.js +204 -0
- package/dist/cli/commands/bench.js +227 -0
- package/dist/cli/commands/build.js +47 -6
- package/dist/cli/commands/export.js +241 -0
- package/dist/cli/commands/migrate.js +267 -0
- package/dist/cli/commands/test.js +118 -0
- package/dist/templates/app/client/entry.tsx +5 -1
- package/dist/templates/app/shared/DefaultDocument.tsx +19 -5
- package/dist/templates/app/shared/RouterContext.tsx +4 -1
- package/dist/templates/go-server/actions.go +147 -0
- package/dist/templates/go-server/main.go +107 -4
- package/dist/templates/go-server/middleware.go +1 -1
- package/dist/templates/go-server/redirects.go +203 -0
- package/dist/templates/go-server/ssg.go +230 -0
- package/dist/templates/node-ssr/server.ts +222 -2
- package/dist/templates/scripts/generate-routes.ts +141 -9
- package/package.json +16 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
// cli/commands/audit.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
// cli/utils.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
var c = {
|
|
9
|
+
reset: "\x1B[0m",
|
|
10
|
+
bold: "\x1B[1m",
|
|
11
|
+
dim: "\x1B[2m",
|
|
12
|
+
red: "\x1B[31m",
|
|
13
|
+
green: "\x1B[32m",
|
|
14
|
+
yellow: "\x1B[33m",
|
|
15
|
+
blue: "\x1B[34m",
|
|
16
|
+
magenta: "\x1B[35m",
|
|
17
|
+
cyan: "\x1B[36m",
|
|
18
|
+
white: "\x1B[37m",
|
|
19
|
+
gray: "\x1B[90m"
|
|
20
|
+
};
|
|
21
|
+
var log = {
|
|
22
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
23
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
24
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
25
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
26
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
27
|
+
blank: () => console.log("")
|
|
28
|
+
};
|
|
29
|
+
function getVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
32
|
+
let dir = path.dirname(thisFile);
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
const pkgFile = path.join(dir, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgFile)) {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
37
|
+
if (pkg.name === "blumenjs" || pkg.name === "go-react-ssr") {
|
|
38
|
+
return pkg.version;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
dir = path.dirname(dir);
|
|
42
|
+
}
|
|
43
|
+
return "0.0.0";
|
|
44
|
+
} catch {
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function banner() {
|
|
49
|
+
const version = getVersion();
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(
|
|
52
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v${version}${c.reset}`
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
56
|
+
);
|
|
57
|
+
console.log("");
|
|
58
|
+
}
|
|
59
|
+
function divider() {
|
|
60
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// cli/commands/audit.ts
|
|
64
|
+
function severityColor(severity) {
|
|
65
|
+
switch (severity) {
|
|
66
|
+
case "critical":
|
|
67
|
+
return c.red;
|
|
68
|
+
case "high":
|
|
69
|
+
return c.red;
|
|
70
|
+
case "moderate":
|
|
71
|
+
return c.yellow;
|
|
72
|
+
case "low":
|
|
73
|
+
return c.dim;
|
|
74
|
+
default:
|
|
75
|
+
return c.dim;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function severityIcon(severity) {
|
|
79
|
+
switch (severity) {
|
|
80
|
+
case "critical":
|
|
81
|
+
return "\u{1F534}";
|
|
82
|
+
case "high":
|
|
83
|
+
return "\u{1F7E0}";
|
|
84
|
+
case "moderate":
|
|
85
|
+
return "\u{1F7E1}";
|
|
86
|
+
case "low":
|
|
87
|
+
return "\u{1F535}";
|
|
88
|
+
default:
|
|
89
|
+
return "\u26AA";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function audit(args = []) {
|
|
93
|
+
banner();
|
|
94
|
+
const fix = args.includes("--fix");
|
|
95
|
+
const ci = args.includes("--ci");
|
|
96
|
+
const jsonOutput = args.includes("--json");
|
|
97
|
+
if (fix) {
|
|
98
|
+
log.info("Attempting to fix vulnerabilities...");
|
|
99
|
+
log.blank();
|
|
100
|
+
try {
|
|
101
|
+
execSync("npm audit fix", {
|
|
102
|
+
stdio: "inherit",
|
|
103
|
+
cwd: process.cwd()
|
|
104
|
+
});
|
|
105
|
+
log.blank();
|
|
106
|
+
log.success("Audit fix completed.");
|
|
107
|
+
} catch {
|
|
108
|
+
log.warn("Some vulnerabilities could not be fixed automatically.");
|
|
109
|
+
log.info(`Run ${c.bold}npm audit fix --force${c.reset} to force-fix (may include breaking changes).`);
|
|
110
|
+
}
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (jsonOutput) {
|
|
114
|
+
try {
|
|
115
|
+
execSync("npm audit --json", {
|
|
116
|
+
stdio: "inherit",
|
|
117
|
+
cwd: process.cwd()
|
|
118
|
+
});
|
|
119
|
+
} catch {
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
log.info("Scanning dependencies for known vulnerabilities...");
|
|
125
|
+
log.blank();
|
|
126
|
+
let auditOutput;
|
|
127
|
+
let hasVulnerabilities = false;
|
|
128
|
+
try {
|
|
129
|
+
auditOutput = execSync("npm audit --json 2>/dev/null", {
|
|
130
|
+
cwd: process.cwd(),
|
|
131
|
+
encoding: "utf-8"
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
auditOutput = err.stdout || "{}";
|
|
135
|
+
hasVulnerabilities = true;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const report = JSON.parse(auditOutput);
|
|
139
|
+
const meta = report.metadata || {};
|
|
140
|
+
const vulns = meta.vulnerabilities || {
|
|
141
|
+
info: 0,
|
|
142
|
+
low: 0,
|
|
143
|
+
moderate: 0,
|
|
144
|
+
high: 0,
|
|
145
|
+
critical: 0,
|
|
146
|
+
total: 0
|
|
147
|
+
};
|
|
148
|
+
const deps = meta.dependencies || {};
|
|
149
|
+
const total = vulns.total || vulns.critical + vulns.high + vulns.moderate + vulns.low + (vulns.info || 0);
|
|
150
|
+
divider();
|
|
151
|
+
log.blank();
|
|
152
|
+
if (total === 0) {
|
|
153
|
+
log.success(`${c.bold}No vulnerabilities found!${c.reset} \u2728`);
|
|
154
|
+
log.blank();
|
|
155
|
+
if (deps.total) {
|
|
156
|
+
log.info(`Scanned ${c.bold}${deps.total}${c.reset} dependencies.`);
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
log.warn(`${c.bold}${total} vulnerabilit${total === 1 ? "y" : "ies"} found${c.reset}`);
|
|
160
|
+
log.blank();
|
|
161
|
+
const levels = [
|
|
162
|
+
{ name: "critical", count: vulns.critical },
|
|
163
|
+
{ name: "high", count: vulns.high },
|
|
164
|
+
{ name: "moderate", count: vulns.moderate },
|
|
165
|
+
{ name: "low", count: vulns.low }
|
|
166
|
+
];
|
|
167
|
+
for (const level of levels) {
|
|
168
|
+
if (level.count > 0) {
|
|
169
|
+
const color = severityColor(level.name);
|
|
170
|
+
const icon = severityIcon(level.name);
|
|
171
|
+
console.log(` ${icon} ${color}${level.count} ${level.name}${c.reset}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
log.blank();
|
|
175
|
+
if (deps.total) {
|
|
176
|
+
log.info(`Scanned ${c.bold}${deps.total}${c.reset} dependencies.`);
|
|
177
|
+
}
|
|
178
|
+
log.info(`Run ${c.bold}blumen audit --fix${c.reset} to attempt automatic fixes.`);
|
|
179
|
+
}
|
|
180
|
+
log.blank();
|
|
181
|
+
divider();
|
|
182
|
+
log.blank();
|
|
183
|
+
if (ci && (vulns.critical > 0 || vulns.high > 0)) {
|
|
184
|
+
log.error("CI check failed: high or critical vulnerabilities detected.");
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
log.info("Detailed report:");
|
|
189
|
+
log.blank();
|
|
190
|
+
try {
|
|
191
|
+
execSync("npm audit", {
|
|
192
|
+
stdio: "inherit",
|
|
193
|
+
cwd: process.cwd()
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
if (ci) {
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export {
|
|
203
|
+
audit
|
|
204
|
+
};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
// cli/commands/bench.ts
|
|
2
|
+
import * as http from "http";
|
|
3
|
+
|
|
4
|
+
// cli/utils.ts
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
var c = {
|
|
9
|
+
reset: "\x1B[0m",
|
|
10
|
+
bold: "\x1B[1m",
|
|
11
|
+
dim: "\x1B[2m",
|
|
12
|
+
red: "\x1B[31m",
|
|
13
|
+
green: "\x1B[32m",
|
|
14
|
+
yellow: "\x1B[33m",
|
|
15
|
+
blue: "\x1B[34m",
|
|
16
|
+
magenta: "\x1B[35m",
|
|
17
|
+
cyan: "\x1B[36m",
|
|
18
|
+
white: "\x1B[37m",
|
|
19
|
+
gray: "\x1B[90m"
|
|
20
|
+
};
|
|
21
|
+
var log = {
|
|
22
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
23
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
24
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
25
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
26
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
27
|
+
blank: () => console.log("")
|
|
28
|
+
};
|
|
29
|
+
function getVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
32
|
+
let dir = path.dirname(thisFile);
|
|
33
|
+
for (let i = 0; i < 5; i++) {
|
|
34
|
+
const pkgFile = path.join(dir, "package.json");
|
|
35
|
+
if (fs.existsSync(pkgFile)) {
|
|
36
|
+
const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
|
|
37
|
+
if (pkg.name === "blumenjs" || pkg.name === "go-react-ssr") {
|
|
38
|
+
return pkg.version;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
dir = path.dirname(dir);
|
|
42
|
+
}
|
|
43
|
+
return "0.0.0";
|
|
44
|
+
} catch {
|
|
45
|
+
return "0.0.0";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function banner() {
|
|
49
|
+
const version = getVersion();
|
|
50
|
+
console.log("");
|
|
51
|
+
console.log(
|
|
52
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v${version}${c.reset}`
|
|
53
|
+
);
|
|
54
|
+
console.log(
|
|
55
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
56
|
+
);
|
|
57
|
+
console.log("");
|
|
58
|
+
}
|
|
59
|
+
function divider() {
|
|
60
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// cli/commands/bench.ts
|
|
64
|
+
function percentile(sorted, p) {
|
|
65
|
+
const idx = Math.ceil(p / 100 * sorted.length) - 1;
|
|
66
|
+
return sorted[Math.max(0, idx)];
|
|
67
|
+
}
|
|
68
|
+
function makeRequest(url) {
|
|
69
|
+
return new Promise((resolve, reject) => {
|
|
70
|
+
const start = performance.now();
|
|
71
|
+
const req = http.get(url, (res) => {
|
|
72
|
+
let body = "";
|
|
73
|
+
res.on("data", (chunk) => {
|
|
74
|
+
body += chunk;
|
|
75
|
+
});
|
|
76
|
+
res.on("end", () => {
|
|
77
|
+
resolve({
|
|
78
|
+
latency: performance.now() - start,
|
|
79
|
+
status: res.statusCode || 0,
|
|
80
|
+
cacheHeader: res.headers["x-blumen-cache"] || ""
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
req.on("error", reject);
|
|
85
|
+
req.setTimeout(1e4, () => {
|
|
86
|
+
req.destroy();
|
|
87
|
+
reject(new Error("Timeout"));
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async function runBenchmark(url, totalRequests, concurrency) {
|
|
92
|
+
const latencies = [];
|
|
93
|
+
let successful = 0;
|
|
94
|
+
let failed = 0;
|
|
95
|
+
let cacheHits = 0;
|
|
96
|
+
let cacheMisses = 0;
|
|
97
|
+
let streamed = 0;
|
|
98
|
+
let completed = 0;
|
|
99
|
+
const start = performance.now();
|
|
100
|
+
for (let i = 0; i < totalRequests; i += concurrency) {
|
|
101
|
+
const batchSize = Math.min(concurrency, totalRequests - i);
|
|
102
|
+
const batch = Array.from(
|
|
103
|
+
{ length: batchSize },
|
|
104
|
+
() => makeRequest(url).then((result) => {
|
|
105
|
+
latencies.push(result.latency);
|
|
106
|
+
if (result.status >= 200 && result.status < 400) {
|
|
107
|
+
successful++;
|
|
108
|
+
} else {
|
|
109
|
+
failed++;
|
|
110
|
+
}
|
|
111
|
+
if (result.cacheHeader === "HIT" || result.cacheHeader === "STALE")
|
|
112
|
+
cacheHits++;
|
|
113
|
+
else if (result.cacheHeader === "MISS")
|
|
114
|
+
cacheMisses++;
|
|
115
|
+
else if (result.cacheHeader === "STREAM")
|
|
116
|
+
streamed++;
|
|
117
|
+
completed++;
|
|
118
|
+
}).catch(() => {
|
|
119
|
+
failed++;
|
|
120
|
+
completed++;
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
await Promise.all(batch);
|
|
124
|
+
const pct = Math.round(completed / totalRequests * 100);
|
|
125
|
+
process.stdout.write(`\r \u25CF Progress: ${pct}% (${completed}/${totalRequests})`);
|
|
126
|
+
}
|
|
127
|
+
process.stdout.write("\r" + " ".repeat(60) + "\r");
|
|
128
|
+
const totalTimeMs = performance.now() - start;
|
|
129
|
+
const sorted = latencies.sort((a, b) => a - b);
|
|
130
|
+
return {
|
|
131
|
+
url,
|
|
132
|
+
totalRequests,
|
|
133
|
+
successfulRequests: successful,
|
|
134
|
+
failedRequests: failed,
|
|
135
|
+
totalTimeMs,
|
|
136
|
+
requestsPerSec: successful / totalTimeMs * 1e3,
|
|
137
|
+
latencies: sorted,
|
|
138
|
+
p50: sorted.length ? percentile(sorted, 50) : 0,
|
|
139
|
+
p95: sorted.length ? percentile(sorted, 95) : 0,
|
|
140
|
+
p99: sorted.length ? percentile(sorted, 99) : 0,
|
|
141
|
+
min: sorted.length ? sorted[0] : 0,
|
|
142
|
+
max: sorted.length ? sorted[sorted.length - 1] : 0,
|
|
143
|
+
avg: sorted.length ? sorted.reduce((a, b) => a + b, 0) / sorted.length : 0,
|
|
144
|
+
cacheHits,
|
|
145
|
+
cacheMisses,
|
|
146
|
+
streamed
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function formatMs(ms) {
|
|
150
|
+
if (ms < 1)
|
|
151
|
+
return `${(ms * 1e3).toFixed(0)}\xB5s`;
|
|
152
|
+
if (ms < 1e3)
|
|
153
|
+
return `${ms.toFixed(1)}ms`;
|
|
154
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
155
|
+
}
|
|
156
|
+
function printResult(result) {
|
|
157
|
+
console.log(` ${c.bold}URL${c.reset} ${result.url}`);
|
|
158
|
+
console.log(` ${c.bold}Requests${c.reset} ${result.successfulRequests}/${result.totalRequests} successful${result.failedRequests > 0 ? ` (${result.failedRequests} failed)` : ""}`);
|
|
159
|
+
console.log(` ${c.bold}Duration${c.reset} ${formatMs(result.totalTimeMs)}`);
|
|
160
|
+
console.log(` ${c.bold}Throughput${c.reset} ${c.green}${result.requestsPerSec.toFixed(1)} req/sec${c.reset}`);
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(` ${c.bold}Latency${c.reset}`);
|
|
163
|
+
console.log(` min ${formatMs(result.min)}`);
|
|
164
|
+
console.log(` avg ${formatMs(result.avg)}`);
|
|
165
|
+
console.log(` p50 ${formatMs(result.p50)}`);
|
|
166
|
+
console.log(` p95 ${c.yellow}${formatMs(result.p95)}${c.reset}`);
|
|
167
|
+
console.log(` p99 ${c.yellow}${formatMs(result.p99)}${c.reset}`);
|
|
168
|
+
console.log(` max ${formatMs(result.max)}`);
|
|
169
|
+
if (result.cacheHits > 0 || result.cacheMisses > 0 || result.streamed > 0) {
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log(` ${c.bold}Cache${c.reset}`);
|
|
172
|
+
if (result.cacheHits > 0)
|
|
173
|
+
console.log(` HIT ${result.cacheHits} (${(result.cacheHits / result.totalRequests * 100).toFixed(0)}%)`);
|
|
174
|
+
if (result.cacheMisses > 0)
|
|
175
|
+
console.log(` MISS ${result.cacheMisses}`);
|
|
176
|
+
if (result.streamed > 0)
|
|
177
|
+
console.log(` STREAM ${result.streamed}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function bench(args = []) {
|
|
181
|
+
banner();
|
|
182
|
+
let baseUrl = "http://localhost:3000";
|
|
183
|
+
let totalRequests = 200;
|
|
184
|
+
let concurrency = 10;
|
|
185
|
+
for (let i = 0; i < args.length; i++) {
|
|
186
|
+
if (args[i] === "--url" && args[i + 1]) {
|
|
187
|
+
baseUrl = args[++i];
|
|
188
|
+
} else if (args[i] === "--requests" && args[i + 1]) {
|
|
189
|
+
totalRequests = parseInt(args[++i], 10);
|
|
190
|
+
} else if (args[i] === "--concurrency" && args[i + 1]) {
|
|
191
|
+
concurrency = parseInt(args[++i], 10);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
log.info(`Benchmarking ${c.bold}${baseUrl}${c.reset}`);
|
|
195
|
+
log.info(`${totalRequests} requests, ${concurrency} concurrent connections`);
|
|
196
|
+
log.blank();
|
|
197
|
+
divider();
|
|
198
|
+
const endpoints = [
|
|
199
|
+
{ name: "Homepage (SSR)", path: "/" },
|
|
200
|
+
{ name: "About (SSR)", path: "/about" },
|
|
201
|
+
{ name: "Static Asset", path: "/static/js/runtime.js" }
|
|
202
|
+
];
|
|
203
|
+
for (const endpoint of endpoints) {
|
|
204
|
+
const url = `${baseUrl}${endpoint.path}`;
|
|
205
|
+
log.blank();
|
|
206
|
+
console.log(` ${c.bold}${c.cyan}\u25B8 ${endpoint.name}${c.reset}`);
|
|
207
|
+
log.blank();
|
|
208
|
+
try {
|
|
209
|
+
const result = await runBenchmark(url, totalRequests, concurrency);
|
|
210
|
+
printResult(result);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
log.error(` Failed to benchmark ${url}: ${err.message}`);
|
|
213
|
+
log.info(` Make sure the server is running (${c.bold}blumen dev${c.reset} or ${c.bold}blumen start${c.reset})`);
|
|
214
|
+
}
|
|
215
|
+
log.blank();
|
|
216
|
+
divider();
|
|
217
|
+
}
|
|
218
|
+
log.blank();
|
|
219
|
+
log.success("Benchmark complete! \u2728");
|
|
220
|
+
log.blank();
|
|
221
|
+
log.info(`For production load testing, use k6:`);
|
|
222
|
+
log.info(` ${c.bold}k6 run benchmarks/k6-load-test.js${c.reset}`);
|
|
223
|
+
log.blank();
|
|
224
|
+
}
|
|
225
|
+
export {
|
|
226
|
+
bench
|
|
227
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// cli/commands/build.ts
|
|
2
2
|
import { execSync } from "child_process";
|
|
3
|
+
import * as fs2 from "fs";
|
|
4
|
+
import * as path2 from "path";
|
|
3
5
|
|
|
4
6
|
// cli/utils.ts
|
|
5
7
|
import * as fs from "fs";
|
|
@@ -61,6 +63,37 @@ function divider() {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
// cli/commands/build.ts
|
|
66
|
+
function generateChunkManifest() {
|
|
67
|
+
const jsDir = path2.resolve("static/js");
|
|
68
|
+
const chunksDir = path2.resolve("static/js/chunks");
|
|
69
|
+
const manifest = {};
|
|
70
|
+
if (fs2.existsSync(jsDir)) {
|
|
71
|
+
for (const file of fs2.readdirSync(jsDir)) {
|
|
72
|
+
if (!file.endsWith(".js") || file.endsWith(".map"))
|
|
73
|
+
continue;
|
|
74
|
+
const match = file.match(/^([^.]+)\.[a-f0-9]+\.js$/);
|
|
75
|
+
if (match) {
|
|
76
|
+
manifest[match[1]] = `/static/js/${file}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (fs2.existsSync(chunksDir)) {
|
|
81
|
+
for (const file of fs2.readdirSync(chunksDir)) {
|
|
82
|
+
if (!file.endsWith(".js") || file.endsWith(".map"))
|
|
83
|
+
continue;
|
|
84
|
+
const match = file.match(/^([^.]+)\.[a-f0-9]+\.js$/);
|
|
85
|
+
if (match) {
|
|
86
|
+
manifest[match[1]] = `/static/js/chunks/${file}`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
fs2.mkdirSync("dist", { recursive: true });
|
|
91
|
+
fs2.writeFileSync("dist/chunk-manifest.json", JSON.stringify(manifest, null, 2));
|
|
92
|
+
const coreChunks = ["runtime", "vendor", "framework", "main"].filter((k) => manifest[k]);
|
|
93
|
+
const pageChunks = Object.keys(manifest).filter((k) => k.startsWith("page-"));
|
|
94
|
+
log.info(` Core chunks: ${coreChunks.join(", ")} (${coreChunks.length})`);
|
|
95
|
+
log.info(` Page chunks: ${pageChunks.length} page(s)`);
|
|
96
|
+
}
|
|
64
97
|
async function build(args = []) {
|
|
65
98
|
banner();
|
|
66
99
|
const analyze = args.includes("--analyze");
|
|
@@ -75,10 +108,14 @@ async function build(args = []) {
|
|
|
75
108
|
cmd: "npx tsx scripts/generate-routes.ts"
|
|
76
109
|
},
|
|
77
110
|
{
|
|
78
|
-
label: "Building client bundle",
|
|
111
|
+
label: "Building client bundle (code splitting)",
|
|
79
112
|
cmd: "npx webpack --mode production",
|
|
80
113
|
env: analyze ? { BLUMEN_ANALYZE: "1" } : void 0
|
|
81
114
|
},
|
|
115
|
+
{
|
|
116
|
+
label: "Generating chunk manifest",
|
|
117
|
+
fn: generateChunkManifest
|
|
118
|
+
},
|
|
82
119
|
{
|
|
83
120
|
label: "Building SSR server",
|
|
84
121
|
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --packages=external --alias:@=./app"
|
|
@@ -95,11 +132,15 @@ async function build(args = []) {
|
|
|
95
132
|
const step = steps[i];
|
|
96
133
|
log.step(`[${i + 1}/${steps.length}] ${step.label}...`);
|
|
97
134
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
if (step.fn) {
|
|
136
|
+
step.fn();
|
|
137
|
+
} else if (step.cmd) {
|
|
138
|
+
execSync(step.cmd, {
|
|
139
|
+
stdio: "inherit",
|
|
140
|
+
cwd: process.cwd(),
|
|
141
|
+
env: { ...process.env, ...step.env || {} }
|
|
142
|
+
});
|
|
143
|
+
}
|
|
103
144
|
log.success(step.label);
|
|
104
145
|
} catch {
|
|
105
146
|
if (step.optional) {
|