@hazejs/vite 0.0.0-stage → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,1685 @@
1
+ import { builtinModules, createRequire } from "node:module";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import process from "node:process";
4
+ import { createBuilder, createServer, createServerModuleRunner, loadConfigFromFile, mergeConfig, normalizePath, preview } from "vite";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
6
+ import { nodeUpgradeHandler, registerNodeServer, renderErrorPage, webSocketConfig, webSocketPlugin } from "@hazejs/core";
7
+ import { toNodeHandler } from "srvx/node";
8
+ import { consola } from "consola";
9
+ import { cp, mkdir, readFile, writeFile } from "node:fs/promises";
10
+ import { traceNodeModules } from "nf3";
11
+ import { NodeNativePackages, NonBundleablePackages } from "nf3/db";
12
+ import { execFileSync, spawn } from "node:child_process";
13
+ import { connect, createServer as createServer$1 } from "node:net";
14
+ import { pathToFileURL } from "node:url";
15
+ import { parseEnv } from "node:util";
16
+ import { serve } from "srvx";
17
+ // src/constants.ts
18
+ /** 产品更名时与 `@hazejs/core` 的 constants.ts 一起修改 */
19
+ const PLUGIN_NAME = "hazejs";
20
+ const CONFIG_BASENAME = "haze.config";
21
+ const VIRTUAL_MODULES = {
22
+ /** 编译期发现的配置 / 路由 / 提供者 / 异常处理器 */
23
+ manifest: "virtual:haze/manifest",
24
+ /** `createApp(options)`:用 manifest 创建应用 */
25
+ app: "virtual:haze/app",
26
+ /** 生产环境入口:创建应用并监听端口 */
27
+ serverEntry: "virtual:haze/server-entry",
28
+ /** 打包进产物的迁移文件(database/ 下的 drizzle-kit 输出) */
29
+ migrations: "virtual:haze/migrations",
30
+ /** 生产迁移脚本入口(migrate.mjs) */
31
+ migrateEntry: "virtual:haze/migrate-entry",
32
+ /** 队列 worker 与定时任务入口(worker.mjs) */
33
+ workerEntry: "virtual:haze/worker-entry"
34
+ };
35
+ // src/config.ts
36
+ function defineHazeConfig(config) {
37
+ return config;
38
+ }
39
+ const EXTENSIONS = [
40
+ "ts",
41
+ "mts",
42
+ "js",
43
+ "mjs"
44
+ ];
45
+ /** 存在 vite.config.* 视为全栈项目:由用户的 Vite 配置(含 haze() 插件)驱动 */
46
+ function findViteConfig(root) {
47
+ return EXTENSIONS.map((ext) => resolve(root, `vite.config.${ext}`)).find((file) => existsSync(file));
48
+ }
49
+ /** 读取项目根目录下的 `haze.config.ts`(可选) */
50
+ async function loadHazeConfig(root, mode = "development") {
51
+ const configFile = EXTENSIONS.map((ext) => resolve(root, `${CONFIG_BASENAME}.${ext}`)).find((file) => existsSync(file));
52
+ let config = {};
53
+ if (configFile) config = (await loadConfigFromFile({
54
+ command: "serve",
55
+ mode
56
+ }, configFile, root, "silent"))?.config ?? {};
57
+ return {
58
+ root,
59
+ srcDir: resolve(root, config.srcDir ?? "."),
60
+ port: config.port ?? 3e3,
61
+ host: config.host,
62
+ outDir: resolve(root, config.outDir ?? "dist"),
63
+ typegen: config.typegen ?? true,
64
+ preset: config.preset,
65
+ cloudflare: config.cloudflare,
66
+ vercel: config.vercel,
67
+ externals: config.externals,
68
+ vite: config.vite ?? {},
69
+ configFile
70
+ };
71
+ }
72
+ // src/typegen.ts
73
+ const SOURCE_RE$1 = /\.[cm]?[jt]s$/;
74
+ /** 生成文件位置(需在 tsconfig 的 include 中加入 `.haze/types/**\/*.d.ts`) */
75
+ function typesFile(root) {
76
+ return join(root, ".haze", "types", "haze.d.ts");
77
+ }
78
+ /** 生成配置键与命名路由的类型声明(模块扩充 @hazejs/core) */
79
+ function generateTypes({ root, srcDir, routes }) {
80
+ const file = typesFile(root);
81
+ const typesDir = join(file, "..");
82
+ const configDir = join(srcDir, "config");
83
+ const configs = (existsSync(configDir) ? readdirSync(configDir).filter((name) => SOURCE_RE$1.test(name) && !name.endsWith(".d.ts")).sort() : []).map((name) => {
84
+ const key = name.replace(SOURCE_RE$1, "");
85
+ let path = normalizePath(relative(typesDir, join(configDir, key)));
86
+ if (!path.startsWith(".")) path = `./${path}`;
87
+ return {
88
+ key,
89
+ line: ` ${JSON.stringify(key)}: ConfigValue<typeof import(${JSON.stringify(path)}).default>`
90
+ };
91
+ });
92
+ const policyDir = join(srcDir, "app", "policy");
93
+ const policies = (existsSync(policyDir) ? readdirSync(policyDir).filter((name) => SOURCE_RE$1.test(name) && !name.endsWith(".d.ts")).sort() : []).map((name) => {
94
+ const key = name.replace(SOURCE_RE$1, "");
95
+ let path = normalizePath(relative(typesDir, join(policyDir, key)));
96
+ if (!path.startsWith(".")) path = `./${path}`;
97
+ return {
98
+ key,
99
+ line: ` ${JSON.stringify(key)}: typeof import(${JSON.stringify(path)}).default`
100
+ };
101
+ });
102
+ const named = routes.filter((route) => route.name).map((route) => {
103
+ const params = [...route.path.matchAll(/:(\w+)/g)].map((match) => match[1]);
104
+ const type = params.length ? `{ ${params.map((param) => `${param}: string | number`).join(", ")} }` : "Record<never, never>";
105
+ return {
106
+ name: route.name,
107
+ line: ` ${JSON.stringify(route.name)}: ${type}`
108
+ };
109
+ }).sort((a, b) => a.name.localeCompare(b.name));
110
+ return {
111
+ file,
112
+ content: `// 由 ${PLUGIN_NAME} 自动生成(haze dev / haze typegen),请勿手动修改
113
+ import type { ConfigValue } from '@hazejs/core'
114
+
115
+ declare module '@hazejs/core' {
116
+ interface GeneratedConfig {
117
+ ${configs.map((item) => item.line).join("\n")}
118
+ }
119
+
120
+ interface RouteNames {
121
+ ${named.map((item) => item.line).join("\n")}
122
+ }
123
+
124
+ interface GeneratedPolicies {
125
+ ${policies.map((item) => item.line).join("\n")}
126
+ }
127
+ }
128
+
129
+ export {}
130
+ `,
131
+ configs: configs.map((item) => item.key),
132
+ routes: named.map((item) => item.name),
133
+ policies: policies.map((item) => item.key)
134
+ };
135
+ }
136
+ /** 内容变化时才写入,避免触发编辑器与文件监听的无谓刷新 */
137
+ async function writeTypes(options) {
138
+ const { content, ...result } = generateTypes(options);
139
+ if (await readFile(result.file, "utf8").catch(() => void 0) === content) return {
140
+ ...result,
141
+ changed: false
142
+ };
143
+ await mkdir(join(result.file, ".."), { recursive: true });
144
+ await writeFile(result.file, content);
145
+ return {
146
+ ...result,
147
+ changed: true
148
+ };
149
+ }
150
+ // src/host.ts
151
+ function createAppHost({ vite, api, debounce = 30 }) {
152
+ const root = vite.config.root;
153
+ const runner = createServerModuleRunner(vite.environments.ssr, { hmr: false });
154
+ const listeners = /* @__PURE__ */ new Set();
155
+ let current;
156
+ let lastError;
157
+ let started;
158
+ let queue;
159
+ let timer;
160
+ let closed = false;
161
+ async function load(file) {
162
+ const start = performance.now();
163
+ try {
164
+ runner.clearCache();
165
+ const { createApp } = await runner.import(VIRTUAL_MODULES.app);
166
+ const app = await createApp({ mode: "development" });
167
+ const previous = current;
168
+ current = {
169
+ app,
170
+ active: 0
171
+ };
172
+ lastError = void 0;
173
+ if (previous) retire(previous);
174
+ await generateTypes(app);
175
+ return {
176
+ ok: true,
177
+ file,
178
+ duration: performance.now() - start
179
+ };
180
+ } catch (error) {
181
+ lastError = error;
182
+ return {
183
+ ok: false,
184
+ file,
185
+ duration: performance.now() - start,
186
+ error
187
+ };
188
+ }
189
+ }
190
+ async function generateTypes(app) {
191
+ const { srcDir, typegen } = api.settings();
192
+ if (!typegen) return;
193
+ try {
194
+ await writeTypes({
195
+ root,
196
+ srcDir,
197
+ routes: app.router.list()
198
+ });
199
+ } catch (error) {
200
+ consola.warn(`[${PLUGIN_NAME}] failed to generate types`, error);
201
+ }
202
+ }
203
+ async function retire(slot) {
204
+ if (slot.active > 0) await new Promise((resolve) => slot.onIdle = resolve);
205
+ try {
206
+ await slot.app.dispose({ reason: "hmr" });
207
+ } catch (error) {
208
+ consola.warn(`[${PLUGIN_NAME}] failed to dispose previous app`, error);
209
+ }
210
+ }
211
+ function reload(file) {
212
+ const run = async () => {
213
+ const result = await load(file);
214
+ report(result);
215
+ return result;
216
+ };
217
+ queue = (queue ?? Promise.resolve()).then(run, run);
218
+ return queue;
219
+ }
220
+ function start() {
221
+ started ??= reload();
222
+ return started;
223
+ }
224
+ function report(result) {
225
+ listeners.forEach((listener) => listener(result));
226
+ const ms = `${Math.round(result.duration)}ms`;
227
+ if (result.ok && result.file) consola.success(`[${PLUGIN_NAME}] reloaded in ${ms} (${result.file})`);
228
+ else if (!result.ok) consola.error(`[${PLUGIN_NAME}] ${result.file ? `reload failed (${result.file}), still serving the previous version` : "failed to start"}\n`, result.error);
229
+ }
230
+ const unsubscribe = api.onReload((event) => {
231
+ if (closed || !started) return;
232
+ const file = normalizePath(relative(root, event.file));
233
+ clearTimeout(timer);
234
+ timer = setTimeout(() => void reload(file), debounce);
235
+ });
236
+ async function fetch(request) {
237
+ if (!started) await start();
238
+ await started;
239
+ const slot = current;
240
+ if (!slot) return errorResponse(lastError, request, root);
241
+ slot.active++;
242
+ try {
243
+ return await slot.app.fetch(request);
244
+ } finally {
245
+ slot.active--;
246
+ if (slot.active === 0) slot.onIdle?.();
247
+ }
248
+ }
249
+ return {
250
+ get app() {
251
+ return current?.app;
252
+ },
253
+ start,
254
+ fetch,
255
+ reload,
256
+ import: (id) => runner.import(id),
257
+ onReload(listener) {
258
+ listeners.add(listener);
259
+ return () => listeners.delete(listener);
260
+ },
261
+ async close() {
262
+ if (closed) return;
263
+ closed = true;
264
+ unsubscribe();
265
+ clearTimeout(timer);
266
+ await queue;
267
+ await current?.app.dispose({ reason: "shutdown" });
268
+ current = void 0;
269
+ await runner.close();
270
+ }
271
+ };
272
+ }
273
+ /** 应用启动失败时的响应:浏览器显示错误页,其余返回 JSON */
274
+ function errorResponse(error, request, rootDir) {
275
+ const err = error instanceof Error ? error : new Error(String(error ?? "Application is starting"));
276
+ const message = `[${PLUGIN_NAME}] ${err.message}`;
277
+ if (request.headers.get("accept")?.includes("text/html")) return new Response(renderErrorPage({
278
+ error: err,
279
+ status: 500,
280
+ message,
281
+ request,
282
+ rootDir
283
+ }), {
284
+ status: 500,
285
+ headers: { "content-type": "text/html; charset=utf-8" }
286
+ });
287
+ return Response.json({
288
+ code: 500,
289
+ message,
290
+ stack: err.stack?.split("\n").map((line) => line.trim())
291
+ }, { status: 500 });
292
+ }
293
+ // src/presets/deps.ts
294
+ /** 数据库驱动:含原生二进制 / WASM / 可选原生依赖,不打包,运行时从 node_modules 加载(或追踪进产物) */
295
+ const DRIVER_PACKAGES = [
296
+ "@aws-sdk/client-s3",
297
+ "@aws-sdk/client-sesv2",
298
+ "@aws-sdk/lib-storage",
299
+ "@aws-sdk/s3-request-presigner",
300
+ "@libsql/client",
301
+ "@socket.io/bun-engine",
302
+ "@socket.io/redis-adapter",
303
+ "@upstash/redis",
304
+ "bullmq",
305
+ "ioredis",
306
+ "libsql",
307
+ "@electric-sql/pglite",
308
+ "mongodb",
309
+ "mysql2",
310
+ "nodemailer",
311
+ "postgres",
312
+ "socket.io"
313
+ ];
314
+ /** Edge(Workers)上无法运行的驱动:构建时替换为调用即报错的空模块 */
315
+ const EDGE_UNSUPPORTED_DRIVERS = [
316
+ "@electric-sql/pglite",
317
+ "@socket.io/bun-engine",
318
+ "@socket.io/redis-adapter",
319
+ "bullmq",
320
+ "socket.io",
321
+ "ioredis",
322
+ "mysql2",
323
+ "mongodb",
324
+ "libsql",
325
+ "nodemailer"
326
+ ];
327
+ /** 匹配“除给定包外的全部依赖”,用作 ssr.noExternal */
328
+ function bundleAllExcept(names) {
329
+ return new RegExp(`^(?!(?:${names.map((name) => name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|")})(?:/|$))`);
330
+ }
331
+ /**
332
+ * 匹配给定包(含子路径)的正则,用作 Rolldown 的 external:
333
+ * 不经 Vite 解析直接保留导入,项目未安装的可选驱动(如 drizzle-orm 的 mysql2 / pglite)
334
+ * 不会被替换为没有导出的空模块而导致构建失败
335
+ */
336
+ function externalPattern(names) {
337
+ return new RegExp(`^(?:${names.map((name) => name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|")})(?:/|$)`);
338
+ }
339
+ /** 不打包、运行时从 node_modules 加载的包:数据库驱动、原生模块、已知无法打包的包与 traceDeps */
340
+ function externalPackages(options = {}) {
341
+ const inline = new Set(options.inline);
342
+ return [.../* @__PURE__ */ new Set([
343
+ ...DRIVER_PACKAGES,
344
+ ...NodeNativePackages,
345
+ ...NonBundleablePackages,
346
+ ...(options.traceDeps ?? []).map((name) => name.replace(/\*$/, ""))
347
+ ])].filter((name) => !inline.has(name));
348
+ }
349
+ /** 按平台发布的原生二进制包,如 @libsql/darwin-arm64、@rollup/rollup-linux-x64-gnu */
350
+ const PLATFORM_PACKAGE_RE = /(?:^|[-/])(?:darwin|win32|linux|freebsd)-(?:x64|arm64|ia32|arm)/;
351
+ /**
352
+ * 用 nf3(@vercel/nft)从产物入口追踪运行时实际加载的文件,复制为 `<serverDir>/node_modules`。
353
+ * 同名多版本放在 `node_modules/.nf3/<name>@<version>` 并链接到依赖方。
354
+ */
355
+ async function traceDependencies(serverDir, root, options) {
356
+ const inputs = walk(serverDir).filter((file) => /\.m?js$/.test(file) && !file.startsWith(join(serverDir, "client")));
357
+ const names = options.traceDeps ?? [];
358
+ const layout = await traceNodeModules(inputs, {
359
+ rootDir: root,
360
+ outDir: serverDir,
361
+ ...options.conditions && { conditions: [
362
+ ...options.conditions,
363
+ "import",
364
+ "default"
365
+ ] },
366
+ traceInclude: names.filter((name) => !name.endsWith("*")),
367
+ fullTraceInclude: names.filter((name) => name.endsWith("*")).map((name) => name.slice(0, -1)),
368
+ chmod: true
369
+ });
370
+ const packages = Object.keys(layout.packages).sort();
371
+ return {
372
+ packages,
373
+ foreignNative: (options.platform ?? process.platform) === process.platform ? [] : packages.filter((name) => PLATFORM_PACKAGE_RE.test(name) || NodeNativePackages.includes(name))
374
+ };
375
+ }
376
+ const builtins = new Set(builtinModules);
377
+ /** 产物中的 import / export ... from / import() 语句(Rolldown 输出的规范格式) */
378
+ const IMPORT_RE = /(?:^|[;\n])\s*(?:import|export)\s[^;]*?\bfrom\s*["']([^"']+)["']|(?:^|[;\n])\s*import\s*["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']\s*\)/g;
379
+ const PACKAGE_RE = /^(?:@[\w.~-]+\/)?[\w.~-]+(?:\/.*)?$/;
380
+ /** 扫描产物中的裸模块导入,得到运行时仍需要的包名 */
381
+ function scanExternals(dir) {
382
+ const names = /* @__PURE__ */ new Set();
383
+ for (const file of walk(dir)) {
384
+ if (!/\.m?js$/.test(file)) continue;
385
+ for (const match of readFileSync(file, "utf8").matchAll(IMPORT_RE)) {
386
+ const specifier = match[1] ?? match[2] ?? match[3];
387
+ if (!specifier || !PACKAGE_RE.test(specifier) || specifier.startsWith(".") || builtins.has(specifier.split("/")[0])) continue;
388
+ names.add(packageName$2(specifier));
389
+ }
390
+ }
391
+ return [...names].sort();
392
+ }
393
+ /** 从 root 开始按 Node 解析规则查找已安装的包(跟随 pnpm 的符号链接) */
394
+ function findInstalled(name, from) {
395
+ for (let dir = from;; dir = dirname(dir)) {
396
+ const candidate = join(dir, "node_modules", name, "package.json");
397
+ if (existsSync(candidate)) {
398
+ const real = realpathSync(dirname(candidate));
399
+ const { version } = JSON.parse(readFileSync(join(real, "package.json"), "utf8"));
400
+ return {
401
+ name,
402
+ version,
403
+ dir: real
404
+ };
405
+ }
406
+ if (dirname(dir) === dir) return void 0;
407
+ }
408
+ }
409
+ /** 生成只包含运行时依赖的 package.json(Docker 中 npm install 使用) */
410
+ async function writeRuntimePackageJson(file, root, names) {
411
+ const manifest = join(root, "package.json");
412
+ const project = existsSync(manifest) ? JSON.parse(readFileSync(manifest, "utf8")) : {};
413
+ const dependencies = {};
414
+ for (const name of names) {
415
+ const found = findInstalled(name, root);
416
+ if (found) dependencies[name] = found.version;
417
+ }
418
+ await writeFile(file, `${JSON.stringify({
419
+ name: project.name ?? "app",
420
+ private: true,
421
+ type: "module",
422
+ dependencies
423
+ }, null, 2)}\n`);
424
+ }
425
+ function packageName$2(specifier) {
426
+ const parts = specifier.split("/");
427
+ return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
428
+ }
429
+ function walk(dir) {
430
+ if (!existsSync(dir)) return [];
431
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
432
+ const path = join(dir, entry.name);
433
+ if (entry.isDirectory()) return entry.name === "node_modules" ? [] : walk(path);
434
+ return [path];
435
+ });
436
+ }
437
+ // src/presets/types.ts
438
+ const PRESET_NAMES = [
439
+ "node",
440
+ "bun",
441
+ "deno",
442
+ "docker",
443
+ "cloudflare",
444
+ "vercel",
445
+ "netlify",
446
+ "aws-lambda"
447
+ ];
448
+ // src/presets/index.ts
449
+ const header = `import process from 'node:process'
450
+ import { fileURLToPath } from 'node:url'
451
+ import { createApp } from '${VIRTUAL_MODULES.app}'
452
+ `;
453
+ function staticOption(context, layout) {
454
+ if (!context.fullstack || !layout.staticFromServer) return "";
455
+ return `static: { dir: fileURLToPath(new URL(${JSON.stringify(layout.staticFromServer)}, import.meta.url)), spaFallback: true, exclude: ${JSON.stringify(context.prefixes)} },`;
456
+ }
457
+ /** 常驻进程:监听端口 */
458
+ function listenEntry(context) {
459
+ return `${header}
460
+ const app = await createApp({ mode: 'production', ${staticOption(context, context.layout)} })
461
+ app.serve({
462
+ port: Number(process.env.PORT) || ${context.port},
463
+ hostname: process.env.HOST || ${JSON.stringify(context.host ?? "0.0.0.0")},
464
+ })
465
+ `;
466
+ }
467
+ /** 按需创建应用(Serverless / Edge 冷启动时) */
468
+ function lazyApp(context, extra = "") {
469
+ return `${header}
470
+ let app
471
+ function getApp() {
472
+ return (app ??= createApp({ mode: 'production', ${staticOption(context, context.layout)} ${extra} }))
473
+ }
474
+ void process
475
+ void fileURLToPath
476
+ `;
477
+ }
478
+ /**
479
+ * 生产迁移脚本(migrate.mjs):迁移文件已打包进产物,不需要源码与开发工具链。
480
+ * `node migrate.mjs [--connection <name>] [--all]`
481
+ */
482
+ function migrateEntryCode() {
483
+ return `import process from 'node:process'
484
+ import { parseArgs } from 'node:util'
485
+ import { runMigrations } from '@hazejs/orm'
486
+ import { createApp } from '${VIRTUAL_MODULES.app}'
487
+ import { files } from '${VIRTUAL_MODULES.migrations}'
488
+
489
+ const { values } = parseArgs({
490
+ options: {
491
+ connection: { type: 'string', short: 'c' },
492
+ all: { type: 'boolean', default: false },
493
+ },
494
+ })
495
+ const app = await createApp({ mode: 'production' })
496
+ try {
497
+ for (const result of await runMigrations({ files, connection: values.connection, all: values.all })) {
498
+ app.logger.info(result.skipped
499
+ ? \`[\${result.connection}] skipped (\${result.skipped})\`
500
+ : \`[\${result.connection}] \${result.applied} of \${result.total} migrations applied (\${result.folder})\`)
501
+ }
502
+ }
503
+ catch (error) {
504
+ app.logger.error('Migration failed', error)
505
+ process.exitCode = 1
506
+ }
507
+ finally {
508
+ await app.dispose()
509
+ }
510
+ `;
511
+ }
512
+ function serverLayout(context, dir = "server") {
513
+ return {
514
+ serverDir: join(context.outDir, dir),
515
+ clientDir: context.fullstack ? join(context.outDir, "client") : void 0,
516
+ staticFromServer: "../client"
517
+ };
518
+ }
519
+ function packageName$1(root) {
520
+ const file = join(root, "package.json");
521
+ return ((existsSync(file) ? JSON.parse(readFileSync(file, "utf8")).name : void 0) ?? "hazejs-app").replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
522
+ }
523
+ const rel = (context, path) => normalizePath(relative(context.root, path)) || ".";
524
+ /**
525
+ * 队列 worker 与定时任务(worker.mjs):
526
+ * `node worker.mjs queue [--queue a,b] [--concurrency 5] [--connection redis]`、
527
+ * `node worker.mjs schedule`(常驻)、`node worker.mjs schedule:run`(执行一次到期任务)
528
+ */
529
+ function workerEntryCode() {
530
+ return `import process from 'node:process'
531
+ import { parseArgs } from 'node:util'
532
+ import { QueueManager, Scheduler } from '@hazejs/core'
533
+ import { createApp } from '${VIRTUAL_MODULES.app}'
534
+
535
+ const { positionals, values } = parseArgs({
536
+ allowPositionals: true,
537
+ options: {
538
+ queue: { type: 'string', short: 'q' },
539
+ concurrency: { type: 'string', short: 'c' },
540
+ connection: { type: 'string' },
541
+ },
542
+ })
543
+ const command = positionals[0] ?? 'queue'
544
+ const app = await createApp({ mode: 'production' })
545
+ const controller = new AbortController()
546
+ let stopQueue
547
+ const shutdown = async () => {
548
+ controller.abort()
549
+ // 等待执行中的任务完成
550
+ await stopQueue?.()
551
+ await app.dispose()
552
+ process.exit(0)
553
+ }
554
+ process.once('SIGTERM', shutdown)
555
+ process.once('SIGINT', shutdown)
556
+
557
+ if (command === 'queue') {
558
+ const queues = values.queue?.split(',').map(name => name.trim()).filter(Boolean) ?? ['default']
559
+ const worker = await app.make(QueueManager).work({ connection: values.connection, queues, concurrency: Number(values.concurrency ?? 1) })
560
+ stopQueue = worker.stop
561
+ app.logger.info(\`Queue worker started: \${queues.join(', ')}\`)
562
+ }
563
+ else if (command === 'schedule') {
564
+ app.logger.info(\`Scheduler started: \${app.make(Scheduler).tasks().length} task(s)\`)
565
+ await app.make(Scheduler).work(controller.signal)
566
+ }
567
+ else if (command === 'schedule:run') {
568
+ const runs = await app.make(Scheduler).runDue()
569
+ await app.dispose()
570
+ process.exit(runs.some(run => run.status === 'failed') ? 1 : 0)
571
+ }
572
+ else {
573
+ app.logger.error(\`Unknown command "\${command}" (queue | schedule | schedule:run)\`)
574
+ await app.dispose()
575
+ process.exit(1)
576
+ }
577
+ `;
578
+ }
579
+ function workerHint(context, command) {
580
+ const worker = rel(context, join(context.layout.serverDir, "worker.mjs"));
581
+ return `\nQueue worker: ${command} ${worker} queue${context.schedules ? `\nScheduler: ${command} ${worker} schedule` : ""}`;
582
+ }
583
+ function migrateHint(context, command) {
584
+ return context.orm ? `\nMigrate: ${command} ${rel(context, join(context.layout.serverDir, "migrate.mjs"))}` : "";
585
+ }
586
+ const presets = {
587
+ "node": {
588
+ name: "node",
589
+ bundle: "drivers-external",
590
+ trace: "default",
591
+ layout: (context) => context.fullstack ? serverLayout(context) : {
592
+ serverDir: context.outDir,
593
+ clientDir: void 0,
594
+ staticFromServer: void 0
595
+ },
596
+ migrateScript: true,
597
+ entry: listenEntry,
598
+ hint: (context) => `Run: node ${rel(context, join(context.layout.serverDir, "index.mjs"))} (or haze start)${migrateHint(context, "node")}${workerHint(context, "node")}`
599
+ },
600
+ "bun": {
601
+ name: "bun",
602
+ bundle: "drivers-external",
603
+ trace: "default",
604
+ conditions: [
605
+ "bun",
606
+ "module",
607
+ "node",
608
+ "production"
609
+ ],
610
+ layout: (context) => context.fullstack ? serverLayout(context) : {
611
+ serverDir: context.outDir,
612
+ clientDir: void 0,
613
+ staticFromServer: void 0
614
+ },
615
+ migrateScript: true,
616
+ entry: listenEntry,
617
+ hint: (context) => `Run: bun ${rel(context, join(context.layout.serverDir, "index.mjs"))}${migrateHint(context, "bun")}${workerHint(context, "bun")}`
618
+ },
619
+ "deno": {
620
+ name: "deno",
621
+ bundle: "drivers-external",
622
+ trace: "default",
623
+ conditions: [
624
+ "deno",
625
+ "module",
626
+ "node",
627
+ "production"
628
+ ],
629
+ layout: serverLayout,
630
+ migrateScript: true,
631
+ entry: listenEntry,
632
+ hint: (context) => `Run: deno run -A ${rel(context, join(context.layout.serverDir, "index.mjs"))}${migrateHint(context, "deno run -A")}${workerHint(context, "deno run -A")}`
633
+ },
634
+ "docker": {
635
+ name: "docker",
636
+ bundle: "drivers-external",
637
+ layout: serverLayout,
638
+ migrateScript: true,
639
+ entry: listenEntry,
640
+ async finalize(context) {
641
+ await writeRuntimePackageJson(join(context.outDir, "package.json"), context.root, context.externals);
642
+ await writeFile(join(context.outDir, "Dockerfile"), `# 由 haze build --preset docker 生成
643
+ FROM node:22-slim
644
+ WORKDIR /app
645
+ ENV NODE_ENV=production
646
+ ENV PORT=${context.port}
647
+ COPY package.json ./
648
+ RUN npm install --omit=dev --no-audit --no-fund
649
+ COPY . .
650
+ EXPOSE ${context.port}
651
+ CMD ["node", "server/index.mjs"]
652
+ `);
653
+ await writeFile(join(context.outDir, ".dockerignore"), "node_modules\nDockerfile\n.dockerignore\n");
654
+ },
655
+ hint: (context) => `Build image: docker build -t ${packageName$1(context.root)} ${rel(context, context.outDir)}\nRun: docker run -p ${context.port}:${context.port} -e APP_KEY=<key> ${packageName$1(context.root)}${context.orm ? `\nMigrate: docker run --rm ${packageName$1(context.root)} node server/migrate.mjs` : ""}\nQueue worker: docker run ${packageName$1(context.root)} node server/worker.mjs queue${context.schedules ? `\nScheduler: docker run ${packageName$1(context.root)} node server/worker.mjs schedule` : ""}`
656
+ },
657
+ "aws-lambda": {
658
+ name: "aws-lambda",
659
+ bundle: "drivers-external",
660
+ trace: "always",
661
+ runtimePlatform: "linux",
662
+ layout: (context) => ({
663
+ serverDir: join(context.outDir, "lambda"),
664
+ clientDir: context.fullstack ? join(context.outDir, "lambda", "client") : void 0,
665
+ staticFromServer: "./client"
666
+ }),
667
+ entry: (context) => `${lazyApp(context)}
668
+ import { createLambdaHandler } from '@hazejs/core'
669
+
670
+ /** API Gateway(HTTP API / REST API)、ALB、Function URL 事件 */
671
+ export const handler = createLambdaHandler(async () => (await getApp()).fetch)
672
+
673
+ import { Scheduler } from '@hazejs/core'
674
+
675
+ /** 定时任务:handler 设为 index.schedule,由 EventBridge 每分钟触发(rate(1 minute)) */
676
+ export async function schedule(event = {}) {
677
+ const runs = await (await getApp()).make(Scheduler).runDue(event.time ? new Date(event.time) : new Date())
678
+ return runs.map(({ name, status, reason }) => ({ name, status, reason }))
679
+ }
680
+ ${context.orm ? `
681
+ import { runMigrations } from '@hazejs/orm'
682
+ import { files } from '${VIRTUAL_MODULES.migrations}'
683
+
684
+ /** 生产迁移:handler 设为 index.migrate 的函数(或直接调用),事件 { connection?, all? } */
685
+ export async function migrate(event = {}) {
686
+ await getApp()
687
+ return runMigrations({ files, connection: event.connection, all: event.all })
688
+ }
689
+ ` : ""}`,
690
+ async finalize(context) {
691
+ const dir = context.layout.serverDir;
692
+ await writeFile(join(dir, "package.json"), "{ \"type\": \"module\" }\n");
693
+ },
694
+ hint: (context) => `Zip ${rel(context, context.layout.serverDir)} and set the Lambda handler to index.handler (runtime nodejs22.x), with APP_KEY in the environment${context.orm ? "\nMigrate: invoke the same bundle with handler index.migrate (event: { \"connection\"?: string, \"all\"?: boolean })" : ""}${context.schedules ? "\nScheduler: a function with handler index.schedule, triggered by an EventBridge rule rate(1 minute)" : ""}`
695
+ },
696
+ "vercel": {
697
+ name: "vercel",
698
+ bundle: "drivers-external",
699
+ trace: "always",
700
+ runtimePlatform: "linux",
701
+ layout: (context) => ({
702
+ serverDir: join(context.root, ".vercel", "output", "functions", "__server.func"),
703
+ clientDir: join(context.root, ".vercel", "output", "static"),
704
+ staticFromServer: void 0
705
+ }),
706
+ entry: (context) => `${lazyApp(context)}
707
+ import { Scheduler, toNodeHandler } from '@hazejs/core'
708
+
709
+ let handler
710
+ export default async function vercelHandler(req, res) {
711
+ // Vercel Cron:调用 /__haze/schedule,并带上 Authorization: Bearer $CRON_SECRET
712
+ if (req.url?.split('?')[0] === '/__haze/schedule') {
713
+ if (!process.env.CRON_SECRET || req.headers.authorization !== \`Bearer \${process.env.CRON_SECRET}\`) {
714
+ res.statusCode = 401
715
+ return res.end('Unauthorized')
716
+ }
717
+ const runs = await (await getApp()).make(Scheduler).runDue()
718
+ res.setHeader('content-type', 'application/json')
719
+ return res.end(JSON.stringify(runs.map(({ name, status, reason }) => ({ name, status, reason }))))
720
+ }
721
+ handler ??= toNodeHandler((await getApp()).h3)
722
+ return handler(req, res)
723
+ }
724
+ `,
725
+ async finalize(context) {
726
+ const output = join(context.root, ".vercel", "output");
727
+ const dir = context.layout.serverDir;
728
+ await mkdir(join(output, "static"), { recursive: true });
729
+ await writeFile(join(dir, ".vc-config.json"), `${JSON.stringify({
730
+ runtime: "nodejs22.x",
731
+ handler: "index.mjs",
732
+ launcherType: "Nodejs",
733
+ shouldAddSourcemapSupport: true,
734
+ supportsResponseStreaming: true
735
+ }, null, 2)}\n`);
736
+ await writeFile(join(dir, "package.json"), "{ \"type\": \"module\" }\n");
737
+ const cron = context.schedules ? [{
738
+ src: "^/__haze/schedule$",
739
+ dest: "/__server"
740
+ }] : [];
741
+ const routes = context.fullstack ? [
742
+ { handle: "filesystem" },
743
+ ...cron,
744
+ ...context.prefixes.map((prefix) => ({
745
+ src: `^${prefix}(?:/.*)?$`,
746
+ dest: "/__server"
747
+ })),
748
+ {
749
+ src: "/(.*)",
750
+ dest: "/index.html"
751
+ }
752
+ ] : [{ handle: "filesystem" }, {
753
+ src: "/(.*)",
754
+ dest: "/__server"
755
+ }];
756
+ const crons = context.schedules ? [{
757
+ path: "/__haze/schedule",
758
+ schedule: context.vercel?.cron ?? "* * * * *"
759
+ }] : void 0;
760
+ await writeFile(join(output, "config.json"), `${JSON.stringify({
761
+ version: 3,
762
+ routes,
763
+ ...crons && { crons }
764
+ }, null, 2)}\n`);
765
+ },
766
+ hint: (context) => `Deploy the prebuilt output: vercel deploy --prebuilt (set APP_KEY with: vercel env add APP_KEY)${context.schedules ? "\nScheduler: Vercel Cron calls /__haze/schedule; set CRON_SECRET (vercel env add CRON_SECRET). Hobby plans only allow daily crons (haze.config.ts: vercel.cron)" : ""}`
767
+ },
768
+ "netlify": {
769
+ name: "netlify",
770
+ bundle: "drivers-external",
771
+ layout: (context) => ({
772
+ ...serverLayout(context),
773
+ staticFromServer: void 0
774
+ }),
775
+ entry: (context) => `${lazyApp(context)}
776
+ /** Netlify Functions(v2):Web Request → Response */
777
+ export default async function netlifyHandler(request) {
778
+ return (await getApp()).fetch(request)
779
+ }
780
+ `,
781
+ async finalize(context) {
782
+ const functions = join(context.root, ".netlify", "v1", "functions");
783
+ await mkdir(functions, { recursive: true });
784
+ const entry = normalizePath(relative(functions, join(context.layout.serverDir, "index.mjs")));
785
+ const paths = context.fullstack ? context.prefixes.flatMap((prefix) => [prefix, `${prefix}/*`]) : ["/*"];
786
+ await writeFile(join(functions, "haze-server.mjs"), `// 由 haze build --preset netlify 生成
787
+ export { default } from '${entry.startsWith(".") ? entry : `./${entry}`}'
788
+
789
+ export const config = {
790
+ name: 'hazejs server',
791
+ generator: 'hazejs',
792
+ path: ${JSON.stringify(paths)},
793
+ preferStatic: true,
794
+ }
795
+ `);
796
+ const publish = context.layout.clientDir ?? join(context.outDir, "public");
797
+ await mkdir(publish, { recursive: true });
798
+ if (context.fullstack) await writeFile(join(publish, "_redirects"), "/* /index.html 200\n");
799
+ const toml = join(context.root, "netlify.toml");
800
+ if (!existsSync(toml)) await writeFile(toml, `[build]\n command = "npx haze build --preset netlify"\n publish = "${rel(context, publish)}"\n`);
801
+ },
802
+ hint: (context) => `${existsSync(join(context.root, "netlify.toml")) ? "Deploy with: netlify deploy --build (publish directory is set in netlify.toml)" : "Deploy with: netlify deploy --build"}\nSet APP_KEY with: netlify env:set APP_KEY <key>`
803
+ },
804
+ "cloudflare": {
805
+ name: "cloudflare",
806
+ bundle: "all",
807
+ conditions: [
808
+ "workerd",
809
+ "worker",
810
+ "browser",
811
+ "module",
812
+ "production"
813
+ ],
814
+ mainFields: [
815
+ "browser",
816
+ "module",
817
+ "jsnext:main",
818
+ "jsnext",
819
+ "main"
820
+ ],
821
+ target: "es2022",
822
+ platform: "browser",
823
+ singleFile: true,
824
+ unsupported: EDGE_UNSUPPORTED_DRIVERS,
825
+ external: [/^cloudflare:/],
826
+ layout: (context) => ({
827
+ serverDir: context.outDir,
828
+ clientDir: context.fullstack ? join(context.outDir, "client") : void 0,
829
+ staticFromServer: void 0
830
+ }),
831
+ entry: (context) => `${lazyApp(context, `env: false,`)}
832
+ import { QueueManager, Scheduler, setPlatformBindings } from '@hazejs/core'
833
+ ${context.websocket ? `import { createWebSocketDurableObject } from '@hazejs/core/cloudflare'
834
+
835
+ // WebSocket:所有连接在同一个 Durable Object 中,订阅 / 广播全局生效;钩子在 DO 内经应用解析(含中间件)
836
+ const websocket = createWebSocketDurableObject({ fetch: async request => (await getApp()).fetch(request), bind: env => bind(env) })
837
+ export const HazeWebSocket = websocket.HazeWebSocket
838
+ ` : ""}
839
+ function bind(env) {
840
+ setPlatformBindings(env)
841
+ if (!app) {
842
+ for (const [key, value] of Object.entries(env ?? {})) {
843
+ if (typeof value === 'string' && process.env[key] === undefined)
844
+ process.env[key] = value
845
+ }
846
+ }
847
+ }
848
+
849
+ /** Cloudflare Workers:静态资源由 Workers Assets 提供,环境变量与 D1 等来自绑定 */
850
+ export default {
851
+ async fetch(request, env, ctx) {
852
+ bind(env)
853
+ ${context.websocket ? ` if (request.headers.get('upgrade')?.toLowerCase() === 'websocket')
854
+ return websocket.handleUpgrade(request, env, ctx)
855
+ ` : ""} return (await getApp()).fetch(request)
856
+ },
857
+ /** Cloudflare Queues 消费:重试映射为 message.retry({ delaySeconds }) */
858
+ async queue(batch, env) {
859
+ bind(env)
860
+ await (await getApp()).make(QueueManager).handleBatch(batch)
861
+ },
862
+ /** Cron Triggers:执行该分钟到期的定时任务 */
863
+ async scheduled(controller, env, ctx) {
864
+ bind(env)
865
+ ctx.waitUntil((async () => (await getApp()).make(Scheduler).runDue(new Date(controller.scheduledTime)))())
866
+ },
867
+ }
868
+ `,
869
+ async finalize(context) {
870
+ const { compatibility_flags: flags, assets, d1_databases: d1, ...user } = context.cloudflare?.wrangler ?? {};
871
+ const config = {
872
+ name: packageName$1(context.root),
873
+ main: "index.mjs",
874
+ compatibility_date: "2026-01-01",
875
+ ...user,
876
+ compatibility_flags: [.../* @__PURE__ */ new Set(["nodejs_compat", ...flags ?? []])]
877
+ };
878
+ if (context.fullstack || assets) config.assets = {
879
+ ...context.fullstack && {
880
+ directory: "./client",
881
+ not_found_handling: "single-page-application",
882
+ run_worker_first: context.prefixes.map((prefix) => `${prefix}/*`)
883
+ },
884
+ ...assets
885
+ };
886
+ if (d1?.length) {
887
+ config.d1_databases = d1.map((database) => ({
888
+ migrations_dir: "migrations",
889
+ ...database
890
+ }));
891
+ const source = join(context.srcDir, "database", "migrations");
892
+ if (existsSync(source)) await cp(source, join(context.outDir, "migrations"), { recursive: true });
893
+ }
894
+ if (context.websocket) {
895
+ const durable = config.durable_objects ?? {};
896
+ config.durable_objects = {
897
+ ...durable,
898
+ bindings: [...durable.bindings ?? [], {
899
+ name: "HAZE_WEBSOCKET",
900
+ class_name: "HazeWebSocket"
901
+ }]
902
+ };
903
+ config.migrations = [...config.migrations ?? [], {
904
+ tag: "haze-websocket-v1",
905
+ new_sqlite_classes: ["HazeWebSocket"]
906
+ }];
907
+ }
908
+ if (context.schedules && !config.triggers) config.triggers = { crons: ["* * * * *"] };
909
+ await writeFile(join(context.outDir, "wrangler.json"), `${JSON.stringify(config, null, 2)}\n`);
910
+ },
911
+ hint: (context) => {
912
+ const wrangler = rel(context, join(context.outDir, "wrangler.json"));
913
+ return `Deploy: npx wrangler deploy -c ${wrangler}\nSecrets: npx wrangler secret put APP_KEY -c ${wrangler}${(context.cloudflare?.wrangler?.d1_databases ?? []).map((database) => `\nApply D1 migrations: npx wrangler d1 migrations apply ${database.database_name ?? database.binding} --remote -c ${wrangler}`).join("")}`;
914
+ }
915
+ }
916
+ };
917
+ function resolvePreset(name) {
918
+ const preset = presets[name ?? "node"];
919
+ if (!preset) throw new Error(`Unknown preset "${name}". Available: ${PRESET_NAMES.join(", ")}`);
920
+ return preset;
921
+ }
922
+ // src/preview.ts
923
+ function manifestFile(root) {
924
+ return join(root, ".haze", "build.json");
925
+ }
926
+ function writeBuildManifest(root, manifest) {
927
+ const file = manifestFile(root);
928
+ mkdirSync(dirname(file), { recursive: true });
929
+ writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
930
+ }
931
+ function readBuildManifest(root) {
932
+ const file = manifestFile(root);
933
+ if (!existsSync(file)) throw new Error("No build found. Run `vite build` (or `haze build`) before previewing.");
934
+ return JSON.parse(readFileSync(file, "utf8"));
935
+ }
936
+ /** 前端静态文件由 Vite 托管的预设(产物中由平台 CDN 提供),其余预设的产物自己托管全部请求 */
937
+ function staticDirOf(manifest) {
938
+ return manifest.fullstack && !manifest.layout.staticFromServer ? manifest.layout.clientDir : void 0;
939
+ }
940
+ /**
941
+ * 按部署预设运行**构建产物**(而不是源码):
942
+ * - node / bun / deno / docker:在随机端口启动产物进程,请求全部转发给它
943
+ * - aws-lambda / vercel / netlify:进程内加载函数 handler
944
+ * - cloudflare:用 Miniflare(workerd)运行 Worker,绑定来自 wrangler.json
945
+ */
946
+ async function startPreview(root, manifest, logger) {
947
+ const prefixes = manifest.prefixes;
948
+ const matches = staticDirOf(manifest) ? (url) => prefixes.some((prefix) => url === prefix || url.startsWith(`${prefix}/`) || url.startsWith(`${prefix}?`)) : () => true;
949
+ if ([
950
+ "aws-lambda",
951
+ "vercel",
952
+ "netlify"
953
+ ].includes(manifest.preset)) {
954
+ const { loadEnv } = await import("@hazejs/core");
955
+ loadEnv({
956
+ cwd: root,
957
+ mode: "production"
958
+ });
959
+ }
960
+ switch (manifest.preset) {
961
+ case "node":
962
+ case "bun":
963
+ case "deno":
964
+ case "docker": return {
965
+ matches,
966
+ ...await startProcess(root, manifest)
967
+ };
968
+ case "aws-lambda": {
969
+ const { lambdaResultToResponse, requestToLambdaEvent } = await import("@hazejs/core");
970
+ const { handler } = await importEntry(manifest.entry);
971
+ return fetchPreview(matches, async (request) => lambdaResultToResponse(await handler(await requestToLambdaEvent(request))));
972
+ }
973
+ case "vercel": {
974
+ const { default: handler } = await importEntry(manifest.entry);
975
+ return {
976
+ matches,
977
+ handle: handler,
978
+ close: async () => {}
979
+ };
980
+ }
981
+ case "netlify": {
982
+ const { default: handler } = await importEntry(join(root, ".netlify", "v1", "functions", "haze-server.mjs"));
983
+ return fetchPreview(matches, (request) => handler(request, {}));
984
+ }
985
+ case "cloudflare": return {
986
+ ...await startWorker(root, manifest, logger),
987
+ matches
988
+ };
989
+ }
990
+ }
991
+ function fetchPreview(matches, fetch, close = async () => {}) {
992
+ return {
993
+ matches,
994
+ handle: toNodeHandler(fetch),
995
+ close
996
+ };
997
+ }
998
+ async function importEntry(file) {
999
+ return import(`${pathToFileURL(file).href}?t=${Date.now()}`);
1000
+ }
1001
+ /** 产物按预设的运行时构建(bun / deno 的导出条件),必须用对应运行时执行 */
1002
+ function runtimeCommand(root, preset, entry) {
1003
+ if (preset === "bun") {
1004
+ const bun = which("bun");
1005
+ if (!bun) throw new Error("Previewing the bun build needs Bun (https://bun.sh)");
1006
+ return [bun, entry];
1007
+ }
1008
+ if (preset === "deno") {
1009
+ const deno = which("deno");
1010
+ if (deno) return [
1011
+ deno,
1012
+ "run",
1013
+ "-A",
1014
+ entry
1015
+ ];
1016
+ try {
1017
+ return [
1018
+ process.execPath,
1019
+ createRequire(join(root, "package.json")).resolve("deno/bin.cjs"),
1020
+ "run",
1021
+ "-A",
1022
+ entry
1023
+ ];
1024
+ } catch {
1025
+ throw new Error("Previewing the deno build needs Deno (https://deno.com) or the deno npm package");
1026
+ }
1027
+ }
1028
+ return [process.execPath, entry];
1029
+ }
1030
+ async function startProcess(root, manifest) {
1031
+ const port = await freePort();
1032
+ const [command, ...args] = runtimeCommand(root, manifest.preset, manifest.entry);
1033
+ const loaded = globalThis.__HAZEJS__envKeys;
1034
+ const inherited = Object.fromEntries(Object.entries(process.env).filter(([key]) => !loaded?.has(key)));
1035
+ const child = spawn(command, args, {
1036
+ cwd: root,
1037
+ env: {
1038
+ ...inherited,
1039
+ NODE_ENV: "production",
1040
+ PORT: String(port),
1041
+ HOST: "127.0.0.1"
1042
+ },
1043
+ stdio: [
1044
+ "ignore",
1045
+ "pipe",
1046
+ "pipe"
1047
+ ]
1048
+ });
1049
+ child.stdout?.pipe(process.stdout);
1050
+ child.stderr?.pipe(process.stderr);
1051
+ const origin = `http://127.0.0.1:${port}`;
1052
+ await waitUntilReady(origin, child);
1053
+ return {
1054
+ handle: toNodeHandler((request) => forward(request, origin)),
1055
+ upgrade: proxyUpgrade(port),
1056
+ close: () => stopProcess(child)
1057
+ };
1058
+ }
1059
+ /** 转发到产物进程(去掉逐跳头;fetch 已解压响应体,去掉对应的编码头) */
1060
+ async function forward(request, origin) {
1061
+ const url = new URL(request.url);
1062
+ const headers = new Headers(request.headers);
1063
+ for (const name of [
1064
+ "host",
1065
+ "connection",
1066
+ "keep-alive",
1067
+ "transfer-encoding",
1068
+ "upgrade"
1069
+ ]) headers.delete(name);
1070
+ headers.set("x-forwarded-host", url.host);
1071
+ headers.set("x-forwarded-proto", url.protocol.slice(0, -1));
1072
+ const hasBody = request.method !== "GET" && request.method !== "HEAD";
1073
+ const response = await fetch(new URL(url.pathname + url.search, origin), {
1074
+ method: request.method,
1075
+ headers,
1076
+ body: hasBody ? request.body : void 0,
1077
+ redirect: "manual",
1078
+ duplex: "half"
1079
+ });
1080
+ const out = new Headers(response.headers);
1081
+ out.delete("content-encoding");
1082
+ out.delete("content-length");
1083
+ return new Response(response.body, {
1084
+ status: response.status,
1085
+ statusText: response.statusText,
1086
+ headers: out
1087
+ });
1088
+ }
1089
+ /** 把升级请求原样写给产物进程,之后双向转发字节 */
1090
+ function proxyUpgrade(port) {
1091
+ return (req, socket, head) => {
1092
+ const upstream = connect(port, "127.0.0.1", () => {
1093
+ const lines = [`${req.method} ${req.url} HTTP/${req.httpVersion}`];
1094
+ for (let index = 0; index < req.rawHeaders.length; index += 2) lines.push(`${req.rawHeaders[index]}: ${req.rawHeaders[index + 1]}`);
1095
+ upstream.write(`${lines.join("\r\n")}\r\n\r\n`);
1096
+ if (head.length) upstream.write(head);
1097
+ upstream.pipe(socket);
1098
+ socket.pipe(upstream);
1099
+ });
1100
+ const destroy = () => {
1101
+ upstream.destroy();
1102
+ socket.destroy();
1103
+ };
1104
+ upstream.on("error", destroy);
1105
+ socket.on("error", destroy);
1106
+ };
1107
+ }
1108
+ async function startWorker(root, manifest, logger) {
1109
+ const dir = manifest.layout.serverDir;
1110
+ const wrangler = JSON.parse(readFileSync(join(dir, "wrangler.json"), "utf8"));
1111
+ let Miniflare;
1112
+ try {
1113
+ const require = createRequire(join(root, "package.json"));
1114
+ ({Miniflare} = await import(pathToFileURL(require.resolve("miniflare")).href));
1115
+ } catch {
1116
+ throw new Error(`Previewing the cloudflare build needs miniflare (pnpm add -D miniflare), or run: npx wrangler dev -c ${join(dir, "wrangler.json")}`);
1117
+ }
1118
+ const persist = join(root, ".haze", "preview");
1119
+ const d1 = wrangler.d1_databases ?? [];
1120
+ const mf = new Miniflare({
1121
+ modules: true,
1122
+ scriptPath: manifest.entry,
1123
+ compatibilityDate: wrangler.compatibility_date,
1124
+ compatibilityFlags: wrangler.compatibility_flags,
1125
+ bindings: {
1126
+ ...envFileValues(root),
1127
+ ...wrangler.vars
1128
+ },
1129
+ d1Databases: Object.fromEntries(d1.map((database) => [database.binding, database.database_id ?? database.binding])),
1130
+ d1Persist: join(persist, "d1"),
1131
+ kvNamespaces: Object.fromEntries((wrangler.kv_namespaces ?? []).map((namespace) => [namespace.binding, namespace.id ?? namespace.binding])),
1132
+ durableObjects: Object.fromEntries((wrangler.durable_objects?.bindings ?? []).map((binding) => [binding.name, binding.class_name])),
1133
+ kvPersist: join(persist, "kv")
1134
+ });
1135
+ for (const database of d1) {
1136
+ const applied = await applyD1Migrations(await mf.getD1Database(database.binding), join(dir, database.migrations_dir ?? "migrations"));
1137
+ if (applied.length) logger.info(`[hazejs] D1 ${database.database_name ?? database.binding}: applied ${applied.join(", ")}`);
1138
+ }
1139
+ return fetchPreview(() => true, (request) => mf.dispatchFetch(request.url, {
1140
+ method: request.method,
1141
+ headers: request.headers,
1142
+ body: request.body,
1143
+ redirect: "manual",
1144
+ duplex: "half"
1145
+ }), () => mf.dispose());
1146
+ }
1147
+ /** 读取项目根目录的 .env / .env.production(不写入 process.env) */
1148
+ function envFileValues(root) {
1149
+ const values = {};
1150
+ for (const file of [".env", ".env.production"]) {
1151
+ const path = join(root, file);
1152
+ if (existsSync(path)) Object.assign(values, parseEnv(readFileSync(path, "utf8")));
1153
+ }
1154
+ return values;
1155
+ }
1156
+ async function applyD1Migrations(db, dir) {
1157
+ if (!existsSync(dir)) return [];
1158
+ await db.exec("CREATE TABLE IF NOT EXISTS d1_migrations (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL)");
1159
+ const done = new Set((await db.prepare("SELECT name FROM d1_migrations").all()).results.map((row) => row.name));
1160
+ const applied = [];
1161
+ for (const file of readdirSync(dir).filter((file) => file.endsWith(".sql")).sort()) {
1162
+ if (done.has(file)) continue;
1163
+ const statements = readFileSync(join(dir, file), "utf8").split("--> statement-breakpoint").filter((statement) => statement.trim());
1164
+ await db.batch([...statements.map((statement) => db.prepare(statement)), db.prepare("INSERT INTO d1_migrations (name) VALUES (?)").bind(file)]);
1165
+ applied.push(file);
1166
+ }
1167
+ return applied;
1168
+ }
1169
+ async function waitUntilReady(origin, child) {
1170
+ const exited = () => child.exitCode !== null || child.signalCode !== null;
1171
+ for (let attempt = 0; attempt < 150 && !exited(); attempt++) try {
1172
+ await fetch(origin, { redirect: "manual" });
1173
+ return;
1174
+ } catch {
1175
+ await new Promise((resolve) => setTimeout(resolve, 100));
1176
+ }
1177
+ child.kill();
1178
+ throw new Error(exited() ? "The built server exited before it started listening" : `The built server did not respond on ${origin}`);
1179
+ }
1180
+ function stopProcess(child) {
1181
+ if (child.exitCode !== null) return Promise.resolve();
1182
+ return new Promise((resolve) => {
1183
+ child.once("exit", () => resolve());
1184
+ child.kill("SIGTERM");
1185
+ });
1186
+ }
1187
+ function freePort() {
1188
+ return new Promise((resolve, reject) => {
1189
+ const server = createServer$1().listen(0, "127.0.0.1", () => {
1190
+ const { port } = server.address();
1191
+ server.close(() => resolve(port));
1192
+ }).on("error", reject);
1193
+ });
1194
+ }
1195
+ function which(command) {
1196
+ try {
1197
+ return execFileSync(process.platform === "win32" ? "where" : "which", [command], { encoding: "utf8" }).split("\n")[0].trim() || void 0;
1198
+ } catch {
1199
+ return;
1200
+ }
1201
+ }
1202
+ // src/plugin.ts
1203
+ const RESOLVED_PREFIX = "\0";
1204
+ const SOURCE_RE = /\.[cm]?[jt]s$/;
1205
+ const virtualIds = new Set(Object.values(VIRTUAL_MODULES));
1206
+ /**
1207
+ * hazejs 的 Vite 插件。
1208
+ *
1209
+ * - 纯后端项目:由 `hazejs` 命令行内部使用
1210
+ * - Vue 等前端项目:在 vite.config.ts 中加入 `haze()`,开发时把 `/api/**` 交给后端(同端口、热替换),
1211
+ * `vite build` 一次产出 `dist/client` 与 `dist/server`
1212
+ */
1213
+ function haze(options = {}) {
1214
+ const listeners = /* @__PURE__ */ new Set();
1215
+ const settings = {
1216
+ root: process.cwd(),
1217
+ srcDir: process.cwd(),
1218
+ outDir: resolve("dist"),
1219
+ typegen: true,
1220
+ port: 3e3,
1221
+ host: void 0,
1222
+ prefixes: normalizePrefixes(options.prefix),
1223
+ fullstack: !options.standalone,
1224
+ preset: "node",
1225
+ orm: false,
1226
+ cloudflare: void 0,
1227
+ vercel: void 0,
1228
+ externals: {}
1229
+ };
1230
+ let lastBuild;
1231
+ let unsupported = /* @__PURE__ */ new Set();
1232
+ /** 全量打包时,未安装的驱动也替换为空模块(ORM 以字面量导入全部驱动) */
1233
+ let stubMissing = false;
1234
+ const api = {
1235
+ onReload(listener) {
1236
+ listeners.add(listener);
1237
+ return () => listeners.delete(listener);
1238
+ },
1239
+ settings: () => settings,
1240
+ lastBuild: () => lastBuild
1241
+ };
1242
+ let isDev = true;
1243
+ return {
1244
+ name: PLUGIN_NAME,
1245
+ api,
1246
+ async config(userConfig, env) {
1247
+ const base = normalizePath(resolve(userConfig.root ?? process.cwd()));
1248
+ settings.root = base;
1249
+ const fileConfig = options.srcDir === void 0 ? await loadHazeConfig(base) : void 0;
1250
+ settings.srcDir = options.srcDir === void 0 ? normalizePath(fileConfig?.srcDir ?? base) : normalizePath(isAbsolute(options.srcDir) ? options.srcDir : resolve(base, options.srcDir));
1251
+ settings.outDir = normalizePath(options.outDir ?? fileConfig?.outDir ?? resolve(base, "dist"));
1252
+ settings.port = options.port ?? fileConfig?.port ?? 3e3;
1253
+ settings.host = options.host ?? fileConfig?.host;
1254
+ settings.typegen = options.typegen ?? fileConfig?.typegen ?? true;
1255
+ settings.preset = options.preset ?? process.env.HAZE_PRESET ?? fileConfig?.preset ?? "node";
1256
+ settings.cloudflare = options.cloudflare ?? fileConfig?.cloudflare;
1257
+ settings.vercel = options.vercel ?? fileConfig?.vercel;
1258
+ settings.externals = options.externals ?? fileConfig?.externals ?? {};
1259
+ settings.orm = Boolean(findInstalled("@hazejs/orm", base));
1260
+ const config = {};
1261
+ if (options.alias !== false) config.resolve = { alias: [{
1262
+ find: new RegExp(`^${escapeRegExp(options.alias ?? "~")}/`),
1263
+ replacement: `${settings.srcDir}/`
1264
+ }] };
1265
+ if (env.isPreview) {
1266
+ const manifest = existsSync(manifestFile(base)) ? readBuildManifest(base) : void 0;
1267
+ if (manifest) config.environments = { client: { build: { outDir: staticDirOf(manifest) ?? manifest.layout.clientDir ?? manifest.layout.serverDir } } };
1268
+ }
1269
+ if (env.command === "build") {
1270
+ const preset = resolvePreset(settings.preset);
1271
+ const context = presetContext(settings);
1272
+ const layout = preset.layout(context);
1273
+ const migrateScript = Boolean(preset.migrateScript && settings.orm);
1274
+ const trace = preset.trace === "always" || preset.trace === "default" && settings.externals.trace !== false;
1275
+ const bundle = !trace && preset.trace === "default" && preset.name !== "deno" ? "external" : preset.bundle;
1276
+ config.environments = {
1277
+ ssr: {
1278
+ resolve: {
1279
+ ...preset.conditions && { conditions: preset.conditions },
1280
+ ...preset.mainFields && { mainFields: preset.mainFields },
1281
+ ...bundle === "drivers-external" && { noExternal: bundleAllExcept(externalPackages(settings.externals)) },
1282
+ ...bundle === "all" && { noExternal: true }
1283
+ },
1284
+ build: {
1285
+ outDir: layout.serverDir,
1286
+ emptyOutDir: true,
1287
+ target: preset.target ?? "node22",
1288
+ sourcemap: true,
1289
+ rolldownOptions: {
1290
+ ...preset.platform && { platform: preset.platform },
1291
+ ...preset.unsupported?.length && { shimMissingExports: true },
1292
+ ...(preset.external || bundle === "drivers-external") && { external: [...preset.external ?? [], ...bundle === "drivers-external" ? [externalPattern(externalPackages(settings.externals))] : []] },
1293
+ input: {
1294
+ index: VIRTUAL_MODULES.serverEntry,
1295
+ ...migrateScript && { migrate: VIRTUAL_MODULES.migrateEntry },
1296
+ ...preset.migrateScript && { worker: VIRTUAL_MODULES.workerEntry }
1297
+ },
1298
+ output: {
1299
+ entryFileNames: "[name].mjs",
1300
+ chunkFileNames: "chunks/[name]-[hash].mjs",
1301
+ ...preset.singleFile && { codeSplitting: false }
1302
+ }
1303
+ }
1304
+ }
1305
+ },
1306
+ ...settings.fullstack && layout.clientDir && { client: { build: { outDir: layout.clientDir } } }
1307
+ };
1308
+ unsupported = new Set(preset.unsupported);
1309
+ stubMissing = bundle === "all";
1310
+ config.builder = { async buildApp(builder) {
1311
+ await builder.build(builder.environments.ssr);
1312
+ if (settings.fullstack) await builder.build(builder.environments.client);
1313
+ const externals = scanExternals(layout.serverDir).filter((name) => !DRIVER_PACKAGES.includes(name) || findInstalled(name, context.root));
1314
+ const traced = trace ? await traceDependencies(layout.serverDir, context.root, {
1315
+ conditions: preset.conditions,
1316
+ traceDeps: settings.externals.traceDeps,
1317
+ platform: preset.runtimePlatform
1318
+ }) : void 0;
1319
+ if (traced?.foreignNative.length) builder.config.logger.warn(`[hazejs] ${preset.name} runs on ${preset.runtimePlatform}, but these native packages were traced from this ${process.platform} machine: ${traced.foreignNative.join(", ")}. Build on ${preset.runtimePlatform} (CI or Docker) so the right binaries are copied.`);
1320
+ const finalize = {
1321
+ ...context,
1322
+ layout,
1323
+ externals,
1324
+ traced
1325
+ };
1326
+ await preset.finalize?.(finalize);
1327
+ writeBuildManifest(settings.root, {
1328
+ preset: preset.name,
1329
+ layout,
1330
+ entry: join(layout.serverDir, "index.mjs"),
1331
+ fullstack: settings.fullstack,
1332
+ prefixes: settings.prefixes
1333
+ });
1334
+ lastBuild = {
1335
+ preset: preset.name,
1336
+ layout,
1337
+ externals,
1338
+ traced,
1339
+ entry: join(layout.serverDir, "index.mjs"),
1340
+ ...migrateScript && { migrate: join(layout.serverDir, "migrate.mjs") },
1341
+ hint: preset.hint(finalize)
1342
+ };
1343
+ } };
1344
+ }
1345
+ return config;
1346
+ },
1347
+ configResolved(config) {
1348
+ settings.root = normalizePath(config.root);
1349
+ isDev = config.command === "serve";
1350
+ },
1351
+ configureServer(server) {
1352
+ if (!settings.fullstack) return;
1353
+ const host = createAppHost({
1354
+ vite: server,
1355
+ api
1356
+ });
1357
+ const handle = toNodeHandler(host.fetch);
1358
+ const matches = (url) => settings.prefixes.some((prefix) => url === prefix || url.startsWith(`${prefix}/`) || url.startsWith(`${prefix}?`));
1359
+ server.middlewares.use((req, res, next) => {
1360
+ if (!req.url || !matches(req.url)) return next();
1361
+ Promise.resolve(handle(req, res)).catch(next);
1362
+ });
1363
+ server.httpServer?.once("listening", () => void host.start());
1364
+ if (server.httpServer) {
1365
+ registerNodeServer(server.httpServer);
1366
+ const upgrade = nodeUpgradeHandler(host.fetch);
1367
+ server.httpServer.on("upgrade", (req, socket, head) => {
1368
+ if (req.url && matches(req.url) && req.headers["sec-websocket-protocol"] !== "vite-hmr") upgrade(req, socket, head);
1369
+ });
1370
+ }
1371
+ const close = server.close.bind(server);
1372
+ server.close = async () => {
1373
+ await host.close();
1374
+ return close();
1375
+ };
1376
+ },
1377
+ async configurePreviewServer(server) {
1378
+ const manifest = readBuildManifest(settings.root);
1379
+ const preview = await startPreview(settings.root, manifest, server.config.logger);
1380
+ server.config.logger.info(` [hazejs] previewing the ${manifest.preset} build`);
1381
+ server.middlewares.use((req, res, next) => {
1382
+ if (!req.url || !preview.matches(req.url)) return next();
1383
+ Promise.resolve(preview.handle(req, res)).catch(next);
1384
+ });
1385
+ if (preview.upgrade) server.httpServer.on("upgrade", (req, socket, head) => {
1386
+ if (req.url && preview.matches(req.url)) preview.upgrade(req, socket, head);
1387
+ });
1388
+ server.httpServer.once("close", () => void preview.close());
1389
+ },
1390
+ resolveId: {
1391
+ order: "pre",
1392
+ async handler(id, importer) {
1393
+ if (virtualIds.has(id)) return RESOLVED_PREFIX + id;
1394
+ if (this.environment?.name !== "ssr" || isDev) return;
1395
+ const name = packageName(id);
1396
+ if (unsupported.has(name)) return `${RESOLVED_PREFIX}haze-unsupported:${id}`;
1397
+ if (stubMissing && DRIVER_PACKAGES.includes(name) && !await this.resolve(id, importer, { skipSelf: true })) return `${RESOLVED_PREFIX}haze-missing:${id}`;
1398
+ }
1399
+ },
1400
+ load(id) {
1401
+ if (!id.startsWith(RESOLVED_PREFIX)) return;
1402
+ if (id.startsWith(`${RESOLVED_PREFIX}haze-unsupported:`)) {
1403
+ const name = id.slice(`${RESOLVED_PREFIX}haze-unsupported:`.length);
1404
+ return `throw new Error(${JSON.stringify(`"${name}" is not supported on the "${settings.preset}" deployment preset`)})\nexport default undefined\n`;
1405
+ }
1406
+ if (id.startsWith(`${RESOLVED_PREFIX}haze-missing:`)) {
1407
+ const name = id.slice(`${RESOLVED_PREFIX}haze-missing:`.length);
1408
+ return `throw new Error(${JSON.stringify(`"${name}" is not installed`)})\nexport default undefined\n`;
1409
+ }
1410
+ switch (id.slice(1)) {
1411
+ case VIRTUAL_MODULES.manifest: return manifestCode(settings, isDev);
1412
+ case VIRTUAL_MODULES.app: return appCode();
1413
+ case VIRTUAL_MODULES.serverEntry: return serverEntryCode(settings);
1414
+ case VIRTUAL_MODULES.migrations: return migrationsCode(settings);
1415
+ case VIRTUAL_MODULES.migrateEntry: return migrateEntryCode();
1416
+ case VIRTUAL_MODULES.workerEntry: return workerEntryCode();
1417
+ }
1418
+ },
1419
+ hotUpdate({ type, file, modules }) {
1420
+ if (this.environment.name !== "ssr") return;
1421
+ if (file.startsWith(`${settings.root}/.haze/`) || file.endsWith(".d.ts")) return;
1422
+ const isEnvFile = dirname(file) === settings.root && basename(file).startsWith(".env");
1423
+ const isNewSource = type !== "update" && file.startsWith(`${settings.srcDir}/`) && SOURCE_RE.test(file);
1424
+ if (!modules.length && !isEnvFile && !isNewSource) return;
1425
+ if (type !== "update") {
1426
+ const manifest = this.environment.moduleGraph.getModuleById(RESOLVED_PREFIX + VIRTUAL_MODULES.manifest);
1427
+ if (manifest) this.environment.moduleGraph.invalidateModule(manifest);
1428
+ }
1429
+ listeners.forEach((listener) => listener({
1430
+ type,
1431
+ file
1432
+ }));
1433
+ return [];
1434
+ }
1435
+ };
1436
+ }
1437
+ function manifestCode(settings, dev) {
1438
+ const rel = srcDirFromRoot(settings);
1439
+ const base = rel ? `/${rel}` : "";
1440
+ const glob = (pattern, extra = "") => `import.meta.glob(${JSON.stringify(`${base}/${pattern}`)}, { eager: true${extra} })`;
1441
+ return [
1442
+ `export const rootDir = ${dev ? JSON.stringify(settings.root) : "undefined"}`,
1443
+ `export const config = ${glob("config/*.{ts,mts,js,mjs}")}`,
1444
+ `export const routes = ${glob("route/*.{ts,mts,js,mjs}")}`,
1445
+ `export const providers = ${glob("app/provider/*.{ts,mts,js,mjs}")}`,
1446
+ `const handlers = ${glob("app/exception/Handler.{ts,mts,js,mjs}", ", import: 'default'")}`,
1447
+ `export const exceptionHandler = Object.values(handlers)[0]`,
1448
+ `const formatters = ${glob("app/response/Formatter.{ts,mts,js,mjs}", ", import: 'default'")}`,
1449
+ `export const responseFormatter = Object.values(formatters)[0]`,
1450
+ `export const interceptors = ${glob("app/interceptor/*.{ts,mts,js,mjs}")}`,
1451
+ `export const policies = ${glob("app/policy/*.{ts,mts,js,mjs}")}`,
1452
+ `export const jobs = ${glob("app/job/*.{ts,mts,js,mjs}")}`,
1453
+ `export const schedules = ${glob("schedule/*.{ts,mts,js,mjs}")}`,
1454
+ `export const events = ${glob("app/event/*.{ts,mts,js,mjs}")}`,
1455
+ `export const listeners = ${glob("app/listener/*.{ts,mts,js,mjs}")}`,
1456
+ `export const discovered = { sockets: ${glob("app/socket/*.{ts,mts,js,mjs}")} }`,
1457
+ `export const apps = ${glob("app/*/{route,config,provider,exception,response}/*.{ts,mts,js,mjs}")}`
1458
+ ].join("\n");
1459
+ }
1460
+ /** database/ 下 drizzle-kit 生成的 SQL 与 journal,键为相对 srcDir 的路径(与 config/database.ts 的 migrations 一致) */
1461
+ function migrationsCode(settings) {
1462
+ const rel = srcDirFromRoot(settings);
1463
+ const base = rel ? `/${rel}/` : "/";
1464
+ return `const raw = {
1465
+ ...import.meta.glob(${JSON.stringify(`${base}database/**/*.sql`)}, { query: '?raw', import: 'default', eager: true }),
1466
+ ...import.meta.glob(${JSON.stringify(`${base}database/**/meta/_journal.json`)}, { query: '?raw', import: 'default', eager: true }),
1467
+ }
1468
+ export const files = Object.fromEntries(Object.entries(raw).map(([path, content]) => [path.slice(${base.length}), content]))
1469
+ `;
1470
+ }
1471
+ function appCode() {
1472
+ return `
1473
+ import { createApplication } from '@hazejs/core'
1474
+ import * as manifest from '${VIRTUAL_MODULES.manifest}'
1475
+
1476
+ export { manifest }
1477
+
1478
+ export function createApp(options = {}) {
1479
+ return createApplication({
1480
+ rootDir: manifest.rootDir,
1481
+ config: manifest.config,
1482
+ routes: manifest.routes,
1483
+ providers: manifest.providers,
1484
+ exceptionHandler: manifest.exceptionHandler,
1485
+ responseFormatter: manifest.responseFormatter,
1486
+ interceptors: manifest.interceptors,
1487
+ policies: manifest.policies,
1488
+ jobs: manifest.jobs,
1489
+ schedules: manifest.schedules,
1490
+ events: manifest.events,
1491
+ listeners: manifest.listeners,
1492
+ discovered: manifest.discovered,
1493
+ apps: manifest.apps,
1494
+ ...options,
1495
+ })
1496
+ }
1497
+ `;
1498
+ }
1499
+ function presetContext(settings) {
1500
+ return {
1501
+ root: settings.root,
1502
+ externalsOptions: settings.externals,
1503
+ srcDir: settings.srcDir,
1504
+ orm: settings.orm,
1505
+ cloudflare: settings.cloudflare,
1506
+ vercel: settings.vercel,
1507
+ schedules: hasSources(join(settings.srcDir, "schedule")),
1508
+ websocket: settings.cloudflare?.websocket ?? usesWebSocket(settings.srcDir),
1509
+ outDir: settings.outDir,
1510
+ fullstack: settings.fullstack,
1511
+ prefixes: settings.prefixes,
1512
+ port: settings.port,
1513
+ host: settings.host
1514
+ };
1515
+ }
1516
+ /** 生产入口由部署预设生成(监听端口 / Serverless handler / Worker) */
1517
+ function serverEntryCode(settings) {
1518
+ const preset = resolvePreset(settings.preset);
1519
+ const context = presetContext(settings);
1520
+ return preset.entry({
1521
+ ...context,
1522
+ layout: preset.layout(context)
1523
+ });
1524
+ }
1525
+ function normalizePrefixes(prefix) {
1526
+ return (Array.isArray(prefix) ? prefix : [prefix ?? "/api"]).map((item) => `/${item.replace(/^\/+|\/+$/g, "")}`);
1527
+ }
1528
+ /** 后端源码目录相对项目根的路径(相同时为空字符串;normalizePath('') 会得到 '.') */
1529
+ function srcDirFromRoot(settings) {
1530
+ const rel = normalizePath(relative(settings.root, settings.srcDir));
1531
+ return rel === "." ? "" : rel;
1532
+ }
1533
+ /** 路由文件中是否定义了 WebSocket(r.ws(...)) */
1534
+ function usesWebSocket(srcDir) {
1535
+ return [join(srcDir, "route"), ...existsSync(join(srcDir, "app")) ? readdirSync(join(srcDir, "app"), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(srcDir, "app", entry.name, "route")) : []].some((dir) => existsSync(dir) && readdirSync(dir).some((name) => SOURCE_RE.test(name) && /\.ws\s*\(/.test(readFileSync(join(dir, name), "utf8"))));
1536
+ }
1537
+ /** 目录下是否有源码文件(如 schedule/*.ts) */
1538
+ function hasSources(dir) {
1539
+ return existsSync(dir) && readdirSync(dir).some((name) => SOURCE_RE.test(name) && !name.endsWith(".d.ts"));
1540
+ }
1541
+ function packageName(id) {
1542
+ return id.split("/").slice(0, id.startsWith("@") ? 2 : 1).join("/");
1543
+ }
1544
+ function escapeRegExp(value) {
1545
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1546
+ }
1547
+ // src/build.ts
1548
+ /**
1549
+ * 构建纯后端项目的生产产物。产物布局、打包策略与入口由部署预设决定;
1550
+ * node 预设下依赖保持 external(运行时需要 node_modules),`.env` 从运行时工作目录加载。
1551
+ */
1552
+ async function buildServer(options = {}) {
1553
+ const root = resolve(options.root ?? process.cwd());
1554
+ const config = await loadHazeConfig(root, "production");
1555
+ const outDir = options.outDir ? resolve(root, options.outDir) : config.outDir;
1556
+ const plugin = haze({
1557
+ srcDir: config.srcDir,
1558
+ outDir,
1559
+ port: config.port,
1560
+ host: config.host,
1561
+ preset: options.preset ?? config.preset,
1562
+ cloudflare: config.cloudflare,
1563
+ vercel: config.vercel,
1564
+ externals: config.externals,
1565
+ standalone: true
1566
+ });
1567
+ await (await createBuilder(mergeConfig(config.vite, {
1568
+ root,
1569
+ configFile: false,
1570
+ mode: "production",
1571
+ envDir: false,
1572
+ logLevel: "warn",
1573
+ plugins: [plugin],
1574
+ build: {
1575
+ minify: options.minify ?? false,
1576
+ sourcemap: options.sourcemap ?? true
1577
+ }
1578
+ }))).buildApp();
1579
+ const info = plugin.api.lastBuild();
1580
+ if (!info) throw new Error("Build did not produce a server bundle");
1581
+ return {
1582
+ ...info,
1583
+ outDir
1584
+ };
1585
+ }
1586
+ // src/dev.ts
1587
+ /**
1588
+ * 纯后端项目的开发服务器:HTTP 监听只创建一次,文件变化时通过 Vite ModuleRunner
1589
+ * 重新执行应用代码并原子替换 fetch 处理器——进程不重启、端口不断开。
1590
+ */
1591
+ async function createDevServer(options = {}) {
1592
+ const root = resolve(options.root ?? process.cwd());
1593
+ const config = await loadHazeConfig(root);
1594
+ const plugin = haze({
1595
+ srcDir: config.srcDir,
1596
+ port: config.port,
1597
+ host: config.host,
1598
+ typegen: config.typegen,
1599
+ standalone: true
1600
+ });
1601
+ const vite = await createServer(mergeConfig(config.vite, {
1602
+ root,
1603
+ configFile: false,
1604
+ appType: "custom",
1605
+ clearScreen: false,
1606
+ logLevel: "warn",
1607
+ envDir: false,
1608
+ optimizeDeps: {
1609
+ noDiscovery: true,
1610
+ entries: []
1611
+ },
1612
+ server: {
1613
+ middlewareMode: true,
1614
+ ws: false,
1615
+ watch: { ignored: [`${normalizePath(config.outDir)}/**`, `${normalizePath(root)}/.haze/**`] }
1616
+ },
1617
+ plugins: [plugin]
1618
+ }));
1619
+ const host = createAppHost({
1620
+ vite,
1621
+ api: plugin.api,
1622
+ debounce: options.debounce
1623
+ });
1624
+ if (!(await host.start()).ok && options.listen === false) consola.warn(`[${PLUGIN_NAME}] the app failed to start; requests will return the error until it is fixed`);
1625
+ const server = options.listen === false ? void 0 : serve({
1626
+ fetch: host.fetch,
1627
+ port: options.port ?? (Number(process.env.PORT) || config.port),
1628
+ hostname: options.host ?? config.host,
1629
+ silent: true,
1630
+ gracefulShutdown: false,
1631
+ plugins: [webSocketPlugin(host.fetch, host.app ? webSocketConfig(host.app.config) : void 0)]
1632
+ });
1633
+ await server?.ready();
1634
+ return {
1635
+ get url() {
1636
+ return server?.url;
1637
+ },
1638
+ vite,
1639
+ get app() {
1640
+ return host.app;
1641
+ },
1642
+ fetch: host.fetch,
1643
+ reload: host.reload,
1644
+ onReload: host.onReload,
1645
+ import: host.import,
1646
+ async close() {
1647
+ await server?.close(true);
1648
+ await host.close();
1649
+ await vite.close();
1650
+ }
1651
+ };
1652
+ }
1653
+ // src/preview-server.ts
1654
+ /**
1655
+ * 本地预览构建产物(`haze preview`):按构建时的部署预设运行产物本身。
1656
+ * 全栈项目使用项目的 vite.config.ts(等同 `vite preview`),纯后端项目使用内置配置。
1657
+ */
1658
+ async function previewServer(options = {}) {
1659
+ const root = resolve(options.root ?? process.cwd());
1660
+ const server = { preview: {
1661
+ ...options.port !== void 0 && { port: options.port },
1662
+ ...options.host !== void 0 && { host: options.host },
1663
+ ...options.open !== void 0 && { open: options.open }
1664
+ } };
1665
+ const viteConfig = findViteConfig(root);
1666
+ if (viteConfig) return preview({
1667
+ root,
1668
+ configFile: viteConfig,
1669
+ ...server
1670
+ });
1671
+ const config = await loadHazeConfig(root, "production");
1672
+ return preview(mergeConfig(config.vite, {
1673
+ root,
1674
+ configFile: false,
1675
+ appType: "custom",
1676
+ envDir: false,
1677
+ plugins: [haze({
1678
+ srcDir: config.srcDir,
1679
+ outDir: config.outDir,
1680
+ standalone: true
1681
+ })],
1682
+ ...server
1683
+ }));
1684
+ }
1685
+ export { CONFIG_BASENAME, PRESET_NAMES, VIRTUAL_MODULES, buildServer, createAppHost, createDevServer, defineHazeConfig, findViteConfig, generateTypes, haze, loadHazeConfig, presets, previewServer, readBuildManifest, resolvePreset, typesFile, writeTypes };