heximon 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/bin.mjs +1669 -37
  3. package/package.json +17 -7
package/dist/bin.mjs CHANGED
@@ -3,18 +3,19 @@ import { c as RegistryAccess, d as ClackPrompter, f as PromptCancelledError, l a
3
3
  import { createRequire, register } from "node:module";
4
4
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner } from "@clack/prompts";
5
5
  import process from "node:process";
6
- import { spawnSync } from "node:child_process";
6
+ import { spawn } from "node:child_process";
7
7
  import { defineCommand, runMain } from "citty";
8
- import { basename, dirname, join, relative, resolve } from "pathe";
8
+ import { basename, dirname, isAbsolute, join, relative, resolve } from "pathe";
9
9
  import tab from "@bomb.sh/tab/citty";
10
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
11
- import { CompileMode, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
12
- import { ConfigLoader, LibraryEmitter, ProjectCompiler, RuntimeConfigLoader } from "@heximon/compiler/project";
10
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
11
+ import { AstNodeType, CompileEnvironment, CompileMode, DeployPlatform, DiagnosticCode, DiagnosticRenderMode, DiagnosticRenderer, DiagnosticSeverity, ImportKind, ReexportIndex, RuntimeConfigFile, RuntimeSingletons, TerminalStyle, UpgradeBaselineFile } from "@heximon/compiler";
12
+ import { ConfigLoader, LibraryEmitter, LibraryFactoryPreflight, ProjectCompiler, RuntimeConfigLoader, SourceModuleResolution, SourceParser, WorkspaceTopologyReader } from "@heximon/compiler/project";
13
13
  import { NodeCompileSeam } from "@heximon/node/compiler";
14
14
  import { TransactionCensus, TransactionCensusPolicy } from "@heximon/cqrs/compiler";
15
15
  import { BoundaryProvenanceAuditor, BoundaryProvenanceStatus, CredentialSelection, DatabaseConfigDetector, DeployDiagnosticSeverity, DeployLedger, DeployManifestStore, DeployRunner, DeployStrategyUnavailableError, DeployTargetResolver, DeployableServiceReader, DotenvCascade, DrizzleKitRunner, EnvFileReader, EnvFileUnreadableError, HostManagedDeployUnsupportedError, ImportScanner, MigrationInspector, MigrationTargetComposer, PackageCopyAuditor, PeerClosureAuditor, PlatformTracingAuditor, PlatformTracingStatus, WorkspaceConsumerLocator } from "@heximon/deploy";
16
16
  import { RouteCensus, RouteScopeGap } from "@heximon/http/compiler";
17
17
  import { pathToFileURL } from "node:url";
18
+ import { homedir } from "node:os";
18
19
  //#region src/compile-diagnostics.ts
19
20
  /**
20
21
  * Renders compile diagnostics to stderr with the same codeframe layout the dev server shows — a box-drawing
@@ -176,6 +177,42 @@ var ProjectCompileSession = class ProjectCompileSession {
176
177
  ...options.verifyBaseline === true ? { verifyBaseline: true } : {}
177
178
  });
178
179
  }
180
+ /** The platform the session compiles for, or `undefined` for the zero-config Node default. */
181
+ platformName() {
182
+ return this.strategy.platformName;
183
+ }
184
+ /**
185
+ * Run the compile `vp build` runs, into a chosen directory: the production graph with the sidecar's wiring
186
+ * mode, diagnostic dials and cross-service topology, which {@link compile} leaves out.
187
+ *
188
+ * @param outputDirectory - The directory the generated files will be written to. It must sit directly
189
+ * under the project root, because the generated imports of the app source are relative to it.
190
+ * @param aliases - The module aliases of the project (prefix → absolute path), when it has some.
191
+ * @returns The compile output. Nothing is written.
192
+ */
193
+ async compileProduction(outputDirectory, aliases = {
194
+ exact: {},
195
+ prefixes: {}
196
+ }) {
197
+ const workspaceSubscribers = await WorkspaceTopologyReader.resolveFromConfig(this.rootDir, this.plugins, this.config);
198
+ return this.compiler.compile({
199
+ rootDir: this.rootDir,
200
+ outputDirectory,
201
+ plugins: this.plugins,
202
+ strategy: this.strategy,
203
+ environment: CompileEnvironment.Production,
204
+ aliases: aliases.prefixes,
205
+ exactAliases: aliases.exact,
206
+ ...this.sourceDirectories === void 0 ? {} : { sourceDirectories: this.sourceDirectories },
207
+ ...this.config === void 0 ? {} : { config: this.config },
208
+ ...this.config?.wiringMode === void 0 ? {} : { wiringMode: this.config.wiringMode },
209
+ ...this.config?.strictDiagnostics === void 0 ? {} : { strictDiagnostics: this.config.strictDiagnostics },
210
+ ...this.config?.suppressDiagnostics === void 0 ? {} : { suppressDiagnostics: this.config.suppressDiagnostics },
211
+ ...this.runtimeConfig === void 0 ? {} : { runtimeConfig: this.runtimeConfig },
212
+ ...this.config?.root === void 0 ? {} : { apps: { main: this.config.root } },
213
+ ...workspaceSubscribers === void 0 ? {} : { workspaceSubscribers }
214
+ });
215
+ }
179
216
  /**
180
217
  * Run the library-mode compile — a published module package goes through the same {@link LibraryEmitter}
181
218
  * its own `vp pack` build uses, rather than being compiled as an app. Not a byte-for-byte replay of that
@@ -2137,66 +2174,150 @@ var RouteTableReport = class RouteTableReport {
2137
2174
  }
2138
2175
  };
2139
2176
  //#endregion
2140
- //#region src/routes-command.ts
2177
+ //#region src/transform-boot.ts
2141
2178
  /**
2142
- * Boots an app in-process to read its live HTTP route table — the `--boot` fallback for a route surface the
2143
- * compiler cannot describe statically. Booting imports the app's TypeScript source through `.js` specifiers,
2144
- * and Heximon's parameter-property classes need a TypeScript TRANSFORM (not just type-stripping), so the boot
2145
- * runs under a one-time re-exec with Node's transform-types enabled plus a resolve hook that maps those `.js`
2146
- * specifiers to their `.ts` sources.
2179
+ * Runs a command that imports an app's TypeScript source in-process (`routes --boot`, `task run`).
2180
+ * The generated wiring imports the source through `.js` specifiers, and Heximon's parameter-property
2181
+ * classes need a TypeScript TRANSFORM (not just type-stripping), so such a command runs under a one-time
2182
+ * re-exec with Node's transform-types enabled plus a resolve hook that maps those `.js` specifiers to their
2183
+ * `.ts` sources.
2147
2184
  */
2148
- var RoutesBoot = class RoutesBoot {
2185
+ var TransformBoot = class TransformBoot {
2149
2186
  /** Set on the re-exec'd child so it boots rather than re-exec'ing again. */
2150
2187
  static transformMarker = "HEXIMON_ROUTES_TRANSFORM";
2188
+ /** The signals that reach only the parent, and so are passed on to the child. */
2189
+ static forwardedSignals = ["SIGTERM", "SIGHUP"];
2151
2190
  /** The `.js`/extensionless → `.ts` ESM resolve hook, inlined as a `data:` module so it ships in no file. */
2152
- static sourceResolverModule = RoutesBoot.buildSourceResolverModule();
2191
+ static sourceResolverModule = TransformBoot.buildSourceResolverModule();
2153
2192
  /** Whether this process is the transform-enabled child that performs the boot. */
2154
2193
  static isBootProcess() {
2155
- return process.env[RoutesBoot.transformMarker] === "1";
2194
+ return process.env[TransformBoot.transformMarker] === "1";
2156
2195
  }
2157
2196
  /**
2158
2197
  * Re-run this exact CLI invocation under `node --experimental-transform-types`, inheriting stdio so the
2159
2198
  * child's output and prompts reach the terminal directly.
2160
2199
  *
2200
+ * The child owns an interrupt: a terminal sends Ctrl-C to both processes, and the child cleans up. A
2201
+ * `SIGTERM` or `SIGHUP` that reaches only this process is passed on, so the child never outlives it.
2202
+ *
2161
2203
  * A child that never started, or that a signal killed, reports no exit code. Both become `1` — but they
2162
2204
  * are NAMED on stderr first, because the child printed nothing of its own and a bare `exit 1` after a
2163
2205
  * silent run reads as "the command found a problem" rather than "the command never ran".
2164
2206
  *
2207
+ * @param commandLabel - The command name for the failure messages (`routes --boot`, `task run`).
2165
2208
  * @returns The child process exit code (1 when it did not report one).
2166
2209
  */
2167
- static reexecUnderTransform() {
2168
- const result = spawnSync(process.execPath, [
2169
- "--experimental-transform-types",
2170
- "--disable-warning=ExperimentalWarning",
2171
- process.argv[1] ?? "",
2172
- ...process.argv.slice(2)
2173
- ], {
2174
- stdio: "inherit",
2175
- env: {
2176
- ...process.env,
2177
- [RoutesBoot.transformMarker]: "1"
2210
+ static reexecUnderTransform(commandLabel) {
2211
+ return new Promise((resolve) => {
2212
+ const child = spawn(process.execPath, [
2213
+ "--experimental-transform-types",
2214
+ "--disable-warning=ExperimentalWarning",
2215
+ process.argv[1] ?? "",
2216
+ ...process.argv.slice(2)
2217
+ ], {
2218
+ stdio: "inherit",
2219
+ env: {
2220
+ ...process.env,
2221
+ [TransformBoot.transformMarker]: "1"
2222
+ }
2223
+ });
2224
+ const ignoreInterrupt = () => void 0;
2225
+ const forwarded = TransformBoot.forwardedSignals.map((signal) => {
2226
+ const forward = () => {
2227
+ child.kill(signal);
2228
+ };
2229
+ process.on(signal, forward);
2230
+ return [signal, forward];
2231
+ });
2232
+ process.on("SIGINT", ignoreInterrupt);
2233
+ const finish = (code) => {
2234
+ process.off("SIGINT", ignoreInterrupt);
2235
+ for (const [signal, forward] of forwarded) process.off(signal, forward);
2236
+ resolve(code);
2237
+ };
2238
+ child.once("error", (error) => {
2239
+ process.stderr.write(`heximon ${commandLabel}: could not start the transform-enabled child (${process.execPath}): ${error.message}\n`);
2240
+ finish(1);
2241
+ });
2242
+ child.once("close", (code, signal) => {
2243
+ if (signal !== null) process.stderr.write(`heximon ${commandLabel}: the transform-enabled child was killed by ${signal}.\n`);
2244
+ finish(code ?? 1);
2245
+ });
2246
+ });
2247
+ }
2248
+ /**
2249
+ * Install the source hooks so the imminent boot can import the app's `.ts` source.
2250
+ *
2251
+ * @param rewrites - Source text to serve instead of what is on disk, by absolute file path. A production
2252
+ * build applies these rewrites, which drop the imports of dev-only modules, so a production run applies them too.
2253
+ * @param aliases - The module aliases of the project. An exact alias (`@app` → one file) matches that specifier
2254
+ * only. A prefix alias (`@users` → a directory) matches `@users/<rest>` on whole path segments, the longest
2255
+ * prefix first. An exact alias wins over a prefix. This is the precedence of the compile and of TypeScript.
2256
+ */
2257
+ static registerSourceResolver(rewrites = /* @__PURE__ */ new Map(), aliases = {
2258
+ exact: {},
2259
+ prefixes: {}
2260
+ }) {
2261
+ register(TransformBoot.sourceResolverModule, {
2262
+ parentURL: import.meta.url,
2263
+ data: {
2264
+ rewrites: [...rewrites].map(([filePath, content]) => [pathToFileURL(TransformBoot.realPath(filePath)).href, content]),
2265
+ exact: Object.entries(aliases.exact),
2266
+ aliases: Object.entries(aliases.prefixes),
2267
+ compilerUrl: import.meta.resolve("@heximon/compiler/project")
2178
2268
  }
2179
2269
  });
2180
- if (result.error !== void 0) process.stderr.write(`heximon routes --boot: could not start the transform-enabled child (${process.execPath}): ${result.error.message}\n`);
2181
- else if (result.signal !== null && result.signal !== void 0) process.stderr.write(`heximon routes --boot: the transform-enabled child was killed by ${result.signal}.\n`);
2182
- return result.status ?? 1;
2183
2270
  }
2184
- /** Install the source resolve hook so the imminent boot can import the app's `.ts` source. */
2185
- static registerSourceResolver() {
2186
- register(RoutesBoot.sourceResolverModule);
2271
+ /** The real path of `path`, or `path` itself when it does not exist. */
2272
+ static realPath(path) {
2273
+ try {
2274
+ return realpathSync(path);
2275
+ } catch {
2276
+ return path;
2277
+ }
2187
2278
  }
2188
2279
  /** Build the inline resolve-hook module — mirrors the compiler's own relative-import resolution. */
2189
2280
  static buildSourceResolverModule() {
2190
2281
  const source = [
2191
- "import { existsSync } from \"node:fs\";",
2282
+ "import { existsSync, realpathSync } from \"node:fs\";",
2192
2283
  "import { fileURLToPath, pathToFileURL } from \"node:url\";",
2284
+ "let rewrites = new Map();",
2285
+ "let resolver;",
2286
+ "export async function initialize(data) {",
2287
+ " rewrites = new Map(data.rewrites);",
2288
+ " // The alias rule is the compiler's own, so the boot and the compile agree on every specifier.",
2289
+ " if (data.aliases.length > 0 || data.exact.length > 0) {",
2290
+ " const compiler = await import(data.compilerUrl);",
2291
+ " resolver = new compiler.AliasModuleResolver(Object.fromEntries(data.aliases), undefined, Object.fromEntries(data.exact));",
2292
+ " }",
2293
+ "}",
2294
+ "export async function load(url, context, nextLoad) {",
2295
+ " const result = await nextLoad(url, context);",
2296
+ " const content = rewrites.get(url);",
2297
+ " return content === undefined ? result : { ...result, source: content };",
2298
+ "}",
2193
2299
  "export async function resolve(specifier, context, nextResolve) {",
2194
2300
  " if ((specifier.startsWith(\"./\") || specifier.startsWith(\"../\")) && context.parentURL) {",
2195
2301
  " const base = fileURLToPath(new URL(specifier, context.parentURL));",
2196
2302
  " const withoutJs = base.endsWith(\".js\") ? base.slice(0, -3) : base;",
2197
2303
  " for (const candidate of [withoutJs + \".ts\", base + \".ts\", base + \"/index.ts\"]) {",
2198
2304
  " if (candidate.endsWith(\".ts\") && existsSync(candidate)) {",
2199
- " return { url: pathToFileURL(candidate).href, shortCircuit: true };",
2305
+ " return { url: pathToFileURL(realpathSync(candidate)).href, shortCircuit: true };",
2306
+ " }",
2307
+ " }",
2308
+ " }",
2309
+ " if (resolver !== undefined) {",
2310
+ " const resolution = resolver.explain(specifier);",
2311
+ " if (resolution.kind === \"scanned\" || resolution.kind === \"outside-scan\") {",
2312
+ " return { url: pathToFileURL(realpathSync(resolution.filePath)).href, shortCircuit: true };",
2313
+ " }",
2314
+ " if (resolution.kind === \"no-source-file\") {",
2315
+ " // The compile finds no file here either and leaves the specifier to the normal resolution, so an",
2316
+ " // installed package of the same name still loads. The alias is named only when that fails too.",
2317
+ " try {",
2318
+ " return await nextResolve(specifier, context);",
2319
+ " } catch (error) {",
2320
+ " throw new Error('The alias \"' + resolution.prefix + '\" does not resolve \"' + specifier + '\": no file exists at ' + resolution.base + '.', { cause: error });",
2200
2321
  " }",
2201
2322
  " }",
2202
2323
  " }",
@@ -2206,6 +2327,8 @@ var RoutesBoot = class RoutesBoot {
2206
2327
  return `data:text/javascript,${encodeURIComponent(source)}`;
2207
2328
  }
2208
2329
  };
2330
+ //#endregion
2331
+ //#region src/routes-command.ts
2209
2332
  /**
2210
2333
  * `heximon routes` — print the app's unified HTTP route table (inline-mode AND contract-mode routes
2211
2334
  * together), each row carrying the authorization scopes the app enforces on it.
@@ -2263,8 +2386,8 @@ const routesCommand = defineCommand({
2263
2386
  },
2264
2387
  async run({ args }) {
2265
2388
  const asBoot = args.boot;
2266
- if (asBoot && !RoutesBoot.isBootProcess()) {
2267
- process.exitCode = RoutesBoot.reexecUnderTransform();
2389
+ if (asBoot && !TransformBoot.isBootProcess()) {
2390
+ process.exitCode = await TransformBoot.reexecUnderTransform("routes --boot");
2268
2391
  return;
2269
2392
  }
2270
2393
  const rootDir = resolve(process.cwd(), args.cwd);
@@ -2341,7 +2464,7 @@ var RoutesCommand = class RoutesCommand {
2341
2464
  let app;
2342
2465
  let inventory;
2343
2466
  try {
2344
- RoutesBoot.registerSourceResolver();
2467
+ TransformBoot.registerSourceResolver();
2345
2468
  const sourceDirectories = SourceDirectories.resolve(rootDir, args.src);
2346
2469
  DotenvCascade.loadInto(rootDir, args.env);
2347
2470
  const config = await ConfigLoader.load(rootDir, args.env);
@@ -2455,6 +2578,1514 @@ var RoutesCommand = class RoutesCommand {
2455
2578
  }
2456
2579
  };
2457
2580
  //#endregion
2581
+ //#region src/project-modules.ts
2582
+ /**
2583
+ * Imports packages the way the generated wiring imports them: through a small module written into the
2584
+ * compile output directory, which sits in the project. The specifiers inside it resolve from the project, so
2585
+ * the command gets the copy of a package that the wiring gets. A package the command imported from its own
2586
+ * install would be a second set of class objects, and `app.from(CoreModule)` would not know them.
2587
+ */
2588
+ var ProjectModules = class ProjectModules {
2589
+ /**
2590
+ * Import `specifiers` of the project.
2591
+ *
2592
+ * @param directory - A directory inside the project, with write access (the compile output directory).
2593
+ * @param fileName - The name of the module to write, unique among the calls for one directory.
2594
+ * @param source - The module source: `export … from "<specifier>"` lines.
2595
+ * @returns The namespace of the written module.
2596
+ */
2597
+ static async import(directory, fileName, source) {
2598
+ const file = join(directory, fileName);
2599
+ writeFileSync(file, source);
2600
+ return await import(pathToFileURL(file).href);
2601
+ }
2602
+ /** The runtime of the project: everything the command asks of `@heximon/runtime`. */
2603
+ static runtime(directory) {
2604
+ return ProjectModules.import(directory, "project-runtime.js", "export * from \"@heximon/runtime\";\n");
2605
+ }
2606
+ };
2607
+ //#endregion
2608
+ //#region src/nitro-host.ts
2609
+ /**
2610
+ * The production inputs of a Nitro host project, for the preset that was read from its config files. The
2611
+ * preset picks the deploy platform, and the compile adds the Nitro host plugin, so a compile that reads the
2612
+ * sidecar alone would build another graph. `@heximon/nitro` of the project makes the inputs from the preset
2613
+ * name. Both packages load from the project, so the plugins, the strategy and the compiler are one copy. No
2614
+ * Nitro or Vite config runs.
2615
+ */
2616
+ var NitroHost = class NitroHost {
2617
+ context;
2618
+ aliases;
2619
+ constructor(context, aliases) {
2620
+ this.context = context;
2621
+ this.aliases = aliases;
2622
+ }
2623
+ /**
2624
+ * Resolve the inputs of the Nitro host.
2625
+ *
2626
+ * @param rootDir - The project root.
2627
+ * @param directory - A directory inside the project to write the import module into.
2628
+ * @param request - The preset (or the choice to take the platform from the sidecar), the literal source
2629
+ * directories of the Nitro config, and the module aliases of the project (the tsconfig `paths`).
2630
+ * @throws {Error} When `@heximon/nitro` is not installed in the project.
2631
+ */
2632
+ static async open(rootDir, directory, request) {
2633
+ const modules = await ProjectModules.import(directory, "project-nitro.js", "export { resolveNitroPresetInputs } from \"@heximon/nitro/module\";\n");
2634
+ return new NitroHost(await modules.resolveNitroPresetInputs({
2635
+ rootDirectory: rootDir,
2636
+ preset: request.preset,
2637
+ aliases: request.aliases.prefixes,
2638
+ ...request.serverDir === void 0 ? {} : { serverDir: request.serverDir },
2639
+ ...request.scanDirs === void 0 ? {} : { scanDirs: request.scanDirs },
2640
+ ...request.platformFromSidecar === true ? { platformFromSidecar: true } : {}
2641
+ }), request.aliases);
2642
+ }
2643
+ /** Whether the sidecar declares the deploy platform. */
2644
+ hasSidecarPlatform() {
2645
+ return this.context.config?.platform !== void 0;
2646
+ }
2647
+ /**
2648
+ * Run the compile the host's production build runs.
2649
+ *
2650
+ * @param rootDir - The project root.
2651
+ * @param outputDirectory - Where the generated files will be written.
2652
+ * @param sourceDirectories - The source directories to scan instead of the host's, when the caller has some.
2653
+ */
2654
+ compile(rootDir, outputDirectory, sourceDirectories) {
2655
+ const context = this.context;
2656
+ const config = context.config;
2657
+ const compiler = new ProjectCompiler();
2658
+ return {
2659
+ output: compiler.compile({
2660
+ rootDir,
2661
+ outputDirectory,
2662
+ sourceDirectories: sourceDirectories ?? context.sourceDirectories,
2663
+ aliases: this.aliases.prefixes,
2664
+ exactAliases: this.aliases.exact,
2665
+ plugins: context.plugins,
2666
+ strategy: context.strategy,
2667
+ ...context.wiringMode === void 0 ? {} : { wiringMode: context.wiringMode },
2668
+ ...config === void 0 ? {} : { config },
2669
+ ...context.runtimeConfig === void 0 ? {} : { runtimeConfig: context.runtimeConfig },
2670
+ ...config?.root === void 0 ? {} : { apps: { main: config.root } },
2671
+ ...context.workspaceSubscribers === void 0 ? {} : { workspaceSubscribers: context.workspaceSubscribers }
2672
+ }),
2673
+ compiler,
2674
+ platformName: context.strategy.platformName
2675
+ };
2676
+ }
2677
+ };
2678
+ //#endregion
2679
+ //#region src/nitro-static-config.ts
2680
+ /**
2681
+ * Reads the Nitro options that choose the graph of a build (`preset`, `serverDir`, `scanDirs`) from the source of
2682
+ * the config files, without running them. A config file is project code: it can read the environment, call a
2683
+ * service, or write a file. So the files are parsed with oxc, and the reader ACCEPTS only a shape that it can
2684
+ * prove. Everything else is "computed", and the command then refuses or asks for an explicit input.
2685
+ *
2686
+ * Accepted in `nitro.config.*`: `export default {…}`, or `export default defineNitroConfig({…})` with
2687
+ * `defineNitroConfig` imported from `nitro` or `nitro/config`, or either one through ONE top-level `const` that is
2688
+ * never assigned again. The object has no spread. A `$production` or `$env.production` overlay applies; the other
2689
+ * environments do not, because the command always compiles for production.
2690
+ *
2691
+ * Accepted in `vite.config.*`: ONE call of the Nitro plugin of `nitro/vite` (a named or a namespace import), as a
2692
+ * direct element of the `plugins` array literal of the exported config (`export default {…}`, or `defineConfig`
2693
+ * from `vite` over one, or either through one `const`). No ternary, no `&&`, no spread, no helper, no function.
2694
+ *
2695
+ * The precedence is Nitro's own: the options of the Vite config, then `NITRO_PRESET` / `SERVER_PRESET`, then the
2696
+ * Nitro config file.
2697
+ */
2698
+ var NitroStaticConfig = class NitroStaticConfig {
2699
+ static nitroConfigNames = [
2700
+ "nitro.config.ts",
2701
+ "nitro.config.mts",
2702
+ "nitro.config.js",
2703
+ "nitro.config.mjs"
2704
+ ];
2705
+ static viteConfigNames = [
2706
+ "vite.config.ts",
2707
+ "vite.config.mts",
2708
+ "vite.config.js",
2709
+ "vite.config.mjs"
2710
+ ];
2711
+ /** Nuxt creates its own Nitro, which a static read of these files cannot describe. */
2712
+ static nuxtConfigNames = [
2713
+ "nuxt.config.ts",
2714
+ "nuxt.config.mts",
2715
+ "nuxt.config.js",
2716
+ "nuxt.config.mjs"
2717
+ ];
2718
+ /** The extensions c12 tries for a config file, scripts and data formats. */
2719
+ static configExtensions = [
2720
+ ".js",
2721
+ ".ts",
2722
+ ".mjs",
2723
+ ".cjs",
2724
+ ".mts",
2725
+ ".cts",
2726
+ ".json",
2727
+ ".jsonc",
2728
+ ".json5",
2729
+ ".yaml",
2730
+ ".yml",
2731
+ ".toml"
2732
+ ];
2733
+ /** The exact name of the rc file c12 reads for Nitro. */
2734
+ static resourceFileName = ".nitrorc";
2735
+ /** The expression nodes that only wrap another expression. */
2736
+ static wrapperTypes = [
2737
+ "TSAsExpression",
2738
+ "TSSatisfiesExpression",
2739
+ "TSNonNullExpression",
2740
+ "TSTypeAssertion",
2741
+ "ParenthesizedExpression"
2742
+ ];
2743
+ /** The module that exports the Nitro Vite plugin. */
2744
+ static nitroVitePackage = "nitro/vite";
2745
+ /** The helper that returns its Nitro config unchanged, and where it is imported from. */
2746
+ static nitroConfigHelper = {
2747
+ name: "defineNitroConfig",
2748
+ packages: ["nitro", "nitro/config"]
2749
+ };
2750
+ /** The helper that returns its Vite config unchanged, and where it is imported from. */
2751
+ static viteConfigHelper = {
2752
+ name: "defineConfig",
2753
+ packages: ["vite", "vite-plus"]
2754
+ };
2755
+ /**
2756
+ * @param rootDir - The project root.
2757
+ * @param environment - The environment to read `NITRO_PRESET` from; the process environment by default.
2758
+ */
2759
+ static read(rootDir, environment = process.env) {
2760
+ const find = (names) => names.map((name) => join(rootDir, name)).find((path) => existsSync(path));
2761
+ const notAHost = {
2762
+ isNitroHost: false,
2763
+ preset: { kind: "none" },
2764
+ serverDir: { kind: "none" },
2765
+ scanDirs: { kind: "none" }
2766
+ };
2767
+ if (find(NitroStaticConfig.nuxtConfigNames) !== void 0) return notAHost;
2768
+ const configFiles = NitroStaticConfig.configFilesOf(rootDir);
2769
+ const nitroFile = configFiles.length === 1 && NitroStaticConfig.nitroConfigNames.some((name) => join(rootDir, name) === configFiles[0]) ? configFiles[0] : void 0;
2770
+ const viteFile = find(NitroStaticConfig.viteConfigNames);
2771
+ const vite = viteFile === void 0 ? void 0 : NitroStaticConfig.readVite(viteFile);
2772
+ const hasNitroSource = configFiles.length > 0 || vite?.usesNitroPlugin === true;
2773
+ if (!hasNitroSource && !NitroStaticConfig.dependsOnNitro(rootDir)) return notAHost;
2774
+ const viteSources = vite?.sources ?? [];
2775
+ const fileSources = nitroFile === void 0 ? [] : NitroStaticConfig.readNitroConfig(nitroFile);
2776
+ const hiddenSources = NitroStaticConfig.hiddenSources(rootDir, hasNitroSource, nitroFile === void 0 ? configFiles : []);
2777
+ const all = [
2778
+ ...viteSources,
2779
+ ...fileSources,
2780
+ ...hiddenSources
2781
+ ];
2782
+ const fromVite = NitroStaticConfig.scalar("preset", viteSources, true);
2783
+ const fromEnvironment = environment["NITRO_PRESET"] || environment["SERVER_PRESET"];
2784
+ const found = fromVite.kind !== "none" ? fromVite : fromEnvironment ? {
2785
+ kind: "literal",
2786
+ value: fromEnvironment
2787
+ } : NitroStaticConfig.scalar("preset", [...fileSources, ...hiddenSources], true);
2788
+ const unreadable = all.find((source) => source.kind === "unproven");
2789
+ return {
2790
+ isNitroHost: true,
2791
+ preset: unreadable !== void 0 && found.kind !== "computed" ? {
2792
+ kind: "computed",
2793
+ file: unreadable.file,
2794
+ reason: unreadable.reason
2795
+ } : NitroStaticConfig.withHidden(found, all, ["static", "extends"], ["defaultPreset"]),
2796
+ serverDir: NitroStaticConfig.withHidden(NitroStaticConfig.scalar("serverDir", all), all, [], ["extends"]),
2797
+ scanDirs: NitroStaticConfig.withHiddenList(NitroStaticConfig.list("scanDirs", all), all)
2798
+ };
2799
+ }
2800
+ /**
2801
+ * Sources of options that the project has but the reader cannot read: a `.nitrorc` file (in the project or the
2802
+ * home directory), the `nitro` key of `package.json`, and a dependency on `nitro` when no config file or
2803
+ * provable plugin call exists.
2804
+ */
2805
+ static hiddenSources(rootDir, hasNitroSource, unreadFiles) {
2806
+ const unproven = (reason) => ({
2807
+ kind: "unproven",
2808
+ file: join(rootDir, "package.json"),
2809
+ reason
2810
+ });
2811
+ const sources = [];
2812
+ const resourceFile = [rootDir, homedir()].map((directory) => join(directory, NitroStaticConfig.resourceFileName)).find((path) => existsSync(path));
2813
+ if (resourceFile !== void 0) sources.push(unproven(`the file ${resourceFile} sets Nitro options`));
2814
+ if (unreadFiles.length > 0) sources.push(unproven(`the file ${unreadFiles[0]} sets Nitro options in a form the command does not read`));
2815
+ const manifest = NitroStaticConfig.manifestOf(rootDir);
2816
+ if (manifest === "invalid") sources.push(unproven("package.json is not valid JSON"));
2817
+ else if (manifest !== void 0) {
2818
+ if (manifest.nitro !== void 0) sources.push(unproven("the `nitro` key of package.json sets Nitro options"));
2819
+ if (!hasNitroSource && NitroStaticConfig.dependsOnNitro(rootDir)) sources.push(unproven("the project depends on nitro, but no Nitro config file or Nitro Vite plugin call was found"));
2820
+ }
2821
+ return sources;
2822
+ }
2823
+ /**
2824
+ * Every file or directory that the config loader of Nitro (c12) would take as its main config: the name
2825
+ * `nitro.config` in the project, in `.config/`, and `.config/nitro`, bare, with each supported extension, and as
2826
+ * a directory with an `index` file. The project's own script forms are among them.
2827
+ */
2828
+ static configFilesOf(rootDir) {
2829
+ return [
2830
+ join(rootDir, "nitro.config"),
2831
+ join(rootDir, ".config", "nitro.config"),
2832
+ join(rootDir, ".config", "nitro")
2833
+ ].flatMap((base) => ["", "/index"].flatMap((suffix) => ["", ...NitroStaticConfig.configExtensions].map((extension) => `${base}${suffix}${extension}`))).filter((path) => existsSync(path));
2834
+ }
2835
+ /** The parsed package.json of the project, `undefined` when it has none, `"invalid"` when it is not JSON. */
2836
+ static manifestOf(rootDir) {
2837
+ const file = join(rootDir, "package.json");
2838
+ if (!existsSync(file)) return;
2839
+ try {
2840
+ return JSON.parse(readFileSync(file, "utf8"));
2841
+ } catch {
2842
+ return "invalid";
2843
+ }
2844
+ }
2845
+ /** Whether package.json lists `nitro` as a dependency or a dev dependency. */
2846
+ static dependsOnNitro(rootDir) {
2847
+ const manifest = NitroStaticConfig.manifestOf(rootDir);
2848
+ return manifest !== void 0 && manifest !== "invalid" && ("nitro" in (manifest.dependencies ?? {}) || "nitro" in (manifest.devDependencies ?? {}));
2849
+ }
2850
+ /**
2851
+ * Apply the keys that change a value without showing in it. A key in `always` (for the preset: `static`, which
2852
+ * turns a preset into its static variant) makes the value unknown. A key in `whenUnset` (`defaultPreset`,
2853
+ * `extends`: the layers sit below every other source) matters only when no literal was found.
2854
+ */
2855
+ static withHidden(value, sources, always, whenUnset) {
2856
+ const hidden = (keys) => {
2857
+ for (const source of sources) {
2858
+ const key = source.kind === "object" ? keys.find((name) => source.entries.has(name)) : void 0;
2859
+ if (source.kind === "object" && key !== void 0) return {
2860
+ file: source.file,
2861
+ key
2862
+ };
2863
+ }
2864
+ };
2865
+ const alwaysHidden = hidden(always);
2866
+ const unsetHidden = value.kind === "none" ? hidden(whenUnset) : void 0;
2867
+ const reason = alwaysHidden ?? unsetHidden;
2868
+ return reason === void 0 ? value : {
2869
+ kind: "computed",
2870
+ file: reason.file,
2871
+ reason: `\`${reason.key}\` can change the value`
2872
+ };
2873
+ }
2874
+ /** A list gets every `extends` layer on top of it, so a layer makes it unknown. */
2875
+ static withHiddenList(value, sources) {
2876
+ const layered = sources.find((source) => source.kind === "object" && source.entries.has("extends"));
2877
+ return layered === void 0 || layered.kind !== "object" ? value : {
2878
+ kind: "computed",
2879
+ file: layered.file,
2880
+ reason: "`extends` can add values"
2881
+ };
2882
+ }
2883
+ /**
2884
+ * The option sources of a Nitro config file, highest priority first: the production overlays, then the
2885
+ * object itself. An `extends` or `defaultPreset` key brings values that the source does not show, so it adds
2886
+ * an unproven source below the object.
2887
+ */
2888
+ static readNitroConfig(file) {
2889
+ const { program, imports } = NitroStaticConfig.parse(file);
2890
+ const resolved = NitroStaticConfig.exportedObject(program, imports, NitroStaticConfig.nitroConfigHelper);
2891
+ if (resolved.kind === "unproven") return [{
2892
+ kind: "unproven",
2893
+ file,
2894
+ reason: resolved.reason
2895
+ }];
2896
+ const base = NitroStaticConfig.prove(resolved.node, file);
2897
+ if (base.kind === "unproven") return [base];
2898
+ const overlay = /* @__PURE__ */ new Map();
2899
+ const sources = [];
2900
+ const overlays = [base.entries.get("$production")];
2901
+ const environments = base.entries.get("$env");
2902
+ if (environments !== void 0) {
2903
+ const proven = NitroStaticConfig.prove(environments, file);
2904
+ if (proven.kind === "unproven") sources.push(proven);
2905
+ else overlays.push(proven.entries.get("production"));
2906
+ }
2907
+ for (const node of overlays) {
2908
+ if (node === void 0) continue;
2909
+ const proven = NitroStaticConfig.prove(node, file);
2910
+ if (proven.kind === "unproven") sources.push(proven);
2911
+ else for (const [key, value] of proven.entries) overlay.set(key, value);
2912
+ }
2913
+ if (overlay.size > 0) sources.push({
2914
+ kind: "object",
2915
+ file,
2916
+ entries: overlay
2917
+ });
2918
+ sources.push(base);
2919
+ return sources;
2920
+ }
2921
+ /**
2922
+ * What the Vite config says: whether it imports the Nitro plugin, and the option sources it gives. Once the
2923
+ * plugin is imported, anything the reader cannot prove makes the sources unproven; the project is still a
2924
+ * Nitro host.
2925
+ */
2926
+ static readVite(file) {
2927
+ const { program, imports } = NitroStaticConfig.parse(file);
2928
+ const named = /* @__PURE__ */ new Set();
2929
+ const namespaces = /* @__PURE__ */ new Set();
2930
+ for (const entry of imports) {
2931
+ if (entry.request !== NitroStaticConfig.nitroVitePackage || entry.isTypeOnly) continue;
2932
+ if (entry.importKind === ImportKind.Namespace) namespaces.add(entry.localName);
2933
+ else if (entry.importKind === ImportKind.Named && entry.importedName === "nitro") named.add(entry.localName);
2934
+ }
2935
+ if (named.size === 0 && namespaces.size === 0) return {
2936
+ usesNitroPlugin: false,
2937
+ sources: []
2938
+ };
2939
+ const unproven = (reason) => ({
2940
+ usesNitroPlugin: true,
2941
+ sources: [{
2942
+ kind: "unproven",
2943
+ file,
2944
+ reason
2945
+ }]
2946
+ });
2947
+ const importStarts = new Set(program.descendants("ImportDeclaration").flatMap((declaration) => declaration.descendants(AstNodeType.Identifier)).map((identifier) => identifier.start));
2948
+ const calls = program.descendants(AstNodeType.CallExpression).flatMap((call) => {
2949
+ const callee = call.child("callee");
2950
+ if (callee.is(AstNodeType.Identifier) && named.has(callee.name ?? "")) return [{
2951
+ call,
2952
+ referenceStart: callee.start
2953
+ }];
2954
+ if (callee.is(AstNodeType.MemberExpression) && !callee.flagAt("computed") && callee.child("property").name === "nitro" && callee.child("object").is(AstNodeType.Identifier) && namespaces.has(callee.child("object").name ?? "")) return [{
2955
+ call,
2956
+ referenceStart: callee.child("object").start
2957
+ }];
2958
+ return [];
2959
+ });
2960
+ const callStarts = new Set(calls.map((entry) => entry.referenceStart));
2961
+ const nameStarts = NitroStaticConfig.nameStarts(program);
2962
+ if (program.descendants(AstNodeType.Identifier).filter((identifier) => (named.has(identifier.name ?? "") || namespaces.has(identifier.name ?? "")) && !importStarts.has(identifier.start) && !nameStarts.has(identifier.start) && !callStarts.has(identifier.start)).length > 0) return unproven("the Nitro Vite plugin is used other than by a direct call");
2963
+ if (calls.length !== 1) return unproven(`the Nitro Vite plugin is called ${calls.length} times, not once`);
2964
+ const resolved = NitroStaticConfig.exportedObject(program, imports, NitroStaticConfig.viteConfigHelper);
2965
+ if (resolved.kind === "unproven") return unproven(resolved.reason);
2966
+ const config = NitroStaticConfig.prove(resolved.node, file);
2967
+ if (config.kind === "unproven") return {
2968
+ usesNitroPlugin: true,
2969
+ sources: [config]
2970
+ };
2971
+ const plugins = NitroStaticConfig.unwrap(config.entries.get("plugins"));
2972
+ const call = calls[0].call;
2973
+ if (!(plugins?.is(AstNodeType.ArrayExpression) === true && plugins.children("elements").some((element) => NitroStaticConfig.unwrap(element)?.start === call.start))) return unproven("the Nitro Vite plugin is not a direct element of the `plugins` array");
2974
+ const sources = [];
2975
+ const argument = NitroStaticConfig.unwrap(call.children("arguments")[0]);
2976
+ if (argument !== void 0) {
2977
+ const options = NitroStaticConfig.prove(argument, file);
2978
+ sources.push(options);
2979
+ const nested = options.kind === "object" ? options.entries.get("config") : void 0;
2980
+ if (nested !== void 0) sources.push(NitroStaticConfig.prove(nested, file));
2981
+ }
2982
+ const nitroKey = config.entries.get("nitro");
2983
+ if (nitroKey !== void 0) sources.push(NitroStaticConfig.prove(nitroKey, file));
2984
+ return {
2985
+ usesNitroPlugin: true,
2986
+ sources
2987
+ };
2988
+ }
2989
+ /**
2990
+ * The object that `export default` gives: a literal, a call of the verified identity helper over one, or a
2991
+ * single top-level `const` that holds either and is never assigned again.
2992
+ */
2993
+ static exportedObject(program, imports, helper) {
2994
+ const helperNames = new Set(imports.filter((entry) => entry.importKind === ImportKind.Named && entry.importedName === helper.name && helper.packages.includes(entry.request) && !entry.isTypeOnly).map((entry) => entry.localName));
2995
+ const declaration = program.children("body").find((statement) => statement.is(AstNodeType.ExportDefaultDeclaration))?.child("declaration");
2996
+ const objectOf = (node, viaConstant) => {
2997
+ const expression = NitroStaticConfig.unwrap(node);
2998
+ if (expression === void 0) return {
2999
+ kind: "unproven",
3000
+ reason: "the config has no default export"
3001
+ };
3002
+ if (expression.is(AstNodeType.ObjectExpression)) return {
3003
+ kind: "found",
3004
+ node: expression
3005
+ };
3006
+ if (expression.is(AstNodeType.CallExpression)) {
3007
+ const callee = expression.child("callee");
3008
+ return callee.is(AstNodeType.Identifier) && helperNames.has(callee.name ?? "") && expression.children("arguments").length === 1 ? objectOf(expression.children("arguments")[0], viaConstant) : {
3009
+ kind: "unproven",
3010
+ reason: `the config is a call of something other than ${helper.name} from ${helper.packages.join(" or ")}`
3011
+ };
3012
+ }
3013
+ if (expression.is(AstNodeType.Identifier) && !viaConstant) {
3014
+ const initializer = NitroStaticConfig.constantOf(program, expression.name ?? "", expression.start);
3015
+ return initializer === void 0 ? {
3016
+ kind: "unproven",
3017
+ reason: `\`${expression.name}\` is not a top-level const that nothing else uses`
3018
+ } : objectOf(initializer, true);
3019
+ }
3020
+ return {
3021
+ kind: "unproven",
3022
+ reason: "the default export is not an object the reader can prove"
3023
+ };
3024
+ };
3025
+ return objectOf(declaration, false);
3026
+ }
3027
+ /**
3028
+ * The initializer of `const <name> = …` at the top level, when the name has no use except the one at
3029
+ * `exportStart` (the default export, or the single wrapper call around it). A member assignment
3030
+ * (`config.preset = …`), a call that gets the name (`Object.assign(config, …)`), or a spread of it is another
3031
+ * use, and it makes the value unknown.
3032
+ */
3033
+ static constantOf(program, name, exportStart) {
3034
+ const initializers = [];
3035
+ for (const statement of program.children("body")) {
3036
+ const declaration = statement.is(AstNodeType.ExportNamedDeclaration) ? statement.child("declaration") : statement;
3037
+ if (declaration.stringAt("kind") !== "const") continue;
3038
+ for (const declarator of declaration.children("declarations")) if (declarator.child("id").name === name) initializers.push({
3039
+ init: declarator.child("init"),
3040
+ idStart: declarator.child("id").start
3041
+ });
3042
+ }
3043
+ if (initializers.length !== 1) return;
3044
+ const names = NitroStaticConfig.nameStarts(program);
3045
+ return program.descendants(AstNodeType.Identifier).filter((identifier) => identifier.name === name && identifier.start !== initializers[0].idStart && identifier.start !== exportStart && !names.has(identifier.start)).length === 0 ? initializers[0].init : void 0;
3046
+ }
3047
+ /** The starts of identifiers that are names, not references: a property key and a member name. */
3048
+ static nameStarts(program) {
3049
+ return /* @__PURE__ */ new Set([...program.descendants(AstNodeType.Property).filter((property) => !property.flagAt("computed") && !property.flagAt("shorthand")).map((property) => property.child("key").start), ...program.descendants(AstNodeType.MemberExpression).filter((member) => !member.flagAt("computed")).map((member) => member.child("property").start)]);
3050
+ }
3051
+ /** The options of one object, or the reason that the reader cannot prove it. */
3052
+ static prove(node, file) {
3053
+ const object = NitroStaticConfig.unwrap(node);
3054
+ if (object === void 0 || !object.is(AstNodeType.ObjectExpression)) return {
3055
+ kind: "unproven",
3056
+ file,
3057
+ reason: "an options value is not an object literal"
3058
+ };
3059
+ const entries = /* @__PURE__ */ new Map();
3060
+ for (const property of object.children("properties")) {
3061
+ if (!property.is(AstNodeType.Property)) return {
3062
+ kind: "unproven",
3063
+ file,
3064
+ reason: "an options object has a spread"
3065
+ };
3066
+ const key = property.child("key");
3067
+ const name = key.name ?? key.stringAt("value");
3068
+ if (property.flagAt("computed") || property.flagAt("method") || property.stringAt("kind") !== "init" || name === void 0) return {
3069
+ kind: "unproven",
3070
+ file,
3071
+ reason: "an options object has a computed key, a getter or a method"
3072
+ };
3073
+ entries.set(name, property.child("value"));
3074
+ }
3075
+ return {
3076
+ kind: "object",
3077
+ file,
3078
+ entries
3079
+ };
3080
+ }
3081
+ /** A string option: the first source that sets it decides. */
3082
+ static scalar(key, sources, emptyIsAbsent = false) {
3083
+ for (const source of sources) {
3084
+ if (source.kind === "unproven") return {
3085
+ kind: "computed",
3086
+ file: source.file,
3087
+ reason: source.reason
3088
+ };
3089
+ const node = source.entries.get(key);
3090
+ if (node === void 0) continue;
3091
+ const value = NitroStaticConfig.stringOf(node);
3092
+ if (emptyIsAbsent && value === "") continue;
3093
+ return value === void 0 ? {
3094
+ kind: "computed",
3095
+ file: source.file,
3096
+ reason: `\`${key}\` is not a string literal`
3097
+ } : {
3098
+ kind: "literal",
3099
+ value
3100
+ };
3101
+ }
3102
+ return { kind: "none" };
3103
+ }
3104
+ /** A list option: every source adds to it, so one unproven source makes it unknown. */
3105
+ static list(key, sources) {
3106
+ const values = [];
3107
+ let found = false;
3108
+ for (const source of sources) {
3109
+ if (source.kind === "unproven") return {
3110
+ kind: "computed",
3111
+ file: source.file,
3112
+ reason: source.reason
3113
+ };
3114
+ const node = NitroStaticConfig.unwrap(source.entries.get(key));
3115
+ if (node === void 0) continue;
3116
+ const strings = (node.is(AstNodeType.ArrayExpression) ? node.children("elements") : []).map((element) => NitroStaticConfig.stringOf(element));
3117
+ if (!node.is(AstNodeType.ArrayExpression) || strings.some((value) => value === void 0)) return {
3118
+ kind: "computed",
3119
+ file: source.file,
3120
+ reason: `\`${key}\` is not an array of string literals`
3121
+ };
3122
+ found = true;
3123
+ values.push(...strings);
3124
+ }
3125
+ return found ? {
3126
+ kind: "literal",
3127
+ value: values
3128
+ } : { kind: "none" };
3129
+ }
3130
+ /** The text of a string literal or of a template literal with no placeholder; else `undefined`. */
3131
+ static stringOf(node) {
3132
+ const value = NitroStaticConfig.unwrap(node);
3133
+ if (value === void 0) return;
3134
+ if (typeof value.literalValue === "string") return value.literalValue;
3135
+ if (value.is(AstNodeType.TemplateLiteral) && !value.hasChildren("expressions")) return value.children("quasis")[0]?.child("value").stringAt("cooked");
3136
+ }
3137
+ /** The node without its wrappers (`as`, `satisfies`, `!`, parentheses); `undefined` when absent. */
3138
+ static unwrap(node) {
3139
+ let current = node;
3140
+ while (current !== void 0 && current.exists() && NitroStaticConfig.wrapperTypes.includes(current.type ?? "")) current = current.child("expression");
3141
+ return current === void 0 || !current.exists() ? void 0 : current;
3142
+ }
3143
+ static parse(file) {
3144
+ const parsed = new SourceParser().parse(file, readFileSync(file, "utf8"));
3145
+ return {
3146
+ program: parsed.program,
3147
+ imports: parsed.imports
3148
+ };
3149
+ }
3150
+ };
3151
+ //#endregion
3152
+ //#region src/runtime-export-resolver.ts
3153
+ /**
3154
+ * Tells whether a name that generated code imports is a given export of a package, through the barrel files
3155
+ * of the app. Generated code imports from app source (`../../src/runtime-barrel.js`), and a barrel can
3156
+ * re-export the package under the same name, another name, or through `export *`. The chain is followed with
3157
+ * the compiler's own {@link ReexportIndex}.
3158
+ */
3159
+ var RuntimeExportResolver = class RuntimeExportResolver {
3160
+ outputDirectory;
3161
+ packageName;
3162
+ exportName;
3163
+ parser = new SourceParser();
3164
+ /** The parsed barrel files reached so far, by absolute path. */
3165
+ files = /* @__PURE__ */ new Map();
3166
+ /** The index over {@link files}; dropped when a file joins. */
3167
+ index;
3168
+ /**
3169
+ * @param outputDirectory - The compile output directory, the base of the relative requests of generated code.
3170
+ * @param packageName - The package whose export is looked for (`@heximon/runtime`).
3171
+ * @param exportName - The export looked for (`Platform`).
3172
+ */
3173
+ constructor(outputDirectory, packageName, exportName) {
3174
+ this.outputDirectory = outputDirectory;
3175
+ this.packageName = packageName;
3176
+ this.exportName = exportName;
3177
+ }
3178
+ /**
3179
+ * Whether `name`, imported from `request` by the generated file `importer`, is the export looked for.
3180
+ *
3181
+ * @param importer - The generated file's path, relative to the output directory.
3182
+ * @param request - The module request of the import.
3183
+ * @param name - The imported name; `exportName` for a namespace member.
3184
+ */
3185
+ isTarget(importer, request, name) {
3186
+ if (request === this.packageName) return name === this.exportName;
3187
+ if (!request.startsWith(".")) return false;
3188
+ const barrel = RuntimeExportResolver.resolveFile(dirname(join(this.outputDirectory, importer)), request);
3189
+ if (barrel === void 0) return false;
3190
+ this.load(barrel);
3191
+ const canonical = this.indexOfFiles().canonicalize({
3192
+ sourceFile: barrel,
3193
+ className: name
3194
+ });
3195
+ return canonical.sourceFile === this.packageName && canonical.className === this.exportName;
3196
+ }
3197
+ /** The source file a relative request names, or `undefined` when no file exists (a generated file, for one). */
3198
+ static resolveFile(directory, request) {
3199
+ return SourceModuleResolution.resolve(join(directory, request));
3200
+ }
3201
+ /** Parse `path` and the files its export statements lead to. */
3202
+ load(path) {
3203
+ if (this.files.has(path)) return;
3204
+ let parsed;
3205
+ try {
3206
+ parsed = this.parser.parse(path, readFileSync(path, "utf8"));
3207
+ } catch {
3208
+ return;
3209
+ }
3210
+ this.files.set(path, parsed);
3211
+ this.index = void 0;
3212
+ const exportedLocals = new Set(parsed.exports.flatMap((entry) => entry.request === null ? [entry.localName ?? entry.exportedName] : []));
3213
+ const requests = [...parsed.exports.map((entry) => entry.request), ...parsed.imports.filter((entry) => exportedLocals.has(entry.localName)).map((entry) => entry.request)];
3214
+ for (const request of requests) if (request?.startsWith(".")) {
3215
+ const next = RuntimeExportResolver.resolveFile(dirname(path), request);
3216
+ if (next !== void 0) this.load(next);
3217
+ }
3218
+ }
3219
+ indexOfFiles() {
3220
+ this.index ??= new ReexportIndex([...this.files].map(([filePath, parsed]) => ({
3221
+ filePath,
3222
+ exports: parsed.exports,
3223
+ imports: parsed.imports
3224
+ })), (from, request) => RuntimeExportResolver.resolveFile(dirname(from), request) ?? null, false, (specifier, name) => specifier === this.packageName && name === this.exportName);
3225
+ return this.index;
3226
+ }
3227
+ };
3228
+ //#endregion
3229
+ //#region src/platform-binding-census.ts
3230
+ /**
3231
+ * Finds the platform bindings (Workers KV, Hyperdrive, Durable Objects, …) a compiled graph needs, before
3232
+ * anything boots. A binding exists only on its platform, so a run outside it must refuse instead of failing
3233
+ * on the first read.
3234
+ *
3235
+ * Two facts name a binding. The generated wiring holds the string literal of each `Platform.binding("NAME")`
3236
+ * call, with the module that owns the file. The deploy-config fragments of the compile list the bindings the
3237
+ * sidecar declares. A binding read through a variable name is in neither place, and the run then fails on
3238
+ * the read itself.
3239
+ */
3240
+ var PlatformBindingCensus = class PlatformBindingCensus {
3241
+ /** The package whose `Platform` the generated code reads bindings from. */
3242
+ static runtimePackage = "@heximon/runtime";
3243
+ /** The `Platform` class and the method of it that reads a binding. */
3244
+ static platformExport = "Platform";
3245
+ static bindingMethod = "binding";
3246
+ /** The module wiring files: `modules/<name>.wiring.js` exports `init<ModuleClass>`. */
3247
+ static moduleInitializerPattern = /export async function init(\w+)\(/;
3248
+ static platformBindingKind = "platform binding";
3249
+ /** The deploy-config sections that declare bindings, by section name. */
3250
+ static declaringSections = {
3251
+ "kv-namespaces": (payload) => PlatformBindingCensus.entriesOf(payload["kvNamespaces"], "binding").map((binding) => ({
3252
+ binding,
3253
+ neededBy: "`cloudflare.kvNamespaces` in heximon.config.ts"
3254
+ })),
3255
+ hyperdrive: (payload) => PlatformBindingCensus.entriesOf(payload["hyperdrive"], "binding").map((binding) => ({
3256
+ binding,
3257
+ neededBy: "`cloudflare.hyperdrive` in heximon.config.ts"
3258
+ })),
3259
+ "r2-buckets": (payload) => PlatformBindingCensus.entriesOf(payload["r2Buckets"], "binding").map((binding) => ({
3260
+ binding,
3261
+ neededBy: "`cloudflare.r2Buckets` in heximon.config.ts"
3262
+ })),
3263
+ browser: (payload) => PlatformBindingCensus.entriesOf([payload["browser"]], "binding").map((binding) => ({
3264
+ binding,
3265
+ neededBy: "`cloudflare.browser` in heximon.config.ts"
3266
+ })),
3267
+ "queue-bindings": (payload) => PlatformBindingCensus.entriesOf(payload["producers"], "binding").map((binding) => ({
3268
+ binding,
3269
+ neededBy: "`cloudflare.queues` in heximon.config.ts"
3270
+ })),
3271
+ "integration-event-subscriptions": (payload) => PlatformBindingCensus.entriesOf(payload["producers"], "binding").map((binding) => ({
3272
+ binding,
3273
+ neededBy: "the cross-service integration events"
3274
+ })),
3275
+ "workflow-bindings": (payload) => PlatformBindingCensus.entriesOf(payload["workflows"], "binding").map((binding) => ({
3276
+ binding,
3277
+ neededBy: "a Cloudflare workflow"
3278
+ })),
3279
+ "durable-objects": (payload) => PlatformBindingCensus.durableBindings(payload["bindings"])
3280
+ };
3281
+ /** The kind label of each deploy-config section. */
3282
+ static sectionKinds = {
3283
+ "kv-namespaces": "Workers KV",
3284
+ hyperdrive: "Hyperdrive",
3285
+ "r2-buckets": "R2 bucket",
3286
+ browser: "Browser Rendering",
3287
+ "queue-bindings": "Cloudflare Queue",
3288
+ "integration-event-subscriptions": "Cloudflare Queue",
3289
+ "workflow-bindings": "Cloudflare Workflow",
3290
+ "durable-objects": "Durable Object"
3291
+ };
3292
+ /**
3293
+ * The bindings the compile output needs, sorted by name.
3294
+ *
3295
+ * @param output - The production compile output.
3296
+ * @param platformName - The platform the graph was compiled for. A deploy-config fragment for another
3297
+ * platform is inert and does not count: the Durable Objects of a Node graph run in-process.
3298
+ * @param outputDirectory - The compile output directory, where the relative imports of generated code start.
3299
+ * @returns One entry for each binding name, with every reason the compile found.
3300
+ */
3301
+ static collect(output, platformName, outputDirectory) {
3302
+ const needs = /* @__PURE__ */ new Map();
3303
+ const record = (binding, kind, neededBy) => {
3304
+ const known = needs.get(binding);
3305
+ if (known === void 0) {
3306
+ needs.set(binding, {
3307
+ kind,
3308
+ neededBy: /* @__PURE__ */ new Set([neededBy])
3309
+ });
3310
+ return;
3311
+ }
3312
+ known.neededBy.add(neededBy);
3313
+ if (known.kind === PlatformBindingCensus.platformBindingKind) known.kind = kind;
3314
+ };
3315
+ const hostEntryPaths = new Set(output.hostEntries.map((entry) => entry.path));
3316
+ const platform = new RuntimeExportResolver(outputDirectory, PlatformBindingCensus.runtimePackage, PlatformBindingCensus.platformExport);
3317
+ for (const file of output.files) {
3318
+ if (!file.path.endsWith(".js") || hostEntryPaths.has(file.path)) continue;
3319
+ for (const binding of PlatformBindingCensus.calledBindings(file, platform)) record(binding, PlatformBindingCensus.platformBindingKind, PlatformBindingCensus.ownerOf(file));
3320
+ }
3321
+ for (const contribution of output.buildConfigContributions) for (const declared of PlatformBindingCensus.declaredBy(contribution, platformName)) record(declared.binding, PlatformBindingCensus.sectionKinds[contribution.section] ?? PlatformBindingCensus.platformBindingKind, declared.neededBy);
3322
+ return [...needs.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([binding, need]) => ({
3323
+ binding,
3324
+ kind: need.kind,
3325
+ neededBy: [...need.neededBy].sort()
3326
+ }));
3327
+ }
3328
+ /**
3329
+ * The refusal text: each binding with its kind and the places that need it.
3330
+ *
3331
+ * @param needs - The bindings from {@link collect}.
3332
+ * @param platformName - The platform of the compiled graph, when the sidecar names one.
3333
+ */
3334
+ static explain(needs, platformName) {
3335
+ const lines = needs.map((need) => ` - ${need.binding} (${need.kind}): ${need.neededBy.join("; ")}`);
3336
+ return [
3337
+ `This graph needs platform bindings that exist only on ${platformName ?? "its deploy platform"}, so a run outside it would fail on the first read. Nothing was booted.`,
3338
+ ...lines,
3339
+ "Run this task on the platform that has these bindings."
3340
+ ].join("\n");
3341
+ }
3342
+ /**
3343
+ * The names in the literal `Platform.binding("NAME")` calls of one generated file. The callee must resolve
3344
+ * to the `Platform` of `@heximon/runtime`, under any local name, through a namespace import, or through the
3345
+ * barrel files of the app that re-export it; a call on another object is not a binding read.
3346
+ */
3347
+ static calledBindings(file, platform) {
3348
+ let parsed;
3349
+ try {
3350
+ parsed = new SourceParser().parse(file.path, file.content);
3351
+ } catch {
3352
+ return [];
3353
+ }
3354
+ if (parsed.errors.length > 0) return [];
3355
+ const platformNames = /* @__PURE__ */ new Set();
3356
+ const namespaces = /* @__PURE__ */ new Map();
3357
+ for (const entry of parsed.imports) {
3358
+ if (entry.isTypeOnly) continue;
3359
+ if (entry.importKind === ImportKind.Namespace) namespaces.set(entry.localName, entry.request);
3360
+ else if (entry.importKind === ImportKind.Named && platform.isTarget(file.path, entry.request, entry.importedName)) platformNames.add(entry.localName);
3361
+ }
3362
+ const bindings = [];
3363
+ for (const call of parsed.program.descendants(AstNodeType.CallExpression)) {
3364
+ const callee = call.child("callee");
3365
+ if (callee.is(AstNodeType.MemberExpression) && PlatformBindingCensus.memberName(callee) === PlatformBindingCensus.bindingMethod && PlatformBindingCensus.isPlatform(callee.child("object"), file.path, platform, platformNames, namespaces)) {
3366
+ const name = PlatformBindingCensus.literalText(call.children("arguments")[0]);
3367
+ if (name !== void 0) bindings.push(name);
3368
+ }
3369
+ }
3370
+ return bindings;
3371
+ }
3372
+ /** Whether `node` names the runtime `Platform`: an imported name, or `<namespace>.<name>` of a module that exports it. */
3373
+ static isPlatform(node, importer, platform, platformNames, namespaces) {
3374
+ if (node.is(AstNodeType.Identifier)) return platformNames.has(node.name ?? "");
3375
+ const namespace = node.child("object");
3376
+ const request = namespace.is(AstNodeType.Identifier) ? namespaces.get(namespace.name ?? "") : void 0;
3377
+ const member = node.is(AstNodeType.MemberExpression) ? PlatformBindingCensus.memberName(node) : void 0;
3378
+ return request !== void 0 && member !== void 0 && platform.isTarget(importer, request, member);
3379
+ }
3380
+ /** The property name of `object.name` or `object["name"]`, or `undefined` for a computed name. */
3381
+ static memberName(node) {
3382
+ const property = node.child("property");
3383
+ if (!node.flagAt("computed")) return property.name;
3384
+ return typeof property.literalValue === "string" ? property.literalValue : void 0;
3385
+ }
3386
+ /** The text of a string literal, or of a template literal with no placeholder; else `undefined`. */
3387
+ static literalText(node) {
3388
+ if (node === void 0 || !node.exists()) return;
3389
+ if (typeof node.literalValue === "string") return node.literalValue;
3390
+ const quasis = node.is(AstNodeType.TemplateLiteral) ? node.children("quasis") : [];
3391
+ return quasis.length === 1 && !node.hasChildren("expressions") ? quasis[0].child("value").stringAt("cooked") ?? void 0 : void 0;
3392
+ }
3393
+ /** Who a generated file belongs to: its module, or the file itself. */
3394
+ static ownerOf(file) {
3395
+ const moduleClass = PlatformBindingCensus.moduleInitializerPattern.exec(file.content)?.[1];
3396
+ return moduleClass === void 0 ? `generated file ${file.path}` : `module ${moduleClass}`;
3397
+ }
3398
+ /** The bindings a Cloudflare-targeted deploy-config fragment declares. */
3399
+ static declaredBy(contribution, platformName) {
3400
+ if (contribution.target !== DeployPlatform.Cloudflare || contribution.target !== platformName) return [];
3401
+ const declare = PlatformBindingCensus.declaringSections[contribution.section];
3402
+ const payload = contribution.payload;
3403
+ if (declare === void 0 || typeof payload !== "object" || payload === null) return [];
3404
+ return declare(payload);
3405
+ }
3406
+ /** The Durable Object bindings, each owned by its class. */
3407
+ static durableBindings(value) {
3408
+ if (!Array.isArray(value)) return [];
3409
+ const declared = [];
3410
+ for (const entry of value) {
3411
+ const record = entry;
3412
+ if (typeof record?.name === "string") declared.push({
3413
+ binding: record.name,
3414
+ neededBy: typeof record.class_name === "string" ? `Durable Object class ${record.class_name}` : "a Durable Object"
3415
+ });
3416
+ }
3417
+ return declared;
3418
+ }
3419
+ /** The string `key` of each record in a payload list. */
3420
+ static entriesOf(value, key) {
3421
+ if (!Array.isArray(value)) return [];
3422
+ return value.flatMap((entry) => {
3423
+ const name = entry?.[key];
3424
+ return typeof name === "string" && name.length > 0 ? [name] : [];
3425
+ });
3426
+ }
3427
+ };
3428
+ //#endregion
3429
+ //#region src/stdout-redirect.ts
3430
+ /**
3431
+ * Sends everything written to stdout to stderr for a while, so the log lines of an app that runs in this
3432
+ * process cannot mix with the JSON the command prints. The original writer stays reachable through
3433
+ * {@link write} for that JSON.
3434
+ */
3435
+ var StdoutRedirect = class {
3436
+ original;
3437
+ /** Start the redirect. A second call does nothing. */
3438
+ start() {
3439
+ if (this.original !== void 0) return;
3440
+ this.original = process.stdout.write.bind(process.stdout);
3441
+ process.stdout.write = ((...parameters) => process.stderr.write(...parameters));
3442
+ }
3443
+ /** Write to the real stdout, redirected or not. */
3444
+ write(text) {
3445
+ (this.original ?? process.stdout.write.bind(process.stdout))(text);
3446
+ }
3447
+ /** Stop the redirect. */
3448
+ stop() {
3449
+ if (this.original !== void 0) {
3450
+ process.stdout.write = this.original;
3451
+ this.original = void 0;
3452
+ }
3453
+ }
3454
+ };
3455
+ //#endregion
3456
+ //#region src/task-target-resolver.ts
3457
+ /** Thrown when the names do not pick exactly one module and one task. The message is ready to print. */
3458
+ var TaskTargetError = class extends Error {
3459
+ constructor(message) {
3460
+ super(message);
3461
+ this.name = "TaskTargetError";
3462
+ }
3463
+ };
3464
+ /**
3465
+ * Picks the module class and the task class that `heximon task run <ModuleClass> <TaskClass>` names, by class
3466
+ * name, in the compiled graph. A name that matches nothing or more than one class fails with the names that
3467
+ * come close.
3468
+ */
3469
+ var TaskTargetResolver = class TaskTargetResolver {
3470
+ /** The most edits between a typed name and a suggestion. */
3471
+ static maximumSuggestionDistance = 3;
3472
+ /** The most names a message lists. */
3473
+ static maximumListed = 8;
3474
+ /**
3475
+ * @param graph - The module graph of the production compile.
3476
+ * @param moduleName - The module class name.
3477
+ * @param taskName - The task class name; the module must provide it.
3478
+ * @returns The module and the task.
3479
+ * @throws {TaskTargetError} When a name matches no class, or more than one.
3480
+ */
3481
+ static resolve(graph, moduleName, taskName) {
3482
+ const modules = TaskTargetResolver.distinct(graph.modules.map((node) => node.reference), moduleName);
3483
+ if (modules.length === 0) throw new TaskTargetError(`No module class named "${moduleName}" is in the compiled graph.` + TaskTargetResolver.suggestions(moduleName, graph.modules.map((node) => TaskTargetResolver.nameOf(node.reference)), "Modules in the graph"));
3484
+ if (modules.length > 1) throw new TaskTargetError(`More than one module class is named "${moduleName}":\n` + modules.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
3485
+ const module = modules[0];
3486
+ const nodes = graph.modules.filter((node) => TaskTargetResolver.isSame(node.reference, module));
3487
+ const providerTokens = nodes.flatMap((node) => node.providers.map((provider) => provider.token));
3488
+ const tasks = TaskTargetResolver.distinct(providerTokens, taskName);
3489
+ if (tasks.length === 0) throw new TaskTargetError(`The module ${moduleName} does not provide a class named "${taskName}".` + TaskTargetResolver.otherOwner(graph, nodes, taskName) + TaskTargetResolver.suggestions(taskName, providerTokens.map((token) => TaskTargetResolver.nameOf(token)), `Providers of ${moduleName}`));
3490
+ if (tasks.length > 1) throw new TaskTargetError(`The module ${moduleName} provides more than one class named "${taskName}":\n` + tasks.map((reference) => ` - ${reference.sourceFile}`).join("\n"));
3491
+ return {
3492
+ module,
3493
+ task: tasks[0]
3494
+ };
3495
+ }
3496
+ /** The class name a person typed: the declared name when the compiler aliased the export. */
3497
+ static nameOf(reference) {
3498
+ return reference.displayName ?? reference.className;
3499
+ }
3500
+ /** Whether two references name the same class. */
3501
+ static isSame(left, right) {
3502
+ return left.sourceFile === right.sourceFile && left.className === right.className;
3503
+ }
3504
+ /** The references that carry `name`, one for each class, in graph order. */
3505
+ static distinct(references, name) {
3506
+ const found = [];
3507
+ for (const reference of references) if (TaskTargetResolver.nameOf(reference) === name && !found.some((known) => TaskTargetResolver.isSame(known, reference))) found.push(reference);
3508
+ return found;
3509
+ }
3510
+ /** A hint when another module of the graph provides the class. */
3511
+ static otherOwner(graph, own, taskName) {
3512
+ const owners = graph.modules.filter((node) => !own.includes(node) && node.providers.some((provider) => TaskTargetResolver.nameOf(provider.token) === taskName));
3513
+ if (owners.length === 0) return "";
3514
+ return ` The class is provided by ${[...new Set(owners.map((node) => TaskTargetResolver.nameOf(node.reference)))].join(", ")}.`;
3515
+ }
3516
+ /** The "did you mean" line, or the start of the list when nothing is close. */
3517
+ static suggestions(typed, candidates, listTitle) {
3518
+ const unique = [...new Set(candidates)];
3519
+ const close = unique.filter((candidate) => candidate.toLowerCase().includes(typed.toLowerCase()) || typed.toLowerCase().includes(candidate.toLowerCase()) || TaskTargetResolver.distance(candidate.toLowerCase(), typed.toLowerCase()) <= TaskTargetResolver.maximumSuggestionDistance);
3520
+ if (close.length > 0) return ` Did you mean ${close.slice(0, TaskTargetResolver.maximumListed).map((name) => `"${name}"`).join(", ")}?`;
3521
+ const listed = unique.slice(0, TaskTargetResolver.maximumListed);
3522
+ return listed.length === 0 ? "" : ` ${listTitle}: ${listed.join(", ")}${unique.length > listed.length ? ", …" : ""}.`;
3523
+ }
3524
+ /** The Levenshtein distance between two names. */
3525
+ static distance(left, right) {
3526
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
3527
+ for (let row = 1; row <= left.length; row += 1) {
3528
+ const current = [row];
3529
+ for (let column = 1; column <= right.length; column += 1) {
3530
+ const substitution = left[row - 1] === right[column - 1] ? 0 : 1;
3531
+ current[column] = Math.min((previous[column] ?? 0) + 1, (current[column - 1] ?? 0) + 1, (previous[column - 1] ?? 0) + substitution);
3532
+ }
3533
+ previous = current;
3534
+ }
3535
+ return previous[right.length] ?? 0;
3536
+ }
3537
+ };
3538
+ //#endregion
3539
+ //#region src/tsconfig-aliases.ts
3540
+ /**
3541
+ * Reads the module aliases of a Nitro project from the `paths` of its tsconfig files. Nitro writes the aliases
3542
+ * of a project into a generated tsconfig, and the project's tsconfig extends it and adds its own. The
3543
+ * files are read as they are on disk: nothing is generated, and no config of the project runs.
3544
+ *
3545
+ * TypeScript's rules apply. A `paths` of a config replaces the `paths` of what it extends. `baseUrl` is the
3546
+ * nearest one in the chain, and a target is relative to it; without one, a target is relative to the directory
3547
+ * of the config that sets `paths`. An exact pattern wins over a wildcard one, and a longer wildcard prefix wins
3548
+ * over a shorter one.
3549
+ */
3550
+ var TsconfigAliases = class TsconfigAliases {
3551
+ /** Where Nitro writes its generated tsconfig, below the build directory. */
3552
+ static generatedFiles = ["node_modules/.nitro/types/tsconfig.json", ".nitro/types/tsconfig.json"];
3553
+ /** The longest `extends` chain followed. */
3554
+ static maximumDepth = 8;
3555
+ /**
3556
+ * The aliases of the project. The project's tsconfig wins over the generated one that it does not extend.
3557
+ *
3558
+ * @param rootDir - The project root.
3559
+ */
3560
+ static read(rootDir) {
3561
+ const exact = {};
3562
+ const prefixes = {};
3563
+ for (const file of [...TsconfigAliases.generatedFiles.map((name) => join(rootDir, name)), join(rootDir, "tsconfig.json")]) {
3564
+ const options = TsconfigAliases.optionsOf(file, 0);
3565
+ if (options.paths === void 0) continue;
3566
+ const base = options.baseUrl ?? options.paths.directory;
3567
+ for (const [pattern, targets] of Object.entries(options.paths.entries)) {
3568
+ const target = targets[0];
3569
+ if (target === void 0) continue;
3570
+ if (pattern.endsWith("/*") && target.endsWith("/*")) prefixes[pattern.slice(0, -2)] = resolve(base, target.slice(0, -2));
3571
+ else if (!pattern.includes("*") && !target.includes("*")) exact[pattern] = resolve(base, target);
3572
+ }
3573
+ }
3574
+ return {
3575
+ exact,
3576
+ prefixes
3577
+ };
3578
+ }
3579
+ /** The effective options of one tsconfig, after the files it extends. */
3580
+ static optionsOf(file, depth) {
3581
+ if (!existsSync(file) || depth > TsconfigAliases.maximumDepth) return {};
3582
+ let config;
3583
+ try {
3584
+ config = JSON.parse(TsconfigAliases.toJson(readFileSync(file, "utf8")));
3585
+ } catch {
3586
+ return {};
3587
+ }
3588
+ const options = {};
3589
+ const parents = typeof config.extends === "string" ? [config.extends] : [...config.extends ?? []];
3590
+ for (const parent of parents) if (parent.startsWith(".") || isAbsolute(parent)) {
3591
+ const path = resolve(dirname(file), parent);
3592
+ const inherited = TsconfigAliases.optionsOf(path.endsWith(".json") ? path : `${path}.json`, depth + 1);
3593
+ Object.assign(options, inherited);
3594
+ }
3595
+ if (config.compilerOptions?.baseUrl !== void 0) options.baseUrl = resolve(dirname(file), config.compilerOptions.baseUrl);
3596
+ if (config.compilerOptions?.paths !== void 0) options.paths = {
3597
+ entries: config.compilerOptions.paths,
3598
+ directory: dirname(file)
3599
+ };
3600
+ return options;
3601
+ }
3602
+ /**
3603
+ * A tsconfig is JSON with comments and trailing commas. This removes both and leaves every string as it is,
3604
+ * so `JSON.parse` can read the text.
3605
+ */
3606
+ static toJson(text) {
3607
+ let withoutComments = "";
3608
+ let inString = false;
3609
+ for (let index = 0; index < text.length; index += 1) {
3610
+ const character = text[index];
3611
+ if (inString) {
3612
+ withoutComments += character;
3613
+ if (character === "\\") withoutComments += text[++index] ?? "";
3614
+ else if (character === "\"") inString = false;
3615
+ } else if (character === "\"") {
3616
+ inString = true;
3617
+ withoutComments += character;
3618
+ } else if (character === "/" && text[index + 1] === "/") {
3619
+ while (index < text.length && text[index] !== "\n") index += 1;
3620
+ withoutComments += "\n";
3621
+ } else if (character === "/" && text[index + 1] === "*") {
3622
+ index = text.indexOf("*/", index + 2);
3623
+ index = index === -1 ? text.length : index + 1;
3624
+ } else withoutComments += character;
3625
+ }
3626
+ let json = "";
3627
+ inString = false;
3628
+ for (let index = 0; index < withoutComments.length; index += 1) {
3629
+ const character = withoutComments[index];
3630
+ if (inString) {
3631
+ json += character;
3632
+ if (character === "\\") json += withoutComments[++index] ?? "";
3633
+ else if (character === "\"") inString = false;
3634
+ } else if (character === "\"") {
3635
+ inString = true;
3636
+ json += character;
3637
+ } else if (character === "," && /^\s*[}\]]/.test(withoutComments.slice(index + 1))) continue;
3638
+ else json += character;
3639
+ }
3640
+ return json;
3641
+ }
3642
+ };
3643
+ //#endregion
3644
+ //#region src/task-runner.ts
3645
+ /** The in-process client of a run. A run serves no HTTP, so every call fails with the reason. */
3646
+ var RefusingInternalClient = class {
3647
+ fetch(input) {
3648
+ return Promise.reject(/* @__PURE__ */ new Error(`InternalClient.fetch(${JSON.stringify(input)}) is not available in \`heximon task run\`: the run boots no HTTP surface, so a task cannot call the app's own routes.`));
3649
+ }
3650
+ };
3651
+ /** The failure a run reports to the caller; the message is ready to print. */
3652
+ var TaskRunFailure = class extends Error {};
3653
+ /**
3654
+ * `heximon task run`: compiles the production graph into a temporary directory, boots it in this process,
3655
+ * runs one {@link Task}, prints its result as JSON, and removes the compile output.
3656
+ *
3657
+ * The boot starts no HTTP listener and none of the standing work of the deployed app — no schedule timers,
3658
+ * queue consumers, pollers or sweeps, and no outbox relay unless the caller asked for it — so a run delivers
3659
+ * nothing of what the deployed app has pending. The result goes to stdout; everything else goes to stderr, the
3660
+ * log lines of the app included.
3661
+ */
3662
+ var TaskRunner = class TaskRunner {
3663
+ options;
3664
+ /** The name prefix of the temporary compile output; the workspace ignore globs match it. */
3665
+ static outputDirectoryPrefix = ".heximon-task-";
3666
+ /** The most the run waits for in-flight background work before it shuts the app down. */
3667
+ static drainTimeoutMilliseconds = 1e4;
3668
+ /** The most the interrupt handler waits for the app to shut down. */
3669
+ static interruptShutdownMilliseconds = 5e3;
3670
+ static wiringFilePattern = /^apps\/.+\.wiring\.js$/;
3671
+ outputDirectory;
3672
+ app;
3673
+ context;
3674
+ platformName;
3675
+ /** The Nitro alias table of the project; empty for any other project. */
3676
+ aliases = {
3677
+ exact: {},
3678
+ prefixes: {}
3679
+ };
3680
+ runtime;
3681
+ restoreStandingWork;
3682
+ restoreHostBindings;
3683
+ stdout = new StdoutRedirect();
3684
+ /** The result of the task as one line of JSON, printed after the shutdown. */
3685
+ serialized;
3686
+ disposing;
3687
+ constructor(options) {
3688
+ this.options = options;
3689
+ }
3690
+ /**
3691
+ * Run the task.
3692
+ *
3693
+ * @returns The process exit code: `0` when the task returned, `1` for every other outcome. The reason is
3694
+ * already on stderr.
3695
+ */
3696
+ async run() {
3697
+ let interrupts = 0;
3698
+ const interrupt = (signal) => {
3699
+ interrupts += 1;
3700
+ this.interrupted(signal, interrupts > 1);
3701
+ };
3702
+ process.on("SIGINT", interrupt);
3703
+ process.on("SIGTERM", interrupt);
3704
+ let exitCode = 0;
3705
+ this.stdout.start();
3706
+ try {
3707
+ this.outputDirectory = mkdtempSync(join(this.options.rootDir, TaskRunner.outputDirectoryPrefix));
3708
+ this.runtime = await TaskRunner.projectRuntime(this.outputDirectory);
3709
+ const output = await this.compile();
3710
+ const target = this.pickTarget(output);
3711
+ TaskRunner.refuseBindings(output, this.platformName, this.outputDirectory);
3712
+ this.materialize(output);
3713
+ await this.boot(output);
3714
+ await this.execute(target);
3715
+ } catch (error) {
3716
+ TaskRunner.report(error);
3717
+ exitCode = 1;
3718
+ } finally {
3719
+ if (!await this.dispose()) exitCode = 1;
3720
+ this.stdout.stop();
3721
+ if (this.serialized !== void 0) this.stdout.write(`${this.serialized}\n`);
3722
+ process.off("SIGINT", interrupt);
3723
+ process.off("SIGTERM", interrupt);
3724
+ }
3725
+ return exitCode;
3726
+ }
3727
+ /** The `@heximon/runtime` of the project, the copy the generated wiring imports. */
3728
+ static async projectRuntime(directory) {
3729
+ const runtime = await ProjectModules.runtime(directory);
3730
+ if (typeof runtime.StandingWork?.suspend !== "function") throw new TaskRunFailure("The @heximon/runtime of this project is older than this command. Update @heximon/runtime and heximon to one version.");
3731
+ return runtime;
3732
+ }
3733
+ /**
3734
+ * Compile the production graph into the temporary directory. A Nitro host compiles with the inputs of its
3735
+ * own build (the preset, the plugins, the source directories); any other project with its sidecar.
3736
+ */
3737
+ async compile() {
3738
+ const rootDir = this.options.rootDir;
3739
+ const outputDirectory = this.outputDirectory;
3740
+ let output;
3741
+ let compiler;
3742
+ const nitro = NitroStaticConfig.read(rootDir);
3743
+ this.aliases = nitro.isNitroHost ? TsconfigAliases.read(rootDir) : {
3744
+ exact: {},
3745
+ prefixes: {}
3746
+ };
3747
+ if (nitro.isNitroHost) {
3748
+ const sourceDirectories = TaskRunner.sourceDirectoriesOf(nitro, this.options.isSourceExplicit === true ? this.options.sourceDirectories ?? [] : void 0);
3749
+ const preset = nitro.preset;
3750
+ const host = await NitroHost.open(rootDir, outputDirectory, {
3751
+ preset: preset.kind === "literal" ? preset.value : void 0,
3752
+ platformFromSidecar: preset.kind === "computed",
3753
+ aliases: this.aliases,
3754
+ ...sourceDirectories
3755
+ });
3756
+ if (preset.kind === "computed" && !host.hasSidecarPlatform()) throw new TaskRunFailure(`The Nitro preset is computed (${preset.reason}, in ${preset.file}), and the command does not run your config files, so it cannot read the platform: declare \`platform\` in heximon.config.ts (for example \`platform: new CloudflareWorkersStrategy()\`), or set NITRO_PRESET. Nothing was booted.`);
3757
+ const compiled = host.compile(rootDir, outputDirectory, this.options.isSourceExplicit === true ? this.options.sourceDirectories : void 0);
3758
+ ({output, compiler} = compiled);
3759
+ this.platformName = compiled.platformName;
3760
+ } else {
3761
+ const session = await ProjectCompileSession.open({
3762
+ rootDir,
3763
+ environment: "production",
3764
+ ...this.options.sourceDirectories === void 0 ? {} : { sourceDirectories: this.options.sourceDirectories }
3765
+ });
3766
+ this.platformName = session.platformName();
3767
+ output = await session.compileProduction(outputDirectory, this.aliases);
3768
+ compiler = session.compiler;
3769
+ }
3770
+ const errors = output.diagnostics.filter((diagnostic) => diagnostic.severity === DiagnosticSeverity.Error);
3771
+ if (output.diagnostics.length > 0) CompileDiagnostics.renderToStderr(rootDir, output.diagnostics);
3772
+ if (errors.length > 0) throw new TaskRunFailure(`The production graph has ${errors.length} compile error${errors.length === 1 ? "" : "s"}. Nothing was booted.`);
3773
+ const findings = LibraryFactoryPreflight.inspect(compiler.consumedBoundaryPackages(), LibraryFactoryPreflight.productionBuildOptions);
3774
+ if (findings.length > 0) throw new TaskRunFailure(LibraryFactoryPreflight.explain(findings));
3775
+ return output;
3776
+ }
3777
+ /**
3778
+ * The literal source directories of the Nitro config, for the host inputs. A value that is not a literal is
3779
+ * refused, because dropping it would compile a smaller graph; `--src` names the directories instead.
3780
+ */
3781
+ static sourceDirectoriesOf(nitro, explicit) {
3782
+ if (explicit !== void 0) return { scanDirs: explicit };
3783
+ for (const [name, value] of [["serverDir", nitro.serverDir], ["scanDirs", nitro.scanDirs]]) if (value.kind === "computed") throw new TaskRunFailure(`The Nitro \`${name}\` is computed (${value.reason}, in ${value.file}), and the command does not run your config files, so it cannot read the source directories. Name them with \`--src <dir>\` (repeat the option, or separate directories with commas). Nothing was booted.`);
3784
+ return {
3785
+ ...nitro.serverDir.kind === "literal" ? { serverDir: nitro.serverDir.value } : {},
3786
+ ...nitro.scanDirs.kind === "literal" ? { scanDirs: nitro.scanDirs.value } : {}
3787
+ };
3788
+ }
3789
+ /** Pick the module and the task from the compiled graph. */
3790
+ pickTarget(output) {
3791
+ if (output.moduleGraph === void 0) throw new TaskRunFailure("The compile produced no module graph, so the task cannot be found.");
3792
+ return TaskTargetResolver.resolve(output.moduleGraph, this.options.moduleName, this.options.taskName);
3793
+ }
3794
+ /** Refuse a graph that needs bindings only its platform has, before anything boots. */
3795
+ static refuseBindings(output, platformName, outputDirectory) {
3796
+ const needs = PlatformBindingCensus.collect(output, platformName, outputDirectory);
3797
+ if (needs.length > 0) throw new TaskRunFailure(PlatformBindingCensus.explain(needs, platformName));
3798
+ }
3799
+ /** Write the generated files into the temporary directory. */
3800
+ materialize(output) {
3801
+ for (const file of output.files) {
3802
+ const target = join(this.outputDirectory, file.path);
3803
+ mkdirSync(dirname(target), { recursive: true });
3804
+ writeFileSync(target, file.content);
3805
+ }
3806
+ }
3807
+ /** Boot the app with no HTTP surface and without the standing work of the deployed app. */
3808
+ async boot(output) {
3809
+ const wiring = output.files.find((file) => TaskRunner.wiringFilePattern.test(file.path));
3810
+ if (wiring === void 0) throw new TaskRunFailure("No app wiring file was generated. Make sure the project has a root module (a module that no other module imports).");
3811
+ const runtime = this.runtime;
3812
+ if (TransformBoot.isBootProcess()) TransformBoot.registerSourceResolver(new Map(output.sourceRewrites.map((rewrite) => [rewrite.filePath, rewrite.content])), this.aliases);
3813
+ runtime.StandingWork.suspend(runtime.StandingWork.kinds().filter((kind) => !(this.options.withOutboxRelay && kind === runtime.StandingWorkKind.OutboxRelay)));
3814
+ this.restoreStandingWork = () => runtime.StandingWork.suspend([]);
3815
+ runtime.Platform.capture(process.env);
3816
+ const runtimeConfigFile = output.files.find((file) => file.path === RuntimeConfigFile.path);
3817
+ const source = (runtimeConfigFile === void 0 ? void 0 : await import(pathToFileURL(join(this.outputDirectory, runtimeConfigFile.path)).href))?.[RuntimeConfigFile.sourceExport];
3818
+ const coreConfig = source === void 0 ? new runtime.CoreConfig(process.env) : runtime.CoreConfig.resolve(process.env, source);
3819
+ runtime.Logger.configure(coreConfig.logger);
3820
+ const wiringModule = await import(pathToFileURL(join(this.outputDirectory, wiring.path)).href);
3821
+ const bindings = {
3822
+ InternalClient: new RefusingInternalClient(),
3823
+ CoreConfig: coreConfig
3824
+ };
3825
+ runtime.HostBindings.capture(bindings);
3826
+ this.restoreHostBindings = () => runtime.HostBindings.capture({});
3827
+ this.app = await wiringModule.createApp(bindings);
3828
+ this.context = await this.app.from(runtime.CoreModule).get(runtime.Context);
3829
+ }
3830
+ /** Run the task and print its result. */
3831
+ async execute(target) {
3832
+ const runtime = this.runtime;
3833
+ const moduleClass = await this.importClass(target.module.sourceFile, target.module.className);
3834
+ const taskClass = await this.importClass(target.task.sourceFile, target.task.className);
3835
+ if (typeof taskClass.prototype.run !== "function") throw new TaskRunFailure(`${this.options.taskName} is not a task: the class has no run() method. A task extends Task.`);
3836
+ const manager = new runtime.TaskManager(this.app, this.context, await this.seedsOf(runtime));
3837
+ let outcome;
3838
+ try {
3839
+ outcome = await manager.from(moduleClass).run(taskClass, {
3840
+ name: this.options.taskName,
3841
+ ...this.options.payload === void 0 ? {} : { payload: this.options.payload }
3842
+ });
3843
+ } catch (error) {
3844
+ throw new TaskRunFailure(`The task ${this.options.taskName} failed:\n${error instanceof Error ? error.stack ?? error.message : String(error)}`, { cause: error });
3845
+ }
3846
+ try {
3847
+ this.serialized = JSON.stringify(outcome);
3848
+ } catch (error) {
3849
+ throw new TaskRunFailure(`The task ran, but its result is not JSON: ${error instanceof Error ? error.message : String(error)}`);
3850
+ }
3851
+ }
3852
+ /** The seeds the app declares for every background frame, or none. */
3853
+ async seedsOf(runtime) {
3854
+ const resolvable = this.app;
3855
+ try {
3856
+ return await resolvable.resolveUnscoped?.(runtime.ContextSeeds);
3857
+ } catch (error) {
3858
+ if (error instanceof Error && error.message.startsWith("No provider for")) return;
3859
+ throw error;
3860
+ }
3861
+ }
3862
+ /** Import a class by file (or package specifier) and export name. */
3863
+ async importClass(sourceFile, className) {
3864
+ const found = (await (isAbsolute(sourceFile) ? import(pathToFileURL(sourceFile).href) : import(sourceFile)))[className];
3865
+ if (found === void 0) throw new TaskRunFailure(`${sourceFile} does not export a class named ${className}.`);
3866
+ return found;
3867
+ }
3868
+ /**
3869
+ * Shut the app down and remove the compile output. Safe to call twice.
3870
+ *
3871
+ * @returns `false` when the shutdown failed. The reason is already on stderr.
3872
+ */
3873
+ dispose() {
3874
+ this.disposing ??= this.disposeOnce();
3875
+ return this.disposing;
3876
+ }
3877
+ async disposeOnce() {
3878
+ let clean = true;
3879
+ try {
3880
+ await this.runtime?.StandingWork.settled({ timeoutMs: TaskRunner.drainTimeoutMilliseconds });
3881
+ await this.context?.drain({ timeoutMs: TaskRunner.drainTimeoutMilliseconds });
3882
+ await this.app?.shutdown();
3883
+ } catch (error) {
3884
+ clean = false;
3885
+ process.stderr.write(`The task finished, but shutting the app down failed — a client may still be open: ${error instanceof Error ? error.message : String(error)}\n`);
3886
+ }
3887
+ this.restoreStandingWork?.();
3888
+ this.restoreStandingWork = void 0;
3889
+ this.restoreHostBindings?.();
3890
+ this.restoreHostBindings = void 0;
3891
+ if (this.outputDirectory !== void 0) {
3892
+ rmSync(this.outputDirectory, {
3893
+ recursive: true,
3894
+ force: true
3895
+ });
3896
+ this.outputDirectory = void 0;
3897
+ }
3898
+ return clean;
3899
+ }
3900
+ /** An interrupt: shut down within a budget, remove the output, and leave with the signal's code. */
3901
+ async interrupted(signal, immediately) {
3902
+ process.stderr.write(`\nReceived ${signal}: shutting down and removing the compile output.\n`);
3903
+ if (!immediately) {
3904
+ const budget = new Promise((resolve) => {
3905
+ setTimeout(resolve, TaskRunner.interruptShutdownMilliseconds).unref();
3906
+ });
3907
+ await Promise.race([this.dispose(), budget]);
3908
+ }
3909
+ if (this.outputDirectory !== void 0) rmSync(this.outputDirectory, {
3910
+ recursive: true,
3911
+ force: true
3912
+ });
3913
+ process.exit(signal === "SIGINT" ? 130 : 143);
3914
+ }
3915
+ /** Print a failure on stderr. */
3916
+ static report(error) {
3917
+ if (error instanceof TaskRunFailure || error instanceof TaskTargetError) {
3918
+ process.stderr.write(`${error.message}\n`);
3919
+ return;
3920
+ }
3921
+ const text = error instanceof Error ? error.stack ?? error.message : String(error);
3922
+ process.stderr.write(`${text}\n`);
3923
+ }
3924
+ };
3925
+ //#endregion
3926
+ //#region src/task-command.ts
3927
+ /** Helpers of {@link taskCommand}: the argument checks, the environment file and the exit. */
3928
+ var TaskCommand = class TaskCommand {
3929
+ /**
3930
+ * The longest a finished run may keep the process alive. A task can leave a handle open (a socket, a
3931
+ * timer) that its app did not close, and a one-off run must end.
3932
+ */
3933
+ static exitGraceMilliseconds = 1e4;
3934
+ /**
3935
+ * Read `--payload`. It must be a JSON object, because it becomes the payload of the task's event.
3936
+ *
3937
+ * @param raw - The raw `--payload` text, when the caller passed one.
3938
+ */
3939
+ static parsePayload(raw) {
3940
+ if (raw === void 0) return {
3941
+ ok: true,
3942
+ payload: void 0
3943
+ };
3944
+ let parsed;
3945
+ try {
3946
+ parsed = JSON.parse(raw);
3947
+ } catch (error) {
3948
+ return {
3949
+ ok: false,
3950
+ reason: `--payload is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
3951
+ };
3952
+ }
3953
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {
3954
+ ok: false,
3955
+ reason: "--payload must be a JSON object, such as '{\"year\": 2026}'."
3956
+ };
3957
+ return {
3958
+ ok: true,
3959
+ payload: parsed
3960
+ };
3961
+ }
3962
+ /**
3963
+ * Put the variables of an env file into `process.env`. A variable that the shell already sets keeps the
3964
+ * shell's value.
3965
+ *
3966
+ * @param filePath - The absolute path of a dotenv-format file.
3967
+ * @throws {EnvFileUnreadableError} When the file cannot be read.
3968
+ */
3969
+ static async loadEnvironmentFile(filePath) {
3970
+ for (const [name, value] of await EnvFileReader.read(filePath)) if (process.env[name] === void 0) process.env[name] = value;
3971
+ }
3972
+ /**
3973
+ * Every `--src` of the raw arguments, as one comma-separated text. The argument parser keeps only the last
3974
+ * value of a repeated option, so the values are collected here, before it parses: `--src server --src extra`,
3975
+ * `--src=server`, and `--src server,extra` all count.
3976
+ *
3977
+ * @param rawArgs - The arguments after `task run`.
3978
+ * @returns The text, or `undefined` when the option is absent.
3979
+ */
3980
+ static sourceOption(rawArgs) {
3981
+ const values = [];
3982
+ for (let index = 0; index < rawArgs.length; index += 1) {
3983
+ const argument = rawArgs[index];
3984
+ if (argument === "--") break;
3985
+ if (argument === "--src") {
3986
+ const value = rawArgs[index + 1];
3987
+ if (value !== void 0 && !value.startsWith("--")) {
3988
+ values.push(value);
3989
+ index += 1;
3990
+ }
3991
+ } else if (argument.startsWith("--src=")) values.push(argument.slice(6));
3992
+ }
3993
+ return values.length === 0 ? void 0 : values.join(",");
3994
+ }
3995
+ /**
3996
+ * Set the exit code and make sure the process ends soon after, even when the task left a handle open.
3997
+ * The timer does not keep the process alive by itself.
3998
+ */
3999
+ static finish(exitCode) {
4000
+ process.exitCode = exitCode;
4001
+ setTimeout(() => process.exit(exitCode), TaskCommand.exitGraceMilliseconds).unref();
4002
+ }
4003
+ /** Print a usage failure and set the exit code. */
4004
+ static refuse(reason) {
4005
+ process.stderr.write(`heximon task run: ${reason}\n`);
4006
+ process.exitCode = 1;
4007
+ }
4008
+ };
4009
+ const taskRunCommand = defineCommand({
4010
+ meta: {
4011
+ name: "run",
4012
+ description: "Run one Task against the production graph of the app, then exit. Prints the result as JSON on stdout."
4013
+ },
4014
+ args: {
4015
+ module: {
4016
+ type: "positional",
4017
+ description: "The class name of the module that declares the task.",
4018
+ required: true
4019
+ },
4020
+ task: {
4021
+ type: "positional",
4022
+ description: "The class name of the task.",
4023
+ required: true
4024
+ },
4025
+ cwd: {
4026
+ type: "string",
4027
+ description: "Project root directory.",
4028
+ default: "."
4029
+ },
4030
+ payload: {
4031
+ type: "string",
4032
+ description: "A JSON object handed to the task as the payload of its event."
4033
+ },
4034
+ "env-file": {
4035
+ type: "string",
4036
+ description: "A dotenv-format file loaded before the compile. A variable the shell already sets keeps its value."
4037
+ },
4038
+ "with-outbox-relay": {
4039
+ type: "boolean",
4040
+ description: "Start the outbox relay. It delivers every pending row of the database, the rows of the deployed app included.",
4041
+ default: false
4042
+ },
4043
+ src: {
4044
+ type: "string",
4045
+ description: "Source directories to scan: repeat the option, or separate directories with commas. Defaults to `src/`, or the directories of the Nitro config for a Nitro host."
4046
+ }
4047
+ },
4048
+ async run({ args, rawArgs }) {
4049
+ if (!TransformBoot.isBootProcess()) {
4050
+ process.exitCode = await TransformBoot.reexecUnderTransform("task run");
4051
+ return;
4052
+ }
4053
+ const rootDir = resolve(process.cwd(), args.cwd);
4054
+ const parsed = TaskCommand.parsePayload(args.payload);
4055
+ if (!parsed.ok) {
4056
+ TaskCommand.refuse(parsed.reason);
4057
+ return;
4058
+ }
4059
+ const envFile = args["env-file"];
4060
+ if (envFile !== void 0) try {
4061
+ await TaskCommand.loadEnvironmentFile(resolve(process.cwd(), envFile));
4062
+ } catch (error) {
4063
+ TaskCommand.refuse(error instanceof Error ? error.message : String(error));
4064
+ return;
4065
+ }
4066
+ const sourceOption = TaskCommand.sourceOption(rawArgs);
4067
+ const sourceDirectories = SourceDirectories.resolve(rootDir, sourceOption);
4068
+ const exitCode = await new TaskRunner({
4069
+ rootDir,
4070
+ moduleName: args.module,
4071
+ taskName: args.task,
4072
+ withOutboxRelay: args["with-outbox-relay"],
4073
+ isSourceExplicit: sourceOption !== void 0 && sourceOption.trim() !== "",
4074
+ ...parsed.payload === void 0 ? {} : { payload: parsed.payload },
4075
+ ...sourceDirectories === void 0 ? {} : { sourceDirectories }
4076
+ }).run();
4077
+ TaskCommand.finish(exitCode);
4078
+ }
4079
+ });
4080
+ /** `heximon task` — run one-off work of the app. */
4081
+ const taskCommand = defineCommand({
4082
+ meta: {
4083
+ name: "task",
4084
+ description: "Run one-off work of the app."
4085
+ },
4086
+ subCommands: { run: taskRunCommand }
4087
+ });
4088
+ //#endregion
2458
4089
  //#region src/workflow-status-report.ts
2459
4090
  /** How one workflow's current step graph relates to the committed baseline gate. */
2460
4091
  const WorkflowBaselineState = {
@@ -2915,7 +4546,8 @@ const main = defineCommand({
2915
4546
  provision: provisionCommand,
2916
4547
  migrations: migrationsCommand,
2917
4548
  workflows: workflowsCommand,
2918
- skills: skillsCommand
4549
+ skills: skillsCommand,
4550
+ task: taskCommand
2919
4551
  }
2920
4552
  });
2921
4553
  await tab(main);