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