blumenjs 0.2.2 → 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.
@@ -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
+ };
@@ -0,0 +1,241 @@
1
+ // cli/commands/export.ts
2
+ import { execSync, spawn } from "child_process";
3
+ import * as fs2 from "fs";
4
+ import * as path2 from "path";
5
+ import * as http from "http";
6
+
7
+ // cli/utils.ts
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import { fileURLToPath } from "url";
11
+ var c = {
12
+ reset: "\x1B[0m",
13
+ bold: "\x1B[1m",
14
+ dim: "\x1B[2m",
15
+ red: "\x1B[31m",
16
+ green: "\x1B[32m",
17
+ yellow: "\x1B[33m",
18
+ blue: "\x1B[34m",
19
+ magenta: "\x1B[35m",
20
+ cyan: "\x1B[36m",
21
+ white: "\x1B[37m",
22
+ gray: "\x1B[90m"
23
+ };
24
+ var log = {
25
+ info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
26
+ success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
27
+ error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
28
+ warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
29
+ step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
30
+ blank: () => console.log("")
31
+ };
32
+ function getVersion() {
33
+ try {
34
+ const thisFile = fileURLToPath(import.meta.url);
35
+ let dir = path.dirname(thisFile);
36
+ for (let i = 0; i < 5; i++) {
37
+ const pkgFile = path.join(dir, "package.json");
38
+ if (fs.existsSync(pkgFile)) {
39
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, "utf-8"));
40
+ if (pkg.name === "blumenjs" || pkg.name === "go-react-ssr") {
41
+ return pkg.version;
42
+ }
43
+ }
44
+ dir = path.dirname(dir);
45
+ }
46
+ return "0.0.0";
47
+ } catch {
48
+ return "0.0.0";
49
+ }
50
+ }
51
+ function banner() {
52
+ const version = getVersion();
53
+ console.log("");
54
+ console.log(
55
+ ` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v${version}${c.reset}`
56
+ );
57
+ console.log(
58
+ ` ${c.dim}The React framework powered by Go${c.reset}`
59
+ );
60
+ console.log("");
61
+ }
62
+ function divider() {
63
+ console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
64
+ }
65
+
66
+ // cli/commands/export.ts
67
+ async function renderPage(route, ssrUrl) {
68
+ const body = JSON.stringify({
69
+ path: route,
70
+ query: {},
71
+ params: {}
72
+ });
73
+ return new Promise((resolve2, reject) => {
74
+ const req = http.request(ssrUrl, {
75
+ method: "POST",
76
+ headers: {
77
+ "Content-Type": "application/json",
78
+ "Content-Length": Buffer.byteLength(body)
79
+ }
80
+ }, (res) => {
81
+ let data = "";
82
+ res.on("data", (chunk) => data += chunk);
83
+ res.on("end", () => {
84
+ try {
85
+ const json = JSON.parse(data);
86
+ if (json.html) {
87
+ resolve2(json.html);
88
+ } else {
89
+ reject(new Error("SSR response missing html field"));
90
+ }
91
+ } catch {
92
+ reject(new Error(`Invalid SSR response: ${data.slice(0, 100)}`));
93
+ }
94
+ });
95
+ });
96
+ req.on("error", (err) => reject(new Error(`SSR server unreachable: ${err.message}`)));
97
+ req.setTimeout(15e3, () => {
98
+ req.destroy();
99
+ reject(new Error("SSR timeout"));
100
+ });
101
+ req.write(body);
102
+ req.end();
103
+ });
104
+ }
105
+ function discoverRoutes() {
106
+ const pagesDir = path2.resolve("app/pages");
107
+ const routes = [];
108
+ function scan(dir, prefix) {
109
+ if (!fs2.existsSync(dir))
110
+ return;
111
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
112
+ for (const entry of entries) {
113
+ if (entry.name.startsWith("_") || entry.name.startsWith("."))
114
+ continue;
115
+ if (entry.isDirectory()) {
116
+ scan(path2.join(dir, entry.name), `${prefix}/${entry.name.toLowerCase()}`);
117
+ } else if (entry.name.endsWith(".tsx") && !entry.name.startsWith("NotFound")) {
118
+ const name = entry.name.replace(".tsx", "");
119
+ if (name.toLowerCase() === "home") {
120
+ routes.push("/");
121
+ } else if (name === "index") {
122
+ routes.push(prefix || "/");
123
+ } else if (!name.includes("[")) {
124
+ routes.push(`${prefix}/${name.toLowerCase()}`);
125
+ }
126
+ }
127
+ }
128
+ }
129
+ scan(pagesDir, "");
130
+ return routes;
131
+ }
132
+ async function waitForServer(url, maxWaitMs = 15e3) {
133
+ const start = Date.now();
134
+ while (Date.now() - start < maxWaitMs) {
135
+ try {
136
+ await new Promise((resolve2, reject) => {
137
+ const req = http.get(url, () => resolve2());
138
+ req.on("error", reject);
139
+ req.setTimeout(1e3, () => {
140
+ req.destroy();
141
+ reject(new Error("timeout"));
142
+ });
143
+ });
144
+ return true;
145
+ } catch {
146
+ await new Promise((r) => setTimeout(r, 500));
147
+ }
148
+ }
149
+ return false;
150
+ }
151
+ async function exportSite(args = []) {
152
+ banner();
153
+ let outDir = "dist/export";
154
+ for (let i = 0; i < args.length; i++) {
155
+ if (args[i] === "--out" && args[i + 1]) {
156
+ outDir = args[++i];
157
+ }
158
+ }
159
+ log.info("Exporting static site...");
160
+ log.blank();
161
+ log.info("Step 1/4: Building production bundle...");
162
+ try {
163
+ execSync("npx tsx cli/blumen.ts build", { stdio: "inherit", cwd: process.cwd() });
164
+ } catch {
165
+ log.error("Build failed. Fix errors and try again.");
166
+ process.exit(1);
167
+ }
168
+ log.blank();
169
+ log.info("Step 2/4: Starting SSR server for pre-rendering...");
170
+ const ssrProcess = spawn("node", ["dist/ssr-server.js"], {
171
+ cwd: process.cwd(),
172
+ env: { ...process.env, NODE_ENV: "production", PORT: "4001" },
173
+ stdio: "pipe"
174
+ });
175
+ const ssrUrl = "http://localhost:4001/render";
176
+ const serverReady = await waitForServer("http://localhost:4001/health", 15e3);
177
+ if (!serverReady) {
178
+ log.warn("SSR server health check failed, attempting rendering anyway...");
179
+ }
180
+ log.blank();
181
+ log.info("Step 3/4: Pre-rendering pages...");
182
+ const routes = discoverRoutes();
183
+ const outputDir = path2.resolve(outDir);
184
+ fs2.mkdirSync(outputDir, { recursive: true });
185
+ let success = 0;
186
+ let failed = 0;
187
+ for (const route of routes) {
188
+ try {
189
+ const html = await renderPage(route, ssrUrl);
190
+ const filePath = route === "/" ? path2.join(outputDir, "index.html") : path2.join(outputDir, route.slice(1), "index.html");
191
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
192
+ fs2.writeFileSync(filePath, html, "utf-8");
193
+ console.log(` ${c.green}\u2713${c.reset} ${route} \u2192 ${path2.relative(process.cwd(), filePath)}`);
194
+ success++;
195
+ } catch (err) {
196
+ console.log(` ${c.red}\u2717${c.reset} ${route}: ${err.message}`);
197
+ failed++;
198
+ }
199
+ }
200
+ ssrProcess.kill("SIGTERM");
201
+ log.blank();
202
+ log.info("Step 4/4: Copying static assets...");
203
+ const staticDir = path2.resolve("static");
204
+ if (fs2.existsSync(staticDir)) {
205
+ copyDir(staticDir, path2.join(outputDir, "static"));
206
+ log.success("Static assets copied.");
207
+ }
208
+ const distJs = path2.resolve("dist/client");
209
+ if (fs2.existsSync(distJs)) {
210
+ copyDir(distJs, path2.join(outputDir, "static/js"));
211
+ }
212
+ log.blank();
213
+ divider();
214
+ log.blank();
215
+ log.success(`Static export complete! \u2728`);
216
+ log.info(`${success} page(s) exported${failed > 0 ? `, ${failed} failed` : ""}`);
217
+ log.info(`Output: ${c.bold}${outDir}/${c.reset}`);
218
+ log.blank();
219
+ log.info(`Deploy anywhere:`);
220
+ log.info(` ${c.dim}GitHub Pages:${c.reset} push ${outDir}/ to gh-pages branch`);
221
+ log.info(` ${c.dim}Netlify:${c.reset} set publish directory to ${outDir}/`);
222
+ log.info(` ${c.dim}S3:${c.reset} aws s3 sync ${outDir}/ s3://my-bucket`);
223
+ log.info(` ${c.dim}Local:${c.reset} npx serve ${outDir}/`);
224
+ log.blank();
225
+ }
226
+ function copyDir(src, dest) {
227
+ fs2.mkdirSync(dest, { recursive: true });
228
+ const entries = fs2.readdirSync(src, { withFileTypes: true });
229
+ for (const entry of entries) {
230
+ const srcPath = path2.join(src, entry.name);
231
+ const destPath = path2.join(dest, entry.name);
232
+ if (entry.isDirectory()) {
233
+ copyDir(srcPath, destPath);
234
+ } else {
235
+ fs2.copyFileSync(srcPath, destPath);
236
+ }
237
+ }
238
+ }
239
+ export {
240
+ exportSite
241
+ };