@ereo/cli 0.1.6
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 +169 -0
- package/dist/commands/build.d.ts +19 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/create.d.ts +17 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/db.d.ts +77 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/deploy.d.ts +45 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/dev.d.ts +18 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/start.d.ts +17 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2335 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2335 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
14
|
+
|
|
15
|
+
// src/commands/build.ts
|
|
16
|
+
var exports_build = {};
|
|
17
|
+
__export(exports_build, {
|
|
18
|
+
build: () => build
|
|
19
|
+
});
|
|
20
|
+
import { join as join2 } from "path";
|
|
21
|
+
import { build as bundlerBuild, printBuildReport } from "@ereo/bundler";
|
|
22
|
+
async function build(options = {}) {
|
|
23
|
+
const root = process.cwd();
|
|
24
|
+
console.log(`
|
|
25
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Production Build
|
|
26
|
+
`);
|
|
27
|
+
let config = {};
|
|
28
|
+
const configPath = join2(root, "ereo.config.ts");
|
|
29
|
+
try {
|
|
30
|
+
if (await Bun.file(configPath).exists()) {
|
|
31
|
+
const configModule = await import(configPath);
|
|
32
|
+
config = configModule.default || configModule;
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.warn("Could not load config:", error);
|
|
36
|
+
}
|
|
37
|
+
const configTarget = config.build?.target || "bun";
|
|
38
|
+
const bundlerTarget = ["bun", "node", "browser"].includes(configTarget) ? configTarget : "bun";
|
|
39
|
+
const buildOptions = {
|
|
40
|
+
root,
|
|
41
|
+
outDir: options.outDir || config.build?.outDir || ".ereo",
|
|
42
|
+
minify: options.minify ?? config.build?.minify ?? true,
|
|
43
|
+
sourcemap: options.sourcemap ?? config.build?.sourcemap ?? true,
|
|
44
|
+
target: bundlerTarget
|
|
45
|
+
};
|
|
46
|
+
console.log(` Target: ${buildOptions.target}`);
|
|
47
|
+
console.log(` Output: ${buildOptions.outDir}`);
|
|
48
|
+
console.log("");
|
|
49
|
+
const result = await bundlerBuild(buildOptions);
|
|
50
|
+
if (result.success) {
|
|
51
|
+
printBuildReport(result);
|
|
52
|
+
console.log(`
|
|
53
|
+
\x1B[32m\u2713\x1B[0m Build completed successfully
|
|
54
|
+
`);
|
|
55
|
+
} else {
|
|
56
|
+
console.error(`
|
|
57
|
+
\x1B[31m\u2717\x1B[0m Build failed
|
|
58
|
+
`);
|
|
59
|
+
if (result.errors) {
|
|
60
|
+
for (const error of result.errors) {
|
|
61
|
+
console.error(` ${error}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
var init_build = () => {};
|
|
68
|
+
|
|
69
|
+
// src/commands/dev.ts
|
|
70
|
+
import { join } from "path";
|
|
71
|
+
import {
|
|
72
|
+
createApp,
|
|
73
|
+
setupEnv
|
|
74
|
+
} from "@ereo/core";
|
|
75
|
+
import { initFileRouter } from "@ereo/router";
|
|
76
|
+
import { createServer } from "@ereo/server";
|
|
77
|
+
import {
|
|
78
|
+
createHMRServer,
|
|
79
|
+
createHMRWatcher,
|
|
80
|
+
createHMRWebSocket,
|
|
81
|
+
HMR_CLIENT_CODE,
|
|
82
|
+
ERROR_OVERLAY_SCRIPT
|
|
83
|
+
} from "@ereo/bundler";
|
|
84
|
+
async function dev(options = {}) {
|
|
85
|
+
const port = options.port || 3000;
|
|
86
|
+
const hostname = options.host || "localhost";
|
|
87
|
+
const root = process.cwd();
|
|
88
|
+
console.log(`
|
|
89
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Dev Server
|
|
90
|
+
`);
|
|
91
|
+
let config = {};
|
|
92
|
+
const configPath = join(root, "ereo.config.ts");
|
|
93
|
+
try {
|
|
94
|
+
if (await Bun.file(configPath).exists()) {
|
|
95
|
+
const configModule = await import(configPath);
|
|
96
|
+
config = configModule.default || configModule;
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.warn("Could not load config:", error);
|
|
100
|
+
}
|
|
101
|
+
if (config.env) {
|
|
102
|
+
console.log(" \x1B[2mLoading environment variables...\x1B[0m");
|
|
103
|
+
const envResult = await setupEnv(root, config.env, "development");
|
|
104
|
+
if (!envResult.valid) {
|
|
105
|
+
console.error(`
|
|
106
|
+
\x1B[31m\u2716\x1B[0m Environment validation failed
|
|
107
|
+
`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
console.log(` \x1B[32m\u2713\x1B[0m Loaded ${Object.keys(envResult.env).length} environment variables
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
const app = createApp({
|
|
114
|
+
config: {
|
|
115
|
+
...config,
|
|
116
|
+
server: {
|
|
117
|
+
port,
|
|
118
|
+
hostname,
|
|
119
|
+
development: true,
|
|
120
|
+
...config.server
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
const router = await initFileRouter({
|
|
125
|
+
routesDir: config.routesDir || "app/routes",
|
|
126
|
+
watch: true
|
|
127
|
+
});
|
|
128
|
+
await router.loadAllModules();
|
|
129
|
+
const hmr = createHMRServer();
|
|
130
|
+
const hmrWatcher = createHMRWatcher(hmr);
|
|
131
|
+
hmrWatcher.watch(join(root, "app"));
|
|
132
|
+
router.on("reload", async () => {
|
|
133
|
+
console.log("\x1B[33m\u27F3\x1B[0m Routes reloaded");
|
|
134
|
+
await router.loadAllModules();
|
|
135
|
+
hmr.reload();
|
|
136
|
+
});
|
|
137
|
+
router.on("change", (route) => {
|
|
138
|
+
console.log(`\x1B[33m\u27F3\x1B[0m ${route.path} changed`);
|
|
139
|
+
hmr.jsUpdate(route.file);
|
|
140
|
+
});
|
|
141
|
+
const server = createServer({
|
|
142
|
+
port,
|
|
143
|
+
hostname,
|
|
144
|
+
development: true,
|
|
145
|
+
logging: true,
|
|
146
|
+
websocket: createHMRWebSocket(hmr)
|
|
147
|
+
});
|
|
148
|
+
server.setApp(app);
|
|
149
|
+
server.setRouter(router);
|
|
150
|
+
const pluginRegistry = app.getPluginRegistry();
|
|
151
|
+
await pluginRegistry.registerAll(config.plugins || []);
|
|
152
|
+
const devServer = {
|
|
153
|
+
ws: {
|
|
154
|
+
send: (data) => {
|
|
155
|
+
if (data && typeof data === "object") {
|
|
156
|
+
hmr.send(data);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
on: (event, callback) => {}
|
|
160
|
+
},
|
|
161
|
+
restart: async () => {
|
|
162
|
+
console.log("\x1B[33m\u27F3\x1B[0m Restarting server...");
|
|
163
|
+
hmr.reload();
|
|
164
|
+
},
|
|
165
|
+
middlewares: [],
|
|
166
|
+
watcher: {
|
|
167
|
+
add: (path) => hmrWatcher.watch(path),
|
|
168
|
+
on: (event, callback) => {}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
await pluginRegistry.configureServer(devServer);
|
|
172
|
+
for (const middleware of devServer.middlewares) {
|
|
173
|
+
server.use(middleware);
|
|
174
|
+
}
|
|
175
|
+
server.use(async (request, context, next) => {
|
|
176
|
+
const url = new URL(request.url);
|
|
177
|
+
if (url.pathname === "/__hmr-client.js") {
|
|
178
|
+
return new Response(HMR_CLIENT_CODE, {
|
|
179
|
+
headers: { "Content-Type": "text/javascript" }
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const response = await next();
|
|
184
|
+
if (response.headers.get("Content-Type")?.includes("text/html")) {
|
|
185
|
+
let html = await response.text();
|
|
186
|
+
const scripts = `
|
|
187
|
+
<script src="/__hmr-client.js"></script>
|
|
188
|
+
${ERROR_OVERLAY_SCRIPT}
|
|
189
|
+
`;
|
|
190
|
+
html = html.replace("</body>", `${scripts}</body>`);
|
|
191
|
+
return new Response(html, {
|
|
192
|
+
status: response.status,
|
|
193
|
+
headers: response.headers
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return response;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
hmr.error(error instanceof Error ? error.message : String(error), error instanceof Error ? error.stack : undefined);
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
await server.start();
|
|
203
|
+
console.log(` \x1B[32m\u279C\x1B[0m Local: \x1B[36mhttp://${hostname}:${port}/\x1B[0m`);
|
|
204
|
+
if (options.open) {
|
|
205
|
+
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
206
|
+
Bun.spawn([opener, `http://${hostname}:${port}`]);
|
|
207
|
+
}
|
|
208
|
+
console.log(`
|
|
209
|
+
\x1B[2mpress h to show help\x1B[0m
|
|
210
|
+
`);
|
|
211
|
+
if (process.stdin.isTTY) {
|
|
212
|
+
process.stdin.setRawMode(true);
|
|
213
|
+
process.stdin.resume();
|
|
214
|
+
process.stdin.on("data", async (data) => {
|
|
215
|
+
const key = data.toString();
|
|
216
|
+
switch (key) {
|
|
217
|
+
case "h":
|
|
218
|
+
console.log(`
|
|
219
|
+
Shortcuts:`);
|
|
220
|
+
console.log(" r - Reload routes");
|
|
221
|
+
console.log(" c - Clear console");
|
|
222
|
+
console.log(" q - Quit");
|
|
223
|
+
console.log("");
|
|
224
|
+
break;
|
|
225
|
+
case "r":
|
|
226
|
+
console.log("\x1B[33m\u27F3\x1B[0m Reloading routes...");
|
|
227
|
+
await router.discoverRoutes();
|
|
228
|
+
await router.loadAllModules();
|
|
229
|
+
hmr.reload();
|
|
230
|
+
break;
|
|
231
|
+
case "c":
|
|
232
|
+
console.clear();
|
|
233
|
+
console.log(`
|
|
234
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Dev Server
|
|
235
|
+
`);
|
|
236
|
+
console.log(` \x1B[32m\u279C\x1B[0m Local: \x1B[36mhttp://${hostname}:${port}/\x1B[0m
|
|
237
|
+
`);
|
|
238
|
+
break;
|
|
239
|
+
case "q":
|
|
240
|
+
case "\x03":
|
|
241
|
+
console.log(`
|
|
242
|
+
Shutting down...
|
|
243
|
+
`);
|
|
244
|
+
server.stop();
|
|
245
|
+
hmrWatcher.stop();
|
|
246
|
+
process.exit(0);
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
process.on("SIGINT", () => {
|
|
252
|
+
console.log(`
|
|
253
|
+
Shutting down...
|
|
254
|
+
`);
|
|
255
|
+
server.stop();
|
|
256
|
+
hmrWatcher.stop();
|
|
257
|
+
process.exit(0);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/index.ts
|
|
262
|
+
init_build();
|
|
263
|
+
|
|
264
|
+
// src/commands/start.ts
|
|
265
|
+
import { join as join3 } from "path";
|
|
266
|
+
import { createApp as createApp2 } from "@ereo/core";
|
|
267
|
+
import { initFileRouter as initFileRouter2 } from "@ereo/router";
|
|
268
|
+
import { createServer as createServer2 } from "@ereo/server";
|
|
269
|
+
async function start(options = {}) {
|
|
270
|
+
const root = process.cwd();
|
|
271
|
+
const buildDir = join3(root, ".ereo");
|
|
272
|
+
console.log(`
|
|
273
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Production Server
|
|
274
|
+
`);
|
|
275
|
+
const manifestPath = join3(buildDir, "manifest.json");
|
|
276
|
+
if (!await Bun.file(manifestPath).exists()) {
|
|
277
|
+
console.error(" \x1B[31m\u2717\x1B[0m No build found. Run `ereo build` first.\n");
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
const manifest = await Bun.file(manifestPath).json();
|
|
281
|
+
let config = {};
|
|
282
|
+
const configPath = join3(root, "ereo.config.ts");
|
|
283
|
+
try {
|
|
284
|
+
if (await Bun.file(configPath).exists()) {
|
|
285
|
+
const configModule = await import(configPath);
|
|
286
|
+
config = configModule.default || configModule;
|
|
287
|
+
}
|
|
288
|
+
} catch {}
|
|
289
|
+
const port = options.port || config.server?.port || 3000;
|
|
290
|
+
const hostname = options.host || config.server?.hostname || "0.0.0.0";
|
|
291
|
+
const app = createApp2({
|
|
292
|
+
config: {
|
|
293
|
+
...config,
|
|
294
|
+
server: {
|
|
295
|
+
port,
|
|
296
|
+
hostname,
|
|
297
|
+
development: false
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const router = await initFileRouter2({
|
|
302
|
+
routesDir: config.routesDir || "app/routes",
|
|
303
|
+
watch: false
|
|
304
|
+
});
|
|
305
|
+
await router.loadAllModules();
|
|
306
|
+
const server = createServer2({
|
|
307
|
+
port,
|
|
308
|
+
hostname,
|
|
309
|
+
development: false,
|
|
310
|
+
logging: true,
|
|
311
|
+
static: {
|
|
312
|
+
root: join3(buildDir, "client"),
|
|
313
|
+
prefix: "/_ereo",
|
|
314
|
+
maxAge: 31536000,
|
|
315
|
+
immutable: true
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
server.setApp(app);
|
|
319
|
+
server.setRouter(router);
|
|
320
|
+
await server.start();
|
|
321
|
+
console.log(` \x1B[32m\u279C\x1B[0m Server running at \x1B[36mhttp://${hostname}:${port}/\x1B[0m
|
|
322
|
+
`);
|
|
323
|
+
process.on("SIGINT", () => {
|
|
324
|
+
console.log(`
|
|
325
|
+
Shutting down...
|
|
326
|
+
`);
|
|
327
|
+
server.stop();
|
|
328
|
+
process.exit(0);
|
|
329
|
+
});
|
|
330
|
+
process.on("SIGTERM", () => {
|
|
331
|
+
console.log(`
|
|
332
|
+
Shutting down...
|
|
333
|
+
`);
|
|
334
|
+
server.stop();
|
|
335
|
+
process.exit(0);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/create.ts
|
|
340
|
+
import { join as join4 } from "path";
|
|
341
|
+
import { mkdir } from "fs/promises";
|
|
342
|
+
async function create(projectName, options = {}) {
|
|
343
|
+
const template = options.template || "tailwind";
|
|
344
|
+
const typescript = options.typescript !== false;
|
|
345
|
+
console.log(`
|
|
346
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Create Project
|
|
347
|
+
`);
|
|
348
|
+
console.log(` Creating ${projectName} with ${template} template...
|
|
349
|
+
`);
|
|
350
|
+
const projectDir = join4(process.cwd(), projectName);
|
|
351
|
+
await mkdir(projectDir, { recursive: true });
|
|
352
|
+
await mkdir(join4(projectDir, "app/routes"), { recursive: true });
|
|
353
|
+
await mkdir(join4(projectDir, "app/components"), { recursive: true });
|
|
354
|
+
await mkdir(join4(projectDir, "app/middleware"), { recursive: true });
|
|
355
|
+
await mkdir(join4(projectDir, "public"), { recursive: true });
|
|
356
|
+
const files = generateTemplateFiles(template, typescript, projectName);
|
|
357
|
+
const sortedPaths = Object.keys(files).sort();
|
|
358
|
+
for (const path of sortedPaths) {
|
|
359
|
+
const content = files[path];
|
|
360
|
+
const fullPath = join4(projectDir, path);
|
|
361
|
+
await mkdir(join4(fullPath, ".."), { recursive: true });
|
|
362
|
+
await Bun.write(fullPath, content);
|
|
363
|
+
console.log(` \x1B[32m+\x1B[0m ${path}`);
|
|
364
|
+
}
|
|
365
|
+
console.log(`
|
|
366
|
+
\x1B[32m\u2713\x1B[0m Project created successfully!
|
|
367
|
+
`);
|
|
368
|
+
console.log(` Next steps:
|
|
369
|
+
`);
|
|
370
|
+
console.log(` cd ${projectName}`);
|
|
371
|
+
console.log(" bun install");
|
|
372
|
+
console.log(` bun run dev
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
function generateTemplateFiles(template, typescript, projectName) {
|
|
376
|
+
const ext = typescript ? "tsx" : "jsx";
|
|
377
|
+
const files = {};
|
|
378
|
+
const dependencies = {
|
|
379
|
+
"@ereo/core": "^0.1.0",
|
|
380
|
+
"@ereo/router": "^0.1.0",
|
|
381
|
+
"@ereo/server": "^0.1.0",
|
|
382
|
+
"@ereo/client": "^0.1.0",
|
|
383
|
+
"@ereo/data": "^0.1.0",
|
|
384
|
+
"@ereo/cli": "^0.1.0",
|
|
385
|
+
react: "^18.2.0",
|
|
386
|
+
"react-dom": "^18.2.0"
|
|
387
|
+
};
|
|
388
|
+
if (template === "tailwind") {
|
|
389
|
+
dependencies["@ereo/plugin-tailwind"] = "^0.1.0";
|
|
390
|
+
}
|
|
391
|
+
files["package.json"] = JSON.stringify({
|
|
392
|
+
name: projectName,
|
|
393
|
+
version: "0.1.0",
|
|
394
|
+
type: "module",
|
|
395
|
+
scripts: {
|
|
396
|
+
dev: "ereo dev",
|
|
397
|
+
build: "ereo build",
|
|
398
|
+
start: "ereo start"
|
|
399
|
+
},
|
|
400
|
+
dependencies,
|
|
401
|
+
devDependencies: typescript ? {
|
|
402
|
+
"@types/react": "^18.2.0",
|
|
403
|
+
"@types/react-dom": "^18.2.0",
|
|
404
|
+
typescript: "^5.4.0"
|
|
405
|
+
} : {}
|
|
406
|
+
}, null, 2);
|
|
407
|
+
if (typescript) {
|
|
408
|
+
files["tsconfig.json"] = JSON.stringify({
|
|
409
|
+
compilerOptions: {
|
|
410
|
+
target: "ESNext",
|
|
411
|
+
module: "ESNext",
|
|
412
|
+
moduleResolution: "bundler",
|
|
413
|
+
jsx: "react-jsx",
|
|
414
|
+
strict: true,
|
|
415
|
+
esModuleInterop: true,
|
|
416
|
+
skipLibCheck: true,
|
|
417
|
+
forceConsistentCasingInFileNames: true,
|
|
418
|
+
types: ["bun-types"]
|
|
419
|
+
},
|
|
420
|
+
include: ["app/**/*", "ereo.config.ts"]
|
|
421
|
+
}, null, 2);
|
|
422
|
+
}
|
|
423
|
+
files[`ereo.config.${typescript ? "ts" : "js"}`] = generateEreoConfig(template);
|
|
424
|
+
files[".env"] = generateEnvFile();
|
|
425
|
+
files[".env.example"] = generateEnvFile();
|
|
426
|
+
files[`app/routes/_layout.${ext}`] = generateRootLayout(template, typescript);
|
|
427
|
+
files[`app/entry.client.${ext}`] = generateClientEntry(typescript);
|
|
428
|
+
files[`app/routes/index.${ext}`] = generateIndexPage(template, typescript);
|
|
429
|
+
files[`app/routes/about.${ext}`] = generateAboutPage(template, typescript);
|
|
430
|
+
files[`app/routes/contact.${ext}`] = generateContactPage(template, typescript);
|
|
431
|
+
files[`app/components/Counter.${ext}`] = generateCounterComponent(template, typescript);
|
|
432
|
+
files[`app/middleware/logger.${typescript ? "ts" : "js"}`] = generateLoggerMiddleware(typescript);
|
|
433
|
+
files[`app/routes/_error.${ext}`] = generateErrorBoundary(template, typescript);
|
|
434
|
+
files[`app/routes/blog/[slug].${ext}`] = generateDynamicRoute(template, typescript);
|
|
435
|
+
files[`app/routes/blog/index.${ext}`] = generateBlogIndex(template, typescript);
|
|
436
|
+
files[`app/routes/api/health.${typescript ? "ts" : "js"}`] = generateApiRoute(typescript);
|
|
437
|
+
if (template === "tailwind") {
|
|
438
|
+
files["tailwind.config.js"] = generateTailwindConfig();
|
|
439
|
+
files["app/globals.css"] = generateGlobalCSS();
|
|
440
|
+
}
|
|
441
|
+
files[".gitignore"] = generateGitignore();
|
|
442
|
+
return files;
|
|
443
|
+
}
|
|
444
|
+
function generateEreoConfig(template) {
|
|
445
|
+
const tailwindImport = template === "tailwind" ? `import tailwind from '@ereo/plugin-tailwind';
|
|
446
|
+
` : "";
|
|
447
|
+
const tailwindPlugin = template === "tailwind" ? " tailwind()," : "";
|
|
448
|
+
return `import { defineConfig } from '@ereo/core';
|
|
449
|
+
${tailwindImport}
|
|
450
|
+
export default defineConfig({
|
|
451
|
+
server: {
|
|
452
|
+
port: 3000,
|
|
453
|
+
},
|
|
454
|
+
build: {
|
|
455
|
+
target: 'bun',
|
|
456
|
+
},
|
|
457
|
+
plugins: [
|
|
458
|
+
${tailwindPlugin}
|
|
459
|
+
],
|
|
460
|
+
});
|
|
461
|
+
`.trim();
|
|
462
|
+
}
|
|
463
|
+
function generateEnvFile() {
|
|
464
|
+
return `# Environment Variables
|
|
465
|
+
# Prefix with EREO_PUBLIC_ to expose to the client
|
|
466
|
+
|
|
467
|
+
# Server-only (never sent to browser)
|
|
468
|
+
DATABASE_URL=postgresql://localhost:5432/mydb
|
|
469
|
+
API_SECRET=your-secret-key
|
|
470
|
+
|
|
471
|
+
# Public (available in client code)
|
|
472
|
+
EREO_PUBLIC_APP_NAME=EreoJS App
|
|
473
|
+
EREO_PUBLIC_API_URL=http://localhost:3000/api
|
|
474
|
+
`.trim();
|
|
475
|
+
}
|
|
476
|
+
function generateRootLayout(template, typescript) {
|
|
477
|
+
const imports = typescript ? `import type { ReactNode } from 'react';
|
|
478
|
+
import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
|
|
479
|
+
const propsType = typescript ? ": { children: ReactNode }" : "";
|
|
480
|
+
const tailwindStyles = template === "tailwind";
|
|
481
|
+
const navClasses = tailwindStyles ? ' className="flex gap-4 p-4 border-b border-gray-200 dark:border-gray-700"' : "";
|
|
482
|
+
const linkClasses = tailwindStyles ? ' className="text-blue-600 hover:text-blue-800 dark:text-blue-400"' : "";
|
|
483
|
+
const bodyClasses = tailwindStyles ? ' className="min-h-screen bg-white dark:bg-gray-900 text-gray-900 dark:text-white"' : "";
|
|
484
|
+
const stylesheet = tailwindStyles ? `
|
|
485
|
+
<link rel="stylesheet" href="/app/globals.css" />` : "";
|
|
486
|
+
return `${imports}
|
|
487
|
+
|
|
488
|
+
export default function RootLayout({ children }${propsType}) {
|
|
489
|
+
return (
|
|
490
|
+
<html lang="en">
|
|
491
|
+
<head>
|
|
492
|
+
<meta charSet="utf-8" />
|
|
493
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />${stylesheet}
|
|
494
|
+
</head>
|
|
495
|
+
<body${bodyClasses}>
|
|
496
|
+
<nav${navClasses}>
|
|
497
|
+
<Link to="/"${linkClasses}>Home</Link>
|
|
498
|
+
<Link to="/about"${linkClasses}>About</Link>
|
|
499
|
+
<Link to="/blog"${linkClasses}>Blog</Link>
|
|
500
|
+
<Link to="/contact"${linkClasses}>Contact</Link>
|
|
501
|
+
</nav>
|
|
502
|
+
{children}
|
|
503
|
+
{/* Client-side hydration script - bundled by EreoJS */}
|
|
504
|
+
<script type="module" src="/@ereo/client-entry.js" />
|
|
505
|
+
</body>
|
|
506
|
+
</html>
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
`.trim();
|
|
510
|
+
}
|
|
511
|
+
function generateClientEntry(typescript) {
|
|
512
|
+
return `/**
|
|
513
|
+
* Client Entry Point
|
|
514
|
+
*
|
|
515
|
+
* This file initializes the client-side runtime:
|
|
516
|
+
* - Hydrates island components
|
|
517
|
+
* - Sets up client-side navigation
|
|
518
|
+
* - Enables link prefetching
|
|
519
|
+
*/
|
|
520
|
+
import { initClient } from '@ereo/client';
|
|
521
|
+
|
|
522
|
+
// Initialize the EreoJS client runtime
|
|
523
|
+
initClient();
|
|
524
|
+
|
|
525
|
+
// You can also manually hydrate specific islands:
|
|
526
|
+
// import { hydrateIslands } from '@ereo/client';
|
|
527
|
+
// hydrateIslands();
|
|
528
|
+
`.trim();
|
|
529
|
+
}
|
|
530
|
+
function generateIndexPage(template, typescript) {
|
|
531
|
+
const imports = typescript ? `import type { LoaderArgs, MetaArgs, RouteConfig } from '@ereo/core';
|
|
532
|
+
import { Counter } from '../components/Counter';` : `import { Counter } from '../components/Counter';`;
|
|
533
|
+
const loaderType = typescript ? ": LoaderArgs" : "";
|
|
534
|
+
const loaderDataType = `{ message: string; timestamp: string; visitors: number }`;
|
|
535
|
+
const metaType = typescript ? `: MetaArgs<${loaderDataType}>` : "";
|
|
536
|
+
const tailwindStyles = template === "tailwind";
|
|
537
|
+
const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
|
|
538
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
|
|
539
|
+
const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 mb-2"' : "";
|
|
540
|
+
const sectionClasses = tailwindStyles ? ' className="mt-8 p-6 border border-gray-200 dark:border-gray-700 rounded-lg"' : "";
|
|
541
|
+
const h2Classes = tailwindStyles ? ' className="text-xl font-semibold mb-4"' : "";
|
|
542
|
+
const configType = typescript ? ": RouteConfig" : "";
|
|
543
|
+
return `${imports}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Route Configuration
|
|
547
|
+
*
|
|
548
|
+
* Export a config object to configure middleware, caching,
|
|
549
|
+
* rendering mode, and other route-level settings.
|
|
550
|
+
*/
|
|
551
|
+
export const config${configType} = {
|
|
552
|
+
// Apply middleware to this route
|
|
553
|
+
middleware: ['logger'],
|
|
554
|
+
// Cache configuration
|
|
555
|
+
cache: {
|
|
556
|
+
edge: {
|
|
557
|
+
maxAge: 60,
|
|
558
|
+
staleWhileRevalidate: 300,
|
|
559
|
+
},
|
|
560
|
+
data: {
|
|
561
|
+
tags: ['homepage', 'content'],
|
|
562
|
+
},
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Loader - Server-side data fetching
|
|
568
|
+
*
|
|
569
|
+
* Runs on the server for every request. Use context.cache
|
|
570
|
+
* for explicit cache control with tagged invalidation.
|
|
571
|
+
*/
|
|
572
|
+
export async function loader({ request, params, context }${loaderType}) {
|
|
573
|
+
// Access environment variables
|
|
574
|
+
const appName = context.env.EREO_PUBLIC_APP_NAME || 'EreoJS App';
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
message: \`Welcome to \${appName}!\`,
|
|
578
|
+
timestamp: new Date().toISOString(),
|
|
579
|
+
visitors: Math.floor(Math.random() * 1000),
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Meta - Dynamic SEO metadata
|
|
585
|
+
*
|
|
586
|
+
* Generate meta tags based on loader data.
|
|
587
|
+
*/
|
|
588
|
+
export function meta({ data }${metaType}) {
|
|
589
|
+
return [
|
|
590
|
+
{ title: data.message },
|
|
591
|
+
{ name: 'description', content: 'A blazing fast React framework built on Bun' },
|
|
592
|
+
{ property: 'og:title', content: data.message },
|
|
593
|
+
];
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Page Component
|
|
598
|
+
*
|
|
599
|
+
* Receives loaderData from the loader function.
|
|
600
|
+
* For client components, use useLoaderData() hook from @ereo/client.
|
|
601
|
+
*/
|
|
602
|
+
export default function HomePage({ loaderData }${typescript ? `: { loaderData: ${loaderDataType} }` : ""}) {
|
|
603
|
+
return (
|
|
604
|
+
<main${mainClasses}>
|
|
605
|
+
<h1${h1Classes}>
|
|
606
|
+
{loaderData.message}
|
|
607
|
+
</h1>
|
|
608
|
+
<p${pClasses}>
|
|
609
|
+
Server time: {loaderData.timestamp}
|
|
610
|
+
</p>
|
|
611
|
+
<p${pClasses}>
|
|
612
|
+
Today's visitors: {loaderData.visitors}
|
|
613
|
+
</p>
|
|
614
|
+
|
|
615
|
+
{/* Islands Architecture Example */}
|
|
616
|
+
<section${sectionClasses}>
|
|
617
|
+
<h2${h2Classes}>Interactive Island</h2>
|
|
618
|
+
<p${pClasses}>
|
|
619
|
+
This counter is an "island" - only this component hydrates on the client.
|
|
620
|
+
The rest of the page stays static HTML with zero JavaScript.
|
|
621
|
+
</p>
|
|
622
|
+
{/* client:load hydrates immediately */}
|
|
623
|
+
<Counter client:load initialCount={0} />
|
|
624
|
+
</section>
|
|
625
|
+
</main>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
`.trim();
|
|
629
|
+
}
|
|
630
|
+
function generateAboutPage(template, typescript) {
|
|
631
|
+
const imports = typescript ? `import type { MetaFunction } from '@ereo/core';` : "";
|
|
632
|
+
const tailwindStyles = template === "tailwind";
|
|
633
|
+
const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
|
|
634
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
|
|
635
|
+
const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 max-w-2xl text-center"' : "";
|
|
636
|
+
const ulClasses = tailwindStyles ? ' className="mt-6 space-y-2 text-left"' : "";
|
|
637
|
+
const liClasses = tailwindStyles ? ' className="flex items-center gap-2"' : "";
|
|
638
|
+
const metaExport = typescript ? `
|
|
639
|
+
/**
|
|
640
|
+
* Static meta tags for this page
|
|
641
|
+
*/
|
|
642
|
+
export const meta: MetaFunction = () => {
|
|
643
|
+
return [
|
|
644
|
+
{ title: 'About - EreoJS App' },
|
|
645
|
+
{ name: 'description', content: 'Learn about the EreoJS framework' },
|
|
646
|
+
];
|
|
647
|
+
};
|
|
648
|
+
` : `
|
|
649
|
+
/**
|
|
650
|
+
* Static meta tags for this page
|
|
651
|
+
*/
|
|
652
|
+
export function meta() {
|
|
653
|
+
return [
|
|
654
|
+
{ title: 'About - EreoJS App' },
|
|
655
|
+
{ name: 'description', content: 'Learn about the EreoJS framework' },
|
|
656
|
+
];
|
|
657
|
+
}
|
|
658
|
+
`;
|
|
659
|
+
return `${imports}
|
|
660
|
+
${metaExport}
|
|
661
|
+
/**
|
|
662
|
+
* About Page - Static content (no loader needed)
|
|
663
|
+
*
|
|
664
|
+
* Pages without loaders are rendered as static HTML.
|
|
665
|
+
*/
|
|
666
|
+
export default function AboutPage() {
|
|
667
|
+
return (
|
|
668
|
+
<main${mainClasses}>
|
|
669
|
+
<h1${h1Classes}>
|
|
670
|
+
About EreoJS
|
|
671
|
+
</h1>
|
|
672
|
+
<p${pClasses}>
|
|
673
|
+
EreoJS is a React fullstack framework built on Bun, designed for
|
|
674
|
+
simplicity and performance. It features islands architecture for
|
|
675
|
+
minimal JavaScript and explicit caching for predictable behavior.
|
|
676
|
+
</p>
|
|
677
|
+
<ul${ulClasses}>
|
|
678
|
+
<li${liClasses}>
|
|
679
|
+
<span>5-6x faster than Node.js</span>
|
|
680
|
+
</li>
|
|
681
|
+
<li${liClasses}>
|
|
682
|
+
<span>Islands architecture for minimal JS</span>
|
|
683
|
+
</li>
|
|
684
|
+
<li${liClasses}>
|
|
685
|
+
<span>One unified loader pattern</span>
|
|
686
|
+
</li>
|
|
687
|
+
<li${liClasses}>
|
|
688
|
+
<span>Explicit tagged cache invalidation</span>
|
|
689
|
+
</li>
|
|
690
|
+
</ul>
|
|
691
|
+
</main>
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
`.trim();
|
|
695
|
+
}
|
|
696
|
+
function generateContactPage(template, typescript) {
|
|
697
|
+
const imports = typescript ? `import type { ActionArgs, LoaderArgs, RouteConfig } from '@ereo/core';
|
|
698
|
+
import { json } from '@ereo/data';
|
|
699
|
+
import { Form, useActionData, useNavigation } from '@ereo/client';` : `import { json } from '@ereo/data';
|
|
700
|
+
import { Form, useActionData, useNavigation } from '@ereo/client';`;
|
|
701
|
+
const loaderType = typescript ? ": LoaderArgs" : "";
|
|
702
|
+
const actionType = typescript ? ": ActionArgs" : "";
|
|
703
|
+
const propsType = typescript ? ": { loaderData: { csrfToken: string } }" : "";
|
|
704
|
+
const actionDataType = typescript ? `
|
|
705
|
+
interface ActionData {
|
|
706
|
+
success: boolean;
|
|
707
|
+
message?: string;
|
|
708
|
+
errors?: Record<string, string>;
|
|
709
|
+
}` : "";
|
|
710
|
+
const tailwindStyles = template === "tailwind";
|
|
711
|
+
const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
|
|
712
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
|
|
713
|
+
const formClasses = tailwindStyles ? ' className="w-full max-w-md space-y-4"' : "";
|
|
714
|
+
const labelClasses = tailwindStyles ? ' className="block text-sm font-medium mb-1"' : "";
|
|
715
|
+
const inputClasses = tailwindStyles ? ' className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800"' : "";
|
|
716
|
+
const textareaClasses = tailwindStyles ? ' className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 h-32"' : "";
|
|
717
|
+
const buttonClasses = tailwindStyles ? ' className="w-full px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"' : "";
|
|
718
|
+
const errorClasses = tailwindStyles ? ' className="text-red-600 dark:text-red-400 text-sm mt-1"' : "";
|
|
719
|
+
const successClasses = tailwindStyles ? ' className="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 p-4 rounded-md mb-4"' : "";
|
|
720
|
+
return `${imports}
|
|
721
|
+
${actionDataType}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Route Configuration
|
|
725
|
+
*/
|
|
726
|
+
export const config${typescript ? ": RouteConfig" : ""} = {
|
|
727
|
+
// Progressive enhancement - form works without JS
|
|
728
|
+
progressive: {
|
|
729
|
+
forms: {
|
|
730
|
+
fallback: 'server',
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Loader - Provide CSRF token for form security
|
|
737
|
+
*/
|
|
738
|
+
export async function loader({ context }${loaderType}) {
|
|
739
|
+
return {
|
|
740
|
+
csrfToken: crypto.randomUUID(),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* Action - Handle form submission
|
|
746
|
+
*
|
|
747
|
+
* Actions handle POST/PUT/DELETE requests.
|
|
748
|
+
* Use json() for responses, redirect() for redirects.
|
|
749
|
+
*/
|
|
750
|
+
export async function action({ request, context }${actionType})${typescript ? ": Promise<Response>" : ""} {
|
|
751
|
+
const formData = await request.formData();
|
|
752
|
+
|
|
753
|
+
const name = formData.get('name') as string;
|
|
754
|
+
const email = formData.get('email') as string;
|
|
755
|
+
const message = formData.get('message') as string;
|
|
756
|
+
|
|
757
|
+
// Validate
|
|
758
|
+
const errors${typescript ? ": Record<string, string>" : ""} = {};
|
|
759
|
+
if (!name || name.length < 2) {
|
|
760
|
+
errors.name = 'Name must be at least 2 characters';
|
|
761
|
+
}
|
|
762
|
+
if (!email || !email.includes('@')) {
|
|
763
|
+
errors.email = 'Please enter a valid email';
|
|
764
|
+
}
|
|
765
|
+
if (!message || message.length < 10) {
|
|
766
|
+
errors.message = 'Message must be at least 10 characters';
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (Object.keys(errors).length > 0) {
|
|
770
|
+
return json({ success: false, errors }, { status: 400 });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Process the submission (e.g., send email, save to DB)
|
|
774
|
+
console.log('Contact form submitted:', { name, email, message });
|
|
775
|
+
|
|
776
|
+
// Return success or redirect
|
|
777
|
+
return json({ success: true, message: 'Thank you for your message!' });
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Contact Page with Enhanced Form
|
|
782
|
+
*
|
|
783
|
+
* Uses the Form component from @ereo/client for:
|
|
784
|
+
* - Automatic loading states
|
|
785
|
+
* - Client-side validation feedback
|
|
786
|
+
* - Progressive enhancement (works without JS)
|
|
787
|
+
*
|
|
788
|
+
* The useActionData hook provides access to the action response.
|
|
789
|
+
* The useNavigation hook provides loading state.
|
|
790
|
+
*/
|
|
791
|
+
export default function ContactPage({ loaderData }${propsType}) {
|
|
792
|
+
// Get action response data (available after form submission)
|
|
793
|
+
const actionData = useActionData${typescript ? "<ActionData>" : ""}();
|
|
794
|
+
// Get navigation state for loading indicator
|
|
795
|
+
const navigation = useNavigation();
|
|
796
|
+
const isSubmitting = navigation.state === 'submitting';
|
|
797
|
+
|
|
798
|
+
return (
|
|
799
|
+
<main${mainClasses}>
|
|
800
|
+
<h1${h1Classes}>Contact Us</h1>
|
|
801
|
+
|
|
802
|
+
{actionData?.success && (
|
|
803
|
+
<div${successClasses}>
|
|
804
|
+
{actionData.message}
|
|
805
|
+
</div>
|
|
806
|
+
)}
|
|
807
|
+
|
|
808
|
+
<Form method="post"${formClasses}>
|
|
809
|
+
<input type="hidden" name="csrf" value={loaderData.csrfToken} />
|
|
810
|
+
|
|
811
|
+
<div>
|
|
812
|
+
<label htmlFor="name"${labelClasses}>Name</label>
|
|
813
|
+
<input
|
|
814
|
+
type="text"
|
|
815
|
+
id="name"
|
|
816
|
+
name="name"
|
|
817
|
+
required${inputClasses}
|
|
818
|
+
aria-invalid={actionData?.errors?.name ? 'true' : undefined}
|
|
819
|
+
/>
|
|
820
|
+
{actionData?.errors?.name && (
|
|
821
|
+
<p${errorClasses}>{actionData.errors.name}</p>
|
|
822
|
+
)}
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div>
|
|
826
|
+
<label htmlFor="email"${labelClasses}>Email</label>
|
|
827
|
+
<input
|
|
828
|
+
type="email"
|
|
829
|
+
id="email"
|
|
830
|
+
name="email"
|
|
831
|
+
required${inputClasses}
|
|
832
|
+
aria-invalid={actionData?.errors?.email ? 'true' : undefined}
|
|
833
|
+
/>
|
|
834
|
+
{actionData?.errors?.email && (
|
|
835
|
+
<p${errorClasses}>{actionData.errors.email}</p>
|
|
836
|
+
)}
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<div>
|
|
840
|
+
<label htmlFor="message"${labelClasses}>Message</label>
|
|
841
|
+
<textarea
|
|
842
|
+
id="message"
|
|
843
|
+
name="message"
|
|
844
|
+
required${textareaClasses}
|
|
845
|
+
aria-invalid={actionData?.errors?.message ? 'true' : undefined}
|
|
846
|
+
/>
|
|
847
|
+
{actionData?.errors?.message && (
|
|
848
|
+
<p${errorClasses}>{actionData.errors.message}</p>
|
|
849
|
+
)}
|
|
850
|
+
</div>
|
|
851
|
+
|
|
852
|
+
<button type="submit" disabled={isSubmitting}${buttonClasses}>
|
|
853
|
+
{isSubmitting ? 'Sending...' : 'Send Message'}
|
|
854
|
+
</button>
|
|
855
|
+
</Form>
|
|
856
|
+
</main>
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
`.trim();
|
|
860
|
+
}
|
|
861
|
+
function generateCounterComponent(template, typescript) {
|
|
862
|
+
const propsType = typescript ? ": { initialCount?: number }" : "";
|
|
863
|
+
const tailwindStyles = template === "tailwind";
|
|
864
|
+
const containerClasses = tailwindStyles ? ' className="flex items-center gap-4 mt-4"' : "";
|
|
865
|
+
const buttonClasses = tailwindStyles ? ' className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"' : "";
|
|
866
|
+
const countClasses = tailwindStyles ? ' className="text-2xl font-bold min-w-[3ch] text-center"' : "";
|
|
867
|
+
return `'use client';
|
|
868
|
+
/**
|
|
869
|
+
* Counter Component - Island Example
|
|
870
|
+
*
|
|
871
|
+
* This component demonstrates the islands architecture.
|
|
872
|
+
* Only components with 'use client' directive hydrate on the client.
|
|
873
|
+
*
|
|
874
|
+
* Hydration strategies:
|
|
875
|
+
* - client:load - Hydrate immediately on page load
|
|
876
|
+
* - client:idle - Hydrate when browser is idle
|
|
877
|
+
* - client:visible - Hydrate when element is visible (IntersectionObserver)
|
|
878
|
+
* - client:media - Hydrate when media query matches
|
|
879
|
+
*
|
|
880
|
+
* Usage:
|
|
881
|
+
* <Counter client:load initialCount={0} />
|
|
882
|
+
* <Counter client:visible initialCount={5} />
|
|
883
|
+
*/
|
|
884
|
+
import { useState } from 'react';
|
|
885
|
+
|
|
886
|
+
export function Counter({ initialCount = 0 }${propsType}) {
|
|
887
|
+
const [count, setCount] = useState(initialCount);
|
|
888
|
+
|
|
889
|
+
return (
|
|
890
|
+
<div${containerClasses}>
|
|
891
|
+
<button onClick={() => setCount(c => c - 1)}${buttonClasses}>
|
|
892
|
+
-
|
|
893
|
+
</button>
|
|
894
|
+
<span${countClasses}>{count}</span>
|
|
895
|
+
<button onClick={() => setCount(c => c + 1)}${buttonClasses}>
|
|
896
|
+
+
|
|
897
|
+
</button>
|
|
898
|
+
</div>
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
`.trim();
|
|
902
|
+
}
|
|
903
|
+
function generateLoggerMiddleware(typescript) {
|
|
904
|
+
const imports = typescript ? `import type { MiddlewareHandler } from '@ereo/core';` : "";
|
|
905
|
+
const typeAnnotation = typescript ? ": MiddlewareHandler" : "";
|
|
906
|
+
return `${imports}
|
|
907
|
+
/**
|
|
908
|
+
* Logger Middleware
|
|
909
|
+
*
|
|
910
|
+
* Logs request information and timing.
|
|
911
|
+
*
|
|
912
|
+
* Register in ereo.config.ts or use the route config:
|
|
913
|
+
*
|
|
914
|
+
* export const config = {
|
|
915
|
+
* middleware: ['logger'],
|
|
916
|
+
* };
|
|
917
|
+
*/
|
|
918
|
+
export const logger${typeAnnotation} = async (request, context, next) => {
|
|
919
|
+
const start = Date.now();
|
|
920
|
+
const url = new URL(request.url);
|
|
921
|
+
|
|
922
|
+
console.log(\`--> \${request.method} \${url.pathname}\`);
|
|
923
|
+
|
|
924
|
+
// Call next middleware/handler
|
|
925
|
+
const response = await next();
|
|
926
|
+
|
|
927
|
+
const duration = Date.now() - start;
|
|
928
|
+
console.log(\`<-- \${request.method} \${url.pathname} \${response.status} \${duration}ms\`);
|
|
929
|
+
|
|
930
|
+
return response;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
export default logger;
|
|
934
|
+
`.trim();
|
|
935
|
+
}
|
|
936
|
+
function generateErrorBoundary(template, typescript) {
|
|
937
|
+
const imports = typescript ? `import type { RouteErrorComponentProps } from '@ereo/core';
|
|
938
|
+
import { Link } from '@ereo/client';
|
|
939
|
+
import { isRouteErrorResponse } from '@ereo/client';` : `import { Link } from '@ereo/client';
|
|
940
|
+
import { isRouteErrorResponse } from '@ereo/client';`;
|
|
941
|
+
const propsType = typescript ? ": RouteErrorComponentProps" : "";
|
|
942
|
+
const tailwindStyles = template === "tailwind";
|
|
943
|
+
const mainClasses = tailwindStyles ? ' className="flex flex-col items-center justify-center min-h-[80vh] p-8"' : "";
|
|
944
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold text-red-600 mb-4"' : "";
|
|
945
|
+
const pClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-400 mb-4"' : "";
|
|
946
|
+
const preClasses = tailwindStyles ? ' className="p-4 bg-gray-100 dark:bg-gray-800 rounded-md overflow-auto max-w-2xl text-sm"' : "";
|
|
947
|
+
const linkClasses = tailwindStyles ? ' className="mt-6 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 inline-block"' : "";
|
|
948
|
+
const statusClasses = tailwindStyles ? ' className="text-6xl font-bold text-gray-300 dark:text-gray-600 mb-4"' : "";
|
|
949
|
+
return `${imports}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Error Boundary Page
|
|
953
|
+
*
|
|
954
|
+
* This component renders when an error occurs in a route.
|
|
955
|
+
* It receives the error and route params.
|
|
956
|
+
*
|
|
957
|
+
* Error types:
|
|
958
|
+
* - Response errors (404, 500, etc.) - thrown via \`throw new Response()\`
|
|
959
|
+
* - JavaScript errors - unexpected exceptions
|
|
960
|
+
*
|
|
961
|
+
* File naming:
|
|
962
|
+
* - _error.tsx - Catches errors in current route and children
|
|
963
|
+
* - error.tsx - Same as above (alternative naming)
|
|
964
|
+
*
|
|
965
|
+
* The error boundary closest to the error will be used.
|
|
966
|
+
*/
|
|
967
|
+
export default function ErrorBoundary({ error, params }${propsType}) {
|
|
968
|
+
// Check if this is a Response error (e.g., 404)
|
|
969
|
+
if (isRouteErrorResponse(error)) {
|
|
970
|
+
return (
|
|
971
|
+
<main${mainClasses}>
|
|
972
|
+
<div${statusClasses}>{error.status}</div>
|
|
973
|
+
<h1${h1Classes}>
|
|
974
|
+
{error.status === 404 ? 'Page Not Found' : 'Error'}
|
|
975
|
+
</h1>
|
|
976
|
+
<p${pClasses}>
|
|
977
|
+
{error.status === 404
|
|
978
|
+
? "The page you're looking for doesn't exist."
|
|
979
|
+
: error.statusText || 'An error occurred.'}
|
|
980
|
+
</p>
|
|
981
|
+
<Link to="/"${linkClasses}>
|
|
982
|
+
Go back home
|
|
983
|
+
</Link>
|
|
984
|
+
</main>
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// JavaScript/runtime error
|
|
989
|
+
return (
|
|
990
|
+
<main${mainClasses}>
|
|
991
|
+
<h1${h1Classes}>Something went wrong</h1>
|
|
992
|
+
<p${pClasses}>
|
|
993
|
+
We're sorry, but an error occurred while processing your request.
|
|
994
|
+
</p>
|
|
995
|
+
|
|
996
|
+
{process.env.NODE_ENV === 'development' && (
|
|
997
|
+
<pre${preClasses}>
|
|
998
|
+
<code>{error.message}</code>
|
|
999
|
+
{error.stack && (
|
|
1000
|
+
<>
|
|
1001
|
+
{'\\n\\n'}
|
|
1002
|
+
{error.stack}
|
|
1003
|
+
</>
|
|
1004
|
+
)}
|
|
1005
|
+
</pre>
|
|
1006
|
+
)}
|
|
1007
|
+
|
|
1008
|
+
<Link to="/"${linkClasses}>
|
|
1009
|
+
Go back home
|
|
1010
|
+
</Link>
|
|
1011
|
+
</main>
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
`.trim();
|
|
1015
|
+
}
|
|
1016
|
+
function generateTailwindConfig() {
|
|
1017
|
+
return `/** @type {import('tailwindcss').Config} */
|
|
1018
|
+
export default {
|
|
1019
|
+
content: [
|
|
1020
|
+
'./app/**/*.{js,ts,jsx,tsx}',
|
|
1021
|
+
'./components/**/*.{js,ts,jsx,tsx}',
|
|
1022
|
+
],
|
|
1023
|
+
darkMode: 'class',
|
|
1024
|
+
theme: {
|
|
1025
|
+
extend: {
|
|
1026
|
+
// Add your custom theme extensions here
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
plugins: [],
|
|
1030
|
+
};
|
|
1031
|
+
`.trim();
|
|
1032
|
+
}
|
|
1033
|
+
function generateGlobalCSS() {
|
|
1034
|
+
return `@tailwind base;
|
|
1035
|
+
@tailwind components;
|
|
1036
|
+
@tailwind utilities;
|
|
1037
|
+
|
|
1038
|
+
/* Custom global styles */
|
|
1039
|
+
@layer base {
|
|
1040
|
+
html {
|
|
1041
|
+
@apply antialiased;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
body {
|
|
1045
|
+
@apply bg-white dark:bg-gray-900 text-gray-900 dark:text-white;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
@layer components {
|
|
1050
|
+
/* Add reusable component styles here */
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
@layer utilities {
|
|
1054
|
+
/* Add custom utilities here */
|
|
1055
|
+
}
|
|
1056
|
+
`.trim();
|
|
1057
|
+
}
|
|
1058
|
+
function generateGitignore() {
|
|
1059
|
+
return `# Dependencies
|
|
1060
|
+
node_modules
|
|
1061
|
+
|
|
1062
|
+
# Build output
|
|
1063
|
+
.ereo
|
|
1064
|
+
dist
|
|
1065
|
+
|
|
1066
|
+
# Environment
|
|
1067
|
+
.env
|
|
1068
|
+
.env.local
|
|
1069
|
+
.env.*.local
|
|
1070
|
+
|
|
1071
|
+
# Logs
|
|
1072
|
+
*.log
|
|
1073
|
+
npm-debug.log*
|
|
1074
|
+
|
|
1075
|
+
# OS
|
|
1076
|
+
.DS_Store
|
|
1077
|
+
Thumbs.db
|
|
1078
|
+
|
|
1079
|
+
# IDE
|
|
1080
|
+
.vscode
|
|
1081
|
+
.idea
|
|
1082
|
+
*.swp
|
|
1083
|
+
*.swo
|
|
1084
|
+
|
|
1085
|
+
# Bun
|
|
1086
|
+
bun.lockb
|
|
1087
|
+
`.trim();
|
|
1088
|
+
}
|
|
1089
|
+
function generateDynamicRoute(template, typescript) {
|
|
1090
|
+
const imports = typescript ? `import type { LoaderArgs, MetaArgs, RouteConfig } from '@ereo/core';
|
|
1091
|
+
import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
|
|
1092
|
+
const loaderType = typescript ? ": LoaderArgs" : "";
|
|
1093
|
+
const configType = typescript ? ": RouteConfig" : "";
|
|
1094
|
+
const postType = typescript ? `
|
|
1095
|
+
interface Post {
|
|
1096
|
+
slug: string;
|
|
1097
|
+
title: string;
|
|
1098
|
+
content: string;
|
|
1099
|
+
author: string;
|
|
1100
|
+
publishedAt: string;
|
|
1101
|
+
}` : "";
|
|
1102
|
+
const metaType = typescript ? ": MetaArgs<Post>" : "";
|
|
1103
|
+
const propsType = typescript ? ": { loaderData: Post }" : "";
|
|
1104
|
+
const tailwindStyles = template === "tailwind";
|
|
1105
|
+
const articleClasses = tailwindStyles ? ' className="max-w-3xl mx-auto p-8"' : "";
|
|
1106
|
+
const backLinkClasses = tailwindStyles ? ' className="text-blue-600 hover:text-blue-800 dark:text-blue-400 mb-6 inline-block"' : "";
|
|
1107
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-4"' : "";
|
|
1108
|
+
const metaClasses = tailwindStyles ? ' className="text-gray-500 dark:text-gray-400 mb-8"' : "";
|
|
1109
|
+
const contentClasses = tailwindStyles ? ' className="prose dark:prose-invert max-w-none"' : "";
|
|
1110
|
+
return `${imports}
|
|
1111
|
+
${postType}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Route Configuration for dynamic routes
|
|
1115
|
+
*
|
|
1116
|
+
* Cache by slug parameter for efficient CDN caching.
|
|
1117
|
+
*/
|
|
1118
|
+
export const config${configType} = {
|
|
1119
|
+
cache: {
|
|
1120
|
+
edge: {
|
|
1121
|
+
maxAge: 3600,
|
|
1122
|
+
staleWhileRevalidate: 86400,
|
|
1123
|
+
},
|
|
1124
|
+
data: {
|
|
1125
|
+
// Dynamic tags based on the slug parameter
|
|
1126
|
+
tags: (params) => ['blog', \`post:\${params.slug}\`],
|
|
1127
|
+
},
|
|
1128
|
+
},
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Loader - Fetch blog post by slug
|
|
1133
|
+
*
|
|
1134
|
+
* The slug parameter comes from the [slug] in the filename.
|
|
1135
|
+
* Access via params.slug.
|
|
1136
|
+
*/
|
|
1137
|
+
export async function loader({ params, context }${loaderType})${typescript ? ": Promise<Post>" : ""} {
|
|
1138
|
+
const { slug } = params;
|
|
1139
|
+
|
|
1140
|
+
// In a real app, fetch from database or CMS
|
|
1141
|
+
// Example: const post = await db.posts.findBySlug(slug);
|
|
1142
|
+
|
|
1143
|
+
// Mock data for demonstration
|
|
1144
|
+
const posts${typescript ? ": Record<string, Post>" : ""} = {
|
|
1145
|
+
'hello-world': {
|
|
1146
|
+
slug: 'hello-world',
|
|
1147
|
+
title: 'Hello World',
|
|
1148
|
+
content: 'This is the first blog post using EreoJS framework. It demonstrates dynamic routing with [slug] parameters.',
|
|
1149
|
+
author: 'EreoJS Team',
|
|
1150
|
+
publishedAt: '2024-01-15',
|
|
1151
|
+
},
|
|
1152
|
+
'getting-started': {
|
|
1153
|
+
slug: 'getting-started',
|
|
1154
|
+
title: 'Getting Started with EreoJS',
|
|
1155
|
+
content: 'Learn how to build blazing fast applications with EreoJS and Bun. This guide covers loaders, actions, and islands architecture.',
|
|
1156
|
+
author: 'EreoJS Team',
|
|
1157
|
+
publishedAt: '2024-01-20',
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
const post = posts[slug${typescript ? " as string" : ""}];
|
|
1162
|
+
|
|
1163
|
+
if (!post) {
|
|
1164
|
+
throw new Response('Post not found', { status: 404 });
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return post;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* Meta - Generate SEO tags from post data
|
|
1172
|
+
*/
|
|
1173
|
+
export function meta({ data }${metaType}) {
|
|
1174
|
+
return [
|
|
1175
|
+
{ title: \`\${data.title} - Blog\` },
|
|
1176
|
+
{ name: 'description', content: data.content.slice(0, 160) },
|
|
1177
|
+
{ property: 'og:title', content: data.title },
|
|
1178
|
+
{ property: 'og:type', content: 'article' },
|
|
1179
|
+
{ property: 'article:author', content: data.author },
|
|
1180
|
+
{ property: 'article:published_time', content: data.publishedAt },
|
|
1181
|
+
];
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
/**
|
|
1185
|
+
* Blog Post Page
|
|
1186
|
+
*/
|
|
1187
|
+
export default function BlogPost({ loaderData: post }${propsType}) {
|
|
1188
|
+
return (
|
|
1189
|
+
<article${articleClasses}>
|
|
1190
|
+
<Link to="/blog"${backLinkClasses}>
|
|
1191
|
+
\u2190 Back to Blog
|
|
1192
|
+
</Link>
|
|
1193
|
+
|
|
1194
|
+
<h1${h1Classes}>{post.title}</h1>
|
|
1195
|
+
|
|
1196
|
+
<div${metaClasses}>
|
|
1197
|
+
By {post.author} \u2022 {new Date(post.publishedAt).toLocaleDateString()}
|
|
1198
|
+
</div>
|
|
1199
|
+
|
|
1200
|
+
<div${contentClasses}>
|
|
1201
|
+
<p>{post.content}</p>
|
|
1202
|
+
</div>
|
|
1203
|
+
</article>
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
`.trim();
|
|
1207
|
+
}
|
|
1208
|
+
function generateBlogIndex(template, typescript) {
|
|
1209
|
+
const imports = typescript ? `import type { LoaderArgs, MetaArgs } from '@ereo/core';
|
|
1210
|
+
import { Link } from '@ereo/client';` : `import { Link } from '@ereo/client';`;
|
|
1211
|
+
const loaderType = typescript ? ": LoaderArgs" : "";
|
|
1212
|
+
const postType = typescript ? `
|
|
1213
|
+
interface PostSummary {
|
|
1214
|
+
slug: string;
|
|
1215
|
+
title: string;
|
|
1216
|
+
excerpt: string;
|
|
1217
|
+
publishedAt: string;
|
|
1218
|
+
}` : "";
|
|
1219
|
+
const metaType = typescript ? ": MetaArgs<PostSummary[]>" : "";
|
|
1220
|
+
const propsType = typescript ? ": { loaderData: PostSummary[] }" : "";
|
|
1221
|
+
const tailwindStyles = template === "tailwind";
|
|
1222
|
+
const mainClasses = tailwindStyles ? ' className="max-w-3xl mx-auto p-8"' : "";
|
|
1223
|
+
const h1Classes = tailwindStyles ? ' className="text-4xl font-bold mb-8"' : "";
|
|
1224
|
+
const listClasses = tailwindStyles ? ' className="space-y-6"' : "";
|
|
1225
|
+
const cardClasses = tailwindStyles ? ' className="border border-gray-200 dark:border-gray-700 rounded-lg p-6 hover:shadow-lg transition-shadow"' : "";
|
|
1226
|
+
const titleClasses = tailwindStyles ? ' className="text-xl font-semibold text-blue-600 dark:text-blue-400 hover:underline"' : "";
|
|
1227
|
+
const dateClasses = tailwindStyles ? ' className="text-sm text-gray-500 dark:text-gray-400 mt-1"' : "";
|
|
1228
|
+
const excerptClasses = tailwindStyles ? ' className="text-gray-600 dark:text-gray-300 mt-2"' : "";
|
|
1229
|
+
return `${imports}
|
|
1230
|
+
${postType}
|
|
1231
|
+
|
|
1232
|
+
/**
|
|
1233
|
+
* Meta - Blog listing page
|
|
1234
|
+
*/
|
|
1235
|
+
export function meta() {
|
|
1236
|
+
return [
|
|
1237
|
+
{ title: 'Blog - EreoJS App' },
|
|
1238
|
+
{ name: 'description', content: 'Read our latest blog posts' },
|
|
1239
|
+
];
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Loader - Fetch all blog posts
|
|
1244
|
+
*/
|
|
1245
|
+
export async function loader({ context }${loaderType})${typescript ? ": Promise<PostSummary[]>" : ""} {
|
|
1246
|
+
// Set cache for the listing page
|
|
1247
|
+
context.cache.set({
|
|
1248
|
+
maxAge: 300,
|
|
1249
|
+
tags: ['blog', 'blog-list'],
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
// In a real app, fetch from database
|
|
1253
|
+
// Example: const posts = await db.posts.findMany({ orderBy: { publishedAt: 'desc' } });
|
|
1254
|
+
|
|
1255
|
+
return [
|
|
1256
|
+
{
|
|
1257
|
+
slug: 'getting-started',
|
|
1258
|
+
title: 'Getting Started with EreoJS',
|
|
1259
|
+
excerpt: 'Learn how to build blazing fast applications with EreoJS and Bun.',
|
|
1260
|
+
publishedAt: '2024-01-20',
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
slug: 'hello-world',
|
|
1264
|
+
title: 'Hello World',
|
|
1265
|
+
excerpt: 'This is the first blog post using EreoJS framework.',
|
|
1266
|
+
publishedAt: '2024-01-15',
|
|
1267
|
+
},
|
|
1268
|
+
];
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/**
|
|
1272
|
+
* Blog Index Page
|
|
1273
|
+
*/
|
|
1274
|
+
export default function BlogIndex({ loaderData: posts }${propsType}) {
|
|
1275
|
+
return (
|
|
1276
|
+
<main${mainClasses}>
|
|
1277
|
+
<h1${h1Classes}>Blog</h1>
|
|
1278
|
+
|
|
1279
|
+
<ul${listClasses}>
|
|
1280
|
+
{posts.map((post) => (
|
|
1281
|
+
<li key={post.slug}${cardClasses}>
|
|
1282
|
+
<Link to={\`/blog/\${post.slug}\`}${titleClasses}>
|
|
1283
|
+
{post.title}
|
|
1284
|
+
</Link>
|
|
1285
|
+
<p${dateClasses}>
|
|
1286
|
+
{new Date(post.publishedAt).toLocaleDateString()}
|
|
1287
|
+
</p>
|
|
1288
|
+
<p${excerptClasses}>{post.excerpt}</p>
|
|
1289
|
+
</li>
|
|
1290
|
+
))}
|
|
1291
|
+
</ul>
|
|
1292
|
+
</main>
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
`.trim();
|
|
1296
|
+
}
|
|
1297
|
+
function generateApiRoute(typescript) {
|
|
1298
|
+
const typeAnnotations = typescript ? `
|
|
1299
|
+
interface HealthResponse {
|
|
1300
|
+
status: 'ok' | 'error';
|
|
1301
|
+
timestamp: string;
|
|
1302
|
+
version: string;
|
|
1303
|
+
uptime: number;
|
|
1304
|
+
}
|
|
1305
|
+
` : "";
|
|
1306
|
+
const loaderType = typescript ? ": LoaderArgs" : "";
|
|
1307
|
+
const returnType = typescript ? ": Promise<Response>" : "";
|
|
1308
|
+
return `/**
|
|
1309
|
+
* API Route Example - /api/health
|
|
1310
|
+
*
|
|
1311
|
+
* API routes return Response objects directly.
|
|
1312
|
+
* They don't render React components.
|
|
1313
|
+
*
|
|
1314
|
+
* Common patterns:
|
|
1315
|
+
* - GET /api/health -> Health check
|
|
1316
|
+
* - GET /api/users -> List users
|
|
1317
|
+
* - POST /api/users -> Create user
|
|
1318
|
+
* - GET /api/users/[id] -> Get user by ID
|
|
1319
|
+
*/
|
|
1320
|
+
import type { LoaderArgs, ActionArgs } from '@ereo/core';
|
|
1321
|
+
import { json } from '@ereo/data';
|
|
1322
|
+
${typeAnnotations}
|
|
1323
|
+
const startTime = Date.now();
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* GET /api/health
|
|
1327
|
+
*
|
|
1328
|
+
* Health check endpoint for monitoring and load balancers.
|
|
1329
|
+
*/
|
|
1330
|
+
export async function loader({ request, context }${loaderType})${returnType} {
|
|
1331
|
+
const health${typescript ? ": HealthResponse" : ""} = {
|
|
1332
|
+
status: 'ok',
|
|
1333
|
+
timestamp: new Date().toISOString(),
|
|
1334
|
+
version: '0.1.0',
|
|
1335
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
return json(health, {
|
|
1339
|
+
headers: {
|
|
1340
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
1341
|
+
},
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* POST /api/health
|
|
1347
|
+
*
|
|
1348
|
+
* Example of handling POST requests in API routes.
|
|
1349
|
+
* Use this pattern for webhooks, form submissions, etc.
|
|
1350
|
+
*/
|
|
1351
|
+
export async function action({ request, context }${typescript ? ": ActionArgs" : ""})${returnType} {
|
|
1352
|
+
// Only allow POST
|
|
1353
|
+
if (request.method !== 'POST') {
|
|
1354
|
+
return json(
|
|
1355
|
+
{ error: 'Method not allowed' },
|
|
1356
|
+
{ status: 405 }
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Parse request body
|
|
1361
|
+
const body = await request.json().catch(() => ({}));
|
|
1362
|
+
|
|
1363
|
+
// Example: Log the health check ping
|
|
1364
|
+
console.log('Health check ping received:', body);
|
|
1365
|
+
|
|
1366
|
+
return json({ received: true, timestamp: new Date().toISOString() });
|
|
1367
|
+
}
|
|
1368
|
+
`.trim();
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/commands/deploy.ts
|
|
1372
|
+
import { join as join5 } from "path";
|
|
1373
|
+
async function deploy(options = {}) {
|
|
1374
|
+
const root = process.cwd();
|
|
1375
|
+
const target = options.target || await detectTarget(root);
|
|
1376
|
+
console.log(`
|
|
1377
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m Deploy
|
|
1378
|
+
`);
|
|
1379
|
+
console.log(` Target: \x1B[33m${target}\x1B[0m
|
|
1380
|
+
`);
|
|
1381
|
+
let config = {};
|
|
1382
|
+
const configPath = join5(root, "ereo.config.ts");
|
|
1383
|
+
try {
|
|
1384
|
+
if (await Bun.file(configPath).exists()) {
|
|
1385
|
+
const configModule = await import(configPath);
|
|
1386
|
+
config = configModule.default || configModule;
|
|
1387
|
+
}
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
console.warn("Could not load config:", error);
|
|
1390
|
+
}
|
|
1391
|
+
if (options.build !== false) {
|
|
1392
|
+
console.log(" \x1B[2mBuilding for production...\x1B[0m");
|
|
1393
|
+
const { build: build2 } = await Promise.resolve().then(() => (init_build(), exports_build));
|
|
1394
|
+
await build2({ production: true });
|
|
1395
|
+
console.log(` \x1B[32m\u2713\x1B[0m Build complete
|
|
1396
|
+
`);
|
|
1397
|
+
}
|
|
1398
|
+
if (options.dryRun) {
|
|
1399
|
+
console.log(` \x1B[33m\u26A0\x1B[0m Dry run - skipping actual deployment
|
|
1400
|
+
`);
|
|
1401
|
+
return generateDeployPreview(target, root, config);
|
|
1402
|
+
}
|
|
1403
|
+
try {
|
|
1404
|
+
switch (target) {
|
|
1405
|
+
case "vercel":
|
|
1406
|
+
return await deployToVercel(root, options);
|
|
1407
|
+
case "cloudflare":
|
|
1408
|
+
return await deployToCloudflare(root, options);
|
|
1409
|
+
case "fly":
|
|
1410
|
+
return await deployToFly(root, options);
|
|
1411
|
+
case "netlify":
|
|
1412
|
+
return await deployToNetlify(root, options);
|
|
1413
|
+
case "docker":
|
|
1414
|
+
return await deployToDocker(root, options);
|
|
1415
|
+
default:
|
|
1416
|
+
return {
|
|
1417
|
+
success: false,
|
|
1418
|
+
error: `Unknown deployment target: ${target}`
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
return {
|
|
1423
|
+
success: false,
|
|
1424
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async function detectTarget(root) {
|
|
1429
|
+
if (await Bun.file(join5(root, "vercel.json")).exists()) {
|
|
1430
|
+
return "vercel";
|
|
1431
|
+
}
|
|
1432
|
+
if (await Bun.file(join5(root, "wrangler.toml")).exists()) {
|
|
1433
|
+
return "cloudflare";
|
|
1434
|
+
}
|
|
1435
|
+
if (await Bun.file(join5(root, "fly.toml")).exists()) {
|
|
1436
|
+
return "fly";
|
|
1437
|
+
}
|
|
1438
|
+
if (await Bun.file(join5(root, "netlify.toml")).exists()) {
|
|
1439
|
+
return "netlify";
|
|
1440
|
+
}
|
|
1441
|
+
if (await Bun.file(join5(root, "Dockerfile")).exists()) {
|
|
1442
|
+
return "docker";
|
|
1443
|
+
}
|
|
1444
|
+
return "vercel";
|
|
1445
|
+
}
|
|
1446
|
+
function generateDeployPreview(target, root, config) {
|
|
1447
|
+
const logs = [];
|
|
1448
|
+
logs.push(`Would deploy to ${target}`);
|
|
1449
|
+
logs.push(`Project root: ${root}`);
|
|
1450
|
+
logs.push(`Build target: ${config.build?.target || "bun"}`);
|
|
1451
|
+
logs.push(`Output directory: ${config.build?.outDir || "dist"}`);
|
|
1452
|
+
console.log(" Preview:");
|
|
1453
|
+
for (const log of logs) {
|
|
1454
|
+
console.log(` ${log}`);
|
|
1455
|
+
}
|
|
1456
|
+
console.log("");
|
|
1457
|
+
return {
|
|
1458
|
+
success: true,
|
|
1459
|
+
logs
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
async function deployToVercel(root, options) {
|
|
1463
|
+
console.log(` Deploying to Vercel...
|
|
1464
|
+
`);
|
|
1465
|
+
const hasVercelCLI = await checkCommand("vercel");
|
|
1466
|
+
if (!hasVercelCLI) {
|
|
1467
|
+
console.log(` \x1B[33m\u26A0\x1B[0m Vercel CLI not found. Installing...
|
|
1468
|
+
`);
|
|
1469
|
+
await runCommand("bun", ["add", "-g", "vercel"]);
|
|
1470
|
+
}
|
|
1471
|
+
const vercelConfigPath = join5(root, "vercel.json");
|
|
1472
|
+
if (!await Bun.file(vercelConfigPath).exists()) {
|
|
1473
|
+
const vercelConfig = {
|
|
1474
|
+
buildCommand: "bun run build",
|
|
1475
|
+
outputDirectory: "dist",
|
|
1476
|
+
framework: null,
|
|
1477
|
+
functions: {
|
|
1478
|
+
"api/**/*.ts": {
|
|
1479
|
+
runtime: "@vercel/bun@0.1.0"
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
await Bun.write(vercelConfigPath, JSON.stringify(vercelConfig, null, 2));
|
|
1484
|
+
console.log(` \x1B[32m\u2713\x1B[0m Generated vercel.json
|
|
1485
|
+
`);
|
|
1486
|
+
}
|
|
1487
|
+
const args = ["deploy"];
|
|
1488
|
+
if (options.production) {
|
|
1489
|
+
args.push("--prod");
|
|
1490
|
+
}
|
|
1491
|
+
if (options.name) {
|
|
1492
|
+
args.push("--name", options.name);
|
|
1493
|
+
}
|
|
1494
|
+
const result = await runCommandWithOutput("vercel", args, root);
|
|
1495
|
+
if (result.success) {
|
|
1496
|
+
const urlMatch = result.output.match(/https:\/\/[^\s]+\.vercel\.app/);
|
|
1497
|
+
const url = urlMatch ? urlMatch[0] : undefined;
|
|
1498
|
+
console.log(`
|
|
1499
|
+
\x1B[32m\u2713\x1B[0m Deployed successfully!`);
|
|
1500
|
+
if (url) {
|
|
1501
|
+
console.log(` \x1B[36m\u279C\x1B[0m ${url}
|
|
1502
|
+
`);
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
success: true,
|
|
1506
|
+
url,
|
|
1507
|
+
logs: [result.output]
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
return {
|
|
1511
|
+
success: false,
|
|
1512
|
+
error: result.error || "Deployment failed",
|
|
1513
|
+
logs: [result.output]
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
async function deployToCloudflare(root, options) {
|
|
1517
|
+
console.log(` Deploying to Cloudflare...
|
|
1518
|
+
`);
|
|
1519
|
+
const hasWrangler = await checkCommand("wrangler");
|
|
1520
|
+
if (!hasWrangler) {
|
|
1521
|
+
console.log(` \x1B[33m\u26A0\x1B[0m Wrangler CLI not found. Installing...
|
|
1522
|
+
`);
|
|
1523
|
+
await runCommand("bun", ["add", "-g", "wrangler"]);
|
|
1524
|
+
}
|
|
1525
|
+
const wranglerConfigPath = join5(root, "wrangler.toml");
|
|
1526
|
+
if (!await Bun.file(wranglerConfigPath).exists()) {
|
|
1527
|
+
const projectName = options.name || "ereo-app";
|
|
1528
|
+
const wranglerConfig = `name = "${projectName}"
|
|
1529
|
+
main = "dist/server/index.js"
|
|
1530
|
+
compatibility_date = "2024-01-01"
|
|
1531
|
+
|
|
1532
|
+
[site]
|
|
1533
|
+
bucket = "./dist/client"
|
|
1534
|
+
|
|
1535
|
+
[build]
|
|
1536
|
+
command = "bun run build"
|
|
1537
|
+
`;
|
|
1538
|
+
await Bun.write(wranglerConfigPath, wranglerConfig);
|
|
1539
|
+
console.log(` \x1B[32m\u2713\x1B[0m Generated wrangler.toml
|
|
1540
|
+
`);
|
|
1541
|
+
}
|
|
1542
|
+
const result = await runCommandWithOutput("wrangler", ["deploy"], root);
|
|
1543
|
+
if (result.success) {
|
|
1544
|
+
const urlMatch = result.output.match(/https:\/\/[^\s]+\.workers\.dev/);
|
|
1545
|
+
const url = urlMatch ? urlMatch[0] : undefined;
|
|
1546
|
+
console.log(`
|
|
1547
|
+
\x1B[32m\u2713\x1B[0m Deployed successfully!`);
|
|
1548
|
+
if (url) {
|
|
1549
|
+
console.log(` \x1B[36m\u279C\x1B[0m ${url}
|
|
1550
|
+
`);
|
|
1551
|
+
}
|
|
1552
|
+
return {
|
|
1553
|
+
success: true,
|
|
1554
|
+
url,
|
|
1555
|
+
logs: [result.output]
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
return {
|
|
1559
|
+
success: false,
|
|
1560
|
+
error: result.error || "Deployment failed",
|
|
1561
|
+
logs: [result.output]
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
async function deployToFly(root, options) {
|
|
1565
|
+
console.log(` Deploying to Fly.io...
|
|
1566
|
+
`);
|
|
1567
|
+
const hasFly = await checkCommand("flyctl");
|
|
1568
|
+
if (!hasFly) {
|
|
1569
|
+
console.log(" \x1B[33m\u26A0\x1B[0m Fly CLI not found.");
|
|
1570
|
+
console.log(` Install from: https://fly.io/docs/hands-on/install-flyctl/
|
|
1571
|
+
`);
|
|
1572
|
+
return {
|
|
1573
|
+
success: false,
|
|
1574
|
+
error: "Fly CLI not installed"
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
const flyConfigPath = join5(root, "fly.toml");
|
|
1578
|
+
if (!await Bun.file(flyConfigPath).exists()) {
|
|
1579
|
+
const projectName = options.name || "ereo-app";
|
|
1580
|
+
const flyConfig = `app = "${projectName}"
|
|
1581
|
+
primary_region = "iad"
|
|
1582
|
+
|
|
1583
|
+
[build]
|
|
1584
|
+
builder = "oven/bun"
|
|
1585
|
+
|
|
1586
|
+
[http_service]
|
|
1587
|
+
internal_port = 3000
|
|
1588
|
+
force_https = true
|
|
1589
|
+
auto_stop_machines = true
|
|
1590
|
+
auto_start_machines = true
|
|
1591
|
+
min_machines_running = 0
|
|
1592
|
+
`;
|
|
1593
|
+
await Bun.write(flyConfigPath, flyConfig);
|
|
1594
|
+
console.log(` \x1B[32m\u2713\x1B[0m Generated fly.toml
|
|
1595
|
+
`);
|
|
1596
|
+
}
|
|
1597
|
+
const result = await runCommandWithOutput("flyctl", ["deploy"], root);
|
|
1598
|
+
if (result.success) {
|
|
1599
|
+
const urlMatch = result.output.match(/https:\/\/[^\s]+\.fly\.dev/);
|
|
1600
|
+
const url = urlMatch ? urlMatch[0] : undefined;
|
|
1601
|
+
console.log(`
|
|
1602
|
+
\x1B[32m\u2713\x1B[0m Deployed successfully!`);
|
|
1603
|
+
if (url) {
|
|
1604
|
+
console.log(` \x1B[36m\u279C\x1B[0m ${url}
|
|
1605
|
+
`);
|
|
1606
|
+
}
|
|
1607
|
+
return {
|
|
1608
|
+
success: true,
|
|
1609
|
+
url,
|
|
1610
|
+
logs: [result.output]
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
success: false,
|
|
1615
|
+
error: result.error || "Deployment failed",
|
|
1616
|
+
logs: [result.output]
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
async function deployToNetlify(root, options) {
|
|
1620
|
+
console.log(` Deploying to Netlify...
|
|
1621
|
+
`);
|
|
1622
|
+
const hasNetlify = await checkCommand("netlify");
|
|
1623
|
+
if (!hasNetlify) {
|
|
1624
|
+
console.log(` \x1B[33m\u26A0\x1B[0m Netlify CLI not found. Installing...
|
|
1625
|
+
`);
|
|
1626
|
+
await runCommand("bun", ["add", "-g", "netlify-cli"]);
|
|
1627
|
+
}
|
|
1628
|
+
const netlifyConfigPath = join5(root, "netlify.toml");
|
|
1629
|
+
if (!await Bun.file(netlifyConfigPath).exists()) {
|
|
1630
|
+
const netlifyConfig = `[build]
|
|
1631
|
+
command = "bun run build"
|
|
1632
|
+
publish = "dist/client"
|
|
1633
|
+
functions = "dist/server"
|
|
1634
|
+
|
|
1635
|
+
[functions]
|
|
1636
|
+
node_bundler = "esbuild"
|
|
1637
|
+
|
|
1638
|
+
[[redirects]]
|
|
1639
|
+
from = "/api/*"
|
|
1640
|
+
to = "/.netlify/functions/api/:splat"
|
|
1641
|
+
status = 200
|
|
1642
|
+
|
|
1643
|
+
[[redirects]]
|
|
1644
|
+
from = "/*"
|
|
1645
|
+
to = "/index.html"
|
|
1646
|
+
status = 200
|
|
1647
|
+
`;
|
|
1648
|
+
await Bun.write(netlifyConfigPath, netlifyConfig);
|
|
1649
|
+
console.log(` \x1B[32m\u2713\x1B[0m Generated netlify.toml
|
|
1650
|
+
`);
|
|
1651
|
+
}
|
|
1652
|
+
const args = ["deploy", "--dir=dist/client"];
|
|
1653
|
+
if (options.production) {
|
|
1654
|
+
args.push("--prod");
|
|
1655
|
+
}
|
|
1656
|
+
const result = await runCommandWithOutput("netlify", args, root);
|
|
1657
|
+
if (result.success) {
|
|
1658
|
+
const urlMatch = result.output.match(/https:\/\/[^\s]+\.netlify\.app/);
|
|
1659
|
+
const url = urlMatch ? urlMatch[0] : undefined;
|
|
1660
|
+
console.log(`
|
|
1661
|
+
\x1B[32m\u2713\x1B[0m Deployed successfully!`);
|
|
1662
|
+
if (url) {
|
|
1663
|
+
console.log(` \x1B[36m\u279C\x1B[0m ${url}
|
|
1664
|
+
`);
|
|
1665
|
+
}
|
|
1666
|
+
return {
|
|
1667
|
+
success: true,
|
|
1668
|
+
url,
|
|
1669
|
+
logs: [result.output]
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
return {
|
|
1673
|
+
success: false,
|
|
1674
|
+
error: result.error || "Deployment failed",
|
|
1675
|
+
logs: [result.output]
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
async function deployToDocker(root, options) {
|
|
1679
|
+
console.log(` Building Docker image...
|
|
1680
|
+
`);
|
|
1681
|
+
const hasDocker = await checkCommand("docker");
|
|
1682
|
+
if (!hasDocker) {
|
|
1683
|
+
return {
|
|
1684
|
+
success: false,
|
|
1685
|
+
error: "Docker not installed"
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
const dockerfilePath = join5(root, "Dockerfile");
|
|
1689
|
+
if (!await Bun.file(dockerfilePath).exists()) {
|
|
1690
|
+
const dockerfile = `# Build stage
|
|
1691
|
+
FROM oven/bun:1 as builder
|
|
1692
|
+
WORKDIR /app
|
|
1693
|
+
COPY package.json bun.lockb* ./
|
|
1694
|
+
RUN bun install --frozen-lockfile
|
|
1695
|
+
COPY . .
|
|
1696
|
+
RUN bun run build
|
|
1697
|
+
|
|
1698
|
+
# Production stage
|
|
1699
|
+
FROM oven/bun:1-slim
|
|
1700
|
+
WORKDIR /app
|
|
1701
|
+
COPY --from=builder /app/dist ./dist
|
|
1702
|
+
COPY --from=builder /app/package.json ./
|
|
1703
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
1704
|
+
ENV NODE_ENV=production
|
|
1705
|
+
EXPOSE 3000
|
|
1706
|
+
CMD ["bun", "run", "start"]
|
|
1707
|
+
`;
|
|
1708
|
+
await Bun.write(dockerfilePath, dockerfile);
|
|
1709
|
+
console.log(` \x1B[32m\u2713\x1B[0m Generated Dockerfile
|
|
1710
|
+
`);
|
|
1711
|
+
}
|
|
1712
|
+
const imageName = options.name || "ereo-app";
|
|
1713
|
+
const tag = options.production ? "latest" : "dev";
|
|
1714
|
+
const result = await runCommandWithOutput("docker", ["build", "-t", `${imageName}:${tag}`, "."], root);
|
|
1715
|
+
if (result.success) {
|
|
1716
|
+
console.log(`
|
|
1717
|
+
\x1B[32m\u2713\x1B[0m Docker image built: ${imageName}:${tag}`);
|
|
1718
|
+
console.log(` Run with: docker run -p 3000:3000 ${imageName}:${tag}
|
|
1719
|
+
`);
|
|
1720
|
+
return {
|
|
1721
|
+
success: true,
|
|
1722
|
+
deploymentId: `${imageName}:${tag}`,
|
|
1723
|
+
logs: [result.output]
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
return {
|
|
1727
|
+
success: false,
|
|
1728
|
+
error: result.error || "Docker build failed",
|
|
1729
|
+
logs: [result.output]
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
async function checkCommand(command) {
|
|
1733
|
+
try {
|
|
1734
|
+
const proc = Bun.spawn(["which", command], {
|
|
1735
|
+
stdout: "pipe",
|
|
1736
|
+
stderr: "pipe"
|
|
1737
|
+
});
|
|
1738
|
+
await proc.exited;
|
|
1739
|
+
return proc.exitCode === 0;
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
async function runCommand(command, args) {
|
|
1745
|
+
const proc = Bun.spawn([command, ...args], {
|
|
1746
|
+
stdout: "inherit",
|
|
1747
|
+
stderr: "inherit"
|
|
1748
|
+
});
|
|
1749
|
+
await proc.exited;
|
|
1750
|
+
return proc.exitCode === 0;
|
|
1751
|
+
}
|
|
1752
|
+
async function runCommandWithOutput(command, args, cwd) {
|
|
1753
|
+
try {
|
|
1754
|
+
const proc = Bun.spawn([command, ...args], {
|
|
1755
|
+
cwd,
|
|
1756
|
+
stdout: "pipe",
|
|
1757
|
+
stderr: "pipe"
|
|
1758
|
+
});
|
|
1759
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1760
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1761
|
+
await proc.exited;
|
|
1762
|
+
return {
|
|
1763
|
+
success: proc.exitCode === 0,
|
|
1764
|
+
output: stdout,
|
|
1765
|
+
error: stderr || undefined
|
|
1766
|
+
};
|
|
1767
|
+
} catch (error) {
|
|
1768
|
+
return {
|
|
1769
|
+
success: false,
|
|
1770
|
+
output: "",
|
|
1771
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
function printDeployHelp() {
|
|
1776
|
+
console.log(`
|
|
1777
|
+
Usage: ereo deploy [target] [options]
|
|
1778
|
+
|
|
1779
|
+
Targets:
|
|
1780
|
+
vercel Deploy to Vercel (default)
|
|
1781
|
+
cloudflare Deploy to Cloudflare Pages/Workers
|
|
1782
|
+
fly Deploy to Fly.io
|
|
1783
|
+
netlify Deploy to Netlify
|
|
1784
|
+
docker Build Docker image
|
|
1785
|
+
|
|
1786
|
+
Options:
|
|
1787
|
+
--prod Deploy to production
|
|
1788
|
+
--dry-run Preview deployment without actually deploying
|
|
1789
|
+
--name Project name for new deployments
|
|
1790
|
+
--no-build Skip build step
|
|
1791
|
+
|
|
1792
|
+
Examples:
|
|
1793
|
+
ereo deploy Deploy to auto-detected platform
|
|
1794
|
+
ereo deploy vercel --prod Deploy to Vercel production
|
|
1795
|
+
ereo deploy cloudflare Deploy to Cloudflare
|
|
1796
|
+
ereo deploy docker --name app Build Docker image named 'app'
|
|
1797
|
+
`);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
// src/commands/db.ts
|
|
1801
|
+
import { spawn } from "child_process";
|
|
1802
|
+
import { existsSync } from "fs";
|
|
1803
|
+
import { resolve, join as join6 } from "path";
|
|
1804
|
+
function findDrizzleConfig(customPath) {
|
|
1805
|
+
const cwd = process.cwd();
|
|
1806
|
+
if (customPath) {
|
|
1807
|
+
const absolutePath = resolve(cwd, customPath);
|
|
1808
|
+
if (existsSync(absolutePath)) {
|
|
1809
|
+
return absolutePath;
|
|
1810
|
+
}
|
|
1811
|
+
throw new Error(`Config file not found: ${customPath}`);
|
|
1812
|
+
}
|
|
1813
|
+
const configNames = [
|
|
1814
|
+
"drizzle.config.ts",
|
|
1815
|
+
"drizzle.config.js",
|
|
1816
|
+
"drizzle.config.mjs",
|
|
1817
|
+
"drizzle.config.json"
|
|
1818
|
+
];
|
|
1819
|
+
for (const name of configNames) {
|
|
1820
|
+
const configPath = join6(cwd, name);
|
|
1821
|
+
if (existsSync(configPath)) {
|
|
1822
|
+
return configPath;
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
throw new Error("Drizzle config not found. Create a drizzle.config.ts file or specify --config path.");
|
|
1826
|
+
}
|
|
1827
|
+
async function runDrizzleKit(command, args = []) {
|
|
1828
|
+
return new Promise((resolve2) => {
|
|
1829
|
+
console.log(`
|
|
1830
|
+
\x1B[36m\u25B6\x1B[0m Running: drizzle-kit ${command} ${args.join(" ")}
|
|
1831
|
+
`);
|
|
1832
|
+
const runner = existsSync(join6(process.cwd(), "bun.lockb")) ? "bunx" : "npx";
|
|
1833
|
+
const proc = spawn(runner, ["drizzle-kit", command, ...args], {
|
|
1834
|
+
cwd: process.cwd(),
|
|
1835
|
+
stdio: "inherit",
|
|
1836
|
+
shell: true
|
|
1837
|
+
});
|
|
1838
|
+
proc.on("close", (code) => {
|
|
1839
|
+
if (code === 0) {
|
|
1840
|
+
resolve2({ success: true });
|
|
1841
|
+
} else {
|
|
1842
|
+
resolve2({ success: false, error: `drizzle-kit ${command} failed with exit code ${code}` });
|
|
1843
|
+
}
|
|
1844
|
+
});
|
|
1845
|
+
proc.on("error", (error) => {
|
|
1846
|
+
resolve2({ success: false, error: error.message });
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
async function dbMigrate(options = {}) {
|
|
1851
|
+
console.log(`
|
|
1852
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mRunning database migrations...\x1B[0m`);
|
|
1853
|
+
try {
|
|
1854
|
+
const configPath = findDrizzleConfig(options.config);
|
|
1855
|
+
const args = ["--config", configPath];
|
|
1856
|
+
if (options.verbose) {
|
|
1857
|
+
args.push("--verbose");
|
|
1858
|
+
}
|
|
1859
|
+
const result = await runDrizzleKit("migrate", args);
|
|
1860
|
+
if (result.success) {
|
|
1861
|
+
console.log(`
|
|
1862
|
+
\x1B[32m\u2713\x1B[0m Migrations completed successfully
|
|
1863
|
+
`);
|
|
1864
|
+
} else {
|
|
1865
|
+
console.error(`
|
|
1866
|
+
\x1B[31m\u2717\x1B[0m ${result.error}
|
|
1867
|
+
`);
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
}
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
console.error(`
|
|
1872
|
+
\x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
|
|
1873
|
+
`);
|
|
1874
|
+
process.exit(1);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
async function dbGenerate(options) {
|
|
1878
|
+
console.log(`
|
|
1879
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mGenerating migration...\x1B[0m`);
|
|
1880
|
+
if (!options.name) {
|
|
1881
|
+
console.error(`
|
|
1882
|
+
\x1B[31m\u2717\x1B[0m Migration name is required (--name <name>)
|
|
1883
|
+
`);
|
|
1884
|
+
process.exit(1);
|
|
1885
|
+
}
|
|
1886
|
+
try {
|
|
1887
|
+
const configPath = findDrizzleConfig(options.config);
|
|
1888
|
+
const args = ["--config", configPath, "--name", options.name];
|
|
1889
|
+
if (options.out) {
|
|
1890
|
+
args.push("--out", options.out);
|
|
1891
|
+
}
|
|
1892
|
+
const result = await runDrizzleKit("generate", args);
|
|
1893
|
+
if (result.success) {
|
|
1894
|
+
console.log(`
|
|
1895
|
+
\x1B[32m\u2713\x1B[0m Migration "${options.name}" generated successfully
|
|
1896
|
+
`);
|
|
1897
|
+
} else {
|
|
1898
|
+
console.error(`
|
|
1899
|
+
\x1B[31m\u2717\x1B[0m ${result.error}
|
|
1900
|
+
`);
|
|
1901
|
+
process.exit(1);
|
|
1902
|
+
}
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
console.error(`
|
|
1905
|
+
\x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
|
|
1906
|
+
`);
|
|
1907
|
+
process.exit(1);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
async function dbStudio(options = {}) {
|
|
1911
|
+
console.log(`
|
|
1912
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mStarting Drizzle Studio...\x1B[0m`);
|
|
1913
|
+
try {
|
|
1914
|
+
const configPath = findDrizzleConfig(options.config);
|
|
1915
|
+
const args = ["--config", configPath];
|
|
1916
|
+
if (options.port) {
|
|
1917
|
+
args.push("--port", options.port.toString());
|
|
1918
|
+
}
|
|
1919
|
+
const result = await runDrizzleKit("studio", args);
|
|
1920
|
+
if (!result.success) {
|
|
1921
|
+
console.error(`
|
|
1922
|
+
\x1B[31m\u2717\x1B[0m ${result.error}
|
|
1923
|
+
`);
|
|
1924
|
+
process.exit(1);
|
|
1925
|
+
}
|
|
1926
|
+
} catch (error) {
|
|
1927
|
+
console.error(`
|
|
1928
|
+
\x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
|
|
1929
|
+
`);
|
|
1930
|
+
process.exit(1);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
async function dbPush(options = {}) {
|
|
1934
|
+
console.log(`
|
|
1935
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mPushing schema to database...\x1B[0m`);
|
|
1936
|
+
console.log(` \x1B[33m\u26A0\x1B[0m This should only be used in development
|
|
1937
|
+
`);
|
|
1938
|
+
try {
|
|
1939
|
+
const configPath = findDrizzleConfig(options.config);
|
|
1940
|
+
const args = ["--config", configPath];
|
|
1941
|
+
if (options.force) {
|
|
1942
|
+
args.push("--force");
|
|
1943
|
+
}
|
|
1944
|
+
if (options.verbose) {
|
|
1945
|
+
args.push("--verbose");
|
|
1946
|
+
}
|
|
1947
|
+
const result = await runDrizzleKit("push", args);
|
|
1948
|
+
if (result.success) {
|
|
1949
|
+
console.log(`
|
|
1950
|
+
\x1B[32m\u2713\x1B[0m Schema pushed successfully
|
|
1951
|
+
`);
|
|
1952
|
+
} else {
|
|
1953
|
+
console.error(`
|
|
1954
|
+
\x1B[31m\u2717\x1B[0m ${result.error}
|
|
1955
|
+
`);
|
|
1956
|
+
process.exit(1);
|
|
1957
|
+
}
|
|
1958
|
+
} catch (error) {
|
|
1959
|
+
console.error(`
|
|
1960
|
+
\x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
|
|
1961
|
+
`);
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
async function dbSeed(options = {}) {
|
|
1966
|
+
console.log(`
|
|
1967
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mRunning database seeders...\x1B[0m`);
|
|
1968
|
+
const cwd = process.cwd();
|
|
1969
|
+
const seedFile = options.file ?? findSeedFile(cwd);
|
|
1970
|
+
if (!seedFile) {
|
|
1971
|
+
console.error(`
|
|
1972
|
+
\x1B[31m\u2717\x1B[0m No seed file found.`);
|
|
1973
|
+
console.log(" Create a seed file at one of these locations:");
|
|
1974
|
+
console.log(" - db/seed.ts");
|
|
1975
|
+
console.log(" - src/db/seed.ts");
|
|
1976
|
+
console.log(" - seeds/index.ts");
|
|
1977
|
+
console.log(` Or specify a custom path with --file
|
|
1978
|
+
`);
|
|
1979
|
+
process.exit(1);
|
|
1980
|
+
}
|
|
1981
|
+
console.log(` Using seed file: ${seedFile}
|
|
1982
|
+
`);
|
|
1983
|
+
try {
|
|
1984
|
+
const runner = existsSync(join6(cwd, "bun.lockb")) ? "bun" : "npx";
|
|
1985
|
+
const runArgs = runner === "bun" ? ["run", seedFile] : ["tsx", seedFile];
|
|
1986
|
+
return new Promise((resolve2) => {
|
|
1987
|
+
const proc = spawn(runner, runArgs, {
|
|
1988
|
+
cwd,
|
|
1989
|
+
stdio: "inherit",
|
|
1990
|
+
shell: true,
|
|
1991
|
+
env: {
|
|
1992
|
+
...process.env,
|
|
1993
|
+
DB_SEED_RESET: options.reset ? "1" : ""
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
proc.on("close", (code) => {
|
|
1997
|
+
if (code === 0) {
|
|
1998
|
+
console.log(`
|
|
1999
|
+
\x1B[32m\u2713\x1B[0m Seeding completed successfully
|
|
2000
|
+
`);
|
|
2001
|
+
resolve2();
|
|
2002
|
+
} else {
|
|
2003
|
+
console.error(`
|
|
2004
|
+
\x1B[31m\u2717\x1B[0m Seeding failed with exit code ${code}
|
|
2005
|
+
`);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
proc.on("error", (error) => {
|
|
2010
|
+
console.error(`
|
|
2011
|
+
\x1B[31m\u2717\x1B[0m ${error.message}
|
|
2012
|
+
`);
|
|
2013
|
+
process.exit(1);
|
|
2014
|
+
});
|
|
2015
|
+
});
|
|
2016
|
+
} catch (error) {
|
|
2017
|
+
console.error(`
|
|
2018
|
+
\x1B[31m\u2717\x1B[0m ${error instanceof Error ? error.message : error}
|
|
2019
|
+
`);
|
|
2020
|
+
process.exit(1);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
function findSeedFile(cwd) {
|
|
2024
|
+
const locations = [
|
|
2025
|
+
"db/seed.ts",
|
|
2026
|
+
"db/seed.js",
|
|
2027
|
+
"src/db/seed.ts",
|
|
2028
|
+
"src/db/seed.js",
|
|
2029
|
+
"seeds/index.ts",
|
|
2030
|
+
"seeds/index.js",
|
|
2031
|
+
"drizzle/seed.ts",
|
|
2032
|
+
"drizzle/seed.js"
|
|
2033
|
+
];
|
|
2034
|
+
for (const loc of locations) {
|
|
2035
|
+
const fullPath = join6(cwd, loc);
|
|
2036
|
+
if (existsSync(fullPath)) {
|
|
2037
|
+
return fullPath;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
return null;
|
|
2041
|
+
}
|
|
2042
|
+
function printDbHelp() {
|
|
2043
|
+
console.log(`
|
|
2044
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo Database Commands\x1B[0m
|
|
2045
|
+
|
|
2046
|
+
\x1B[1mUsage:\x1B[0m
|
|
2047
|
+
ereo db:<command> [options]
|
|
2048
|
+
|
|
2049
|
+
\x1B[1mCommands:\x1B[0m
|
|
2050
|
+
db:migrate Run pending database migrations
|
|
2051
|
+
db:generate --name <n> Generate migration from schema changes
|
|
2052
|
+
db:studio Open Drizzle Studio GUI
|
|
2053
|
+
db:push Push schema directly (dev only)
|
|
2054
|
+
db:seed Run database seeders
|
|
2055
|
+
|
|
2056
|
+
\x1B[1mMigrate Options:\x1B[0m
|
|
2057
|
+
--config <path> Path to drizzle config file
|
|
2058
|
+
--verbose Enable verbose output
|
|
2059
|
+
|
|
2060
|
+
\x1B[1mGenerate Options:\x1B[0m
|
|
2061
|
+
--name <name> Migration name (required)
|
|
2062
|
+
--config <path> Path to drizzle config file
|
|
2063
|
+
--out <dir> Output directory for migrations
|
|
2064
|
+
|
|
2065
|
+
\x1B[1mStudio Options:\x1B[0m
|
|
2066
|
+
--port <port> Port for Drizzle Studio
|
|
2067
|
+
--config <path> Path to drizzle config file
|
|
2068
|
+
|
|
2069
|
+
\x1B[1mPush Options:\x1B[0m
|
|
2070
|
+
--config <path> Path to drizzle config file
|
|
2071
|
+
--force Skip confirmation prompts
|
|
2072
|
+
--verbose Enable verbose output
|
|
2073
|
+
|
|
2074
|
+
\x1B[1mSeed Options:\x1B[0m
|
|
2075
|
+
--file <path> Path to seed file
|
|
2076
|
+
--reset Reset database before seeding
|
|
2077
|
+
|
|
2078
|
+
\x1B[1mExamples:\x1B[0m
|
|
2079
|
+
ereo db:generate --name add_users_table
|
|
2080
|
+
ereo db:migrate
|
|
2081
|
+
ereo db:studio --port 4000
|
|
2082
|
+
ereo db:push --force
|
|
2083
|
+
ereo db:seed --reset
|
|
2084
|
+
`);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
// src/index.ts
|
|
2088
|
+
var VERSION = "0.1.0";
|
|
2089
|
+
function printHelp() {
|
|
2090
|
+
console.log(`
|
|
2091
|
+
\x1B[36m\u2B21\x1B[0m \x1B[1mEreo\x1B[0m - React Fullstack Framework
|
|
2092
|
+
|
|
2093
|
+
\x1B[1mUsage:\x1B[0m
|
|
2094
|
+
ereo <command> [options]
|
|
2095
|
+
|
|
2096
|
+
\x1B[1mCommands:\x1B[0m
|
|
2097
|
+
dev Start development server
|
|
2098
|
+
build Build for production
|
|
2099
|
+
start Start production server
|
|
2100
|
+
create Create new project
|
|
2101
|
+
deploy Deploy to production
|
|
2102
|
+
db:* Database commands (db:migrate, db:generate, db:studio, db:push, db:seed)
|
|
2103
|
+
|
|
2104
|
+
\x1B[1mDev Options:\x1B[0m
|
|
2105
|
+
--port, -p Port number (default: 3000)
|
|
2106
|
+
--host, -h Host name (default: localhost)
|
|
2107
|
+
--open, -o Open browser
|
|
2108
|
+
|
|
2109
|
+
\x1B[1mBuild Options:\x1B[0m
|
|
2110
|
+
--outDir Output directory (default: .ereo)
|
|
2111
|
+
--minify Enable minification (default: true)
|
|
2112
|
+
--sourcemap Generate sourcemaps (default: true)
|
|
2113
|
+
|
|
2114
|
+
\x1B[1mStart Options:\x1B[0m
|
|
2115
|
+
--port, -p Port number (default: 3000)
|
|
2116
|
+
--host, -h Host name (default: 0.0.0.0)
|
|
2117
|
+
|
|
2118
|
+
\x1B[1mCreate Options:\x1B[0m
|
|
2119
|
+
--template, -t Template (minimal, default, tailwind)
|
|
2120
|
+
--typescript Use TypeScript (default: true)
|
|
2121
|
+
|
|
2122
|
+
\x1B[1mDeploy Options:\x1B[0m
|
|
2123
|
+
--prod Deploy to production
|
|
2124
|
+
--dry-run Preview deployment
|
|
2125
|
+
--name Project name
|
|
2126
|
+
|
|
2127
|
+
\x1B[1mDatabase Commands:\x1B[0m
|
|
2128
|
+
ereo db:migrate Run pending migrations
|
|
2129
|
+
ereo db:generate --name Generate migration from schema
|
|
2130
|
+
ereo db:studio Open Drizzle Studio
|
|
2131
|
+
ereo db:push Push schema (dev only)
|
|
2132
|
+
ereo db:seed Run database seeders
|
|
2133
|
+
|
|
2134
|
+
\x1B[1mExamples:\x1B[0m
|
|
2135
|
+
ereo dev --port 8080
|
|
2136
|
+
ereo build --minify
|
|
2137
|
+
ereo start --port 3001
|
|
2138
|
+
ereo create my-app --template tailwind
|
|
2139
|
+
ereo deploy vercel --prod
|
|
2140
|
+
ereo db:generate --name add_users
|
|
2141
|
+
|
|
2142
|
+
Version: ${VERSION}
|
|
2143
|
+
`);
|
|
2144
|
+
}
|
|
2145
|
+
function parseArgs(args) {
|
|
2146
|
+
const options = {};
|
|
2147
|
+
const positional = [];
|
|
2148
|
+
let command = "";
|
|
2149
|
+
for (let i = 0;i < args.length; i++) {
|
|
2150
|
+
const arg = args[i];
|
|
2151
|
+
if (!command && !arg.startsWith("-")) {
|
|
2152
|
+
command = arg;
|
|
2153
|
+
continue;
|
|
2154
|
+
}
|
|
2155
|
+
if (arg.startsWith("--")) {
|
|
2156
|
+
const [key, value] = arg.slice(2).split("=");
|
|
2157
|
+
if (value !== undefined) {
|
|
2158
|
+
options[key] = value;
|
|
2159
|
+
} else if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
2160
|
+
options[key] = args[++i];
|
|
2161
|
+
} else {
|
|
2162
|
+
options[key] = true;
|
|
2163
|
+
}
|
|
2164
|
+
} else if (arg.startsWith("-")) {
|
|
2165
|
+
const key = arg.slice(1);
|
|
2166
|
+
if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
2167
|
+
options[key] = args[++i];
|
|
2168
|
+
} else {
|
|
2169
|
+
options[key] = true;
|
|
2170
|
+
}
|
|
2171
|
+
} else {
|
|
2172
|
+
positional.push(arg);
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
return { command, options, positional };
|
|
2176
|
+
}
|
|
2177
|
+
async function main() {
|
|
2178
|
+
const args = process.argv.slice(2);
|
|
2179
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
2180
|
+
printHelp();
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
2184
|
+
console.log(VERSION);
|
|
2185
|
+
return;
|
|
2186
|
+
}
|
|
2187
|
+
const { command, options, positional } = parseArgs(args);
|
|
2188
|
+
try {
|
|
2189
|
+
switch (command) {
|
|
2190
|
+
case "dev": {
|
|
2191
|
+
const devOptions = {
|
|
2192
|
+
port: options.port ? parseInt(options.port) : undefined,
|
|
2193
|
+
host: options.host || options.h,
|
|
2194
|
+
open: !!(options.open || options.o)
|
|
2195
|
+
};
|
|
2196
|
+
await dev(devOptions);
|
|
2197
|
+
break;
|
|
2198
|
+
}
|
|
2199
|
+
case "build": {
|
|
2200
|
+
const buildOptions = {
|
|
2201
|
+
outDir: options.outDir,
|
|
2202
|
+
minify: options.minify === "false" ? false : true,
|
|
2203
|
+
sourcemap: options.sourcemap === "false" ? false : true
|
|
2204
|
+
};
|
|
2205
|
+
await build(buildOptions);
|
|
2206
|
+
break;
|
|
2207
|
+
}
|
|
2208
|
+
case "start": {
|
|
2209
|
+
const startOptions = {
|
|
2210
|
+
port: options.port ? parseInt(options.port) : undefined,
|
|
2211
|
+
host: options.host || options.h
|
|
2212
|
+
};
|
|
2213
|
+
await start(startOptions);
|
|
2214
|
+
break;
|
|
2215
|
+
}
|
|
2216
|
+
case "create": {
|
|
2217
|
+
const projectName = positional[0];
|
|
2218
|
+
if (!projectName) {
|
|
2219
|
+
console.error(`
|
|
2220
|
+
\x1B[31m\u2717\x1B[0m Please provide a project name
|
|
2221
|
+
`);
|
|
2222
|
+
console.log(` Usage: ereo create <project-name> [options]
|
|
2223
|
+
`);
|
|
2224
|
+
process.exit(1);
|
|
2225
|
+
}
|
|
2226
|
+
const createOptions = {
|
|
2227
|
+
template: options.template || options.t,
|
|
2228
|
+
typescript: options.typescript !== "false"
|
|
2229
|
+
};
|
|
2230
|
+
await create(projectName, createOptions);
|
|
2231
|
+
break;
|
|
2232
|
+
}
|
|
2233
|
+
case "deploy": {
|
|
2234
|
+
if (options.help || options.h) {
|
|
2235
|
+
printDeployHelp();
|
|
2236
|
+
break;
|
|
2237
|
+
}
|
|
2238
|
+
const target = positional[0];
|
|
2239
|
+
const deployOptions = {
|
|
2240
|
+
target,
|
|
2241
|
+
production: !!(options.prod || options.production),
|
|
2242
|
+
dryRun: !!(options["dry-run"] || options.dryRun),
|
|
2243
|
+
name: options.name,
|
|
2244
|
+
build: options["no-build"] ? false : true
|
|
2245
|
+
};
|
|
2246
|
+
const result = await deploy(deployOptions);
|
|
2247
|
+
if (!result.success) {
|
|
2248
|
+
console.error(`
|
|
2249
|
+
\x1B[31m\u2717\x1B[0m ${result.error}
|
|
2250
|
+
`);
|
|
2251
|
+
process.exit(1);
|
|
2252
|
+
}
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
case "db:migrate": {
|
|
2256
|
+
const migrateOptions = {
|
|
2257
|
+
config: options.config,
|
|
2258
|
+
verbose: !!(options.verbose || options.v)
|
|
2259
|
+
};
|
|
2260
|
+
await dbMigrate(migrateOptions);
|
|
2261
|
+
break;
|
|
2262
|
+
}
|
|
2263
|
+
case "db:generate": {
|
|
2264
|
+
const generateOptions = {
|
|
2265
|
+
name: options.name || positional[0],
|
|
2266
|
+
config: options.config,
|
|
2267
|
+
out: options.out
|
|
2268
|
+
};
|
|
2269
|
+
await dbGenerate(generateOptions);
|
|
2270
|
+
break;
|
|
2271
|
+
}
|
|
2272
|
+
case "db:studio": {
|
|
2273
|
+
const studioOptions = {
|
|
2274
|
+
port: options.port ? parseInt(options.port) : undefined,
|
|
2275
|
+
config: options.config,
|
|
2276
|
+
open: options.open !== false
|
|
2277
|
+
};
|
|
2278
|
+
await dbStudio(studioOptions);
|
|
2279
|
+
break;
|
|
2280
|
+
}
|
|
2281
|
+
case "db:push": {
|
|
2282
|
+
const pushOptions = {
|
|
2283
|
+
config: options.config,
|
|
2284
|
+
force: !!(options.force || options.f),
|
|
2285
|
+
verbose: !!(options.verbose || options.v)
|
|
2286
|
+
};
|
|
2287
|
+
await dbPush(pushOptions);
|
|
2288
|
+
break;
|
|
2289
|
+
}
|
|
2290
|
+
case "db:seed": {
|
|
2291
|
+
const seedOptions = {
|
|
2292
|
+
file: options.file,
|
|
2293
|
+
reset: !!(options.reset || options.r)
|
|
2294
|
+
};
|
|
2295
|
+
await dbSeed(seedOptions);
|
|
2296
|
+
break;
|
|
2297
|
+
}
|
|
2298
|
+
case "db":
|
|
2299
|
+
case "db:help": {
|
|
2300
|
+
printDbHelp();
|
|
2301
|
+
break;
|
|
2302
|
+
}
|
|
2303
|
+
default:
|
|
2304
|
+
if (command.startsWith("db:")) {
|
|
2305
|
+
console.error(`
|
|
2306
|
+
\x1B[31m\u2717\x1B[0m Unknown database command: ${command}
|
|
2307
|
+
`);
|
|
2308
|
+
printDbHelp();
|
|
2309
|
+
process.exit(1);
|
|
2310
|
+
}
|
|
2311
|
+
console.error(`
|
|
2312
|
+
\x1B[31m\u2717\x1B[0m Unknown command: ${command}
|
|
2313
|
+
`);
|
|
2314
|
+
printHelp();
|
|
2315
|
+
process.exit(1);
|
|
2316
|
+
}
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
console.error(`
|
|
2319
|
+
\x1B[31m\u2717\x1B[0m Error:`, error instanceof Error ? error.message : error);
|
|
2320
|
+
process.exit(1);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
main().catch(console.error);
|
|
2324
|
+
export {
|
|
2325
|
+
start,
|
|
2326
|
+
dev,
|
|
2327
|
+
deploy,
|
|
2328
|
+
dbStudio,
|
|
2329
|
+
dbSeed,
|
|
2330
|
+
dbPush,
|
|
2331
|
+
dbMigrate,
|
|
2332
|
+
dbGenerate,
|
|
2333
|
+
create,
|
|
2334
|
+
build
|
|
2335
|
+
};
|