@ubean/cli 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +2134 -0
- package/dist/index.d.ts +133 -0
- package/dist/index.js +2 -0
- package/dist/page-Cmp7s9l2.js +672 -0
- package/package.json +11 -11
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2134 @@
|
|
|
1
|
+
import { S as createFsOps, a as scaffold, i as recoverScaffold, n as listScaffoldableFiles, r as pageCommand, t as deleteScaffold, v as renderTemplate } from "./page-Cmp7s9l2.js";
|
|
2
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { extname, normalize } from "node:path";
|
|
4
|
+
import { consola } from "consola";
|
|
5
|
+
import { defineCommand, runMain } from "citty";
|
|
6
|
+
import { readFile, rm, stat } from "node:fs/promises";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { buildProduction } from "@ubean/build/production";
|
|
9
|
+
import { generateOpenApiTypesFromServer, generateTypes } from "@ubean/codegen";
|
|
10
|
+
import { loadUbeanConfig, resolvePrerenderConfig } from "@ubean/config";
|
|
11
|
+
import { prerender } from "@ubean/prerender";
|
|
12
|
+
import { NODE_REQUIREMENTS, createCapabilitySet, diagnoseCapabilities, registerBuiltinPresets, resolvePresetByName } from "@ubean/preset";
|
|
13
|
+
import { scanProject } from "@ubean/routing";
|
|
14
|
+
import { basename, join as join$1, resolve as resolve$1 } from "pathe";
|
|
15
|
+
import { createUbeanApp } from "@ubean/app";
|
|
16
|
+
import { createDevRunner, createDevWatcher, logDiagnostics } from "@ubean/dev-server";
|
|
17
|
+
import { bold, cyan, dim, green } from "kolorist";
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { createServer } from "node:http";
|
|
20
|
+
import { findAvailablePort, waitForPort } from "@ubean/utils";
|
|
21
|
+
//#region src/config.ts
|
|
22
|
+
const logger$8 = consola.withTag("ubean-cli");
|
|
23
|
+
const CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
|
|
24
|
+
|
|
25
|
+
export default defineConfig({
|
|
26
|
+
srcDir: 'src',
|
|
27
|
+
preset: '{{preset}}'
|
|
28
|
+
});
|
|
29
|
+
`;
|
|
30
|
+
const CONFIG_EXAMPLE_FULL = `import { defineConfig } from 'ubean';
|
|
31
|
+
|
|
32
|
+
export default defineConfig({
|
|
33
|
+
// Source directory
|
|
34
|
+
srcDir: 'src',
|
|
35
|
+
|
|
36
|
+
// Preset: 'standard' | 'node' | 'cloudflare'
|
|
37
|
+
preset: 'standard',
|
|
38
|
+
|
|
39
|
+
// Dev server options
|
|
40
|
+
dev: {
|
|
41
|
+
port: 9527,
|
|
42
|
+
host: 'localhost',
|
|
43
|
+
open: false
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
// Build options
|
|
47
|
+
build: {
|
|
48
|
+
outDir: 'dist'
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
// View transitions
|
|
52
|
+
viewTransitions: {
|
|
53
|
+
enabled: true
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// DevTools
|
|
57
|
+
devtools: {
|
|
58
|
+
enabled: true
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
`;
|
|
62
|
+
async function findConfigFile(fs) {
|
|
63
|
+
for (const name of [
|
|
64
|
+
"ubean.config.ts",
|
|
65
|
+
"ubean.config.js",
|
|
66
|
+
"ubean.config.mjs",
|
|
67
|
+
"ubean.config.mts"
|
|
68
|
+
]) if (await fs.exists(name)) return name;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const configCommand = {
|
|
72
|
+
meta: {
|
|
73
|
+
name: "config",
|
|
74
|
+
description: "Manage ubean configuration"
|
|
75
|
+
},
|
|
76
|
+
subCommands: {
|
|
77
|
+
init: {
|
|
78
|
+
meta: {
|
|
79
|
+
name: "init",
|
|
80
|
+
description: "Create a default ubean.config.ts file"
|
|
81
|
+
},
|
|
82
|
+
args: {
|
|
83
|
+
preset: {
|
|
84
|
+
type: "string",
|
|
85
|
+
description: "Preset to use (standard, node, cloudflare)",
|
|
86
|
+
default: "standard"
|
|
87
|
+
},
|
|
88
|
+
force: {
|
|
89
|
+
type: "boolean",
|
|
90
|
+
description: "Overwrite existing config file",
|
|
91
|
+
default: false,
|
|
92
|
+
alias: "f"
|
|
93
|
+
},
|
|
94
|
+
dry: {
|
|
95
|
+
type: "boolean",
|
|
96
|
+
description: "Show what would be created",
|
|
97
|
+
default: false
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
async run({ args }) {
|
|
101
|
+
const fs = createFsOps(process.cwd());
|
|
102
|
+
const preset = args.preset;
|
|
103
|
+
const existing = await findConfigFile(fs);
|
|
104
|
+
if (existing && !args.force) {
|
|
105
|
+
logger$8.warn(`Config file already exists: ${existing} (use --force to overwrite)`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const content = renderTemplate(CONFIG_TEMPLATE, { variables: { preset } });
|
|
109
|
+
if (args.dry) {
|
|
110
|
+
logger$8.success("[dry-run] Would create ubean.config.ts");
|
|
111
|
+
logger$8.log(`\n${content}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await fs.writeFile("ubean.config.ts", content);
|
|
115
|
+
logger$8.success("Created ubean.config.ts");
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
show: {
|
|
119
|
+
meta: {
|
|
120
|
+
name: "show",
|
|
121
|
+
description: "Show current configuration file location and content"
|
|
122
|
+
},
|
|
123
|
+
async run() {
|
|
124
|
+
const fs = createFsOps(process.cwd());
|
|
125
|
+
const configFile = await findConfigFile(fs);
|
|
126
|
+
if (!configFile) {
|
|
127
|
+
logger$8.warn("No ubean config file found. Run `ubean config init` to create one.");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
logger$8.info(`Config file: ${configFile}`);
|
|
131
|
+
logger$8.log("");
|
|
132
|
+
const content = await fs.readFile(configFile);
|
|
133
|
+
logger$8.log(content);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
example: {
|
|
137
|
+
meta: {
|
|
138
|
+
name: "example",
|
|
139
|
+
description: "Show a full example configuration with all options"
|
|
140
|
+
},
|
|
141
|
+
async run() {
|
|
142
|
+
logger$8.log(CONFIG_EXAMPLE_FULL);
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
path: {
|
|
146
|
+
meta: {
|
|
147
|
+
name: "path",
|
|
148
|
+
description: "Print resolved config file path"
|
|
149
|
+
},
|
|
150
|
+
async run() {
|
|
151
|
+
const fs = createFsOps(process.cwd());
|
|
152
|
+
const configFile = await findConfigFile(fs);
|
|
153
|
+
if (!configFile) {
|
|
154
|
+
logger$8.error("No config file found");
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
logger$8.log(fs.resolve(configFile));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/build.ts
|
|
164
|
+
const logger$7 = consola.withTag("ubean-cli");
|
|
165
|
+
/**
|
|
166
|
+
* Creates a fetcher that invokes the built SSR entry to render real HTML.
|
|
167
|
+
* Inspired by void's Node prerender runner: import the built `entry.mjs`,
|
|
168
|
+
* call its `createFetchHandler()` to obtain a `fetch(req)` function, then
|
|
169
|
+
* drive it with synthetic `http://localhost<path>` requests.
|
|
170
|
+
*
|
|
171
|
+
* If the SSR entry cannot be loaded (e.g. missing dependencies), returns
|
|
172
|
+
* `undefined` so the caller falls back to the existing placeholder behavior.
|
|
173
|
+
*/
|
|
174
|
+
async function createSsrFetcher(cwd, manifest) {
|
|
175
|
+
try {
|
|
176
|
+
const mod = await import(pathToFileURL(resolve$1(cwd, manifest.serverDir, "entry.mjs")).href);
|
|
177
|
+
const createFetchHandler = mod.default ?? mod.createFetchHandler;
|
|
178
|
+
if (typeof createFetchHandler !== "function") {
|
|
179
|
+
logger$7.warn("SSR entry does not export createFetchHandler; falling back to placeholder prerender.");
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const fetch = await createFetchHandler();
|
|
183
|
+
if (typeof fetch !== "function") {
|
|
184
|
+
logger$7.warn("SSR entry did not return a fetch handler; falling back to placeholder prerender.");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
return async (url) => {
|
|
188
|
+
const req = new Request(`http://localhost${url || "/"}`, { headers: { "x-ubean-prerender": "1" } });
|
|
189
|
+
const res = await fetch(req);
|
|
190
|
+
return {
|
|
191
|
+
html: typeof res.text === "function" ? await res.text() : String(res.body ?? ""),
|
|
192
|
+
statusCode: res.status ?? 200
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
} catch (err) {
|
|
196
|
+
logger$7.warn(`Failed to load SSR entry for prerender: ${err instanceof Error ? err.message : String(err)}. Falling back to placeholder prerender.`);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const buildCommand = {
|
|
201
|
+
meta: {
|
|
202
|
+
name: "build",
|
|
203
|
+
description: "Build the ubean application for production"
|
|
204
|
+
},
|
|
205
|
+
args: {
|
|
206
|
+
preset: {
|
|
207
|
+
type: "string",
|
|
208
|
+
description: "Deployment preset (node, bun, deno, cloudflare, vercel, netlify, standard)"
|
|
209
|
+
},
|
|
210
|
+
mode: {
|
|
211
|
+
type: "string",
|
|
212
|
+
description: "App mode (fullstack, spa, ssg, backend). Overrides ubean.config.ts mode"
|
|
213
|
+
},
|
|
214
|
+
ssr: {
|
|
215
|
+
type: "boolean",
|
|
216
|
+
description: "Enable SSR in fullstack mode (only effective with --mode fullstack). Use --no-ssr to disable"
|
|
217
|
+
},
|
|
218
|
+
ssg: {
|
|
219
|
+
type: "boolean",
|
|
220
|
+
description: "Shortcut for --mode ssg",
|
|
221
|
+
default: false
|
|
222
|
+
},
|
|
223
|
+
minify: {
|
|
224
|
+
type: "boolean",
|
|
225
|
+
description: "Minify output",
|
|
226
|
+
default: true
|
|
227
|
+
},
|
|
228
|
+
sourcemap: {
|
|
229
|
+
type: "boolean",
|
|
230
|
+
description: "Generate sourcemaps",
|
|
231
|
+
default: false
|
|
232
|
+
},
|
|
233
|
+
prerender: {
|
|
234
|
+
type: "boolean",
|
|
235
|
+
description: "Pre-render static pages (SSG)"
|
|
236
|
+
},
|
|
237
|
+
cwd: {
|
|
238
|
+
type: "string",
|
|
239
|
+
description: "Project root directory",
|
|
240
|
+
default: "."
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
async run({ args }) {
|
|
244
|
+
const cwd = resolve$1(args.cwd || process.cwd());
|
|
245
|
+
logger$7.start("Building ubean application...");
|
|
246
|
+
try {
|
|
247
|
+
registerBuiltinPresets();
|
|
248
|
+
const config = await loadUbeanConfig(cwd);
|
|
249
|
+
if (args.ssg) config.mode = "ssg";
|
|
250
|
+
else if (args.mode) config.mode = args.mode;
|
|
251
|
+
if (config.mode === "fullstack" && args.ssr !== void 0) config.ssr = args.ssr;
|
|
252
|
+
if (config.mode === "ssg") {
|
|
253
|
+
if (!config.prerender.all && config.prerender.include.length === 0) config.prerender = resolvePrerenderConfig({ all: true });
|
|
254
|
+
} else if (config.mode === "spa" || config.mode === "backend") config.prerender = resolvePrerenderConfig();
|
|
255
|
+
else if (config.mode === "fullstack" && !config.ssr) config.prerender = resolvePrerenderConfig();
|
|
256
|
+
if (args.prerender === true) {
|
|
257
|
+
if (!config.prerender.enabled) config.prerender = resolvePrerenderConfig({ all: true });
|
|
258
|
+
} else if (args.prerender === false) config.prerender = resolvePrerenderConfig();
|
|
259
|
+
const preset = resolvePresetByName(args.preset || config.build.preset);
|
|
260
|
+
logger$7.info(`Using preset: ${preset.name}`);
|
|
261
|
+
logger$7.info(`App mode: ${config.mode}${config.mode === "fullstack" ? ` (ssr=${config.ssr})` : ""}`);
|
|
262
|
+
logger$7.info("Scanning project...");
|
|
263
|
+
const result = await scanProject({
|
|
264
|
+
cwd,
|
|
265
|
+
srcDir: config.srcDir,
|
|
266
|
+
dirs: config.dir,
|
|
267
|
+
ignore: config.scanOptions?.ignore
|
|
268
|
+
});
|
|
269
|
+
logger$7.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts`);
|
|
270
|
+
logger$7.info("Generating types...");
|
|
271
|
+
await generateTypes(result, {
|
|
272
|
+
cwd,
|
|
273
|
+
srcDir: config.srcDir,
|
|
274
|
+
buildDir: ".ubean",
|
|
275
|
+
dirs: config.dir,
|
|
276
|
+
imports: config.imports,
|
|
277
|
+
components: config.components,
|
|
278
|
+
composablesDirs: config.imports.dirs,
|
|
279
|
+
componentsDirs: config.components.dirs
|
|
280
|
+
});
|
|
281
|
+
const resolvedPreset = preset;
|
|
282
|
+
if (resolvedPreset.hooks?.["build:before"]) await resolvedPreset.hooks["build:before"]({
|
|
283
|
+
cwd,
|
|
284
|
+
outputDir: config.build.outputDir,
|
|
285
|
+
preset: resolvedPreset,
|
|
286
|
+
config
|
|
287
|
+
});
|
|
288
|
+
logger$7.info("Building with Vite...");
|
|
289
|
+
const manifest = await buildProduction({
|
|
290
|
+
cwd,
|
|
291
|
+
config,
|
|
292
|
+
preset: resolvedPreset,
|
|
293
|
+
scanResult: result,
|
|
294
|
+
minify: args.minify,
|
|
295
|
+
sourcemap: args.sourcemap
|
|
296
|
+
});
|
|
297
|
+
if (config.prerender.enabled) {
|
|
298
|
+
logger$7.info("Prerendering static pages...");
|
|
299
|
+
const fetcher = await createSsrFetcher(cwd, manifest);
|
|
300
|
+
await prerender({
|
|
301
|
+
cwd,
|
|
302
|
+
outputDir: config.build.outputDir,
|
|
303
|
+
pages: result.pages,
|
|
304
|
+
prerender: config.prerender,
|
|
305
|
+
fetcher
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (config.mode === "ssg") {
|
|
309
|
+
const serverDir = join$1(cwd, config.build.outputDir, "server");
|
|
310
|
+
if (existsSync(serverDir)) {
|
|
311
|
+
logger$7.info("Cleaning temporary SSR bundle (SSG mode)...");
|
|
312
|
+
await rm(serverDir, {
|
|
313
|
+
recursive: true,
|
|
314
|
+
force: true
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (resolvedPreset.hooks?.["build:after"]) await resolvedPreset.hooks["build:after"]({
|
|
319
|
+
cwd,
|
|
320
|
+
outputDir: config.build.outputDir,
|
|
321
|
+
preset: resolvedPreset,
|
|
322
|
+
config,
|
|
323
|
+
manifest
|
|
324
|
+
});
|
|
325
|
+
logger$7.success(`Build complete for preset "${resolvedPreset.name}"!`);
|
|
326
|
+
logger$7.info(` Output directory: ${resolve$1(cwd, config.build.outputDir)}`);
|
|
327
|
+
if (manifest.entry) logger$7.info(` Server entry: ${manifest.entry}`);
|
|
328
|
+
logger$7.info(` Client assets: ${manifest.assets.length} files`);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
logger$7.error(err instanceof Error ? err.message : String(err));
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|
|
333
|
+
process.exit(0);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/dev.ts
|
|
338
|
+
const logger$6 = consola.withTag("ubean-cli");
|
|
339
|
+
/**
|
|
340
|
+
* Best-effort loader for the optional `@ubean/devtools` peer dependency.
|
|
341
|
+
*
|
|
342
|
+
* `getCustomTabs()` returns user-registered DevTools tabs (registered via
|
|
343
|
+
* `defineDevToolsTab`). `@ubean/devtools` is an optional peer dep — when not
|
|
344
|
+
* installed, fall back to an empty list. Future versions of `@ubean/devtools`
|
|
345
|
+
* may expose `getCustomTabs` from its main entry; we try dynamic import on
|
|
346
|
+
* startup and replace the placeholder if the export is available.
|
|
347
|
+
*/
|
|
348
|
+
let _customTabsGetter = () => [];
|
|
349
|
+
import("@ubean/devtools").then((mod) => {
|
|
350
|
+
if (mod && typeof mod.getCustomTabs === "function") _customTabsGetter = mod.getCustomTabs;
|
|
351
|
+
}).catch(() => {});
|
|
352
|
+
function getCustomTabs() {
|
|
353
|
+
return _customTabsGetter();
|
|
354
|
+
}
|
|
355
|
+
const devCommand = {
|
|
356
|
+
meta: {
|
|
357
|
+
name: "dev",
|
|
358
|
+
description: "Start the ubean development server"
|
|
359
|
+
},
|
|
360
|
+
args: {
|
|
361
|
+
port: {
|
|
362
|
+
type: "string",
|
|
363
|
+
description: "Port to listen on",
|
|
364
|
+
default: "9527"
|
|
365
|
+
},
|
|
366
|
+
host: {
|
|
367
|
+
type: "string",
|
|
368
|
+
description: "Host to listen on",
|
|
369
|
+
default: "localhost"
|
|
370
|
+
},
|
|
371
|
+
strictPort: {
|
|
372
|
+
type: "boolean",
|
|
373
|
+
description: "Exit if the port is already in use, instead of auto-incrementing",
|
|
374
|
+
default: false
|
|
375
|
+
},
|
|
376
|
+
open: {
|
|
377
|
+
type: "boolean",
|
|
378
|
+
description: "Open browser on startup",
|
|
379
|
+
default: false
|
|
380
|
+
},
|
|
381
|
+
cwd: {
|
|
382
|
+
type: "string",
|
|
383
|
+
description: "Project root directory",
|
|
384
|
+
default: "."
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
async run({ args }) {
|
|
388
|
+
const cwd = resolve$1(args.cwd || process.cwd());
|
|
389
|
+
logger$6.start("Starting ubean dev server...");
|
|
390
|
+
registerBuiltinPresets();
|
|
391
|
+
const config = await loadUbeanConfig(cwd);
|
|
392
|
+
const port = Number(args.port) || config.dev.port;
|
|
393
|
+
const host = args.host || config.dev.host;
|
|
394
|
+
logger$6.info(`Root directory: ${config.rootDir}`);
|
|
395
|
+
logger$6.info(`Source directory: ${config.srcDir}`);
|
|
396
|
+
const preset = resolvePresetByName(config.build.preset);
|
|
397
|
+
logger$6.info(`Preset: ${preset.name}`);
|
|
398
|
+
const capabilities = createCapabilitySet(preset.capabilities || {});
|
|
399
|
+
logger$6.info("Running capability diagnostics...");
|
|
400
|
+
const diagnostics = diagnoseCapabilities(preset.name, preset.capabilities || {}, NODE_REQUIREMENTS);
|
|
401
|
+
logDiagnostics(diagnostics);
|
|
402
|
+
if (!diagnostics.valid) logger$6.warn("Some capability requirements are not met. Dev server may not function correctly.");
|
|
403
|
+
let { app: currentApp, layouts: currentLayouts, scanResult: currentScanResult } = await buildApp(cwd, config);
|
|
404
|
+
let rescanInProgress = false;
|
|
405
|
+
async function rescan() {
|
|
406
|
+
if (rescanInProgress) return;
|
|
407
|
+
rescanInProgress = true;
|
|
408
|
+
try {
|
|
409
|
+
const { app: newApp, layouts: newLayouts, scanResult } = await buildApp(cwd, config);
|
|
410
|
+
currentApp = newApp;
|
|
411
|
+
currentLayouts = newLayouts;
|
|
412
|
+
currentScanResult = scanResult;
|
|
413
|
+
runner.updateApp(currentApp, currentLayouts);
|
|
414
|
+
await runner.reload();
|
|
415
|
+
} catch (err) {
|
|
416
|
+
logger$6.error(`Rescan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
417
|
+
} finally {
|
|
418
|
+
rescanInProgress = false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const runner = await createDevRunner({
|
|
422
|
+
cwd,
|
|
423
|
+
srcDir: config.srcDir,
|
|
424
|
+
port,
|
|
425
|
+
host,
|
|
426
|
+
strictPort: args.strictPort,
|
|
427
|
+
preset,
|
|
428
|
+
config,
|
|
429
|
+
capabilities,
|
|
430
|
+
app: currentApp,
|
|
431
|
+
layouts: currentLayouts,
|
|
432
|
+
devtools: {
|
|
433
|
+
getScanResult: () => currentScanResult,
|
|
434
|
+
getConfigMeta: () => ({
|
|
435
|
+
preset: config.build.preset,
|
|
436
|
+
rootDir: config.rootDir,
|
|
437
|
+
srcDir: config.srcDir,
|
|
438
|
+
openAPI: {
|
|
439
|
+
enabled: true,
|
|
440
|
+
scalarPath: "/_scalar",
|
|
441
|
+
openAPIPath: "/_openapi.json"
|
|
442
|
+
},
|
|
443
|
+
dir: config.dir
|
|
444
|
+
}),
|
|
445
|
+
getCustomTabs: () => getCustomTabs(),
|
|
446
|
+
triggerRescan: () => rescan()
|
|
447
|
+
},
|
|
448
|
+
onListen({ url }) {
|
|
449
|
+
const label = (text) => dim(text);
|
|
450
|
+
const lines = [
|
|
451
|
+
`${green(bold("🚀 ubean dev server ready"))}\n`,
|
|
452
|
+
` → ${label("Local:")} ${cyan(url)}`,
|
|
453
|
+
` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`,
|
|
454
|
+
` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`
|
|
455
|
+
];
|
|
456
|
+
if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
|
|
457
|
+
lines.push(` → ${dim("Press Ctrl+C to stop")}`);
|
|
458
|
+
logger$6.box(lines.join("\n"));
|
|
459
|
+
generateOpenApiTypesFromServer(url, { outDir: resolve$1(cwd, ".ubean") }).then((filePath) => {
|
|
460
|
+
logger$6.success(`OpenAPI types generated: ${filePath}`);
|
|
461
|
+
}).catch((err) => {
|
|
462
|
+
logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
|
|
463
|
+
});
|
|
464
|
+
},
|
|
465
|
+
onBeforeReload() {
|
|
466
|
+
logger$6.info("Reloading...");
|
|
467
|
+
},
|
|
468
|
+
onAfterReload() {
|
|
469
|
+
logger$6.success("Reloaded");
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
try {
|
|
473
|
+
await runner.start();
|
|
474
|
+
} catch (err) {
|
|
475
|
+
logger$6.error(err?.message || String(err));
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
const watcher = createDevWatcher({
|
|
479
|
+
cwd,
|
|
480
|
+
dirs: [
|
|
481
|
+
"api",
|
|
482
|
+
"pages",
|
|
483
|
+
"middleware",
|
|
484
|
+
"layouts",
|
|
485
|
+
"plugins",
|
|
486
|
+
"app",
|
|
487
|
+
"routes"
|
|
488
|
+
].map((d) => `${config.srcDir}/${d}`),
|
|
489
|
+
ignore: [
|
|
490
|
+
"**/node_modules/**",
|
|
491
|
+
"**/.git/**",
|
|
492
|
+
"**/.ubean/**"
|
|
493
|
+
],
|
|
494
|
+
debounceMs: 150,
|
|
495
|
+
async onChange(events) {
|
|
496
|
+
const relevantEvents = events.filter((e) => /\.(ts|js|vue|mjs|cjs|json)$/.test(e.relativePath) && !e.relativePath.includes(".bak"));
|
|
497
|
+
if (relevantEvents.length === 0) return;
|
|
498
|
+
logger$6.info(`File change detected: ${relevantEvents[0].relativePath}`);
|
|
499
|
+
await rescan();
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
watcher.start();
|
|
503
|
+
const cleanup = async () => {
|
|
504
|
+
logger$6.info("\nShutting down...");
|
|
505
|
+
watcher.stop();
|
|
506
|
+
await runner.stop();
|
|
507
|
+
process.exit(0);
|
|
508
|
+
};
|
|
509
|
+
process.on("SIGINT", cleanup);
|
|
510
|
+
process.on("SIGTERM", cleanup);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
async function buildApp(cwd, config) {
|
|
514
|
+
logger$6.info("Scanning project files...");
|
|
515
|
+
const result = await scanProject({
|
|
516
|
+
cwd,
|
|
517
|
+
srcDir: config.srcDir,
|
|
518
|
+
dirs: config.dir,
|
|
519
|
+
ignore: config.scanOptions?.ignore
|
|
520
|
+
});
|
|
521
|
+
logger$6.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts, ${result.middlewares.length} middlewares, ${result.plugins.length} plugins`);
|
|
522
|
+
logger$6.info("Generating type definitions...");
|
|
523
|
+
await generateTypes(result, {
|
|
524
|
+
cwd,
|
|
525
|
+
srcDir: config.srcDir,
|
|
526
|
+
buildDir: ".ubean",
|
|
527
|
+
dirs: config.dir,
|
|
528
|
+
imports: config.imports,
|
|
529
|
+
components: config.components,
|
|
530
|
+
composablesDirs: config.imports.dirs,
|
|
531
|
+
componentsDirs: config.components.dirs
|
|
532
|
+
});
|
|
533
|
+
const app = createUbeanApp({
|
|
534
|
+
rootDir: cwd,
|
|
535
|
+
routes: result.apiRoutes,
|
|
536
|
+
middleware: result.middlewares,
|
|
537
|
+
pages: result.pages,
|
|
538
|
+
crons: result.crons,
|
|
539
|
+
routeRules: config.routeRules || {},
|
|
540
|
+
publicDir: config.dir.public,
|
|
541
|
+
openAPI: {
|
|
542
|
+
title: "UBEAN Dev API",
|
|
543
|
+
scalarPath: "/_scalar",
|
|
544
|
+
openAPIPath: "/_openapi.json"
|
|
545
|
+
},
|
|
546
|
+
i18nConfig: config.i18n
|
|
547
|
+
});
|
|
548
|
+
app.hooks.hook("request:start", (c) => {
|
|
549
|
+
logger$6.log(`${c.req.method} ${c.req.path}`);
|
|
550
|
+
});
|
|
551
|
+
return {
|
|
552
|
+
app,
|
|
553
|
+
layouts: result.layouts,
|
|
554
|
+
scanResult: result
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
//#endregion
|
|
558
|
+
//#region src/devtools.ts
|
|
559
|
+
const logger$5 = consola.withTag("ubean-cli");
|
|
560
|
+
const devtoolsCommand = {
|
|
561
|
+
meta: {
|
|
562
|
+
name: "devtools",
|
|
563
|
+
description: "Manage DevTools configuration and information"
|
|
564
|
+
},
|
|
565
|
+
subCommands: {
|
|
566
|
+
info: {
|
|
567
|
+
meta: {
|
|
568
|
+
name: "info",
|
|
569
|
+
description: "Show DevTools information"
|
|
570
|
+
},
|
|
571
|
+
async run() {
|
|
572
|
+
logger$5.info("DevTools Information");
|
|
573
|
+
logger$5.log("");
|
|
574
|
+
logger$5.log(" DevTools is built into ubean and available in dev mode.");
|
|
575
|
+
logger$5.log("");
|
|
576
|
+
logger$5.log(" Access:");
|
|
577
|
+
logger$5.log(" - Start dev server: pnpm dev");
|
|
578
|
+
logger$5.log(" - Open DevTools: Press Shift+Alt+D in the browser, or visit /__ubean/devtools/");
|
|
579
|
+
logger$5.log("");
|
|
580
|
+
logger$5.log(" Configuration (ubean.config.ts):");
|
|
581
|
+
logger$5.log(" devtools: {");
|
|
582
|
+
logger$5.log(" enabled: true, // Enable/disable DevTools (dev only)");
|
|
583
|
+
logger$5.log(" }");
|
|
584
|
+
logger$5.log("");
|
|
585
|
+
logger$5.log(" Features:");
|
|
586
|
+
logger$5.log(" - Routes: View all registered routes and their handlers");
|
|
587
|
+
logger$5.log(" - Pages: Inspect page components and layouts");
|
|
588
|
+
logger$5.log(" - APIs: Browse API routes with OpenAPI integration");
|
|
589
|
+
logger$5.log(" - Middleware: View registered middleware");
|
|
590
|
+
logger$5.log(" - Crons: See scheduled tasks and their status");
|
|
591
|
+
logger$5.log(" - Config: View resolved configuration");
|
|
592
|
+
logger$5.log(" - Plugins: Browse installed plugins and their tabs");
|
|
593
|
+
logger$5.log(" - CRUD: Create/delete pages, APIs, layouts, middleware, crons");
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
path: {
|
|
597
|
+
meta: {
|
|
598
|
+
name: "path",
|
|
599
|
+
description: "Print the DevTools URL path"
|
|
600
|
+
},
|
|
601
|
+
args: { port: {
|
|
602
|
+
type: "string",
|
|
603
|
+
description: "Dev server port",
|
|
604
|
+
default: "9527"
|
|
605
|
+
} },
|
|
606
|
+
async run({ args }) {
|
|
607
|
+
const port = args.port;
|
|
608
|
+
logger$5.log(`http://localhost:${port}/__ubean/devtools/`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/env.ts
|
|
615
|
+
const logger$4 = consola.withTag("ubean-cli");
|
|
616
|
+
function parseEnvContent(content) {
|
|
617
|
+
const vars = [];
|
|
618
|
+
const lines = content.split("\n");
|
|
619
|
+
for (const line of lines) {
|
|
620
|
+
const trimmed = line.trim();
|
|
621
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
622
|
+
const eqIndex = trimmed.indexOf("=");
|
|
623
|
+
if (eqIndex === -1) continue;
|
|
624
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
625
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
626
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
627
|
+
const isPublic = key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_") || key.startsWith("PUBLIC_");
|
|
628
|
+
vars.push({
|
|
629
|
+
key,
|
|
630
|
+
value,
|
|
631
|
+
public: isPublic
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
return vars;
|
|
635
|
+
}
|
|
636
|
+
function serializeEnvVars(vars) {
|
|
637
|
+
return `${vars.map((v) => `${v.key}=${v.value}`).join("\n")}\n`;
|
|
638
|
+
}
|
|
639
|
+
async function readEnvFile(fs, file) {
|
|
640
|
+
try {
|
|
641
|
+
return parseEnvContent(await fs.readFile(file));
|
|
642
|
+
} catch {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function writeEnvFile(fs, file, vars, dry = false) {
|
|
647
|
+
if (dry) return true;
|
|
648
|
+
const content = serializeEnvVars(vars);
|
|
649
|
+
await fs.writeFile(file, content);
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
const envCommand = {
|
|
653
|
+
meta: {
|
|
654
|
+
name: "env",
|
|
655
|
+
description: "Manage environment variables"
|
|
656
|
+
},
|
|
657
|
+
subCommands: {
|
|
658
|
+
add: {
|
|
659
|
+
meta: {
|
|
660
|
+
name: "add",
|
|
661
|
+
description: "Add or update an environment variable"
|
|
662
|
+
},
|
|
663
|
+
args: {
|
|
664
|
+
key: {
|
|
665
|
+
type: "positional",
|
|
666
|
+
description: "Variable name (e.g., DATABASE_URL, UBEAN_PUBLIC_API_URL)"
|
|
667
|
+
},
|
|
668
|
+
value: {
|
|
669
|
+
type: "positional",
|
|
670
|
+
description: "Variable value"
|
|
671
|
+
},
|
|
672
|
+
file: {
|
|
673
|
+
type: "string",
|
|
674
|
+
description: "Environment file",
|
|
675
|
+
default: ".env"
|
|
676
|
+
},
|
|
677
|
+
public: {
|
|
678
|
+
type: "boolean",
|
|
679
|
+
description: "Mark as public (prefix with UBEAN_PUBLIC_)",
|
|
680
|
+
default: false
|
|
681
|
+
},
|
|
682
|
+
force: {
|
|
683
|
+
type: "boolean",
|
|
684
|
+
description: "Overwrite existing variable without confirmation",
|
|
685
|
+
default: false,
|
|
686
|
+
alias: "f"
|
|
687
|
+
},
|
|
688
|
+
dry: {
|
|
689
|
+
type: "boolean",
|
|
690
|
+
description: "Show what would be changed",
|
|
691
|
+
default: false
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
async run({ args }) {
|
|
695
|
+
const fs = createFsOps(process.cwd());
|
|
696
|
+
const file = args.file;
|
|
697
|
+
let key = args.key;
|
|
698
|
+
const value = args.value;
|
|
699
|
+
if (args.public && !key.startsWith("UBEAN_PUBLIC_") && !key.startsWith("VITE_") && !key.startsWith("PUBLIC_")) key = `UBEAN_PUBLIC_${key}`;
|
|
700
|
+
const vars = await readEnvFile(fs, file);
|
|
701
|
+
const existing = vars.find((v) => v.key === key);
|
|
702
|
+
if (existing && !args.force) {
|
|
703
|
+
logger$4.warn(`Variable ${key} already exists in ${file} (use --force to overwrite)`);
|
|
704
|
+
logger$4.info(` Current value: ${existing.value}`);
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (existing) existing.value = value;
|
|
708
|
+
else vars.push({
|
|
709
|
+
key,
|
|
710
|
+
value,
|
|
711
|
+
public: key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_")
|
|
712
|
+
});
|
|
713
|
+
await writeEnvFile(fs, file, vars, args.dry);
|
|
714
|
+
if (args.dry) logger$4.success(`[dry-run] Would ${existing ? "update" : "add"} ${key}=${value} in ${file}`);
|
|
715
|
+
else logger$4.success(`${existing ? "Updated" : "Added"} ${key} in ${file}`);
|
|
716
|
+
if (key.startsWith("UBEAN_PUBLIC_") || key.startsWith("VITE_") || key.startsWith("PUBLIC_")) logger$4.info(` Note: ${key} is exposed to client-side code`);
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
remove: {
|
|
720
|
+
meta: {
|
|
721
|
+
name: "remove",
|
|
722
|
+
description: "Remove an environment variable"
|
|
723
|
+
},
|
|
724
|
+
args: {
|
|
725
|
+
key: {
|
|
726
|
+
type: "positional",
|
|
727
|
+
description: "Variable name to remove"
|
|
728
|
+
},
|
|
729
|
+
file: {
|
|
730
|
+
type: "string",
|
|
731
|
+
description: "Environment file",
|
|
732
|
+
default: ".env"
|
|
733
|
+
},
|
|
734
|
+
dry: {
|
|
735
|
+
type: "boolean",
|
|
736
|
+
description: "Show what would be removed",
|
|
737
|
+
default: false
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
async run({ args }) {
|
|
741
|
+
const fs = createFsOps(process.cwd());
|
|
742
|
+
const file = args.file;
|
|
743
|
+
const key = args.key;
|
|
744
|
+
const vars = await readEnvFile(fs, file);
|
|
745
|
+
const index = vars.findIndex((v) => v.key === key);
|
|
746
|
+
if (index === -1) {
|
|
747
|
+
logger$4.warn(`Variable ${key} not found in ${file}`);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
vars.splice(index, 1);
|
|
751
|
+
if (args.dry) logger$4.success(`[dry-run] Would remove ${key} from ${file}`);
|
|
752
|
+
else {
|
|
753
|
+
await writeEnvFile(fs, file, vars);
|
|
754
|
+
logger$4.success(`Removed ${key} from ${file}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
list: {
|
|
759
|
+
meta: {
|
|
760
|
+
name: "list",
|
|
761
|
+
description: "List environment variables"
|
|
762
|
+
},
|
|
763
|
+
args: {
|
|
764
|
+
file: {
|
|
765
|
+
type: "string",
|
|
766
|
+
description: "Environment file",
|
|
767
|
+
default: ".env"
|
|
768
|
+
},
|
|
769
|
+
public: {
|
|
770
|
+
type: "boolean",
|
|
771
|
+
description: "Show only public variables",
|
|
772
|
+
default: false
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
async run({ args }) {
|
|
776
|
+
const fs = createFsOps(process.cwd());
|
|
777
|
+
const file = args.file;
|
|
778
|
+
if (!await fs.exists(file)) {
|
|
779
|
+
logger$4.info(`${file} does not exist`);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const vars = await readEnvFile(fs, file);
|
|
783
|
+
const filtered = args.public ? vars.filter((v) => v.public) : vars;
|
|
784
|
+
if (filtered.length === 0) {
|
|
785
|
+
logger$4.info(`No variables found in ${file}${args.public ? " (public only)" : ""}`);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
logger$4.info(`Variables in ${file} (${filtered.length}):`);
|
|
789
|
+
for (const v of filtered) {
|
|
790
|
+
const suffix = v.public ? " [public]" : "";
|
|
791
|
+
logger$4.log(` ${v.key}=${v.value}${suffix}`);
|
|
792
|
+
}
|
|
793
|
+
const publicCount = vars.filter((v) => v.public).length;
|
|
794
|
+
const privateCount = vars.length - publicCount;
|
|
795
|
+
logger$4.info(`\n ${publicCount} public, ${privateCount} private`);
|
|
796
|
+
}
|
|
797
|
+
},
|
|
798
|
+
init: {
|
|
799
|
+
meta: {
|
|
800
|
+
name: "init",
|
|
801
|
+
description: "Create .env and .env.example files from template"
|
|
802
|
+
},
|
|
803
|
+
args: {
|
|
804
|
+
force: {
|
|
805
|
+
type: "boolean",
|
|
806
|
+
description: "Overwrite existing files",
|
|
807
|
+
default: false,
|
|
808
|
+
alias: "f"
|
|
809
|
+
},
|
|
810
|
+
dry: {
|
|
811
|
+
type: "boolean",
|
|
812
|
+
description: "Show what would be created",
|
|
813
|
+
default: false
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
async run({ args }) {
|
|
817
|
+
const fs = createFsOps(process.cwd());
|
|
818
|
+
for (const [file, content] of [[".env", `# ubean environment variables
|
|
819
|
+
# Server-only variables (never exposed to client)
|
|
820
|
+
DATABASE_URL=
|
|
821
|
+
SESSION_SECRET=
|
|
822
|
+
|
|
823
|
+
# Public variables (exposed to client - prefix with UBEAN_PUBLIC_)
|
|
824
|
+
# UBEAN_PUBLIC_API_URL=/api
|
|
825
|
+
`], [".env.example", `# Example environment variables
|
|
826
|
+
DATABASE_URL=your-database-url
|
|
827
|
+
SESSION_SECRET=your-session-secret
|
|
828
|
+
# UBEAN_PUBLIC_API_URL=/api
|
|
829
|
+
`]]) {
|
|
830
|
+
if (await fs.exists(file) && !args.force) {
|
|
831
|
+
logger$4.warn(`Skipping ${file} (already exists, use --force to overwrite)`);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (args.dry) logger$4.success(`[dry-run] Would create ${file}`);
|
|
835
|
+
else {
|
|
836
|
+
await fs.writeFile(file, content);
|
|
837
|
+
logger$4.success(`Created ${file}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
logger$4.info("\nTip: Add .env to .gitignore (but commit .env.example)");
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
};
|
|
845
|
+
//#endregion
|
|
846
|
+
//#region src/init.ts
|
|
847
|
+
const logger$3 = consola.withTag("ubean-cli");
|
|
848
|
+
const TEMPLATES = [
|
|
849
|
+
{
|
|
850
|
+
value: "minimal",
|
|
851
|
+
label: "Minimal (just a hello world page)"
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
value: "starter",
|
|
855
|
+
label: "Starter (recommended, includes pages/api/layouts)"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
value: "blog",
|
|
859
|
+
label: "Blog (markdown-based blog structure)"
|
|
860
|
+
}
|
|
861
|
+
];
|
|
862
|
+
const PRESETS = [
|
|
863
|
+
{
|
|
864
|
+
value: "standard",
|
|
865
|
+
label: "Standard (Node.js/Bun compatible)"
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
value: "node",
|
|
869
|
+
label: "Node.js"
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
value: "cloudflare",
|
|
873
|
+
label: "Cloudflare Workers"
|
|
874
|
+
}
|
|
875
|
+
];
|
|
876
|
+
const PACKAGE_MANAGERS = [
|
|
877
|
+
"npm",
|
|
878
|
+
"pnpm",
|
|
879
|
+
"yarn",
|
|
880
|
+
"bun"
|
|
881
|
+
];
|
|
882
|
+
async function prompt(question, defaultValue) {
|
|
883
|
+
const rl = (await import("node:readline")).createInterface({
|
|
884
|
+
input: process.stdin,
|
|
885
|
+
output: process.stdout
|
|
886
|
+
});
|
|
887
|
+
return new Promise((resolvePromise) => {
|
|
888
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
889
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
890
|
+
rl.close();
|
|
891
|
+
resolvePromise(answer.trim() || defaultValue || "");
|
|
892
|
+
});
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
async function select(question, options, defaultValue) {
|
|
896
|
+
const rl = (await import("node:readline")).createInterface({
|
|
897
|
+
input: process.stdin,
|
|
898
|
+
output: process.stdout
|
|
899
|
+
});
|
|
900
|
+
console.log(`\n${question}`);
|
|
901
|
+
options.forEach((opt, i) => {
|
|
902
|
+
const marker = opt.value === defaultValue ? " (default)" : "";
|
|
903
|
+
console.log(` ${i + 1}. ${opt.label}${marker} [${opt.value}]`);
|
|
904
|
+
});
|
|
905
|
+
return new Promise((resolvePromise) => {
|
|
906
|
+
rl.question("> ", (answer) => {
|
|
907
|
+
rl.close();
|
|
908
|
+
const idx = parseInt(answer, 10) - 1;
|
|
909
|
+
if (!answer.trim() && defaultValue) resolvePromise(defaultValue);
|
|
910
|
+
else if (idx >= 0 && idx < options.length) resolvePromise(options[idx].value);
|
|
911
|
+
else {
|
|
912
|
+
const found = options.find((o) => o.value === answer.trim());
|
|
913
|
+
resolvePromise(found ? found.value : defaultValue || options[0].value);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
async function confirm(question, defaultValue = true) {
|
|
919
|
+
const rl = (await import("node:readline")).createInterface({
|
|
920
|
+
input: process.stdin,
|
|
921
|
+
output: process.stdout
|
|
922
|
+
});
|
|
923
|
+
const suffix = defaultValue ? " (Y/n)" : " (y/N)";
|
|
924
|
+
return new Promise((resolvePromise) => {
|
|
925
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
926
|
+
rl.close();
|
|
927
|
+
const a = answer.trim().toLowerCase();
|
|
928
|
+
if (!a) resolvePromise(defaultValue);
|
|
929
|
+
else resolvePromise(a === "y" || a === "yes");
|
|
930
|
+
});
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
function isNonInteractive() {
|
|
934
|
+
return !process.stdin.isTTY || process.env.CI !== void 0 || process.env.NONINTERACTIVE !== void 0;
|
|
935
|
+
}
|
|
936
|
+
const PACKAGE_JSON_TEMPLATE = `{
|
|
937
|
+
"name": "{{name}}",
|
|
938
|
+
"private": true,
|
|
939
|
+
"type": "module",
|
|
940
|
+
"scripts": {
|
|
941
|
+
"dev": "ubean dev",
|
|
942
|
+
"build": "ubean build",
|
|
943
|
+
"preview": "ubean preview",
|
|
944
|
+
"prepare": "ubean prepare",
|
|
945
|
+
"typecheck": "vue-tsc --noEmit"
|
|
946
|
+
},
|
|
947
|
+
"dependencies": {
|
|
948
|
+
"ubean": "latest",
|
|
949
|
+
"vue": "^3.5.0"
|
|
950
|
+
},
|
|
951
|
+
"devDependencies": {
|
|
952
|
+
"typescript": "^5.6.0",
|
|
953
|
+
"vue-tsc": "^2.1.0"
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
`;
|
|
957
|
+
const TSCONFIG_TEMPLATE = `{
|
|
958
|
+
"compilerOptions": {
|
|
959
|
+
"target": "ESNext",
|
|
960
|
+
"module": "ESNext",
|
|
961
|
+
"moduleResolution": "Bundler",
|
|
962
|
+
"strict": true,
|
|
963
|
+
"jsx": "preserve",
|
|
964
|
+
"resolveJsonModule": true,
|
|
965
|
+
"esModuleInterop": true,
|
|
966
|
+
"skipLibCheck": true,
|
|
967
|
+
"isolatedModules": true,
|
|
968
|
+
"allowImportingTsExtensions": true,
|
|
969
|
+
"noEmit": true,
|
|
970
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
971
|
+
"types": ["ubean/client"]
|
|
972
|
+
},
|
|
973
|
+
"include": ["src/**/*.ts", "src/**/*.vue", ".ubean/**/*.d.ts"],
|
|
974
|
+
"exclude": ["node_modules", "dist"]
|
|
975
|
+
}
|
|
976
|
+
`;
|
|
977
|
+
const UBEAN_CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
|
|
978
|
+
|
|
979
|
+
export default defineConfig({
|
|
980
|
+
srcDir: 'src',
|
|
981
|
+
preset: '{{preset}}'
|
|
982
|
+
});
|
|
983
|
+
`;
|
|
984
|
+
const APP_VUE_TEMPLATE = `<template>
|
|
985
|
+
<div id="app">
|
|
986
|
+
<RouterView />
|
|
987
|
+
</div>
|
|
988
|
+
</template>
|
|
989
|
+
|
|
990
|
+
<script setup lang="ts">
|
|
991
|
+
import { RouterView } from 'vue-router';
|
|
992
|
+
<\/script>
|
|
993
|
+
|
|
994
|
+
<style>
|
|
995
|
+
* {
|
|
996
|
+
margin: 0;
|
|
997
|
+
padding: 0;
|
|
998
|
+
box-sizing: border-box;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
body {
|
|
1002
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
#app {
|
|
1006
|
+
min-height: 100vh;
|
|
1007
|
+
}
|
|
1008
|
+
</style>
|
|
1009
|
+
`;
|
|
1010
|
+
const INDEX_PAGE_TEMPLATE = `<template>
|
|
1011
|
+
<div class="home-page">
|
|
1012
|
+
<h1>Welcome to ubean</h1>
|
|
1013
|
+
<p>Your Vue meta framework is ready! 🚀</p>
|
|
1014
|
+
<div class="links">
|
|
1015
|
+
<RouterLink to="/about">About</RouterLink>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
</template>
|
|
1019
|
+
|
|
1020
|
+
<script setup lang="ts">
|
|
1021
|
+
import { RouterLink } from 'vue-router';
|
|
1022
|
+
|
|
1023
|
+
definePage({
|
|
1024
|
+
meta: {
|
|
1025
|
+
title: 'Home'
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
1028
|
+
<\/script>
|
|
1029
|
+
|
|
1030
|
+
<style scoped>
|
|
1031
|
+
.home-page {
|
|
1032
|
+
display: flex;
|
|
1033
|
+
flex-direction: column;
|
|
1034
|
+
align-items: center;
|
|
1035
|
+
justify-content: center;
|
|
1036
|
+
min-height: 100vh;
|
|
1037
|
+
padding: 2rem;
|
|
1038
|
+
text-align: center;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
h1 {
|
|
1042
|
+
font-size: 3rem;
|
|
1043
|
+
margin-bottom: 1rem;
|
|
1044
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1045
|
+
-webkit-background-clip: text;
|
|
1046
|
+
-webkit-text-fill-color: transparent;
|
|
1047
|
+
background-clip: text;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
p {
|
|
1051
|
+
color: #666;
|
|
1052
|
+
font-size: 1.2rem;
|
|
1053
|
+
margin-bottom: 2rem;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
.links {
|
|
1057
|
+
display: flex;
|
|
1058
|
+
gap: 1rem;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
a {
|
|
1062
|
+
padding: 0.75rem 1.5rem;
|
|
1063
|
+
border-radius: 8px;
|
|
1064
|
+
background: #667eea;
|
|
1065
|
+
color: white;
|
|
1066
|
+
text-decoration: none;
|
|
1067
|
+
font-weight: 500;
|
|
1068
|
+
transition: all 0.2s;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
a:hover {
|
|
1072
|
+
background: #5a67d8;
|
|
1073
|
+
transform: translateY(-1px);
|
|
1074
|
+
}
|
|
1075
|
+
</style>
|
|
1076
|
+
`;
|
|
1077
|
+
const ABOUT_PAGE_TEMPLATE = `<template>
|
|
1078
|
+
<div class="about-page">
|
|
1079
|
+
<h1>About</h1>
|
|
1080
|
+
<p>This page is built with ubean.</p>
|
|
1081
|
+
<RouterLink to="/">Back to Home</RouterLink>
|
|
1082
|
+
</div>
|
|
1083
|
+
</template>
|
|
1084
|
+
|
|
1085
|
+
<script setup lang="ts">
|
|
1086
|
+
import { RouterLink } from 'vue-router';
|
|
1087
|
+
|
|
1088
|
+
definePage({
|
|
1089
|
+
meta: {
|
|
1090
|
+
title: 'About'
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
<\/script>
|
|
1094
|
+
|
|
1095
|
+
<style scoped>
|
|
1096
|
+
.about-page {
|
|
1097
|
+
max-width: 800px;
|
|
1098
|
+
margin: 0 auto;
|
|
1099
|
+
padding: 3rem 2rem;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
h1 {
|
|
1103
|
+
margin-bottom: 1rem;
|
|
1104
|
+
color: #333;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
p {
|
|
1108
|
+
color: #666;
|
|
1109
|
+
line-height: 1.6;
|
|
1110
|
+
margin-bottom: 2rem;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
a {
|
|
1114
|
+
color: #667eea;
|
|
1115
|
+
text-decoration: none;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
a:hover {
|
|
1119
|
+
text-decoration: underline;
|
|
1120
|
+
}
|
|
1121
|
+
</style>
|
|
1122
|
+
`;
|
|
1123
|
+
const HELLO_API_TEMPLATE = `import { defineHandler } from 'ubean';
|
|
1124
|
+
|
|
1125
|
+
export const GET = defineHandler(c => {
|
|
1126
|
+
return c.json({
|
|
1127
|
+
message: 'Hello from ubean API!',
|
|
1128
|
+
timestamp: Date.now()
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
`;
|
|
1132
|
+
/**
|
|
1133
|
+
* 类型化请求客户端模板 - 浏览器端
|
|
1134
|
+
*
|
|
1135
|
+
* 启动 dev server 后,ubean 会自动生成 .ubean/openapi.d.ts(包含项目所有 API 的 paths 类型)。
|
|
1136
|
+
* 本文件将 paths 类型绑定到 createTypedClient,使浏览器端请求的路径、参数、请求体和返回值都获得类型安全。
|
|
1137
|
+
*
|
|
1138
|
+
* 使用方式:
|
|
1139
|
+
* import { api } from '../request/client';
|
|
1140
|
+
* const user = await api.get('/api/users/{id}', { params: { path: { id: 1 } } });
|
|
1141
|
+
*/
|
|
1142
|
+
const REQUEST_CLIENT_TEMPLATE = `import { createRequest } from '@soybeanjs/fetch';
|
|
1143
|
+
import { createTypedClient, toFlatTypedClient } from '@soybeanjs/fetch/openapi';
|
|
1144
|
+
import type { paths } from '../../.ubean/openapi';
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* 底层 HTTP 请求实例(@soybeanjs/fetch 封装)。
|
|
1148
|
+
*
|
|
1149
|
+
* api 和 flatApi 共用同一份 RequestInstance,共享 baseURL/timeout/headers/state 等。
|
|
1150
|
+
* 调整配置时只需修改此处一处。
|
|
1151
|
+
*/
|
|
1152
|
+
const request = createRequest({
|
|
1153
|
+
// baseURL: '/api', // 按需设置 API 基础路径
|
|
1154
|
+
// timeout: 10000,
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
/**
|
|
1158
|
+
* 浏览器端类型化 HTTP 客户端(抛异常模式)
|
|
1159
|
+
*
|
|
1160
|
+
* 路径、参数、请求体和返回值类型均从 OpenAPI schema 自动推断。
|
|
1161
|
+
* 启动 dev server 后类型会自动更新(.ubean/openapi.d.ts)。
|
|
1162
|
+
*
|
|
1163
|
+
* 支持通过 responseType 配置不同的返回类型:
|
|
1164
|
+
* - 'json' (默认): 返回解析后的 JSON 数据
|
|
1165
|
+
* - 'blob': 返回 { file: Blob; filename: string; contentType: string }
|
|
1166
|
+
* - 'text': 返回 string
|
|
1167
|
+
* - 'arraybuffer': 返回 { file: ArrayBuffer; filename: string; contentType: string }
|
|
1168
|
+
*
|
|
1169
|
+
* @example
|
|
1170
|
+
* import { api } from '../request/client';
|
|
1171
|
+
*
|
|
1172
|
+
* // JSON 请求(默认)
|
|
1173
|
+
* const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
|
|
1174
|
+
*
|
|
1175
|
+
* // 文件下载
|
|
1176
|
+
* const file = await api.get('/api/export', { responseType: 'blob' });
|
|
1177
|
+
* // file: { file: Blob; filename: string; contentType: string }
|
|
1178
|
+
*
|
|
1179
|
+
* // 文本
|
|
1180
|
+
* const text = await api.get('/api/readme', { responseType: 'text' });
|
|
1181
|
+
* // text: string
|
|
1182
|
+
*/
|
|
1183
|
+
export const api = createTypedClient<paths>(request);
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* 扁平模式类型化 HTTP 客户端(不抛异常,通过返回值判断)
|
|
1187
|
+
*
|
|
1188
|
+
* toFlatTypedClient 接收同一个 RequestInstance,内部通过 withResponse + try/catch
|
|
1189
|
+
* 实现扁平化返回,与 api 共享 state/cache/auth。
|
|
1190
|
+
*
|
|
1191
|
+
* 返回 { data, error, response } — 成功时 error 为 null,失败时 data 为 null。
|
|
1192
|
+
* 接口与 api 一致,同样支持 responseType。
|
|
1193
|
+
*
|
|
1194
|
+
* @example
|
|
1195
|
+
* import { flatApi } from '../request/client';
|
|
1196
|
+
*
|
|
1197
|
+
* const { data, error } = await flatApi.get('/api/users/{id}', {
|
|
1198
|
+
* pathParams: { id: 1 }
|
|
1199
|
+
* });
|
|
1200
|
+
* if (error) {
|
|
1201
|
+
* console.error('Failed:', error.message);
|
|
1202
|
+
* } else {
|
|
1203
|
+
* console.log('User:', data);
|
|
1204
|
+
* }
|
|
1205
|
+
*/
|
|
1206
|
+
export const flatApi = toFlatTypedClient<paths>(request);
|
|
1207
|
+
`;
|
|
1208
|
+
/**
|
|
1209
|
+
* 类型化请求客户端模板 - server 端内部调度
|
|
1210
|
+
*
|
|
1211
|
+
* 在 API 路由或 useData 的 fetcher 中使用,自动转发 cookie/authorization 等请求头。
|
|
1212
|
+
* 与 client.ts 接口一致,但内部通过 createInternalAdapter + @soybeanjs/fetch 发起请求(进程内调度)。
|
|
1213
|
+
*
|
|
1214
|
+
* 使用方式:
|
|
1215
|
+
* import { createServerApi } from '../request/internal';
|
|
1216
|
+
* export const GET = defineHandler(async (c) => {
|
|
1217
|
+
* const api = createServerApi(c);
|
|
1218
|
+
* const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
|
|
1219
|
+
* return c.json(user);
|
|
1220
|
+
* });
|
|
1221
|
+
*/
|
|
1222
|
+
const REQUEST_INTERNAL_TEMPLATE = `import { createInternalAdapter } from 'ubean';
|
|
1223
|
+
import { createRequest } from '@soybeanjs/fetch';
|
|
1224
|
+
import { createTypedClient } from '@soybeanjs/fetch/openapi';
|
|
1225
|
+
import type { paths } from '../../.ubean/openapi';
|
|
1226
|
+
|
|
1227
|
+
/**
|
|
1228
|
+
* server 端类型化内部 fetch
|
|
1229
|
+
*
|
|
1230
|
+
* 在 API 路由或 useData 的 fetcher 中使用,自动转发 cookie/authorization 等请求头。
|
|
1231
|
+
* 与 client.ts 的 api 接口一致,但通过进程内调度发起请求(不发起新的网络请求)。
|
|
1232
|
+
*
|
|
1233
|
+
* @example
|
|
1234
|
+
* // src/routes/api/posts.ts
|
|
1235
|
+
* import { defineHandler } from 'ubean';
|
|
1236
|
+
* import { createServerApi } from '../request/internal';
|
|
1237
|
+
*
|
|
1238
|
+
* export const GET = defineHandler(async (c) => {
|
|
1239
|
+
* const api = createServerApi(c);
|
|
1240
|
+
* const user = await api.get('/api/users/{id}', { pathParams: { id: 1 } });
|
|
1241
|
+
* return c.json(user);
|
|
1242
|
+
* });
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* // 在 useData 中使用
|
|
1246
|
+
* import { useData, defineHandler } from 'ubean';
|
|
1247
|
+
* import { createServerApi } from '../request/internal';
|
|
1248
|
+
*
|
|
1249
|
+
* export const GET = defineHandler(async (c) => {
|
|
1250
|
+
* const api = createServerApi(c);
|
|
1251
|
+
* const result = await useData({
|
|
1252
|
+
* fetcher: async () => api.get('/api/users/{id}', { pathParams: { id: 1 } })
|
|
1253
|
+
* });
|
|
1254
|
+
* return c.json(result.data);
|
|
1255
|
+
* });
|
|
1256
|
+
*/
|
|
1257
|
+
export function createServerApi(context: Parameters<typeof createInternalAdapter>[0]) {
|
|
1258
|
+
const adapter = createInternalAdapter(context);
|
|
1259
|
+
const request = createRequest(
|
|
1260
|
+
{ retry: { retries: 0 }, adapter },
|
|
1261
|
+
{ isBackendSuccess: () => true, transform: response => response.data }
|
|
1262
|
+
);
|
|
1263
|
+
return createTypedClient<paths>(request);
|
|
1264
|
+
}
|
|
1265
|
+
`;
|
|
1266
|
+
const DEFAULT_LAYOUT_TEMPLATE = `<template>
|
|
1267
|
+
<slot />
|
|
1268
|
+
</template>
|
|
1269
|
+
|
|
1270
|
+
<script setup lang="ts">
|
|
1271
|
+
<\/script>
|
|
1272
|
+
|
|
1273
|
+
<style>
|
|
1274
|
+
body {
|
|
1275
|
+
line-height: 1.5;
|
|
1276
|
+
}
|
|
1277
|
+
</style>
|
|
1278
|
+
`;
|
|
1279
|
+
const GITIGNORE_TEMPLATE = `node_modules
|
|
1280
|
+
dist
|
|
1281
|
+
.ubean
|
|
1282
|
+
.DS_Store
|
|
1283
|
+
*.log
|
|
1284
|
+
.env
|
|
1285
|
+
.env.local
|
|
1286
|
+
.env.*.local
|
|
1287
|
+
`;
|
|
1288
|
+
const FAVICON_SVG_TEMPLATE = `<svg width="100%" height="100%" version="1.1" viewBox="0 0 1000 1000" xmlns="http://www.w3.org/2000/svg"
|
|
1289
|
+
xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
1290
|
+
<g>
|
|
1291
|
+
<path
|
|
1292
|
+
d="M 200,866 C 100,866 50,779.4 100,692.8 L 200,519.6 C 220,485 240,490 265,499.6 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
|
1293
|
+
fill="url(#LinearGradient)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1294
|
+
<path
|
|
1295
|
+
d="M 420,866 C 455,861 478,846 500,827 C 614,696 615,597 500,517 C 394,444 333,374 380,207.82 L 260,415.67 C 240.22,450 254.37,465.1 275.28,481.79 S 360,542.68 360,542.68 C 480.5,601 498,642.5 500,720 C 498,811 462,856 420,866"
|
|
1296
|
+
fill="url(#LinearGradient_2)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1297
|
+
<path d="M 500,517 C 394,444 333,374 380,207.82 L 400,173.2 C 367,295 421,350 603,428 C 572,440 524,474 500,517"
|
|
1298
|
+
fill="url(#LinearGradient_3)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1299
|
+
<path d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 572,440 524,474 500,517 C 615,597 614,696 500,827"
|
|
1300
|
+
fill="url(#LinearGradient_4)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1301
|
+
<path d="M 400,173.2 C 367,295 421,350 603,428 C 690,389, 750,445 788,500 L 600,173.2 C 550,86.6 450,86.6 400,173.2"
|
|
1302
|
+
fill="url(#LinearGradient_5)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1303
|
+
<path
|
|
1304
|
+
d="M 500,827 L 660,660 C 738,589 710,482 603,428 C 690,389, 750,445 788,500 C 816,554 797,606 750,640 L 500,827"
|
|
1305
|
+
fill="url(#LinearGradient_6)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1306
|
+
<path
|
|
1307
|
+
d="M 788,500 C 816,554 797,606 750,640 L 500,827 C 497,851 513,862 540,866 L 800,866 C 900,866 950,779.4 900,692.8 L 788,500"
|
|
1308
|
+
fill="url(#LinearGradient_7)" fill-rule="nonzero" opacity="1" stroke="none" />
|
|
1309
|
+
<g transform="translate(140, 650) scale(0.18)">
|
|
1310
|
+
<path d="M539.04,146.5c-77.37,32.74-126.47,114.53-123.22,198,.61,150.92,169.47,210.6,214.99,341.13,39.71,96.62-16.62,230.68-122.31,253.16-374.89-83.15-390.44-785.49,30.54-792.29ZM564.61,948.5c68.1-32.18,118.51-95.45,132.49-169.97,41.81-214.6-158.22-254.7-215.15-413.02-31.14-86.21,21.33-198.66,114.56-214.57,129.21,21.58,214.93,148.7,230.96,272.19,7.94,38.53,21.57,75.37,34.41,112.43,75.75,198.26-76.06,438.99-297.26,412.94Z" fill="#ffffff" fill-rule="evenodd"/>
|
|
1311
|
+
</g>
|
|
1312
|
+
</g>
|
|
1313
|
+
<defs>
|
|
1314
|
+
<linearGradient gradientTransform="matrix(104.391 -73.3432 73.3432 104.391 277.441 710.122)"
|
|
1315
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient" x1="0" x2="1" y1="0" y2="0">
|
|
1316
|
+
<stop offset="0" stop-color="#373ebf" />
|
|
1317
|
+
<stop offset="1" stop-color="#5058e6" />
|
|
1318
|
+
</linearGradient>
|
|
1319
|
+
<linearGradient gradientTransform="matrix(-173.747 557.324 -557.324 -173.747 508.829 258.172)"
|
|
1320
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_2" x1="0" x2="1" y1="0" y2="0">
|
|
1321
|
+
<stop offset="0" stop-color="#c2d6ff" />
|
|
1322
|
+
<stop offset="1" stop-color="#646cff" />
|
|
1323
|
+
</linearGradient>
|
|
1324
|
+
<linearGradient gradientTransform="matrix(157.951 295.666 -295.666 157.951 382.944 193.642)"
|
|
1325
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_3" x1="0" x2="1" y1="0" y2="0">
|
|
1326
|
+
<stop offset="0" stop-color="#5058e6" />
|
|
1327
|
+
<stop offset="1" stop-color="#373ebf" />
|
|
1328
|
+
</linearGradient>
|
|
1329
|
+
<linearGradient gradientTransform="matrix(-44.3023 219.578 -219.578 -44.3023 619.69 469.652)"
|
|
1330
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_4" x1="0" x2="1" y1="0" y2="0">
|
|
1331
|
+
<stop offset="0" stop-color="#91a7ff" />
|
|
1332
|
+
<stop offset="1" stop-color="#5058e6" />
|
|
1333
|
+
</linearGradient>
|
|
1334
|
+
<linearGradient gradientTransform="matrix(125.52 334.256 -334.256 125.52 539.723 235.139)"
|
|
1335
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_5" x1="0" x2="1" y1="0" y2="0">
|
|
1336
|
+
<stop offset="0" stop-color="#646cff" />
|
|
1337
|
+
<stop offset="1" stop-color="#c2d6ff" />
|
|
1338
|
+
</linearGradient>
|
|
1339
|
+
<linearGradient gradientTransform="matrix(-241.23 357.206 -357.206 -241.23 754.054 449.312)"
|
|
1340
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_6" x1="0" x2="1" y1="0" y2="0">
|
|
1341
|
+
<stop offset="0" stop-color="#c2d6ff" />
|
|
1342
|
+
<stop offset="1" stop-color="#646cff" />
|
|
1343
|
+
</linearGradient>
|
|
1344
|
+
<linearGradient gradientTransform="matrix(125.978 210.065 -210.065 125.978 596.433 613.665)"
|
|
1345
|
+
gradientUnits="userSpaceOnUse" id="LinearGradient_7" x1="0" x2="1" y1="0" y2="0">
|
|
1346
|
+
<stop offset="0" stop-color="#373ebf" />
|
|
1347
|
+
<stop offset="1" stop-color="#5058e6" />
|
|
1348
|
+
</linearGradient>
|
|
1349
|
+
</defs>
|
|
1350
|
+
</svg>
|
|
1351
|
+
`;
|
|
1352
|
+
const README_TEMPLATE = `# {{name}}
|
|
1353
|
+
|
|
1354
|
+
A Vue meta-framework project powered by ubean.
|
|
1355
|
+
|
|
1356
|
+
## Getting Started
|
|
1357
|
+
|
|
1358
|
+
\`\`\`bash
|
|
1359
|
+
# Install dependencies
|
|
1360
|
+
{{pm}} install
|
|
1361
|
+
|
|
1362
|
+
# Start dev server
|
|
1363
|
+
{{pm}} dev
|
|
1364
|
+
|
|
1365
|
+
# Build for production
|
|
1366
|
+
{{pm}} build
|
|
1367
|
+
|
|
1368
|
+
# Preview production build
|
|
1369
|
+
{{pm}} preview
|
|
1370
|
+
\`\`\`
|
|
1371
|
+
|
|
1372
|
+
## Project Structure
|
|
1373
|
+
|
|
1374
|
+
\`\`\`
|
|
1375
|
+
├── src/
|
|
1376
|
+
│ ├── pages/ # File-based routing
|
|
1377
|
+
│ ├── layouts/ # Layout components
|
|
1378
|
+
│ ├── routes/ # API routes
|
|
1379
|
+
│ ├── components/ # Vue components
|
|
1380
|
+
│ ├── composables/ # Auto-imported composables
|
|
1381
|
+
│ ├── plugins/ # App plugins
|
|
1382
|
+
│ ├── middleware/ # Request middleware
|
|
1383
|
+
│ ├── crons/ # Scheduled tasks
|
|
1384
|
+
│ ├── queues/ # Background jobs
|
|
1385
|
+
│ ├── request/ # Typed HTTP client & internal fetch
|
|
1386
|
+
│ └── app.vue # Root app component
|
|
1387
|
+
├── public/ # Static assets
|
|
1388
|
+
└── ubean.config.ts # ubean configuration
|
|
1389
|
+
\`\`\`
|
|
1390
|
+
`;
|
|
1391
|
+
const BLOG_INDEX_TEMPLATE = `<template>
|
|
1392
|
+
<div class="blog-page">
|
|
1393
|
+
<h1>Blog</h1>
|
|
1394
|
+
<div class="posts">
|
|
1395
|
+
<article v-for="post in posts" :key="post.slug" class="post-card">
|
|
1396
|
+
<h2><RouterLink :to="'/blog/' + post.slug">{{ post.title }}</RouterLink></h2>
|
|
1397
|
+
<p class="date">{{ post.date }}</p>
|
|
1398
|
+
<p class="excerpt">{{ post.excerpt }}</p>
|
|
1399
|
+
</article>
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
</template>
|
|
1403
|
+
|
|
1404
|
+
<script setup lang="ts">
|
|
1405
|
+
import { RouterLink } from 'vue-router';
|
|
1406
|
+
import { useData } from 'ubean';
|
|
1407
|
+
|
|
1408
|
+
definePage({
|
|
1409
|
+
meta: {
|
|
1410
|
+
title: 'Blog'
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1414
|
+
const { data: posts } = await useData('posts', () => {
|
|
1415
|
+
return [
|
|
1416
|
+
{ slug: 'hello-world', title: 'Hello World', date: '2024-01-15', excerpt: 'Welcome to my blog!' }
|
|
1417
|
+
];
|
|
1418
|
+
});
|
|
1419
|
+
<\/script>
|
|
1420
|
+
|
|
1421
|
+
<style scoped>
|
|
1422
|
+
.blog-page {
|
|
1423
|
+
max-width: 800px;
|
|
1424
|
+
margin: 0 auto;
|
|
1425
|
+
padding: 3rem 2rem;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
h1 {
|
|
1429
|
+
margin-bottom: 2rem;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
.posts {
|
|
1433
|
+
display: flex;
|
|
1434
|
+
flex-direction: column;
|
|
1435
|
+
gap: 2rem;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
.post-card {
|
|
1439
|
+
padding: 1.5rem;
|
|
1440
|
+
border-radius: 8px;
|
|
1441
|
+
border: 1px solid #eee;
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
h2 {
|
|
1445
|
+
font-size: 1.5rem;
|
|
1446
|
+
margin-bottom: 0.5rem;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
h2 a {
|
|
1450
|
+
color: #333;
|
|
1451
|
+
text-decoration: none;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
h2 a:hover {
|
|
1455
|
+
color: #667eea;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
.date {
|
|
1459
|
+
color: #999;
|
|
1460
|
+
font-size: 0.9rem;
|
|
1461
|
+
margin-bottom: 0.5rem;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.excerpt {
|
|
1465
|
+
color: #666;
|
|
1466
|
+
}
|
|
1467
|
+
</style>
|
|
1468
|
+
`;
|
|
1469
|
+
const BLOG_POST_MD_TEMPLATE = `---
|
|
1470
|
+
title: Hello World
|
|
1471
|
+
date: 2024-01-15
|
|
1472
|
+
description: Welcome to my ubean blog!
|
|
1473
|
+
---
|
|
1474
|
+
|
|
1475
|
+
# Hello World
|
|
1476
|
+
|
|
1477
|
+
Welcome to my new blog built with **ubean**!
|
|
1478
|
+
|
|
1479
|
+
This is a markdown blog post.
|
|
1480
|
+
|
|
1481
|
+
## Features
|
|
1482
|
+
|
|
1483
|
+
- File-based routing
|
|
1484
|
+
- Markdown support
|
|
1485
|
+
- Auto-imports
|
|
1486
|
+
- API routes
|
|
1487
|
+
- And much more!
|
|
1488
|
+
`;
|
|
1489
|
+
async function scaffoldProject(options) {
|
|
1490
|
+
const targetDir = resolve$1(options.cwd, options.dir);
|
|
1491
|
+
const fs = createFsOps(targetDir);
|
|
1492
|
+
const projectName = options.name || (options.dir === "." ? basename(options.cwd) : basename(options.dir));
|
|
1493
|
+
const pm = options.packageManager || "npm";
|
|
1494
|
+
const nonInteractive = options.nonInteractive || isNonInteractive();
|
|
1495
|
+
if (await fs.exists(".") && !options.force) {
|
|
1496
|
+
if ((await fs.readDir(".").catch(() => [])).filter((f) => !f.startsWith(".") && f !== "node_modules").length > 0) {
|
|
1497
|
+
if (nonInteractive) throw new Error(`Directory ${targetDir} is not empty. Use --force to overwrite.`);
|
|
1498
|
+
if (!await confirm(`Directory ${targetDir} is not empty. Continue anyway?`, false)) {
|
|
1499
|
+
console.log("Aborted.");
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
await fs.ensureDir("src");
|
|
1505
|
+
await fs.ensureDir("src/pages");
|
|
1506
|
+
await fs.ensureDir("src/layouts");
|
|
1507
|
+
await fs.ensureDir("src/routes");
|
|
1508
|
+
await fs.ensureDir("src/components");
|
|
1509
|
+
await fs.ensureDir("src/composables");
|
|
1510
|
+
await fs.ensureDir("src/plugins");
|
|
1511
|
+
await fs.ensureDir("src/request");
|
|
1512
|
+
await fs.ensureDir("public");
|
|
1513
|
+
await fs.writeFile("package.json", renderTemplate(PACKAGE_JSON_TEMPLATE, { variables: { name: projectName } }));
|
|
1514
|
+
await fs.writeFile("public/favicon.svg", FAVICON_SVG_TEMPLATE);
|
|
1515
|
+
await fs.writeFile("tsconfig.json", TSCONFIG_TEMPLATE);
|
|
1516
|
+
await fs.writeFile("ubean.config.ts", renderTemplate(UBEAN_CONFIG_TEMPLATE, { variables: { preset: options.preset || "standard" } }));
|
|
1517
|
+
await fs.writeFile(".gitignore", GITIGNORE_TEMPLATE);
|
|
1518
|
+
await fs.writeFile("README.md", renderTemplate(README_TEMPLATE, { variables: {
|
|
1519
|
+
name: projectName,
|
|
1520
|
+
pm
|
|
1521
|
+
} }));
|
|
1522
|
+
await fs.writeFile("src/app.vue", APP_VUE_TEMPLATE);
|
|
1523
|
+
await fs.writeFile("src/layouts/default.vue", DEFAULT_LAYOUT_TEMPLATE);
|
|
1524
|
+
await fs.writeFile("src/request/client.ts", REQUEST_CLIENT_TEMPLATE);
|
|
1525
|
+
await fs.writeFile("src/request/internal.ts", REQUEST_INTERNAL_TEMPLATE);
|
|
1526
|
+
if (options.template === "minimal") await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
|
|
1527
|
+
else if (options.template === "blog") {
|
|
1528
|
+
await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
|
|
1529
|
+
await fs.writeFile("src/pages/about.vue", ABOUT_PAGE_TEMPLATE);
|
|
1530
|
+
await fs.ensureDir("src/pages/blog");
|
|
1531
|
+
await fs.writeFile("src/pages/blog/index.vue", BLOG_INDEX_TEMPLATE);
|
|
1532
|
+
await fs.writeFile("src/pages/blog/hello-world.md", BLOG_POST_MD_TEMPLATE);
|
|
1533
|
+
await fs.writeFile("src/routes/hello.get.ts", HELLO_API_TEMPLATE);
|
|
1534
|
+
} else {
|
|
1535
|
+
await fs.writeFile("src/pages/index.vue", INDEX_PAGE_TEMPLATE);
|
|
1536
|
+
await fs.writeFile("src/pages/about.vue", ABOUT_PAGE_TEMPLATE);
|
|
1537
|
+
await fs.writeFile("src/routes/hello.get.ts", HELLO_API_TEMPLATE);
|
|
1538
|
+
}
|
|
1539
|
+
if (options.git) try {
|
|
1540
|
+
const { execSync } = await import("node:child_process");
|
|
1541
|
+
execSync("git init", {
|
|
1542
|
+
cwd: targetDir,
|
|
1543
|
+
stdio: "ignore"
|
|
1544
|
+
});
|
|
1545
|
+
} catch {}
|
|
1546
|
+
console.log("\n✨ ubean project created successfully!\n");
|
|
1547
|
+
console.log(`Project: ${projectName}`);
|
|
1548
|
+
console.log(`Location: ${targetDir}`);
|
|
1549
|
+
console.log(`Preset: ${options.preset || "standard"}`);
|
|
1550
|
+
console.log(`Template: ${options.template || "starter"}`);
|
|
1551
|
+
console.log("\nNext steps:");
|
|
1552
|
+
if (options.dir !== ".") console.log(` cd ${options.dir}`);
|
|
1553
|
+
console.log(` ${pm} install`);
|
|
1554
|
+
console.log(` ${pm} dev`);
|
|
1555
|
+
console.log("");
|
|
1556
|
+
}
|
|
1557
|
+
const initCommand = {
|
|
1558
|
+
meta: {
|
|
1559
|
+
name: "init",
|
|
1560
|
+
description: "Initialize a new ubean project"
|
|
1561
|
+
},
|
|
1562
|
+
args: {
|
|
1563
|
+
dir: {
|
|
1564
|
+
type: "positional",
|
|
1565
|
+
description: "Directory to initialize",
|
|
1566
|
+
default: "."
|
|
1567
|
+
},
|
|
1568
|
+
force: {
|
|
1569
|
+
type: "boolean",
|
|
1570
|
+
description: "Overwrite existing files",
|
|
1571
|
+
default: false,
|
|
1572
|
+
alias: "f"
|
|
1573
|
+
},
|
|
1574
|
+
name: {
|
|
1575
|
+
type: "string",
|
|
1576
|
+
description: "Project name"
|
|
1577
|
+
},
|
|
1578
|
+
template: {
|
|
1579
|
+
type: "string",
|
|
1580
|
+
description: "Project template (minimal/starter/blog)"
|
|
1581
|
+
},
|
|
1582
|
+
preset: {
|
|
1583
|
+
type: "string",
|
|
1584
|
+
description: "Target preset (standard/node/cloudflare)"
|
|
1585
|
+
},
|
|
1586
|
+
packageManager: {
|
|
1587
|
+
type: "string",
|
|
1588
|
+
description: "Package manager (npm/pnpm/yarn/bun)",
|
|
1589
|
+
alias: "pm"
|
|
1590
|
+
},
|
|
1591
|
+
git: {
|
|
1592
|
+
type: "boolean",
|
|
1593
|
+
description: "Initialize git repository"
|
|
1594
|
+
},
|
|
1595
|
+
yes: {
|
|
1596
|
+
type: "boolean",
|
|
1597
|
+
description: "Skip prompts and use defaults",
|
|
1598
|
+
default: false,
|
|
1599
|
+
alias: "y"
|
|
1600
|
+
}
|
|
1601
|
+
},
|
|
1602
|
+
async run({ args, data }) {
|
|
1603
|
+
const cwd = data?.cwd || process.cwd();
|
|
1604
|
+
const nonInteractive = isNonInteractive() || args.yes;
|
|
1605
|
+
let template = args.template;
|
|
1606
|
+
let preset = args.preset;
|
|
1607
|
+
let packageManager = args.packageManager;
|
|
1608
|
+
let git = args.git;
|
|
1609
|
+
let name = args.name;
|
|
1610
|
+
logger$3.info("🚀 ubean project initializer");
|
|
1611
|
+
if (!nonInteractive) {
|
|
1612
|
+
name = name || await prompt("Project name", args.dir === "." ? basename(cwd) : args.dir);
|
|
1613
|
+
template = template || await select("Select a template:", TEMPLATES, "starter");
|
|
1614
|
+
preset = preset || await select("Select a preset:", PRESETS, "standard");
|
|
1615
|
+
packageManager = packageManager || await prompt("Package manager (npm/pnpm/yarn/bun)", "npm");
|
|
1616
|
+
if (git === void 0) git = await confirm("Initialize git repository?", true);
|
|
1617
|
+
}
|
|
1618
|
+
const options = {
|
|
1619
|
+
cwd,
|
|
1620
|
+
dir: args.dir,
|
|
1621
|
+
force: args.force,
|
|
1622
|
+
template: template || "starter",
|
|
1623
|
+
preset: preset || "standard",
|
|
1624
|
+
packageManager: packageManager || "npm",
|
|
1625
|
+
git: git !== false,
|
|
1626
|
+
name,
|
|
1627
|
+
nonInteractive
|
|
1628
|
+
};
|
|
1629
|
+
if (!PACKAGE_MANAGERS.includes(options.packageManager)) options.packageManager = "npm";
|
|
1630
|
+
await scaffoldProject(options);
|
|
1631
|
+
}
|
|
1632
|
+
};
|
|
1633
|
+
//#endregion
|
|
1634
|
+
//#region src/prepare.ts
|
|
1635
|
+
const logger$2 = consola.withTag("ubean-cli");
|
|
1636
|
+
async function ensureBuildDir(cwd, buildDir) {
|
|
1637
|
+
const fullPath = join$1(cwd, buildDir);
|
|
1638
|
+
if (!existsSync(fullPath)) mkdirSync(fullPath, { recursive: true });
|
|
1639
|
+
}
|
|
1640
|
+
const prepareCommand = {
|
|
1641
|
+
meta: {
|
|
1642
|
+
name: "prepare",
|
|
1643
|
+
description: "Generate type definitions and prepare the project (runs automatically before dev/build)"
|
|
1644
|
+
},
|
|
1645
|
+
args: { cwd: {
|
|
1646
|
+
type: "string",
|
|
1647
|
+
description: "Project root directory",
|
|
1648
|
+
default: "."
|
|
1649
|
+
} },
|
|
1650
|
+
async run({ args }) {
|
|
1651
|
+
const cwd = resolve$1(args.cwd || process.cwd());
|
|
1652
|
+
logger$2.start(`Preparing ubean project at ${cwd}...`);
|
|
1653
|
+
const config = await loadUbeanConfig(cwd);
|
|
1654
|
+
const typesDir = ".ubean";
|
|
1655
|
+
await ensureBuildDir(cwd, typesDir);
|
|
1656
|
+
logger$2.info("Scanning project files...");
|
|
1657
|
+
const result = await scanProject({
|
|
1658
|
+
cwd,
|
|
1659
|
+
srcDir: config.srcDir,
|
|
1660
|
+
dirs: config.dir,
|
|
1661
|
+
ignore: config.scanOptions?.ignore
|
|
1662
|
+
});
|
|
1663
|
+
logger$2.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts, ${result.middlewares.length} middlewares`);
|
|
1664
|
+
logger$2.info("Generating type definitions...");
|
|
1665
|
+
const codegenResult = await generateTypes(result, {
|
|
1666
|
+
cwd,
|
|
1667
|
+
srcDir: config.srcDir,
|
|
1668
|
+
buildDir: typesDir,
|
|
1669
|
+
dirs: config.dir,
|
|
1670
|
+
imports: config.imports,
|
|
1671
|
+
components: config.components,
|
|
1672
|
+
composablesDirs: config.imports.dirs,
|
|
1673
|
+
componentsDirs: config.components.dirs
|
|
1674
|
+
});
|
|
1675
|
+
for (const file of codegenResult.generated) logger$2.success(`Generated ${file.replace(`${cwd}/`, "")}`);
|
|
1676
|
+
logger$2.success("Prepare complete!");
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/preview.ts
|
|
1681
|
+
const MIME_TYPES = {
|
|
1682
|
+
".html": "text/html; charset=utf-8",
|
|
1683
|
+
".js": "text/javascript; charset=utf-8",
|
|
1684
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
1685
|
+
".css": "text/css; charset=utf-8",
|
|
1686
|
+
".json": "application/json; charset=utf-8",
|
|
1687
|
+
".svg": "image/svg+xml",
|
|
1688
|
+
".png": "image/png",
|
|
1689
|
+
".jpg": "image/jpeg",
|
|
1690
|
+
".jpeg": "image/jpeg",
|
|
1691
|
+
".gif": "image/gif",
|
|
1692
|
+
".webp": "image/webp",
|
|
1693
|
+
".ico": "image/x-icon",
|
|
1694
|
+
".woff": "font/woff",
|
|
1695
|
+
".woff2": "font/woff2",
|
|
1696
|
+
".ttf": "font/ttf",
|
|
1697
|
+
".otf": "font/otf",
|
|
1698
|
+
".wasm": "application/wasm",
|
|
1699
|
+
".txt": "text/plain; charset=utf-8",
|
|
1700
|
+
".map": "application/json; charset=utf-8"
|
|
1701
|
+
};
|
|
1702
|
+
/**
|
|
1703
|
+
* 启动静态文件服务器,用于 spa / ssg 模式预览。
|
|
1704
|
+
*
|
|
1705
|
+
* - ssg 模式:优先查找 `<path>/index.html` 或 `<path>.html`,找不到时返回 404
|
|
1706
|
+
* - spa 模式:所有非文件路由回退到 `index.html`(客户端路由接管)
|
|
1707
|
+
*
|
|
1708
|
+
* 使用 Node 内置 `http` + `fs`,避免引入 `sirv` 等依赖。
|
|
1709
|
+
*/
|
|
1710
|
+
function startStaticServer(opts) {
|
|
1711
|
+
const { root, port, host, mode } = opts;
|
|
1712
|
+
const spaFallback = mode === "spa";
|
|
1713
|
+
return createServer(async (req, res) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const url = new URL(req.url || "/", `http://${host}`);
|
|
1716
|
+
let pathname = normalize(decodeURIComponent(url.pathname));
|
|
1717
|
+
if (pathname.includes("..")) {
|
|
1718
|
+
res.statusCode = 400;
|
|
1719
|
+
res.end("Bad Request");
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1722
|
+
let filePath = join$1(root, pathname);
|
|
1723
|
+
let fileExists = existsSync(filePath) && (await stat(filePath)).isFile();
|
|
1724
|
+
if (!fileExists && existsSync(filePath) && (await stat(filePath)).isDirectory()) {
|
|
1725
|
+
const indexPath = join$1(filePath, "index.html");
|
|
1726
|
+
if (existsSync(indexPath)) {
|
|
1727
|
+
filePath = indexPath;
|
|
1728
|
+
fileExists = true;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (!fileExists && mode === "ssg") {
|
|
1732
|
+
const indexPath = join$1(filePath, "index.html");
|
|
1733
|
+
if (existsSync(indexPath) && (await stat(indexPath)).isFile()) {
|
|
1734
|
+
filePath = indexPath;
|
|
1735
|
+
fileExists = true;
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
if (!fileExists && mode === "ssg") {
|
|
1739
|
+
const htmlPath = `${filePath}.html`;
|
|
1740
|
+
if (existsSync(htmlPath) && (await stat(htmlPath)).isFile()) {
|
|
1741
|
+
filePath = htmlPath;
|
|
1742
|
+
fileExists = true;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (!fileExists && spaFallback) {
|
|
1746
|
+
const fallbackPath = join$1(root, "index.html");
|
|
1747
|
+
if (existsSync(fallbackPath)) {
|
|
1748
|
+
filePath = fallbackPath;
|
|
1749
|
+
fileExists = true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (!fileExists) {
|
|
1753
|
+
res.statusCode = 404;
|
|
1754
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
1755
|
+
res.end("Not Found");
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
const ext = extname(filePath).toLowerCase();
|
|
1759
|
+
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
1760
|
+
const body = await readFile(filePath);
|
|
1761
|
+
res.statusCode = 200;
|
|
1762
|
+
res.setHeader("Content-Type", contentType);
|
|
1763
|
+
res.setHeader("Content-Length", body.length);
|
|
1764
|
+
res.end(body);
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
res.statusCode = 500;
|
|
1767
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
1768
|
+
res.end(err instanceof Error ? err.message : String(err));
|
|
1769
|
+
}
|
|
1770
|
+
}).listen(port, host);
|
|
1771
|
+
}
|
|
1772
|
+
const logger$1 = consola.withTag("ubean-cli");
|
|
1773
|
+
const previewCommand = {
|
|
1774
|
+
meta: {
|
|
1775
|
+
name: "preview",
|
|
1776
|
+
description: "Preview the production build locally"
|
|
1777
|
+
},
|
|
1778
|
+
args: {
|
|
1779
|
+
port: {
|
|
1780
|
+
type: "string",
|
|
1781
|
+
description: "Port to listen on"
|
|
1782
|
+
},
|
|
1783
|
+
host: {
|
|
1784
|
+
type: "string",
|
|
1785
|
+
description: "Host to listen on"
|
|
1786
|
+
},
|
|
1787
|
+
strictPort: {
|
|
1788
|
+
type: "boolean",
|
|
1789
|
+
description: "Exit if the port is already in use, instead of auto-incrementing",
|
|
1790
|
+
default: false
|
|
1791
|
+
},
|
|
1792
|
+
cwd: {
|
|
1793
|
+
type: "string",
|
|
1794
|
+
description: "Project root directory",
|
|
1795
|
+
default: "."
|
|
1796
|
+
}
|
|
1797
|
+
},
|
|
1798
|
+
async run({ args }) {
|
|
1799
|
+
const cwd = resolve$1(args.cwd || process.cwd());
|
|
1800
|
+
logger$1.start("Starting ubean preview server...");
|
|
1801
|
+
registerBuiltinPresets();
|
|
1802
|
+
const config = await loadUbeanConfig(cwd);
|
|
1803
|
+
const preset = resolvePresetByName(config.build.preset);
|
|
1804
|
+
logger$1.info(`Preset: ${preset.name}`);
|
|
1805
|
+
const outputDir = config.build.outputDir || "dist";
|
|
1806
|
+
const mode = config.mode;
|
|
1807
|
+
const host = args.host || config.preview.host;
|
|
1808
|
+
const strictPort = args.strictPort ?? config.preview.strictPort;
|
|
1809
|
+
const requestedPort = Number(args.port) || config.preview.port;
|
|
1810
|
+
let actualPort;
|
|
1811
|
+
try {
|
|
1812
|
+
actualPort = await findAvailablePort(requestedPort, {
|
|
1813
|
+
host,
|
|
1814
|
+
strictPort
|
|
1815
|
+
});
|
|
1816
|
+
} catch (err) {
|
|
1817
|
+
if (err?.code === "EADDRINUSE") logger$1.error(`Port ${requestedPort} is already in use${host ? ` on ${host}` : ""}. Try a different port or remove the --strictPort flag.`);
|
|
1818
|
+
else logger$1.error(`Failed to resolve a port: ${err?.message || String(err)}`);
|
|
1819
|
+
process.exit(1);
|
|
1820
|
+
}
|
|
1821
|
+
if (actualPort !== requestedPort) logger$1.warn(`Port ${requestedPort} is in use, trying ${actualPort} instead.`);
|
|
1822
|
+
const label = (text) => dim(text);
|
|
1823
|
+
const printBanner = (port, modeLabel) => {
|
|
1824
|
+
const bannerUrl = `http://${host}:${port}`;
|
|
1825
|
+
logger$1.box(`${green(bold("🚀 ubean preview server ready"))}\n\n → ${label("Local:")} ${cyan(bannerUrl)}\n → ${label("Mode:")} ${dim(modeLabel)}\n → ${label("Preset:")} ${cyan(preset.name)}\n → ${dim("Press Ctrl+C to stop")}`);
|
|
1826
|
+
};
|
|
1827
|
+
if (mode === "spa" || mode === "ssg") {
|
|
1828
|
+
const staticRoot = join$1(cwd, outputDir, "public");
|
|
1829
|
+
if (!existsSync(staticRoot)) {
|
|
1830
|
+
logger$1.error(`Build output not found: ${staticRoot}`);
|
|
1831
|
+
logger$1.info("Run `ubean build` first to create a production build.");
|
|
1832
|
+
process.exit(1);
|
|
1833
|
+
}
|
|
1834
|
+
const server = startStaticServer({
|
|
1835
|
+
root: staticRoot,
|
|
1836
|
+
port: actualPort,
|
|
1837
|
+
host,
|
|
1838
|
+
mode
|
|
1839
|
+
});
|
|
1840
|
+
printBanner(actualPort, `static (${mode})`);
|
|
1841
|
+
const cleanup = () => {
|
|
1842
|
+
server.close(() => process.exit(0));
|
|
1843
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
1844
|
+
};
|
|
1845
|
+
process.on("SIGINT", cleanup);
|
|
1846
|
+
process.on("SIGTERM", cleanup);
|
|
1847
|
+
server.on("error", (err) => {
|
|
1848
|
+
logger$1.error(`Preview server error: ${err.message || err}`);
|
|
1849
|
+
process.exit(1);
|
|
1850
|
+
});
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const presetName = preset.name;
|
|
1854
|
+
let serverFile;
|
|
1855
|
+
if (presetName === "node" || presetName === "bun" || presetName === "deno") serverFile = "server.mjs";
|
|
1856
|
+
else if (presetName === "cloudflare") {
|
|
1857
|
+
logger$1.error("Cloudflare preset preview is not supported yet. Use `wrangler dev` instead.");
|
|
1858
|
+
process.exit(1);
|
|
1859
|
+
} else {
|
|
1860
|
+
serverFile = "handler.mjs";
|
|
1861
|
+
logger$1.warn(`Preset "${presetName}" uses a fetch handler entry. Preview may not work as a standalone server.`);
|
|
1862
|
+
}
|
|
1863
|
+
const serverPath = join$1(cwd, outputDir, "server", serverFile);
|
|
1864
|
+
if (!existsSync(serverPath)) {
|
|
1865
|
+
logger$1.error(`Build output not found: ${serverPath}`);
|
|
1866
|
+
logger$1.info("Run `ubean build` first to create a production build.");
|
|
1867
|
+
process.exit(1);
|
|
1868
|
+
}
|
|
1869
|
+
const { child, exited } = spawnPreviewServer({
|
|
1870
|
+
serverPath,
|
|
1871
|
+
cwd,
|
|
1872
|
+
port: actualPort,
|
|
1873
|
+
host,
|
|
1874
|
+
strictPort
|
|
1875
|
+
});
|
|
1876
|
+
try {
|
|
1877
|
+
await waitForPort(actualPort, {
|
|
1878
|
+
host,
|
|
1879
|
+
retries: 40,
|
|
1880
|
+
delay: 250
|
|
1881
|
+
});
|
|
1882
|
+
} catch {
|
|
1883
|
+
if (exited.value) logger$1.error(`Preview server exited before becoming ready on port ${actualPort}.`);
|
|
1884
|
+
else logger$1.warn(`Preview server on port ${actualPort} is not responding yet, the banner may be premature.`);
|
|
1885
|
+
}
|
|
1886
|
+
printBanner(actualPort, "production");
|
|
1887
|
+
const cleanup = () => {
|
|
1888
|
+
child.kill("SIGTERM");
|
|
1889
|
+
process.exit(0);
|
|
1890
|
+
};
|
|
1891
|
+
process.on("SIGINT", cleanup);
|
|
1892
|
+
process.on("SIGTERM", cleanup);
|
|
1893
|
+
child.on("exit", (code) => {
|
|
1894
|
+
if (code !== 0 && code !== null) logger$1.error(`Preview server exited with code ${code}`);
|
|
1895
|
+
process.exit(code ?? 0);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
};
|
|
1899
|
+
function spawnPreviewServer(opts) {
|
|
1900
|
+
const exited = { value: false };
|
|
1901
|
+
const child = spawn("node", [opts.serverPath], {
|
|
1902
|
+
cwd: opts.cwd,
|
|
1903
|
+
env: {
|
|
1904
|
+
...process.env,
|
|
1905
|
+
PORT: String(opts.port),
|
|
1906
|
+
HOST: opts.host
|
|
1907
|
+
},
|
|
1908
|
+
stdio: [
|
|
1909
|
+
"inherit",
|
|
1910
|
+
"inherit",
|
|
1911
|
+
"pipe"
|
|
1912
|
+
]
|
|
1913
|
+
});
|
|
1914
|
+
let addrInUseReported = false;
|
|
1915
|
+
child.stderr?.on("data", (chunk) => {
|
|
1916
|
+
const text = chunk.toString();
|
|
1917
|
+
process.stderr.write(text);
|
|
1918
|
+
if (!addrInUseReported && /EADDRINUSE/.test(text)) {
|
|
1919
|
+
addrInUseReported = true;
|
|
1920
|
+
logger$1.error(`Port ${opts.port} is already in use${opts.host ? ` on ${opts.host}` : ""}. Try a different port${opts.strictPort ? " or remove the --strictPort flag" : ""}.`);
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
child.on("exit", () => {
|
|
1924
|
+
exited.value = true;
|
|
1925
|
+
});
|
|
1926
|
+
return {
|
|
1927
|
+
child,
|
|
1928
|
+
exited
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
//#endregion
|
|
1932
|
+
//#region src/scaffold-commands.ts
|
|
1933
|
+
const logger = consola.withTag("ubean-cli");
|
|
1934
|
+
const COMMAND_META = {
|
|
1935
|
+
api: {
|
|
1936
|
+
name: "api",
|
|
1937
|
+
description: "Scaffold API routes",
|
|
1938
|
+
pathLabel: "API route path",
|
|
1939
|
+
pathExample: "users/[id]"
|
|
1940
|
+
},
|
|
1941
|
+
layout: {
|
|
1942
|
+
name: "layout",
|
|
1943
|
+
description: "Scaffold layout components",
|
|
1944
|
+
pathLabel: "Layout path",
|
|
1945
|
+
pathExample: "admin"
|
|
1946
|
+
},
|
|
1947
|
+
middleware: {
|
|
1948
|
+
name: "middleware",
|
|
1949
|
+
description: "Scaffold middleware",
|
|
1950
|
+
pathLabel: "Middleware path",
|
|
1951
|
+
pathExample: "auth"
|
|
1952
|
+
},
|
|
1953
|
+
cron: {
|
|
1954
|
+
name: "cron",
|
|
1955
|
+
description: "Scaffold scheduled cron tasks",
|
|
1956
|
+
pathLabel: "Cron task path",
|
|
1957
|
+
pathExample: "daily-cleanup"
|
|
1958
|
+
},
|
|
1959
|
+
plugin: {
|
|
1960
|
+
name: "plugin",
|
|
1961
|
+
description: "Scaffold ubean plugins",
|
|
1962
|
+
pathLabel: "Plugin path",
|
|
1963
|
+
pathExample: "my-plugin"
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
function createScaffoldCommand(type) {
|
|
1967
|
+
const meta = COMMAND_META[type];
|
|
1968
|
+
return {
|
|
1969
|
+
meta: {
|
|
1970
|
+
name: meta.name,
|
|
1971
|
+
description: meta.description
|
|
1972
|
+
},
|
|
1973
|
+
subCommands: {
|
|
1974
|
+
add: {
|
|
1975
|
+
meta: {
|
|
1976
|
+
name: "add",
|
|
1977
|
+
description: `Add a new ${type}`
|
|
1978
|
+
},
|
|
1979
|
+
args: {
|
|
1980
|
+
path: {
|
|
1981
|
+
type: "positional",
|
|
1982
|
+
description: `${meta.pathLabel} (e.g., ${meta.pathExample})`
|
|
1983
|
+
},
|
|
1984
|
+
...type === "api" ? { method: {
|
|
1985
|
+
type: "string",
|
|
1986
|
+
description: "HTTP method for API routes (GET, POST, PUT, PATCH, DELETE)",
|
|
1987
|
+
default: "GET"
|
|
1988
|
+
} } : {},
|
|
1989
|
+
...type === "cron" ? { schedule: {
|
|
1990
|
+
type: "string",
|
|
1991
|
+
description: "Cron schedule expression (e.g., \"0 0 * * *\" for daily at midnight)",
|
|
1992
|
+
default: "* * * * *"
|
|
1993
|
+
} } : {},
|
|
1994
|
+
force: {
|
|
1995
|
+
type: "boolean",
|
|
1996
|
+
description: "Overwrite existing files (creates .bak backup)",
|
|
1997
|
+
default: false,
|
|
1998
|
+
alias: "f"
|
|
1999
|
+
},
|
|
2000
|
+
dry: {
|
|
2001
|
+
type: "boolean",
|
|
2002
|
+
description: "Show what would be created without writing files",
|
|
2003
|
+
default: false
|
|
2004
|
+
}
|
|
2005
|
+
},
|
|
2006
|
+
async run({ args }) {
|
|
2007
|
+
const result = await scaffold({
|
|
2008
|
+
cwd: process.cwd(),
|
|
2009
|
+
type,
|
|
2010
|
+
path: args.path,
|
|
2011
|
+
method: args.method,
|
|
2012
|
+
schedule: args.schedule,
|
|
2013
|
+
force: args.force,
|
|
2014
|
+
dry: args.dry
|
|
2015
|
+
});
|
|
2016
|
+
for (const file of result.created) logger.success(`${args.dry ? "[dry-run] Would create" : "Created"} ${file}`);
|
|
2017
|
+
for (const file of result.skipped) logger.warn(`Skipped ${file} (already exists, use --force to overwrite)`);
|
|
2018
|
+
for (const err of result.errors) logger.error(err);
|
|
2019
|
+
}
|
|
2020
|
+
},
|
|
2021
|
+
delete: {
|
|
2022
|
+
meta: {
|
|
2023
|
+
name: "delete",
|
|
2024
|
+
description: `Delete a ${type} (creates backup by default)`
|
|
2025
|
+
},
|
|
2026
|
+
args: {
|
|
2027
|
+
path: {
|
|
2028
|
+
type: "positional",
|
|
2029
|
+
description: `${meta.pathLabel} to delete`
|
|
2030
|
+
},
|
|
2031
|
+
force: {
|
|
2032
|
+
type: "boolean",
|
|
2033
|
+
description: "Delete permanently without creating backup",
|
|
2034
|
+
default: false,
|
|
2035
|
+
alias: "f"
|
|
2036
|
+
},
|
|
2037
|
+
dry: {
|
|
2038
|
+
type: "boolean",
|
|
2039
|
+
description: "Show what would be deleted",
|
|
2040
|
+
default: false
|
|
2041
|
+
}
|
|
2042
|
+
},
|
|
2043
|
+
async run({ args }) {
|
|
2044
|
+
const result = await deleteScaffold({
|
|
2045
|
+
cwd: process.cwd(),
|
|
2046
|
+
type,
|
|
2047
|
+
path: args.path,
|
|
2048
|
+
force: args.force,
|
|
2049
|
+
dry: args.dry
|
|
2050
|
+
});
|
|
2051
|
+
for (const file of result.deleted) logger.success(`${args.dry ? "[dry-run] Would delete" : args.force ? "Deleted" : "Deleted (backup created)"} ${file}`);
|
|
2052
|
+
for (const err of result.errors) logger.error(err);
|
|
2053
|
+
}
|
|
2054
|
+
},
|
|
2055
|
+
recovery: {
|
|
2056
|
+
meta: {
|
|
2057
|
+
name: "recovery",
|
|
2058
|
+
description: `Recover a deleted ${type} from .bak backup`
|
|
2059
|
+
},
|
|
2060
|
+
args: {
|
|
2061
|
+
path: {
|
|
2062
|
+
type: "positional",
|
|
2063
|
+
description: `${meta.pathLabel} to recover`
|
|
2064
|
+
},
|
|
2065
|
+
dry: {
|
|
2066
|
+
type: "boolean",
|
|
2067
|
+
description: "Check if recovery is possible",
|
|
2068
|
+
default: false
|
|
2069
|
+
}
|
|
2070
|
+
},
|
|
2071
|
+
async run({ args }) {
|
|
2072
|
+
const result = await recoverScaffold({
|
|
2073
|
+
cwd: process.cwd(),
|
|
2074
|
+
type,
|
|
2075
|
+
path: args.path,
|
|
2076
|
+
dry: args.dry
|
|
2077
|
+
});
|
|
2078
|
+
for (const file of result.restored) logger.success(`${args.dry ? "[dry-run] Backup exists for" : "Recovered"} ${file}`);
|
|
2079
|
+
for (const err of result.errors) logger.error(err);
|
|
2080
|
+
}
|
|
2081
|
+
},
|
|
2082
|
+
list: {
|
|
2083
|
+
meta: {
|
|
2084
|
+
name: "list",
|
|
2085
|
+
description: `List existing ${type} files`
|
|
2086
|
+
},
|
|
2087
|
+
async run() {
|
|
2088
|
+
const files = await listScaffoldableFiles(process.cwd(), type);
|
|
2089
|
+
if (files.length === 0) {
|
|
2090
|
+
logger.info(`No ${type} files found`);
|
|
2091
|
+
return;
|
|
2092
|
+
}
|
|
2093
|
+
logger.info(`Found ${files.length} ${type}(s):`);
|
|
2094
|
+
for (const file of files) logger.log(` ${file}`);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
}
|
|
2100
|
+
//#endregion
|
|
2101
|
+
//#region src/cli.ts
|
|
2102
|
+
/**
|
|
2103
|
+
* @ubean/cli — CLI 入口(运行时调用 `runMain`)
|
|
2104
|
+
*
|
|
2105
|
+
* 此文件仅由 `bin/ubean-next.mjs` 导入,不应被库代码 import
|
|
2106
|
+
* (会产生启动 CLI 的副作用)。
|
|
2107
|
+
*
|
|
2108
|
+
* 库代码请从 `@ubean/cli`(主入口)导入 scaffold / fs-ops / templates。
|
|
2109
|
+
*/
|
|
2110
|
+
runMain(defineCommand({
|
|
2111
|
+
meta: {
|
|
2112
|
+
name: "ubean",
|
|
2113
|
+
version: "0.0.1",
|
|
2114
|
+
description: "Vue meta framework built on Vite-Plus with Hono"
|
|
2115
|
+
},
|
|
2116
|
+
subCommands: {
|
|
2117
|
+
dev: devCommand,
|
|
2118
|
+
build: buildCommand,
|
|
2119
|
+
prepare: prepareCommand,
|
|
2120
|
+
preview: previewCommand,
|
|
2121
|
+
init: initCommand,
|
|
2122
|
+
page: pageCommand,
|
|
2123
|
+
env: envCommand,
|
|
2124
|
+
config: configCommand,
|
|
2125
|
+
devtools: devtoolsCommand,
|
|
2126
|
+
api: createScaffoldCommand("api"),
|
|
2127
|
+
layout: createScaffoldCommand("layout"),
|
|
2128
|
+
middleware: createScaffoldCommand("middleware"),
|
|
2129
|
+
cron: createScaffoldCommand("cron"),
|
|
2130
|
+
plugin: createScaffoldCommand("plugin")
|
|
2131
|
+
}
|
|
2132
|
+
}));
|
|
2133
|
+
//#endregion
|
|
2134
|
+
export {};
|