@ubean/cli 0.5.1 → 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 +363 -966
- 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 {
|
|
5
|
-
import { getLogger, setDebugLogging } from "@ubean/shared/logger";
|
|
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,283 +443,99 @@ const buildCommand = {
|
|
|
487
443
|
}
|
|
488
444
|
};
|
|
489
445
|
//#endregion
|
|
490
|
-
//#region src/dev-server/vite
|
|
491
|
-
const logger$8 = getLogger("dev-server");
|
|
492
|
-
/**
|
|
493
|
-
* Compute the vue-router locale param (e.g. `:locale(zh)?`) from the resolved
|
|
494
|
-
* i18n routing config, mirroring `localeVueParamFromI18n` (`@ubean/build`).
|
|
495
|
-
* Returns `''` when i18n is disabled or no prefix is needed.
|
|
496
|
-
*/
|
|
497
|
-
function resolveLocaleVueParam(config) {
|
|
498
|
-
if (!config?.enabled || !config.locales?.length) return "";
|
|
499
|
-
return getVueLocaleParam({
|
|
500
|
-
defaultLocale: config.defaultLocale,
|
|
501
|
-
locales: config.locales.map((l) => l.code),
|
|
502
|
-
strategy: config.strategy
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
async function toWebRequest(req, host, protocol) {
|
|
506
|
-
const url = `${protocol}://${req.headers.host || host}${req.url || "/"}`;
|
|
507
|
-
const headers = new Headers();
|
|
508
|
-
for (const [key, value] of Object.entries(req.headers)) if (value) {
|
|
509
|
-
if (Array.isArray(value)) for (const v of value) headers.append(key, v);
|
|
510
|
-
else headers.set(key, value);
|
|
511
|
-
}
|
|
512
|
-
const method = req.method || "GET";
|
|
513
|
-
return new Request(url, {
|
|
514
|
-
method,
|
|
515
|
-
headers,
|
|
516
|
-
body: method === "GET" || method === "HEAD" ? void 0 : req,
|
|
517
|
-
duplex: "half"
|
|
518
|
-
});
|
|
519
|
-
}
|
|
520
|
-
async function sendWebResponse(res, webRes) {
|
|
521
|
-
res.statusCode = webRes.status;
|
|
522
|
-
res.statusMessage = webRes.statusText;
|
|
523
|
-
webRes.headers.forEach((value, key) => {
|
|
524
|
-
res.setHeader(key, value);
|
|
525
|
-
});
|
|
526
|
-
if (webRes.body) {
|
|
527
|
-
const reader = webRes.body.getReader();
|
|
528
|
-
try {
|
|
529
|
-
while (true) {
|
|
530
|
-
const { done, value } = await reader.read();
|
|
531
|
-
if (done) break;
|
|
532
|
-
res.write(value);
|
|
533
|
-
}
|
|
534
|
-
} finally {
|
|
535
|
-
reader.releaseLock();
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
res.end();
|
|
539
|
-
}
|
|
540
|
-
function loadScaffoldOps() {
|
|
541
|
-
return {
|
|
542
|
-
createFsOps,
|
|
543
|
-
scaffold,
|
|
544
|
-
deleteScaffold,
|
|
545
|
-
recoverScaffold
|
|
546
|
-
};
|
|
547
|
-
}
|
|
548
|
-
/** Vite's browser-facing prefix for virtual module ids (see `wrapId`). */
|
|
549
|
-
const VALID_ID_PREFIX = "/@id/";
|
|
550
|
-
/** Vite's URL-safe encoding of the `\0` virtual-module marker. */
|
|
551
|
-
const NULL_BYTE_PLACEHOLDER = "__x00__";
|
|
446
|
+
//#region src/dev-server/dev-vite.ts
|
|
552
447
|
/**
|
|
553
|
-
*
|
|
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 数据上报)。
|
|
554
456
|
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
558
|
-
* in `transformIndexHtml`. Mirror Vite's own import-analysis rewriting
|
|
559
|
-
* (`wrapId`: `/@id/` prefix + `\0` → `__x00__`) so the href stays valid
|
|
560
|
-
* and resolvable by the dev server.
|
|
457
|
+
* 保留两处 CLI 专有接线(无法下沉):`@ubean/devtools` / `@vitejs/devtools` 的按需加载
|
|
458
|
+
* (可选依赖 + `config.devtools.enabled` 闸门),以及 DevTools 的 scaffold CRUD
|
|
459
|
+
* (页面增删改是 CLI 的能力)。
|
|
561
460
|
*/
|
|
562
|
-
|
|
563
|
-
return url.includes("\0") ? `${VALID_ID_PREFIX}${url.replace(/\0/g, NULL_BYTE_PLACEHOLDER)}` : url;
|
|
564
|
-
}
|
|
461
|
+
const logger$8 = getLogger("dev-server");
|
|
565
462
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
* In dev, CSS imported from JS (virtual uno.css entry, global.css, font css)
|
|
569
|
-
* is only applied once the client entry module graph has fully loaded and
|
|
570
|
-
* executed — SSR HTML paints unstyled for that gap (FOUC). Injecting the same
|
|
571
|
-
* CSS modules as blocking `<link>` tags lets the browser fetch them in
|
|
572
|
-
* parallel with the JS graph, so the first paint is styled.
|
|
573
|
-
*
|
|
574
|
-
* `?direct` tells Vite's transform middleware to return raw CSS
|
|
575
|
-
* (`Content-Type: text/css`) instead of the JS style-injection wrapper.
|
|
576
|
-
* Modules whose URL already carries a query (SFC `<style>` blocks such as
|
|
577
|
-
* `*.vue?vue&type=style&...`) cannot be served this way and are skipped.
|
|
463
|
+
* 收编 Vite 原生日志到 ubean 分类闸门:`lifecycle` 关闭时(默认)info 降为 debug,
|
|
464
|
+
* warn/error 原样保留(这两级永远可见)。
|
|
578
465
|
*/
|
|
579
|
-
function
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
function injectStylesheetLinks(html, hrefs) {
|
|
590
|
-
if (!hrefs.length || !html.includes("</head>")) return html;
|
|
591
|
-
const tags = hrefs.map((href) => `<link rel="stylesheet" href="${href}">`).join("");
|
|
592
|
-
return html.replace("</head>", `${tags}</head>`);
|
|
466
|
+
function createGatedViteLogger(logging) {
|
|
467
|
+
return {
|
|
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: () => {},
|
|
473
|
+
hasWarned: false,
|
|
474
|
+
...logging.lifecycle ? { info: (msg) => logger$8.info(msg) } : {}
|
|
475
|
+
};
|
|
593
476
|
}
|
|
594
|
-
async function
|
|
595
|
-
const { cwd, config,
|
|
596
|
-
const
|
|
597
|
-
const strictPort = options.strictPort ?? false;
|
|
598
|
-
let currentApp = initialApp;
|
|
599
|
-
let currentLayouts = initialLayouts;
|
|
600
|
-
let httpServer = null;
|
|
601
|
-
let viteServer = null;
|
|
602
|
-
let serverConfigApplied = false;
|
|
603
|
-
let serverReadyCalled = false;
|
|
604
|
-
let cachedServerConfig = null;
|
|
605
|
-
let refreshDevtools = null;
|
|
606
|
-
const requestedPort = options.port;
|
|
607
|
-
let actualPort;
|
|
608
|
-
try {
|
|
609
|
-
actualPort = await findAvailablePort(requestedPort, {
|
|
610
|
-
host,
|
|
611
|
-
strictPort
|
|
612
|
-
});
|
|
613
|
-
} catch (err) {
|
|
614
|
-
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 });
|
|
615
|
-
throw err;
|
|
616
|
-
}
|
|
617
|
-
if (actualPort !== requestedPort) logger$8.warn(`Port ${requestedPort} is in use, trying ${actualPort} instead.`);
|
|
618
|
-
httpServer = createServer(async (req, res) => {
|
|
619
|
-
try {
|
|
620
|
-
const pathname = (req.url || "/").split("?")[0].split("#")[0];
|
|
621
|
-
if (devtoolsEnabled && (pathname === "/_devtools" || pathname === "/_devtools/")) {
|
|
622
|
-
res.statusCode = 302;
|
|
623
|
-
res.setHeader("Location", "/__devtools/");
|
|
624
|
-
res.end();
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
if (!viteServer) {
|
|
628
|
-
res.statusCode = 503;
|
|
629
|
-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
630
|
-
res.end("ubean dev server is shutting down");
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
await new Promise((resolve, reject) => {
|
|
634
|
-
viteServer.middlewares(req, res, (err) => {
|
|
635
|
-
if (err) reject(err);
|
|
636
|
-
else resolve();
|
|
637
|
-
});
|
|
638
|
-
});
|
|
639
|
-
if (res.writableEnded) return;
|
|
640
|
-
try {
|
|
641
|
-
await viteServer.ssrLoadModule("ubean:locales");
|
|
642
|
-
} catch (err) {
|
|
643
|
-
logger$8.warn("[ubean] Failed to resolve locales module:", err);
|
|
644
|
-
}
|
|
645
|
-
if (!serverConfigApplied) {
|
|
646
|
-
serverConfigApplied = true;
|
|
647
|
-
try {
|
|
648
|
-
const serverMod = await viteServer.ssrLoadModule("virtual:ubean-server");
|
|
649
|
-
if (serverMod?.resolveServerConfig) {
|
|
650
|
-
cachedServerConfig = serverMod.resolveServerConfig("dev");
|
|
651
|
-
await applyServerConfig(currentApp, cachedServerConfig);
|
|
652
|
-
}
|
|
653
|
-
} catch (err) {
|
|
654
|
-
logger$8.warn("[ubean] Failed to load server config:", err);
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
await currentApp.init();
|
|
658
|
-
if (!serverReadyCalled) {
|
|
659
|
-
serverReadyCalled = true;
|
|
660
|
-
if (cachedServerConfig?.onServerReady) try {
|
|
661
|
-
await cachedServerConfig.onServerReady(currentApp);
|
|
662
|
-
} catch (err) {
|
|
663
|
-
logger$8.warn("[ubean] onServerReady error:", err);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
const protocol = req.socket?.encrypted ? "https" : "http";
|
|
667
|
-
const webReq = await toWebRequest(req, host, protocol);
|
|
668
|
-
const webRes = await currentApp.fetch(webReq);
|
|
669
|
-
const contentType = webRes.headers.get("content-type") || "";
|
|
670
|
-
const skipTransform = (req.url || "").startsWith("/_devtools");
|
|
671
|
-
if (contentType.includes("text/html") && webRes.body && !skipTransform) {
|
|
672
|
-
const htmlWithCss = injectStylesheetLinks(await webRes.text(), collectDevCssLinks(viteServer.moduleGraph));
|
|
673
|
-
const transformedHtml = await viteServer.transformIndexHtml(req.url || "/", htmlWithCss);
|
|
674
|
-
res.statusCode = webRes.status;
|
|
675
|
-
res.statusMessage = webRes.statusText;
|
|
676
|
-
webRes.headers.forEach((value, key) => {
|
|
677
|
-
res.setHeader(key, value);
|
|
678
|
-
});
|
|
679
|
-
res.end(transformedHtml);
|
|
680
|
-
} else await sendWebResponse(res, webRes);
|
|
681
|
-
} catch (err) {
|
|
682
|
-
if (viteServer) viteServer.ssrFixStacktrace(err);
|
|
683
|
-
if (!res.headersSent) {
|
|
684
|
-
res.statusCode = 500;
|
|
685
|
-
const errorHtml = `<!DOCTYPE html>
|
|
686
|
-
<html>
|
|
687
|
-
<head>
|
|
688
|
-
<title>Server Error</title>
|
|
689
|
-
<style>
|
|
690
|
-
body { font-family: monospace; padding: 2rem; background: #1a1a1a; color: #ff6b6b; }
|
|
691
|
-
pre { background: #2d2d2d; padding: 1rem; border-radius: 4px; overflow-x: auto; }
|
|
692
|
-
</style>
|
|
693
|
-
</head>
|
|
694
|
-
<body>
|
|
695
|
-
<h1>Internal Server Error</h1>
|
|
696
|
-
<pre>${err.stack || err.message}</pre>
|
|
697
|
-
</body>
|
|
698
|
-
</html>`;
|
|
699
|
-
res.setHeader("Content-Type", "text/html");
|
|
700
|
-
res.end(errorHtml);
|
|
701
|
-
} else if (!res.writableEnded) res.end(err instanceof Error ? err.message : "Internal Server Error");
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
const devtoolsEnabled = options.config.devtools.enabled;
|
|
705
|
-
let devtoolsPlugin = null;
|
|
706
|
-
if (devtoolsEnabled) try {
|
|
707
|
-
const { ubeanDevtoolsPlugin } = await import("@ubean/devtools");
|
|
708
|
-
const devtoolsOpts = options.devtools;
|
|
709
|
-
const scaffoldOps = await loadScaffoldOps();
|
|
710
|
-
devtoolsPlugin = ubeanDevtoolsPlugin({
|
|
711
|
-
getCwd: () => cwd,
|
|
712
|
-
getApp: () => currentApp,
|
|
713
|
-
...scaffoldOps ? { scaffoldOps } : {},
|
|
714
|
-
...devtoolsOpts?.getScanResult ? { getScanResult: devtoolsOpts.getScanResult } : {},
|
|
715
|
-
...devtoolsOpts?.getConfigMeta ? { getConfigMeta: devtoolsOpts.getConfigMeta } : {},
|
|
716
|
-
...devtoolsOpts?.getCustomTabs ? { getCustomTabs: devtoolsOpts.getCustomTabs } : {},
|
|
717
|
-
...devtoolsOpts?.ai ? { ai: devtoolsOpts.ai } : {},
|
|
718
|
-
...devtoolsOpts?.getApp ? { getApp: devtoolsOpts.getApp } : {},
|
|
719
|
-
...devtoolsOpts?.triggerRescan ? { triggerRescan: devtoolsOpts.triggerRescan } : {},
|
|
720
|
-
registerRefresh: (fn) => {
|
|
721
|
-
refreshDevtools = fn;
|
|
722
|
-
}
|
|
723
|
-
});
|
|
724
|
-
} catch {}
|
|
725
|
-
let viteDevtoolsPlugins = [];
|
|
726
|
-
if (devtoolsEnabled) try {
|
|
727
|
-
const { DevTools } = await import("@vitejs/devtools");
|
|
728
|
-
// @vitejs/devtools 也解析到含 @vitejs+devtools peer 上下文的 vite-plus-core
|
|
729
|
-
viteDevtoolsPlugins = await DevTools({ builtinDevTools: false });
|
|
730
|
-
} catch {}
|
|
477
|
+
async function createDevViteServer(options) {
|
|
478
|
+
const { cwd, config, host, strictPort = false } = options;
|
|
479
|
+
const devtoolsEnabled = config.devtools.enabled;
|
|
731
480
|
const userViteConfig = findUserViteConfig(cwd);
|
|
732
481
|
const hasUserViteConfig = !!userViteConfig;
|
|
733
482
|
const isBackendMode = config.mode === "backend";
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
+
}
|
|
754
525
|
const { plugins } = await resolveModules({
|
|
755
526
|
cwd,
|
|
756
527
|
config,
|
|
757
528
|
builtinPlugins
|
|
758
529
|
});
|
|
759
|
-
if (devtoolsEnabled) process.env.VITE_DEVTOOLS_DISABLE_CLIENT_AUTH = "true";
|
|
760
530
|
viteServer = await createServer$1({
|
|
761
531
|
root: cwd,
|
|
762
532
|
configFile: userViteConfig ?? false,
|
|
533
|
+
customLogger: createGatedViteLogger({ lifecycle: config.logging.lifecycle }),
|
|
763
534
|
resolve: { dedupe: ["vue", "vue-router"] },
|
|
764
535
|
server: {
|
|
765
|
-
|
|
766
|
-
|
|
536
|
+
host,
|
|
537
|
+
port: options.port,
|
|
538
|
+
strictPort
|
|
767
539
|
},
|
|
768
540
|
appType: "custom",
|
|
769
541
|
plugins,
|
|
@@ -778,98 +550,29 @@ async function createViteDevServer(options) {
|
|
|
778
550
|
"virtual:ubean-app",
|
|
779
551
|
"virtual:ubean-server",
|
|
780
552
|
"virtual:ubean-client-entry",
|
|
781
|
-
"
|
|
782
|
-
"
|
|
783
|
-
"
|
|
784
|
-
"
|
|
553
|
+
"ubean:locales",
|
|
554
|
+
"ubean:pages",
|
|
555
|
+
"ubean:routes",
|
|
556
|
+
"ubean:meta",
|
|
557
|
+
"ubean:app-config"
|
|
785
558
|
] },
|
|
786
559
|
ssr: {
|
|
787
560
|
noExternal: ["ubean", /^@ubean\//],
|
|
788
561
|
external: ["@ubean/i18n"]
|
|
789
562
|
}
|
|
790
563
|
});
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
for (const page of app.options.pages || []) {
|
|
804
|
-
const fullPath = (page.isReuse && page.reuseTarget ? pageByName.get(page.reuseTarget) : void 0)?.fullPath || page.fullPath;
|
|
805
|
-
pageLoaders[page.relativePath] = () => viteServer.ssrLoadModule(fullPath);
|
|
806
|
-
}
|
|
807
|
-
app.options.pageLoaders = pageLoaders;
|
|
808
|
-
const middlewareLoaders = {};
|
|
809
|
-
for (const mw of app.options.middleware || []) {
|
|
810
|
-
const fullPath = mw.fullPath;
|
|
811
|
-
middlewareLoaders[mw.relativePath] = () => viteServer.ssrLoadModule(fullPath);
|
|
812
|
-
}
|
|
813
|
-
app.options.middlewareLoaders = middlewareLoaders;
|
|
814
|
-
const cronFiles = app.options.crons || [];
|
|
815
|
-
if (cronFiles.length > 0) Promise.all(cronFiles.map((c) => viteServer.ssrLoadModule(c.fullPath))).then(async () => {
|
|
816
|
-
(await import("@ubean/server/cron")).startCronScheduler();
|
|
817
|
-
}).catch((err) => {
|
|
818
|
-
console.error("[ubean] Failed to load cron files:", err);
|
|
819
|
-
});
|
|
820
|
-
const layoutMap = /* @__PURE__ */ new Map();
|
|
821
|
-
for (const layout of layouts) layoutMap.set(layout.name, layout.fullPath);
|
|
822
|
-
const defaultLayout = layouts.find((l) => l.isDefault)?.name || null;
|
|
823
|
-
if (config.mode === "backend") {
|
|
824
|
-
app.resetInit();
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
const scannedPages = app.options.pages || [];
|
|
828
|
-
const ssrLocaleParam = resolveLocaleVueParam(config.i18n);
|
|
829
|
-
const scannedPageByName = /* @__PURE__ */ new Map();
|
|
830
|
-
for (const p of scannedPages) scannedPageByName.set(p.name, p);
|
|
831
|
-
const routes = scannedPages.map((p) => {
|
|
832
|
-
const componentFullPath = (p.isReuse && p.reuseTarget ? scannedPageByName.get(p.reuseTarget) : void 0)?.fullPath || p.fullPath;
|
|
833
|
-
const routePath = p.route.replace(/\*\*:(\w[\w-]*)/g, ":$1(.*)*");
|
|
834
|
-
return {
|
|
835
|
-
path: ssrLocaleParam ? toVueRouterLocalePath(routePath, ssrLocaleParam) : routePath,
|
|
836
|
-
name: p.name,
|
|
837
|
-
component: async () => {
|
|
838
|
-
const mod = await viteServer.ssrLoadModule(componentFullPath);
|
|
839
|
-
return mod.default || mod;
|
|
840
|
-
},
|
|
841
|
-
meta: {
|
|
842
|
-
layout: p.layout === false ? false : p.layout || defaultLayout,
|
|
843
|
-
pageName: p.name,
|
|
844
|
-
cache: p.cache === true ? true : void 0
|
|
845
|
-
}
|
|
846
|
-
};
|
|
847
|
-
});
|
|
848
|
-
let appConfigModule = null;
|
|
849
|
-
const getAppConfigModule = () => {
|
|
850
|
-
if (!appConfigModule) appConfigModule = viteServer.ssrLoadModule("virtual:ubean-app");
|
|
851
|
-
return appConfigModule;
|
|
852
|
-
};
|
|
853
|
-
app.options.pageRenderer = createVueRenderer({
|
|
854
|
-
routes,
|
|
855
|
-
async resolveLayoutComponent(name) {
|
|
856
|
-
if (name === false || name == null) return null;
|
|
857
|
-
const fullPath = layoutMap.get(name);
|
|
858
|
-
if (!fullPath) return null;
|
|
859
|
-
const mod = await viteServer.ssrLoadModule(fullPath);
|
|
860
|
-
return mod.default || mod;
|
|
861
|
-
},
|
|
862
|
-
defaultLayout,
|
|
863
|
-
async resolveAppConfig() {
|
|
864
|
-
return (await getAppConfigModule()).resolveAppConfig("server");
|
|
865
|
-
}
|
|
866
|
-
});
|
|
867
|
-
app.resetInit();
|
|
868
|
-
}
|
|
869
|
-
enhanceAppWithVite(currentApp, currentLayouts);
|
|
870
|
-
const displayHost = isWildcardHost(host) ? "localhost" : host;
|
|
871
|
-
const getNetworkUrls = () => isLoopbackHost(host) ? [] : getLanAddresses().map((address) => `http://${address}:${actualPort}`);
|
|
872
|
-
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
|
+
});
|
|
873
576
|
return {
|
|
874
577
|
get port() {
|
|
875
578
|
return actualPort;
|
|
@@ -878,213 +581,19 @@ async function createViteDevServer(options) {
|
|
|
878
581
|
return host;
|
|
879
582
|
},
|
|
880
583
|
get url() {
|
|
881
|
-
return
|
|
584
|
+
return url;
|
|
882
585
|
},
|
|
883
586
|
get viteServer() {
|
|
884
587
|
return viteServer;
|
|
885
588
|
},
|
|
886
|
-
async
|
|
887
|
-
await
|
|
888
|
-
const tryListen = (port) => {
|
|
889
|
-
httpServer.removeAllListeners("error");
|
|
890
|
-
httpServer.once("error", (err) => {
|
|
891
|
-
if (err.code === "EADDRINUSE" && !strictPort) {
|
|
892
|
-
logger$8.warn(`Port ${port} is in use, trying ${port + 1} instead.`);
|
|
893
|
-
tryListen(port + 1);
|
|
894
|
-
} else reject(err);
|
|
895
|
-
});
|
|
896
|
-
httpServer.listen(port, host, () => {
|
|
897
|
-
const addr = httpServer.address();
|
|
898
|
-
actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
899
|
-
resolve();
|
|
900
|
-
});
|
|
901
|
-
};
|
|
902
|
-
tryListen(actualPort);
|
|
903
|
-
});
|
|
904
|
-
options.onListen?.({
|
|
905
|
-
port: actualPort,
|
|
906
|
-
host,
|
|
907
|
-
url: getUrl(),
|
|
908
|
-
networkUrls: getNetworkUrls()
|
|
909
|
-
});
|
|
589
|
+
async rescan() {
|
|
590
|
+
await rescanDev();
|
|
910
591
|
},
|
|
911
592
|
async stop() {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
await new Promise((resolve) => {
|
|
916
|
-
server.close(() => resolve());
|
|
917
|
-
server.closeAllConnections();
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
if (viteServer) {
|
|
921
|
-
await viteServer.close();
|
|
922
|
-
viteServer = null;
|
|
923
|
-
}
|
|
924
|
-
},
|
|
925
|
-
updateApp(app, layouts) {
|
|
926
|
-
currentApp = app;
|
|
927
|
-
if (layouts) currentLayouts = layouts;
|
|
928
|
-
serverConfigApplied = false;
|
|
929
|
-
serverReadyCalled = false;
|
|
930
|
-
cachedServerConfig = null;
|
|
931
|
-
enhanceAppWithVite(app, currentLayouts);
|
|
932
|
-
refreshDevtools?.();
|
|
933
|
-
},
|
|
934
|
-
sendFullReload() {
|
|
935
|
-
if (viteServer) viteServer.ws.send({ type: "full-reload" });
|
|
936
|
-
}
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
//#endregion
|
|
940
|
-
//#region src/dev-server/runner.ts
|
|
941
|
-
var ViteNodeDevRunner = class {
|
|
942
|
-
viteDevServer = null;
|
|
943
|
-
currentApp;
|
|
944
|
-
currentLayouts;
|
|
945
|
-
options;
|
|
946
|
-
_port;
|
|
947
|
-
constructor(options) {
|
|
948
|
-
this.options = options;
|
|
949
|
-
this.currentApp = options.app;
|
|
950
|
-
this.currentLayouts = options.layouts || [];
|
|
951
|
-
this._port = options.port;
|
|
952
|
-
}
|
|
953
|
-
get port() {
|
|
954
|
-
return this._port;
|
|
955
|
-
}
|
|
956
|
-
get host() {
|
|
957
|
-
return this.options.host;
|
|
958
|
-
}
|
|
959
|
-
get url() {
|
|
960
|
-
return `http://${this.options.host}:${this._port}`;
|
|
961
|
-
}
|
|
962
|
-
get preset() {
|
|
963
|
-
return this.options.preset;
|
|
964
|
-
}
|
|
965
|
-
async start() {
|
|
966
|
-
this.viteDevServer = await createViteDevServer({
|
|
967
|
-
cwd: this.options.cwd,
|
|
968
|
-
port: this._port,
|
|
969
|
-
host: this.options.host,
|
|
970
|
-
strictPort: this.options.strictPort,
|
|
971
|
-
config: this.options.config,
|
|
972
|
-
app: this.currentApp,
|
|
973
|
-
layouts: this.currentLayouts,
|
|
974
|
-
devtools: this.options.devtools,
|
|
975
|
-
onListen: ({ port, host, url, networkUrls }) => {
|
|
976
|
-
this._port = port;
|
|
977
|
-
this.options.onListen?.({
|
|
978
|
-
port,
|
|
979
|
-
host,
|
|
980
|
-
url,
|
|
981
|
-
networkUrls
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
});
|
|
985
|
-
await this.viteDevServer.start();
|
|
986
|
-
}
|
|
987
|
-
async stop() {
|
|
988
|
-
if (this.viteDevServer) {
|
|
989
|
-
await this.viteDevServer.stop();
|
|
990
|
-
this.viteDevServer = null;
|
|
593
|
+
const server = viteServer;
|
|
594
|
+
viteServer = null;
|
|
595
|
+
await server?.close();
|
|
991
596
|
}
|
|
992
|
-
}
|
|
993
|
-
async reload() {
|
|
994
|
-
await this.options.onBeforeReload?.();
|
|
995
|
-
if (this.viteDevServer) this.viteDevServer.updateApp(this.currentApp, this.currentLayouts);
|
|
996
|
-
await this.options.onAfterReload?.();
|
|
997
|
-
}
|
|
998
|
-
updateApp(app, layouts) {
|
|
999
|
-
this.currentApp = app;
|
|
1000
|
-
if (layouts) this.currentLayouts = layouts;
|
|
1001
|
-
if (this.viteDevServer) this.viteDevServer.updateApp(app, this.currentLayouts);
|
|
1002
|
-
}
|
|
1003
|
-
sendFullReload() {
|
|
1004
|
-
this.viteDevServer?.sendFullReload();
|
|
1005
|
-
}
|
|
1006
|
-
};
|
|
1007
|
-
const runners = [{
|
|
1008
|
-
name: "vite-node",
|
|
1009
|
-
isAvailable() {
|
|
1010
|
-
return typeof process !== "undefined" && !!process.versions?.node;
|
|
1011
|
-
},
|
|
1012
|
-
async createRunner(options) {
|
|
1013
|
-
return new ViteNodeDevRunner(options);
|
|
1014
|
-
}
|
|
1015
|
-
}];
|
|
1016
|
-
async function selectRunner(_preset) {
|
|
1017
|
-
for (const runner of runners) if (await runner.isAvailable()) return runner;
|
|
1018
|
-
return null;
|
|
1019
|
-
}
|
|
1020
|
-
async function createDevRunner(options) {
|
|
1021
|
-
const runner = await selectRunner(options.preset);
|
|
1022
|
-
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).`);
|
|
1023
|
-
return runner.createRunner(options);
|
|
1024
|
-
}
|
|
1025
|
-
//#endregion
|
|
1026
|
-
//#region src/dev-server/watcher.ts
|
|
1027
|
-
function createDevWatcher(options) {
|
|
1028
|
-
const { cwd, dirs, ignore = [], debounceMs = 100 } = options;
|
|
1029
|
-
const watchers = [];
|
|
1030
|
-
const watchedDirs = /* @__PURE__ */ new Set();
|
|
1031
|
-
let pendingEvents = [];
|
|
1032
|
-
let debounceTimer = null;
|
|
1033
|
-
function isIgnored(relPath) {
|
|
1034
|
-
return ignore.some((pattern) => {
|
|
1035
|
-
if (pattern.includes("*")) return new RegExp(`^${pattern.replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(relPath);
|
|
1036
|
-
return relPath.startsWith(pattern) || relPath === pattern;
|
|
1037
|
-
});
|
|
1038
|
-
}
|
|
1039
|
-
function queueEvent(type, fullPath) {
|
|
1040
|
-
const relPath = relative(cwd, fullPath).replace(/\\/g, "/");
|
|
1041
|
-
if (isIgnored(relPath)) return;
|
|
1042
|
-
if (relPath.startsWith("node_modules/") || relPath.startsWith(".git/")) return;
|
|
1043
|
-
if (relPath.endsWith(".bak") || relPath.startsWith(".ubean/")) return;
|
|
1044
|
-
pendingEvents.push({
|
|
1045
|
-
type,
|
|
1046
|
-
path: fullPath,
|
|
1047
|
-
relativePath: relPath
|
|
1048
|
-
});
|
|
1049
|
-
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1050
|
-
debounceTimer = setTimeout(() => {
|
|
1051
|
-
const events = pendingEvents;
|
|
1052
|
-
pendingEvents = [];
|
|
1053
|
-
debounceTimer = null;
|
|
1054
|
-
options.onChange?.(events);
|
|
1055
|
-
}, debounceMs);
|
|
1056
|
-
}
|
|
1057
|
-
function watchDir(dir) {
|
|
1058
|
-
if (watchedDirs.has(dir)) return;
|
|
1059
|
-
watchedDirs.add(dir);
|
|
1060
|
-
try {
|
|
1061
|
-
const w = watch(dir, { recursive: true }, (eventType, filename) => {
|
|
1062
|
-
if (!filename) return;
|
|
1063
|
-
const fullPath = join(dir, filename);
|
|
1064
|
-
queueEvent(eventType === "rename" ? "unlink" : "change", fullPath);
|
|
1065
|
-
});
|
|
1066
|
-
watchers.push(w);
|
|
1067
|
-
} catch {}
|
|
1068
|
-
}
|
|
1069
|
-
function start() {
|
|
1070
|
-
for (const dir of dirs) watchDir(join(cwd, dir));
|
|
1071
|
-
}
|
|
1072
|
-
function stop() {
|
|
1073
|
-
for (const w of watchers) w.close();
|
|
1074
|
-
watchers.length = 0;
|
|
1075
|
-
watchedDirs.clear();
|
|
1076
|
-
if (debounceTimer) {
|
|
1077
|
-
clearTimeout(debounceTimer);
|
|
1078
|
-
debounceTimer = null;
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
function addDir(dir) {
|
|
1082
|
-
watchDir(join(cwd, dir));
|
|
1083
|
-
}
|
|
1084
|
-
return {
|
|
1085
|
-
start,
|
|
1086
|
-
stop,
|
|
1087
|
-
addDir
|
|
1088
597
|
};
|
|
1089
598
|
}
|
|
1090
599
|
//#endregion
|
|
@@ -1100,7 +609,13 @@ function formatDiagnostics(diagnostics) {
|
|
|
1100
609
|
warnings
|
|
1101
610
|
};
|
|
1102
611
|
}
|
|
1103
|
-
|
|
612
|
+
/**
|
|
613
|
+
* 输出能力诊断结果。
|
|
614
|
+
*
|
|
615
|
+
* `'auto'` 语义(默认):健康时静默 —— "All capability checks passed" 仅在
|
|
616
|
+
* `verbose` 时输出;告警/错误属于 warn/error 级别,始终输出。
|
|
617
|
+
*/
|
|
618
|
+
function logDiagnostics(diagnostics, options = {}) {
|
|
1104
619
|
const { errors, warnings } = formatDiagnostics(diagnostics);
|
|
1105
620
|
if (errors.length > 0) {
|
|
1106
621
|
logger$7.error(`Capability errors (${errors.length}):`);
|
|
@@ -1110,7 +625,7 @@ function logDiagnostics(diagnostics) {
|
|
|
1110
625
|
logger$7.warn(`Capability warnings (${warnings.length}):`);
|
|
1111
626
|
for (const w of warnings) logger$7.warn(w);
|
|
1112
627
|
}
|
|
1113
|
-
if (errors.length === 0 && warnings.length === 0) logger$7.info("All capability checks passed");
|
|
628
|
+
if (errors.length === 0 && warnings.length === 0 && options.verbose) logger$7.info("All capability checks passed");
|
|
1114
629
|
}
|
|
1115
630
|
//#endregion
|
|
1116
631
|
//#region src/dev.ts
|
|
@@ -1131,20 +646,6 @@ import("@ubean/devtools").then((mod) => {
|
|
|
1131
646
|
function getCustomTabs() {
|
|
1132
647
|
return _customTabsGetter();
|
|
1133
648
|
}
|
|
1134
|
-
/**
|
|
1135
|
-
* dev 环境 SecurityHeaders 解析(ADR-0011)。
|
|
1136
|
-
*
|
|
1137
|
-
* ssg 模式默认关闭:静态产物不携带框架注入的安全头(响应头由托管平台
|
|
1138
|
-
* 控制),dev 默认关闭以对齐生产行为,消除「dev 有 CSP / 线上没有」的
|
|
1139
|
-
* 偏差。显式配置(`headers: true` 或对象)仍会被尊重,作为 CSP 调试
|
|
1140
|
-
* 入口。其余模式默认开启不变。
|
|
1141
|
-
*/
|
|
1142
|
-
function resolveDevSecurityHeaders(config) {
|
|
1143
|
-
if (config.security === false) return false;
|
|
1144
|
-
if (config.security?.headers === false) return false;
|
|
1145
|
-
if (config.security?.headers !== void 0) return config.security.headers;
|
|
1146
|
-
return config.mode !== "ssg";
|
|
1147
|
-
}
|
|
1148
649
|
const devCommand = {
|
|
1149
650
|
meta: {
|
|
1150
651
|
name: "dev",
|
|
@@ -1164,6 +665,14 @@ const devCommand = {
|
|
|
1164
665
|
description: "Exit if the port is already in use, instead of auto-incrementing",
|
|
1165
666
|
default: false
|
|
1166
667
|
},
|
|
668
|
+
verbose: {
|
|
669
|
+
type: "boolean",
|
|
670
|
+
description: "Show detailed startup logs (directories, scan, codegen, diagnostics, reload events)"
|
|
671
|
+
},
|
|
672
|
+
logRequests: {
|
|
673
|
+
type: "boolean",
|
|
674
|
+
description: "Log each request (GET /path 200 12ms). Ignored in ssg/spa modes."
|
|
675
|
+
},
|
|
1167
676
|
open: {
|
|
1168
677
|
type: "boolean",
|
|
1169
678
|
description: "Open browser on startup"
|
|
@@ -1176,142 +685,142 @@ const devCommand = {
|
|
|
1176
685
|
},
|
|
1177
686
|
async run({ args }) {
|
|
1178
687
|
const cwd = resolve(args.cwd || process.cwd());
|
|
1179
|
-
logger$6.info("Starting ubean dev server...");
|
|
1180
688
|
registerBuiltinPresets();
|
|
1181
689
|
const config = await loadUbeanConfig(cwd);
|
|
690
|
+
const logging = config.logging;
|
|
691
|
+
if (args.verbose) {
|
|
692
|
+
logging.scan = true;
|
|
693
|
+
logging.lifecycle = true;
|
|
694
|
+
logging.diagnostics = true;
|
|
695
|
+
}
|
|
696
|
+
if (args.logRequests) {
|
|
697
|
+
logging.request = true;
|
|
698
|
+
logging.requestSuppressed = false;
|
|
699
|
+
}
|
|
700
|
+
const backendCapable = config.mode === "fullstack" || config.mode === "backend";
|
|
701
|
+
if (logging.request && !backendCapable) {
|
|
702
|
+
logging.request = false;
|
|
703
|
+
logging.requestSuppressed = true;
|
|
704
|
+
}
|
|
705
|
+
if (logging.requestSuppressed) logger$6.warn(`Request logging is not applicable in "${config.mode}" mode — skipping.`);
|
|
706
|
+
setMinLevel(logging.level);
|
|
707
|
+
const startedAt = Date.now();
|
|
708
|
+
if (logging.lifecycle) logger$6.info("Starting ubean dev server...");
|
|
1182
709
|
const port = Number(args.port) || config.dev.port;
|
|
1183
710
|
const host = args.host || config.dev.host;
|
|
1184
|
-
logger$6.info(`Root directory: ${config.rootDir}`);
|
|
1185
|
-
logger$6.info(`Source directory: ${config.srcDir}`);
|
|
1186
711
|
const preset = resolvePresetByName(config.build.preset);
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
712
|
+
if (logging.scan) {
|
|
713
|
+
logger$6.info(`Root directory: ${config.rootDir}`);
|
|
714
|
+
logger$6.info(`Source directory: ${config.srcDir}`);
|
|
715
|
+
logger$6.info(`Preset: ${preset.name}`);
|
|
716
|
+
}
|
|
717
|
+
if (logging.scan) logger$6.info("Running capability diagnostics...");
|
|
1190
718
|
const diagnostics = diagnoseCapabilities(preset.name, preset.capabilities || {}, NODE_REQUIREMENTS);
|
|
1191
|
-
logDiagnostics(diagnostics);
|
|
719
|
+
logDiagnostics(diagnostics, { verbose: logging.diagnostics });
|
|
1192
720
|
if (!diagnostics.valid) logger$6.warn("Some capability requirements are not met. Dev server may not function correctly.");
|
|
1193
|
-
let
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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;
|
|
1198
735
|
try {
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
+
});
|
|
1206
744
|
} catch (err) {
|
|
1207
745
|
logger$6.error(`Rescan failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1208
|
-
} finally {
|
|
1209
|
-
rescanInProgress = false;
|
|
1210
746
|
}
|
|
1211
747
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
srcDir: config.srcDir,
|
|
1215
|
-
port,
|
|
1216
|
-
host,
|
|
1217
|
-
strictPort: args.strictPort,
|
|
1218
|
-
preset,
|
|
1219
|
-
config,
|
|
1220
|
-
capabilities,
|
|
1221
|
-
app: currentApp,
|
|
1222
|
-
layouts: currentLayouts,
|
|
1223
|
-
devtools: {
|
|
1224
|
-
getScanResult: () => currentScanResult,
|
|
1225
|
-
getConfigMeta: () => ({
|
|
1226
|
-
preset: config.build.preset,
|
|
1227
|
-
rootDir: config.rootDir,
|
|
1228
|
-
srcDir: config.srcDir,
|
|
1229
|
-
openAPI: {
|
|
1230
|
-
enabled: true,
|
|
1231
|
-
scalarPath: "/_scalar",
|
|
1232
|
-
openAPIPath: "/_openapi.json"
|
|
1233
|
-
},
|
|
1234
|
-
dir: config.dir
|
|
1235
|
-
}),
|
|
1236
|
-
getCustomTabs: () => getCustomTabs(),
|
|
1237
|
-
triggerRescan: () => rescan()
|
|
1238
|
-
},
|
|
1239
|
-
onListen({ url, networkUrls }) {
|
|
1240
|
-
const label = (text) => dim(text);
|
|
1241
|
-
const lines = [
|
|
1242
|
-
`${green(bold("🚀 ubean dev server ready"))}\n`,
|
|
1243
|
-
` → ${label("Local:")} ${cyan(url)}`,
|
|
1244
|
-
...(networkUrls || []).map((n) => ` → ${label("Network:")} ${cyan(n)}`)
|
|
1245
|
-
];
|
|
1246
|
-
const hasBackend = config.mode !== "ssg" && config.mode !== "spa";
|
|
1247
|
-
if (hasBackend) lines.push(` → ${label("Scalar UI:")} ${cyan(`${url}/_scalar`)}`, ` → ${label("OpenAPI:")} ${cyan(`${url}/_openapi.json`)}`);
|
|
1248
|
-
if (config.devtools.enabled) lines.push(` → ${label("DevTools:")} ${cyan(`${url}${config.devtools.route}`)}`);
|
|
1249
|
-
lines.push(` → ${dim("Press Ctrl+C to stop")}`);
|
|
1250
|
-
logger$6.info(lines.join("\n"));
|
|
1251
|
-
if (hasBackend) generateOpenApiTypesFromServer(url, { outDir: resolve(cwd, ".ubean") }).then((filePath) => {
|
|
1252
|
-
if (filePath) logger$6.info(`OpenAPI types generated: ${filePath}`);
|
|
1253
|
-
}).catch((err) => {
|
|
1254
|
-
logger$6.warn(`Failed to generate OpenAPI types: ${err instanceof Error ? err.message : String(err)}`);
|
|
1255
|
-
});
|
|
1256
|
-
},
|
|
1257
|
-
onBeforeReload() {
|
|
1258
|
-
logger$6.info("Reloading...");
|
|
1259
|
-
},
|
|
1260
|
-
onAfterReload() {
|
|
1261
|
-
logger$6.info("Reloaded");
|
|
1262
|
-
}
|
|
1263
|
-
});
|
|
748
|
+
let lastChangedFile = null;
|
|
749
|
+
let devServer = null;
|
|
1264
750
|
try {
|
|
1265
|
-
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
|
+
});
|
|
1266
793
|
} catch (err) {
|
|
1267
794
|
logger$6.error(err?.stack || err?.message || String(err));
|
|
1268
795
|
process.exit(1);
|
|
1269
796
|
}
|
|
1270
|
-
const watcher = createDevWatcher({
|
|
1271
|
-
cwd,
|
|
1272
|
-
dirs: [
|
|
1273
|
-
"api",
|
|
1274
|
-
"pages",
|
|
1275
|
-
"middleware",
|
|
1276
|
-
"layouts",
|
|
1277
|
-
"plugins",
|
|
1278
|
-
"app",
|
|
1279
|
-
"routes"
|
|
1280
|
-
].map((d) => `${config.srcDir}/${d}`),
|
|
1281
|
-
ignore: [
|
|
1282
|
-
"**/node_modules/**",
|
|
1283
|
-
"**/.git/**",
|
|
1284
|
-
"**/.ubean/**"
|
|
1285
|
-
],
|
|
1286
|
-
debounceMs: 150,
|
|
1287
|
-
async onChange(events) {
|
|
1288
|
-
const relevantEvents = events.filter((e) => /\.(ts|js|vue|mjs|cjs|json)$/.test(e.relativePath) && !e.relativePath.includes(".bak"));
|
|
1289
|
-
if (relevantEvents.length === 0) return;
|
|
1290
|
-
logger$6.info(`File change detected: ${relevantEvents[0].relativePath}`);
|
|
1291
|
-
await rescan();
|
|
1292
|
-
}
|
|
1293
|
-
});
|
|
1294
|
-
watcher.start();
|
|
1295
797
|
const cleanup = async () => {
|
|
1296
|
-
logger$6.info("\nShutting down...");
|
|
1297
|
-
|
|
1298
|
-
await runner.stop();
|
|
798
|
+
if (logging.lifecycle) logger$6.info("\nShutting down...");
|
|
799
|
+
await devServer?.stop();
|
|
1299
800
|
process.exit(0);
|
|
1300
801
|
};
|
|
1301
802
|
process.on("SIGINT", cleanup);
|
|
1302
803
|
process.on("SIGTERM", cleanup);
|
|
1303
804
|
}
|
|
1304
805
|
};
|
|
1305
|
-
|
|
1306
|
-
|
|
806
|
+
/**
|
|
807
|
+
* 初始扫描 + 类型生成。
|
|
808
|
+
*
|
|
809
|
+
* RM-V14 起 CLI 不在这里建 app(由 `ubeanDevRequestPlugin` 自举),只负责扫描与 codegen;
|
|
810
|
+
* 扫描结果交给 DevTools 作启动数据,并在每次文件变更后由 `onScan` 刷新。
|
|
811
|
+
*/
|
|
812
|
+
async function initialScan(cwd, config, logging) {
|
|
813
|
+
if (logging.scan) logger$6.info("Scanning project files...");
|
|
1307
814
|
const result = await scanProject({
|
|
1308
815
|
cwd,
|
|
1309
816
|
srcDir: config.srcDir,
|
|
1310
817
|
dirs: config.dir,
|
|
1311
818
|
ignore: config.scanOptions?.ignore
|
|
1312
819
|
});
|
|
1313
|
-
|
|
1314
|
-
|
|
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
|
+
}
|
|
1315
824
|
await generateTypes(result, {
|
|
1316
825
|
cwd,
|
|
1317
826
|
srcDir: config.srcDir,
|
|
@@ -1320,39 +829,12 @@ async function buildApp(cwd, config) {
|
|
|
1320
829
|
autoImports: config.autoImports,
|
|
1321
830
|
components: config.components
|
|
1322
831
|
});
|
|
1323
|
-
|
|
1324
|
-
rootDir: cwd,
|
|
1325
|
-
routes: result.apiRoutes,
|
|
1326
|
-
middleware: result.middlewares,
|
|
1327
|
-
pages: result.pages,
|
|
1328
|
-
crons: result.crons,
|
|
1329
|
-
routeRules: config.routeRules || {},
|
|
1330
|
-
publicDir: config.dir.public,
|
|
1331
|
-
pageAssetTags: config.favicon ? { favicon: config.favicon } : void 0,
|
|
1332
|
-
openAPI: {
|
|
1333
|
-
title: "UBEAN Dev API",
|
|
1334
|
-
scalarPath: "/_scalar",
|
|
1335
|
-
openAPIPath: "/_openapi.json"
|
|
1336
|
-
},
|
|
1337
|
-
i18nConfig: config.i18n,
|
|
1338
|
-
ssrExclude: config.ssr.exclude,
|
|
1339
|
-
streaming: config.ssr.streaming,
|
|
1340
|
-
notFoundPage: result.notFoundPage,
|
|
1341
|
-
csrf: config.security === false ? false : config.security?.csrf ?? true,
|
|
1342
|
-
securityHeaders: resolveDevSecurityHeaders(config),
|
|
1343
|
-
dataCache: config.dataCache,
|
|
1344
|
-
cache: config.cache,
|
|
1345
|
-
seoConventions: { srcDir: resolve(cwd, config.srcDir) }
|
|
1346
|
-
});
|
|
1347
|
-
app.hooks.hook("request:start", (c) => {
|
|
1348
|
-
logger$6.debug(`${c.req.method} ${c.req.path}`);
|
|
1349
|
-
});
|
|
1350
|
-
return {
|
|
1351
|
-
app,
|
|
1352
|
-
layouts: result.layouts,
|
|
1353
|
-
scanResult: result
|
|
1354
|
-
};
|
|
832
|
+
return result;
|
|
1355
833
|
}
|
|
834
|
+
/**
|
|
835
|
+
* RM-V14:请求日志(`logging.request`)不再由 CLI 实现 —— 见 `@ubean/build` 的
|
|
836
|
+
* `createDevApp({ logger })`。配置域行为放 builder,两条 dev 路径输出一致。
|
|
837
|
+
*/
|
|
1356
838
|
//#endregion
|
|
1357
839
|
//#region src/devtools.ts
|
|
1358
840
|
const logger$5 = getLogger("cli");
|
|
@@ -2057,89 +1539,38 @@ const prepareCommand = {
|
|
|
2057
1539
|
};
|
|
2058
1540
|
//#endregion
|
|
2059
1541
|
//#region src/preview.ts
|
|
2060
|
-
const MIME_TYPES = {
|
|
2061
|
-
".html": "text/html; charset=utf-8",
|
|
2062
|
-
".js": "text/javascript; charset=utf-8",
|
|
2063
|
-
".mjs": "text/javascript; charset=utf-8",
|
|
2064
|
-
".css": "text/css; charset=utf-8",
|
|
2065
|
-
".json": "application/json; charset=utf-8",
|
|
2066
|
-
".svg": "image/svg+xml",
|
|
2067
|
-
".png": "image/png",
|
|
2068
|
-
".jpg": "image/jpeg",
|
|
2069
|
-
".jpeg": "image/jpeg",
|
|
2070
|
-
".gif": "image/gif",
|
|
2071
|
-
".webp": "image/webp",
|
|
2072
|
-
".ico": "image/x-icon",
|
|
2073
|
-
".woff": "font/woff",
|
|
2074
|
-
".woff2": "font/woff2",
|
|
2075
|
-
".ttf": "font/ttf",
|
|
2076
|
-
".otf": "font/otf",
|
|
2077
|
-
".wasm": "application/wasm",
|
|
2078
|
-
".txt": "text/plain; charset=utf-8",
|
|
2079
|
-
".map": "application/json; charset=utf-8"
|
|
2080
|
-
};
|
|
2081
1542
|
/**
|
|
2082
|
-
*
|
|
1543
|
+
* 静态文件服务器(spa / ssg 的降级路径)。
|
|
2083
1544
|
*
|
|
2084
|
-
* -
|
|
2085
|
-
*
|
|
1545
|
+
* RM-V25:判定规则不再自己写一份,改用 `@ubean/build/vite` 的 `resolvePreviewFile` —— 插件接管
|
|
1546
|
+
* `vite preview` 时用的是同一个函数,两套方言必然分叉(文件名含合法 `...` 的产物曾被
|
|
1547
|
+
* `includes('..')` 判成路径穿越)。
|
|
2086
1548
|
*
|
|
2087
|
-
*
|
|
1549
|
+
* 主路径已改为 `vite preview` + 插件中间件(RM-V24);本函数保留给「Vite 预览起不来」的
|
|
1550
|
+
* spa / ssg 场景(静态产物不需要服务端能力),以及既有回归测试。
|
|
2088
1551
|
*/
|
|
2089
1552
|
function startStaticServer(opts) {
|
|
2090
1553
|
const { root, port, host, mode } = opts;
|
|
2091
|
-
const spaFallback = mode === "spa";
|
|
2092
1554
|
return createServer(async (req, res) => {
|
|
2093
1555
|
try {
|
|
2094
1556
|
const url = new URL(req.url || "/", `http://${host}`);
|
|
2095
1557
|
const pathname = decodeURIComponent(url.pathname);
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
if (filePath !== rootPath && !filePath.startsWith(rootPath + sep)) {
|
|
1558
|
+
const resolvedFile = resolvePreviewFile(root, pathname, mode);
|
|
1559
|
+
if (resolvedFile.kind === "forbidden") {
|
|
2099
1560
|
res.statusCode = 400;
|
|
1561
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2100
1562
|
res.end("Bad Request");
|
|
2101
1563
|
return;
|
|
2102
1564
|
}
|
|
2103
|
-
|
|
2104
|
-
if (!fileExists && existsSync(filePath) && (await stat(filePath)).isDirectory()) {
|
|
2105
|
-
const indexPath = join(filePath, "index.html");
|
|
2106
|
-
if (existsSync(indexPath)) {
|
|
2107
|
-
filePath = indexPath;
|
|
2108
|
-
fileExists = true;
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
if (!fileExists && mode === "ssg") {
|
|
2112
|
-
const indexPath = join(filePath, "index.html");
|
|
2113
|
-
if (existsSync(indexPath) && (await stat(indexPath)).isFile()) {
|
|
2114
|
-
filePath = indexPath;
|
|
2115
|
-
fileExists = true;
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
if (!fileExists && mode === "ssg") {
|
|
2119
|
-
const htmlPath = `${filePath}.html`;
|
|
2120
|
-
if (existsSync(htmlPath) && (await stat(htmlPath)).isFile()) {
|
|
2121
|
-
filePath = htmlPath;
|
|
2122
|
-
fileExists = true;
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
if (!fileExists && spaFallback) {
|
|
2126
|
-
const fallbackPath = join(root, "index.html");
|
|
2127
|
-
if (existsSync(fallbackPath)) {
|
|
2128
|
-
filePath = fallbackPath;
|
|
2129
|
-
fileExists = true;
|
|
2130
|
-
}
|
|
2131
|
-
}
|
|
2132
|
-
if (!fileExists) {
|
|
1565
|
+
if (resolvedFile.kind === "missing") {
|
|
2133
1566
|
res.statusCode = 404;
|
|
2134
1567
|
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
2135
1568
|
res.end("Not Found");
|
|
2136
1569
|
return;
|
|
2137
1570
|
}
|
|
2138
|
-
const
|
|
2139
|
-
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
2140
|
-
const body = await readFile(filePath);
|
|
1571
|
+
const body = await readFile(resolvedFile.path);
|
|
2141
1572
|
res.statusCode = 200;
|
|
2142
|
-
res.setHeader("Content-Type",
|
|
1573
|
+
res.setHeader("Content-Type", previewMimeType(resolvedFile.path));
|
|
2143
1574
|
res.setHeader("Content-Length", body.length);
|
|
2144
1575
|
res.end(body);
|
|
2145
1576
|
} catch (err) {
|
|
@@ -2173,6 +1604,10 @@ const previewCommand = {
|
|
|
2173
1604
|
type: "string",
|
|
2174
1605
|
description: "Project root directory",
|
|
2175
1606
|
default: "."
|
|
1607
|
+
},
|
|
1608
|
+
outDir: {
|
|
1609
|
+
type: "string",
|
|
1610
|
+
description: "Output directory to preview (overrides ubean.config.ts build.outputDir)"
|
|
2176
1611
|
}
|
|
2177
1612
|
},
|
|
2178
1613
|
async run({ args }) {
|
|
@@ -2182,6 +1617,7 @@ const previewCommand = {
|
|
|
2182
1617
|
const config = await loadUbeanConfig(cwd);
|
|
2183
1618
|
const preset = resolvePresetByName(config.build.preset);
|
|
2184
1619
|
logger$1.info(`Preset: ${preset.name}`);
|
|
1620
|
+
if (args.outDir) config.build.outputDir = String(args.outDir);
|
|
2185
1621
|
const outputDir = config.build.outputDir || "dist";
|
|
2186
1622
|
const mode = config.mode;
|
|
2187
1623
|
const host = args.host || config.preview.host;
|
|
@@ -2204,110 +1640,71 @@ const previewCommand = {
|
|
|
2204
1640
|
const bannerUrl = `http://${host}:${port}`;
|
|
2205
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")}`);
|
|
2206
1642
|
};
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
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)}`);
|
|
2212
1681
|
process.exit(1);
|
|
2213
1682
|
}
|
|
2214
|
-
|
|
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({
|
|
2215
1685
|
root: staticRoot,
|
|
2216
1686
|
port: actualPort,
|
|
2217
1687
|
host,
|
|
2218
1688
|
mode
|
|
2219
1689
|
});
|
|
2220
1690
|
printBanner(actualPort, `static (${mode})`);
|
|
2221
|
-
const
|
|
2222
|
-
|
|
1691
|
+
const cleanupFallback = () => {
|
|
1692
|
+
fallback.close(() => process.exit(0));
|
|
2223
1693
|
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2224
1694
|
};
|
|
2225
|
-
process.on("SIGINT",
|
|
2226
|
-
process.on("SIGTERM",
|
|
2227
|
-
server.on("error", (err) => {
|
|
2228
|
-
logger$1.error(`Preview server error: ${err.message || err}`);
|
|
2229
|
-
process.exit(1);
|
|
2230
|
-
});
|
|
1695
|
+
process.on("SIGINT", cleanupFallback);
|
|
1696
|
+
process.on("SIGTERM", cleanupFallback);
|
|
2231
1697
|
return;
|
|
2232
1698
|
}
|
|
2233
|
-
|
|
2234
|
-
let serverFile;
|
|
2235
|
-
if (presetName === "node" || presetName === "bun" || presetName === "deno") serverFile = "server.mjs";
|
|
2236
|
-
else if (presetName === "cloudflare") {
|
|
2237
|
-
logger$1.error("Cloudflare preset preview is not supported yet. Use `wrangler dev` instead.");
|
|
2238
|
-
process.exit(1);
|
|
2239
|
-
} else {
|
|
2240
|
-
serverFile = "handler.mjs";
|
|
2241
|
-
logger$1.warn(`Preset "${presetName}" uses a fetch handler entry. Preview may not work as a standalone server.`);
|
|
2242
|
-
}
|
|
2243
|
-
const serverPath = join(cwd, outputDir, "server", serverFile);
|
|
2244
|
-
if (!existsSync(serverPath)) {
|
|
2245
|
-
logger$1.error(`Build output not found: ${serverPath}`);
|
|
2246
|
-
logger$1.info("Run `ubean build` first to create a production build.");
|
|
2247
|
-
process.exit(1);
|
|
2248
|
-
}
|
|
2249
|
-
const { child, exited } = spawnPreviewServer({
|
|
2250
|
-
serverPath,
|
|
2251
|
-
cwd,
|
|
2252
|
-
port: actualPort,
|
|
2253
|
-
host,
|
|
2254
|
-
strictPort
|
|
2255
|
-
});
|
|
2256
|
-
try {
|
|
2257
|
-
await waitForPort(actualPort, {
|
|
2258
|
-
host,
|
|
2259
|
-
retries: 40,
|
|
2260
|
-
delay: 250
|
|
2261
|
-
});
|
|
2262
|
-
} catch {
|
|
2263
|
-
if (exited.value) logger$1.error(`Preview server exited before becoming ready on port ${actualPort}.`);
|
|
2264
|
-
else logger$1.warn(`Preview server on port ${actualPort} is not responding yet, the banner may be premature.`);
|
|
2265
|
-
}
|
|
2266
|
-
printBanner(actualPort, "production");
|
|
1699
|
+
printBanner(actualPort, staticMode ? `static (${mode})` : "production");
|
|
2267
1700
|
const cleanup = () => {
|
|
2268
|
-
|
|
2269
|
-
process.exit(0);
|
|
1701
|
+
server?.close().catch(() => void 0).finally(() => process.exit(0));
|
|
1702
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
2270
1703
|
};
|
|
2271
1704
|
process.on("SIGINT", cleanup);
|
|
2272
1705
|
process.on("SIGTERM", cleanup);
|
|
2273
|
-
child.on("exit", (code) => {
|
|
2274
|
-
if (code !== 0 && code !== null) logger$1.error(`Preview server exited with code ${code}`);
|
|
2275
|
-
process.exit(code ?? 0);
|
|
2276
|
-
});
|
|
2277
1706
|
}
|
|
2278
1707
|
};
|
|
2279
|
-
function spawnPreviewServer(opts) {
|
|
2280
|
-
const exited = { value: false };
|
|
2281
|
-
const child = spawn("node", [opts.serverPath], {
|
|
2282
|
-
cwd: opts.cwd,
|
|
2283
|
-
env: {
|
|
2284
|
-
...process.env,
|
|
2285
|
-
PORT: String(opts.port),
|
|
2286
|
-
HOST: opts.host
|
|
2287
|
-
},
|
|
2288
|
-
stdio: [
|
|
2289
|
-
"inherit",
|
|
2290
|
-
"inherit",
|
|
2291
|
-
"pipe"
|
|
2292
|
-
]
|
|
2293
|
-
});
|
|
2294
|
-
let addrInUseReported = false;
|
|
2295
|
-
child.stderr?.on("data", (chunk) => {
|
|
2296
|
-
const text = chunk.toString();
|
|
2297
|
-
process.stderr.write(text);
|
|
2298
|
-
if (!addrInUseReported && /EADDRINUSE/.test(text)) {
|
|
2299
|
-
addrInUseReported = true;
|
|
2300
|
-
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" : ""}.`);
|
|
2301
|
-
}
|
|
2302
|
-
});
|
|
2303
|
-
child.on("exit", () => {
|
|
2304
|
-
exited.value = true;
|
|
2305
|
-
});
|
|
2306
|
-
return {
|
|
2307
|
-
child,
|
|
2308
|
-
exited
|
|
2309
|
-
};
|
|
2310
|
-
}
|
|
2311
1708
|
//#endregion
|
|
2312
1709
|
//#region src/scaffold-commands.ts
|
|
2313
1710
|
const logger = getLogger("cli");
|