@ubean/cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,26 +1,30 @@
1
- import { E as createFsOps, S as renderTemplate, a as scaffold, c as scaffoldStarterTemplate, i as recoverScaffold, l as scaffoldUnifyTemplate, n as listScaffoldableFiles, o as scaffoldBlogTemplate, r as pageCommand, s as scaffoldMinimalTemplate, t as deleteScaffold } from "./page-D-LZUcPP.js";
2
- import { existsSync, mkdirSync, readFileSync } from "node:fs";
1
+ import { A as summarizeBundle, D as compareBundleBaseline, E as createFsOps, O as findClientManifest, S as renderTemplate, a as scaffold, c as scaffoldStarterTemplate, i as recoverScaffold, j as writeBundleBaseline, k as readViteManifest, l as scaffoldUnifyTemplate, n as listScaffoldableFiles, o as scaffoldBlogTemplate, r as pageCommand, s as scaffoldMinimalTemplate, t as deleteScaffold } from "./page-BRUWKaig.js";
2
+ import { existsSync, mkdirSync, readFileSync, watch } from "node:fs";
3
+ import { basename, join, relative, resolve } from "pathe";
3
4
  import { extname, normalize } from "node:path";
4
- import { getLogger } from "@ubean/logger";
5
+ import { getLogger } from "@ubean/shared/logger";
5
6
  import { defineCommand, runMain } from "citty";
7
+ import { BUILTIN_MODULES, isBuiltinDisabled, loadUbeanConfig, resolveModules, resolvePrerenderConfig, resolveSsrConfig } from "@ubean/config";
6
8
  import { readFile, rm, stat } from "node:fs/promises";
7
9
  import { pathToFileURL } from "node:url";
10
+ import { generateOpenApiTypesFromServer, generateTypes } from "@ubean/build/codegen";
11
+ import { prerender } from "@ubean/build/prerender";
8
12
  import { buildProduction } from "@ubean/build/production";
9
- import { generateOpenApiTypesFromServer, generateTypes } from "@ubean/codegen";
10
- import { loadUbeanConfig, resolvePrerenderConfig, resolveSsrConfig } from "@ubean/config";
11
- import { prerender } from "@ubean/prerender";
12
13
  import { NODE_REQUIREMENTS, createCapabilitySet, diagnoseCapabilities, registerBuiltinPresets, resolvePresetByName } from "@ubean/preset";
13
14
  import { scanProject } from "@ubean/scan";
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";
15
+ import { applyServerConfig, createUbeanApp } from "@ubean/app";
17
16
  import { bold, cyan, dim, green } from "kolorist";
18
- import { execSync, spawn } from "node:child_process";
19
- import { BUILTIN_MODULES, isBuiltinDisabled } from "@ubean/modules";
20
17
  import { createServer } from "node:http";
21
- import { findAvailablePort, waitForPort } from "@ubean/shared/node";
18
+ import { createServer as createServer$1 } from "vite";
19
+ import vue from "@vitejs/plugin-vue";
20
+ import { ubeanPlugin } from "@ubean/build/vite";
21
+ import { VUE_PLUGIN_INCLUDE, ubeanVite } from "@ubean/build/vue";
22
+ import { createVueRenderer } from "@ubean/client/ssr";
23
+ import { ubeanIslandsPlugin } from "@ubean/islands/vite";
24
+ import { findAvailablePort, findUserViteConfig, waitForPort } from "@ubean/shared/node";
25
+ import { execSync, spawn } from "node:child_process";
22
26
  //#region src/config.ts
23
- const logger$8 = getLogger("cli");
27
+ const logger$11 = getLogger("cli");
24
28
  const CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
25
29
 
26
30
  export default defineConfig({
@@ -104,17 +108,17 @@ const configCommand = {
104
108
  const preset = args.preset;
105
109
  const existing = await findConfigFile(fs);
106
110
  if (existing && !args.force) {
107
- logger$8.warn(`Config file already exists: ${existing} (use --force to overwrite)`);
111
+ logger$11.warn(`Config file already exists: ${existing} (use --force to overwrite)`);
108
112
  return;
109
113
  }
110
114
  const content = renderTemplate(CONFIG_TEMPLATE, { variables: { preset } });
111
115
  if (args.dry) {
112
- logger$8.info("[dry-run] Would create ubean.config.ts");
113
- logger$8.info(`\n${content}`);
116
+ logger$11.info("[dry-run] Would create ubean.config.ts");
117
+ logger$11.info(`\n${content}`);
114
118
  return;
115
119
  }
116
120
  await fs.writeFile("ubean.config.ts", content);
117
- logger$8.info("Created ubean.config.ts");
121
+ logger$11.info("Created ubean.config.ts");
118
122
  }
119
123
  },
120
124
  show: {
@@ -127,13 +131,13 @@ const configCommand = {
127
131
  const fs = createFsOps(cwd);
128
132
  const configFile = await findConfigFile(fs);
129
133
  if (!configFile) {
130
- logger$8.warn("No ubean config file found. Run `ubean config init` to create one.");
134
+ logger$11.warn("No ubean config file found. Run `ubean config init` to create one.");
131
135
  return;
132
136
  }
133
- logger$8.info(`Config file: ${configFile}`);
134
- logger$8.info("");
137
+ logger$11.info(`Config file: ${configFile}`);
138
+ logger$11.info("");
135
139
  const content = await fs.readFile(configFile);
136
- logger$8.info(content);
140
+ logger$11.info(content);
137
141
  }
138
142
  },
139
143
  example: {
@@ -142,7 +146,7 @@ const configCommand = {
142
146
  description: "Show a full example configuration with all options"
143
147
  },
144
148
  async run() {
145
- logger$8.info(CONFIG_EXAMPLE_FULL);
149
+ logger$11.info(CONFIG_EXAMPLE_FULL);
146
150
  }
147
151
  },
148
152
  path: {
@@ -155,17 +159,112 @@ const configCommand = {
155
159
  const fs = createFsOps(cwd);
156
160
  const configFile = await findConfigFile(fs);
157
161
  if (!configFile) {
158
- logger$8.error("No config file found");
162
+ logger$11.error("No config file found");
159
163
  process.exit(1);
160
164
  }
161
- logger$8.info(fs.resolve(configFile));
165
+ logger$11.info(fs.resolve(configFile));
162
166
  }
163
167
  }
164
168
  }
165
169
  };
166
170
  //#endregion
171
+ //#region src/analyze.ts
172
+ const logger$10 = getLogger("cli");
173
+ function readStringArg(value) {
174
+ return typeof value === "string" && value.length > 0 ? value : void 0;
175
+ }
176
+ const analyzeCommand = {
177
+ meta: {
178
+ name: "analyze",
179
+ description: "Report client JS budget from the Vite client manifest (run after `ubean build`)"
180
+ },
181
+ args: {
182
+ cwd: {
183
+ type: "string",
184
+ description: "Project root (defaults to process.cwd())"
185
+ },
186
+ write: {
187
+ type: "boolean",
188
+ description: "Write `.ubean/bundle-baseline.json`",
189
+ default: true
190
+ },
191
+ out: {
192
+ type: "string",
193
+ description: "Baseline JSON path (default: .ubean/bundle-baseline.json)"
194
+ },
195
+ check: {
196
+ type: "string",
197
+ description: "Committed baseline JSON to compare against (fails if gzip grows past --max-increase)"
198
+ },
199
+ maxIncrease: {
200
+ type: "string",
201
+ description: "Allowed relative gzip growth when using --check (default 0.05)"
202
+ }
203
+ },
204
+ async run({ args }) {
205
+ const cwd = resolve(typeof args.cwd === "string" ? args.cwd : process.cwd());
206
+ if (!existsSync(join(cwd, "package.json"))) throw new Error(`[ubean] analyze: ${cwd} is not a project root`);
207
+ await loadUbeanConfig(cwd).catch(() => void 0);
208
+ const found = findClientManifest(cwd);
209
+ if (!found) {
210
+ logger$10.error("No client Vite manifest found. Run `ubean build` first.");
211
+ throw new Error("missing client manifest");
212
+ }
213
+ const manifest = readViteManifest(found.manifestPath);
214
+ const baseline = summarizeBundle(found.outDir, manifest);
215
+ baseline.outDir = relative(cwd, found.outDir).replace(/\\/g, "/") || baseline.outDir;
216
+ logger$10.info(`client JS: ${(baseline.totalGzip / 1024).toFixed(1)} kB gzip (${baseline.entries.length} js chunks, entry ${(baseline.entryGzip / 1024).toFixed(1)} kB)`);
217
+ for (const entry of baseline.entries.slice(0, 12)) logger$10.info(` ${entry.isEntry ? "[entry] " : ""}${entry.file} ${(entry.gzip / 1024).toFixed(1)} kB gzip`);
218
+ if (args.write !== false) {
219
+ const outArg = readStringArg(args.out);
220
+ const out = outArg ? resolve(cwd, outArg) : join(cwd, ".ubean/bundle-baseline.json");
221
+ writeBundleBaseline(out, baseline);
222
+ logger$10.info(`wrote ${out}`);
223
+ }
224
+ const checkArg = readStringArg(args.check);
225
+ if (checkArg) {
226
+ const checkPath = resolve(cwd, checkArg);
227
+ if (!existsSync(checkPath)) {
228
+ logger$10.error(`baseline not found: ${checkPath}`);
229
+ throw new Error("missing budget baseline");
230
+ }
231
+ const committed = JSON.parse(readFileSync(checkPath, "utf8"));
232
+ const maxIncrease = Number(args.maxIncrease ?? .05);
233
+ const result = compareBundleBaseline(baseline, {
234
+ totalGzip: committed.totalGzip ?? 0,
235
+ entryGzip: committed.entryGzip ?? 0
236
+ }, { maxIncrease: Number.isFinite(maxIncrease) ? maxIncrease : .05 });
237
+ if (!result.ok) {
238
+ for (const message of result.messages) logger$10.error(message);
239
+ throw new Error("client JS budget exceeded");
240
+ }
241
+ logger$10.info(`budget ok (total ${(result.totalRatio * 100).toFixed(1)}%, entry ${(result.entryRatio * 100).toFixed(1)}% vs baseline, max ${(result.maxIncrease * 100).toFixed(0)}%)`);
242
+ }
243
+ }
244
+ };
245
+ //#endregion
167
246
  //#region src/build.ts
168
- const logger$7 = getLogger("cli");
247
+ const logger$9 = getLogger("cli");
248
+ async function loadContentForBuild(cwd, content) {
249
+ if (!content) return {
250
+ snapshot: void 0,
251
+ routes: []
252
+ };
253
+ try {
254
+ const mod = await import("@ubean/content");
255
+ const options = content === true ? {} : content;
256
+ const snapshot = mod.scanContentSources(cwd, options);
257
+ return {
258
+ snapshot,
259
+ routes: mod.extractContentPageRoutes(Object.values(snapshot).flat())
260
+ };
261
+ } catch {
262
+ return {
263
+ snapshot: void 0,
264
+ routes: []
265
+ };
266
+ }
267
+ }
169
268
  /**
170
269
  * Creates a fetcher that invokes the built SSR entry to render real HTML.
171
270
  * Inspired by void's Node prerender runner: import the built `entry.mjs`,
@@ -177,16 +276,16 @@ const logger$7 = getLogger("cli");
177
276
  */
178
277
  async function createSsrFetcher(cwd, manifest) {
179
278
  try {
180
- const entryPath = resolve$1(cwd, manifest.serverDir, "entry.mjs");
279
+ const entryPath = resolve(cwd, manifest.serverDir, "entry.mjs");
181
280
  const mod = await import(pathToFileURL(entryPath).href);
182
281
  const createFetchHandler = mod.default ?? mod.createFetchHandler;
183
282
  if (typeof createFetchHandler !== "function") {
184
- logger$7.warn("SSR entry does not export createFetchHandler; falling back to placeholder prerender.");
283
+ logger$9.warn("SSR entry does not export createFetchHandler; falling back to placeholder prerender.");
185
284
  return;
186
285
  }
187
286
  const fetch = await createFetchHandler();
188
287
  if (typeof fetch !== "function") {
189
- logger$7.warn("SSR entry did not return a fetch handler; falling back to placeholder prerender.");
288
+ logger$9.warn("SSR entry did not return a fetch handler; falling back to placeholder prerender.");
190
289
  return;
191
290
  }
192
291
  return async (url) => {
@@ -198,7 +297,7 @@ async function createSsrFetcher(cwd, manifest) {
198
297
  };
199
298
  };
200
299
  } catch (err) {
201
- logger$7.warn(`Failed to load SSR entry for prerender: ${err instanceof Error ? err.message : String(err)}. Falling back to placeholder prerender.`);
300
+ logger$9.warn(`Failed to load SSR entry for prerender: ${err instanceof Error ? err.message : String(err)}. Falling back to placeholder prerender.`);
202
301
  return;
203
302
  }
204
303
  }
@@ -246,8 +345,8 @@ const buildCommand = {
246
345
  }
247
346
  },
248
347
  async run({ args }) {
249
- const cwd = resolve$1(args.cwd || process.cwd());
250
- logger$7.info("Building ubean application...");
348
+ const cwd = resolve(args.cwd || process.cwd());
349
+ logger$9.info("Building ubean application...");
251
350
  try {
252
351
  registerBuiltinPresets();
253
352
  const config = await loadUbeanConfig(cwd);
@@ -263,17 +362,17 @@ const buildCommand = {
263
362
  } else if (args.prerender === false) config.prerender = resolvePrerenderConfig();
264
363
  const presetName = args.preset || config.build.preset;
265
364
  const preset = resolvePresetByName(presetName);
266
- logger$7.info(`Using preset: ${preset.name}`);
267
- logger$7.info(`App mode: ${config.mode}${config.mode === "fullstack" ? ` (ssr=${config.ssr.enabled})` : ""}`);
268
- logger$7.info("Scanning project...");
365
+ logger$9.info(`Using preset: ${preset.name}`);
366
+ logger$9.info(`App mode: ${config.mode}${config.mode === "fullstack" ? ` (ssr=${config.ssr.enabled})` : ""}`);
367
+ logger$9.info("Scanning project...");
269
368
  const result = await scanProject({
270
369
  cwd,
271
370
  srcDir: config.srcDir,
272
371
  dirs: config.dir,
273
372
  ignore: config.scanOptions?.ignore
274
373
  });
275
- logger$7.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts`);
276
- logger$7.info("Generating types...");
374
+ logger$9.info(`Found ${result.apiRoutes.length} API routes, ${result.pages.length} pages, ${result.layouts.length} layouts`);
375
+ logger$9.info("Generating types...");
277
376
  await generateTypes(result, {
278
377
  cwd,
279
378
  srcDir: config.srcDir,
@@ -291,18 +390,20 @@ const buildCommand = {
291
390
  preset: resolvedPreset,
292
391
  config
293
392
  });
294
- logger$7.info("Building with Vite...");
393
+ logger$9.info("Building with Vite...");
394
+ const { snapshot: contentSnapshot, routes: contentRoutes } = await loadContentForBuild(cwd, config.content);
295
395
  const manifest = await buildProduction({
296
396
  cwd,
297
397
  config,
298
398
  preset: resolvedPreset,
299
399
  scanResult: result,
300
400
  minify: args.minify,
301
- sourcemap: args.sourcemap
401
+ sourcemap: args.sourcemap,
402
+ contentSnapshot
302
403
  });
303
404
  const hasPrerenderRules = Object.values(config.routeRules || {}).some((r) => r?.prerender === true);
304
405
  if (config.prerender.enabled || hasPrerenderRules) {
305
- logger$7.info("Prerendering static pages...");
406
+ logger$9.info("Prerendering static pages...");
306
407
  const fetcher = await createSsrFetcher(cwd, manifest);
307
408
  await prerender({
308
409
  cwd,
@@ -310,13 +411,14 @@ const buildCommand = {
310
411
  pages: result.pages,
311
412
  prerender: config.prerender,
312
413
  routeRules: config.routeRules,
414
+ contentRoutes,
313
415
  fetcher
314
416
  });
315
417
  }
316
418
  if (config.mode === "ssg" && !process.env.UBEAN_KEEP_SSR) {
317
- const serverDir = join$1(cwd, config.build.outputDir, "server");
419
+ const serverDir = join(cwd, config.build.outputDir, "server");
318
420
  if (existsSync(serverDir)) {
319
- logger$7.info("Cleaning temporary SSR bundle (SSG mode)...");
421
+ logger$9.info("Cleaning temporary SSR bundle (SSG mode)...");
320
422
  await rm(serverDir, {
321
423
  recursive: true,
322
424
  force: true
@@ -330,18 +432,567 @@ const buildCommand = {
330
432
  config,
331
433
  manifest
332
434
  });
333
- logger$7.info(`Build complete for preset "${resolvedPreset.name}"!`);
334
- logger$7.info(` Output directory: ${resolve$1(cwd, config.build.outputDir)}`);
335
- if (manifest.entry) logger$7.info(` Server entry: ${manifest.entry}`);
336
- logger$7.info(` Client assets: ${manifest.assets.length} files`);
435
+ logger$9.info(`Build complete for preset "${resolvedPreset.name}"!`);
436
+ logger$9.info(` Output directory: ${resolve(cwd, config.build.outputDir)}`);
437
+ if (manifest.entry) logger$9.info(` Server entry: ${manifest.entry}`);
438
+ logger$9.info(` Client assets: ${manifest.assets.length} files`);
337
439
  } catch (err) {
338
- logger$7.error(err instanceof Error ? err.message : String(err));
440
+ logger$9.error(err instanceof Error ? err.message : String(err));
339
441
  process.exit(1);
340
442
  }
341
443
  process.exit(0);
342
444
  }
343
445
  };
344
446
  //#endregion
447
+ //#region src/dev-server/vite-server.ts
448
+ const logger$8 = getLogger("dev-server");
449
+ async function toWebRequest(req, host, protocol) {
450
+ const url = `${protocol}://${req.headers.host || host}${req.url || "/"}`;
451
+ const headers = new Headers();
452
+ for (const [key, value] of Object.entries(req.headers)) if (value) {
453
+ if (Array.isArray(value)) for (const v of value) headers.append(key, v);
454
+ else headers.set(key, value);
455
+ }
456
+ const method = req.method || "GET";
457
+ return new Request(url, {
458
+ method,
459
+ headers,
460
+ body: method === "GET" || method === "HEAD" ? void 0 : req,
461
+ duplex: "half"
462
+ });
463
+ }
464
+ async function sendWebResponse(res, webRes) {
465
+ res.statusCode = webRes.status;
466
+ res.statusMessage = webRes.statusText;
467
+ webRes.headers.forEach((value, key) => {
468
+ res.setHeader(key, value);
469
+ });
470
+ if (webRes.body) {
471
+ const reader = webRes.body.getReader();
472
+ try {
473
+ while (true) {
474
+ const { done, value } = await reader.read();
475
+ if (done) break;
476
+ res.write(value);
477
+ }
478
+ } finally {
479
+ reader.releaseLock();
480
+ }
481
+ }
482
+ res.end();
483
+ }
484
+ function loadScaffoldOps() {
485
+ return {
486
+ createFsOps,
487
+ scaffold,
488
+ deleteScaffold,
489
+ recoverScaffold
490
+ };
491
+ }
492
+ async function createViteDevServer(options) {
493
+ const { cwd, config, app: initialApp, layouts: initialLayouts = [] } = options;
494
+ const host = options.host || "localhost";
495
+ const strictPort = options.strictPort ?? false;
496
+ let currentApp = initialApp;
497
+ let currentLayouts = initialLayouts;
498
+ let httpServer = null;
499
+ let viteServer = null;
500
+ let serverConfigApplied = false;
501
+ let serverReadyCalled = false;
502
+ let cachedServerConfig = null;
503
+ let refreshDevtools = null;
504
+ const requestedPort = options.port;
505
+ let actualPort;
506
+ try {
507
+ actualPort = await findAvailablePort(requestedPort, {
508
+ host,
509
+ strictPort
510
+ });
511
+ } catch (err) {
512
+ if (err?.code === "EADDRINUSE") throw new Error(`Port ${requestedPort} is already in use${host ? ` on ${host}` : ""}. Try a different port or remove the --strictPort flag.`, { cause: err });
513
+ throw err;
514
+ }
515
+ if (actualPort !== requestedPort) logger$8.warn(`Port ${requestedPort} is in use, trying ${actualPort} instead.`);
516
+ httpServer = createServer(async (req, res) => {
517
+ try {
518
+ const pathname = (req.url || "/").split("?")[0].split("#")[0];
519
+ if (devtoolsEnabled && (pathname === "/_devtools" || pathname === "/_devtools/")) {
520
+ res.statusCode = 302;
521
+ res.setHeader("Location", "/__devtools/");
522
+ res.end();
523
+ return;
524
+ }
525
+ await new Promise((resolve, reject) => {
526
+ viteServer.middlewares(req, res, (err) => {
527
+ if (err) reject(err);
528
+ else resolve();
529
+ });
530
+ });
531
+ if (res.writableEnded) return;
532
+ try {
533
+ await viteServer.ssrLoadModule("ubean:locales");
534
+ } catch (err) {
535
+ logger$8.warn("[ubean] Failed to resolve locales module:", err);
536
+ }
537
+ if (!serverConfigApplied) {
538
+ serverConfigApplied = true;
539
+ try {
540
+ const serverMod = await viteServer.ssrLoadModule("virtual:ubean-server");
541
+ if (serverMod?.resolveServerConfig) {
542
+ cachedServerConfig = serverMod.resolveServerConfig("dev");
543
+ await applyServerConfig(currentApp, cachedServerConfig);
544
+ }
545
+ } catch (err) {
546
+ logger$8.warn("[ubean] Failed to load server config:", err);
547
+ }
548
+ }
549
+ await currentApp.init();
550
+ if (!serverReadyCalled) {
551
+ serverReadyCalled = true;
552
+ if (cachedServerConfig?.onServerReady) try {
553
+ await cachedServerConfig.onServerReady(currentApp);
554
+ } catch (err) {
555
+ logger$8.warn("[ubean] onServerReady error:", err);
556
+ }
557
+ }
558
+ const protocol = req.socket?.encrypted ? "https" : "http";
559
+ const webReq = await toWebRequest(req, host, protocol);
560
+ const webRes = await currentApp.fetch(webReq);
561
+ const contentType = webRes.headers.get("content-type") || "";
562
+ const skipTransform = (req.url || "").startsWith("/_devtools");
563
+ if (contentType.includes("text/html") && webRes.body && !skipTransform) {
564
+ const html = await webRes.text();
565
+ const transformedHtml = await viteServer.transformIndexHtml(req.url || "/", html);
566
+ res.statusCode = webRes.status;
567
+ res.statusMessage = webRes.statusText;
568
+ webRes.headers.forEach((value, key) => {
569
+ res.setHeader(key, value);
570
+ });
571
+ res.end(transformedHtml);
572
+ } else await sendWebResponse(res, webRes);
573
+ } catch (err) {
574
+ if (viteServer) viteServer.ssrFixStacktrace(err);
575
+ if (!res.headersSent) {
576
+ res.statusCode = 500;
577
+ const errorHtml = `<!DOCTYPE html>
578
+ <html>
579
+ <head>
580
+ <title>Server Error</title>
581
+ <style>
582
+ body { font-family: monospace; padding: 2rem; background: #1a1a1a; color: #ff6b6b; }
583
+ pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
584
+ </style>
585
+ </head>
586
+ <body>
587
+ <h1>Internal Server Error</h1>
588
+ <pre>${err.stack || err.message}</pre>
589
+ </body>
590
+ </html>`;
591
+ res.setHeader("Content-Type", "text/html");
592
+ res.end(errorHtml);
593
+ } else if (!res.writableEnded) res.end(err instanceof Error ? err.message : "Internal Server Error");
594
+ }
595
+ });
596
+ const devtoolsEnabled = options.config.devtools.enabled;
597
+ let devtoolsPlugin = null;
598
+ if (devtoolsEnabled) try {
599
+ const { ubeanDevtoolsPlugin } = await import("@ubean/devtools");
600
+ const devtoolsOpts = options.devtools;
601
+ const scaffoldOps = await loadScaffoldOps();
602
+ devtoolsPlugin = ubeanDevtoolsPlugin({
603
+ getCwd: () => cwd,
604
+ getApp: () => currentApp,
605
+ ...scaffoldOps ? { scaffoldOps } : {},
606
+ ...devtoolsOpts?.getScanResult ? { getScanResult: devtoolsOpts.getScanResult } : {},
607
+ ...devtoolsOpts?.getConfigMeta ? { getConfigMeta: devtoolsOpts.getConfigMeta } : {},
608
+ ...devtoolsOpts?.getCustomTabs ? { getCustomTabs: devtoolsOpts.getCustomTabs } : {},
609
+ ...devtoolsOpts?.ai ? { ai: devtoolsOpts.ai } : {},
610
+ ...devtoolsOpts?.getApp ? { getApp: devtoolsOpts.getApp } : {},
611
+ ...devtoolsOpts?.triggerRescan ? { triggerRescan: devtoolsOpts.triggerRescan } : {},
612
+ registerRefresh: (fn) => {
613
+ refreshDevtools = fn;
614
+ }
615
+ });
616
+ } catch {}
617
+ let viteDevtoolsPlugins = [];
618
+ if (devtoolsEnabled) try {
619
+ const { DevTools } = await import("@vitejs/devtools");
620
+ viteDevtoolsPlugins = await DevTools({ builtinDevTools: false });
621
+ } catch {}
622
+ const userViteConfig = findUserViteConfig(cwd);
623
+ const hasUserViteConfig = !!userViteConfig;
624
+ const isBackendMode = config.mode === "backend";
625
+ const builtinPlugins = [
626
+ {
627
+ name: "ubean:http-server-binder",
628
+ enforce: "pre",
629
+ configureServer(server) {
630
+ if (!server.httpServer && httpServer) server.httpServer = httpServer;
631
+ }
632
+ },
633
+ ...isBackendMode ? [] : [vue({
634
+ include: VUE_PLUGIN_INCLUDE,
635
+ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("ubean-") } }
636
+ })],
637
+ ...hasUserViteConfig ? [] : [
638
+ ubeanPlugin({ config }),
639
+ ...isBackendMode ? [] : ubeanVite({ config }),
640
+ ...isBackendMode ? [] : [ubeanIslandsPlugin()]
641
+ ],
642
+ ...viteDevtoolsPlugins,
643
+ ...devtoolsPlugin ? [devtoolsPlugin] : []
644
+ ];
645
+ const { plugins } = await resolveModules({
646
+ cwd,
647
+ config,
648
+ builtinPlugins
649
+ });
650
+ if (devtoolsEnabled) process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = "true";
651
+ viteServer = await createServer$1({
652
+ root: cwd,
653
+ configFile: userViteConfig ?? false,
654
+ server: {
655
+ middlewareMode: { server: httpServer },
656
+ hmr: { port: actualPort + 1e3 }
657
+ },
658
+ appType: "custom",
659
+ plugins,
660
+ devtools: {
661
+ enabled: devtoolsEnabled,
662
+ clientAuth: false
663
+ },
664
+ optimizeDeps: { exclude: [
665
+ "ubean",
666
+ "virtual:ubean-pages",
667
+ "virtual:ubean-app",
668
+ "virtual:ubean-server",
669
+ "virtual:ubean-client-entry",
670
+ "#ubean-pages",
671
+ "#ubean-app",
672
+ "#ubean-server",
673
+ "#ubean-client-entry"
674
+ ] },
675
+ ssr: {
676
+ noExternal: ["ubean"],
677
+ external: ["@ubean/i18n"]
678
+ }
679
+ });
680
+ function enhanceAppWithVite(app, layouts = []) {
681
+ app.options.layouts = layouts;
682
+ const routeLoaders = {};
683
+ for (const route of app.options.routes || []) {
684
+ const fullPath = route.fullPath;
685
+ routeLoaders[route.relativePath] = () => viteServer.ssrLoadModule(fullPath);
686
+ }
687
+ app.options.routeLoaders = routeLoaders;
688
+ const pageLoaders = {};
689
+ const pageByName = /* @__PURE__ */ new Map();
690
+ for (const page of app.options.pages || []) pageByName.set(page.name, page);
691
+ for (const page of app.options.pages || []) {
692
+ const fullPath = (page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
693
+ pageLoaders[page.relativePath] = () => viteServer.ssrLoadModule(fullPath);
694
+ }
695
+ app.options.pageLoaders = pageLoaders;
696
+ const middlewareLoaders = {};
697
+ for (const mw of app.options.middleware || []) {
698
+ const fullPath = mw.fullPath;
699
+ middlewareLoaders[mw.relativePath] = () => viteServer.ssrLoadModule(fullPath);
700
+ }
701
+ app.options.middlewareLoaders = middlewareLoaders;
702
+ const cronFiles = app.options.crons || [];
703
+ if (cronFiles.length > 0) Promise.all(cronFiles.map((c) => viteServer.ssrLoadModule(c.fullPath))).then(async () => {
704
+ (await import("@ubean/server/cron")).startCronScheduler();
705
+ }).catch((err) => {
706
+ console.error("[ubean] Failed to load cron files:", err);
707
+ });
708
+ const layoutMap = /* @__PURE__ */ new Map();
709
+ for (const layout of layouts) layoutMap.set(layout.name, layout.fullPath);
710
+ const defaultLayout = layouts.find((l) => l.isDefault)?.name || null;
711
+ if (config.mode === "backend") {
712
+ app.resetInit();
713
+ return;
714
+ }
715
+ const scannedPages = app.options.pages || [];
716
+ const scannedPageByName = /* @__PURE__ */ new Map();
717
+ for (const p of scannedPages) scannedPageByName.set(p.name, p);
718
+ const routes = scannedPages.map((p) => {
719
+ const componentFullPath = (p.isReuse && p.reuseTarget ? scannedPageByName.get(p.reuseTarget) : void 0)?.fullPath || p.fullPath;
720
+ return {
721
+ path: p.route.replace(/\*\*:(\w[\w-]*)/g, ":$1(.*)*"),
722
+ name: p.name,
723
+ component: async () => {
724
+ const mod = await viteServer.ssrLoadModule(componentFullPath);
725
+ return mod.default || mod;
726
+ },
727
+ meta: {
728
+ layout: p.layout === false ? false : p.layout || defaultLayout,
729
+ pageName: p.name,
730
+ cache: p.cache === true ? true : void 0
731
+ }
732
+ };
733
+ });
734
+ let appConfigModule = null;
735
+ const getAppConfigModule = () => {
736
+ if (!appConfigModule) appConfigModule = viteServer.ssrLoadModule("virtual:ubean-app");
737
+ return appConfigModule;
738
+ };
739
+ app.options.pageRenderer = createVueRenderer({
740
+ routes,
741
+ async resolveLayoutComponent(name) {
742
+ if (name === false || name == null) return null;
743
+ const fullPath = layoutMap.get(name);
744
+ if (!fullPath) return null;
745
+ const mod = await viteServer.ssrLoadModule(fullPath);
746
+ return mod.default || mod;
747
+ },
748
+ defaultLayout,
749
+ async resolveAppConfig() {
750
+ return (await getAppConfigModule()).resolveAppConfig("server");
751
+ }
752
+ });
753
+ app.resetInit();
754
+ }
755
+ enhanceAppWithVite(currentApp, currentLayouts);
756
+ const getUrl = () => `http://${host}:${actualPort}`;
757
+ return {
758
+ get port() {
759
+ return actualPort;
760
+ },
761
+ get host() {
762
+ return host;
763
+ },
764
+ get url() {
765
+ return getUrl();
766
+ },
767
+ get viteServer() {
768
+ return viteServer;
769
+ },
770
+ async start() {
771
+ await new Promise((resolve, reject) => {
772
+ const tryListen = (port) => {
773
+ httpServer.removeAllListeners("error");
774
+ httpServer.once("error", (err) => {
775
+ if (err.code === "EADDRINUSE" && !strictPort) {
776
+ logger$8.warn(`Port ${port} is in use, trying ${port + 1} instead.`);
777
+ tryListen(port + 1);
778
+ } else reject(err);
779
+ });
780
+ httpServer.listen(port, host, () => {
781
+ const addr = httpServer.address();
782
+ actualPort = typeof addr === "object" && addr ? addr.port : port;
783
+ resolve();
784
+ });
785
+ };
786
+ tryListen(actualPort);
787
+ });
788
+ options.onListen?.({
789
+ port: actualPort,
790
+ host,
791
+ url: getUrl()
792
+ });
793
+ },
794
+ async stop() {
795
+ if (viteServer) {
796
+ await viteServer.close();
797
+ viteServer = null;
798
+ }
799
+ if (httpServer) {
800
+ await new Promise((res, rej) => {
801
+ httpServer.close((err) => err ? rej(err) : res());
802
+ });
803
+ httpServer = null;
804
+ }
805
+ },
806
+ updateApp(app, layouts) {
807
+ currentApp = app;
808
+ if (layouts) currentLayouts = layouts;
809
+ serverConfigApplied = false;
810
+ serverReadyCalled = false;
811
+ cachedServerConfig = null;
812
+ enhanceAppWithVite(app, currentLayouts);
813
+ refreshDevtools?.();
814
+ },
815
+ sendFullReload() {
816
+ if (viteServer) viteServer.ws.send({ type: "full-reload" });
817
+ }
818
+ };
819
+ }
820
+ //#endregion
821
+ //#region src/dev-server/runner.ts
822
+ var ViteNodeDevRunner = class {
823
+ viteDevServer = null;
824
+ currentApp;
825
+ currentLayouts;
826
+ options;
827
+ _port;
828
+ constructor(options) {
829
+ this.options = options;
830
+ this.currentApp = options.app;
831
+ this.currentLayouts = options.layouts || [];
832
+ this._port = options.port;
833
+ }
834
+ get port() {
835
+ return this._port;
836
+ }
837
+ get host() {
838
+ return this.options.host;
839
+ }
840
+ get url() {
841
+ return `http://${this.options.host}:${this._port}`;
842
+ }
843
+ get preset() {
844
+ return this.options.preset;
845
+ }
846
+ async start() {
847
+ this.viteDevServer = await createViteDevServer({
848
+ cwd: this.options.cwd,
849
+ port: this._port,
850
+ host: this.options.host,
851
+ strictPort: this.options.strictPort,
852
+ config: this.options.config,
853
+ app: this.currentApp,
854
+ layouts: this.currentLayouts,
855
+ devtools: this.options.devtools,
856
+ onListen: ({ port, host, url }) => {
857
+ this._port = port;
858
+ this.options.onListen?.({
859
+ port,
860
+ host,
861
+ url
862
+ });
863
+ }
864
+ });
865
+ await this.viteDevServer.start();
866
+ }
867
+ async stop() {
868
+ if (this.viteDevServer) {
869
+ await this.viteDevServer.stop();
870
+ this.viteDevServer = null;
871
+ }
872
+ }
873
+ async reload() {
874
+ await this.options.onBeforeReload?.();
875
+ if (this.viteDevServer) this.viteDevServer.updateApp(this.currentApp, this.currentLayouts);
876
+ await this.options.onAfterReload?.();
877
+ }
878
+ updateApp(app, layouts) {
879
+ this.currentApp = app;
880
+ if (layouts) this.currentLayouts = layouts;
881
+ if (this.viteDevServer) this.viteDevServer.updateApp(app, this.currentLayouts);
882
+ }
883
+ sendFullReload() {
884
+ this.viteDevServer?.sendFullReload();
885
+ }
886
+ };
887
+ const runners = [{
888
+ name: "vite-node",
889
+ isAvailable() {
890
+ return typeof process !== "undefined" && !!process.versions?.node;
891
+ },
892
+ async createRunner(options) {
893
+ return new ViteNodeDevRunner(options);
894
+ }
895
+ }];
896
+ async function selectRunner(_preset) {
897
+ for (const runner of runners) if (await runner.isAvailable()) return runner;
898
+ return null;
899
+ }
900
+ async function createDevRunner(options) {
901
+ const runner = await selectRunner(options.preset);
902
+ if (!runner) throw new Error(`No compatible dev runner found for preset "${options.preset.name}". Ensure you are running in a supported environment (Node.js).`);
903
+ return runner.createRunner(options);
904
+ }
905
+ //#endregion
906
+ //#region src/dev-server/watcher.ts
907
+ function createDevWatcher(options) {
908
+ const { cwd, dirs, ignore = [], debounceMs = 100 } = options;
909
+ const watchers = [];
910
+ const watchedDirs = /* @__PURE__ */ new Set();
911
+ let pendingEvents = [];
912
+ let debounceTimer = null;
913
+ function isIgnored(relPath) {
914
+ return ignore.some((pattern) => {
915
+ if (pattern.includes("*")) return new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(relPath);
916
+ return relPath.startsWith(pattern) || relPath === pattern;
917
+ });
918
+ }
919
+ function queueEvent(type, fullPath) {
920
+ const relPath = relative(cwd, fullPath).replace(/\\/g, "/");
921
+ if (isIgnored(relPath)) return;
922
+ if (relPath.startsWith("node_modules/") || relPath.startsWith(".git/")) return;
923
+ if (relPath.endsWith(".bak") || relPath.startsWith(".ubean/")) return;
924
+ pendingEvents.push({
925
+ type,
926
+ path: fullPath,
927
+ relativePath: relPath
928
+ });
929
+ if (debounceTimer) clearTimeout(debounceTimer);
930
+ debounceTimer = setTimeout(() => {
931
+ const events = pendingEvents;
932
+ pendingEvents = [];
933
+ debounceTimer = null;
934
+ options.onChange?.(events);
935
+ }, debounceMs);
936
+ }
937
+ function watchDir(dir) {
938
+ if (watchedDirs.has(dir)) return;
939
+ watchedDirs.add(dir);
940
+ try {
941
+ const w = watch(dir, { recursive: true }, (eventType, filename) => {
942
+ if (!filename) return;
943
+ const fullPath = join(dir, filename);
944
+ queueEvent(eventType === "rename" ? "unlink" : "change", fullPath);
945
+ });
946
+ watchers.push(w);
947
+ } catch {}
948
+ }
949
+ function start() {
950
+ for (const dir of dirs) watchDir(join(cwd, dir));
951
+ }
952
+ function stop() {
953
+ for (const w of watchers) w.close();
954
+ watchers.length = 0;
955
+ watchedDirs.clear();
956
+ if (debounceTimer) {
957
+ clearTimeout(debounceTimer);
958
+ debounceTimer = null;
959
+ }
960
+ }
961
+ function addDir(dir) {
962
+ watchDir(join(cwd, dir));
963
+ }
964
+ return {
965
+ start,
966
+ stop,
967
+ addDir
968
+ };
969
+ }
970
+ //#endregion
971
+ //#region src/dev-server/index.ts
972
+ const logger$7 = getLogger("dev-server");
973
+ function formatDiagnostics(diagnostics) {
974
+ const errors = [];
975
+ const warnings = [];
976
+ for (const err of diagnostics.diagnostics.filter((d) => !d.supported && d.required)) errors.push(` ✗ ${err.capability}: ${err.message}`);
977
+ for (const warn of diagnostics.diagnostics.filter((d) => !d.supported && !d.required)) warnings.push(` ! ${warn.capability}: ${warn.message}`);
978
+ return {
979
+ errors,
980
+ warnings
981
+ };
982
+ }
983
+ function logDiagnostics(diagnostics) {
984
+ const { errors, warnings } = formatDiagnostics(diagnostics);
985
+ if (errors.length > 0) {
986
+ logger$7.error(`Capability errors (${errors.length}):`);
987
+ for (const e of errors) logger$7.error(e);
988
+ }
989
+ if (warnings.length > 0) {
990
+ logger$7.warn(`Capability warnings (${warnings.length}):`);
991
+ for (const w of warnings) logger$7.warn(w);
992
+ }
993
+ if (errors.length === 0 && warnings.length === 0) logger$7.info("All capability checks passed");
994
+ }
995
+ //#endregion
345
996
  //#region src/dev.ts
346
997
  const logger$6 = getLogger("cli");
347
998
  /**
@@ -393,7 +1044,7 @@ const devCommand = {
393
1044
  }
394
1045
  },
395
1046
  async run({ args }) {
396
- const cwd = resolve$1(args.cwd || process.cwd());
1047
+ const cwd = resolve(args.cwd || process.cwd());
397
1048
  logger$6.info("Starting ubean dev server...");
398
1049
  registerBuiltinPresets();
399
1050
  const config = await loadUbeanConfig(cwd);
@@ -465,7 +1116,7 @@ const devCommand = {
465
1116
  if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
466
1117
  lines.push(` → ${dim("Press Ctrl+C to stop")}`);
467
1118
  logger$6.info(lines.join("\n"));
468
- generateOpenApiTypesFromServer(url, { outDir: resolve$1(cwd, ".ubean") }).then((filePath) => {
1119
+ generateOpenApiTypesFromServer(url, { outDir: resolve(cwd, ".ubean") }).then((filePath) => {
469
1120
  logger$6.info(`OpenAPI types generated: ${filePath}`);
470
1121
  }).catch((err) => {
471
1122
  logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
@@ -556,7 +1207,12 @@ async function buildApp(cwd, config) {
556
1207
  i18nConfig: config.i18n,
557
1208
  ssrExclude: config.ssr.exclude,
558
1209
  streaming: config.ssr.streaming,
559
- notFoundPage: result.notFoundPage
1210
+ notFoundPage: result.notFoundPage,
1211
+ csrf: config.security === false ? false : config.security?.csrf ?? true,
1212
+ securityHeaders: config.security === false ? false : config.security?.headers === false ? false : config.security?.headers ?? true,
1213
+ dataCache: config.dataCache,
1214
+ cache: config.cache,
1215
+ seoConventions: { srcDir: resolve(cwd, config.srcDir) }
560
1216
  });
561
1217
  app.hooks.hook("request:start", (c) => {
562
1218
  logger$6.info(`${c.req.method} ${c.req.path}`);
@@ -955,7 +1611,7 @@ function isNonInteractive() {
955
1611
  return !process.stdin.isTTY || process.env.CI !== void 0 || process.env.NONINTERACTIVE !== void 0;
956
1612
  }
957
1613
  async function scaffoldProject(options) {
958
- const targetDir = resolve$1(options.cwd, options.dir);
1614
+ const targetDir = resolve(options.cwd, options.dir);
959
1615
  const fs = createFsOps(targetDir);
960
1616
  const projectName = options.name || (options.dir === "." ? basename(options.cwd) : basename(options.dir));
961
1617
  const pm = options.packageManager || "npm";
@@ -1084,7 +1740,7 @@ const initCommand = {
1084
1740
  //#region src/prepare.ts
1085
1741
  const logger$2 = getLogger("cli");
1086
1742
  async function ensureBuildDir(cwd, buildDir) {
1087
- const fullPath = join$1(cwd, buildDir);
1743
+ const fullPath = join(cwd, buildDir);
1088
1744
  if (!existsSync(fullPath)) mkdirSync(fullPath, { recursive: true });
1089
1745
  }
1090
1746
  /**
@@ -1094,9 +1750,9 @@ async function ensureBuildDir(cwd, buildDir) {
1094
1750
  * 导出供单测复用(OPT-04 4c)。
1095
1751
  */
1096
1752
  function detectPackageManager(cwd) {
1097
- if (existsSync(join$1(cwd, "pnpm-lock.yaml"))) return "pnpm";
1098
- if (existsSync(join$1(cwd, "yarn.lock"))) return "yarn";
1099
- if (existsSync(join$1(cwd, "bun.lockb")) || existsSync(join$1(cwd, "bun.lock"))) return "bun";
1753
+ if (existsSync(join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1754
+ if (existsSync(join(cwd, "yarn.lock"))) return "yarn";
1755
+ if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) return "bun";
1100
1756
  return "npm";
1101
1757
  }
1102
1758
  /**
@@ -1147,7 +1803,7 @@ function extractPackageName(modulePath) {
1147
1803
  */
1148
1804
  function getUbeanVersion(cwd) {
1149
1805
  try {
1150
- const pkgPath = join$1(cwd, "package.json");
1806
+ const pkgPath = join(cwd, "package.json");
1151
1807
  if (!existsSync(pkgPath)) return null;
1152
1808
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
1153
1809
  const spec = pkg.dependencies?.ubean || pkg.devDependencies?.ubean;
@@ -1199,7 +1855,7 @@ async function ensureBuiltinModules(cwd, config, autoInstall) {
1199
1855
  const pm = detectPackageManager(cwd);
1200
1856
  const ubeanVersion = getUbeanVersion(cwd);
1201
1857
  const packagesToInstall = missing.map((m) => ubeanVersion ? `${m.packageName}@^${ubeanVersion}` : m.packageName);
1202
- if (!autoInstall || !existsSync(join$1(cwd, "package.json"))) {
1858
+ if (!autoInstall || !existsSync(join(cwd, "package.json"))) {
1203
1859
  for (const m of missing) {
1204
1860
  const versionSuffix = ubeanVersion ? `@^${ubeanVersion}` : "";
1205
1861
  logger$2.warn(`Built-in module "${m.key}" is enabled but \`${m.packageName}\` is not installed. Run: ${pm} add ${m.packageName}${versionSuffix}`);
@@ -1242,7 +1898,7 @@ const prepareCommand = {
1242
1898
  }
1243
1899
  },
1244
1900
  async run({ args }) {
1245
- const cwd = resolve$1(args.cwd || process.cwd());
1901
+ const cwd = resolve(args.cwd || process.cwd());
1246
1902
  logger$2.info(`Preparing ubean project at ${cwd}...`);
1247
1903
  const config = await loadUbeanConfig(cwd);
1248
1904
  await ensureBuiltinModules(cwd, config, args.install !== false);
@@ -1314,17 +1970,17 @@ function startStaticServer(opts) {
1314
1970
  res.end("Bad Request");
1315
1971
  return;
1316
1972
  }
1317
- let filePath = join$1(root, pathname);
1973
+ let filePath = join(root, pathname);
1318
1974
  let fileExists = existsSync(filePath) && (await stat(filePath)).isFile();
1319
1975
  if (!fileExists && existsSync(filePath) && (await stat(filePath)).isDirectory()) {
1320
- const indexPath = join$1(filePath, "index.html");
1976
+ const indexPath = join(filePath, "index.html");
1321
1977
  if (existsSync(indexPath)) {
1322
1978
  filePath = indexPath;
1323
1979
  fileExists = true;
1324
1980
  }
1325
1981
  }
1326
1982
  if (!fileExists && mode === "ssg") {
1327
- const indexPath = join$1(filePath, "index.html");
1983
+ const indexPath = join(filePath, "index.html");
1328
1984
  if (existsSync(indexPath) && (await stat(indexPath)).isFile()) {
1329
1985
  filePath = indexPath;
1330
1986
  fileExists = true;
@@ -1338,7 +1994,7 @@ function startStaticServer(opts) {
1338
1994
  }
1339
1995
  }
1340
1996
  if (!fileExists && spaFallback) {
1341
- const fallbackPath = join$1(root, "index.html");
1997
+ const fallbackPath = join(root, "index.html");
1342
1998
  if (existsSync(fallbackPath)) {
1343
1999
  filePath = fallbackPath;
1344
2000
  fileExists = true;
@@ -1391,7 +2047,7 @@ const previewCommand = {
1391
2047
  }
1392
2048
  },
1393
2049
  async run({ args }) {
1394
- const cwd = resolve$1(args.cwd || process.cwd());
2050
+ const cwd = resolve(args.cwd || process.cwd());
1395
2051
  logger$1.info("Starting ubean preview server...");
1396
2052
  registerBuiltinPresets();
1397
2053
  const config = await loadUbeanConfig(cwd);
@@ -1420,7 +2076,7 @@ const previewCommand = {
1420
2076
  logger$1.info(`${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")}`);
1421
2077
  };
1422
2078
  if (mode === "spa" || mode === "ssg") {
1423
- const staticRoot = join$1(cwd, outputDir, "public");
2079
+ const staticRoot = join(cwd, outputDir, "public");
1424
2080
  if (!existsSync(staticRoot)) {
1425
2081
  logger$1.error(`Build output not found: ${staticRoot}`);
1426
2082
  logger$1.info("Run `ubean build` first to create a production build.");
@@ -1455,7 +2111,7 @@ const previewCommand = {
1455
2111
  serverFile = "handler.mjs";
1456
2112
  logger$1.warn(`Preset "${presetName}" uses a fetch handler entry. Preview may not work as a standalone server.`);
1457
2113
  }
1458
- const serverPath = join$1(cwd, outputDir, "server", serverFile);
2114
+ const serverPath = join(cwd, outputDir, "server", serverFile);
1459
2115
  if (!existsSync(serverPath)) {
1460
2116
  logger$1.error(`Build output not found: ${serverPath}`);
1461
2117
  logger$1.info("Run `ubean build` first to create a production build.");
@@ -1727,7 +2383,24 @@ const main = defineCommand({
1727
2383
  layout: layoutCommand,
1728
2384
  middleware: middlewareCommand,
1729
2385
  cron: cronCommand,
1730
- plugin: pluginCommand
2386
+ plugin: pluginCommand,
2387
+ scaffold: {
2388
+ meta: {
2389
+ name: "scaffold",
2390
+ description: "Machine-readable scaffold catalog (studio / IDE plugins)"
2391
+ },
2392
+ subCommands: { describe: {
2393
+ meta: {
2394
+ name: "describe",
2395
+ description: "Print the scaffold JSON manifest (RM-S01)"
2396
+ },
2397
+ async run() {
2398
+ const { getScaffoldManifest } = await import("./scaffold-manifest-TrH6OCX8.js").then((n) => n.r);
2399
+ process.stdout.write(`${JSON.stringify(getScaffoldManifest(), null, 2)}\n`);
2400
+ }
2401
+ } }
2402
+ },
2403
+ analyze: analyzeCommand
1731
2404
  }
1732
2405
  });
1733
2406
  runMain(main);