@ubean/cli 0.5.2 → 0.6.0-beta.2
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 +313 -1013
- package/dist/index.d.ts +24 -3
- package/dist/index.js +1 -1
- package/dist/{page-Um0fHKa9.js → page-C2Bde9EG.js} +73 -11
- package/package.json +18 -17
package/dist/cli.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync
|
|
1
|
+
import { A as readViteManifest, D as compareBundleBaseline, E as createFsOps, M as writeBundleBaseline, O as findClientManifest, S as renderTemplate, a as scaffold, c as scaffoldStarterTemplate, i as recoverScaffold, j as summarizeBundle, k as formatKilobytes, l as scaffoldUnifyTemplate, n as listScaffoldableFiles, o as scaffoldBlogTemplate, r as pageCommand, s as scaffoldMinimalTemplate, t as deleteScaffold } from "./page-C2Bde9EG.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, join, relative, resolve } from "pathe";
|
|
4
|
-
import { extname, normalize, sep } from "node:path";
|
|
5
4
|
import { getLogger, setDebugLogging, setMinLevel } from "@ubean/shared/logger";
|
|
6
5
|
import { defineCommand, runMain } from "citty";
|
|
7
6
|
import { BUILTIN_MODULES, isBuiltinDisabled, loadUbeanConfig, resolveModules, resolvePrerenderConfig, resolveSsrConfig } from "@ubean/config";
|
|
8
|
-
import {
|
|
9
|
-
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { readFile, rm } from "node:fs/promises";
|
|
10
8
|
import { generateOpenApiTypesFromServer, generateTypes } from "@ubean/build/codegen";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { createStaticSsgRenderer } from "@ubean/build/static-render";
|
|
14
|
-
import { NODE_REQUIREMENTS, createCapabilitySet, diagnoseCapabilities, registerBuiltinPresets, resolvePresetByName } from "@ubean/preset";
|
|
9
|
+
import { buildWithEnvironments } from "@ubean/build/production";
|
|
10
|
+
import { NODE_REQUIREMENTS, diagnoseCapabilities, registerBuiltinPresets, resolvePresetByName } from "@ubean/preset";
|
|
15
11
|
import { scanProject } from "@ubean/scan";
|
|
16
|
-
import { applyServerConfig, createUbeanApp } from "@ubean/app";
|
|
17
12
|
import { bold, cyan, dim, green } from "kolorist";
|
|
18
13
|
import { createServer } from "node:http";
|
|
19
|
-
import {
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
import { VUE_PLUGIN_INCLUDE, ubeanVite } from "@ubean/build/vue";
|
|
23
|
-
import { getVueLocaleParam, toVueRouterLocalePath } from "@ubean/i18n";
|
|
14
|
+
import { DEVTOOLS_PASS_THROUGH_PREFIXES, createCloudflarePreviewRunner, createVirtualRegistry, getDevApp, onDevScan, peekDevScanCoordinator, previewMimeType, resolvePreviewFile, ubeanDevRequestPlugin, ubeanPlugin, ubeanPreviewPlugin } from "@ubean/build/vite";
|
|
15
|
+
import { createServer as createServer$1, preview } from "vite";
|
|
16
|
+
import { ubeanVite } from "@ubean/build/vue";
|
|
24
17
|
import { ubeanIslandsPlugin } from "@ubean/islands/vite";
|
|
25
|
-
import { findAvailablePort, findUserViteConfig, getLanAddresses, isLoopbackHost, isWildcardHost
|
|
26
|
-
import { execSync
|
|
18
|
+
import { findAvailablePort, findUserViteConfig, getLanAddresses, isLoopbackHost, isWildcardHost } from "@ubean/shared/node";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
27
20
|
//#region src/config.ts
|
|
28
21
|
const logger$11 = getLogger("cli");
|
|
29
22
|
const CONFIG_TEMPLATE = `import { defineConfig } from 'ubean';
|
|
@@ -174,6 +167,14 @@ const logger$10 = getLogger("cli");
|
|
|
174
167
|
function readStringArg(value) {
|
|
175
168
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
176
169
|
}
|
|
170
|
+
/** Absolute ceilings are given in kB on the CLI; the budget API works in bytes. */
|
|
171
|
+
function readKilobyteArg(value, flag) {
|
|
172
|
+
const raw = readStringArg(value);
|
|
173
|
+
if (raw === void 0) return void 0;
|
|
174
|
+
const kb = Number(raw);
|
|
175
|
+
if (!Number.isFinite(kb) || kb < 0) throw new Error(`[ubean] analyze: ${flag} must be a non-negative number of kB`);
|
|
176
|
+
return Math.round(kb * 1024);
|
|
177
|
+
}
|
|
177
178
|
const analyzeCommand = {
|
|
178
179
|
meta: {
|
|
179
180
|
name: "analyze",
|
|
@@ -200,6 +201,18 @@ const analyzeCommand = {
|
|
|
200
201
|
maxIncrease: {
|
|
201
202
|
type: "string",
|
|
202
203
|
description: "Allowed relative gzip growth when using --check (default 0.05)"
|
|
204
|
+
},
|
|
205
|
+
maxTotalKb: {
|
|
206
|
+
type: "string",
|
|
207
|
+
description: "Absolute ceiling for total client JS gzip in kB (checked against this build alone)"
|
|
208
|
+
},
|
|
209
|
+
maxEntryKb: {
|
|
210
|
+
type: "string",
|
|
211
|
+
description: "Absolute ceiling for entry chunk gzip in kB"
|
|
212
|
+
},
|
|
213
|
+
maxChunkKb: {
|
|
214
|
+
type: "string",
|
|
215
|
+
description: "Absolute ceiling for any single chunk gzip in kB"
|
|
203
216
|
}
|
|
204
217
|
},
|
|
205
218
|
async run({ args }) {
|
|
@@ -214,7 +227,7 @@ const analyzeCommand = {
|
|
|
214
227
|
const manifest = readViteManifest(found.manifestPath);
|
|
215
228
|
const baseline = summarizeBundle(found.outDir, manifest);
|
|
216
229
|
baseline.outDir = relative(cwd, found.outDir).replace(/\\/g, "/") || baseline.outDir;
|
|
217
|
-
logger$10.info(`client JS: ${(baseline.totalGzip /
|
|
230
|
+
logger$10.info(`client JS: ${formatKilobytes(baseline.totalGzip)} gzip / ${formatKilobytes(baseline.totalBrotli ?? 0)} brotli (${baseline.entries.length} js chunks, entry ${formatKilobytes(baseline.entryGzip)} gzip)`);
|
|
218
231
|
for (const entry of baseline.entries.slice(0, 12)) logger$10.info(` ${entry.isEntry ? "[entry] " : ""}${entry.file} ${(entry.gzip / 1024).toFixed(1)} kB gzip`);
|
|
219
232
|
if (args.write !== false) {
|
|
220
233
|
const outArg = readStringArg(args.out);
|
|
@@ -223,23 +236,40 @@ const analyzeCommand = {
|
|
|
223
236
|
logger$10.info(`wrote ${out}`);
|
|
224
237
|
}
|
|
225
238
|
const checkArg = readStringArg(args.check);
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
239
|
+
const absoluteBudget = {
|
|
240
|
+
maxTotalGzip: readKilobyteArg(args.maxTotalKb, "--max-total-kb"),
|
|
241
|
+
maxEntryGzip: readKilobyteArg(args.maxEntryKb, "--max-entry-kb"),
|
|
242
|
+
maxChunkGzip: readKilobyteArg(args.maxChunkKb, "--max-chunk-kb")
|
|
243
|
+
};
|
|
244
|
+
const hasAbsoluteBudget = Object.values(absoluteBudget).some((value) => value !== void 0);
|
|
245
|
+
if (checkArg || hasAbsoluteBudget) {
|
|
246
|
+
let committed;
|
|
247
|
+
if (checkArg) {
|
|
248
|
+
const checkPath = resolve(cwd, checkArg);
|
|
249
|
+
if (!existsSync(checkPath)) {
|
|
250
|
+
logger$10.error(`baseline not found: ${checkPath}`);
|
|
251
|
+
throw new Error("missing budget baseline");
|
|
252
|
+
}
|
|
253
|
+
const parsed = JSON.parse(readFileSync(checkPath, "utf8"));
|
|
254
|
+
committed = {
|
|
255
|
+
totalGzip: parsed.totalGzip ?? 0,
|
|
256
|
+
entryGzip: parsed.entryGzip ?? 0
|
|
257
|
+
};
|
|
258
|
+
} else committed = {
|
|
259
|
+
totalGzip: baseline.totalGzip,
|
|
260
|
+
entryGzip: baseline.entryGzip
|
|
261
|
+
};
|
|
233
262
|
const maxIncrease = Number(args.maxIncrease ?? .05);
|
|
234
|
-
const result = compareBundleBaseline(baseline, {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
263
|
+
const result = compareBundleBaseline(baseline, committed, {
|
|
264
|
+
maxIncrease: Number.isFinite(maxIncrease) ? maxIncrease : .05,
|
|
265
|
+
...absoluteBudget
|
|
266
|
+
});
|
|
238
267
|
if (!result.ok) {
|
|
239
268
|
for (const message of result.messages) logger$10.error(message);
|
|
240
|
-
throw new Error("client JS budget exceeded");
|
|
269
|
+
throw new Error(result.violations.some((v) => v.kind === "missing") ? "client JS budget check failed" : "client JS budget exceeded");
|
|
241
270
|
}
|
|
242
|
-
|
|
271
|
+
const relativeSummary = checkArg ? `total ${(result.totalRatio * 100).toFixed(1)}%, entry ${(result.entryRatio * 100).toFixed(1)}% vs baseline, max ${(result.maxIncrease * 100).toFixed(0)}%` : "no baseline comparison";
|
|
272
|
+
logger$10.info(`budget ok (${relativeSummary})`);
|
|
243
273
|
}
|
|
244
274
|
}
|
|
245
275
|
};
|
|
@@ -275,33 +305,9 @@ async function loadContentForBuild(cwd, content) {
|
|
|
275
305
|
* If the SSR entry cannot be loaded (e.g. missing dependencies), returns
|
|
276
306
|
* `undefined` so the caller falls back to the existing placeholder behavior.
|
|
277
307
|
*/
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const mod = await import(pathToFileURL(entryPath).href);
|
|
282
|
-
const createFetchHandler = mod.default ?? mod.createFetchHandler;
|
|
283
|
-
if (typeof createFetchHandler !== "function") {
|
|
284
|
-
logger$9.warn("SSR entry does not export createFetchHandler; falling back to placeholder prerender.");
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const fetch = await createFetchHandler();
|
|
288
|
-
if (typeof fetch !== "function") {
|
|
289
|
-
logger$9.warn("SSR entry did not return a fetch handler; falling back to placeholder prerender.");
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
return async (url) => {
|
|
293
|
-
const req = new Request(`http://localhost${url || "/"}`, { headers: { "x-ubean-prerender": "1" } });
|
|
294
|
-
const res = await fetch(req);
|
|
295
|
-
return {
|
|
296
|
-
html: typeof res.text === "function" ? await res.text() : String(res.body ?? ""),
|
|
297
|
-
statusCode: res.status ?? 200
|
|
298
|
-
};
|
|
299
|
-
};
|
|
300
|
-
} catch (err) {
|
|
301
|
-
logger$9.warn(`Failed to load SSR entry for prerender: ${err instanceof Error ? err.message : String(err)}. Falling back to placeholder prerender.`);
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
308
|
+
/**
|
|
309
|
+
* RM-V21:`createSsrFetcher` 已随预渲染逻辑下沉到 `@ubean/build` 的 `prerender-step.ts`。
|
|
310
|
+
*/
|
|
305
311
|
const buildCommand = {
|
|
306
312
|
meta: {
|
|
307
313
|
name: "build",
|
|
@@ -325,6 +331,10 @@ const buildCommand = {
|
|
|
325
331
|
description: "Shortcut for --mode ssg",
|
|
326
332
|
default: false
|
|
327
333
|
},
|
|
334
|
+
outDir: {
|
|
335
|
+
type: "string",
|
|
336
|
+
description: "Output directory (overrides ubean.config.ts build.outputDir)"
|
|
337
|
+
},
|
|
328
338
|
minify: {
|
|
329
339
|
type: "boolean",
|
|
330
340
|
description: "Minify output",
|
|
@@ -390,8 +400,12 @@ const buildCommand = {
|
|
|
390
400
|
config
|
|
391
401
|
});
|
|
392
402
|
logger$9.info("Building with Vite...");
|
|
393
|
-
const
|
|
394
|
-
|
|
403
|
+
const nodeEnv = process.env.NODE_ENV;
|
|
404
|
+
if (nodeEnv && nodeEnv !== "production") logger$9.warn(`NODE_ENV=${nodeEnv}:客户端产物会包含开发态代码(如 Vue dev runtime 与开发警告)。生产构建请设置 NODE_ENV=production,或不要设置该变量。`);
|
|
405
|
+
if (args.outDir) config.build.outputDir = String(args.outDir);
|
|
406
|
+
process.env.UBEAN_BUILD_PRESET = resolvedPreset.name;
|
|
407
|
+
const { snapshot: contentSnapshot } = await loadContentForBuild(cwd, config.content);
|
|
408
|
+
const manifest = await buildWithEnvironments({
|
|
395
409
|
cwd,
|
|
396
410
|
config,
|
|
397
411
|
preset: resolvedPreset,
|
|
@@ -400,64 +414,6 @@ const buildCommand = {
|
|
|
400
414
|
sourcemap: args.sourcemap,
|
|
401
415
|
contentSnapshot
|
|
402
416
|
});
|
|
403
|
-
const hasPrerenderRules = Object.values(config.routeRules || {}).some((r) => r?.prerender === true);
|
|
404
|
-
if (config.prerender.enabled || hasPrerenderRules) {
|
|
405
|
-
logger$9.info("Prerendering static pages...");
|
|
406
|
-
let fetcher;
|
|
407
|
-
let notFoundRoute = false;
|
|
408
|
-
let expandRoutes;
|
|
409
|
-
if (config.mode === "ssg") {
|
|
410
|
-
const staticRenderer = await createStaticSsgRenderer(cwd, manifest, {
|
|
411
|
-
pages: result.pages,
|
|
412
|
-
notFoundPage: result.notFoundPage ?? null,
|
|
413
|
-
i18n: config.i18n
|
|
414
|
-
});
|
|
415
|
-
if (staticRenderer) {
|
|
416
|
-
fetcher = staticRenderer.fetcher;
|
|
417
|
-
expandRoutes = staticRenderer.expandRoutes;
|
|
418
|
-
notFoundRoute = Boolean(result.notFoundPage);
|
|
419
|
-
logger$9.info("Using static SSG renderer (direct render, no HTTP pipeline)");
|
|
420
|
-
}
|
|
421
|
-
} else fetcher = await createSsrFetcher(cwd, manifest);
|
|
422
|
-
await prerender({
|
|
423
|
-
cwd,
|
|
424
|
-
outputDir: config.build.outputDir,
|
|
425
|
-
pages: result.pages,
|
|
426
|
-
prerender: config.prerender,
|
|
427
|
-
routeRules: config.routeRules,
|
|
428
|
-
contentRoutes,
|
|
429
|
-
fetcher,
|
|
430
|
-
notFoundRoute,
|
|
431
|
-
expandRoutes
|
|
432
|
-
});
|
|
433
|
-
if (contentSnapshot) try {
|
|
434
|
-
const contentMod = await import("@ubean/content");
|
|
435
|
-
const searchConfig = contentMod.resolveContentSearchConfig(config.content, { ssg: config.mode === "ssg" });
|
|
436
|
-
const staticDir = resolve(cwd, resolvePrerenderConfig(config.prerender).staticDir);
|
|
437
|
-
if (searchConfig.sections) {
|
|
438
|
-
const payload = contentMod.generateSearchSectionsSnapshot(contentSnapshot);
|
|
439
|
-
await mkdir(staticDir, { recursive: true });
|
|
440
|
-
await writeFile(join(staticDir, "__search.json"), JSON.stringify(payload), "utf-8");
|
|
441
|
-
const sectionCount = Object.values(payload).reduce((sum, sections) => sum + sections.length, 0);
|
|
442
|
-
logger$9.info(`Search: wrote __search.json (${sectionCount} sections)`);
|
|
443
|
-
}
|
|
444
|
-
if (searchConfig.pagefind) {
|
|
445
|
-
const res = await contentMod.runPagefindIndex({
|
|
446
|
-
siteDir: staticDir,
|
|
447
|
-
createIndexOptions: searchConfig.pagefindOptions,
|
|
448
|
-
logger: {
|
|
449
|
-
info: (msg) => logger$9.info(msg),
|
|
450
|
-
warn: (msg) => logger$9.warn(msg)
|
|
451
|
-
}
|
|
452
|
-
});
|
|
453
|
-
if (res.indexed) logger$9.info("Search: pagefind index generated");
|
|
454
|
-
else if (res.reason === "pagefind-not-installed") logger$9.info("Search: pagefind is not installed — skipping index. Run `pnpm add -D pagefind` to enable.");
|
|
455
|
-
else logger$9.warn(`Search: pagefind indexing failed — ${res.error ?? res.reason}`);
|
|
456
|
-
}
|
|
457
|
-
} catch (err) {
|
|
458
|
-
logger$9.warn(`Search index generation skipped: ${err instanceof Error ? err.message : String(err)}`);
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
417
|
if (config.mode === "ssg" && !process.env.UBEAN_KEEP_SSR) {
|
|
462
418
|
const serverDir = join(cwd, config.build.outputDir, "server");
|
|
463
419
|
if (existsSync(serverDir)) {
|
|
@@ -487,316 +443,102 @@ const buildCommand = {
|
|
|
487
443
|
}
|
|
488
444
|
};
|
|
489
445
|
//#endregion
|
|
490
|
-
//#region src/dev-server/vite
|
|
446
|
+
//#region src/dev-server/dev-vite.ts
|
|
447
|
+
/**
|
|
448
|
+
* dev 的 Vite 装配(RM-V14:取代 `vite-server.ts` + `runner.ts`)。
|
|
449
|
+
*
|
|
450
|
+
* CLI 不再自建 app、不再持有 HTTP server、也不再管热重载:
|
|
451
|
+
* - **请求路由**与**宿主 app** 由 `@ubean/build` 的 `ubeanDevRequestPlugin` 负责(自举 +
|
|
452
|
+
* 随扫描重建);
|
|
453
|
+
* - **文件监听与重载顺序**由 `dev-scan` 协调器负责;
|
|
454
|
+
* - CLI 只装配插件、起 Vite server、打印 banner,并订阅扫描做命令级事务(类型生成、
|
|
455
|
+
* DevTools 数据上报)。
|
|
456
|
+
*
|
|
457
|
+
* 保留两处 CLI 专有接线(无法下沉):`@ubean/devtools` / `@vitejs/devtools` 的按需加载
|
|
458
|
+
* (可选依赖 + `config.devtools.enabled` 闸门),以及 DevTools 的 scaffold CRUD
|
|
459
|
+
* (页面增删改是 CLI 的能力)。
|
|
460
|
+
*/
|
|
491
461
|
const logger$8 = getLogger("dev-server");
|
|
492
462
|
/**
|
|
493
|
-
* 收编 Vite
|
|
494
|
-
*
|
|
495
|
-
* - warn/error 始终透传(不可静默)
|
|
463
|
+
* 收编 Vite 原生日志到 ubean 分类闸门:`lifecycle` 关闭时(默认)info 降为 debug,
|
|
464
|
+
* warn/error 原样保留(这两级永远可见)。
|
|
496
465
|
*/
|
|
497
466
|
function createGatedViteLogger(logging) {
|
|
498
467
|
return {
|
|
499
|
-
info(msg)
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
},
|
|
505
|
-
warnOnce(msg) {
|
|
506
|
-
logger$8.warn(msg);
|
|
507
|
-
},
|
|
508
|
-
error(msg) {
|
|
509
|
-
logger$8.error(msg);
|
|
510
|
-
},
|
|
511
|
-
clearScreen() {},
|
|
468
|
+
info: (msg) => logger$8.debug(msg),
|
|
469
|
+
warn: (msg) => logger$8.warn(msg),
|
|
470
|
+
warnOnce: (msg) => logger$8.warn(msg),
|
|
471
|
+
error: (msg, options) => logger$8.error(options?.error ? `${msg} ${options.error}` : msg),
|
|
472
|
+
clearScreen: () => {},
|
|
512
473
|
hasWarned: false,
|
|
513
|
-
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
474
|
+
...logging.lifecycle ? { info: (msg) => logger$8.info(msg) } : {}
|
|
516
475
|
};
|
|
517
476
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
* Returns `''` when i18n is disabled or no prefix is needed.
|
|
522
|
-
*/
|
|
523
|
-
function resolveLocaleVueParam(config) {
|
|
524
|
-
if (!config?.enabled || !config.locales?.length) return "";
|
|
525
|
-
return getVueLocaleParam({
|
|
526
|
-
defaultLocale: config.defaultLocale,
|
|
527
|
-
locales: config.locales.map((l) => l.code),
|
|
528
|
-
strategy: config.strategy
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
async function toWebRequest(req, host, protocol) {
|
|
532
|
-
const url = `${protocol}://${req.headers.host || host}${req.url || "/"}`;
|
|
533
|
-
const headers = new Headers();
|
|
534
|
-
for (const [key, value] of Object.entries(req.headers)) if (value) {
|
|
535
|
-
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
536
|
-
else headers.set(key, value);
|
|
537
|
-
}
|
|
538
|
-
const method = req.method || "GET";
|
|
539
|
-
return new Request(url, {
|
|
540
|
-
method,
|
|
541
|
-
headers,
|
|
542
|
-
body: method === "GET" || method === "HEAD" ? void 0 : req,
|
|
543
|
-
duplex: "half"
|
|
544
|
-
});
|
|
545
|
-
}
|
|
546
|
-
async function sendWebResponse(res, webRes) {
|
|
547
|
-
res.statusCode = webRes.status;
|
|
548
|
-
res.statusMessage = webRes.statusText;
|
|
549
|
-
webRes.headers.forEach((value, key) => {
|
|
550
|
-
res.setHeader(key, value);
|
|
551
|
-
});
|
|
552
|
-
if (webRes.body) {
|
|
553
|
-
const reader = webRes.body.getReader();
|
|
554
|
-
try {
|
|
555
|
-
while (true) {
|
|
556
|
-
const { done, value } = await reader.read();
|
|
557
|
-
if (done) break;
|
|
558
|
-
res.write(value);
|
|
559
|
-
}
|
|
560
|
-
} finally {
|
|
561
|
-
reader.releaseLock();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
res.end();
|
|
565
|
-
}
|
|
566
|
-
function loadScaffoldOps() {
|
|
567
|
-
return {
|
|
568
|
-
createFsOps,
|
|
569
|
-
scaffold,
|
|
570
|
-
deleteScaffold,
|
|
571
|
-
recoverScaffold
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
/** Vite's browser-facing prefix for virtual module ids (see `wrapId`). */
|
|
575
|
-
const VALID_ID_PREFIX = "/@id/";
|
|
576
|
-
/** Vite's URL-safe encoding of the `\0` virtual-module marker. */
|
|
577
|
-
const NULL_BYTE_PLACEHOLDER = "__x00__";
|
|
578
|
-
/**
|
|
579
|
-
* Encode a module-graph URL into a browser-servable path.
|
|
580
|
-
*
|
|
581
|
-
* Virtual module ids carry a raw `\0` marker in the module graph (e.g.
|
|
582
|
-
* UnoCSS's `\0/__uno.css`). A literal NUL is invalid inside HTML — parse5
|
|
583
|
-
* rejects it with `unexpected-null-character`, which surfaces as a warning
|
|
584
|
-
* in `transformIndexHtml`. Mirror Vite's own import-analysis rewriting
|
|
585
|
-
* (`wrapId`: `/@id/` prefix + `\0` → `__x00__`) so the href stays valid
|
|
586
|
-
* and resolvable by the dev server.
|
|
587
|
-
*/
|
|
588
|
-
function toDevCssHref(url) {
|
|
589
|
-
return url.includes("\0") ? `${VALID_ID_PREFIX}${url.replace(/\0/g, NULL_BYTE_PLACEHOLDER)}` : url;
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Collect render-blocking CSS URLs from the Vite dev module graph.
|
|
593
|
-
*
|
|
594
|
-
* In dev, CSS imported from JS (virtual uno.css entry, global.css, font css)
|
|
595
|
-
* is only applied once the client entry module graph has fully loaded and
|
|
596
|
-
* executed — SSR HTML paints unstyled for that gap (FOUC). Injecting the same
|
|
597
|
-
* CSS modules as blocking `<link>` tags lets the browser fetch them in
|
|
598
|
-
* parallel with the JS graph, so the first paint is styled.
|
|
599
|
-
*
|
|
600
|
-
* `?direct` tells Vite's transform middleware to return raw CSS
|
|
601
|
-
* (`Content-Type: text/css`) instead of the JS style-injection wrapper.
|
|
602
|
-
* Modules whose URL already carries a query (SFC `<style>` blocks such as
|
|
603
|
-
* `*.vue?vue&type=style&...`) cannot be served this way and are skipped.
|
|
604
|
-
*/
|
|
605
|
-
function collectDevCssLinks(moduleGraph) {
|
|
606
|
-
const links = /* @__PURE__ */ new Set();
|
|
607
|
-
for (const mod of moduleGraph.idToModuleMap.values()) {
|
|
608
|
-
const { url, id } = mod;
|
|
609
|
-
if (!url || url.includes("?")) continue;
|
|
610
|
-
if (id?.endsWith(".css") || url.endsWith(".css")) links.add(`${toDevCssHref(url)}?direct`);
|
|
611
|
-
}
|
|
612
|
-
return [...links];
|
|
613
|
-
}
|
|
614
|
-
/** Inject blocking stylesheet links right before `</head>`. */
|
|
615
|
-
function injectStylesheetLinks(html, hrefs) {
|
|
616
|
-
if (!hrefs.length || !html.includes("</head>")) return html;
|
|
617
|
-
const tags = hrefs.map((href) => `<link rel="stylesheet" href="${href}">`).join("");
|
|
618
|
-
return html.replace("</head>", `${tags}</head>`);
|
|
619
|
-
}
|
|
620
|
-
async function createViteDevServer(options) {
|
|
621
|
-
const { cwd, config, app: initialApp, layouts: initialLayouts = [] } = options;
|
|
622
|
-
const host = options.host || "localhost";
|
|
623
|
-
const strictPort = options.strictPort ?? false;
|
|
624
|
-
let currentApp = initialApp;
|
|
625
|
-
let currentLayouts = initialLayouts;
|
|
626
|
-
let httpServer = null;
|
|
627
|
-
let viteServer = null;
|
|
628
|
-
let serverConfigApplied = false;
|
|
629
|
-
let serverReadyCalled = false;
|
|
630
|
-
let cachedServerConfig = null;
|
|
631
|
-
let refreshDevtools = null;
|
|
632
|
-
const requestedPort = options.port;
|
|
633
|
-
let actualPort;
|
|
634
|
-
try {
|
|
635
|
-
actualPort = await findAvailablePort(requestedPort, {
|
|
636
|
-
host,
|
|
637
|
-
strictPort
|
|
638
|
-
});
|
|
639
|
-
} catch (err) {
|
|
640
|
-
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 });
|
|
641
|
-
throw err;
|
|
642
|
-
}
|
|
643
|
-
if (actualPort !== requestedPort) logger$8.warn(`Port ${requestedPort} is in use, trying ${actualPort} instead.`);
|
|
644
|
-
httpServer = createServer(async (req, res) => {
|
|
645
|
-
try {
|
|
646
|
-
const pathname = (req.url || "/").split("?")[0].split("#")[0];
|
|
647
|
-
if (devtoolsEnabled && (pathname === "/_devtools" || pathname === "/_devtools/")) {
|
|
648
|
-
res.statusCode = 302;
|
|
649
|
-
res.setHeader("Location", "/__devtools/");
|
|
650
|
-
res.end();
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (!viteServer) {
|
|
654
|
-
res.statusCode = 503;
|
|
655
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
656
|
-
res.end("ubean dev server is shutting down");
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
|
-
await new Promise((resolve, reject) => {
|
|
660
|
-
viteServer.middlewares(req, res, (err) => {
|
|
661
|
-
if (err) reject(err);
|
|
662
|
-
else resolve();
|
|
663
|
-
});
|
|
664
|
-
});
|
|
665
|
-
if (res.writableEnded) return;
|
|
666
|
-
try {
|
|
667
|
-
await viteServer.ssrLoadModule("ubean:locales");
|
|
668
|
-
} catch (err) {
|
|
669
|
-
logger$8.warn("[ubean] Failed to resolve locales module:", err);
|
|
670
|
-
}
|
|
671
|
-
if (!serverConfigApplied) {
|
|
672
|
-
serverConfigApplied = true;
|
|
673
|
-
try {
|
|
674
|
-
const serverMod = await viteServer.ssrLoadModule("virtual:ubean-server");
|
|
675
|
-
if (serverMod?.resolveServerConfig) {
|
|
676
|
-
cachedServerConfig = serverMod.resolveServerConfig("dev");
|
|
677
|
-
await applyServerConfig(currentApp, cachedServerConfig);
|
|
678
|
-
}
|
|
679
|
-
} catch (err) {
|
|
680
|
-
logger$8.warn("[ubean] Failed to load server config:", err);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
await currentApp.init();
|
|
684
|
-
if (!serverReadyCalled) {
|
|
685
|
-
serverReadyCalled = true;
|
|
686
|
-
if (cachedServerConfig?.onServerReady) try {
|
|
687
|
-
await cachedServerConfig.onServerReady(currentApp);
|
|
688
|
-
} catch (err) {
|
|
689
|
-
logger$8.warn("[ubean] onServerReady error:", err);
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
const protocol = req.socket?.encrypted ? "https" : "http";
|
|
693
|
-
const webReq = await toWebRequest(req, host, protocol);
|
|
694
|
-
const webRes = await currentApp.fetch(webReq);
|
|
695
|
-
const contentType = webRes.headers.get("content-type") || "";
|
|
696
|
-
const skipTransform = (req.url || "").startsWith("/_devtools");
|
|
697
|
-
if (contentType.includes("text/html") && webRes.body && !skipTransform) {
|
|
698
|
-
const htmlWithCss = injectStylesheetLinks(await webRes.text(), collectDevCssLinks(viteServer.moduleGraph));
|
|
699
|
-
const transformedHtml = await viteServer.transformIndexHtml(req.url || "/", htmlWithCss);
|
|
700
|
-
res.statusCode = webRes.status;
|
|
701
|
-
res.statusMessage = webRes.statusText;
|
|
702
|
-
webRes.headers.forEach((value, key) => {
|
|
703
|
-
res.setHeader(key, value);
|
|
704
|
-
});
|
|
705
|
-
res.end(transformedHtml);
|
|
706
|
-
} else await sendWebResponse(res, webRes);
|
|
707
|
-
} catch (err) {
|
|
708
|
-
if (viteServer) viteServer.ssrFixStacktrace(err);
|
|
709
|
-
if (!res.headersSent) {
|
|
710
|
-
res.statusCode = 500;
|
|
711
|
-
const errorHtml = `<!DOCTYPE html>
|
|
712
|
-
<html>
|
|
713
|
-
<head>
|
|
714
|
-
<title>Server Error</title>
|
|
715
|
-
<style>
|
|
716
|
-
body { font-family: monospace; padding: 2rem; background: #1a1a1a; color: #ff6b6b; }
|
|
717
|
-
pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
|
718
|
-
</style>
|
|
719
|
-
</head>
|
|
720
|
-
<body>
|
|
721
|
-
<h1>Internal Server Error</h1>
|
|
722
|
-
<pre>${err.stack || err.message}</pre>
|
|
723
|
-
</body>
|
|
724
|
-
</html>`;
|
|
725
|
-
res.setHeader("Content-Type", "text/html");
|
|
726
|
-
res.end(errorHtml);
|
|
727
|
-
} else if (!res.writableEnded) res.end(err instanceof Error ? err.message : "Internal Server Error");
|
|
728
|
-
}
|
|
729
|
-
});
|
|
730
|
-
const devtoolsEnabled = options.config.devtools.enabled;
|
|
731
|
-
let devtoolsPlugin = null;
|
|
732
|
-
if (devtoolsEnabled) try {
|
|
733
|
-
const { ubeanDevtoolsPlugin } = await import("@ubean/devtools");
|
|
734
|
-
const devtoolsOpts = options.devtools;
|
|
735
|
-
const scaffoldOps = await loadScaffoldOps();
|
|
736
|
-
devtoolsPlugin = ubeanDevtoolsPlugin({
|
|
737
|
-
getCwd: () => cwd,
|
|
738
|
-
getApp: () => currentApp,
|
|
739
|
-
...scaffoldOps ? { scaffoldOps } : {},
|
|
740
|
-
...devtoolsOpts?.getScanResult ? { getScanResult: devtoolsOpts.getScanResult } : {},
|
|
741
|
-
...devtoolsOpts?.getConfigMeta ? { getConfigMeta: devtoolsOpts.getConfigMeta } : {},
|
|
742
|
-
...devtoolsOpts?.getCustomTabs ? { getCustomTabs: devtoolsOpts.getCustomTabs } : {},
|
|
743
|
-
...devtoolsOpts?.ai ? { ai: devtoolsOpts.ai } : {},
|
|
744
|
-
...devtoolsOpts?.getApp ? { getApp: devtoolsOpts.getApp } : {},
|
|
745
|
-
...devtoolsOpts?.triggerRescan ? { triggerRescan: devtoolsOpts.triggerRescan } : {},
|
|
746
|
-
registerRefresh: (fn) => {
|
|
747
|
-
refreshDevtools = fn;
|
|
748
|
-
}
|
|
749
|
-
});
|
|
750
|
-
} catch {}
|
|
751
|
-
let viteDevtoolsPlugins = [];
|
|
752
|
-
if (devtoolsEnabled) try {
|
|
753
|
-
const { DevTools } = await import("@vitejs/devtools");
|
|
754
|
-
// @vitejs/devtools 也解析到含 @vitejs+devtools peer 上下文的 vite-plus-core
|
|
755
|
-
viteDevtoolsPlugins = await DevTools({ builtinDevTools: false });
|
|
756
|
-
} catch {}
|
|
477
|
+
async function createDevViteServer(options) {
|
|
478
|
+
const { cwd, config, host, strictPort = false } = options;
|
|
479
|
+
const devtoolsEnabled = config.devtools.enabled;
|
|
757
480
|
const userViteConfig = findUserViteConfig(cwd);
|
|
758
481
|
const hasUserViteConfig = !!userViteConfig;
|
|
759
482
|
const isBackendMode = config.mode === "backend";
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
483
|
+
let viteServer = null;
|
|
484
|
+
const rescanDev = () => peekDevScanCoordinator(viteServer)?.rescan();
|
|
485
|
+
const builtinPlugins = [...hasUserViteConfig ? [] : [ubeanDevRequestPlugin({
|
|
486
|
+
passThrough: devtoolsEnabled ? [...DEVTOOLS_PASS_THROUGH_PREFIXES] : [],
|
|
487
|
+
devtoolsRedirect: devtoolsEnabled
|
|
488
|
+
})], ...hasUserViteConfig ? [] : [
|
|
489
|
+
ubeanPlugin({
|
|
490
|
+
config,
|
|
491
|
+
registry: createVirtualRegistry()
|
|
492
|
+
}),
|
|
493
|
+
...isBackendMode ? [] : ubeanVite({
|
|
494
|
+
config,
|
|
495
|
+
registry: createVirtualRegistry()
|
|
496
|
+
}),
|
|
497
|
+
...isBackendMode ? [] : [ubeanIslandsPlugin()]
|
|
498
|
+
]];
|
|
499
|
+
if (devtoolsEnabled) {
|
|
500
|
+
process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = "true";
|
|
501
|
+
try {
|
|
502
|
+
const { ubeanDevtoolsPlugin } = await import("@ubean/devtools");
|
|
503
|
+
builtinPlugins.push(ubeanDevtoolsPlugin({
|
|
504
|
+
getScanResult: options.devtools?.getScanResult,
|
|
505
|
+
getConfigMeta: options.devtools?.getConfigMeta,
|
|
506
|
+
getCustomTabs: options.devtools?.getCustomTabs,
|
|
507
|
+
getCwd: () => cwd,
|
|
508
|
+
scaffoldOps: {
|
|
509
|
+
createFsOps,
|
|
510
|
+
scaffold,
|
|
511
|
+
deleteScaffold,
|
|
512
|
+
recoverScaffold
|
|
513
|
+
},
|
|
514
|
+
getApp: () => getDevApp(viteServer) ?? void 0,
|
|
515
|
+
triggerRescan: async () => {
|
|
516
|
+
await rescanDev();
|
|
517
|
+
}
|
|
518
|
+
}));
|
|
519
|
+
} catch {}
|
|
520
|
+
}
|
|
780
521
|
const { plugins } = await resolveModules({
|
|
781
522
|
cwd,
|
|
782
523
|
config,
|
|
783
524
|
builtinPlugins
|
|
784
525
|
});
|
|
785
|
-
if (devtoolsEnabled) process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = "true";
|
|
786
526
|
viteServer = await createServer$1({
|
|
787
527
|
root: cwd,
|
|
788
528
|
configFile: userViteConfig ?? false,
|
|
789
529
|
customLogger: createGatedViteLogger({ lifecycle: config.logging.lifecycle }),
|
|
790
530
|
resolve: { dedupe: ["vue", "vue-router"] },
|
|
791
531
|
server: {
|
|
792
|
-
|
|
793
|
-
|
|
532
|
+
host,
|
|
533
|
+
port: options.port,
|
|
534
|
+
strictPort
|
|
794
535
|
},
|
|
795
536
|
appType: "custom",
|
|
796
537
|
plugins,
|
|
797
538
|
devtools: {
|
|
798
539
|
enabled: devtoolsEnabled,
|
|
799
|
-
clientAuth: false
|
|
540
|
+
clientAuth: false,
|
|
541
|
+
builtinDevTools: false
|
|
800
542
|
},
|
|
801
543
|
optimizeDeps: { exclude: [
|
|
802
544
|
"ubean",
|
|
@@ -805,98 +547,29 @@ async function createViteDevServer(options) {
|
|
|
805
547
|
"virtual:ubean-app",
|
|
806
548
|
"virtual:ubean-server",
|
|
807
549
|
"virtual:ubean-client-entry",
|
|
808
|
-
"
|
|
809
|
-
"
|
|
810
|
-
"
|
|
811
|
-
"
|
|
550
|
+
"ubean:locales",
|
|
551
|
+
"ubean:pages",
|
|
552
|
+
"ubean:routes",
|
|
553
|
+
"ubean:meta",
|
|
554
|
+
"ubean:app-config"
|
|
812
555
|
] },
|
|
813
556
|
ssr: {
|
|
814
557
|
noExternal: ["ubean", /^@ubean\//],
|
|
815
558
|
external: ["@ubean/i18n"]
|
|
816
559
|
}
|
|
817
560
|
});
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
for (const page of app.options.pages || []) {
|
|
831
|
-
const fullPath = (page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
|
|
832
|
-
pageLoaders[page.relativePath] = () => viteServer.ssrLoadModule(fullPath);
|
|
833
|
-
}
|
|
834
|
-
app.options.pageLoaders = pageLoaders;
|
|
835
|
-
const middlewareLoaders = {};
|
|
836
|
-
for (const mw of app.options.middleware || []) {
|
|
837
|
-
const fullPath = mw.fullPath;
|
|
838
|
-
middlewareLoaders[mw.relativePath] = () => viteServer.ssrLoadModule(fullPath);
|
|
839
|
-
}
|
|
840
|
-
app.options.middlewareLoaders = middlewareLoaders;
|
|
841
|
-
const cronFiles = app.options.crons || [];
|
|
842
|
-
if (cronFiles.length > 0) Promise.all(cronFiles.map((c) => viteServer.ssrLoadModule(c.fullPath))).then(async () => {
|
|
843
|
-
(await import("@ubean/server/cron")).startCronScheduler();
|
|
844
|
-
}).catch((err) => {
|
|
845
|
-
console.error("[ubean] Failed to load cron files:", err);
|
|
846
|
-
});
|
|
847
|
-
const layoutMap = /* @__PURE__ */ new Map();
|
|
848
|
-
for (const layout of layouts) layoutMap.set(layout.name, layout.fullPath);
|
|
849
|
-
const defaultLayout = layouts.find((l) => l.isDefault)?.name || null;
|
|
850
|
-
if (config.mode === "backend") {
|
|
851
|
-
app.resetInit();
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
const scannedPages = app.options.pages || [];
|
|
855
|
-
const ssrLocaleParam = resolveLocaleVueParam(config.i18n);
|
|
856
|
-
const scannedPageByName = /* @__PURE__ */ new Map();
|
|
857
|
-
for (const p of scannedPages) scannedPageByName.set(p.name, p);
|
|
858
|
-
const routes = scannedPages.map((p) => {
|
|
859
|
-
const componentFullPath = (p.isReuse && p.reuseTarget ? scannedPageByName.get(p.reuseTarget) : void 0)?.fullPath || p.fullPath;
|
|
860
|
-
const routePath = p.route.replace(/\*\*:(\w[\w-]*)/g, ":$1(.*)*");
|
|
861
|
-
return {
|
|
862
|
-
path: ssrLocaleParam ? toVueRouterLocalePath(routePath, ssrLocaleParam) : routePath,
|
|
863
|
-
name: p.name,
|
|
864
|
-
component: async () => {
|
|
865
|
-
const mod = await viteServer.ssrLoadModule(componentFullPath);
|
|
866
|
-
return mod.default || mod;
|
|
867
|
-
},
|
|
868
|
-
meta: {
|
|
869
|
-
layout: p.layout === false ? false : p.layout || defaultLayout,
|
|
870
|
-
pageName: p.name,
|
|
871
|
-
cache: p.cache === true ? true : void 0
|
|
872
|
-
}
|
|
873
|
-
};
|
|
874
|
-
});
|
|
875
|
-
let appConfigModule = null;
|
|
876
|
-
const getAppConfigModule = () => {
|
|
877
|
-
if (!appConfigModule) appConfigModule = viteServer.ssrLoadModule("virtual:ubean-app");
|
|
878
|
-
return appConfigModule;
|
|
879
|
-
};
|
|
880
|
-
app.options.pageRenderer = createVueRenderer({
|
|
881
|
-
routes,
|
|
882
|
-
async resolveLayoutComponent(name) {
|
|
883
|
-
if (name === false || name == null) return null;
|
|
884
|
-
const fullPath = layoutMap.get(name);
|
|
885
|
-
if (!fullPath) return null;
|
|
886
|
-
const mod = await viteServer.ssrLoadModule(fullPath);
|
|
887
|
-
return mod.default || mod;
|
|
888
|
-
},
|
|
889
|
-
defaultLayout,
|
|
890
|
-
async resolveAppConfig() {
|
|
891
|
-
return (await getAppConfigModule()).resolveAppConfig("server");
|
|
892
|
-
}
|
|
893
|
-
});
|
|
894
|
-
app.resetInit();
|
|
895
|
-
}
|
|
896
|
-
enhanceAppWithVite(currentApp, currentLayouts);
|
|
897
|
-
const displayHost = isWildcardHost(host) ? "localhost" : host;
|
|
898
|
-
const getNetworkUrls = () => isLoopbackHost(host) ? [] : getLanAddresses().map((address) => `http://${address}:${actualPort}`);
|
|
899
|
-
const getUrl = () => `http://${displayHost}:${actualPort}`;
|
|
561
|
+
if (options.onScan) onDevScan(viteServer, options.onScan);
|
|
562
|
+
await viteServer.listen();
|
|
563
|
+
const address = viteServer.httpServer?.address();
|
|
564
|
+
const actualPort = address && typeof address === "object" ? address.port : options.port;
|
|
565
|
+
const url = `http://${isWildcardHost(host) ? "localhost" : host}:${actualPort}`;
|
|
566
|
+
const networkUrls = isLoopbackHost(host) ? [] : getLanAddresses().map((lanAddress) => `http://${lanAddress}:${actualPort}`);
|
|
567
|
+
options.onListen?.({
|
|
568
|
+
port: actualPort,
|
|
569
|
+
host,
|
|
570
|
+
url,
|
|
571
|
+
networkUrls
|
|
572
|
+
});
|
|
900
573
|
return {
|
|
901
574
|
get port() {
|
|
902
575
|
return actualPort;
|
|
@@ -905,213 +578,19 @@ async function createViteDevServer(options) {
|
|
|
905
578
|
return host;
|
|
906
579
|
},
|
|
907
580
|
get url() {
|
|
908
|
-
return
|
|
581
|
+
return url;
|
|
909
582
|
},
|
|
910
583
|
get viteServer() {
|
|
911
584
|
return viteServer;
|
|
912
585
|
},
|
|
913
|
-
async
|
|
914
|
-
await
|
|
915
|
-
const tryListen = (port) => {
|
|
916
|
-
httpServer.removeAllListeners("error");
|
|
917
|
-
httpServer.once("error", (err) => {
|
|
918
|
-
if (err.code === "EADDRINUSE" && !strictPort) {
|
|
919
|
-
logger$8.warn(`Port ${port} is in use, trying ${port + 1} instead.`);
|
|
920
|
-
tryListen(port + 1);
|
|
921
|
-
} else reject(err);
|
|
922
|
-
});
|
|
923
|
-
httpServer.listen(port, host, () => {
|
|
924
|
-
const addr = httpServer.address();
|
|
925
|
-
actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
926
|
-
resolve();
|
|
927
|
-
});
|
|
928
|
-
};
|
|
929
|
-
tryListen(actualPort);
|
|
930
|
-
});
|
|
931
|
-
options.onListen?.({
|
|
932
|
-
port: actualPort,
|
|
933
|
-
host,
|
|
934
|
-
url: getUrl(),
|
|
935
|
-
networkUrls: getNetworkUrls()
|
|
936
|
-
});
|
|
586
|
+
async rescan() {
|
|
587
|
+
await rescanDev();
|
|
937
588
|
},
|
|
938
589
|
async stop() {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
await new Promise((resolve) => {
|
|
943
|
-
server.close(() => resolve());
|
|
944
|
-
server.closeAllConnections();
|
|
945
|
-
});
|
|
946
|
-
}
|
|
947
|
-
if (viteServer) {
|
|
948
|
-
await viteServer.close();
|
|
949
|
-
viteServer = null;
|
|
950
|
-
}
|
|
951
|
-
},
|
|
952
|
-
updateApp(app, layouts) {
|
|
953
|
-
currentApp = app;
|
|
954
|
-
if (layouts) currentLayouts = layouts;
|
|
955
|
-
serverConfigApplied = false;
|
|
956
|
-
serverReadyCalled = false;
|
|
957
|
-
cachedServerConfig = null;
|
|
958
|
-
enhanceAppWithVite(app, currentLayouts);
|
|
959
|
-
refreshDevtools?.();
|
|
960
|
-
},
|
|
961
|
-
sendFullReload() {
|
|
962
|
-
if (viteServer) viteServer.ws.send({ type: "full-reload" });
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
//#endregion
|
|
967
|
-
//#region src/dev-server/runner.ts
|
|
968
|
-
var ViteNodeDevRunner = class {
|
|
969
|
-
viteDevServer = null;
|
|
970
|
-
currentApp;
|
|
971
|
-
currentLayouts;
|
|
972
|
-
options;
|
|
973
|
-
_port;
|
|
974
|
-
constructor(options) {
|
|
975
|
-
this.options = options;
|
|
976
|
-
this.currentApp = options.app;
|
|
977
|
-
this.currentLayouts = options.layouts || [];
|
|
978
|
-
this._port = options.port;
|
|
979
|
-
}
|
|
980
|
-
get port() {
|
|
981
|
-
return this._port;
|
|
982
|
-
}
|
|
983
|
-
get host() {
|
|
984
|
-
return this.options.host;
|
|
985
|
-
}
|
|
986
|
-
get url() {
|
|
987
|
-
return `http://${this.options.host}:${this._port}`;
|
|
988
|
-
}
|
|
989
|
-
get preset() {
|
|
990
|
-
return this.options.preset;
|
|
991
|
-
}
|
|
992
|
-
async start() {
|
|
993
|
-
this.viteDevServer = await createViteDevServer({
|
|
994
|
-
cwd: this.options.cwd,
|
|
995
|
-
port: this._port,
|
|
996
|
-
host: this.options.host,
|
|
997
|
-
strictPort: this.options.strictPort,
|
|
998
|
-
config: this.options.config,
|
|
999
|
-
app: this.currentApp,
|
|
1000
|
-
layouts: this.currentLayouts,
|
|
1001
|
-
devtools: this.options.devtools,
|
|
1002
|
-
onListen: ({ port, host, url, networkUrls }) => {
|
|
1003
|
-
this._port = port;
|
|
1004
|
-
this.options.onListen?.({
|
|
1005
|
-
port,
|
|
1006
|
-
host,
|
|
1007
|
-
url,
|
|
1008
|
-
networkUrls
|
|
1009
|
-
});
|
|
1010
|
-
}
|
|
1011
|
-
});
|
|
1012
|
-
await this.viteDevServer.start();
|
|
1013
|
-
}
|
|
1014
|
-
async stop() {
|
|
1015
|
-
if (this.viteDevServer) {
|
|
1016
|
-
await this.viteDevServer.stop();
|
|
1017
|
-
this.viteDevServer = null;
|
|
1018
|
-
}
|
|
1019
|
-
}
|
|
1020
|
-
async reload() {
|
|
1021
|
-
await this.options.onBeforeReload?.();
|
|
1022
|
-
if (this.viteDevServer) this.viteDevServer.updateApp(this.currentApp, this.currentLayouts);
|
|
1023
|
-
await this.options.onAfterReload?.();
|
|
1024
|
-
}
|
|
1025
|
-
updateApp(app, layouts) {
|
|
1026
|
-
this.currentApp = app;
|
|
1027
|
-
if (layouts) this.currentLayouts = layouts;
|
|
1028
|
-
if (this.viteDevServer) this.viteDevServer.updateApp(app, this.currentLayouts);
|
|
1029
|
-
}
|
|
1030
|
-
sendFullReload() {
|
|
1031
|
-
this.viteDevServer?.sendFullReload();
|
|
1032
|
-
}
|
|
1033
|
-
};
|
|
1034
|
-
const runners = [{
|
|
1035
|
-
name: "vite-node",
|
|
1036
|
-
isAvailable() {
|
|
1037
|
-
return typeof process !== "undefined" && !!process.versions?.node;
|
|
1038
|
-
},
|
|
1039
|
-
async createRunner(options) {
|
|
1040
|
-
return new ViteNodeDevRunner(options);
|
|
1041
|
-
}
|
|
1042
|
-
}];
|
|
1043
|
-
async function selectRunner(_preset) {
|
|
1044
|
-
for (const runner of runners) if (await runner.isAvailable()) return runner;
|
|
1045
|
-
return null;
|
|
1046
|
-
}
|
|
1047
|
-
async function createDevRunner(options) {
|
|
1048
|
-
const runner = await selectRunner(options.preset);
|
|
1049
|
-
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).`);
|
|
1050
|
-
return runner.createRunner(options);
|
|
1051
|
-
}
|
|
1052
|
-
//#endregion
|
|
1053
|
-
//#region src/dev-server/watcher.ts
|
|
1054
|
-
function createDevWatcher(options) {
|
|
1055
|
-
const { cwd, dirs, ignore = [], debounceMs = 100 } = options;
|
|
1056
|
-
const watchers = [];
|
|
1057
|
-
const watchedDirs = /* @__PURE__ */ new Set();
|
|
1058
|
-
let pendingEvents = [];
|
|
1059
|
-
let debounceTimer = null;
|
|
1060
|
-
function isIgnored(relPath) {
|
|
1061
|
-
return ignore.some((pattern) => {
|
|
1062
|
-
if (pattern.includes("*")) return new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(relPath);
|
|
1063
|
-
return relPath.startsWith(pattern) || relPath === pattern;
|
|
1064
|
-
});
|
|
1065
|
-
}
|
|
1066
|
-
function queueEvent(type, fullPath) {
|
|
1067
|
-
const relPath = relative(cwd, fullPath).replace(/\\/g, "/");
|
|
1068
|
-
if (isIgnored(relPath)) return;
|
|
1069
|
-
if (relPath.startsWith("node_modules/") || relPath.startsWith(".git/")) return;
|
|
1070
|
-
if (relPath.endsWith(".bak") || relPath.startsWith(".ubean/")) return;
|
|
1071
|
-
pendingEvents.push({
|
|
1072
|
-
type,
|
|
1073
|
-
path: fullPath,
|
|
1074
|
-
relativePath: relPath
|
|
1075
|
-
});
|
|
1076
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1077
|
-
debounceTimer = setTimeout(() => {
|
|
1078
|
-
const events = pendingEvents;
|
|
1079
|
-
pendingEvents = [];
|
|
1080
|
-
debounceTimer = null;
|
|
1081
|
-
options.onChange?.(events);
|
|
1082
|
-
}, debounceMs);
|
|
1083
|
-
}
|
|
1084
|
-
function watchDir(dir) {
|
|
1085
|
-
if (watchedDirs.has(dir)) return;
|
|
1086
|
-
watchedDirs.add(dir);
|
|
1087
|
-
try {
|
|
1088
|
-
const w = watch(dir, { recursive: true }, (eventType, filename) => {
|
|
1089
|
-
if (!filename) return;
|
|
1090
|
-
const fullPath = join(dir, filename);
|
|
1091
|
-
queueEvent(eventType === "rename" ? "unlink" : "change", fullPath);
|
|
1092
|
-
});
|
|
1093
|
-
watchers.push(w);
|
|
1094
|
-
} catch {}
|
|
1095
|
-
}
|
|
1096
|
-
function start() {
|
|
1097
|
-
for (const dir of dirs) watchDir(join(cwd, dir));
|
|
1098
|
-
}
|
|
1099
|
-
function stop() {
|
|
1100
|
-
for (const w of watchers) w.close();
|
|
1101
|
-
watchers.length = 0;
|
|
1102
|
-
watchedDirs.clear();
|
|
1103
|
-
if (debounceTimer) {
|
|
1104
|
-
clearTimeout(debounceTimer);
|
|
1105
|
-
debounceTimer = null;
|
|
590
|
+
const server = viteServer;
|
|
591
|
+
viteServer = null;
|
|
592
|
+
await server?.close();
|
|
1106
593
|
}
|
|
1107
|
-
}
|
|
1108
|
-
function addDir(dir) {
|
|
1109
|
-
watchDir(join(cwd, dir));
|
|
1110
|
-
}
|
|
1111
|
-
return {
|
|
1112
|
-
start,
|
|
1113
|
-
stop,
|
|
1114
|
-
addDir
|
|
1115
594
|
};
|
|
1116
595
|
}
|
|
1117
596
|
//#endregion
|
|
@@ -1164,20 +643,6 @@ import("@ubean/devtools").then((mod) => {
|
|
|
1164
643
|
function getCustomTabs() {
|
|
1165
644
|
return _customTabsGetter();
|
|
1166
645
|
}
|
|
1167
|
-
/**
|
|
1168
|
-
* dev 环境 SecurityHeaders 解析(ADR-0011)。
|
|
1169
|
-
*
|
|
1170
|
-
* ssg 模式默认关闭:静态产物不携带框架注入的安全头(响应头由托管平台
|
|
1171
|
-
* 控制),dev 默认关闭以对齐生产行为,消除「dev 有 CSP / 线上没有」的
|
|
1172
|
-
* 偏差。显式配置(`headers: true` 或对象)仍会被尊重,作为 CSP 调试
|
|
1173
|
-
* 入口。其余模式默认开启不变。
|
|
1174
|
-
*/
|
|
1175
|
-
function resolveDevSecurityHeaders(config) {
|
|
1176
|
-
if (config.security === false) return false;
|
|
1177
|
-
if (config.security?.headers === false) return false;
|
|
1178
|
-
if (config.security?.headers !== void 0) return config.security.headers;
|
|
1179
|
-
return config.mode !== "ssg";
|
|
1180
|
-
}
|
|
1181
646
|
const devCommand = {
|
|
1182
647
|
meta: {
|
|
1183
648
|
name: "dev",
|
|
@@ -1219,6 +684,7 @@ const devCommand = {
|
|
|
1219
684
|
const cwd = resolve(args.cwd || process.cwd());
|
|
1220
685
|
registerBuiltinPresets();
|
|
1221
686
|
const config = await loadUbeanConfig(cwd);
|
|
687
|
+
process.env.UBEAN_CODEGEN_BY_CLI = "1";
|
|
1222
688
|
const logging = config.logging;
|
|
1223
689
|
if (args.verbose) {
|
|
1224
690
|
logging.scan = true;
|
|
@@ -1246,138 +712,102 @@ const devCommand = {
|
|
|
1246
712
|
logger$6.info(`Source directory: ${config.srcDir}`);
|
|
1247
713
|
logger$6.info(`Preset: ${preset.name}`);
|
|
1248
714
|
}
|
|
1249
|
-
const capabilities = createCapabilitySet(preset.capabilities || {});
|
|
1250
715
|
if (logging.scan) logger$6.info("Running capability diagnostics...");
|
|
1251
716
|
const diagnostics = diagnoseCapabilities(preset.name, preset.capabilities || {}, NODE_REQUIREMENTS);
|
|
1252
717
|
logDiagnostics(diagnostics, { verbose: logging.diagnostics });
|
|
1253
718
|
if (!diagnostics.valid) logger$6.warn("Some capability requirements are not met. Dev server may not function correctly.");
|
|
1254
|
-
let
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
719
|
+
let currentScanResult = await initialScan(cwd, config, logging);
|
|
720
|
+
/**
|
|
721
|
+
* 扫描后的命令级事务(RM-V13:扫描由 dev-scan 协调器驱动)。
|
|
722
|
+
*
|
|
723
|
+
* app 的重建不在这里 —— 它归 `ubeanDevRequestPlugin`(RM-V14)。CLI 负责把类型定义重新
|
|
724
|
+
* 生成,并把新扫描结果交给 DevTools。**重载不由 CLI 发**:协调器会在所有订阅者完成后统一发
|
|
725
|
+
* `full-reload`,因此浏览器不会带着旧的 `definePage` 元数据刷新。
|
|
726
|
+
*/
|
|
727
|
+
async function onScan(result, changed) {
|
|
728
|
+
if (changed.length > 0) {
|
|
729
|
+
lastChangedFile = relative(cwd, changed[0]).replace(/\\/g, "/");
|
|
730
|
+
if (logging.lifecycle) logger$6.info(`File change detected: ${lastChangedFile}`);
|
|
731
|
+
}
|
|
732
|
+
currentScanResult = result;
|
|
1259
733
|
try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
734
|
+
await generateTypes(result, {
|
|
735
|
+
cwd,
|
|
736
|
+
srcDir: config.srcDir,
|
|
737
|
+
buildDir: ".ubean",
|
|
738
|
+
dirs: config.dir,
|
|
739
|
+
autoImports: config.autoImports,
|
|
740
|
+
components: config.components
|
|
741
|
+
});
|
|
1267
742
|
} catch (err) {
|
|
1268
743
|
logger$6.error(`Rescan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1269
|
-
} finally {
|
|
1270
|
-
rescanInProgress = false;
|
|
1271
744
|
}
|
|
1272
745
|
}
|
|
1273
|
-
let reloadStartedAt = Date.now();
|
|
1274
746
|
let lastChangedFile = null;
|
|
1275
|
-
|
|
1276
|
-
cwd,
|
|
1277
|
-
srcDir: config.srcDir,
|
|
1278
|
-
port,
|
|
1279
|
-
host,
|
|
1280
|
-
strictPort: args.strictPort,
|
|
1281
|
-
preset,
|
|
1282
|
-
config,
|
|
1283
|
-
capabilities,
|
|
1284
|
-
app: currentApp,
|
|
1285
|
-
layouts: currentLayouts,
|
|
1286
|
-
devtools: {
|
|
1287
|
-
getScanResult: () => currentScanResult,
|
|
1288
|
-
getConfigMeta: () => ({
|
|
1289
|
-
preset: config.build.preset,
|
|
1290
|
-
rootDir: config.rootDir,
|
|
1291
|
-
srcDir: config.srcDir,
|
|
1292
|
-
openAPI: {
|
|
1293
|
-
enabled: true,
|
|
1294
|
-
scalarPath: "/_scalar",
|
|
1295
|
-
openAPIPath: "/_openapi.json"
|
|
1296
|
-
},
|
|
1297
|
-
dir: config.dir
|
|
1298
|
-
}),
|
|
1299
|
-
getCustomTabs: () => getCustomTabs(),
|
|
1300
|
-
triggerRescan: () => rescan()
|
|
1301
|
-
},
|
|
1302
|
-
onListen({ url, networkUrls }) {
|
|
1303
|
-
const label = (text) => dim(text);
|
|
1304
|
-
const modeSuffix = dim(` (${config.mode} · ${preset.name})`);
|
|
1305
|
-
const lines = [
|
|
1306
|
-
`${green(bold(`🚀 ubean dev server ready in ${Date.now() - startedAt}ms`))}${modeSuffix}\n`,
|
|
1307
|
-
` → ${label("Local:")} ${cyan(url)}`,
|
|
1308
|
-
...(networkUrls || []).map((n) => ` → ${label("Network:")} ${cyan(n)}`)
|
|
1309
|
-
];
|
|
1310
|
-
const hasBackend = config.mode !== "ssg" && config.mode !== "spa";
|
|
1311
|
-
if (hasBackend) lines.push(` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`, ` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`);
|
|
1312
|
-
if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
|
|
1313
|
-
lines.push(` → ${dim("Press Ctrl+C to stop")}`);
|
|
1314
|
-
logger$6.info(lines.join("\n"));
|
|
1315
|
-
if (hasBackend) generateOpenApiTypesFromServer(url, { outDir: resolve(cwd, ".ubean") }).then((filePath) => {
|
|
1316
|
-
if (filePath && logging.scan) logger$6.info(`OpenAPI types generated: ${filePath}`);
|
|
1317
|
-
}).catch((err) => {
|
|
1318
|
-
logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
|
|
1319
|
-
});
|
|
1320
|
-
},
|
|
1321
|
-
onBeforeReload() {
|
|
1322
|
-
reloadStartedAt = Date.now();
|
|
1323
|
-
if (logging.lifecycle) logger$6.info("Reloading...");
|
|
1324
|
-
},
|
|
1325
|
-
onAfterReload() {
|
|
1326
|
-
if (logging.lifecycle) logger$6.info("Reloaded");
|
|
1327
|
-
else {
|
|
1328
|
-
const file = lastChangedFile ? dim(` · ${lastChangedFile}`) : "";
|
|
1329
|
-
logger$6.info(dim(`↻ Reloaded in ${Date.now() - reloadStartedAt}ms${file}`));
|
|
1330
|
-
}
|
|
1331
|
-
lastChangedFile = null;
|
|
1332
|
-
}
|
|
1333
|
-
});
|
|
747
|
+
let devServer = null;
|
|
1334
748
|
try {
|
|
1335
|
-
await
|
|
749
|
+
devServer = await createDevViteServer({
|
|
750
|
+
cwd,
|
|
751
|
+
port,
|
|
752
|
+
host,
|
|
753
|
+
strictPort: args.strictPort,
|
|
754
|
+
config,
|
|
755
|
+
onScan,
|
|
756
|
+
devtools: {
|
|
757
|
+
getScanResult: () => currentScanResult,
|
|
758
|
+
getConfigMeta: () => ({
|
|
759
|
+
preset: config.build.preset,
|
|
760
|
+
rootDir: config.rootDir,
|
|
761
|
+
srcDir: config.srcDir,
|
|
762
|
+
openAPI: {
|
|
763
|
+
enabled: true,
|
|
764
|
+
scalarPath: "/_scalar",
|
|
765
|
+
openAPIPath: "/_openapi.json"
|
|
766
|
+
},
|
|
767
|
+
dir: config.dir
|
|
768
|
+
}),
|
|
769
|
+
getCustomTabs: () => getCustomTabs()
|
|
770
|
+
},
|
|
771
|
+
onListen({ url, networkUrls }) {
|
|
772
|
+
const label = (text) => dim(text);
|
|
773
|
+
const modeSuffix = dim(` (${config.mode} · ${preset.name})`);
|
|
774
|
+
const lines = [
|
|
775
|
+
`${green(bold(`🚀 ubean dev server ready in ${Date.now() - startedAt}ms`))}${modeSuffix}\n`,
|
|
776
|
+
` → ${label("Local:")} ${cyan(url)}`,
|
|
777
|
+
...(networkUrls || []).map((n) => ` → ${label("Network:")} ${cyan(n)}`)
|
|
778
|
+
];
|
|
779
|
+
const hasBackend = config.mode !== "ssg" && config.mode !== "spa";
|
|
780
|
+
if (hasBackend) lines.push(` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`, ` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`);
|
|
781
|
+
if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
|
|
782
|
+
lines.push(` → ${dim("Press Ctrl+C to stop")}`);
|
|
783
|
+
logger$6.info(lines.join("\n"));
|
|
784
|
+
if (hasBackend) generateOpenApiTypesFromServer(url, { outDir: resolve(cwd, ".ubean") }).then((filePath) => {
|
|
785
|
+
if (filePath && logging.scan) logger$6.info(`OpenAPI types generated: ${filePath}`);
|
|
786
|
+
}).catch((err) => {
|
|
787
|
+
logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
});
|
|
1336
791
|
} catch (err) {
|
|
1337
792
|
logger$6.error(err?.stack || err?.message || String(err));
|
|
1338
793
|
process.exit(1);
|
|
1339
794
|
}
|
|
1340
|
-
const watcher = createDevWatcher({
|
|
1341
|
-
cwd,
|
|
1342
|
-
dirs: [
|
|
1343
|
-
"api",
|
|
1344
|
-
"pages",
|
|
1345
|
-
"middleware",
|
|
1346
|
-
"layouts",
|
|
1347
|
-
"plugins",
|
|
1348
|
-
"app",
|
|
1349
|
-
"routes"
|
|
1350
|
-
].map((d) => `${config.srcDir}/${d}`),
|
|
1351
|
-
ignore: [
|
|
1352
|
-
"**/node_modules/**",
|
|
1353
|
-
"**/.git/**",
|
|
1354
|
-
"**/.ubean/**"
|
|
1355
|
-
],
|
|
1356
|
-
debounceMs: 150,
|
|
1357
|
-
async onChange(events) {
|
|
1358
|
-
const relevantEvents = events.filter((e) => /\.(ts|js|vue|mjs|cjs|json)$/.test(e.relativePath) && !e.relativePath.includes(".bak"));
|
|
1359
|
-
if (relevantEvents.length === 0) return;
|
|
1360
|
-
lastChangedFile = relevantEvents[0].relativePath;
|
|
1361
|
-
if (logging.lifecycle) logger$6.info(`File change detected: ${lastChangedFile}`);
|
|
1362
|
-
await rescan();
|
|
1363
|
-
}
|
|
1364
|
-
});
|
|
1365
|
-
watcher.start();
|
|
1366
795
|
const cleanup = async () => {
|
|
1367
796
|
if (logging.lifecycle) logger$6.info("\nShutting down...");
|
|
1368
|
-
|
|
1369
|
-
await runner.stop();
|
|
797
|
+
await devServer?.stop();
|
|
1370
798
|
process.exit(0);
|
|
1371
799
|
};
|
|
1372
800
|
process.on("SIGINT", cleanup);
|
|
1373
801
|
process.on("SIGTERM", cleanup);
|
|
1374
802
|
}
|
|
1375
803
|
};
|
|
1376
|
-
/**
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
804
|
+
/**
|
|
805
|
+
* 初始扫描 + 类型生成。
|
|
806
|
+
*
|
|
807
|
+
* RM-V14 起 CLI 不在这里建 app(由 `ubeanDevRequestPlugin` 自举),只负责扫描与 codegen;
|
|
808
|
+
* 扫描结果交给 DevTools 作启动数据,并在每次文件变更后由 `onScan` 刷新。
|
|
809
|
+
*/
|
|
810
|
+
async function initialScan(cwd, config, logging) {
|
|
1381
811
|
if (logging.scan) logger$6.info("Scanning project files...");
|
|
1382
812
|
const result = await scanProject({
|
|
1383
813
|
cwd,
|
|
@@ -1385,8 +815,10 @@ async function buildApp(cwd, config, logging) {
|
|
|
1385
815
|
dirs: config.dir,
|
|
1386
816
|
ignore: config.scanOptions?.ignore
|
|
1387
817
|
});
|
|
1388
|
-
if (logging.scan)
|
|
1389
|
-
|
|
818
|
+
if (logging.scan) {
|
|
819
|
+
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`);
|
|
820
|
+
logger$6.info("Generating type definitions...");
|
|
821
|
+
}
|
|
1390
822
|
await generateTypes(result, {
|
|
1391
823
|
cwd,
|
|
1392
824
|
srcDir: config.srcDir,
|
|
@@ -1395,59 +827,12 @@ async function buildApp(cwd, config, logging) {
|
|
|
1395
827
|
autoImports: config.autoImports,
|
|
1396
828
|
components: config.components
|
|
1397
829
|
});
|
|
1398
|
-
|
|
1399
|
-
rootDir: cwd,
|
|
1400
|
-
routes: result.apiRoutes,
|
|
1401
|
-
middleware: result.middlewares,
|
|
1402
|
-
pages: result.pages,
|
|
1403
|
-
crons: result.crons,
|
|
1404
|
-
routeRules: config.routeRules || {},
|
|
1405
|
-
publicDir: config.dir.public,
|
|
1406
|
-
pageAssetTags: config.favicon ? { favicon: config.favicon } : void 0,
|
|
1407
|
-
openAPI: {
|
|
1408
|
-
title: "UBEAN Dev API",
|
|
1409
|
-
scalarPath: "/_scalar",
|
|
1410
|
-
openAPIPath: "/_openapi.json"
|
|
1411
|
-
},
|
|
1412
|
-
i18nConfig: config.i18n,
|
|
1413
|
-
ssrExclude: config.ssr.exclude,
|
|
1414
|
-
streaming: config.ssr.streaming,
|
|
1415
|
-
notFoundPage: result.notFoundPage,
|
|
1416
|
-
csrf: config.security === false ? false : config.security?.csrf ?? true,
|
|
1417
|
-
securityHeaders: resolveDevSecurityHeaders(config),
|
|
1418
|
-
dataCache: config.dataCache,
|
|
1419
|
-
cache: config.cache,
|
|
1420
|
-
seoConventions: { srcDir: resolve(cwd, config.srcDir) }
|
|
1421
|
-
});
|
|
1422
|
-
const requestStartTimes = /* @__PURE__ */ new WeakMap();
|
|
1423
|
-
const isInternalPath = (path) => REQUEST_LOG_INTERNAL_PREFIXES.some((p) => path.startsWith(p));
|
|
1424
|
-
app.hooks.hook("request:start", (c) => {
|
|
1425
|
-
requestStartTimes.set(c, Date.now());
|
|
1426
|
-
logger$6.debug(`${c.req.method} ${c.req.path}`);
|
|
1427
|
-
});
|
|
1428
|
-
app.hooks.hook("request:end", (c, res) => {
|
|
1429
|
-
if (!logging.request) return;
|
|
1430
|
-
const start = requestStartTimes.get(c);
|
|
1431
|
-
const duration = start === void 0 ? 0 : Date.now() - start;
|
|
1432
|
-
const line = `${c.req.method} ${c.req.path} ${res.status} ${duration}ms`;
|
|
1433
|
-
if (res.status >= 500) {
|
|
1434
|
-
logger$6.error(line);
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
if (isInternalPath(c.req.path)) return;
|
|
1438
|
-
if (res.status >= 400 || duration >= REQUEST_LOG_SLOW_THRESHOLD) logger$6.warn(line);
|
|
1439
|
-
else logger$6.info(line);
|
|
1440
|
-
});
|
|
1441
|
-
app.hooks.hook("request:error", (c, err) => {
|
|
1442
|
-
if (!logging.request) return;
|
|
1443
|
-
logger$6.error(`${c.req.method} ${c.req.path} ERROR ${err instanceof Error ? err.message : String(err)}`);
|
|
1444
|
-
});
|
|
1445
|
-
return {
|
|
1446
|
-
app,
|
|
1447
|
-
layouts: result.layouts,
|
|
1448
|
-
scanResult: result
|
|
1449
|
-
};
|
|
830
|
+
return result;
|
|
1450
831
|
}
|
|
832
|
+
/**
|
|
833
|
+
* RM-V14:请求日志(`logging.request`)不再由 CLI 实现 —— 见 `@ubean/build` 的
|
|
834
|
+
* `createDevApp({ logger })`。配置域行为放 builder,两条 dev 路径输出一致。
|
|
835
|
+
*/
|
|
1451
836
|
//#endregion
|
|
1452
837
|
//#region src/devtools.ts
|
|
1453
838
|
const logger$5 = getLogger("cli");
|
|
@@ -2152,89 +1537,38 @@ const prepareCommand = {
|
|
|
2152
1537
|
};
|
|
2153
1538
|
//#endregion
|
|
2154
1539
|
//#region src/preview.ts
|
|
2155
|
-
const MIME_TYPES = {
|
|
2156
|
-
".html": "text/html; charset=utf-8",
|
|
2157
|
-
".js": "text/javascript; charset=utf-8",
|
|
2158
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
2159
|
-
".css": "text/css; charset=utf-8",
|
|
2160
|
-
".json": "application/json; charset=utf-8",
|
|
2161
|
-
".svg": "image/svg+xml",
|
|
2162
|
-
".png": "image/png",
|
|
2163
|
-
".jpg": "image/jpeg",
|
|
2164
|
-
".jpeg": "image/jpeg",
|
|
2165
|
-
".gif": "image/gif",
|
|
2166
|
-
".webp": "image/webp",
|
|
2167
|
-
".ico": "image/x-icon",
|
|
2168
|
-
".woff": "font/woff",
|
|
2169
|
-
".woff2": "font/woff2",
|
|
2170
|
-
".ttf": "font/ttf",
|
|
2171
|
-
".otf": "font/otf",
|
|
2172
|
-
".wasm": "application/wasm",
|
|
2173
|
-
".txt": "text/plain; charset=utf-8",
|
|
2174
|
-
".map": "application/json; charset=utf-8"
|
|
2175
|
-
};
|
|
2176
1540
|
/**
|
|
2177
|
-
*
|
|
1541
|
+
* 静态文件服务器(spa / ssg 的降级路径)。
|
|
2178
1542
|
*
|
|
2179
|
-
* -
|
|
2180
|
-
*
|
|
1543
|
+
* RM-V25:判定规则不再自己写一份,改用 `@ubean/build/vite` 的 `resolvePreviewFile` —— 插件接管
|
|
1544
|
+
* `vite preview` 时用的是同一个函数,两套方言必然分叉(文件名含合法 `...` 的产物曾被
|
|
1545
|
+
* `includes('..')` 判成路径穿越)。
|
|
2181
1546
|
*
|
|
2182
|
-
*
|
|
1547
|
+
* 主路径已改为 `vite preview` + 插件中间件(RM-V24);本函数保留给「Vite 预览起不来」的
|
|
1548
|
+
* spa / ssg 场景(静态产物不需要服务端能力),以及既有回归测试。
|
|
2183
1549
|
*/
|
|
2184
1550
|
function startStaticServer(opts) {
|
|
2185
1551
|
const { root, port, host, mode } = opts;
|
|
2186
|
-
const spaFallback = mode === "spa";
|
|
2187
1552
|
return createServer(async (req, res) => {
|
|
2188
1553
|
try {
|
|
2189
1554
|
const url = new URL(req.url || "/", `http://${host}`);
|
|
2190
1555
|
const pathname = decodeURIComponent(url.pathname);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
if (filePath !== rootPath && !filePath.startsWith(rootPath + sep)) {
|
|
1556
|
+
const resolvedFile = resolvePreviewFile(root, pathname, mode);
|
|
1557
|
+
if (resolvedFile.kind === "forbidden") {
|
|
2194
1558
|
res.statusCode = 400;
|
|
1559
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2195
1560
|
res.end("Bad Request");
|
|
2196
1561
|
return;
|
|
2197
1562
|
}
|
|
2198
|
-
|
|
2199
|
-
if (!fileExists && existsSync(filePath) && (await stat(filePath)).isDirectory()) {
|
|
2200
|
-
const indexPath = join(filePath, "index.html");
|
|
2201
|
-
if (existsSync(indexPath)) {
|
|
2202
|
-
filePath = indexPath;
|
|
2203
|
-
fileExists = true;
|
|
2204
|
-
}
|
|
2205
|
-
}
|
|
2206
|
-
if (!fileExists && mode === "ssg") {
|
|
2207
|
-
const indexPath = join(filePath, "index.html");
|
|
2208
|
-
if (existsSync(indexPath) && (await stat(indexPath)).isFile()) {
|
|
2209
|
-
filePath = indexPath;
|
|
2210
|
-
fileExists = true;
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
if (!fileExists && mode === "ssg") {
|
|
2214
|
-
const htmlPath = `${filePath}.html`;
|
|
2215
|
-
if (existsSync(htmlPath) && (await stat(htmlPath)).isFile()) {
|
|
2216
|
-
filePath = htmlPath;
|
|
2217
|
-
fileExists = true;
|
|
2218
|
-
}
|
|
2219
|
-
}
|
|
2220
|
-
if (!fileExists && spaFallback) {
|
|
2221
|
-
const fallbackPath = join(root, "index.html");
|
|
2222
|
-
if (existsSync(fallbackPath)) {
|
|
2223
|
-
filePath = fallbackPath;
|
|
2224
|
-
fileExists = true;
|
|
2225
|
-
}
|
|
2226
|
-
}
|
|
2227
|
-
if (!fileExists) {
|
|
1563
|
+
if (resolvedFile.kind === "missing") {
|
|
2228
1564
|
res.statusCode = 404;
|
|
2229
1565
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2230
1566
|
res.end("Not Found");
|
|
2231
1567
|
return;
|
|
2232
1568
|
}
|
|
2233
|
-
const
|
|
2234
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2235
|
-
const body = await readFile(filePath);
|
|
1569
|
+
const body = await readFile(resolvedFile.path);
|
|
2236
1570
|
res.statusCode = 200;
|
|
2237
|
-
res.setHeader("Content-Type",
|
|
1571
|
+
res.setHeader("Content-Type", previewMimeType(resolvedFile.path));
|
|
2238
1572
|
res.setHeader("Content-Length", body.length);
|
|
2239
1573
|
res.end(body);
|
|
2240
1574
|
} catch (err) {
|
|
@@ -2268,6 +1602,10 @@ const previewCommand = {
|
|
|
2268
1602
|
type: "string",
|
|
2269
1603
|
description: "Project root directory",
|
|
2270
1604
|
default: "."
|
|
1605
|
+
},
|
|
1606
|
+
outDir: {
|
|
1607
|
+
type: "string",
|
|
1608
|
+
description: "Output directory to preview (overrides ubean.config.ts build.outputDir)"
|
|
2271
1609
|
}
|
|
2272
1610
|
},
|
|
2273
1611
|
async run({ args }) {
|
|
@@ -2277,6 +1615,7 @@ const previewCommand = {
|
|
|
2277
1615
|
const config = await loadUbeanConfig(cwd);
|
|
2278
1616
|
const preset = resolvePresetByName(config.build.preset);
|
|
2279
1617
|
logger$1.info(`Preset: ${preset.name}`);
|
|
1618
|
+
if (args.outDir) config.build.outputDir = String(args.outDir);
|
|
2280
1619
|
const outputDir = config.build.outputDir || "dist";
|
|
2281
1620
|
const mode = config.mode;
|
|
2282
1621
|
const host = args.host || config.preview.host;
|
|
@@ -2299,110 +1638,71 @@ const previewCommand = {
|
|
|
2299
1638
|
const bannerUrl = `http://${host}:${port}`;
|
|
2300
1639
|
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")}`);
|
|
2301
1640
|
};
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
1641
|
+
const staticMode = mode === "spa" || mode === "ssg";
|
|
1642
|
+
const isCloudflare = preset.name === "cloudflare";
|
|
1643
|
+
const staticRoot = join(cwd, outputDir, "public");
|
|
1644
|
+
const requiredArtifact = staticMode ? join(staticRoot, "index.html") : isCloudflare ? join(cwd, outputDir, "server", "worker.mjs") : join(cwd, outputDir, "server", "entry.mjs");
|
|
1645
|
+
if (!existsSync(requiredArtifact)) {
|
|
1646
|
+
logger$1.error(`Build output not found: ${requiredArtifact}`);
|
|
1647
|
+
logger$1.info("Run `ubean build` first to create a production build.");
|
|
1648
|
+
process.exit(1);
|
|
1649
|
+
}
|
|
1650
|
+
if (isCloudflare) {
|
|
1651
|
+
const probe = await createCloudflarePreviewRunner({ workerPath: requiredArtifact });
|
|
1652
|
+
if (!probe.ok) {
|
|
1653
|
+
logger$1.error(probe.message);
|
|
1654
|
+
process.exit(1);
|
|
1655
|
+
}
|
|
1656
|
+
await probe.runner.dispose();
|
|
1657
|
+
}
|
|
1658
|
+
const userViteConfig = findUserViteConfig(cwd);
|
|
1659
|
+
let server;
|
|
1660
|
+
try {
|
|
1661
|
+
server = await preview({
|
|
1662
|
+
root: cwd,
|
|
1663
|
+
configFile: userViteConfig ?? false,
|
|
1664
|
+
mode: "production",
|
|
1665
|
+
logLevel: "warn",
|
|
1666
|
+
preview: {
|
|
1667
|
+
port: actualPort,
|
|
1668
|
+
host,
|
|
1669
|
+
strictPort
|
|
1670
|
+
},
|
|
1671
|
+
...userViteConfig ? {} : { plugins: [ubeanPreviewPlugin({
|
|
1672
|
+
config,
|
|
1673
|
+
host
|
|
1674
|
+
})] }
|
|
1675
|
+
});
|
|
1676
|
+
} catch (err) {
|
|
1677
|
+
if (!staticMode) {
|
|
1678
|
+
logger$1.error(`Failed to start the preview server: ${err instanceof Error ? err.message : String(err)}`);
|
|
2307
1679
|
process.exit(1);
|
|
2308
1680
|
}
|
|
2309
|
-
|
|
1681
|
+
logger$1.warn(`Vite preview unavailable (${err instanceof Error ? err.message : String(err)}); falling back to the built-in static server.`);
|
|
1682
|
+
const fallback = startStaticServer({
|
|
2310
1683
|
root: staticRoot,
|
|
2311
1684
|
port: actualPort,
|
|
2312
1685
|
host,
|
|
2313
1686
|
mode
|
|
2314
1687
|
});
|
|
2315
1688
|
printBanner(actualPort, `static (${mode})`);
|
|
2316
|
-
const
|
|
2317
|
-
|
|
1689
|
+
const cleanupFallback = () => {
|
|
1690
|
+
fallback.close(() => process.exit(0));
|
|
2318
1691
|
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2319
1692
|
};
|
|
2320
|
-
process.on("SIGINT",
|
|
2321
|
-
process.on("SIGTERM",
|
|
2322
|
-
server.on("error", (err) => {
|
|
2323
|
-
logger$1.error(`Preview server error: ${err.message || err}`);
|
|
2324
|
-
process.exit(1);
|
|
2325
|
-
});
|
|
1693
|
+
process.on("SIGINT", cleanupFallback);
|
|
1694
|
+
process.on("SIGTERM", cleanupFallback);
|
|
2326
1695
|
return;
|
|
2327
1696
|
}
|
|
2328
|
-
|
|
2329
|
-
let serverFile;
|
|
2330
|
-
if (presetName === "node" || presetName === "bun" || presetName === "deno") serverFile = "server.mjs";
|
|
2331
|
-
else if (presetName === "cloudflare") {
|
|
2332
|
-
logger$1.error("Cloudflare preset preview is not supported yet. Use `wrangler dev` instead.");
|
|
2333
|
-
process.exit(1);
|
|
2334
|
-
} else {
|
|
2335
|
-
serverFile = "handler.mjs";
|
|
2336
|
-
logger$1.warn(`Preset "${presetName}" uses a fetch handler entry. Preview may not work as a standalone server.`);
|
|
2337
|
-
}
|
|
2338
|
-
const serverPath = join(cwd, outputDir, "server", serverFile);
|
|
2339
|
-
if (!existsSync(serverPath)) {
|
|
2340
|
-
logger$1.error(`Build output not found: ${serverPath}`);
|
|
2341
|
-
logger$1.info("Run `ubean build` first to create a production build.");
|
|
2342
|
-
process.exit(1);
|
|
2343
|
-
}
|
|
2344
|
-
const { child, exited } = spawnPreviewServer({
|
|
2345
|
-
serverPath,
|
|
2346
|
-
cwd,
|
|
2347
|
-
port: actualPort,
|
|
2348
|
-
host,
|
|
2349
|
-
strictPort
|
|
2350
|
-
});
|
|
2351
|
-
try {
|
|
2352
|
-
await waitForPort(actualPort, {
|
|
2353
|
-
host,
|
|
2354
|
-
retries: 40,
|
|
2355
|
-
delay: 250
|
|
2356
|
-
});
|
|
2357
|
-
} catch {
|
|
2358
|
-
if (exited.value) logger$1.error(`Preview server exited before becoming ready on port ${actualPort}.`);
|
|
2359
|
-
else logger$1.warn(`Preview server on port ${actualPort} is not responding yet, the banner may be premature.`);
|
|
2360
|
-
}
|
|
2361
|
-
printBanner(actualPort, "production");
|
|
1697
|
+
printBanner(actualPort, staticMode ? `static (${mode})` : "production");
|
|
2362
1698
|
const cleanup = () => {
|
|
2363
|
-
|
|
2364
|
-
process.exit(0);
|
|
1699
|
+
server?.close().catch(() => void 0).finally(() => process.exit(0));
|
|
1700
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2365
1701
|
};
|
|
2366
1702
|
process.on("SIGINT", cleanup);
|
|
2367
1703
|
process.on("SIGTERM", cleanup);
|
|
2368
|
-
child.on("exit", (code) => {
|
|
2369
|
-
if (code !== 0 && code !== null) logger$1.error(`Preview server exited with code ${code}`);
|
|
2370
|
-
process.exit(code ?? 0);
|
|
2371
|
-
});
|
|
2372
1704
|
}
|
|
2373
1705
|
};
|
|
2374
|
-
function spawnPreviewServer(opts) {
|
|
2375
|
-
const exited = { value: false };
|
|
2376
|
-
const child = spawn("node", [opts.serverPath], {
|
|
2377
|
-
cwd: opts.cwd,
|
|
2378
|
-
env: {
|
|
2379
|
-
...process.env,
|
|
2380
|
-
PORT: String(opts.port),
|
|
2381
|
-
HOST: opts.host
|
|
2382
|
-
},
|
|
2383
|
-
stdio: [
|
|
2384
|
-
"inherit",
|
|
2385
|
-
"inherit",
|
|
2386
|
-
"pipe"
|
|
2387
|
-
]
|
|
2388
|
-
});
|
|
2389
|
-
let addrInUseReported = false;
|
|
2390
|
-
child.stderr?.on("data", (chunk) => {
|
|
2391
|
-
const text = chunk.toString();
|
|
2392
|
-
process.stderr.write(text);
|
|
2393
|
-
if (!addrInUseReported && /EADDRINUSE/.test(text)) {
|
|
2394
|
-
addrInUseReported = true;
|
|
2395
|
-
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" : ""}.`);
|
|
2396
|
-
}
|
|
2397
|
-
});
|
|
2398
|
-
child.on("exit", () => {
|
|
2399
|
-
exited.value = true;
|
|
2400
|
-
});
|
|
2401
|
-
return {
|
|
2402
|
-
child,
|
|
2403
|
-
exited
|
|
2404
|
-
};
|
|
2405
|
-
}
|
|
2406
1706
|
//#endregion
|
|
2407
1707
|
//#region src/scaffold-commands.ts
|
|
2408
1708
|
const logger = getLogger("cli");
|