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