blumenjs 0.1.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/README.md +127 -0
- package/dist/cli/blumen.js +697 -0
- package/dist/cli/commands/build.js +85 -0
- package/dist/cli/commands/create.js +384 -0
- package/dist/cli/commands/dev.js +163 -0
- package/dist/cli/commands/start.js +129 -0
- package/dist/cli/utils.js +85 -0
- package/dist/templates/app/client/entry.tsx +41 -0
- package/dist/templates/app/pages/BlumenStarter.tsx +398 -0
- package/dist/templates/app/pages/NotFound.tsx +22 -0
- package/dist/templates/app/shared/DefaultApp.tsx +5 -0
- package/dist/templates/app/shared/DefaultDocument.tsx +76 -0
- package/dist/templates/app/shared/Link.tsx +73 -0
- package/dist/templates/app/shared/RouterContext.tsx +176 -0
- package/dist/templates/app/shared/router.ts +23 -0
- package/dist/templates/go-server/main.go +175 -0
- package/dist/templates/node-ssr/server.ts +141 -0
- package/dist/templates/scripts/generate-routes.ts +220 -0
- package/package.json +77 -0
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// cli/commands/dev.ts
|
|
3
|
+
import { spawn, execSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
// cli/utils.ts
|
|
6
|
+
var c = {
|
|
7
|
+
reset: "\x1B[0m",
|
|
8
|
+
bold: "\x1B[1m",
|
|
9
|
+
dim: "\x1B[2m",
|
|
10
|
+
red: "\x1B[31m",
|
|
11
|
+
green: "\x1B[32m",
|
|
12
|
+
yellow: "\x1B[33m",
|
|
13
|
+
blue: "\x1B[34m",
|
|
14
|
+
magenta: "\x1B[35m",
|
|
15
|
+
cyan: "\x1B[36m",
|
|
16
|
+
white: "\x1B[37m",
|
|
17
|
+
gray: "\x1B[90m"
|
|
18
|
+
};
|
|
19
|
+
var log = {
|
|
20
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
21
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
22
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
23
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
24
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
25
|
+
blank: () => console.log("")
|
|
26
|
+
};
|
|
27
|
+
function banner() {
|
|
28
|
+
console.log("");
|
|
29
|
+
console.log(
|
|
30
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v0.1.0${c.reset}`
|
|
31
|
+
);
|
|
32
|
+
console.log(
|
|
33
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
34
|
+
);
|
|
35
|
+
console.log("");
|
|
36
|
+
}
|
|
37
|
+
function divider() {
|
|
38
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
39
|
+
}
|
|
40
|
+
async function select(question, options) {
|
|
41
|
+
const readline = await import("readline");
|
|
42
|
+
const rl = readline.createInterface({
|
|
43
|
+
input: process.stdin,
|
|
44
|
+
output: process.stdout
|
|
45
|
+
});
|
|
46
|
+
return new Promise((resolve2) => {
|
|
47
|
+
console.log(`
|
|
48
|
+
${c.bold}${question}${c.reset}`);
|
|
49
|
+
options.forEach((opt, i) => {
|
|
50
|
+
console.log(` ${c.cyan}${i + 1}${c.reset}) ${opt}`);
|
|
51
|
+
});
|
|
52
|
+
rl.question(`
|
|
53
|
+
${c.dim}Enter choice [1-${options.length}]:${c.reset} `, (answer) => {
|
|
54
|
+
rl.close();
|
|
55
|
+
const idx = parseInt(answer, 10) - 1;
|
|
56
|
+
if (idx >= 0 && idx < options.length) {
|
|
57
|
+
resolve2(options[idx]);
|
|
58
|
+
} else {
|
|
59
|
+
resolve2(options[0]);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// cli/commands/dev.ts
|
|
66
|
+
async function dev() {
|
|
67
|
+
banner();
|
|
68
|
+
log.step("Generating routes...");
|
|
69
|
+
try {
|
|
70
|
+
execSync("npx tsx scripts/generate-routes.ts", {
|
|
71
|
+
stdio: "pipe",
|
|
72
|
+
cwd: process.cwd()
|
|
73
|
+
});
|
|
74
|
+
log.success("Routes generated");
|
|
75
|
+
} catch {
|
|
76
|
+
log.error("Route generation failed");
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
divider();
|
|
80
|
+
log.info("Starting development servers...");
|
|
81
|
+
log.blank();
|
|
82
|
+
const services = [
|
|
83
|
+
{
|
|
84
|
+
name: "webpack",
|
|
85
|
+
label: "hmr",
|
|
86
|
+
color: c.magenta,
|
|
87
|
+
cmd: "npx",
|
|
88
|
+
args: ["webpack", "serve", "--mode", "development"],
|
|
89
|
+
readyPattern: /compiled successfully/
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "node-ssr",
|
|
93
|
+
label: "ssr",
|
|
94
|
+
color: c.cyan,
|
|
95
|
+
cmd: "npx",
|
|
96
|
+
args: ["tsx", "watch", "node-ssr/server.ts"],
|
|
97
|
+
env: { NODE_ENV: "development" },
|
|
98
|
+
readyPattern: /SSR server running/
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "go-server",
|
|
102
|
+
label: " go",
|
|
103
|
+
color: c.green,
|
|
104
|
+
cmd: "go",
|
|
105
|
+
args: ["run", "go-server/main.go"],
|
|
106
|
+
readyPattern: /Go server starting/
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
const children = [];
|
|
110
|
+
const ready = /* @__PURE__ */ new Set();
|
|
111
|
+
for (const svc of services) {
|
|
112
|
+
const child = spawn(svc.cmd, svc.args, {
|
|
113
|
+
cwd: process.cwd(),
|
|
114
|
+
env: { ...process.env, ...svc.env || {} },
|
|
115
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
116
|
+
shell: false
|
|
117
|
+
});
|
|
118
|
+
const prefix = ` ${svc.color}\u2502${c.reset} ${svc.color}${svc.label}${c.reset} `;
|
|
119
|
+
const handleOutput = (data) => {
|
|
120
|
+
const lines = data.toString().split("\n");
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
const trimmed = line.replace(/\r$/, "");
|
|
123
|
+
if (!trimmed)
|
|
124
|
+
continue;
|
|
125
|
+
console.log(`${prefix}${trimmed}`);
|
|
126
|
+
if (svc.readyPattern && svc.readyPattern.test(trimmed) && !ready.has(svc.name)) {
|
|
127
|
+
ready.add(svc.name);
|
|
128
|
+
if (ready.size === services.length) {
|
|
129
|
+
printReadyBanner();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
child.stdout?.on("data", handleOutput);
|
|
135
|
+
child.stderr?.on("data", handleOutput);
|
|
136
|
+
child.on("exit", (code) => {
|
|
137
|
+
if (code !== null && code !== 0) {
|
|
138
|
+
log.warn(`${svc.name} exited with code ${code}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
children.push(child);
|
|
142
|
+
}
|
|
143
|
+
function printReadyBanner() {
|
|
144
|
+
log.blank();
|
|
145
|
+
divider();
|
|
146
|
+
log.blank();
|
|
147
|
+
log.success(`${c.bold}Ready!${c.reset} All services are running.`);
|
|
148
|
+
log.blank();
|
|
149
|
+
console.log(
|
|
150
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}App${c.reset}: ${c.cyan}http://localhost:3000${c.reset}`
|
|
151
|
+
);
|
|
152
|
+
console.log(
|
|
153
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}SSR${c.reset}: ${c.dim}http://localhost:4000${c.reset}`
|
|
154
|
+
);
|
|
155
|
+
console.log(
|
|
156
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}HMR${c.reset}: ${c.dim}http://localhost:3100${c.reset}`
|
|
157
|
+
);
|
|
158
|
+
log.blank();
|
|
159
|
+
console.log(
|
|
160
|
+
` ${c.dim}Press ${c.bold}Ctrl+C${c.reset}${c.dim} to stop all services.${c.reset}`
|
|
161
|
+
);
|
|
162
|
+
log.blank();
|
|
163
|
+
divider();
|
|
164
|
+
}
|
|
165
|
+
const shutdown = () => {
|
|
166
|
+
log.blank();
|
|
167
|
+
log.info("Shutting down...");
|
|
168
|
+
for (const child of children) {
|
|
169
|
+
if (!child.killed)
|
|
170
|
+
child.kill("SIGTERM");
|
|
171
|
+
}
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
for (const child of children) {
|
|
174
|
+
if (!child.killed)
|
|
175
|
+
child.kill("SIGKILL");
|
|
176
|
+
}
|
|
177
|
+
log.success("Stopped.");
|
|
178
|
+
process.exit(0);
|
|
179
|
+
}, 3e3);
|
|
180
|
+
};
|
|
181
|
+
process.on("SIGINT", shutdown);
|
|
182
|
+
process.on("SIGTERM", shutdown);
|
|
183
|
+
await new Promise(() => {
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// cli/commands/build.ts
|
|
188
|
+
import { execSync as execSync2 } from "child_process";
|
|
189
|
+
async function build() {
|
|
190
|
+
banner();
|
|
191
|
+
log.info("Creating production build...");
|
|
192
|
+
log.blank();
|
|
193
|
+
const steps = [
|
|
194
|
+
{
|
|
195
|
+
label: "Generating routes",
|
|
196
|
+
cmd: "npx tsx scripts/generate-routes.ts"
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: "Building client bundle",
|
|
200
|
+
cmd: "npx webpack --mode production"
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: "Building SSR server",
|
|
204
|
+
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom"
|
|
205
|
+
}
|
|
206
|
+
];
|
|
207
|
+
const startTime = Date.now();
|
|
208
|
+
for (let i = 0; i < steps.length; i++) {
|
|
209
|
+
const step = steps[i];
|
|
210
|
+
log.step(`[${i + 1}/${steps.length}] ${step.label}...`);
|
|
211
|
+
try {
|
|
212
|
+
execSync2(step.cmd, { stdio: "inherit", cwd: process.cwd() });
|
|
213
|
+
log.success(step.label);
|
|
214
|
+
} catch {
|
|
215
|
+
log.error(`Failed: ${step.label}`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
220
|
+
log.blank();
|
|
221
|
+
divider();
|
|
222
|
+
log.blank();
|
|
223
|
+
log.success(
|
|
224
|
+
`${c.bold}Build complete${c.reset} in ${c.cyan}${elapsed}s${c.reset}`
|
|
225
|
+
);
|
|
226
|
+
log.info(
|
|
227
|
+
`Run ${c.bold}blumen start${c.reset} to start the production server.`
|
|
228
|
+
);
|
|
229
|
+
log.blank();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// cli/commands/start.ts
|
|
233
|
+
import { spawn as spawn2 } from "child_process";
|
|
234
|
+
import * as fs from "fs";
|
|
235
|
+
async function start() {
|
|
236
|
+
banner();
|
|
237
|
+
if (!fs.existsSync("dist/ssr-server.js")) {
|
|
238
|
+
log.error("Production build not found.");
|
|
239
|
+
log.info(
|
|
240
|
+
`Run ${c.bold}blumen build${c.reset} first to create a production build.`
|
|
241
|
+
);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
log.info("Starting production server...");
|
|
245
|
+
log.blank();
|
|
246
|
+
const services = [
|
|
247
|
+
{
|
|
248
|
+
name: "ssr",
|
|
249
|
+
label: "ssr",
|
|
250
|
+
color: c.cyan,
|
|
251
|
+
cmd: "node",
|
|
252
|
+
args: ["dist/ssr-server.js"],
|
|
253
|
+
readyPattern: /SSR server running|listening/i
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: "go",
|
|
257
|
+
label: " go",
|
|
258
|
+
color: c.green,
|
|
259
|
+
cmd: "go",
|
|
260
|
+
args: ["run", "go-server/main.go"],
|
|
261
|
+
readyPattern: /Go server starting/
|
|
262
|
+
}
|
|
263
|
+
];
|
|
264
|
+
const children = [];
|
|
265
|
+
const ready = /* @__PURE__ */ new Set();
|
|
266
|
+
for (const svc of services) {
|
|
267
|
+
const child = spawn2(svc.cmd, svc.args, {
|
|
268
|
+
cwd: process.cwd(),
|
|
269
|
+
env: { ...process.env, NODE_ENV: "production" },
|
|
270
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
271
|
+
});
|
|
272
|
+
const prefix = ` ${svc.color}\u2502${c.reset} ${svc.color}${svc.label}${c.reset} `;
|
|
273
|
+
const handleOutput = (data) => {
|
|
274
|
+
for (const line of data.toString().split("\n")) {
|
|
275
|
+
const trimmed = line.replace(/\r$/, "");
|
|
276
|
+
if (!trimmed)
|
|
277
|
+
continue;
|
|
278
|
+
console.log(`${prefix}${trimmed}`);
|
|
279
|
+
if (svc.readyPattern && svc.readyPattern.test(trimmed) && !ready.has(svc.name)) {
|
|
280
|
+
ready.add(svc.name);
|
|
281
|
+
if (ready.size === services.length) {
|
|
282
|
+
log.blank();
|
|
283
|
+
divider();
|
|
284
|
+
log.blank();
|
|
285
|
+
log.success(
|
|
286
|
+
`${c.bold}Production server running.${c.reset}`
|
|
287
|
+
);
|
|
288
|
+
console.log(
|
|
289
|
+
` ${c.dim}\u279C${c.reset} ${c.cyan}http://localhost:3000${c.reset}`
|
|
290
|
+
);
|
|
291
|
+
log.blank();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
child.stdout?.on("data", handleOutput);
|
|
297
|
+
child.stderr?.on("data", handleOutput);
|
|
298
|
+
children.push(child);
|
|
299
|
+
}
|
|
300
|
+
const shutdown = () => {
|
|
301
|
+
log.blank();
|
|
302
|
+
log.info("Shutting down...");
|
|
303
|
+
for (const child of children) {
|
|
304
|
+
if (!child.killed)
|
|
305
|
+
child.kill("SIGTERM");
|
|
306
|
+
}
|
|
307
|
+
setTimeout(() => {
|
|
308
|
+
for (const child of children) {
|
|
309
|
+
if (!child.killed)
|
|
310
|
+
child.kill("SIGKILL");
|
|
311
|
+
}
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}, 3e3);
|
|
314
|
+
};
|
|
315
|
+
process.on("SIGINT", shutdown);
|
|
316
|
+
process.on("SIGTERM", shutdown);
|
|
317
|
+
await new Promise(() => {
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// cli/commands/create.ts
|
|
322
|
+
import * as fs2 from "fs";
|
|
323
|
+
import * as path from "path";
|
|
324
|
+
import { execSync as execSync3 } from "child_process";
|
|
325
|
+
function getFrameworkRoot() {
|
|
326
|
+
const cliEntry = process.argv[1];
|
|
327
|
+
const cliDir = path.dirname(cliEntry);
|
|
328
|
+
return path.resolve(cliDir, "..");
|
|
329
|
+
}
|
|
330
|
+
function readProjectFile(relativePath) {
|
|
331
|
+
const root = getFrameworkRoot();
|
|
332
|
+
const bundledPath = path.join(root, "templates", relativePath);
|
|
333
|
+
if (fs2.existsSync(bundledPath)) {
|
|
334
|
+
return fs2.readFileSync(bundledPath, "utf-8");
|
|
335
|
+
}
|
|
336
|
+
const sourcePath = path.join(root, relativePath);
|
|
337
|
+
if (fs2.existsSync(sourcePath)) {
|
|
338
|
+
return fs2.readFileSync(sourcePath, "utf-8");
|
|
339
|
+
}
|
|
340
|
+
throw new Error(`Template file not found: ${relativePath}`);
|
|
341
|
+
}
|
|
342
|
+
function pkgJson(name) {
|
|
343
|
+
return JSON.stringify(
|
|
344
|
+
{
|
|
345
|
+
name,
|
|
346
|
+
version: "0.1.0",
|
|
347
|
+
description: `${name} \u2014 A Blumen app`,
|
|
348
|
+
type: "module",
|
|
349
|
+
scripts: {
|
|
350
|
+
routes: "tsx scripts/generate-routes.ts",
|
|
351
|
+
dev: 'npm run routes && concurrently "npm run dev:client" "npm run dev:ssr" "npm run dev:go"',
|
|
352
|
+
"dev:client": "webpack serve --mode development",
|
|
353
|
+
"dev:ssr": "NODE_ENV=development tsx watch node-ssr/server.ts",
|
|
354
|
+
"dev:go": "go run go-server/main.go",
|
|
355
|
+
build: "npm run routes && npm run build:client && npm run build:ssr",
|
|
356
|
+
"build:client": "webpack --mode production",
|
|
357
|
+
"build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom",
|
|
358
|
+
start: "node dist/ssr-server.js",
|
|
359
|
+
clean: "rm -rf dist static/js/bundle.js"
|
|
360
|
+
},
|
|
361
|
+
dependencies: {
|
|
362
|
+
"lucide-react": "^1.14.0",
|
|
363
|
+
react: "^18.2.0",
|
|
364
|
+
"react-dom": "^18.2.0"
|
|
365
|
+
},
|
|
366
|
+
devDependencies: {
|
|
367
|
+
"@babel/core": "^7.24.0",
|
|
368
|
+
"@babel/preset-env": "^7.24.0",
|
|
369
|
+
"@babel/preset-react": "^7.24.0",
|
|
370
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
371
|
+
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
|
372
|
+
"@types/node": "^20.10.0",
|
|
373
|
+
"@types/react": "^18.2.0",
|
|
374
|
+
"@types/react-dom": "^18.2.0",
|
|
375
|
+
"babel-loader": "^9.2.1",
|
|
376
|
+
concurrently: "^8.2.2",
|
|
377
|
+
esbuild: "^0.19.0",
|
|
378
|
+
"react-refresh": "^0.14.2",
|
|
379
|
+
"ts-loader": "^9.5.1",
|
|
380
|
+
tsx: "^4.6.0",
|
|
381
|
+
typescript: "^5.3.0",
|
|
382
|
+
webpack: "^5.89.0",
|
|
383
|
+
"webpack-cli": "^5.1.4",
|
|
384
|
+
"webpack-dev-server": "^5.2.0"
|
|
385
|
+
},
|
|
386
|
+
engines: { node: ">=18.0.0" }
|
|
387
|
+
},
|
|
388
|
+
null,
|
|
389
|
+
" "
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
var TSCONFIG = `{
|
|
393
|
+
"compilerOptions": {
|
|
394
|
+
"target": "ES2020",
|
|
395
|
+
"module": "ESNext",
|
|
396
|
+
"moduleResolution": "bundler",
|
|
397
|
+
"jsx": "react-jsx",
|
|
398
|
+
"strict": true,
|
|
399
|
+
"esModuleInterop": true,
|
|
400
|
+
"skipLibCheck": true,
|
|
401
|
+
"forceConsistentCasingInFileNames": true,
|
|
402
|
+
"resolveJsonModule": true,
|
|
403
|
+
"outDir": "./dist",
|
|
404
|
+
"rootDir": "."
|
|
405
|
+
},
|
|
406
|
+
"include": ["app/**/*", "node-ssr/**/*", "scripts/**/*"],
|
|
407
|
+
"exclude": ["node_modules", "dist", "static"]
|
|
408
|
+
}
|
|
409
|
+
`;
|
|
410
|
+
var NOT_FOUND_PAGE = `import React from "react";
|
|
411
|
+
|
|
412
|
+
const NotFoundPage = () => (
|
|
413
|
+
<div style={{ textAlign: "center", padding: "4rem 2rem", color: "#e2e8f0" }}>
|
|
414
|
+
<h1 style={{ fontSize: "2rem", color: "#a855f7" }}>404</h1>
|
|
415
|
+
<p style={{ marginTop: "1rem", color: "#94a3b8" }}>Page not found.</p>
|
|
416
|
+
</div>
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
export default NotFoundPage;
|
|
420
|
+
`;
|
|
421
|
+
var DEFAULT_APP = `import React from "react";
|
|
422
|
+
|
|
423
|
+
export function DefaultApp({ Component, pageProps }: any) {
|
|
424
|
+
return <Component {...pageProps} />;
|
|
425
|
+
}
|
|
426
|
+
`;
|
|
427
|
+
var DEFAULT_DOCUMENT = `import React from "react";
|
|
428
|
+
|
|
429
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
430
|
+
const BUNDLE_SRC = isDev
|
|
431
|
+
? "http://localhost:3100/static/js/bundle.js"
|
|
432
|
+
: "/static/js/bundle.js";
|
|
433
|
+
|
|
434
|
+
export function DefaultDocument({ children, initialProps }: any) {
|
|
435
|
+
return (
|
|
436
|
+
<html lang="en">
|
|
437
|
+
<head>
|
|
438
|
+
<meta charSet="UTF-8" />
|
|
439
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
440
|
+
<title>Blumen App</title>
|
|
441
|
+
</head>
|
|
442
|
+
<body>
|
|
443
|
+
<div id="root">{children}</div>
|
|
444
|
+
<script
|
|
445
|
+
id="ssr-props"
|
|
446
|
+
type="application/json"
|
|
447
|
+
dangerouslySetInnerHTML={{
|
|
448
|
+
__html: JSON.stringify(initialProps).replace(/</g, '\\\\u003c')
|
|
449
|
+
}}
|
|
450
|
+
/>
|
|
451
|
+
<script src={BUNDLE_SRC} defer></script>
|
|
452
|
+
</body>
|
|
453
|
+
</html>
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
`;
|
|
457
|
+
var WEBPACK_CONFIG = [
|
|
458
|
+
"import path from 'path';",
|
|
459
|
+
"import { fileURLToPath } from 'url';",
|
|
460
|
+
"import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';",
|
|
461
|
+
"",
|
|
462
|
+
"const __filename = fileURLToPath(import.meta.url);",
|
|
463
|
+
"const __dirname = path.dirname(__filename);",
|
|
464
|
+
"const HMR_PORT = 3100;",
|
|
465
|
+
"",
|
|
466
|
+
"const config = (env, argv) => {",
|
|
467
|
+
" const isDev = argv.mode === 'development';",
|
|
468
|
+
" return {",
|
|
469
|
+
" entry: './app/client/entry.tsx',",
|
|
470
|
+
" output: {",
|
|
471
|
+
" filename: 'bundle.js',",
|
|
472
|
+
" path: path.resolve(__dirname, 'static', 'js'),",
|
|
473
|
+
" publicPath: isDev ? `http://localhost:${HMR_PORT}/static/js/` : '/static/js/',",
|
|
474
|
+
" },",
|
|
475
|
+
" module: {",
|
|
476
|
+
" rules: [{",
|
|
477
|
+
" test: /\\.tsx?$/,",
|
|
478
|
+
" use: {",
|
|
479
|
+
" loader: 'babel-loader',",
|
|
480
|
+
" options: {",
|
|
481
|
+
" presets: [",
|
|
482
|
+
" ['@babel/preset-env', { targets: { esmodules: true } }],",
|
|
483
|
+
" ['@babel/preset-react', { runtime: 'automatic' }],",
|
|
484
|
+
" '@babel/preset-typescript',",
|
|
485
|
+
" ],",
|
|
486
|
+
" plugins: isDev ? ['react-refresh/babel'] : [],",
|
|
487
|
+
" },",
|
|
488
|
+
" },",
|
|
489
|
+
" exclude: /node_modules/,",
|
|
490
|
+
" }],",
|
|
491
|
+
" },",
|
|
492
|
+
" resolve: { extensions: ['.tsx', '.ts', '.js'] },",
|
|
493
|
+
" mode: isDev ? 'development' : 'production',",
|
|
494
|
+
" devtool: isDev ? 'eval-source-map' : 'source-map',",
|
|
495
|
+
" optimization: { minimize: !isDev },",
|
|
496
|
+
" plugins: isDev ? [new ReactRefreshWebpackPlugin({ overlay: false })] : [],",
|
|
497
|
+
" ...(isDev && {",
|
|
498
|
+
" devServer: {",
|
|
499
|
+
" port: HMR_PORT,",
|
|
500
|
+
" hot: true,",
|
|
501
|
+
" headers: { 'Access-Control-Allow-Origin': '*' },",
|
|
502
|
+
" allowedHosts: 'all',",
|
|
503
|
+
" static: false,",
|
|
504
|
+
" client: { webSocketURL: `ws://localhost:${HMR_PORT}/ws` },",
|
|
505
|
+
" },",
|
|
506
|
+
" }),",
|
|
507
|
+
" };",
|
|
508
|
+
"};",
|
|
509
|
+
"",
|
|
510
|
+
"export default config;",
|
|
511
|
+
""
|
|
512
|
+
].join("\n");
|
|
513
|
+
var LINK_TSX = `import React, { useContext } from "react";
|
|
514
|
+
import { RouterContextRef } from "./RouterContext";
|
|
515
|
+
|
|
516
|
+
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
517
|
+
href: string;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function isExternal(href: string): boolean {
|
|
521
|
+
return /^https?:\\/\\//.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function isModifiedClick(e: React.MouseEvent): boolean {
|
|
525
|
+
return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
|
|
529
|
+
const ctx = useContext(RouterContextRef);
|
|
530
|
+
if (!ctx) return <a href={href} target={target} {...rest}>{children}</a>;
|
|
531
|
+
|
|
532
|
+
const { navigate } = ctx;
|
|
533
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
534
|
+
if (onClick) onClick(e);
|
|
535
|
+
if (e.defaultPrevented) return;
|
|
536
|
+
if (isExternal(href) || target === "_blank" || isModifiedClick(e)) return;
|
|
537
|
+
e.preventDefault();
|
|
538
|
+
navigate(href);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
return <a href={href} onClick={handleClick} target={target} {...rest}>{children}</a>;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export default Link;
|
|
545
|
+
`;
|
|
546
|
+
function writeFile(base, relPath, content) {
|
|
547
|
+
const fullPath = path.join(base, relPath);
|
|
548
|
+
fs2.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
549
|
+
fs2.writeFileSync(fullPath, content, "utf-8");
|
|
550
|
+
}
|
|
551
|
+
function getTemplateFiles(projectName) {
|
|
552
|
+
return [
|
|
553
|
+
// Generated config
|
|
554
|
+
["package.json", pkgJson(projectName)],
|
|
555
|
+
["tsconfig.json", TSCONFIG],
|
|
556
|
+
["webpack.config.js", WEBPACK_CONFIG],
|
|
557
|
+
// Embedded simple templates
|
|
558
|
+
["app/pages/NotFound.tsx", NOT_FOUND_PAGE],
|
|
559
|
+
["app/shared/DefaultApp.tsx", DEFAULT_APP],
|
|
560
|
+
["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
|
|
561
|
+
["app/shared/Link.tsx", LINK_TSX],
|
|
562
|
+
// Complex files — copied from the framework source (avoids escaping hell)
|
|
563
|
+
["app/pages/Home.tsx", readProjectFile("app/pages/BlumenStarter.tsx")],
|
|
564
|
+
["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
|
|
565
|
+
["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
|
|
566
|
+
["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
|
|
567
|
+
["node-ssr/server.ts", readProjectFile("node-ssr/server.ts")],
|
|
568
|
+
["go-server/main.go", readProjectFile("go-server/main.go")],
|
|
569
|
+
["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
|
|
570
|
+
// Placeholder
|
|
571
|
+
["static/js/.gitkeep", ""]
|
|
572
|
+
];
|
|
573
|
+
}
|
|
574
|
+
async function create(projectName) {
|
|
575
|
+
banner();
|
|
576
|
+
log.info("Create a new Blumen project\n");
|
|
577
|
+
if (!projectName) {
|
|
578
|
+
log.error("Please provide a project name.");
|
|
579
|
+
console.log(
|
|
580
|
+
`
|
|
581
|
+
${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
|
|
582
|
+
`
|
|
583
|
+
);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
587
|
+
if (fs2.existsSync(projectDir)) {
|
|
588
|
+
log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
const pkgManager = await select("Which package manager do you want to use?", [
|
|
592
|
+
"npm",
|
|
593
|
+
"yarn",
|
|
594
|
+
"pnpm",
|
|
595
|
+
"bun"
|
|
596
|
+
]);
|
|
597
|
+
log.blank();
|
|
598
|
+
divider();
|
|
599
|
+
log.blank();
|
|
600
|
+
log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
|
|
601
|
+
const files = getTemplateFiles(projectName);
|
|
602
|
+
for (const [relPath, content] of files) {
|
|
603
|
+
writeFile(projectDir, relPath, content);
|
|
604
|
+
}
|
|
605
|
+
log.success(`${files.length} files written`);
|
|
606
|
+
log.step(`Installing dependencies with ${c.bold}${pkgManager}${c.reset}...`);
|
|
607
|
+
const installCmd = {
|
|
608
|
+
npm: "npm install",
|
|
609
|
+
yarn: "yarn",
|
|
610
|
+
pnpm: "pnpm install",
|
|
611
|
+
bun: "bun install"
|
|
612
|
+
};
|
|
613
|
+
try {
|
|
614
|
+
execSync3(installCmd[pkgManager], {
|
|
615
|
+
cwd: projectDir,
|
|
616
|
+
stdio: "inherit"
|
|
617
|
+
});
|
|
618
|
+
log.success("Dependencies installed");
|
|
619
|
+
} catch {
|
|
620
|
+
log.warn(
|
|
621
|
+
"Could not install dependencies. Run the install command manually."
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
log.blank();
|
|
625
|
+
divider();
|
|
626
|
+
log.blank();
|
|
627
|
+
log.success(`${c.bold}Project created!${c.reset}`);
|
|
628
|
+
log.blank();
|
|
629
|
+
const runCmd = {
|
|
630
|
+
npm: "npm run dev",
|
|
631
|
+
yarn: "yarn dev",
|
|
632
|
+
pnpm: "pnpm dev",
|
|
633
|
+
bun: "bun dev"
|
|
634
|
+
};
|
|
635
|
+
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
636
|
+
console.log(` cd ${projectName}`);
|
|
637
|
+
console.log(` ${runCmd[pkgManager]}`);
|
|
638
|
+
log.blank();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// cli/blumen.ts
|
|
642
|
+
var VERSION = "0.1.0";
|
|
643
|
+
async function main() {
|
|
644
|
+
const command = process.argv[2];
|
|
645
|
+
if (!command || command === "--help" || command === "-h") {
|
|
646
|
+
banner();
|
|
647
|
+
console.log(
|
|
648
|
+
` ${c.bold}Usage${c.reset} blumen ${c.dim}<command>${c.reset}
|
|
649
|
+
`
|
|
650
|
+
);
|
|
651
|
+
console.log(` ${c.bold}Commands${c.reset}`);
|
|
652
|
+
console.log(
|
|
653
|
+
` dev Start development server with HMR`
|
|
654
|
+
);
|
|
655
|
+
console.log(` build Create a production build`);
|
|
656
|
+
console.log(` start Start the production server`);
|
|
657
|
+
console.log(
|
|
658
|
+
` create Scaffold a new Blumen project`
|
|
659
|
+
);
|
|
660
|
+
console.log("");
|
|
661
|
+
console.log(` ${c.bold}Options${c.reset}`);
|
|
662
|
+
console.log(` --help Show this help message`);
|
|
663
|
+
console.log(` --version Show version number`);
|
|
664
|
+
console.log("");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (command === "--version" || command === "-v") {
|
|
668
|
+
console.log(`blumen v${VERSION}`);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
switch (command) {
|
|
672
|
+
case "dev":
|
|
673
|
+
await dev();
|
|
674
|
+
break;
|
|
675
|
+
case "build":
|
|
676
|
+
await build();
|
|
677
|
+
break;
|
|
678
|
+
case "start":
|
|
679
|
+
await start();
|
|
680
|
+
break;
|
|
681
|
+
case "create":
|
|
682
|
+
await create(process.argv[3]);
|
|
683
|
+
break;
|
|
684
|
+
default:
|
|
685
|
+
log.error(
|
|
686
|
+
`Unknown command: ${c.bold}${command}${c.reset}`
|
|
687
|
+
);
|
|
688
|
+
log.info(
|
|
689
|
+
`Run ${c.bold}blumen --help${c.reset} for available commands.`
|
|
690
|
+
);
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
main().catch((err) => {
|
|
695
|
+
log.error(err.message || String(err));
|
|
696
|
+
process.exit(1);
|
|
697
|
+
});
|