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,85 @@
|
|
|
1
|
+
// cli/commands/build.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
// cli/utils.ts
|
|
5
|
+
var c = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
bold: "\x1B[1m",
|
|
8
|
+
dim: "\x1B[2m",
|
|
9
|
+
red: "\x1B[31m",
|
|
10
|
+
green: "\x1B[32m",
|
|
11
|
+
yellow: "\x1B[33m",
|
|
12
|
+
blue: "\x1B[34m",
|
|
13
|
+
magenta: "\x1B[35m",
|
|
14
|
+
cyan: "\x1B[36m",
|
|
15
|
+
white: "\x1B[37m",
|
|
16
|
+
gray: "\x1B[90m"
|
|
17
|
+
};
|
|
18
|
+
var log = {
|
|
19
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
20
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
21
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
22
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
23
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
24
|
+
blank: () => console.log("")
|
|
25
|
+
};
|
|
26
|
+
function banner() {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(
|
|
29
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v0.1.0${c.reset}`
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
33
|
+
);
|
|
34
|
+
console.log("");
|
|
35
|
+
}
|
|
36
|
+
function divider() {
|
|
37
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// cli/commands/build.ts
|
|
41
|
+
async function build() {
|
|
42
|
+
banner();
|
|
43
|
+
log.info("Creating production build...");
|
|
44
|
+
log.blank();
|
|
45
|
+
const steps = [
|
|
46
|
+
{
|
|
47
|
+
label: "Generating routes",
|
|
48
|
+
cmd: "npx tsx scripts/generate-routes.ts"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
label: "Building client bundle",
|
|
52
|
+
cmd: "npx webpack --mode production"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: "Building SSR server",
|
|
56
|
+
cmd: "npx esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom"
|
|
57
|
+
}
|
|
58
|
+
];
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
for (let i = 0; i < steps.length; i++) {
|
|
61
|
+
const step = steps[i];
|
|
62
|
+
log.step(`[${i + 1}/${steps.length}] ${step.label}...`);
|
|
63
|
+
try {
|
|
64
|
+
execSync(step.cmd, { stdio: "inherit", cwd: process.cwd() });
|
|
65
|
+
log.success(step.label);
|
|
66
|
+
} catch {
|
|
67
|
+
log.error(`Failed: ${step.label}`);
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
72
|
+
log.blank();
|
|
73
|
+
divider();
|
|
74
|
+
log.blank();
|
|
75
|
+
log.success(
|
|
76
|
+
`${c.bold}Build complete${c.reset} in ${c.cyan}${elapsed}s${c.reset}`
|
|
77
|
+
);
|
|
78
|
+
log.info(
|
|
79
|
+
`Run ${c.bold}blumen start${c.reset} to start the production server.`
|
|
80
|
+
);
|
|
81
|
+
log.blank();
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
build
|
|
85
|
+
};
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
// cli/commands/create.ts
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { execSync } from "child_process";
|
|
5
|
+
|
|
6
|
+
// cli/utils.ts
|
|
7
|
+
var c = {
|
|
8
|
+
reset: "\x1B[0m",
|
|
9
|
+
bold: "\x1B[1m",
|
|
10
|
+
dim: "\x1B[2m",
|
|
11
|
+
red: "\x1B[31m",
|
|
12
|
+
green: "\x1B[32m",
|
|
13
|
+
yellow: "\x1B[33m",
|
|
14
|
+
blue: "\x1B[34m",
|
|
15
|
+
magenta: "\x1B[35m",
|
|
16
|
+
cyan: "\x1B[36m",
|
|
17
|
+
white: "\x1B[37m",
|
|
18
|
+
gray: "\x1B[90m"
|
|
19
|
+
};
|
|
20
|
+
var log = {
|
|
21
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
22
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
23
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
24
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
25
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
26
|
+
blank: () => console.log("")
|
|
27
|
+
};
|
|
28
|
+
function banner() {
|
|
29
|
+
console.log("");
|
|
30
|
+
console.log(
|
|
31
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v0.1.0${c.reset}`
|
|
32
|
+
);
|
|
33
|
+
console.log(
|
|
34
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
35
|
+
);
|
|
36
|
+
console.log("");
|
|
37
|
+
}
|
|
38
|
+
function divider() {
|
|
39
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
40
|
+
}
|
|
41
|
+
async function select(question, options) {
|
|
42
|
+
const readline = await import("readline");
|
|
43
|
+
const rl = readline.createInterface({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout
|
|
46
|
+
});
|
|
47
|
+
return new Promise((resolve2) => {
|
|
48
|
+
console.log(`
|
|
49
|
+
${c.bold}${question}${c.reset}`);
|
|
50
|
+
options.forEach((opt, i) => {
|
|
51
|
+
console.log(` ${c.cyan}${i + 1}${c.reset}) ${opt}`);
|
|
52
|
+
});
|
|
53
|
+
rl.question(`
|
|
54
|
+
${c.dim}Enter choice [1-${options.length}]:${c.reset} `, (answer) => {
|
|
55
|
+
rl.close();
|
|
56
|
+
const idx = parseInt(answer, 10) - 1;
|
|
57
|
+
if (idx >= 0 && idx < options.length) {
|
|
58
|
+
resolve2(options[idx]);
|
|
59
|
+
} else {
|
|
60
|
+
resolve2(options[0]);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// cli/commands/create.ts
|
|
67
|
+
function getFrameworkRoot() {
|
|
68
|
+
const cliEntry = process.argv[1];
|
|
69
|
+
const cliDir = path.dirname(cliEntry);
|
|
70
|
+
return path.resolve(cliDir, "..");
|
|
71
|
+
}
|
|
72
|
+
function readProjectFile(relativePath) {
|
|
73
|
+
const root = getFrameworkRoot();
|
|
74
|
+
const bundledPath = path.join(root, "templates", relativePath);
|
|
75
|
+
if (fs.existsSync(bundledPath)) {
|
|
76
|
+
return fs.readFileSync(bundledPath, "utf-8");
|
|
77
|
+
}
|
|
78
|
+
const sourcePath = path.join(root, relativePath);
|
|
79
|
+
if (fs.existsSync(sourcePath)) {
|
|
80
|
+
return fs.readFileSync(sourcePath, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`Template file not found: ${relativePath}`);
|
|
83
|
+
}
|
|
84
|
+
function pkgJson(name) {
|
|
85
|
+
return JSON.stringify(
|
|
86
|
+
{
|
|
87
|
+
name,
|
|
88
|
+
version: "0.1.0",
|
|
89
|
+
description: `${name} \u2014 A Blumen app`,
|
|
90
|
+
type: "module",
|
|
91
|
+
scripts: {
|
|
92
|
+
routes: "tsx scripts/generate-routes.ts",
|
|
93
|
+
dev: 'npm run routes && concurrently "npm run dev:client" "npm run dev:ssr" "npm run dev:go"',
|
|
94
|
+
"dev:client": "webpack serve --mode development",
|
|
95
|
+
"dev:ssr": "NODE_ENV=development tsx watch node-ssr/server.ts",
|
|
96
|
+
"dev:go": "go run go-server/main.go",
|
|
97
|
+
build: "npm run routes && npm run build:client && npm run build:ssr",
|
|
98
|
+
"build:client": "webpack --mode production",
|
|
99
|
+
"build:ssr": "esbuild node-ssr/server.ts --bundle --platform=node --format=esm --outfile=dist/ssr-server.js --external:react --external:react-dom",
|
|
100
|
+
start: "node dist/ssr-server.js",
|
|
101
|
+
clean: "rm -rf dist static/js/bundle.js"
|
|
102
|
+
},
|
|
103
|
+
dependencies: {
|
|
104
|
+
"lucide-react": "^1.14.0",
|
|
105
|
+
react: "^18.2.0",
|
|
106
|
+
"react-dom": "^18.2.0"
|
|
107
|
+
},
|
|
108
|
+
devDependencies: {
|
|
109
|
+
"@babel/core": "^7.24.0",
|
|
110
|
+
"@babel/preset-env": "^7.24.0",
|
|
111
|
+
"@babel/preset-react": "^7.24.0",
|
|
112
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
113
|
+
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.15",
|
|
114
|
+
"@types/node": "^20.10.0",
|
|
115
|
+
"@types/react": "^18.2.0",
|
|
116
|
+
"@types/react-dom": "^18.2.0",
|
|
117
|
+
"babel-loader": "^9.2.1",
|
|
118
|
+
concurrently: "^8.2.2",
|
|
119
|
+
esbuild: "^0.19.0",
|
|
120
|
+
"react-refresh": "^0.14.2",
|
|
121
|
+
"ts-loader": "^9.5.1",
|
|
122
|
+
tsx: "^4.6.0",
|
|
123
|
+
typescript: "^5.3.0",
|
|
124
|
+
webpack: "^5.89.0",
|
|
125
|
+
"webpack-cli": "^5.1.4",
|
|
126
|
+
"webpack-dev-server": "^5.2.0"
|
|
127
|
+
},
|
|
128
|
+
engines: { node: ">=18.0.0" }
|
|
129
|
+
},
|
|
130
|
+
null,
|
|
131
|
+
" "
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
var TSCONFIG = `{
|
|
135
|
+
"compilerOptions": {
|
|
136
|
+
"target": "ES2020",
|
|
137
|
+
"module": "ESNext",
|
|
138
|
+
"moduleResolution": "bundler",
|
|
139
|
+
"jsx": "react-jsx",
|
|
140
|
+
"strict": true,
|
|
141
|
+
"esModuleInterop": true,
|
|
142
|
+
"skipLibCheck": true,
|
|
143
|
+
"forceConsistentCasingInFileNames": true,
|
|
144
|
+
"resolveJsonModule": true,
|
|
145
|
+
"outDir": "./dist",
|
|
146
|
+
"rootDir": "."
|
|
147
|
+
},
|
|
148
|
+
"include": ["app/**/*", "node-ssr/**/*", "scripts/**/*"],
|
|
149
|
+
"exclude": ["node_modules", "dist", "static"]
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
var NOT_FOUND_PAGE = `import React from "react";
|
|
153
|
+
|
|
154
|
+
const NotFoundPage = () => (
|
|
155
|
+
<div style={{ textAlign: "center", padding: "4rem 2rem", color: "#e2e8f0" }}>
|
|
156
|
+
<h1 style={{ fontSize: "2rem", color: "#a855f7" }}>404</h1>
|
|
157
|
+
<p style={{ marginTop: "1rem", color: "#94a3b8" }}>Page not found.</p>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
export default NotFoundPage;
|
|
162
|
+
`;
|
|
163
|
+
var DEFAULT_APP = `import React from "react";
|
|
164
|
+
|
|
165
|
+
export function DefaultApp({ Component, pageProps }: any) {
|
|
166
|
+
return <Component {...pageProps} />;
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
var DEFAULT_DOCUMENT = `import React from "react";
|
|
170
|
+
|
|
171
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
172
|
+
const BUNDLE_SRC = isDev
|
|
173
|
+
? "http://localhost:3100/static/js/bundle.js"
|
|
174
|
+
: "/static/js/bundle.js";
|
|
175
|
+
|
|
176
|
+
export function DefaultDocument({ children, initialProps }: any) {
|
|
177
|
+
return (
|
|
178
|
+
<html lang="en">
|
|
179
|
+
<head>
|
|
180
|
+
<meta charSet="UTF-8" />
|
|
181
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
182
|
+
<title>Blumen App</title>
|
|
183
|
+
</head>
|
|
184
|
+
<body>
|
|
185
|
+
<div id="root">{children}</div>
|
|
186
|
+
<script
|
|
187
|
+
id="ssr-props"
|
|
188
|
+
type="application/json"
|
|
189
|
+
dangerouslySetInnerHTML={{
|
|
190
|
+
__html: JSON.stringify(initialProps).replace(/</g, '\\\\u003c')
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
<script src={BUNDLE_SRC} defer></script>
|
|
194
|
+
</body>
|
|
195
|
+
</html>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
var WEBPACK_CONFIG = [
|
|
200
|
+
"import path from 'path';",
|
|
201
|
+
"import { fileURLToPath } from 'url';",
|
|
202
|
+
"import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';",
|
|
203
|
+
"",
|
|
204
|
+
"const __filename = fileURLToPath(import.meta.url);",
|
|
205
|
+
"const __dirname = path.dirname(__filename);",
|
|
206
|
+
"const HMR_PORT = 3100;",
|
|
207
|
+
"",
|
|
208
|
+
"const config = (env, argv) => {",
|
|
209
|
+
" const isDev = argv.mode === 'development';",
|
|
210
|
+
" return {",
|
|
211
|
+
" entry: './app/client/entry.tsx',",
|
|
212
|
+
" output: {",
|
|
213
|
+
" filename: 'bundle.js',",
|
|
214
|
+
" path: path.resolve(__dirname, 'static', 'js'),",
|
|
215
|
+
" publicPath: isDev ? `http://localhost:${HMR_PORT}/static/js/` : '/static/js/',",
|
|
216
|
+
" },",
|
|
217
|
+
" module: {",
|
|
218
|
+
" rules: [{",
|
|
219
|
+
" test: /\\.tsx?$/,",
|
|
220
|
+
" use: {",
|
|
221
|
+
" loader: 'babel-loader',",
|
|
222
|
+
" options: {",
|
|
223
|
+
" presets: [",
|
|
224
|
+
" ['@babel/preset-env', { targets: { esmodules: true } }],",
|
|
225
|
+
" ['@babel/preset-react', { runtime: 'automatic' }],",
|
|
226
|
+
" '@babel/preset-typescript',",
|
|
227
|
+
" ],",
|
|
228
|
+
" plugins: isDev ? ['react-refresh/babel'] : [],",
|
|
229
|
+
" },",
|
|
230
|
+
" },",
|
|
231
|
+
" exclude: /node_modules/,",
|
|
232
|
+
" }],",
|
|
233
|
+
" },",
|
|
234
|
+
" resolve: { extensions: ['.tsx', '.ts', '.js'] },",
|
|
235
|
+
" mode: isDev ? 'development' : 'production',",
|
|
236
|
+
" devtool: isDev ? 'eval-source-map' : 'source-map',",
|
|
237
|
+
" optimization: { minimize: !isDev },",
|
|
238
|
+
" plugins: isDev ? [new ReactRefreshWebpackPlugin({ overlay: false })] : [],",
|
|
239
|
+
" ...(isDev && {",
|
|
240
|
+
" devServer: {",
|
|
241
|
+
" port: HMR_PORT,",
|
|
242
|
+
" hot: true,",
|
|
243
|
+
" headers: { 'Access-Control-Allow-Origin': '*' },",
|
|
244
|
+
" allowedHosts: 'all',",
|
|
245
|
+
" static: false,",
|
|
246
|
+
" client: { webSocketURL: `ws://localhost:${HMR_PORT}/ws` },",
|
|
247
|
+
" },",
|
|
248
|
+
" }),",
|
|
249
|
+
" };",
|
|
250
|
+
"};",
|
|
251
|
+
"",
|
|
252
|
+
"export default config;",
|
|
253
|
+
""
|
|
254
|
+
].join("\n");
|
|
255
|
+
var LINK_TSX = `import React, { useContext } from "react";
|
|
256
|
+
import { RouterContextRef } from "./RouterContext";
|
|
257
|
+
|
|
258
|
+
interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
259
|
+
href: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isExternal(href: string): boolean {
|
|
263
|
+
return /^https?:\\/\\//.test(href) || href.startsWith("mailto:") || href.startsWith("tel:");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isModifiedClick(e: React.MouseEvent): boolean {
|
|
267
|
+
return e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function Link({ href, children, onClick, target, ...rest }: LinkProps) {
|
|
271
|
+
const ctx = useContext(RouterContextRef);
|
|
272
|
+
if (!ctx) return <a href={href} target={target} {...rest}>{children}</a>;
|
|
273
|
+
|
|
274
|
+
const { navigate } = ctx;
|
|
275
|
+
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
|
276
|
+
if (onClick) onClick(e);
|
|
277
|
+
if (e.defaultPrevented) return;
|
|
278
|
+
if (isExternal(href) || target === "_blank" || isModifiedClick(e)) return;
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
navigate(href);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
return <a href={href} onClick={handleClick} target={target} {...rest}>{children}</a>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export default Link;
|
|
287
|
+
`;
|
|
288
|
+
function writeFile(base, relPath, content) {
|
|
289
|
+
const fullPath = path.join(base, relPath);
|
|
290
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
291
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
292
|
+
}
|
|
293
|
+
function getTemplateFiles(projectName) {
|
|
294
|
+
return [
|
|
295
|
+
// Generated config
|
|
296
|
+
["package.json", pkgJson(projectName)],
|
|
297
|
+
["tsconfig.json", TSCONFIG],
|
|
298
|
+
["webpack.config.js", WEBPACK_CONFIG],
|
|
299
|
+
// Embedded simple templates
|
|
300
|
+
["app/pages/NotFound.tsx", NOT_FOUND_PAGE],
|
|
301
|
+
["app/shared/DefaultApp.tsx", DEFAULT_APP],
|
|
302
|
+
["app/shared/DefaultDocument.tsx", DEFAULT_DOCUMENT],
|
|
303
|
+
["app/shared/Link.tsx", LINK_TSX],
|
|
304
|
+
// Complex files — copied from the framework source (avoids escaping hell)
|
|
305
|
+
["app/pages/Home.tsx", readProjectFile("app/pages/BlumenStarter.tsx")],
|
|
306
|
+
["app/shared/RouterContext.tsx", readProjectFile("app/shared/RouterContext.tsx")],
|
|
307
|
+
["app/shared/router.ts", readProjectFile("app/shared/router.ts")],
|
|
308
|
+
["app/client/entry.tsx", readProjectFile("app/client/entry.tsx")],
|
|
309
|
+
["node-ssr/server.ts", readProjectFile("node-ssr/server.ts")],
|
|
310
|
+
["go-server/main.go", readProjectFile("go-server/main.go")],
|
|
311
|
+
["scripts/generate-routes.ts", readProjectFile("scripts/generate-routes.ts")],
|
|
312
|
+
// Placeholder
|
|
313
|
+
["static/js/.gitkeep", ""]
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
async function create(projectName) {
|
|
317
|
+
banner();
|
|
318
|
+
log.info("Create a new Blumen project\n");
|
|
319
|
+
if (!projectName) {
|
|
320
|
+
log.error("Please provide a project name.");
|
|
321
|
+
console.log(
|
|
322
|
+
`
|
|
323
|
+
${c.dim}Usage:${c.reset} blumen create ${c.cyan}<project-name>${c.reset}
|
|
324
|
+
`
|
|
325
|
+
);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
const projectDir = path.resolve(process.cwd(), projectName);
|
|
329
|
+
if (fs.existsSync(projectDir)) {
|
|
330
|
+
log.error(`Directory ${c.bold}${projectName}${c.reset} already exists.`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
const pkgManager = await select("Which package manager do you want to use?", [
|
|
334
|
+
"npm",
|
|
335
|
+
"yarn",
|
|
336
|
+
"pnpm",
|
|
337
|
+
"bun"
|
|
338
|
+
]);
|
|
339
|
+
log.blank();
|
|
340
|
+
divider();
|
|
341
|
+
log.blank();
|
|
342
|
+
log.step(`Creating project in ${c.cyan}${projectName}${c.reset}...`);
|
|
343
|
+
const files = getTemplateFiles(projectName);
|
|
344
|
+
for (const [relPath, content] of files) {
|
|
345
|
+
writeFile(projectDir, relPath, content);
|
|
346
|
+
}
|
|
347
|
+
log.success(`${files.length} files written`);
|
|
348
|
+
log.step(`Installing dependencies with ${c.bold}${pkgManager}${c.reset}...`);
|
|
349
|
+
const installCmd = {
|
|
350
|
+
npm: "npm install",
|
|
351
|
+
yarn: "yarn",
|
|
352
|
+
pnpm: "pnpm install",
|
|
353
|
+
bun: "bun install"
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
execSync(installCmd[pkgManager], {
|
|
357
|
+
cwd: projectDir,
|
|
358
|
+
stdio: "inherit"
|
|
359
|
+
});
|
|
360
|
+
log.success("Dependencies installed");
|
|
361
|
+
} catch {
|
|
362
|
+
log.warn(
|
|
363
|
+
"Could not install dependencies. Run the install command manually."
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
log.blank();
|
|
367
|
+
divider();
|
|
368
|
+
log.blank();
|
|
369
|
+
log.success(`${c.bold}Project created!${c.reset}`);
|
|
370
|
+
log.blank();
|
|
371
|
+
const runCmd = {
|
|
372
|
+
npm: "npm run dev",
|
|
373
|
+
yarn: "yarn dev",
|
|
374
|
+
pnpm: "pnpm dev",
|
|
375
|
+
bun: "bun dev"
|
|
376
|
+
};
|
|
377
|
+
console.log(` ${c.dim}Next steps:${c.reset}`);
|
|
378
|
+
console.log(` cd ${projectName}`);
|
|
379
|
+
console.log(` ${runCmd[pkgManager]}`);
|
|
380
|
+
log.blank();
|
|
381
|
+
}
|
|
382
|
+
export {
|
|
383
|
+
create
|
|
384
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// cli/commands/dev.ts
|
|
2
|
+
import { spawn, execSync } from "child_process";
|
|
3
|
+
|
|
4
|
+
// cli/utils.ts
|
|
5
|
+
var c = {
|
|
6
|
+
reset: "\x1B[0m",
|
|
7
|
+
bold: "\x1B[1m",
|
|
8
|
+
dim: "\x1B[2m",
|
|
9
|
+
red: "\x1B[31m",
|
|
10
|
+
green: "\x1B[32m",
|
|
11
|
+
yellow: "\x1B[33m",
|
|
12
|
+
blue: "\x1B[34m",
|
|
13
|
+
magenta: "\x1B[35m",
|
|
14
|
+
cyan: "\x1B[36m",
|
|
15
|
+
white: "\x1B[37m",
|
|
16
|
+
gray: "\x1B[90m"
|
|
17
|
+
};
|
|
18
|
+
var log = {
|
|
19
|
+
info: (msg) => console.log(` ${c.magenta}\u25CF${c.reset} ${msg}`),
|
|
20
|
+
success: (msg) => console.log(` ${c.green}\u2713${c.reset} ${msg}`),
|
|
21
|
+
error: (msg) => console.error(` ${c.red}\u2717${c.reset} ${msg}`),
|
|
22
|
+
warn: (msg) => console.log(` ${c.yellow}\u26A0${c.reset} ${msg}`),
|
|
23
|
+
step: (msg) => console.log(` ${c.dim}\u2192${c.reset} ${msg}`),
|
|
24
|
+
blank: () => console.log("")
|
|
25
|
+
};
|
|
26
|
+
function banner() {
|
|
27
|
+
console.log("");
|
|
28
|
+
console.log(
|
|
29
|
+
` ${c.magenta}${c.bold}\u{1F338} Blumen${c.reset} ${c.dim}v0.1.0${c.reset}`
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
` ${c.dim}The React framework powered by Go${c.reset}`
|
|
33
|
+
);
|
|
34
|
+
console.log("");
|
|
35
|
+
}
|
|
36
|
+
function divider() {
|
|
37
|
+
console.log(` ${c.dim}${"\u2500".repeat(48)}${c.reset}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// cli/commands/dev.ts
|
|
41
|
+
async function dev() {
|
|
42
|
+
banner();
|
|
43
|
+
log.step("Generating routes...");
|
|
44
|
+
try {
|
|
45
|
+
execSync("npx tsx scripts/generate-routes.ts", {
|
|
46
|
+
stdio: "pipe",
|
|
47
|
+
cwd: process.cwd()
|
|
48
|
+
});
|
|
49
|
+
log.success("Routes generated");
|
|
50
|
+
} catch {
|
|
51
|
+
log.error("Route generation failed");
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
divider();
|
|
55
|
+
log.info("Starting development servers...");
|
|
56
|
+
log.blank();
|
|
57
|
+
const services = [
|
|
58
|
+
{
|
|
59
|
+
name: "webpack",
|
|
60
|
+
label: "hmr",
|
|
61
|
+
color: c.magenta,
|
|
62
|
+
cmd: "npx",
|
|
63
|
+
args: ["webpack", "serve", "--mode", "development"],
|
|
64
|
+
readyPattern: /compiled successfully/
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "node-ssr",
|
|
68
|
+
label: "ssr",
|
|
69
|
+
color: c.cyan,
|
|
70
|
+
cmd: "npx",
|
|
71
|
+
args: ["tsx", "watch", "node-ssr/server.ts"],
|
|
72
|
+
env: { NODE_ENV: "development" },
|
|
73
|
+
readyPattern: /SSR server running/
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "go-server",
|
|
77
|
+
label: " go",
|
|
78
|
+
color: c.green,
|
|
79
|
+
cmd: "go",
|
|
80
|
+
args: ["run", "go-server/main.go"],
|
|
81
|
+
readyPattern: /Go server starting/
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
const children = [];
|
|
85
|
+
const ready = /* @__PURE__ */ new Set();
|
|
86
|
+
for (const svc of services) {
|
|
87
|
+
const child = spawn(svc.cmd, svc.args, {
|
|
88
|
+
cwd: process.cwd(),
|
|
89
|
+
env: { ...process.env, ...svc.env || {} },
|
|
90
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
91
|
+
shell: false
|
|
92
|
+
});
|
|
93
|
+
const prefix = ` ${svc.color}\u2502${c.reset} ${svc.color}${svc.label}${c.reset} `;
|
|
94
|
+
const handleOutput = (data) => {
|
|
95
|
+
const lines = data.toString().split("\n");
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
const trimmed = line.replace(/\r$/, "");
|
|
98
|
+
if (!trimmed)
|
|
99
|
+
continue;
|
|
100
|
+
console.log(`${prefix}${trimmed}`);
|
|
101
|
+
if (svc.readyPattern && svc.readyPattern.test(trimmed) && !ready.has(svc.name)) {
|
|
102
|
+
ready.add(svc.name);
|
|
103
|
+
if (ready.size === services.length) {
|
|
104
|
+
printReadyBanner();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
child.stdout?.on("data", handleOutput);
|
|
110
|
+
child.stderr?.on("data", handleOutput);
|
|
111
|
+
child.on("exit", (code) => {
|
|
112
|
+
if (code !== null && code !== 0) {
|
|
113
|
+
log.warn(`${svc.name} exited with code ${code}`);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
children.push(child);
|
|
117
|
+
}
|
|
118
|
+
function printReadyBanner() {
|
|
119
|
+
log.blank();
|
|
120
|
+
divider();
|
|
121
|
+
log.blank();
|
|
122
|
+
log.success(`${c.bold}Ready!${c.reset} All services are running.`);
|
|
123
|
+
log.blank();
|
|
124
|
+
console.log(
|
|
125
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}App${c.reset}: ${c.cyan}http://localhost:3000${c.reset}`
|
|
126
|
+
);
|
|
127
|
+
console.log(
|
|
128
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}SSR${c.reset}: ${c.dim}http://localhost:4000${c.reset}`
|
|
129
|
+
);
|
|
130
|
+
console.log(
|
|
131
|
+
` ${c.dim}\u279C${c.reset} ${c.bold}HMR${c.reset}: ${c.dim}http://localhost:3100${c.reset}`
|
|
132
|
+
);
|
|
133
|
+
log.blank();
|
|
134
|
+
console.log(
|
|
135
|
+
` ${c.dim}Press ${c.bold}Ctrl+C${c.reset}${c.dim} to stop all services.${c.reset}`
|
|
136
|
+
);
|
|
137
|
+
log.blank();
|
|
138
|
+
divider();
|
|
139
|
+
}
|
|
140
|
+
const shutdown = () => {
|
|
141
|
+
log.blank();
|
|
142
|
+
log.info("Shutting down...");
|
|
143
|
+
for (const child of children) {
|
|
144
|
+
if (!child.killed)
|
|
145
|
+
child.kill("SIGTERM");
|
|
146
|
+
}
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
for (const child of children) {
|
|
149
|
+
if (!child.killed)
|
|
150
|
+
child.kill("SIGKILL");
|
|
151
|
+
}
|
|
152
|
+
log.success("Stopped.");
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}, 3e3);
|
|
155
|
+
};
|
|
156
|
+
process.on("SIGINT", shutdown);
|
|
157
|
+
process.on("SIGTERM", shutdown);
|
|
158
|
+
await new Promise(() => {
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export {
|
|
162
|
+
dev
|
|
163
|
+
};
|