@ubean/cli 0.5.2 → 0.6.0-beta.1
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 +314 -1012
- 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 +13 -12
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,310 +443,99 @@ 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
|
+
try {
|
|
521
|
+
const { DevTools } = await import("@vitejs/devtools");
|
|
522
|
+
builtinPlugins.push(...await DevTools({ builtinDevTools: false }));
|
|
523
|
+
} catch {}
|
|
524
|
+
}
|
|
780
525
|
const { plugins } = await resolveModules({
|
|
781
526
|
cwd,
|
|
782
527
|
config,
|
|
783
528
|
builtinPlugins
|
|
784
529
|
});
|
|
785
|
-
if (devtoolsEnabled) process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = "true";
|
|
786
530
|
viteServer = await createServer$1({
|
|
787
531
|
root: cwd,
|
|
788
532
|
configFile: userViteConfig ?? false,
|
|
789
533
|
customLogger: createGatedViteLogger({ lifecycle: config.logging.lifecycle }),
|
|
790
534
|
resolve: { dedupe: ["vue", "vue-router"] },
|
|
791
535
|
server: {
|
|
792
|
-
|
|
793
|
-
|
|
536
|
+
host,
|
|
537
|
+
port: options.port,
|
|
538
|
+
strictPort
|
|
794
539
|
},
|
|
795
540
|
appType: "custom",
|
|
796
541
|
plugins,
|
|
@@ -805,98 +550,29 @@ async function createViteDevServer(options) {
|
|
|
805
550
|
"virtual:ubean-app",
|
|
806
551
|
"virtual:ubean-server",
|
|
807
552
|
"virtual:ubean-client-entry",
|
|
808
|
-
"
|
|
809
|
-
"
|
|
810
|
-
"
|
|
811
|
-
"
|
|
553
|
+
"ubean:locales",
|
|
554
|
+
"ubean:pages",
|
|
555
|
+
"ubean:routes",
|
|
556
|
+
"ubean:meta",
|
|
557
|
+
"ubean:app-config"
|
|
812
558
|
] },
|
|
813
559
|
ssr: {
|
|
814
560
|
noExternal: ["ubean", /^@ubean\//],
|
|
815
561
|
external: ["@ubean/i18n"]
|
|
816
562
|
}
|
|
817
563
|
});
|
|
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}`;
|
|
564
|
+
if (options.onScan) onDevScan(viteServer, options.onScan);
|
|
565
|
+
await viteServer.listen();
|
|
566
|
+
const address = viteServer.httpServer?.address();
|
|
567
|
+
const actualPort = address && typeof address === "object" ? address.port : options.port;
|
|
568
|
+
const url = `http://${isWildcardHost(host) ? "localhost" : host}:${actualPort}`;
|
|
569
|
+
const networkUrls = isLoopbackHost(host) ? [] : getLanAddresses().map((lanAddress) => `http://${lanAddress}:${actualPort}`);
|
|
570
|
+
options.onListen?.({
|
|
571
|
+
port: actualPort,
|
|
572
|
+
host,
|
|
573
|
+
url,
|
|
574
|
+
networkUrls
|
|
575
|
+
});
|
|
900
576
|
return {
|
|
901
577
|
get port() {
|
|
902
578
|
return actualPort;
|
|
@@ -905,213 +581,19 @@ async function createViteDevServer(options) {
|
|
|
905
581
|
return host;
|
|
906
582
|
},
|
|
907
583
|
get url() {
|
|
908
|
-
return
|
|
584
|
+
return url;
|
|
909
585
|
},
|
|
910
586
|
get viteServer() {
|
|
911
587
|
return viteServer;
|
|
912
588
|
},
|
|
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
|
-
});
|
|
589
|
+
async rescan() {
|
|
590
|
+
await rescanDev();
|
|
937
591
|
},
|
|
938
592
|
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;
|
|
593
|
+
const server = viteServer;
|
|
594
|
+
viteServer = null;
|
|
595
|
+
await server?.close();
|
|
1106
596
|
}
|
|
1107
|
-
}
|
|
1108
|
-
function addDir(dir) {
|
|
1109
|
-
watchDir(join(cwd, dir));
|
|
1110
|
-
}
|
|
1111
|
-
return {
|
|
1112
|
-
start,
|
|
1113
|
-
stop,
|
|
1114
|
-
addDir
|
|
1115
597
|
};
|
|
1116
598
|
}
|
|
1117
599
|
//#endregion
|
|
@@ -1164,20 +646,6 @@ import("@ubean/devtools").then((mod) => {
|
|
|
1164
646
|
function getCustomTabs() {
|
|
1165
647
|
return _customTabsGetter();
|
|
1166
648
|
}
|
|
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
649
|
const devCommand = {
|
|
1182
650
|
meta: {
|
|
1183
651
|
name: "dev",
|
|
@@ -1246,138 +714,102 @@ const devCommand = {
|
|
|
1246
714
|
logger$6.info(`Source directory: ${config.srcDir}`);
|
|
1247
715
|
logger$6.info(`Preset: ${preset.name}`);
|
|
1248
716
|
}
|
|
1249
|
-
const capabilities = createCapabilitySet(preset.capabilities || {});
|
|
1250
717
|
if (logging.scan) logger$6.info("Running capability diagnostics...");
|
|
1251
718
|
const diagnostics = diagnoseCapabilities(preset.name, preset.capabilities || {}, NODE_REQUIREMENTS);
|
|
1252
719
|
logDiagnostics(diagnostics, { verbose: logging.diagnostics });
|
|
1253
720
|
if (!diagnostics.valid) logger$6.warn("Some capability requirements are not met. Dev server may not function correctly.");
|
|
1254
|
-
let
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
721
|
+
let currentScanResult = await initialScan(cwd, config, logging);
|
|
722
|
+
/**
|
|
723
|
+
* 扫描后的命令级事务(RM-V13:扫描由 dev-scan 协调器驱动)。
|
|
724
|
+
*
|
|
725
|
+
* app 的重建不在这里 —— 它归 `ubeanDevRequestPlugin`(RM-V14)。CLI 负责把类型定义重新
|
|
726
|
+
* 生成,并把新扫描结果交给 DevTools。**重载不由 CLI 发**:协调器会在所有订阅者完成后统一发
|
|
727
|
+
* `full-reload`,因此浏览器不会带着旧的 `definePage` 元数据刷新。
|
|
728
|
+
*/
|
|
729
|
+
async function onScan(result, changed) {
|
|
730
|
+
if (changed.length > 0) {
|
|
731
|
+
lastChangedFile = relative(cwd, changed[0]).replace(/\\/g, "/");
|
|
732
|
+
if (logging.lifecycle) logger$6.info(`File change detected: ${lastChangedFile}`);
|
|
733
|
+
}
|
|
734
|
+
currentScanResult = result;
|
|
1259
735
|
try {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
736
|
+
await generateTypes(result, {
|
|
737
|
+
cwd,
|
|
738
|
+
srcDir: config.srcDir,
|
|
739
|
+
buildDir: ".ubean",
|
|
740
|
+
dirs: config.dir,
|
|
741
|
+
autoImports: config.autoImports,
|
|
742
|
+
components: config.components
|
|
743
|
+
});
|
|
1267
744
|
} catch (err) {
|
|
1268
745
|
logger$6.error(`Rescan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1269
|
-
} finally {
|
|
1270
|
-
rescanInProgress = false;
|
|
1271
746
|
}
|
|
1272
747
|
}
|
|
1273
|
-
let reloadStartedAt = Date.now();
|
|
1274
748
|
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
|
-
});
|
|
749
|
+
let devServer = null;
|
|
1334
750
|
try {
|
|
1335
|
-
await
|
|
751
|
+
devServer = await createDevViteServer({
|
|
752
|
+
cwd,
|
|
753
|
+
port,
|
|
754
|
+
host,
|
|
755
|
+
strictPort: args.strictPort,
|
|
756
|
+
config,
|
|
757
|
+
onScan,
|
|
758
|
+
devtools: {
|
|
759
|
+
getScanResult: () => currentScanResult,
|
|
760
|
+
getConfigMeta: () => ({
|
|
761
|
+
preset: config.build.preset,
|
|
762
|
+
rootDir: config.rootDir,
|
|
763
|
+
srcDir: config.srcDir,
|
|
764
|
+
openAPI: {
|
|
765
|
+
enabled: true,
|
|
766
|
+
scalarPath: "/_scalar",
|
|
767
|
+
openAPIPath: "/_openapi.json"
|
|
768
|
+
},
|
|
769
|
+
dir: config.dir
|
|
770
|
+
}),
|
|
771
|
+
getCustomTabs: () => getCustomTabs()
|
|
772
|
+
},
|
|
773
|
+
onListen({ url, networkUrls }) {
|
|
774
|
+
const label = (text) => dim(text);
|
|
775
|
+
const modeSuffix = dim(` (${config.mode} · ${preset.name})`);
|
|
776
|
+
const lines = [
|
|
777
|
+
`${green(bold(`🚀 ubean dev server ready in ${Date.now() - startedAt}ms`))}${modeSuffix}\n`,
|
|
778
|
+
` → ${label("Local:")} ${cyan(url)}`,
|
|
779
|
+
...(networkUrls || []).map((n) => ` → ${label("Network:")} ${cyan(n)}`)
|
|
780
|
+
];
|
|
781
|
+
const hasBackend = config.mode !== "ssg" && config.mode !== "spa";
|
|
782
|
+
if (hasBackend) lines.push(` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`, ` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`);
|
|
783
|
+
if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
|
|
784
|
+
lines.push(` → ${dim("Press Ctrl+C to stop")}`);
|
|
785
|
+
logger$6.info(lines.join("\n"));
|
|
786
|
+
if (hasBackend) generateOpenApiTypesFromServer(url, { outDir: resolve(cwd, ".ubean") }).then((filePath) => {
|
|
787
|
+
if (filePath && logging.scan) logger$6.info(`OpenAPI types generated: ${filePath}`);
|
|
788
|
+
}).catch((err) => {
|
|
789
|
+
logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
});
|
|
1336
793
|
} catch (err) {
|
|
1337
794
|
logger$6.error(err?.stack || err?.message || String(err));
|
|
1338
795
|
process.exit(1);
|
|
1339
796
|
}
|
|
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
797
|
const cleanup = async () => {
|
|
1367
798
|
if (logging.lifecycle) logger$6.info("\nShutting down...");
|
|
1368
|
-
|
|
1369
|
-
await runner.stop();
|
|
799
|
+
await devServer?.stop();
|
|
1370
800
|
process.exit(0);
|
|
1371
801
|
};
|
|
1372
802
|
process.on("SIGINT", cleanup);
|
|
1373
803
|
process.on("SIGTERM", cleanup);
|
|
1374
804
|
}
|
|
1375
805
|
};
|
|
1376
|
-
/**
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
806
|
+
/**
|
|
807
|
+
* 初始扫描 + 类型生成。
|
|
808
|
+
*
|
|
809
|
+
* RM-V14 起 CLI 不在这里建 app(由 `ubeanDevRequestPlugin` 自举),只负责扫描与 codegen;
|
|
810
|
+
* 扫描结果交给 DevTools 作启动数据,并在每次文件变更后由 `onScan` 刷新。
|
|
811
|
+
*/
|
|
812
|
+
async function initialScan(cwd, config, logging) {
|
|
1381
813
|
if (logging.scan) logger$6.info("Scanning project files...");
|
|
1382
814
|
const result = await scanProject({
|
|
1383
815
|
cwd,
|
|
@@ -1385,8 +817,10 @@ async function buildApp(cwd, config, logging) {
|
|
|
1385
817
|
dirs: config.dir,
|
|
1386
818
|
ignore: config.scanOptions?.ignore
|
|
1387
819
|
});
|
|
1388
|
-
if (logging.scan)
|
|
1389
|
-
|
|
820
|
+
if (logging.scan) {
|
|
821
|
+
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`);
|
|
822
|
+
logger$6.info("Generating type definitions...");
|
|
823
|
+
}
|
|
1390
824
|
await generateTypes(result, {
|
|
1391
825
|
cwd,
|
|
1392
826
|
srcDir: config.srcDir,
|
|
@@ -1395,59 +829,12 @@ async function buildApp(cwd, config, logging) {
|
|
|
1395
829
|
autoImports: config.autoImports,
|
|
1396
830
|
components: config.components
|
|
1397
831
|
});
|
|
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
|
-
};
|
|
832
|
+
return result;
|
|
1450
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* RM-V14:请求日志(`logging.request`)不再由 CLI 实现 —— 见 `@ubean/build` 的
|
|
836
|
+
* `createDevApp({ logger })`。配置域行为放 builder,两条 dev 路径输出一致。
|
|
837
|
+
*/
|
|
1451
838
|
//#endregion
|
|
1452
839
|
//#region src/devtools.ts
|
|
1453
840
|
const logger$5 = getLogger("cli");
|
|
@@ -2152,89 +1539,38 @@ const prepareCommand = {
|
|
|
2152
1539
|
};
|
|
2153
1540
|
//#endregion
|
|
2154
1541
|
//#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
1542
|
/**
|
|
2177
|
-
*
|
|
1543
|
+
* 静态文件服务器(spa / ssg 的降级路径)。
|
|
2178
1544
|
*
|
|
2179
|
-
* -
|
|
2180
|
-
*
|
|
1545
|
+
* RM-V25:判定规则不再自己写一份,改用 `@ubean/build/vite` 的 `resolvePreviewFile` —— 插件接管
|
|
1546
|
+
* `vite preview` 时用的是同一个函数,两套方言必然分叉(文件名含合法 `...` 的产物曾被
|
|
1547
|
+
* `includes('..')` 判成路径穿越)。
|
|
2181
1548
|
*
|
|
2182
|
-
*
|
|
1549
|
+
* 主路径已改为 `vite preview` + 插件中间件(RM-V24);本函数保留给「Vite 预览起不来」的
|
|
1550
|
+
* spa / ssg 场景(静态产物不需要服务端能力),以及既有回归测试。
|
|
2183
1551
|
*/
|
|
2184
1552
|
function startStaticServer(opts) {
|
|
2185
1553
|
const { root, port, host, mode } = opts;
|
|
2186
|
-
const spaFallback = mode === "spa";
|
|
2187
1554
|
return createServer(async (req, res) => {
|
|
2188
1555
|
try {
|
|
2189
1556
|
const url = new URL(req.url || "/", `http://${host}`);
|
|
2190
1557
|
const pathname = decodeURIComponent(url.pathname);
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
if (filePath !== rootPath && !filePath.startsWith(rootPath + sep)) {
|
|
1558
|
+
const resolvedFile = resolvePreviewFile(root, pathname, mode);
|
|
1559
|
+
if (resolvedFile.kind === "forbidden") {
|
|
2194
1560
|
res.statusCode = 400;
|
|
1561
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2195
1562
|
res.end("Bad Request");
|
|
2196
1563
|
return;
|
|
2197
1564
|
}
|
|
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) {
|
|
1565
|
+
if (resolvedFile.kind === "missing") {
|
|
2228
1566
|
res.statusCode = 404;
|
|
2229
1567
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2230
1568
|
res.end("Not Found");
|
|
2231
1569
|
return;
|
|
2232
1570
|
}
|
|
2233
|
-
const
|
|
2234
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2235
|
-
const body = await readFile(filePath);
|
|
1571
|
+
const body = await readFile(resolvedFile.path);
|
|
2236
1572
|
res.statusCode = 200;
|
|
2237
|
-
res.setHeader("Content-Type",
|
|
1573
|
+
res.setHeader("Content-Type", previewMimeType(resolvedFile.path));
|
|
2238
1574
|
res.setHeader("Content-Length", body.length);
|
|
2239
1575
|
res.end(body);
|
|
2240
1576
|
} catch (err) {
|
|
@@ -2268,6 +1604,10 @@ const previewCommand = {
|
|
|
2268
1604
|
type: "string",
|
|
2269
1605
|
description: "Project root directory",
|
|
2270
1606
|
default: "."
|
|
1607
|
+
},
|
|
1608
|
+
outDir: {
|
|
1609
|
+
type: "string",
|
|
1610
|
+
description: "Output directory to preview (overrides ubean.config.ts build.outputDir)"
|
|
2271
1611
|
}
|
|
2272
1612
|
},
|
|
2273
1613
|
async run({ args }) {
|
|
@@ -2277,6 +1617,7 @@ const previewCommand = {
|
|
|
2277
1617
|
const config = await loadUbeanConfig(cwd);
|
|
2278
1618
|
const preset = resolvePresetByName(config.build.preset);
|
|
2279
1619
|
logger$1.info(`Preset: ${preset.name}`);
|
|
1620
|
+
if (args.outDir) config.build.outputDir = String(args.outDir);
|
|
2280
1621
|
const outputDir = config.build.outputDir || "dist";
|
|
2281
1622
|
const mode = config.mode;
|
|
2282
1623
|
const host = args.host || config.preview.host;
|
|
@@ -2299,110 +1640,71 @@ const previewCommand = {
|
|
|
2299
1640
|
const bannerUrl = `http://${host}:${port}`;
|
|
2300
1641
|
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
1642
|
};
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
1643
|
+
const staticMode = mode === "spa" || mode === "ssg";
|
|
1644
|
+
const isCloudflare = preset.name === "cloudflare";
|
|
1645
|
+
const staticRoot = join(cwd, outputDir, "public");
|
|
1646
|
+
const requiredArtifact = staticMode ? join(staticRoot, "index.html") : isCloudflare ? join(cwd, outputDir, "server", "worker.mjs") : join(cwd, outputDir, "server", "entry.mjs");
|
|
1647
|
+
if (!existsSync(requiredArtifact)) {
|
|
1648
|
+
logger$1.error(`Build output not found: ${requiredArtifact}`);
|
|
1649
|
+
logger$1.info("Run `ubean build` first to create a production build.");
|
|
1650
|
+
process.exit(1);
|
|
1651
|
+
}
|
|
1652
|
+
if (isCloudflare) {
|
|
1653
|
+
const probe = await createCloudflarePreviewRunner({ workerPath: requiredArtifact });
|
|
1654
|
+
if (!probe.ok) {
|
|
1655
|
+
logger$1.error(probe.message);
|
|
1656
|
+
process.exit(1);
|
|
1657
|
+
}
|
|
1658
|
+
await probe.runner.dispose();
|
|
1659
|
+
}
|
|
1660
|
+
const userViteConfig = findUserViteConfig(cwd);
|
|
1661
|
+
let server;
|
|
1662
|
+
try {
|
|
1663
|
+
server = await preview({
|
|
1664
|
+
root: cwd,
|
|
1665
|
+
configFile: userViteConfig ?? false,
|
|
1666
|
+
mode: "production",
|
|
1667
|
+
logLevel: "warn",
|
|
1668
|
+
preview: {
|
|
1669
|
+
port: actualPort,
|
|
1670
|
+
host,
|
|
1671
|
+
strictPort
|
|
1672
|
+
},
|
|
1673
|
+
...userViteConfig ? {} : { plugins: [ubeanPreviewPlugin({
|
|
1674
|
+
config,
|
|
1675
|
+
host
|
|
1676
|
+
})] }
|
|
1677
|
+
});
|
|
1678
|
+
} catch (err) {
|
|
1679
|
+
if (!staticMode) {
|
|
1680
|
+
logger$1.error(`Failed to start the preview server: ${err instanceof Error ? err.message : String(err)}`);
|
|
2307
1681
|
process.exit(1);
|
|
2308
1682
|
}
|
|
2309
|
-
|
|
1683
|
+
logger$1.warn(`Vite preview unavailable (${err instanceof Error ? err.message : String(err)}); falling back to the built-in static server.`);
|
|
1684
|
+
const fallback = startStaticServer({
|
|
2310
1685
|
root: staticRoot,
|
|
2311
1686
|
port: actualPort,
|
|
2312
1687
|
host,
|
|
2313
1688
|
mode
|
|
2314
1689
|
});
|
|
2315
1690
|
printBanner(actualPort, `static (${mode})`);
|
|
2316
|
-
const
|
|
2317
|
-
|
|
1691
|
+
const cleanupFallback = () => {
|
|
1692
|
+
fallback.close(() => process.exit(0));
|
|
2318
1693
|
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2319
1694
|
};
|
|
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
|
-
});
|
|
1695
|
+
process.on("SIGINT", cleanupFallback);
|
|
1696
|
+
process.on("SIGTERM", cleanupFallback);
|
|
2326
1697
|
return;
|
|
2327
1698
|
}
|
|
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");
|
|
1699
|
+
printBanner(actualPort, staticMode ? `static (${mode})` : "production");
|
|
2362
1700
|
const cleanup = () => {
|
|
2363
|
-
|
|
2364
|
-
process.exit(0);
|
|
1701
|
+
server?.close().catch(() => void 0).finally(() => process.exit(0));
|
|
1702
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2365
1703
|
};
|
|
2366
1704
|
process.on("SIGINT", cleanup);
|
|
2367
1705
|
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
1706
|
}
|
|
2373
1707
|
};
|
|
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
1708
|
//#endregion
|
|
2407
1709
|
//#region src/scaffold-commands.ts
|
|
2408
1710
|
const logger = getLogger("cli");
|