neon 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +70 -5
  2. package/dist/_chunks/auth_selection-DGgq6ifc.js +83 -0
  3. package/dist/_chunks/cmd_pipeline-CUbBO9U_.js +2818 -0
  4. package/dist/_chunks/credentials-MYdHdKah.js +188 -0
  5. package/dist/_chunks/env-NbA61JR3.js +585 -0
  6. package/dist/_chunks/env_services-Tz9G4JeT.js +531 -0
  7. package/dist/_chunks/paths-DMq0Lt7a.js +151 -0
  8. package/dist/_chunks/profiles-Ir29rqns.js +217 -0
  9. package/dist/_chunks/psql-DWH-kc69.js +2169 -0
  10. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  11. package/dist/_chunks/secure_file-BucZj4yQ.js +39 -0
  12. package/dist/analytics.js +163 -207
  13. package/dist/api.js +815 -758
  14. package/dist/auth.js +121 -141
  15. package/dist/auth_context.js +39 -53
  16. package/dist/cli.js +4 -7
  17. package/dist/commands/api.js +220 -250
  18. package/dist/commands/api_keys.js +251 -314
  19. package/dist/commands/auth.js +283 -328
  20. package/dist/commands/bootstrap.js +372 -437
  21. package/dist/commands/branches.js +304 -455
  22. package/dist/commands/bucket.js +374 -514
  23. package/dist/commands/checkout.js +213 -298
  24. package/dist/commands/config.js +573 -690
  25. package/dist/commands/connection_string.js +137 -165
  26. package/dist/commands/data_api.js +238 -260
  27. package/dist/commands/databases.js +67 -76
  28. package/dist/commands/deploy.js +31 -25
  29. package/dist/commands/dev.js +639 -719
  30. package/dist/commands/diff.js +156 -200
  31. package/dist/commands/env.js +255 -305
  32. package/dist/commands/functions.js +275 -355
  33. package/dist/commands/index.js +70 -65
  34. package/dist/commands/init.js +84 -119
  35. package/dist/commands/inspect.js +55 -55
  36. package/dist/commands/ip_allow.js +88 -120
  37. package/dist/commands/link.js +874 -1019
  38. package/dist/commands/logs.js +291 -0
  39. package/dist/commands/neon_auth.js +725 -933
  40. package/dist/commands/operations.js +34 -25
  41. package/dist/commands/orgs.js +28 -18
  42. package/dist/commands/profile.js +615 -846
  43. package/dist/commands/projects.js +313 -373
  44. package/dist/commands/psql.js +60 -58
  45. package/dist/commands/roles.js +55 -58
  46. package/dist/commands/schema_diff.js +87 -131
  47. package/dist/commands/set_context.js +34 -26
  48. package/dist/commands/snapshots.js +288 -413
  49. package/dist/commands/status.js +41 -37
  50. package/dist/commands/user.js +21 -10
  51. package/dist/commands/vpc_endpoints.js +85 -113
  52. package/dist/config.js +7 -6
  53. package/dist/config_format.js +50 -66
  54. package/dist/config_template.js +128 -157
  55. package/dist/context.js +183 -235
  56. package/dist/current_branch_fast_path.js +40 -49
  57. package/dist/dev/env.js +2 -446
  58. package/dist/dev/functions.js +54 -68
  59. package/dist/dev/inputs.js +46 -58
  60. package/dist/dev/runtime.js +135 -164
  61. package/dist/dev/websocket.js +766 -959
  62. package/dist/env.js +27 -33
  63. package/dist/env_file.js +118 -132
  64. package/dist/env_services.js +2 -51
  65. package/dist/errors.js +57 -68
  66. package/dist/functions_api.js +45 -43
  67. package/dist/help.js +189 -140
  68. package/dist/index.js +182 -257
  69. package/dist/init/agents.js +137 -118
  70. package/dist/init/auth.js +58 -68
  71. package/dist/init/bootstrap.js +325 -396
  72. package/dist/init/build_config.js +4 -2
  73. package/dist/init/detect_agent.js +56 -101
  74. package/dist/init/editors.js +35 -52
  75. package/dist/init/enrich_output.js +51 -66
  76. package/dist/init/extension.js +134 -171
  77. package/dist/init/inspect.js +179 -266
  78. package/dist/init/interactive.js +510 -622
  79. package/dist/init/neonctl.js +117 -168
  80. package/dist/init/orchestrate.js +157 -173
  81. package/dist/init/phases/auth.js +188 -202
  82. package/dist/init/phases/cleanup.js +23 -23
  83. package/dist/init/phases/db.js +251 -277
  84. package/dist/init/phases/getting_started.js +213 -223
  85. package/dist/init/phases/mcp.js +174 -224
  86. package/dist/init/phases/migrations.js +247 -248
  87. package/dist/init/phases/neon_auth.js +114 -133
  88. package/dist/init/phases/setup.js +546 -703
  89. package/dist/init/phases/skills.js +75 -86
  90. package/dist/init/phases/status.js +72 -67
  91. package/dist/init/resolve_context.js +102 -99
  92. package/dist/init/route_command.js +91 -98
  93. package/dist/init/skills.js +174 -218
  94. package/dist/init/vsix.js +77 -99
  95. package/dist/log.js +17 -16
  96. package/dist/neon_services.js +104 -129
  97. package/dist/parameters.gen.js +481 -471
  98. package/dist/pkg.js +17 -19
  99. package/dist/profile_keys.js +44 -47
  100. package/dist/psql/cli.js +44 -47
  101. package/dist/psql/command/cmd_cond.js +231 -406
  102. package/dist/psql/command/cmd_connect.js +557 -764
  103. package/dist/psql/command/cmd_copy.js +728 -984
  104. package/dist/psql/command/cmd_describe.js +1499 -1688
  105. package/dist/psql/command/cmd_format.js +733 -905
  106. package/dist/psql/command/cmd_io.js +2 -2193
  107. package/dist/psql/command/cmd_lo.js +297 -359
  108. package/dist/psql/command/cmd_meta.js +727 -878
  109. package/dist/psql/command/cmd_misc.js +138 -172
  110. package/dist/psql/command/cmd_pipeline.js +2 -1148
  111. package/dist/psql/command/cmd_restrict.js +119 -155
  112. package/dist/psql/command/cmd_show.js +529 -688
  113. package/dist/psql/command/dispatch.js +260 -325
  114. package/dist/psql/command/inputQueue.js +35 -33
  115. package/dist/psql/command/shared.js +49 -63
  116. package/dist/psql/complete/filenames.js +90 -133
  117. package/dist/psql/complete/index.js +59 -97
  118. package/dist/psql/complete/matcher.js +236 -300
  119. package/dist/psql/complete/psqlVars.js +218 -223
  120. package/dist/psql/complete/queries.js +159 -177
  121. package/dist/psql/complete/rules.js +1493 -2299
  122. package/dist/psql/core/common.js +2 -1253
  123. package/dist/psql/core/help.js +456 -546
  124. package/dist/psql/core/mainloop.js +692 -1303
  125. package/dist/psql/core/prompt.js +391 -408
  126. package/dist/psql/core/settings.js +429 -644
  127. package/dist/psql/core/sqlHelp.js +480 -554
  128. package/dist/psql/core/startup.js +2 -846
  129. package/dist/psql/core/syncVars.js +67 -110
  130. package/dist/psql/core/variables.js +156 -278
  131. package/dist/psql/describe/formatters.js +884 -1285
  132. package/dist/psql/describe/processNamePattern.js +173 -260
  133. package/dist/psql/describe/queries.js +1368 -2403
  134. package/dist/psql/describe/versionGate.js +32 -41
  135. package/dist/psql/index.js +2 -2030
  136. package/dist/psql/io/history.js +232 -271
  137. package/dist/psql/io/input.js +103 -108
  138. package/dist/psql/io/lineEditor/buffer.js +238 -319
  139. package/dist/psql/io/lineEditor/complete.js +135 -213
  140. package/dist/psql/io/lineEditor/filename.js +139 -148
  141. package/dist/psql/io/lineEditor/index.js +653 -870
  142. package/dist/psql/io/lineEditor/keymap.js +544 -702
  143. package/dist/psql/io/lineEditor/vt100.js +294 -341
  144. package/dist/psql/io/pgpass.js +158 -187
  145. package/dist/psql/io/pgservice.js +146 -183
  146. package/dist/psql/io/psqlrc.js +328 -403
  147. package/dist/psql/print/aligned.js +1020 -1683
  148. package/dist/psql/print/asciidoc.js +180 -214
  149. package/dist/psql/print/crosstab.js +281 -442
  150. package/dist/psql/print/csv.js +48 -70
  151. package/dist/psql/print/html.js +195 -226
  152. package/dist/psql/print/json.js +75 -88
  153. package/dist/psql/print/latex.js +291 -364
  154. package/dist/psql/print/pager.js +171 -242
  155. package/dist/psql/print/troff.js +194 -226
  156. package/dist/psql/print/unaligned.js +69 -95
  157. package/dist/psql/print/units.js +167 -169
  158. package/dist/psql/scanner/slash.js +428 -483
  159. package/dist/psql/scanner/sql.js +445 -889
  160. package/dist/psql/scanner/stringutils.js +309 -379
  161. package/dist/psql/types/index.js +8 -7
  162. package/dist/psql/types/scanner.js +25 -22
  163. package/dist/psql/wire/connection.js +2042 -2803
  164. package/dist/psql/wire/copy.js +84 -100
  165. package/dist/psql/wire/notify.js +39 -59
  166. package/dist/psql/wire/pipeline.js +305 -518
  167. package/dist/psql/wire/protocol.js +349 -417
  168. package/dist/psql/wire/sasl.js +180 -265
  169. package/dist/psql/wire/tls.js +400 -561
  170. package/dist/storage_api.js +115 -129
  171. package/dist/test_utils/fixtures.js +94 -113
  172. package/dist/test_utils/oauth_server.js +10 -7
  173. package/dist/test_utils/project_dir.js +33 -0
  174. package/dist/utils/ai_gateway_notice.js +131 -162
  175. package/dist/utils/api_enums.js +21 -28
  176. package/dist/utils/auth.js +10 -4
  177. package/dist/utils/branch_notice.js +20 -19
  178. package/dist/utils/branch_picker.js +83 -89
  179. package/dist/utils/cli_name.js +15 -12
  180. package/dist/utils/compute_units.js +20 -27
  181. package/dist/utils/config_diff.js +127 -158
  182. package/dist/utils/enrichers.js +95 -148
  183. package/dist/utils/esbuild.js +130 -189
  184. package/dist/utils/flags.js +35 -47
  185. package/dist/utils/formats.js +8 -15
  186. package/dist/utils/git_diff.js +69 -80
  187. package/dist/utils/inspect_db.js +101 -143
  188. package/dist/utils/inspect_queries.js +179 -142
  189. package/dist/utils/middlewares.js +39 -45
  190. package/dist/utils/openapi.js +87 -99
  191. package/dist/utils/package_manager.js +312 -110
  192. package/dist/utils/point_in_time.js +49 -53
  193. package/dist/utils/psql.js +89 -106
  194. package/dist/utils/service_picker.js +55 -58
  195. package/dist/utils/string.js +5 -5
  196. package/dist/utils/ui.js +38 -55
  197. package/dist/utils/write_sync.js +26 -35
  198. package/dist/utils/zip.js +4 -3
  199. package/dist/writer.js +67 -87
  200. package/package.json +11 -6
  201. package/dist/_shared/auth_selection.js +0 -86
  202. package/dist/_shared/credentials.js +0 -209
  203. package/dist/_shared/env-core/env.js +0 -558
  204. package/dist/_shared/env-core/reuse-secrets.js +0 -223
  205. package/dist/_shared/paths.js +0 -148
  206. package/dist/_shared/profiles.js +0 -276
  207. package/dist/_shared/secure_file.js +0 -43
@@ -1,798 +1,718 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import { once } from "node:events";
3
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, join, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { describeNativeFinding, findUndeclaredNativePackages, } from "@neon/config-runtime";
7
- import chalk from "chalk";
8
- import { resolveDevEnv } from "../dev/env.js";
9
- import { resolveFunctionsFromConfig, } from "../dev/functions.js";
10
- import { resolveWatchInputs } from "../dev/inputs.js";
11
- import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
1
+ import { t as __exportAll } from "../_chunks/rolldown-runtime-D7D4PA-g.js";
12
2
  import { log } from "../log.js";
13
3
  import { getCliName } from "../utils/cli_name.js";
4
+ import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
14
5
  import { branchIdResolve } from "../utils/enrichers.js";
15
6
  import { bundleEntry } from "../utils/esbuild.js";
16
- export const command = "dev";
17
- export const describe = "Run Neon Functions locally with a dev server";
18
- export const builder = (argv) => argv
19
- .usage("$0 dev [--source <path>] [options]")
20
- .example("$0 dev --source ./functions/hello.ts", "Serve one function on a free port with hot reload")
21
- .example("$0 dev", "Serve every function declared in neon.ts (one dev server each)")
22
- .example("$0 dev --source ./functions/hello.ts --port 3000", "Serve one function on an explicit port (fails if the port is taken)")
23
- .options({
24
- source: {
25
- describe: "Path to a single function entry module. Omit to serve every " +
26
- "function declared in neon.ts.",
27
- type: "string",
28
- },
29
- port: {
30
- describe: "Port to listen on (single-function mode only, with --source). " +
31
- "Fails if taken. Without it (and without a PORT env var) a free " +
32
- "port is chosen automatically.",
33
- type: "number",
34
- },
35
- })
36
- .epilogue([
37
- "",
38
- "Functions run with the linked branch's Neon env injected, the same set the",
39
- "deployed runtime gives them: DATABASE_URL, plus Neon Auth, the Data API,",
40
- "object storage and the AI Gateway where the branch has them. A neon.ts in",
41
- "this directory decides instead, exactly as it does for `env pull`.",
42
- "",
43
- "`dev` reads your .env / .env.local to reuse the branch credential behind the",
44
- "AI Gateway and object storage, and never writes to them. With no such file it",
45
- "issues a credential on every start, so run `env pull` once if you restart often.",
46
- ].join("\n"))
47
- .strict();
7
+ import { i as resolveDevEnv } from "../_chunks/env-NbA61JR3.js";
8
+ import { resolveFunctionsFromConfig } from "../dev/functions.js";
9
+ import { resolveWatchInputs } from "../dev/inputs.js";
10
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
+ import { basename, dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import chalk from "chalk";
14
+ import { spawn, spawnSync } from "node:child_process";
15
+ import { describeNativeFinding, findUndeclaredNativePackages } from "@neon/config-runtime";
16
+ import { once } from "node:events";
17
+ //#region src/commands/dev.ts
18
+ var dev_exports = /* @__PURE__ */ __exportAll({
19
+ builder: () => builder,
20
+ command: () => "dev",
21
+ describe: () => describe,
22
+ devBundleDir: () => devBundleDir,
23
+ devEnvContext: () => devEnvContext,
24
+ diffUnits: () => diffUnits,
25
+ formatEnvSummary: () => formatEnvSummary,
26
+ handler: () => handler,
27
+ reportDevCredential: () => reportDevCredential
28
+ });
29
+ const command = "dev";
30
+ const describe = "Run Neon Functions locally with a dev server";
31
+ const builder = (argv) => argv.usage("$0 dev [--source <path>] [options]").example("$0 dev --source ./functions/hello.ts", "Serve one function on a free port with hot reload").example("$0 dev", "Serve every function declared in neon.ts (one dev server each)").example("$0 dev --source ./functions/hello.ts --port 3000", "Serve one function on an explicit port (fails if the port is taken)").options({
32
+ source: {
33
+ describe: "Path to a single function entry module. Omit to serve every function declared in neon.ts.",
34
+ type: "string"
35
+ },
36
+ port: {
37
+ describe: "Port to listen on (single-function mode only, with --source). Fails if taken. Without it (and without a PORT env var) a free port is chosen automatically.",
38
+ type: "number"
39
+ }
40
+ }).epilogue([
41
+ "",
42
+ "Functions run with the linked branch's Neon env injected, the same set the",
43
+ "deployed runtime gives them: DATABASE_URL, plus Neon Auth, the Data API,",
44
+ "object storage and the AI Gateway where the branch has them. A neon.ts in",
45
+ "this directory decides instead, exactly as it does for `env pull`.",
46
+ "",
47
+ "`dev` reads your .env / .env.local to reuse the branch credential behind the",
48
+ "AI Gateway and object storage, and never writes to them. With no such file it",
49
+ "issues a credential on every start, so run `env pull` once if you restart often."
50
+ ].join("\n")).strict();
48
51
  /**
49
- * The resolver context for a `neon dev` run.
50
- *
51
- * Two things here are what make local dev match the deployed runtime, which injects a
52
- * branch's whole env into a function:
53
- *
54
- * - **The AI Gateway is asked for**, like `env pull` does, because nothing can detect it.
55
- * Without this, a function that works deployed fails locally with no `NEON_AI_GATEWAY_*`,
56
- * which is exactly the difference `dev` exists to eliminate.
57
- * - **The local dotenv file is layered in**, so the branch credential behind the gateway and
58
- * object storage is *reused* rather than re-minted. `dev` writes no file of its own, so
59
- * without a source of persisted secrets every start would mint a credential and leave the
60
- * last one live — one orphan per restart. `neon-env run` already reads the file for this
61
- * reason; `dev` was the one that didn't.
62
- */
63
- export const devEnvContext = (props, branchId, cwd) => {
64
- const envFile = resolveEnvFilePath(cwd);
65
- return {
66
- cwd,
67
- implyAiGateway: true,
68
- env: {
69
- ...process.env,
70
- ...(existsSync(envFile) ? readEnvFile(envFile) : {}),
71
- },
72
- ...(props.projectId ? { projectId: props.projectId } : {}),
73
- ...(branchId ? { branchId } : {}),
74
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
75
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
76
- };
52
+ * The resolver context for a `neon dev` run.
53
+ *
54
+ * Two things here are what make local dev match the deployed runtime, which injects a
55
+ * branch's whole env into a function:
56
+ *
57
+ * - **The AI Gateway is asked for**, like `env pull` does, because nothing can detect it.
58
+ * Without this, a function that works deployed fails locally with no `NEON_AI_GATEWAY_*`,
59
+ * which is exactly the difference `dev` exists to eliminate.
60
+ * - **The local dotenv file is layered in**, so the branch credential behind the gateway and
61
+ * object storage is *reused* rather than re-minted. `dev` writes no file of its own, so
62
+ * without a source of persisted secrets every start would mint a credential and leave the
63
+ * last one live — one orphan per restart. `neon-env run` already reads the file for this
64
+ * reason; `dev` was the one that didn't.
65
+ */
66
+ const devEnvContext = (props, branchId, cwd) => {
67
+ const envFile = resolveEnvFilePath(cwd);
68
+ return {
69
+ cwd,
70
+ implyAiGateway: true,
71
+ env: {
72
+ ...process.env,
73
+ ...existsSync(envFile) ? readEnvFile(envFile) : {}
74
+ },
75
+ ...props.projectId ? { projectId: props.projectId } : {},
76
+ ...branchId ? { branchId } : {},
77
+ ...props.apiKey ? { apiKey: props.apiKey } : {},
78
+ ...props.apiHost ? { apiHost: props.apiHost } : {}
79
+ };
77
80
  };
78
81
  /**
79
- * Say when a run issued a branch credential.
80
- *
81
- * `dev` has nowhere to persist one — it writes no file — so on a branch with nothing to reuse
82
- * it mints per start and cannot name the previous one to revoke it. Every other command that
83
- * mints says so; this is the one that runs dozens of times a day, and the server banner listing
84
- * `NEON_AI_GATEWAY_TOKEN` reads as "fetched", not "just created, and the last one is still
85
- * live". The note names the one action that stops it, so it disappears once followed.
86
- */
87
- export const reportDevCredential = (credential) => {
88
- if (!credential?.issued)
89
- return;
90
- if (credential.revoked.length > 0) {
91
- log.info("Issued a new branch credential — %s changed. Revoked the one it replaced (%s).", credential.keys.join(", "), credential.revoked.join(", "));
92
- return;
93
- }
94
- log.warning("Issued a branch credential for this run (%s) and left any previous one live — a dev " +
95
- `server has nowhere to keep it. Run \`${getCliName()} env pull\` once to write it to ` +
96
- "your .env, and restarts will reuse it instead of issuing another.", credential.keys.join(", "));
82
+ * Say when a run issued a branch credential.
83
+ *
84
+ * `dev` has nowhere to persist one — it writes no file — so on a branch with nothing to reuse
85
+ * it mints per start and cannot name the previous one to revoke it. Every other command that
86
+ * mints says so; this is the one that runs dozens of times a day, and the server banner listing
87
+ * `NEON_AI_GATEWAY_TOKEN` reads as "fetched", not "just created, and the last one is still
88
+ * live". The note names the one action that stops it, so it disappears once followed.
89
+ */
90
+ const reportDevCredential = (credential) => {
91
+ if (!credential?.issued) return;
92
+ if (credential.revoked.length > 0) {
93
+ log.info("Issued a new branch credential — %s changed. Revoked the one it replaced (%s).", credential.keys.join(", "), credential.revoked.join(", "));
94
+ return;
95
+ }
96
+ log.warning(`Issued a branch credential for this run (%s) and left any previous one live — a dev server has nowhere to keep it. Run \`${getCliName()} env pull\` once to write it to your .env, and restarts will reuse it instead of issuing another.`, credential.keys.join(", "));
97
97
  };
98
- export const handler = async (props) => {
99
- if (props.source !== undefined) {
100
- await runSingleSource(props);
101
- return;
102
- }
103
- // No --source: --port has no single target to bind, so reject it explicitly
104
- // rather than silently ignoring it.
105
- if (props.port !== undefined) {
106
- throw new Error("--port can only be used with --source. To set ports for the functions " +
107
- "in neon.ts, give each one a `dev.port` in its config.");
108
- }
109
- await runFromConfig(props);
98
+ const handler = async (props) => {
99
+ if (props.source !== void 0) {
100
+ await runSingleSource(props);
101
+ return;
102
+ }
103
+ if (props.port !== void 0) throw new Error("--port can only be used with --source. To set ports for the functions in neon.ts, give each one a `dev.port` in its config.");
104
+ await runFromConfig(props);
110
105
  };
111
106
  /** Single-function mode: serve exactly the `--source` path (legacy behavior). */
112
107
  const runSingleSource = async (props) => {
113
- const source = resolve(process.cwd(), props.source);
114
- if (!existsSync(source)) {
115
- throw new Error(`Source file not found: ${source}`);
116
- }
117
- const branchId = await resolveBranchId(props);
118
- const { vars: neonEnv, skipped, credential, } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
119
- reportDevCredential(credential);
120
- const unit = {
121
- slug: null,
122
- source,
123
- bundleDir: devBundleDir(process.cwd()),
124
- childEnv: buildChildEnv(neonEnv, portFromProps(props.port)),
125
- label: null,
126
- envSummary: { neon: Object.keys(neonEnv), fn: [] },
127
- };
128
- // No config reload in single-source mode: there's exactly one file to serve, and
129
- // nothing to add or remove. neon.ts hot-reload is config-mode only.
130
- await runSupervisor([unit], {
131
- ...(skipped ? { envNote: skipped.reason } : {}),
132
- });
108
+ const source = resolve(process.cwd(), props.source);
109
+ if (!existsSync(source)) throw new Error(`Source file not found: ${source}`);
110
+ const branchId = await resolveBranchId(props);
111
+ const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
112
+ reportDevCredential(credential);
113
+ const unit = {
114
+ slug: null,
115
+ source,
116
+ bundleDir: devBundleDir(process.cwd()),
117
+ childEnv: buildChildEnv(neonEnv, portFromProps(props.port)),
118
+ label: null,
119
+ envSummary: {
120
+ neon: Object.keys(neonEnv),
121
+ fn: []
122
+ }
123
+ };
124
+ await runSupervisor([unit], { ...skipped ? { envNote: skipped.reason } : {} });
133
125
  };
134
126
  /**
135
- * Multi-function mode: serve every function declared in neon.ts. Requires a neon.ts
136
- * (there is no single source to fall back on), one dev server per function.
137
- */
127
+ * Multi-function mode: serve every function declared in neon.ts. Requires a neon.ts
128
+ * (there is no single source to fall back on), one dev server per function.
129
+ */
138
130
  const runFromConfig = async (props) => {
139
- const branchId = await resolveBranchId(props);
140
- const resolved = await resolveFunctionsFromConfig(process.cwd());
141
- if (resolved === null) {
142
- throw new Error("No --source given and no neon.ts found. Pass --source <path> to run a " +
143
- "single function, or add a neon.ts that declares functions under " +
144
- "`preview.functions`.");
145
- }
146
- const { configPath, functions } = resolved;
147
- if (functions.length === 0) {
148
- throw new Error("neon.ts has no functions to serve. Add at least one under " +
149
- "`preview.functions`, or pass --source <path>.");
150
- }
151
- const { vars: neonEnv, skipped, credential, } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
152
- reportDevCredential(credential);
153
- const units = planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
154
- // Re-derive the units from neon.ts on demand so the config watcher can hot-add/remove
155
- // functions without restarting the dev server. `searchBase` lets a freshly-added unit
156
- // start probing above the ports already taken by live units (the runtime still walks
157
- // upward from there, so this never fails — it just keeps startup deterministic).
158
- const replan = async (searchBase) => {
159
- const re = await resolveFunctionsFromConfig(process.cwd());
160
- if (re === null)
161
- return null;
162
- return planFunctionsToUnits(re.functions, neonEnv, searchBase);
163
- };
164
- await runSupervisor(units, {
165
- reload: { configPath, replan },
166
- ...(skipped ? { envNote: skipped.reason } : {}),
167
- });
131
+ const branchId = await resolveBranchId(props);
132
+ const resolved = await resolveFunctionsFromConfig(process.cwd());
133
+ if (resolved === null) throw new Error("No --source given and no neon.ts found. Pass --source <path> to run a single function, or add a neon.ts that declares functions under `preview.functions`.");
134
+ const { configPath, functions } = resolved;
135
+ if (functions.length === 0) throw new Error("neon.ts has no functions to serve. Add at least one under `preview.functions`, or pass --source <path>.");
136
+ const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
137
+ reportDevCredential(credential);
138
+ const units = planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
139
+ const replan = async (searchBase) => {
140
+ const re = await resolveFunctionsFromConfig(process.cwd());
141
+ if (re === null) return null;
142
+ return planFunctionsToUnits(re.functions, neonEnv, searchBase);
143
+ };
144
+ await runSupervisor(units, {
145
+ reload: {
146
+ configPath,
147
+ replan
148
+ },
149
+ ...skipped ? { envNote: skipped.reason } : {}
150
+ });
168
151
  };
169
152
  /**
170
- * Map a list of {@link PlannedFunction}s to {@link ServedUnit}s, coordinating the search
171
- * base across them so search-mode functions don't all probe the same starting port.
172
- *
173
- * Each search-mode (no `dev.port`) function gets a distinct base starting at
174
- * `searchBase`; the runtime still walks upward from its base, so an occupied base
175
- * self-resolves and this never fails — the offset just makes startup deterministic.
176
- */
153
+ * Map a list of {@link PlannedFunction}s to {@link ServedUnit}s, coordinating the search
154
+ * base across them so search-mode functions don't all probe the same starting port.
155
+ *
156
+ * Each search-mode (no `dev.port`) function gets a distinct base starting at
157
+ * `searchBase`; the runtime still walks upward from its base, so an occupied base
158
+ * self-resolves and this never fails — the offset just makes startup deterministic.
159
+ */
177
160
  const planFunctionsToUnits = (functions, neonEnv, searchBase) => {
178
- let searchOffset = 0;
179
- return functions.map((fn) => {
180
- const base = searchBase + searchOffset;
181
- if (fn.port === undefined)
182
- searchOffset += 1;
183
- return plannedToUnit(fn, neonEnv, base);
184
- });
161
+ let searchOffset = 0;
162
+ return functions.map((fn) => {
163
+ const base = searchBase + searchOffset;
164
+ if (fn.port === void 0) searchOffset += 1;
165
+ return plannedToUnit(fn, neonEnv, base);
166
+ });
185
167
  };
186
168
  /**
187
- * Resolve the selected branch id from props, if any. Best-effort: a failure here only
188
- * means env injection is skipped, so it never throws.
189
- */
169
+ * Resolve the selected branch id from props, if any. Best-effort: a failure here only
170
+ * means env injection is skipped, so it never throws.
171
+ */
190
172
  const resolveBranchId = async (props) => {
191
- if (!props.apiClient || !props.projectId)
192
- return undefined;
193
- const branch = props.branch ?? props.id;
194
- if (!branch)
195
- return undefined;
196
- try {
197
- return await branchIdResolve({
198
- branch,
199
- apiClient: props.apiClient,
200
- projectId: props.projectId,
201
- });
202
- }
203
- catch (err) {
204
- log.debug("dev: could not resolve branch id: %s", err instanceof Error ? err.message : String(err));
205
- return undefined;
206
- }
173
+ if (!props.apiClient || !props.projectId) return void 0;
174
+ const branch = props.branch ?? props.id;
175
+ if (!branch) return void 0;
176
+ try {
177
+ return await branchIdResolve({
178
+ branch,
179
+ apiClient: props.apiClient,
180
+ projectId: props.projectId
181
+ });
182
+ } catch (err) {
183
+ log.debug("dev: could not resolve branch id: %s", err instanceof Error ? err.message : String(err));
184
+ return;
185
+ }
207
186
  };
208
187
  const DEFAULT_PORT_BASE = 8787;
209
188
  const portFromProps = (port) => {
210
- if (port !== undefined)
211
- return { mode: "explicit", port };
212
- if (process.env.PORT !== undefined && process.env.PORT !== "") {
213
- return { mode: "explicit", port: Number(process.env.PORT) };
214
- }
215
- return { mode: "search", from: DEFAULT_PORT_BASE };
189
+ if (port !== void 0) return {
190
+ mode: "explicit",
191
+ port
192
+ };
193
+ if (process.env.PORT !== void 0 && process.env.PORT !== "") return {
194
+ mode: "explicit",
195
+ port: Number(process.env.PORT)
196
+ };
197
+ return {
198
+ mode: "search",
199
+ from: DEFAULT_PORT_BASE
200
+ };
216
201
  };
217
202
  /**
218
- * Where a locally-served function's bundle is written.
219
- *
220
- * The location inside the project's own `node_modules` is load-bearing, not a tidiness
221
- * choice. A function's `externalPackages` are left unbundled, and Node resolves an unbundled
222
- * import by walking up from the importing file — so from here it reaches the project's real
223
- * `node_modules` and finds them, at the host architecture that can actually run locally.
224
- * Moving this directory anywhere outside `node_modules` breaks `neon dev` for those
225
- * functions with a `Cannot find module`, so the path is pinned by a test.
226
- */
227
- export const devBundleDir = (cwd, slug) => slug === undefined
228
- ? join(cwd, "node_modules", ".neon-dev")
229
- : join(cwd, "node_modules", ".neon-dev", slug);
203
+ * Where a locally-served function's bundle is written.
204
+ *
205
+ * The location inside the project's own `node_modules` is load-bearing, not a tidiness
206
+ * choice. A function's `externalPackages` are left unbundled, and Node resolves an unbundled
207
+ * import by walking up from the importing file — so from here it reaches the project's real
208
+ * `node_modules` and finds them, at the host architecture that can actually run locally.
209
+ * Moving this directory anywhere outside `node_modules` breaks `neon dev` for those
210
+ * functions with a `Cannot find module`, so the path is pinned by a test.
211
+ */
212
+ const devBundleDir = (cwd, slug) => slug === void 0 ? join(cwd, "node_modules", ".neon-dev") : join(cwd, "node_modules", ".neon-dev", slug);
230
213
  /**
231
- * Translate a {@link PlannedFunction} into a {@link ServedUnit}. Port rules:
232
- * - explicit `dev.port`: bind exactly, fail if taken.
233
- * - no `dev.port`: search for a free port (base coordinated by the caller).
234
- * Per-function neon.ts env layers over the shared branch env.
235
- */
214
+ * Translate a {@link PlannedFunction} into a {@link ServedUnit}. Port rules:
215
+ * - explicit `dev.port`: bind exactly, fail if taken.
216
+ * - no `dev.port`: search for a free port (base coordinated by the caller).
217
+ * Per-function neon.ts env layers over the shared branch env.
218
+ */
236
219
  const plannedToUnit = (fn, branchEnv, searchBase) => {
237
- const port = fn.port !== undefined
238
- ? { mode: "explicit", port: fn.port }
239
- : { mode: "search", from: searchBase };
240
- const childEnv = buildChildEnv({ ...branchEnv, ...fn.env }, port);
241
- return {
242
- slug: fn.slug,
243
- source: fn.source,
244
- bundleDir: devBundleDir(process.cwd(), fn.slug),
245
- childEnv,
246
- label: fn.slug,
247
- envSummary: { neon: Object.keys(branchEnv), fn: Object.keys(fn.env) },
248
- // Signature of the function's *own* neon.ts config (NOT the dynamically-chosen search
249
- // base) so reconcile can tell a real change from a no-op save. A search-mode function
250
- // re-planned with a different base must hash identically, or it would be needlessly
251
- // restarted — see reconcile().
252
- ...(fn.externalPackages
253
- ? { externalPackages: fn.externalPackages }
254
- : {}),
255
- configKey: JSON.stringify({
256
- source: fn.source,
257
- port: fn.port ?? null,
258
- env: fn.env,
259
- externalPackages: fn.externalPackages ?? null,
260
- }),
261
- };
220
+ const port = fn.port !== void 0 ? {
221
+ mode: "explicit",
222
+ port: fn.port
223
+ } : {
224
+ mode: "search",
225
+ from: searchBase
226
+ };
227
+ const childEnv = buildChildEnv({
228
+ ...branchEnv,
229
+ ...fn.env
230
+ }, port);
231
+ return {
232
+ slug: fn.slug,
233
+ source: fn.source,
234
+ bundleDir: devBundleDir(process.cwd(), fn.slug),
235
+ childEnv,
236
+ label: fn.slug,
237
+ envSummary: {
238
+ neon: Object.keys(branchEnv),
239
+ fn: Object.keys(fn.env)
240
+ },
241
+ ...fn.externalPackages ? { externalPackages: fn.externalPackages } : {},
242
+ configKey: JSON.stringify({
243
+ source: fn.source,
244
+ port: fn.port ?? null,
245
+ env: fn.env,
246
+ externalPackages: fn.externalPackages ?? null
247
+ })
248
+ };
262
249
  };
263
250
  /**
264
- * Build a child's env. Neon vars layer over the inherited environment (so the branch's
265
- * DATABASE_URL wins over a stale inherited value); a function that loads its own `.env`
266
- * at runtime still overrides them. The port spec is encoded for the runtime via
267
- * NEON_DEV_PORT (explicit) or NEON_DEV_PORT_BASE (search).
268
- */
251
+ * Build a child's env. Neon vars layer over the inherited environment (so the branch's
252
+ * DATABASE_URL wins over a stale inherited value); a function that loads its own `.env`
253
+ * at runtime still overrides them. The port spec is encoded for the runtime via
254
+ * NEON_DEV_PORT (explicit) or NEON_DEV_PORT_BASE (search).
255
+ */
269
256
  const buildChildEnv = (neonEnv, port) => {
270
- const env = { ...process.env, ...neonEnv };
271
- delete env.NEON_DEV_PORT;
272
- delete env.NEON_DEV_PORT_BASE;
273
- if (port.mode === "explicit") {
274
- env.NEON_DEV_PORT = String(port.port);
275
- }
276
- else if (port.mode === "search") {
277
- env.NEON_DEV_PORT_BASE = String(port.from);
278
- }
279
- return env;
257
+ const env = {
258
+ ...process.env,
259
+ ...neonEnv
260
+ };
261
+ delete env.NEON_DEV_PORT;
262
+ delete env.NEON_DEV_PORT_BASE;
263
+ if (port.mode === "explicit") env.NEON_DEV_PORT = String(port.port);
264
+ else if (port.mode === "search") env.NEON_DEV_PORT_BASE = String(port.from);
265
+ return env;
280
266
  };
281
267
  const READY_PATTERN = /neon-dev:ready (\d+)/;
282
268
  /**
283
- * Supervise one or more {@link ServedUnit}s: bundle + start each in its own child, watch
284
- * its inputs for hot reload, and tear everything down cleanly on shutdown. Units are
285
- * independent — one crashing or failing to start does not stop the others (it is shown
286
- * as errored and recovered on the next edit). A single SIGINT/SIGTERM shuts all of them
287
- * down, tree-killing each child so no descendant it spawned is orphaned.
288
- *
289
- * In config mode, `reload` lets the supervisor watch `neon.ts` and reconcile the live set
290
- * of units when it changes: a newly-declared function is hot-added (its own child, watcher,
291
- * and port) and a removed one is torn down — all without disturbing the functions that
292
- * stayed the same. A function whose config (env/port/source) changed is restarted
293
- * in place; siblings are untouched.
294
- */
269
+ * Supervise one or more {@link ServedUnit}s: bundle + start each in its own child, watch
270
+ * its inputs for hot reload, and tear everything down cleanly on shutdown. Units are
271
+ * independent — one crashing or failing to start does not stop the others (it is shown
272
+ * as errored and recovered on the next edit). A single SIGINT/SIGTERM shuts all of them
273
+ * down, tree-killing each child so no descendant it spawned is orphaned.
274
+ *
275
+ * In config mode, `reload` lets the supervisor watch `neon.ts` and reconcile the live set
276
+ * of units when it changes: a newly-declared function is hot-added (its own child, watcher,
277
+ * and port) and a removed one is torn down — all without disturbing the functions that
278
+ * stayed the same. A function whose config (env/port/source) changed is restarted
279
+ * in place; siblings are untouched.
280
+ */
295
281
  const runSupervisor = async (units, options = {}) => {
296
- const { reload, envNote } = options;
297
- const runtimePath = resolveRuntimePath();
298
- let shuttingDown = false;
299
- const running = units.map(makeRunningUnit);
300
- const bundleAndStart = async (r) => {
301
- let bundlePath;
302
- try {
303
- bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages,
304
- // The `neon.ts` key, so a function is named the same way here as at deploy.
305
- r.unit.slug ?? undefined);
306
- }
307
- catch (err) {
308
- r.status = "error";
309
- logUnit(r.unit, chalk.red("bundle failed: ") +
310
- (err instanceof Error ? err.message : String(err)));
311
- return;
312
- }
313
- if (r.watcher)
314
- await r.watcher.sync();
315
- const next = spawnChild(r.unit, runtimePath, bundlePath);
316
- r.child = next;
317
- const ready = waitForReady(next);
318
- pipeChildOutput(next, r.unit.label);
319
- next.on("exit", (code, signal) => {
320
- if (shuttingDown || r.child !== next)
321
- return;
322
- if (signal) {
323
- log.debug("runtime for %s exited via %s", r.unit.slug ?? "(source)", signal);
324
- return;
325
- }
326
- if (code && code !== 0 && r.everReady) {
327
- r.status = "error";
328
- logUnit(r.unit, chalk.red(`exited with code ${code} (waiting for a change)`));
329
- }
330
- });
331
- const port = await ready;
332
- if (port !== null) {
333
- r.boundPort = port;
334
- r.everReady = true;
335
- r.status = "ready";
336
- }
337
- else {
338
- r.status = "error";
339
- }
340
- };
341
- const restart = (r) => {
342
- if (shuttingDown)
343
- return;
344
- if (r.restartTimer)
345
- clearTimeout(r.restartTimer);
346
- r.restartTimer = setTimeout(() => {
347
- void (async () => {
348
- logUnit(r.unit, chalk.dim("change detected, restarting…"));
349
- if (r.child)
350
- await killTree(r.child);
351
- if (shuttingDown)
352
- return;
353
- await bundleAndStart(r);
354
- if (r.status === "ready") {
355
- logUnit(r.unit, chalk.green("ready") + ` ${urlFor(r.boundPort)}`);
356
- }
357
- })();
358
- }, 150);
359
- };
360
- // Bring a unit fully online: create its source watcher (before the first bundle so
361
- // bundleAndStart can sync the watch set on every run) then bundle + spawn it.
362
- const startUnit = async (r) => {
363
- r.watcher = await startWatcher(r.unit.source, () => {
364
- restart(r);
365
- });
366
- await bundleAndStart(r);
367
- };
368
- // Tear a unit down completely: stop its restart timer + watcher, reap its child tree, and
369
- // remove its bundle dir. Used both on shutdown and when neon.ts drops a function.
370
- const stopUnit = async (r) => {
371
- if (r.restartTimer)
372
- clearTimeout(r.restartTimer);
373
- await r.watcher?.close();
374
- if (r.child)
375
- await killTree(r.child);
376
- rmSync(r.unit.bundleDir, { recursive: true, force: true });
377
- };
378
- // Start every unit. They are independent: keep going if one fails.
379
- await Promise.all(running.map((r) => startUnit(r)));
380
- if (running.every((r) => r.status === "error")) {
381
- await Promise.all(running.map((r) => stopUnit(r)));
382
- throw new Error("No function started. See the output above for details.");
383
- }
384
- printBanner(running, envNote);
385
- // Config mode only: watch neon.ts and reconcile the live unit set when it changes.
386
- // Reconciles are serialized: a burst of saves (editor write-then-format) must not run
387
- // overlapping diffs against the mutating `running` array. A trailing run coalesces the
388
- // burst and picks up the latest config.
389
- let configWatcher = null;
390
- if (reload) {
391
- const ops = {
392
- isShuttingDown: () => shuttingDown,
393
- startUnit,
394
- stopUnit,
395
- restartUnit: restart,
396
- };
397
- let inFlight = null;
398
- let pending = false;
399
- const drive = () => {
400
- if (inFlight) {
401
- pending = true;
402
- return;
403
- }
404
- inFlight = (async () => {
405
- do {
406
- pending = false;
407
- await reconcileOnce(running, reload.replan, ops);
408
- } while (pending && !shuttingDown);
409
- })().finally(() => {
410
- inFlight = null;
411
- });
412
- };
413
- configWatcher = await startConfigWatcher(reload.configPath, drive);
414
- }
415
- await new Promise((resolveRun) => {
416
- const shutdown = () => {
417
- if (shuttingDown)
418
- return;
419
- shuttingDown = true;
420
- void (async () => {
421
- await configWatcher?.close();
422
- await Promise.all(running.map((r) => stopUnit(r)));
423
- log.info(chalk.dim("Stopped the dev server."));
424
- resolveRun();
425
- })();
426
- };
427
- process.on("SIGINT", shutdown);
428
- process.on("SIGTERM", shutdown);
429
- });
282
+ const { reload, envNote } = options;
283
+ const runtimePath = resolveRuntimePath();
284
+ let shuttingDown = false;
285
+ const running = units.map(makeRunningUnit);
286
+ const bundleAndStart = async (r) => {
287
+ let bundlePath;
288
+ try {
289
+ bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages, r.unit.slug ?? void 0);
290
+ } catch (err) {
291
+ r.status = "error";
292
+ logUnit(r.unit, chalk.red("bundle failed: ") + (err instanceof Error ? err.message : String(err)));
293
+ return;
294
+ }
295
+ if (r.watcher) await r.watcher.sync();
296
+ const next = spawnChild(r.unit, runtimePath, bundlePath);
297
+ r.child = next;
298
+ const ready = waitForReady(next);
299
+ pipeChildOutput(next, r.unit.label);
300
+ next.on("exit", (code, signal) => {
301
+ if (shuttingDown || r.child !== next) return;
302
+ if (signal) {
303
+ log.debug("runtime for %s exited via %s", r.unit.slug ?? "(source)", signal);
304
+ return;
305
+ }
306
+ if (code && code !== 0 && r.everReady) {
307
+ r.status = "error";
308
+ logUnit(r.unit, chalk.red(`exited with code ${code} (waiting for a change)`));
309
+ }
310
+ });
311
+ const port = await ready;
312
+ if (port !== null) {
313
+ r.boundPort = port;
314
+ r.everReady = true;
315
+ r.status = "ready";
316
+ } else r.status = "error";
317
+ };
318
+ const restart = (r) => {
319
+ if (shuttingDown) return;
320
+ if (r.restartTimer) clearTimeout(r.restartTimer);
321
+ r.restartTimer = setTimeout(() => {
322
+ (async () => {
323
+ logUnit(r.unit, chalk.dim("change detected, restarting…"));
324
+ if (r.child) await killTree(r.child);
325
+ if (shuttingDown) return;
326
+ await bundleAndStart(r);
327
+ if (r.status === "ready") logUnit(r.unit, chalk.green("ready") + ` ${urlFor(r.boundPort)}`);
328
+ })();
329
+ }, 150);
330
+ };
331
+ const startUnit = async (r) => {
332
+ r.watcher = await startWatcher(r.unit.source, () => {
333
+ restart(r);
334
+ });
335
+ await bundleAndStart(r);
336
+ };
337
+ const stopUnit = async (r) => {
338
+ if (r.restartTimer) clearTimeout(r.restartTimer);
339
+ await r.watcher?.close();
340
+ if (r.child) await killTree(r.child);
341
+ rmSync(r.unit.bundleDir, {
342
+ recursive: true,
343
+ force: true
344
+ });
345
+ };
346
+ await Promise.all(running.map((r) => startUnit(r)));
347
+ if (running.every((r) => r.status === "error")) {
348
+ await Promise.all(running.map((r) => stopUnit(r)));
349
+ throw new Error("No function started. See the output above for details.");
350
+ }
351
+ printBanner(running, envNote);
352
+ let configWatcher = null;
353
+ if (reload) {
354
+ const ops = {
355
+ isShuttingDown: () => shuttingDown,
356
+ startUnit,
357
+ stopUnit,
358
+ restartUnit: restart
359
+ };
360
+ let inFlight = null;
361
+ let pending = false;
362
+ const drive = () => {
363
+ if (inFlight) {
364
+ pending = true;
365
+ return;
366
+ }
367
+ inFlight = (async () => {
368
+ do {
369
+ pending = false;
370
+ await reconcileOnce(running, reload.replan, ops);
371
+ } while (pending && !shuttingDown);
372
+ })().finally(() => {
373
+ inFlight = null;
374
+ });
375
+ };
376
+ configWatcher = await startConfigWatcher(reload.configPath, drive);
377
+ }
378
+ await new Promise((resolveRun) => {
379
+ const shutdown = () => {
380
+ if (shuttingDown) return;
381
+ shuttingDown = true;
382
+ (async () => {
383
+ await configWatcher?.close();
384
+ await Promise.all(running.map((r) => stopUnit(r)));
385
+ log.info(chalk.dim("Stopped the dev server."));
386
+ resolveRun();
387
+ })();
388
+ };
389
+ process.on("SIGINT", shutdown);
390
+ process.on("SIGTERM", shutdown);
391
+ });
430
392
  };
431
393
  const makeRunningUnit = (unit) => ({
432
- unit,
433
- child: null,
434
- boundPort: null,
435
- everReady: false,
436
- restartTimer: null,
437
- watcher: null,
438
- status: "starting",
394
+ unit,
395
+ child: null,
396
+ boundPort: null,
397
+ everReady: false,
398
+ restartTimer: null,
399
+ watcher: null,
400
+ status: "starting"
439
401
  });
440
402
  /**
441
- * Pure slug-keyed diff of the live units against the freshly-resolved desired set:
442
- * - a slug present now but not before → **add** (new child + watcher + port),
443
- * - a slug gone from neon.ts → **remove** (torn down),
444
- * - a slug whose config (source/port/env) changed → **restart** in place,
445
- * - an unchanged slug → left out of the plan entirely (never touched).
446
- * Functions that stayed the same never die, so an edit that only adds a function is
447
- * non-disruptive. `desired === null` (neon.ts deleted) is treated as "no functions".
448
- */
449
- export const diffUnits = (running, desired) => {
450
- const desiredBySlug = new Map();
451
- for (const u of desired ?? []) {
452
- if (u.slug !== null)
453
- desiredBySlug.set(u.slug, u);
454
- }
455
- const runningBySlug = new Map();
456
- for (const r of running) {
457
- if (r.unit.slug !== null)
458
- runningBySlug.set(r.unit.slug, r);
459
- }
460
- const plan = { remove: [], restart: [], add: [] };
461
- for (const [slug, r] of runningBySlug) {
462
- if (!desiredBySlug.has(slug))
463
- plan.remove.push(r);
464
- }
465
- for (const [slug, want] of desiredBySlug) {
466
- const r = runningBySlug.get(slug);
467
- if (r) {
468
- if (r.unit.configKey !== want.configKey) {
469
- r.unit = want;
470
- plan.restart.push(r);
471
- }
472
- }
473
- else {
474
- plan.add.push(want);
475
- }
476
- }
477
- return plan;
403
+ * Pure slug-keyed diff of the live units against the freshly-resolved desired set:
404
+ * - a slug present now but not before → **add** (new child + watcher + port),
405
+ * - a slug gone from neon.ts → **remove** (torn down),
406
+ * - a slug whose config (source/port/env) changed → **restart** in place,
407
+ * - an unchanged slug → left out of the plan entirely (never touched).
408
+ * Functions that stayed the same never die, so an edit that only adds a function is
409
+ * non-disruptive. `desired === null` (neon.ts deleted) is treated as "no functions".
410
+ */
411
+ const diffUnits = (running, desired) => {
412
+ const desiredBySlug = /* @__PURE__ */ new Map();
413
+ for (const u of desired ?? []) if (u.slug !== null) desiredBySlug.set(u.slug, u);
414
+ const runningBySlug = /* @__PURE__ */ new Map();
415
+ for (const r of running) if (r.unit.slug !== null) runningBySlug.set(r.unit.slug, r);
416
+ const plan = {
417
+ remove: [],
418
+ restart: [],
419
+ add: []
420
+ };
421
+ for (const [slug, r] of runningBySlug) if (!desiredBySlug.has(slug)) plan.remove.push(r);
422
+ for (const [slug, want] of desiredBySlug) {
423
+ const r = runningBySlug.get(slug);
424
+ if (r) {
425
+ if (r.unit.configKey !== want.configKey) {
426
+ r.unit = want;
427
+ plan.restart.push(r);
428
+ }
429
+ } else plan.add.push(want);
430
+ }
431
+ return plan;
478
432
  };
479
433
  /**
480
- * Run one reconcile: re-resolve neon.ts (ignoring the change with a clear message if it no
481
- * longer loads), {@link diffUnits} against the live set, then apply the plan — tearing down
482
- * removed functions, restarting changed ones in place, and hot-adding new ones. Mutates
483
- * `running` in place so the surrounding supervisor sees the converged set.
484
- */
434
+ * Run one reconcile: re-resolve neon.ts (ignoring the change with a clear message if it no
435
+ * longer loads), {@link diffUnits} against the live set, then apply the plan — tearing down
436
+ * removed functions, restarting changed ones in place, and hot-adding new ones. Mutates
437
+ * `running` in place so the surrounding supervisor sees the converged set.
438
+ */
485
439
  const reconcileOnce = async (running, replan, ops) => {
486
- if (ops.isShuttingDown())
487
- return;
488
- let desired;
489
- try {
490
- desired = await replan(nextSearchBase(running));
491
- }
492
- catch (err) {
493
- log.info(chalk.red("neon.ts change ignored: ") +
494
- (err instanceof Error ? err.message : String(err)) +
495
- chalk.dim(" (fix it and save again)"));
496
- return;
497
- }
498
- if (ops.isShuttingDown())
499
- return;
500
- const plan = diffUnits(running, desired);
501
- for (const r of plan.remove) {
502
- logUnit(r.unit, chalk.dim("removed from neon.ts, stopping…"));
503
- await ops.stopUnit(r);
504
- const idx = running.indexOf(r);
505
- if (idx !== -1)
506
- running.splice(idx, 1);
507
- }
508
- for (const r of plan.restart)
509
- ops.restartUnit(r);
510
- if (plan.add.length > 0) {
511
- const added = plan.add.map((unit) => {
512
- const r = makeRunningUnit(unit);
513
- running.push(r);
514
- logUnit(unit, chalk.dim("added in neon.ts, starting…"));
515
- return r;
516
- });
517
- await Promise.all(added.map((r) => ops.startUnit(r)));
518
- for (const r of added) {
519
- if (r.status === "ready") {
520
- const env = formatEnvSummary(r.unit.envSummary);
521
- logUnit(r.unit, chalk.green("ready") +
522
- ` ${urlFor(r.boundPort)}` +
523
- (env ? chalk.dim(` ${env}`) : ""));
524
- }
525
- }
526
- }
440
+ if (ops.isShuttingDown()) return;
441
+ let desired;
442
+ try {
443
+ desired = await replan(nextSearchBase(running));
444
+ } catch (err) {
445
+ log.info(chalk.red("neon.ts change ignored: ") + (err instanceof Error ? err.message : String(err)) + chalk.dim(" (fix it and save again)"));
446
+ return;
447
+ }
448
+ if (ops.isShuttingDown()) return;
449
+ const plan = diffUnits(running, desired);
450
+ for (const r of plan.remove) {
451
+ logUnit(r.unit, chalk.dim("removed from neon.ts, stopping…"));
452
+ await ops.stopUnit(r);
453
+ const idx = running.indexOf(r);
454
+ if (idx !== -1) running.splice(idx, 1);
455
+ }
456
+ for (const r of plan.restart) ops.restartUnit(r);
457
+ if (plan.add.length > 0) {
458
+ const added = plan.add.map((unit) => {
459
+ const r = makeRunningUnit(unit);
460
+ running.push(r);
461
+ logUnit(unit, chalk.dim("added in neon.ts, starting…"));
462
+ return r;
463
+ });
464
+ await Promise.all(added.map((r) => ops.startUnit(r)));
465
+ for (const r of added) if (r.status === "ready") {
466
+ const env = formatEnvSummary(r.unit.envSummary);
467
+ logUnit(r.unit, chalk.green("ready") + ` ${urlFor(r.boundPort)}` + (env ? chalk.dim(` ${env}`) : ""));
468
+ }
469
+ }
527
470
  };
528
471
  /**
529
- * Choose a port search base above every port the live units already bound, so a hot-added
530
- * search-mode function starts probing where there's room. The runtime still walks upward
531
- * from here, so it never fails even if this guess is taken — it just keeps things tidy.
532
- */
472
+ * Choose a port search base above every port the live units already bound, so a hot-added
473
+ * search-mode function starts probing where there's room. The runtime still walks upward
474
+ * from here, so it never fails even if this guess is taken — it just keeps things tidy.
475
+ */
533
476
  const nextSearchBase = (running) => {
534
- let max = DEFAULT_PORT_BASE - 1;
535
- for (const r of running) {
536
- if (r.boundPort !== null && r.boundPort > max)
537
- max = r.boundPort;
538
- }
539
- return max + 1;
477
+ let max = 8786;
478
+ for (const r of running) if (r.boundPort !== null && r.boundPort > max) max = r.boundPort;
479
+ return max + 1;
540
480
  };
541
481
  /**
542
- * Spawn the child for a unit: the bundled output run directly under `node`.
543
- *
544
- * Spawned detached (own process group) so killTree can reap the whole group.
545
- */
482
+ * Spawn the child for a unit: the bundled output run directly under `node`.
483
+ *
484
+ * Spawned detached (own process group) so killTree can reap the whole group.
485
+ */
546
486
  const spawnChild = (unit, runtimePath, bundlePath) => {
547
- return spawn(process.execPath, [runtimePath, bundlePath], {
548
- stdio: ["ignore", "pipe", "pipe"],
549
- env: unit.childEnv,
550
- detached: true,
551
- });
487
+ return spawn(process.execPath, [runtimePath, bundlePath], {
488
+ stdio: [
489
+ "ignore",
490
+ "pipe",
491
+ "pipe"
492
+ ],
493
+ env: unit.childEnv,
494
+ detached: true
495
+ });
552
496
  };
553
497
  /**
554
- * Findings already reported for a served unit, so an advisory that cannot change between
555
- * saves is not reprinted on every one. A dev session on a project with a standing false
556
- * positive would otherwise repeat the whole block for its lifetime.
557
- */
558
- const reportedFindings = new Map();
498
+ * Findings already reported for a served unit, so an advisory that cannot change between
499
+ * saves is not reprinted on every one. A dev session on a project with a standing false
500
+ * positive would otherwise repeat the whole block for its lifetime.
501
+ */
502
+ const reportedFindings = /* @__PURE__ */ new Map();
559
503
  const writeBundle = async (source, bundleDir, externalPackages, label) => {
560
- // Left unbundled only. `bundleDir` sits inside the project's node_modules, so an
561
- // externalized package resolves from the real tree at the host architecture — no install
562
- // or copy is needed or wanted locally, whatever `includeFiles` says for a deploy.
563
- const { files, metafile, warnings } = await bundleEntry(source, {
564
- ...(externalPackages ? { externalPackages } : {}),
565
- });
566
- for (const warning of warnings)
567
- log.warning(warning);
568
- // Local runs resolve native packages from the real tree, so a missing declaration is
569
- // invisible until deploy. Reporting it here is the only signal before then.
570
- const findings = findUndeclaredNativePackages({
571
- metafile,
572
- declared: externalPackages ?? [],
573
- projectDir: dirname(source),
574
- });
575
- const signature = findings.map((f) => f.name).join(",");
576
- if (reportedFindings.get(source) !== signature) {
577
- reportedFindings.set(source, signature);
578
- for (const finding of findings) {
579
- log.warning(describeNativeFinding(label ?? basename(source), finding));
580
- }
581
- }
582
- mkdirSync(bundleDir, { recursive: true });
583
- // bundleEntry emits a single `index.mjs` (no source map). The `.mjs` extension makes Node
584
- // load it as ESM directly, so no `package.json` `"type": "module"` marker is needed.
585
- for (const [name, contents] of Object.entries(files)) {
586
- writeFileSync(join(bundleDir, name), contents);
587
- }
588
- return join(bundleDir, "index.mjs");
504
+ const { files, metafile, warnings } = await bundleEntry(source, { ...externalPackages ? { externalPackages } : {} });
505
+ for (const warning of warnings) log.warning(warning);
506
+ const findings = findUndeclaredNativePackages({
507
+ metafile,
508
+ declared: externalPackages ?? [],
509
+ projectDir: dirname(source)
510
+ });
511
+ const signature = findings.map((f) => f.name).join(",");
512
+ if (reportedFindings.get(source) !== signature) {
513
+ reportedFindings.set(source, signature);
514
+ for (const finding of findings) log.warning(describeNativeFinding(label ?? basename(source), finding));
515
+ }
516
+ mkdirSync(bundleDir, { recursive: true });
517
+ for (const [name, contents] of Object.entries(files)) writeFileSync(join(bundleDir, name), contents);
518
+ return join(bundleDir, "index.mjs");
589
519
  };
590
520
  const urlFor = (port) => port === null ? chalk.red("not running") : `http://localhost:${port}`;
591
521
  const waitForReady = (child) => new Promise((resolveReady) => {
592
- let settled = false;
593
- let buffer = "";
594
- const onData = (chunk) => {
595
- buffer += chunk.toString();
596
- const match = READY_PATTERN.exec(buffer);
597
- if (match && !settled) {
598
- settled = true;
599
- child.stdout?.off("data", onData);
600
- resolveReady(Number(match[1]));
601
- }
602
- };
603
- child.stdout?.on("data", onData);
604
- child.once("exit", () => {
605
- if (!settled) {
606
- settled = true;
607
- resolveReady(null);
608
- }
609
- });
522
+ let settled = false;
523
+ let buffer = "";
524
+ const onData = (chunk) => {
525
+ buffer += chunk.toString();
526
+ const match = READY_PATTERN.exec(buffer);
527
+ if (match && !settled) {
528
+ settled = true;
529
+ child.stdout?.off("data", onData);
530
+ resolveReady(Number(match[1]));
531
+ }
532
+ };
533
+ child.stdout?.on("data", onData);
534
+ child.once("exit", () => {
535
+ if (!settled) {
536
+ settled = true;
537
+ resolveReady(null);
538
+ }
539
+ });
610
540
  });
611
541
  /**
612
- * Forward the child's stdout/stderr to the parent, swallowing the machine-readable
613
- * `neon-dev:ready` line. When `label` is set (multi-function mode), every line is
614
- * prefixed with `[slug]` so concurrent servers' output stays readable.
615
- */
542
+ * Forward the child's stdout/stderr to the parent, swallowing the machine-readable
543
+ * `neon-dev:ready` line. When `label` is set (multi-function mode), every line is
544
+ * prefixed with `[slug]` so concurrent servers' output stays readable.
545
+ */
616
546
  const pipeChildOutput = (child, label) => {
617
- const prefix = label ? chalk.dim(`[${label}] `) : "";
618
- const forward = (stream) => {
619
- let buffer = "";
620
- child[stream]?.on("data", (chunk) => {
621
- buffer += chunk.toString();
622
- const lines = buffer.split("\n");
623
- buffer = lines.pop() ?? "";
624
- for (const line of lines) {
625
- if (READY_PATTERN.test(line))
626
- continue;
627
- process[stream].write(`${prefix}${line}\n`);
628
- }
629
- });
630
- };
631
- forward("stdout");
632
- forward("stderr");
547
+ const prefix = label ? chalk.dim(`[${label}] `) : "";
548
+ const forward = (stream) => {
549
+ let buffer = "";
550
+ child[stream]?.on("data", (chunk) => {
551
+ buffer += chunk.toString();
552
+ const lines = buffer.split("\n");
553
+ buffer = lines.pop() ?? "";
554
+ for (const line of lines) {
555
+ if (READY_PATTERN.test(line)) continue;
556
+ process[stream].write(`${prefix}${line}\n`);
557
+ }
558
+ });
559
+ };
560
+ forward("stdout");
561
+ forward("stderr");
633
562
  };
634
563
  const printBanner = (running, envNote) => {
635
- log.info("");
636
- log.info(chalk.green.bold(" Neon Functions dev server"));
637
- log.info("");
638
- for (const r of running) {
639
- const name = r.unit.label ?? "function";
640
- const url = urlFor(r.boundPort);
641
- log.info(` ${chalk.dim(name.padEnd(20))} ${url}`);
642
- const env = formatEnvSummary(r.unit.envSummary);
643
- if (env)
644
- log.info(` ${" ".repeat(20)} ${chalk.dim(env)}`);
645
- }
646
- if (envNote) {
647
- log.info("");
648
- log.info(` ${chalk.yellow("!")} ${chalk.dim(`Neon env: ${envNote}`)}`);
649
- }
650
- log.info("");
564
+ log.info("");
565
+ log.info(chalk.green.bold(" Neon Functions dev server"));
566
+ log.info("");
567
+ for (const r of running) {
568
+ const name = r.unit.label ?? "function";
569
+ const url = urlFor(r.boundPort);
570
+ log.info(` ${chalk.dim(name.padEnd(20))} ${url}`);
571
+ const env = formatEnvSummary(r.unit.envSummary);
572
+ if (env) log.info(` ${" ".repeat(20)} ${chalk.dim(env)}`);
573
+ }
574
+ if (envNote) {
575
+ log.info("");
576
+ log.info(` ${chalk.yellow("!")} ${chalk.dim(`Neon env: ${envNote}`)}`);
577
+ }
578
+ log.info("");
651
579
  };
652
580
  /**
653
- * Render a unit's injected env into one transparent line for the banner, e.g.
654
- * `env: DATABASE_URL, DATABASE_URL_UNPOOLED · neon.ts: RESEND_API_KEY`. Var **names** only
655
- * (never values — they're secrets). Returns `''` when nothing is injected, so the caller can
656
- * skip the line. Exported for unit testing.
657
- */
658
- export const formatEnvSummary = (summary) => {
659
- if (!summary)
660
- return "";
661
- const parts = [];
662
- if (summary.neon.length > 0) {
663
- parts.push(`env: ${[...summary.neon].sort().join(", ")}`);
664
- }
665
- if (summary.fn.length > 0) {
666
- parts.push(`neon.ts: ${[...summary.fn].sort().join(", ")}`);
667
- }
668
- return parts.join(" · ");
581
+ * Render a unit's injected env into one transparent line for the banner, e.g.
582
+ * `env: DATABASE_URL, DATABASE_URL_UNPOOLED · neon.ts: RESEND_API_KEY`. Var **names** only
583
+ * (never values — they're secrets). Returns `''` when nothing is injected, so the caller can
584
+ * skip the line. Exported for unit testing.
585
+ */
586
+ const formatEnvSummary = (summary) => {
587
+ if (!summary) return "";
588
+ const parts = [];
589
+ if (summary.neon.length > 0) parts.push(`env: ${[...summary.neon].sort().join(", ")}`);
590
+ if (summary.fn.length > 0) parts.push(`neon.ts: ${[...summary.fn].sort().join(", ")}`);
591
+ return parts.join(" · ");
669
592
  };
670
593
  const logUnit = (unit, message) => {
671
- const prefix = unit.label ? chalk.dim(`[${unit.label}] `) : "";
672
- log.info(`${prefix}${message}`);
594
+ const prefix = unit.label ? chalk.dim(`[${unit.label}] `) : "";
595
+ log.info(`${prefix}${message}`);
673
596
  };
674
597
  const startWatcher = async (source, restart) => {
675
- const { default: chokidar } = await import("chokidar");
676
- const initialInputs = await resolveWatchInputs(source);
677
- if (initialInputs === null) {
678
- return startDirectoryWatcher(chokidar, source, restart);
679
- }
680
- return startInputWatcher(chokidar, source, initialInputs, restart);
598
+ const { default: chokidar } = await import("chokidar");
599
+ const initialInputs = await resolveWatchInputs(source);
600
+ if (initialInputs === null) return startDirectoryWatcher(chokidar, source, restart);
601
+ return startInputWatcher(chokidar, source, initialInputs, restart);
681
602
  };
682
603
  /**
683
- * Watch the neon.ts file itself for changes, firing `onChange` on every save. Used by the
684
- * supervisor (config mode only) to hot-add/remove/restart functions when the declared set
685
- * changes. We watch the single config file rather than its import graph: editing neon.ts to
686
- * add or remove a function is the case that matters, and a plain file watch is robust where
687
- * the esbuild-based input resolution (built for function bundles) is not a fit for a
688
- * jiti-loaded config.
689
- */
604
+ * Watch the neon.ts file itself for changes, firing `onChange` on every save. Used by the
605
+ * supervisor (config mode only) to hot-add/remove/restart functions when the declared set
606
+ * changes. We watch the single config file rather than its import graph: editing neon.ts to
607
+ * add or remove a function is the case that matters, and a plain file watch is robust where
608
+ * the esbuild-based input resolution (built for function bundles) is not a fit for a
609
+ * jiti-loaded config.
610
+ */
690
611
  const startConfigWatcher = async (configPath, onChange) => {
691
- const { default: chokidar } = await import("chokidar");
692
- const watcher = chokidar.watch(configPath, { ignoreInitial: true });
693
- await once(watcher, "ready");
694
- watcher.on("all", () => {
695
- onChange();
696
- });
697
- return { sync: () => Promise.resolve(), close: () => watcher.close() };
612
+ const { default: chokidar } = await import("chokidar");
613
+ const watcher = chokidar.watch(configPath, { ignoreInitial: true });
614
+ await once(watcher, "ready");
615
+ watcher.on("all", () => {
616
+ onChange();
617
+ });
618
+ return {
619
+ sync: () => Promise.resolve(),
620
+ close: () => watcher.close()
621
+ };
698
622
  };
699
623
  const startInputWatcher = async (chokidar, source, initialInputs, restart) => {
700
- const watched = new Set([source, ...initialInputs]);
701
- const watcher = chokidar.watch([...watched], { ignoreInitial: true });
702
- await once(watcher, "ready");
703
- watcher.on("all", () => {
704
- restart();
705
- });
706
- const sync = async () => {
707
- const next = await resolveWatchInputs(source);
708
- if (next === null)
709
- return;
710
- const desired = new Set([source, ...next]);
711
- for (const file of desired) {
712
- if (!watched.has(file)) {
713
- watcher.add(file);
714
- watched.add(file);
715
- }
716
- }
717
- for (const file of watched) {
718
- if (!desired.has(file)) {
719
- watcher.unwatch(file);
720
- watched.delete(file);
721
- }
722
- }
723
- };
724
- return { sync, close: () => watcher.close() };
624
+ const watched = /* @__PURE__ */ new Set([source, ...initialInputs]);
625
+ const watcher = chokidar.watch([...watched], { ignoreInitial: true });
626
+ await once(watcher, "ready");
627
+ watcher.on("all", () => {
628
+ restart();
629
+ });
630
+ const sync = async () => {
631
+ const next = await resolveWatchInputs(source);
632
+ if (next === null) return;
633
+ const desired = /* @__PURE__ */ new Set([source, ...next]);
634
+ for (const file of desired) if (!watched.has(file)) {
635
+ watcher.add(file);
636
+ watched.add(file);
637
+ }
638
+ for (const file of watched) if (!desired.has(file)) {
639
+ watcher.unwatch(file);
640
+ watched.delete(file);
641
+ }
642
+ };
643
+ return {
644
+ sync,
645
+ close: () => watcher.close()
646
+ };
725
647
  };
726
648
  const startDirectoryWatcher = async (chokidar, source, restart) => {
727
- const watchedDir = dirname(source);
728
- const isIgnored = (p) => {
729
- const segments = p.split(/[/\\]/);
730
- return (segments.includes("node_modules") ||
731
- segments.includes(".git") ||
732
- segments.includes("dist"));
733
- };
734
- const watcher = chokidar.watch(watchedDir, {
735
- ignoreInitial: true,
736
- ignored: (path) => isIgnored(path),
737
- });
738
- await once(watcher, "ready");
739
- watcher.on("all", () => {
740
- restart();
741
- });
742
- return { sync: () => Promise.resolve(), close: () => watcher.close() };
649
+ const watchedDir = dirname(source);
650
+ const isIgnored = (p) => {
651
+ const segments = p.split(/[/\\]/);
652
+ return segments.includes("node_modules") || segments.includes(".git") || segments.includes("dist");
653
+ };
654
+ const watcher = chokidar.watch(watchedDir, {
655
+ ignoreInitial: true,
656
+ ignored: (path) => isIgnored(path)
657
+ });
658
+ await once(watcher, "ready");
659
+ watcher.on("all", () => {
660
+ restart();
661
+ });
662
+ return {
663
+ sync: () => Promise.resolve(),
664
+ close: () => watcher.close()
665
+ };
743
666
  };
744
667
  /**
745
- * Terminate a child and every descendant it spawned. The child is started `detached`, so
746
- * on POSIX it leads its own process group and a negative-PID signal reaps the group
747
- * (covering the runtime and anything it spawned). On Windows there are no POSIX groups, so we
748
- * shell out to `taskkill /T` to kill the tree. Escalates SIGTERM -> SIGKILL after 2s.
749
- */
668
+ * Terminate a child and every descendant it spawned. The child is started `detached`, so
669
+ * on POSIX it leads its own process group and a negative-PID signal reaps the group
670
+ * (covering the runtime and anything it spawned). On Windows there are no POSIX groups, so we
671
+ * shell out to `taskkill /T` to kill the tree. Escalates SIGTERM -> SIGKILL after 2s.
672
+ */
750
673
  const killTree = (child) => {
751
- if (child.exitCode !== null || child.signalCode !== null) {
752
- return Promise.resolve();
753
- }
754
- const pid = child.pid;
755
- return new Promise((resolveKill) => {
756
- const timeout = setTimeout(() => {
757
- forceKill(child, pid);
758
- }, 2000);
759
- child.once("exit", () => {
760
- clearTimeout(timeout);
761
- resolveKill();
762
- });
763
- if (pid !== undefined && process.platform !== "win32") {
764
- try {
765
- process.kill(-pid, "SIGTERM");
766
- return;
767
- }
768
- catch {
769
- // Fall through to a direct kill if the group is already gone.
770
- }
771
- }
772
- child.kill("SIGTERM");
773
- });
674
+ if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
675
+ const pid = child.pid;
676
+ return new Promise((resolveKill) => {
677
+ const timeout = setTimeout(() => {
678
+ forceKill(child, pid);
679
+ }, 2e3);
680
+ child.once("exit", () => {
681
+ clearTimeout(timeout);
682
+ resolveKill();
683
+ });
684
+ if (pid !== void 0 && process.platform !== "win32") try {
685
+ process.kill(-pid, "SIGTERM");
686
+ return;
687
+ } catch {}
688
+ child.kill("SIGTERM");
689
+ });
774
690
  };
775
691
  const forceKill = (child, pid) => {
776
- if (pid === undefined) {
777
- child.kill("SIGKILL");
778
- return;
779
- }
780
- if (process.platform === "win32") {
781
- spawnSync("taskkill", ["/pid", String(pid), "/T", "/F"]);
782
- return;
783
- }
784
- try {
785
- process.kill(-pid, "SIGKILL");
786
- }
787
- catch {
788
- child.kill("SIGKILL");
789
- }
692
+ if (pid === void 0) {
693
+ child.kill("SIGKILL");
694
+ return;
695
+ }
696
+ if (process.platform === "win32") {
697
+ spawnSync("taskkill", [
698
+ "/pid",
699
+ String(pid),
700
+ "/T",
701
+ "/F"
702
+ ]);
703
+ return;
704
+ }
705
+ try {
706
+ process.kill(-pid, "SIGKILL");
707
+ } catch {
708
+ child.kill("SIGKILL");
709
+ }
790
710
  };
791
711
  const resolveRuntimePath = () => {
792
- const here = dirname(fileURLToPath(import.meta.url));
793
- const candidate = join(here, "..", "dev", "runtime.js");
794
- if (!existsSync(candidate)) {
795
- throw new Error(`Could not locate the dev runtime at ${candidate}`);
796
- }
797
- return candidate;
712
+ const here = dirname(fileURLToPath(import.meta.url));
713
+ const candidate = join(here, "..", "dev", "runtime.js");
714
+ if (!existsSync(candidate)) throw new Error(`Could not locate the dev runtime at ${candidate}`);
715
+ return candidate;
798
716
  };
717
+ //#endregion
718
+ export { builder, command, describe, devBundleDir, devEnvContext, diffUnits, formatEnvSummary, handler, reportDevCredential, dev_exports as t };