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