@wolfstar/http-framework 3.3.0 → 3.4.0-next-20260904111115

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.
@@ -0,0 +1,673 @@
1
+ import { t as _defineProperty } from "./defineProperty-BFrI-_1n.js";
2
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
3
+ import { existsSync, readFileSync, statSync } from "node:fs";
4
+
5
+ //#region src/lib/config/errors.ts
6
+ /**
7
+ * A `stars.config.*` error: an invalid option, or a file that failed to load or parse.
8
+ *
9
+ * This is a plain data error (no exit code or terminal formatting) so it stays meaningful outside a CLI, e.g. for a
10
+ * dashboard or test that calls {@link loadStarsConfig} directly. `@wolfstar/cli` maps it to exit code `2` and renders
11
+ * `message`, `path`, `file` and `hint` for the terminal.
12
+ */
13
+ var ConfigError = class extends Error {
14
+ constructor(message, options) {
15
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
16
+ _defineProperty(this, "code", void 0);
17
+ _defineProperty(this, "hint", void 0);
18
+ _defineProperty(this, "path", void 0);
19
+ _defineProperty(this, "file", void 0);
20
+ this.name = "ConfigError";
21
+ this.code = options.code;
22
+ this.hint = options.hint ?? null;
23
+ this.path = options.path ?? null;
24
+ this.file = options.file ?? null;
25
+ }
26
+ };
27
+
28
+ //#endregion
29
+ //#region src/lib/config/load.ts
30
+ const CONFIG_EXTENSIONS = [
31
+ "ts",
32
+ "mts",
33
+ "cts",
34
+ "js",
35
+ "mjs",
36
+ "cjs"
37
+ ];
38
+ const CONFIG_FILE_NAMES = CONFIG_EXTENSIONS.map((extension) => `stars.config.${extension}`);
39
+ /**
40
+ * Finds the first `stars.config.*` file in `cwd`, in {@link CONFIG_FILE_NAMES} order.
41
+ */
42
+ function discoverConfigFile(cwd) {
43
+ for (const name of CONFIG_FILE_NAMES) {
44
+ const candidate = join(cwd, name);
45
+ if (isFile$1(candidate)) return candidate;
46
+ }
47
+ return null;
48
+ }
49
+ /**
50
+ * Loads the raw configuration object. The loader (`c12`) is imported lazily so
51
+ * commands that never touch the configuration stay fast.
52
+ */
53
+ async function loadConfigFile(options) {
54
+ const cwd = resolve(options.cwd);
55
+ let file;
56
+ if (options.configFile) {
57
+ file = resolve(cwd, options.configFile);
58
+ if (!isFile$1(file)) throw new ConfigError(`Configuration file not found: ${file}`, {
59
+ code: "CONFIG_NOT_FOUND",
60
+ hint: `Pass an existing file to --config, or create one of ${CONFIG_FILE_NAMES.join(", ")} in ${cwd}.`
61
+ });
62
+ } else {
63
+ file = discoverConfigFile(cwd);
64
+ if (!file) return {
65
+ configFile: null,
66
+ config: {}
67
+ };
68
+ }
69
+ const { loadConfig } = await import("c12");
70
+ let loaded;
71
+ try {
72
+ const result = await loadConfig({
73
+ name: "stars",
74
+ cwd: dirname(file),
75
+ configFile: basename(file),
76
+ rcFile: false,
77
+ globalRc: false,
78
+ dotenv: false,
79
+ packageJson: false,
80
+ defaults: {}
81
+ });
82
+ const layer = result.layers?.find((candidate) => candidate.configFile && resolve(candidate.cwd ?? dirname(file), candidate.configFile) === file);
83
+ loaded = layer ? layer.config : result.config;
84
+ } catch (error) {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ throw new ConfigError(`Failed to load the configuration: ${message}`, {
87
+ code: "CONFIG_LOAD_FAILED",
88
+ file,
89
+ hint: "The file must be valid TypeScript/JavaScript and export the configuration as its default export.",
90
+ cause: error
91
+ });
92
+ }
93
+ if (loaded === null || typeof loaded !== "object" || Array.isArray(loaded)) throw new ConfigError("The configuration file must export an object as its default export.", {
94
+ code: "CONFIG_NOT_OBJECT",
95
+ file,
96
+ hint: "Use `export default defineConfig({ ... })` from '@wolfstar/http-framework/config'."
97
+ });
98
+ return {
99
+ configFile: file,
100
+ config: loaded
101
+ };
102
+ }
103
+ function isFile$1(path) {
104
+ try {
105
+ return existsSync(path) && statSync(path).isFile();
106
+ } catch {
107
+ return false;
108
+ }
109
+ }
110
+
111
+ //#endregion
112
+ //#region src/lib/config/resolve.ts
113
+ const DEFAULT_ENTRIES = [
114
+ "src/main.ts",
115
+ "src/main.js",
116
+ "src/index.ts",
117
+ "src/index.js"
118
+ ];
119
+ const DEFAULT_IGNORE = [
120
+ "**/node_modules/**",
121
+ "**/dist/**",
122
+ "**/.git/**"
123
+ ];
124
+ const DEFAULT_DEBOUNCE = 150;
125
+ const DEFAULT_KILL_TIMEOUT = 5e3;
126
+ const DEFAULT_NODE_ARGS = ["--enable-source-maps"];
127
+ const DEFAULT_DEV_PORT = 3e3;
128
+ const DEFAULT_I18N_LOCALES = "src/locales/en-US";
129
+ const DEFAULT_I18N_OUTPUT = "src/@types/i18next.d.ts";
130
+ const DEFAULT_IMPORTS_DIRS = ["src/lib/**", "src/utils/**"];
131
+ const DEFAULT_IMPORTS_PRESETS = ["@wolfstar/http-framework", "@wolfstar/env-utilities"];
132
+ const DEFAULT_IMPORTS_DTS = ".stars/imports.d.ts";
133
+ const DEFAULT_DEV_LOG_FILE = ".stars/dev.log";
134
+ const DEFAULT_TUNNEL_PATH = "/";
135
+ const BUILD_TOOLS = /* @__PURE__ */ new Set([
136
+ "tsdown",
137
+ "tsc",
138
+ "none",
139
+ "vite",
140
+ "auto"
141
+ ]);
142
+ const TYPECHECKERS = /* @__PURE__ */ new Set([
143
+ "tsc",
144
+ "golar",
145
+ "tsz",
146
+ "auto"
147
+ ]);
148
+ const VITE_CONFIG_FILES = [
149
+ "vite.config.ts",
150
+ "vite.config.mts",
151
+ "vite.config.cts",
152
+ "vite.config.js",
153
+ "vite.config.mjs",
154
+ "vite.config.cjs"
155
+ ];
156
+ const TSDOWN_CONFIG_FILES = [
157
+ "tsdown.config.ts",
158
+ "tsdown.config.mts",
159
+ "tsdown.config.cts",
160
+ "tsdown.config.js",
161
+ "tsdown.config.mjs",
162
+ "tsdown.config.cjs",
163
+ "tsdown.config.json"
164
+ ];
165
+ const TYPESCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
166
+ ".ts",
167
+ ".mts",
168
+ ".cts"
169
+ ]);
170
+ /**
171
+ * Applies defaults, validates every option and resolves all paths to absolute ones.
172
+ *
173
+ * @throws {ConfigError} with an actionable `hint` on the first invalid option.
174
+ */
175
+ function resolveStarsConfig(options) {
176
+ const cwd = resolve(options.cwd);
177
+ const env = options.env ?? process.env;
178
+ const file = options.configFile;
179
+ const config = options.config;
180
+ const validator = new Validator(file);
181
+ validator.knownKeys(config, "", [
182
+ "root",
183
+ "entry",
184
+ "build",
185
+ "dev",
186
+ "codegen",
187
+ "imports",
188
+ "experimental",
189
+ "vite",
190
+ "tsdown"
191
+ ]);
192
+ const baseDirectory = file ? dirname(file) : cwd;
193
+ const root = resolve(baseDirectory, validator.string(config.root, "root") ?? ".");
194
+ if (!isDirectory(root)) throw validator.error(`The project root does not exist: ${root}`, "root", "ROOT_NOT_FOUND", "Point `root` to an existing directory, relative to the configuration file.");
195
+ const packageJson = readPackageJson(root);
196
+ const experimental = resolveExperimental(config.experimental ?? {}, validator);
197
+ const entry = resolveEntry(root, validator.string(config.entry, "entry"), validator);
198
+ const build = resolveBuild(root, entry, packageJson, config.build ?? {}, experimental, validator);
199
+ return {
200
+ configFile: file,
201
+ cwd,
202
+ root,
203
+ packageJson,
204
+ entry,
205
+ build,
206
+ dev: resolveDev(root, entry, packageJson, config.dev ?? {}, env, validator),
207
+ codegen: resolveCodegen(root, config.codegen ?? {}, validator),
208
+ imports: resolveImports(root, build.tool, config.imports, validator),
209
+ experimental,
210
+ vite: validator.plainObject(config.vite, "vite") ?? {},
211
+ tsdown: validator.plainObject(config.tsdown, "tsdown") ?? {}
212
+ };
213
+ }
214
+ /**
215
+ * Presents an absolute path relative to `root` when possible, for display purposes.
216
+ */
217
+ function displayPath(root, path) {
218
+ const rel = relative(root, path);
219
+ if (!rel) return ".";
220
+ return rel.startsWith("..") || isAbsolute(rel) ? path : rel;
221
+ }
222
+ function resolveEntry(root, configured, validator) {
223
+ if (configured !== void 0) {
224
+ const entry = resolve(root, configured);
225
+ if (!isFile(entry)) throw validator.error(`The entry file does not exist: ${entry}`, "entry", "ENTRY_NOT_FOUND", "Point `entry` to the file that starts the bot, relative to the project root.");
226
+ return entry;
227
+ }
228
+ for (const candidate of DEFAULT_ENTRIES) {
229
+ const entry = join(root, candidate);
230
+ if (isFile(entry)) return entry;
231
+ }
232
+ throw validator.error(`Could not find the entry file in ${root}`, "entry", "ENTRY_NOT_FOUND", `Set \`entry\` in the configuration, or create one of ${DEFAULT_ENTRIES.join(", ")}.`);
233
+ }
234
+ function resolveBuild(root, entry, packageJson, config, experimental, validator) {
235
+ validator.knownKeys(config, "build", [
236
+ "tool",
237
+ "outDir",
238
+ "tsconfig"
239
+ ]);
240
+ const requested = validator.string(config.tool, "build.tool") ?? "auto";
241
+ if (!BUILD_TOOLS.has(requested)) throw validator.error(`Unknown build tool "${requested}"`, "build.tool", "INVALID_BUILD_TOOL", "Use one of 'tsdown', 'tsc', 'vite', 'none' or 'auto'.");
242
+ if (requested === "vite" && !experimental.enableVite) throw validator.error("The 'vite' build tool is experimental", "build.tool", "EXPERIMENT_REQUIRED", "Set `experimental.enableVite` to true to use it.");
243
+ const isTypeScriptEntry = TYPESCRIPT_EXTENSIONS.has(extname(entry));
244
+ const tool = requested === "auto" ? detectBuildTool(root, packageJson, isTypeScriptEntry, experimental) : requested;
245
+ if (tool === "none" && isTypeScriptEntry) throw validator.error(`The entry ${displayPath(root, entry)} is TypeScript but the build tool is 'none'`, "build.tool", "BUILD_TOOL_REQUIRED", "Set `build.tool` to 'tsdown' or 'tsc', or point `entry` to a JavaScript file.");
246
+ const defaultOutDir = experimental.enableNitro ? ".output" : "dist";
247
+ const outDir = resolve(root, validator.string(config.outDir, "build.outDir") ?? defaultOutDir);
248
+ let tsconfig = null;
249
+ const configuredTsconfig = validator.string(config.tsconfig, "build.tsconfig");
250
+ if (configuredTsconfig !== void 0) {
251
+ tsconfig = resolve(root, configuredTsconfig);
252
+ if (!isFile(tsconfig)) throw validator.error(`The tsconfig file does not exist: ${tsconfig}`, "build.tsconfig", "TSCONFIG_NOT_FOUND", "Point `build.tsconfig` to an existing tsconfig.json, relative to the project root.");
253
+ } else if (tool === "tsc") {
254
+ tsconfig = [join(root, "src", "tsconfig.json"), join(root, "tsconfig.json")].find((candidate) => isFile(candidate)) ?? null;
255
+ if (!tsconfig) throw validator.error(`Could not find a tsconfig.json in ${root}`, "build.tsconfig", "TSCONFIG_NOT_FOUND", "Create src/tsconfig.json or tsconfig.json, or set `build.tsconfig`.");
256
+ }
257
+ const output = experimental.enableNitro ? join(outDir, "server", "index.mjs") : tool === "none" ? entry : resolveBuildOutput(root, entry, outDir, packageJson);
258
+ return {
259
+ tool,
260
+ outDir,
261
+ tsconfig,
262
+ output
263
+ };
264
+ }
265
+ function detectBuildTool(root, packageJson, isTypeScriptEntry, experimental) {
266
+ if (experimental.enableVite) {
267
+ if (VITE_CONFIG_FILES.some((name) => isFile(join(root, name))) || hasDependency(packageJson, "vite")) return "vite";
268
+ }
269
+ if (TSDOWN_CONFIG_FILES.some((name) => isFile(join(root, name))) || hasDependency(packageJson, "tsdown")) return "tsdown";
270
+ if (isTypeScriptEntry) return "tsc";
271
+ return "none";
272
+ }
273
+ /**
274
+ * Resolves the `experimental` block. Every flag is a boolean defaulting to `false`, the way Nuxt's own experimental
275
+ * flags are declared, and the ones that build on each other are checked here rather than surfacing later as a
276
+ * confusing runtime failure.
277
+ */
278
+ function resolveExperimental(config, validator) {
279
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw validator.error("`experimental` must be an object", "experimental", "INVALID_TYPE", "Use `{ enableVite, enableExternalVite, enableNitro, nitro }`.");
280
+ validator.knownKeys(config, "experimental", [
281
+ "enableVite",
282
+ "enableExternalVite",
283
+ "enableNitro",
284
+ "nitro"
285
+ ]);
286
+ const enableVite = validator.boolean(config.enableVite, "experimental.enableVite") ?? false;
287
+ const enableExternalVite = validator.boolean(config.enableExternalVite, "experimental.enableExternalVite") ?? false;
288
+ const enableNitro = validator.boolean(config.enableNitro, "experimental.enableNitro") ?? false;
289
+ if (enableExternalVite && !enableVite) throw validator.error("`experimental.enableExternalVite` needs `experimental.enableVite`", "experimental.enableExternalVite", "EXPERIMENT_REQUIRED", "Set `experimental.enableVite` to true as well, or drop `enableExternalVite`.");
290
+ if (enableNitro && !enableVite) throw validator.error("`experimental.enableNitro` needs `experimental.enableVite`", "experimental.enableNitro", "EXPERIMENT_REQUIRED", "Set `experimental.enableVite` to true as well, or drop `enableNitro`.");
291
+ const rawNitro = "nitro" in config ? config.nitro : void 0;
292
+ if (rawNitro !== void 0 && !enableNitro) throw validator.error("`experimental.nitro` needs `experimental.enableNitro`", "experimental.nitro", "EXPERIMENT_REQUIRED", "Set `experimental.enableNitro` to true as well, or drop `nitro`.");
293
+ if (rawNitro !== void 0 && (rawNitro === null || typeof rawNitro !== "object" || Array.isArray(rawNitro))) throw validator.error("`experimental.nitro` must be an object", "experimental.nitro", "INVALID_TYPE", "Use `{ preset }`.");
294
+ if (rawNitro) validator.knownKeys(rawNitro, "experimental.nitro", ["preset"]);
295
+ return {
296
+ enableVite,
297
+ enableExternalVite,
298
+ enableNitro,
299
+ nitro: { preset: validator.string(rawNitro?.preset, "experimental.nitro.preset") ?? "node-server" }
300
+ };
301
+ }
302
+ function resolveBuildOutput(root, entry, outDir, packageJson) {
303
+ if (packageJson?.main) return resolve(root, packageJson.main);
304
+ const extension = extname(entry);
305
+ const outputExtension = extension === ".mts" ? ".mjs" : extension === ".cts" ? ".cjs" : ".js";
306
+ return join(outDir, `${basename(entry, extension)}${outputExtension}`);
307
+ }
308
+ const ENV_FILES = [".env.local", ".env"];
309
+ const ENV_PORT_KEYS = ["HTTP_PORT", "PORT"];
310
+ /**
311
+ * Reads the project's `.env.local`/`.env` into a plain object, the way `stars dev` and `stars commands` need it:
312
+ * these files are only loaded into `process.env` by the bot itself once it starts (see `@wolfstar/env-utilities`),
313
+ * so by the time the CLI runs they are not there yet. This is a minimal line reader, not a full dotenv
314
+ * implementation — quoting is stripped, but expansion (`dotenv-expand`) is not. Earlier files win, matching
315
+ * dotenv's own precedence.
316
+ */
317
+ function readProjectEnvFiles(root) {
318
+ const result = {};
319
+ for (const file of ENV_FILES) {
320
+ const path = join(root, file);
321
+ if (!isFile(path)) continue;
322
+ let contents;
323
+ try {
324
+ contents = readFileSync(path, "utf-8");
325
+ } catch {
326
+ continue;
327
+ }
328
+ for (const line of contents.split(/\r?\n/)) {
329
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
330
+ if (!match) continue;
331
+ const key = match[1];
332
+ if (key in result) continue;
333
+ result[key] = match[2].trim().replace(/^['"]|['"]$/g, "");
334
+ }
335
+ }
336
+ return result;
337
+ }
338
+ function readDevPortFromEnvFile(root) {
339
+ const values = readProjectEnvFiles(root);
340
+ for (const key of ENV_PORT_KEYS) if (values[key]) return values[key];
341
+ return null;
342
+ }
343
+ function resolveDev(root, entry, packageJson, config, env, validator) {
344
+ validator.knownKeys(config, "dev", [
345
+ "watch",
346
+ "ignore",
347
+ "debounce",
348
+ "env",
349
+ "nodeArgs",
350
+ "args",
351
+ "url",
352
+ "health",
353
+ "killTimeout",
354
+ "typecheck",
355
+ "tunnel",
356
+ "logFile"
357
+ ]);
358
+ const watch = (validator.stringArray(config.watch, "dev.watch") ?? [displayPath(root, dirname(entry))]).map((path) => resolve(root, path));
359
+ const ignore = validator.stringArray(config.ignore, "dev.ignore") ?? [...DEFAULT_IGNORE];
360
+ const debounce = validator.nonNegativeNumber(config.debounce, "dev.debounce") ?? 150;
361
+ const devEnv = validator.stringRecord(config.env, "dev.env") ?? {};
362
+ const nodeArgs = validator.stringArray(config.nodeArgs, "dev.nodeArgs") ?? [...DEFAULT_NODE_ARGS];
363
+ const args = validator.stringArray(config.args, "dev.args") ?? [];
364
+ const killTimeout = validator.nonNegativeNumber(config.killTimeout, "dev.killTimeout") ?? 5e3;
365
+ const health = validator.string(config.health, "dev.health") ?? null;
366
+ let url = validator.string(config.url, "dev.url") ?? null;
367
+ if (url !== null) try {
368
+ new URL(url);
369
+ } catch {
370
+ throw validator.error(`Invalid URL "${url}"`, "dev.url", "INVALID_URL", "Use an absolute URL such as http://localhost:3000.");
371
+ }
372
+ else {
373
+ const port = devEnv.HTTP_PORT ?? env.HTTP_PORT ?? readDevPortFromEnvFile(root) ?? String(3e3);
374
+ url = /^\d+$/.test(port) ? `http://localhost:${port}` : `http://localhost:${DEFAULT_DEV_PORT}`;
375
+ }
376
+ const typecheck = resolveTypecheck(root, packageJson, config.typecheck, validator);
377
+ const tunnel = resolveTunnel(config.tunnel, validator);
378
+ const logFile = config.logFile === false ? null : resolve(root, validator.string(config.logFile, "dev.logFile") ?? ".stars/dev.log");
379
+ return {
380
+ watch,
381
+ ignore,
382
+ debounce,
383
+ env: devEnv,
384
+ nodeArgs,
385
+ args,
386
+ url,
387
+ health,
388
+ killTimeout,
389
+ typecheck,
390
+ tunnel,
391
+ logFile
392
+ };
393
+ }
394
+ /**
395
+ * Resolves `dev.typecheck`. The tsconfig is looked up the same way the `tsc` build tool looks up its own, so a
396
+ * project building with `tsdown` still gets `tsc --watch --noEmit` on the right project file.
397
+ */
398
+ function resolveTypecheck(root, packageJson, config, validator) {
399
+ if (config === void 0 || config === false) return {
400
+ enabled: false,
401
+ tsconfig: null,
402
+ checker: detectTypechecker(packageJson)
403
+ };
404
+ let configured;
405
+ let requestedChecker = "auto";
406
+ if (config !== true) {
407
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw validator.error("`dev.typecheck` must be a boolean or an object", "dev.typecheck", "INVALID_TYPE", "Use `true` to type-check with the project tsconfig, `{ tsconfig }` to pick one, or `false` to disable it.");
408
+ validator.knownKeys(config, "dev.typecheck", ["tsconfig", "checker"]);
409
+ configured = validator.string(config.tsconfig, "dev.typecheck.tsconfig");
410
+ requestedChecker = validator.string(config.checker, "dev.typecheck.checker") ?? "auto";
411
+ if (!TYPECHECKERS.has(requestedChecker)) throw validator.error(`Unknown type checker "${requestedChecker}"`, "dev.typecheck.checker", "INVALID_TYPECHECKER", "Use one of 'tsc', 'golar', 'tsz' or 'auto'.");
412
+ }
413
+ const checker = requestedChecker === "auto" ? detectTypechecker(packageJson) : requestedChecker;
414
+ if (configured !== void 0) {
415
+ const tsconfig = resolve(root, configured);
416
+ if (!isFile(tsconfig)) throw validator.error(`The tsconfig file does not exist: ${tsconfig}`, "dev.typecheck.tsconfig", "TSCONFIG_NOT_FOUND", "Point `dev.typecheck.tsconfig` to an existing tsconfig.json, relative to the project root.");
417
+ return {
418
+ enabled: true,
419
+ tsconfig,
420
+ checker
421
+ };
422
+ }
423
+ const found = [join(root, "src", "tsconfig.json"), join(root, "tsconfig.json")].find((candidate) => isFile(candidate)) ?? null;
424
+ if (!found) throw validator.error(`Could not find a tsconfig.json in ${root}`, "dev.typecheck", "TSCONFIG_NOT_FOUND", "Create src/tsconfig.json or tsconfig.json, or set `dev.typecheck.tsconfig`.");
425
+ return {
426
+ enabled: true,
427
+ tsconfig: found,
428
+ checker
429
+ };
430
+ }
431
+ /**
432
+ * Picks the type checker when `dev.typecheck.checker` is `auto`: `golar` when the project already depends on it
433
+ * (it wraps TypeScript and is what this repository's own `typecheck` scripts run), `tsc` otherwise. `tsz` is never
434
+ * picked automatically — it is an early, tsc-compatible alternative a project opts into.
435
+ */
436
+ function detectTypechecker(packageJson) {
437
+ return hasDependency(packageJson, "golar") ? "golar" : "tsc";
438
+ }
439
+ /**
440
+ * Resolves `dev.tunnel`: `true` (or `{}`) opens a `cloudflared` quick tunnel, a string (or `{ url }`) is an https
441
+ * URL the user already serves and the CLI only checks.
442
+ */
443
+ function resolveTunnel(config, validator) {
444
+ if (config === void 0 || config === false) return { mode: "off" };
445
+ let url;
446
+ let updateEndpoint = false;
447
+ let path = "/";
448
+ if (typeof config === "string") url = config;
449
+ else if (config !== true) {
450
+ if (config === null || typeof config !== "object" || Array.isArray(config)) throw validator.error("`dev.tunnel` must be a boolean, an https URL or an object", "dev.tunnel", "INVALID_TYPE", "Use `true` for a cloudflared quick tunnel, an https URL you already serve, or `false` to disable it.");
451
+ validator.knownKeys(config, "dev.tunnel", [
452
+ "url",
453
+ "updateEndpoint",
454
+ "path"
455
+ ]);
456
+ url = validator.string(config.url, "dev.tunnel.url");
457
+ updateEndpoint = validator.boolean(config.updateEndpoint, "dev.tunnel.updateEndpoint") ?? false;
458
+ path = validator.string(config.path, "dev.tunnel.path") ?? "/";
459
+ }
460
+ if (url === void 0) return {
461
+ mode: "quick",
462
+ path,
463
+ updateEndpoint
464
+ };
465
+ let parsed;
466
+ try {
467
+ parsed = new URL(url);
468
+ } catch {
469
+ throw validator.error(`Invalid URL "${url}"`, "dev.tunnel", "INVALID_URL", "Use an absolute https URL such as https://bot.example.com.");
470
+ }
471
+ if (parsed.protocol !== "https:") throw validator.error(`The tunnel URL must be https, received "${url}"`, "dev.tunnel", "INVALID_URL", "Discord only accepts an https interactions endpoint.");
472
+ return {
473
+ mode: "url",
474
+ url,
475
+ path,
476
+ updateEndpoint
477
+ };
478
+ }
479
+ function resolveCodegen(root, config, validator) {
480
+ validator.knownKeys(config, "codegen", ["i18n"]);
481
+ if (config.i18n === false) return { i18n: null };
482
+ if (config.i18n === void 0) {
483
+ const locales = join(root, DEFAULT_I18N_LOCALES);
484
+ return { i18n: isDirectory(locales) ? {
485
+ locales,
486
+ output: join(root, DEFAULT_I18N_OUTPUT)
487
+ } : null };
488
+ }
489
+ if (config.i18n === null || typeof config.i18n !== "object") throw validator.error("`codegen.i18n` must be an object or `false`", "codegen.i18n", "INVALID_TYPE", "Use `{ locales, output }` to configure it or `false` to disable it.");
490
+ validator.knownKeys(config.i18n, "codegen.i18n", ["locales", "output"]);
491
+ const locales = resolve(root, validator.string(config.i18n.locales, "codegen.i18n.locales") ?? "src/locales/en-US");
492
+ if (!isDirectory(locales)) throw validator.error(`The locales directory does not exist: ${locales}`, "codegen.i18n.locales", "LOCALES_NOT_FOUND", "Point `codegen.i18n.locales` to the base locale directory, relative to the project root.");
493
+ return { i18n: {
494
+ locales,
495
+ output: resolve(root, validator.string(config.i18n.output, "codegen.i18n.output") ?? "src/@types/i18next.d.ts")
496
+ } };
497
+ }
498
+ /**
499
+ * The transform that injects auto imports (`@wolfstar/http-framework/auto-imports`) only runs through `tsdown`'s
500
+ * rolldown pipeline, the same way Nuxt's own auto imports only run through its Vite/webpack build: `tsc` and `none`
501
+ * have no transform step to hook into. `tsdown` is picked as `build.tool` first (see {@link detectBuildTool}) for the
502
+ * same reason — it is the only tool this feature, and this build config in general, treats as the default choice.
503
+ */
504
+ function resolveImports(root, buildTool, config, validator) {
505
+ const defaultDirs = DEFAULT_IMPORTS_DIRS.map((dir) => resolve(root, dir));
506
+ const defaultPresets = [...DEFAULT_IMPORTS_PRESETS];
507
+ const defaultDts = resolve(root, DEFAULT_IMPORTS_DTS);
508
+ if (config === false) return {
509
+ enabled: false,
510
+ dirs: defaultDirs,
511
+ presets: defaultPresets,
512
+ exclude: [],
513
+ dts: defaultDts
514
+ };
515
+ const forcedOn = config === true;
516
+ const options = forcedOn || config === void 0 ? {} : config;
517
+ if (typeof options !== "object" || options === null || Array.isArray(options)) throw validator.error("`imports` must be an object, `true` or `false`", "imports", "INVALID_TYPE", "Use `{ dirs, presets, exclude, dts }`, `true` to enable with defaults, or `false` to disable.");
518
+ validator.knownKeys(options, "imports", [
519
+ "enabled",
520
+ "dirs",
521
+ "presets",
522
+ "exclude",
523
+ "dts"
524
+ ]);
525
+ const requestedOn = forcedOn || validator.boolean(options.enabled, "imports.enabled");
526
+ if (requestedOn && buildTool !== "tsdown") throw validator.error("`imports` requires the `tsdown` build tool", "imports.enabled", "IMPORTS_REQUIRE_TSDOWN", "Set `build.tool` to 'tsdown', or remove `imports`/set it to `false`.");
527
+ const dirs = (validator.stringArray(options.dirs, "imports.dirs") ?? [...DEFAULT_IMPORTS_DIRS]).map((dir) => resolve(root, dir));
528
+ const presets = validator.stringArray(options.presets, "imports.presets") ?? defaultPresets;
529
+ const exclude = validator.stringArray(options.exclude, "imports.exclude") ?? [];
530
+ const dts = resolve(root, validator.string(options.dts, "imports.dts") ?? ".stars/imports.d.ts");
531
+ return {
532
+ enabled: requestedOn ?? buildTool === "tsdown",
533
+ dirs,
534
+ presets,
535
+ exclude,
536
+ dts
537
+ };
538
+ }
539
+ var Validator = class {
540
+ constructor(file) {
541
+ this.file = file;
542
+ }
543
+ error(message, path, code, hint) {
544
+ return new ConfigError(message, {
545
+ code,
546
+ path,
547
+ hint,
548
+ file: this.file
549
+ });
550
+ }
551
+ knownKeys(value, path, keys) {
552
+ for (const key of Object.keys(value)) {
553
+ if (keys.includes(key)) continue;
554
+ const fullPath = path ? `${path}.${key}` : key;
555
+ throw this.error(`Unknown option \`${fullPath}\``, fullPath, "UNKNOWN_OPTION", `Known options${path ? ` of \`${path}\`` : ""}: ${keys.join(", ")}.`);
556
+ }
557
+ }
558
+ string(value, path) {
559
+ if (value === void 0) return void 0;
560
+ if (typeof value !== "string" || value.length === 0) throw this.typeError(path, "a non-empty string", value);
561
+ return value;
562
+ }
563
+ boolean(value, path) {
564
+ if (value === void 0) return void 0;
565
+ if (typeof value !== "boolean") throw this.typeError(path, "a boolean", value);
566
+ return value;
567
+ }
568
+ /** A plain object passed through as-is (e.g. raw `vite`/`tsdown` config merged into the project's own). */
569
+ plainObject(value, path) {
570
+ if (value === void 0) return void 0;
571
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw this.typeError(path, "an object", value);
572
+ return value;
573
+ }
574
+ stringArray(value, path) {
575
+ if (value === void 0) return void 0;
576
+ if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw this.typeError(path, "an array of strings", value);
577
+ return value;
578
+ }
579
+ nonNegativeNumber(value, path) {
580
+ if (value === void 0) return void 0;
581
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) throw this.typeError(path, "a non-negative number", value);
582
+ return value;
583
+ }
584
+ stringRecord(value, path) {
585
+ if (value === void 0) return void 0;
586
+ if (value === null || typeof value !== "object" || Array.isArray(value) || !Object.values(value).every((item) => typeof item === "string")) throw this.typeError(path, "an object of string values", value);
587
+ return value;
588
+ }
589
+ typeError(path, expected, value) {
590
+ return this.error(`\`${path}\` must be ${expected}, received ${describe(value)}`, path, "INVALID_TYPE", `Set \`${path}\` to ${expected} or remove it to use the default.`);
591
+ }
592
+ };
593
+ function describe(value) {
594
+ if (value === null) return "null";
595
+ if (Array.isArray(value)) return "an array";
596
+ if (typeof value === "string") return `"${value}"`;
597
+ return typeof value === "object" ? "an object" : `${typeof value} ${String(value)}`;
598
+ }
599
+ function hasDependency(packageJson, name) {
600
+ return Boolean(packageJson?.dependencies?.[name] ?? packageJson?.devDependencies?.[name]);
601
+ }
602
+ function readPackageJson(root) {
603
+ const file = join(root, "package.json");
604
+ if (!isFile(file)) return null;
605
+ try {
606
+ const parsed = JSON.parse(readFileSync(file, "utf-8"));
607
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
608
+ } catch (error) {
609
+ throw new ConfigError(`Failed to parse ${file}: ${error instanceof Error ? error.message : String(error)}`, {
610
+ code: "PACKAGE_JSON_INVALID",
611
+ hint: "Fix the JSON syntax of the package.json file.",
612
+ cause: error
613
+ });
614
+ }
615
+ }
616
+ function isFile(path) {
617
+ try {
618
+ return existsSync(path) && statSync(path).isFile();
619
+ } catch {
620
+ return false;
621
+ }
622
+ }
623
+ function isDirectory(path) {
624
+ try {
625
+ return existsSync(path) && statSync(path).isDirectory();
626
+ } catch {
627
+ return false;
628
+ }
629
+ }
630
+
631
+ //#endregion
632
+ //#region src/lib/config/index.ts
633
+ /**
634
+ * Loads, validates and resolves a project's `stars.config.*`.
635
+ *
636
+ * @throws {ConfigError} when the configuration file cannot be loaded or contains an invalid option.
637
+ */
638
+ async function loadStarsConfig(options = {}) {
639
+ const cwd = options.cwd ?? process.cwd();
640
+ const loaded = await loadConfigFile({
641
+ cwd,
642
+ configFile: options.configFile
643
+ });
644
+ return resolveStarsConfig({
645
+ cwd,
646
+ configFile: loaded.configFile,
647
+ config: loaded.config,
648
+ env: options.env
649
+ });
650
+ }
651
+
652
+ //#endregion
653
+ //#region src/config.ts
654
+ /**
655
+ * Typed helper for `stars.config.{ts,mts,cts,js,mjs,cjs}` files.
656
+ *
657
+ * @example
658
+ * ```ts
659
+ * import { defineConfig } from '@wolfstar/http-framework/config';
660
+ *
661
+ * export default defineConfig({
662
+ * entry: 'src/main.ts',
663
+ * build: { tool: 'tsdown' }
664
+ * });
665
+ * ```
666
+ */
667
+ function defineConfig(config) {
668
+ return config;
669
+ }
670
+
671
+ //#endregion
672
+ export { CONFIG_EXTENSIONS, CONFIG_FILE_NAMES, ConfigError, defineConfig, discoverConfigFile, displayPath, loadConfigFile, loadStarsConfig, readProjectEnvFiles, resolveStarsConfig };
673
+ //# sourceMappingURL=config.js.map