neon 8.11.1 → 8.11.2
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.
- package/README.md +1 -1
- package/dist/_chunks/api-GbEaBZJ5.js +388 -0
- package/dist/_chunks/api_keys-CNnnugbQ.js +281 -0
- package/dist/_chunks/ask-DVwy0eUL.js +155 -0
- package/dist/_chunks/auth-CIUtXByB.js +495 -0
- package/dist/_chunks/bootstrap-wPZWa1sT.js +499 -0
- package/dist/_chunks/branches-BNa1xX1L.js +460 -0
- package/dist/_chunks/bucket-BCdryTkD.js +409 -0
- package/dist/_chunks/checkout-CXIOydqZ.js +348 -0
- package/dist/_chunks/claim-DqAvAvDm.js +473 -0
- package/dist/_chunks/config-BH_qRHiG.js +1113 -0
- package/dist/_chunks/connection_string-DB4awPzZ.js +154 -0
- package/dist/_chunks/credentials-BSjE9fRR.js +146 -0
- package/dist/_chunks/data_api-Djxj5ZQD.js +263 -0
- package/dist/_chunks/databases-Dok4wru7.js +84 -0
- package/dist/_chunks/deploy-D7B0Xfjg.js +33 -0
- package/dist/_chunks/dev-CSvxAecE.js +945 -0
- package/dist/_chunks/diff-DhxxbdQE.js +178 -0
- package/dist/_chunks/docs-CrVybkTP.js +197 -0
- package/dist/_chunks/env-5HS5qQGd.js +284 -0
- package/dist/_chunks/feedback-CMayyTa8.js +88 -0
- package/dist/_chunks/functions-Q0zuvtnk.js +464 -0
- package/dist/_chunks/git-H32TNt6M.js +356 -0
- package/dist/_chunks/init-BkA6RilG.js +111 -0
- package/dist/_chunks/inspect-g0wekylj.js +90 -0
- package/dist/_chunks/ip_allow-5lLFvIuE.js +106 -0
- package/dist/_chunks/link-BgtkOBws.js +936 -0
- package/dist/_chunks/logs-DWkpjzIH.js +291 -0
- package/dist/_chunks/mcp-Bo-7TKc7.js +253 -0
- package/dist/_chunks/neon_auth-CBs1_3qu.js +822 -0
- package/dist/_chunks/open-CX3BT3Tp.js +33 -0
- package/dist/_chunks/operations-D4I-AxVs.js +37 -0
- package/dist/_chunks/orgs-xGfANwHQ.js +52 -0
- package/dist/_chunks/plugins-DqD7BDO1.js +169 -0
- package/dist/_chunks/profile-yXOiKk9R.js +672 -0
- package/dist/_chunks/projects-BwI2Uo0z.js +439 -0
- package/dist/_chunks/psql-BM-doe8e.js +64 -0
- package/dist/_chunks/realtime-BRqIyGR-.js +79 -0
- package/dist/_chunks/roles-B4w3YT_7.js +79 -0
- package/dist/_chunks/set_context-DbrwoImy.js +38 -0
- package/dist/_chunks/skills-DyiBXKS3.js +255 -0
- package/dist/_chunks/snapshots-RJBzW8GM.js +338 -0
- package/dist/_chunks/status-DV8Z2kw3.js +44 -0
- package/dist/_chunks/triggers-BrRo1z4b.js +302 -0
- package/dist/_chunks/user-dlOGXbX9.js +60 -0
- package/dist/_chunks/vpc_endpoints-CNb0YfMo.js +105 -0
- package/dist/auth_recovery.js +1 -1
- package/dist/commands/api.js +1 -388
- package/dist/commands/api_keys.js +1 -281
- package/dist/commands/ask.js +1 -155
- package/dist/commands/auth.js +1 -495
- package/dist/commands/bootstrap.js +1 -499
- package/dist/commands/branches.js +1 -460
- package/dist/commands/bucket.js +1 -409
- package/dist/commands/checkout.js +1 -348
- package/dist/commands/claim.js +1 -473
- package/dist/commands/config.js +1 -1113
- package/dist/commands/connection_string.js +1 -154
- package/dist/commands/credentials.js +1 -146
- package/dist/commands/data_api.js +1 -263
- package/dist/commands/databases.js +1 -84
- package/dist/commands/deploy.js +1 -33
- package/dist/commands/dev.js +1 -945
- package/dist/commands/diff.js +1 -178
- package/dist/commands/docs.js +1 -197
- package/dist/commands/env.js +1 -284
- package/dist/commands/feedback.js +1 -88
- package/dist/commands/functions.js +1 -464
- package/dist/commands/functions_new.js +1 -1
- package/dist/commands/git.js +1 -356
- package/dist/commands/index.js +45 -45
- package/dist/commands/init.js +1 -111
- package/dist/commands/inspect.js +1 -90
- package/dist/commands/ip_allow.js +1 -106
- package/dist/commands/link.js +1 -936
- package/dist/commands/logs.js +1 -291
- package/dist/commands/mcp.js +1 -253
- package/dist/commands/neon_auth.js +1 -822
- package/dist/commands/open.js +1 -33
- package/dist/commands/operations.js +1 -37
- package/dist/commands/orgs.js +1 -52
- package/dist/commands/plugins.js +1 -169
- package/dist/commands/profile.js +1 -672
- package/dist/commands/projects.js +1 -439
- package/dist/commands/psql.js +1 -64
- package/dist/commands/realtime.js +1 -79
- package/dist/commands/roles.js +1 -79
- package/dist/commands/set_context.js +1 -38
- package/dist/commands/skills.js +1 -255
- package/dist/commands/snapshots.js +1 -338
- package/dist/commands/status.js +1 -44
- package/dist/commands/subscribe.js +1 -1
- package/dist/commands/triggers.js +1 -302
- package/dist/commands/user.js +1 -60
- package/dist/commands/vpc_endpoints.js +1 -105
- package/dist/functions/registry.js +29 -151
- package/dist/functions/source.js +42 -2
- package/dist/index.js +1 -1
- package/dist/init/auth.js +3 -3
- package/dist/init/link.js +2 -2
- package/dist/init/run.js +3 -3
- package/dist/init/tooling.js +3 -3
- package/dist/mcp/mint.js +1 -1
- package/dist/test_utils/registry.js +1 -2
- package/package.json +8 -6
|
@@ -0,0 +1,945 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
+
import { log } from "../log.js";
|
|
3
|
+
import { isClaimableEnvTarget } from "../claimable/state.js";
|
|
4
|
+
import { getCliName } from "../utils/cli_name.js";
|
|
5
|
+
import { defaultDevEnvFiles, readEnvFile, resolveEnvFilePath } from "../env_file.js";
|
|
6
|
+
import { l as functionBaseUrlKey } from "./env-BpP3Dqpe.js";
|
|
7
|
+
import { branchIdResolve } from "../utils/enrichers.js";
|
|
8
|
+
import { bundleEntry } from "../utils/esbuild.js";
|
|
9
|
+
import { a as resolveDevEnv } from "./env-BNm69Qh8.js";
|
|
10
|
+
import { f as envFlag } from "./config-BH_qRHiG.js";
|
|
11
|
+
import { findConfigFunctionBySource, resolveFunctionsFromConfig } from "../dev/functions.js";
|
|
12
|
+
import { resolveWatchInputs } from "../dev/inputs.js";
|
|
13
|
+
import { existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import chalk from "chalk";
|
|
17
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
18
|
+
import { bundleAsIs, describeNativeFinding, findUndeclaredNativePackages, resolveEsbuildEntry } from "@neon/config-runtime";
|
|
19
|
+
import { once } from "node:events";
|
|
20
|
+
import { createServer } from "node:net";
|
|
21
|
+
//#region src/commands/dev.ts
|
|
22
|
+
var dev_exports = /* @__PURE__ */ __exportAll({
|
|
23
|
+
builder: () => builder,
|
|
24
|
+
captureShellEnv: () => captureShellEnv,
|
|
25
|
+
command: () => "dev",
|
|
26
|
+
describe: () => describe,
|
|
27
|
+
devBundleDir: () => devBundleDir,
|
|
28
|
+
devEnvContext: () => devEnvContext,
|
|
29
|
+
diffUnits: () => diffUnits,
|
|
30
|
+
formatEnvFiles: () => formatEnvFiles,
|
|
31
|
+
formatEnvSummary: () => formatEnvSummary,
|
|
32
|
+
handler: () => handler,
|
|
33
|
+
loadDevEnvFiles: () => loadDevEnvFiles,
|
|
34
|
+
localFunctionUrlEnv: () => localFunctionUrlEnv,
|
|
35
|
+
planFunctionsToUnits: () => planFunctionsToUnits,
|
|
36
|
+
reportDevCredential: () => reportDevCredential,
|
|
37
|
+
sourceFunctionUrlOverlay: () => sourceFunctionUrlOverlay
|
|
38
|
+
});
|
|
39
|
+
const command = "dev";
|
|
40
|
+
const describe = "Run Neon Functions locally with a dev server";
|
|
41
|
+
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({
|
|
42
|
+
source: {
|
|
43
|
+
describe: "Path to a single function entry module. Omit to serve every function declared in neon.ts.",
|
|
44
|
+
type: "string"
|
|
45
|
+
},
|
|
46
|
+
port: {
|
|
47
|
+
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.",
|
|
48
|
+
type: "number"
|
|
49
|
+
},
|
|
50
|
+
...envFlag
|
|
51
|
+
}).epilogue([
|
|
52
|
+
"",
|
|
53
|
+
"Functions run with the linked branch's Neon env injected, the same set the",
|
|
54
|
+
"deployed runtime gives them: DATABASE_URL, plus Neon Auth, the Data API,",
|
|
55
|
+
"object storage and the AI Gateway where the branch has them. A neon.ts in",
|
|
56
|
+
"this directory decides instead, exactly as it does for `env pull`.",
|
|
57
|
+
"",
|
|
58
|
+
"NEON_FUNCTION_<SLUG>_BASE_URL from `env pull` / `neon-env run` is the production",
|
|
59
|
+
"invocation URL. `dev` rewrites it to `http://localhost:<port>` for each function",
|
|
60
|
+
"this process is serving (every neon.ts function, or the matching slug when",
|
|
61
|
+
"`--source` names one).",
|
|
62
|
+
"",
|
|
63
|
+
"Before neon.ts is evaluated, `dev` loads the project's .env, then .env.local,",
|
|
64
|
+
"which overrides it, into the environment, so Function env values that read",
|
|
65
|
+
"process.env resolve. A variable already set in the shell wins. `--env <file>`",
|
|
66
|
+
"loads only that file instead. Functions inherit every loaded variable.",
|
|
67
|
+
"",
|
|
68
|
+
"`dev` reuses the branch credential behind the AI Gateway and object storage from",
|
|
69
|
+
"the file `env pull` writes, and never writes to it. With no such file it issues a",
|
|
70
|
+
"credential on every start, so run `env pull` once if you restart often."
|
|
71
|
+
].join("\n")).strict();
|
|
72
|
+
/**
|
|
73
|
+
* The resolver context for a `neon dev` run.
|
|
74
|
+
*
|
|
75
|
+
* Two things here are what make local dev match the deployed runtime, which injects a
|
|
76
|
+
* branch's whole env into a function:
|
|
77
|
+
*
|
|
78
|
+
* - **The AI Gateway is asked for**, like `env pull` does, because nothing can detect it.
|
|
79
|
+
* Without this, a function that works deployed fails locally with no `NEON_AI_GATEWAY_*`,
|
|
80
|
+
* which is exactly the difference `dev` exists to eliminate.
|
|
81
|
+
* - **The local dotenv file is layered in**, so the branch credential behind the gateway and
|
|
82
|
+
* object storage is *reused* rather than re-minted. `dev` writes no file of its own, so
|
|
83
|
+
* without a source of persisted secrets every start would mint a credential and leave the
|
|
84
|
+
* last one live — one orphan per restart. `neon-env run` already reads the file for this
|
|
85
|
+
* reason; `dev` was the one that didn't.
|
|
86
|
+
*/
|
|
87
|
+
const devEnvContext = (props, branchId, cwd, baseEnv = process.env) => {
|
|
88
|
+
const location = {
|
|
89
|
+
cwd,
|
|
90
|
+
...props.contextFile ? { contextFile: props.contextFile } : {}
|
|
91
|
+
};
|
|
92
|
+
const envFile = resolveEnvFilePath(location);
|
|
93
|
+
return {
|
|
94
|
+
...location,
|
|
95
|
+
implyAiGateway: true,
|
|
96
|
+
env: {
|
|
97
|
+
...baseEnv,
|
|
98
|
+
...existsSync(envFile) ? readEnvFile(envFile) : {}
|
|
99
|
+
},
|
|
100
|
+
...props.projectId ? { projectId: props.projectId } : {},
|
|
101
|
+
...branchId ? { branchId } : {},
|
|
102
|
+
...props.apiKey ? { apiKey: props.apiKey } : {},
|
|
103
|
+
...props.apiHost ? { apiHost: props.apiHost } : {},
|
|
104
|
+
...isClaimableEnvTarget({
|
|
105
|
+
apiHost: props.apiHost,
|
|
106
|
+
contextFile: props.contextFile ?? "",
|
|
107
|
+
configDir: props.configDir ?? ""
|
|
108
|
+
}) ? { claimable: true } : {}
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Say when this run obtained credential secrets it did not already hold.
|
|
113
|
+
*
|
|
114
|
+
* `dev` writes no file. With platform defaults, each start re-reveals the same secrets until
|
|
115
|
+
* `env pull` persists them. In a region with no defaults, each start mints another credential
|
|
116
|
+
* and cannot revoke the last one. The note names the one action that stops either loop.
|
|
117
|
+
*/
|
|
118
|
+
const reportDevCredential = (credential) => {
|
|
119
|
+
if (!credential?.issued) return;
|
|
120
|
+
if (credential.revoked.length > 0) {
|
|
121
|
+
log.info("Wrote credential secrets — %s changed. Revoked the leftover it replaced (%s).", credential.keys.join(", "), credential.revoked.join(", "));
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
log.warning(`Resolved credential secrets for this run (%s) without writing them to a file. Run \`${getCliName()} env pull\` once so restarts reuse them instead of revealing or minting again.`, credential.keys.join(", "));
|
|
125
|
+
};
|
|
126
|
+
const handler = async (props) => {
|
|
127
|
+
const shellEnv = captureShellEnv();
|
|
128
|
+
const envFiles = formatEnvFiles(loadDevEnvFiles(props, process.cwd()), process.cwd());
|
|
129
|
+
if (props.source !== void 0) {
|
|
130
|
+
await runSingleSource(props, shellEnv, envFiles);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
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.");
|
|
134
|
+
await runFromConfig(props, shellEnv, envFiles);
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Copy of the shell env before any dotenv file loads. PORT is read through `process.env`,
|
|
138
|
+
* which is case-insensitive on Windows, so a shell `Port` still sets the port. Exported for
|
|
139
|
+
* unit testing.
|
|
140
|
+
*/
|
|
141
|
+
const captureShellEnv = () => ({
|
|
142
|
+
...process.env,
|
|
143
|
+
PORT: process.env.PORT
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* Load dotenv files into process.env before neon.ts is evaluated: `--env <file>` alone
|
|
147
|
+
* (relative to cwd), else the project's {@link defaultDevEnvFiles}. A later file overrides
|
|
148
|
+
* an earlier one; a key set before loading (the shell) is never overridden.
|
|
149
|
+
*/
|
|
150
|
+
const loadDevEnvFiles = (props, cwd) => {
|
|
151
|
+
const files = props.env ? [resolve(cwd, props.env)] : defaultDevEnvFiles({
|
|
152
|
+
cwd,
|
|
153
|
+
...props.contextFile ? { contextFile: props.contextFile } : {}
|
|
154
|
+
});
|
|
155
|
+
const name = (key) => process.platform === "win32" ? key.toUpperCase() : key;
|
|
156
|
+
const preset = new Set(Object.keys(process.env).filter((key) => process.env[key] !== void 0).map(name));
|
|
157
|
+
return files.map((path) => {
|
|
158
|
+
const applied = [];
|
|
159
|
+
for (const [key, value] of Object.entries(readEnvFile(path))) {
|
|
160
|
+
if (preset.has(name(key))) continue;
|
|
161
|
+
process.env[key] = value;
|
|
162
|
+
applied.push(key);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
path,
|
|
166
|
+
applied
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* The banner's account of the dotenv files, in load order, e.g.
|
|
172
|
+
* `.env.local (4 vars) → .env (2 vars)`. Counts are the keys each file set. Exported for
|
|
173
|
+
* unit testing.
|
|
174
|
+
*/
|
|
175
|
+
const formatEnvFiles = (files, cwd) => {
|
|
176
|
+
if (files.length === 0) return "none (no .env.local or .env)";
|
|
177
|
+
return files.map(({ path, applied }) => {
|
|
178
|
+
const count = `${applied.length} var${applied.length === 1 ? "" : "s"}`;
|
|
179
|
+
return `${relative(cwd, path) || path} (${count})`;
|
|
180
|
+
}).join(" → ");
|
|
181
|
+
};
|
|
182
|
+
/** Single-function mode: serve exactly the `--source` path (legacy behavior). */
|
|
183
|
+
const runSingleSource = async (props, shellEnv, envFiles) => {
|
|
184
|
+
const source = resolve(process.cwd(), props.source);
|
|
185
|
+
if (!existsSync(source)) throw new Error(`Source file not found: ${source}`);
|
|
186
|
+
const branchId = await resolveBranchId(props);
|
|
187
|
+
const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd(), shellEnv));
|
|
188
|
+
reportDevCredential(credential);
|
|
189
|
+
const match = await findConfigFunctionBySource(process.cwd(), source, props.contextFile);
|
|
190
|
+
const { overlay, port } = await sourceFunctionUrlOverlay(match, props.port, shellEnv);
|
|
191
|
+
const unit = {
|
|
192
|
+
slug: null,
|
|
193
|
+
source,
|
|
194
|
+
bundleDir: devBundleDir(process.cwd()),
|
|
195
|
+
childEnv: buildChildEnv({
|
|
196
|
+
...neonEnv,
|
|
197
|
+
...overlay
|
|
198
|
+
}, port),
|
|
199
|
+
label: null,
|
|
200
|
+
envSummary: {
|
|
201
|
+
neon: Object.keys(neonEnv),
|
|
202
|
+
fn: []
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
await runSupervisor([unit], {
|
|
206
|
+
envFiles,
|
|
207
|
+
...skipped ? { envNote: skipped.reason } : {}
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Multi-function mode: serve every function declared in neon.ts. Requires a neon.ts
|
|
212
|
+
* (there is no single source to fall back on), one dev server per function.
|
|
213
|
+
*/
|
|
214
|
+
const runFromConfig = async (props, shellEnv, envFiles) => {
|
|
215
|
+
const branchId = await resolveBranchId(props);
|
|
216
|
+
const resolved = await resolveFunctionsFromConfig(process.cwd(), void 0, props.contextFile);
|
|
217
|
+
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 `functions`.");
|
|
218
|
+
const { configPath, functions } = resolved;
|
|
219
|
+
if (functions.length === 0) throw new Error("neon.ts has no functions to serve. Add at least one under `functions`, or pass --source <path>.");
|
|
220
|
+
const { vars: neonEnv, skipped, credential } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd(), shellEnv));
|
|
221
|
+
reportDevCredential(credential);
|
|
222
|
+
const units = await planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
|
|
223
|
+
const replan = async (searchBase, keepPorts) => {
|
|
224
|
+
const re = await resolveFunctionsFromConfig(process.cwd(), void 0, props.contextFile);
|
|
225
|
+
if (re === null) return null;
|
|
226
|
+
return planFunctionsToUnits(re.functions, neonEnv, searchBase, keepPorts);
|
|
227
|
+
};
|
|
228
|
+
await runSupervisor(units, {
|
|
229
|
+
reload: {
|
|
230
|
+
configPath,
|
|
231
|
+
replan
|
|
232
|
+
},
|
|
233
|
+
envFiles,
|
|
234
|
+
...skipped ? { envNote: skipped.reason } : {}
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
/** Ports are chosen before spawn so every sibling's localhost URL can be overlaid. */
|
|
238
|
+
const planFunctionsToUnits = async (functions, neonEnv, searchBase, keepPorts = /* @__PURE__ */ new Map()) => {
|
|
239
|
+
const reserved = /* @__PURE__ */ new Set();
|
|
240
|
+
const portBySlug = /* @__PURE__ */ new Map();
|
|
241
|
+
for (const fn of functions) {
|
|
242
|
+
if (fn.port === void 0) continue;
|
|
243
|
+
portBySlug.set(fn.slug, fn.port);
|
|
244
|
+
reserved.add(fn.port);
|
|
245
|
+
}
|
|
246
|
+
for (const fn of functions) {
|
|
247
|
+
if (portBySlug.has(fn.slug)) continue;
|
|
248
|
+
const kept = keepPorts.get(fn.slug);
|
|
249
|
+
if (kept !== void 0 && !reserved.has(kept)) {
|
|
250
|
+
portBySlug.set(fn.slug, kept);
|
|
251
|
+
reserved.add(kept);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
let searchOffset = 0;
|
|
255
|
+
for (const fn of functions) {
|
|
256
|
+
if (portBySlug.has(fn.slug)) continue;
|
|
257
|
+
const port = await pickFreePort(searchBase + searchOffset, reserved);
|
|
258
|
+
portBySlug.set(fn.slug, port);
|
|
259
|
+
reserved.add(port);
|
|
260
|
+
searchOffset = port - searchBase + 1;
|
|
261
|
+
}
|
|
262
|
+
const assigned = functions.map((fn) => {
|
|
263
|
+
const port = portBySlug.get(fn.slug);
|
|
264
|
+
if (port === void 0) throw new Error(`planFunctionsToUnits: no port for ${fn.slug}`);
|
|
265
|
+
return {
|
|
266
|
+
fn,
|
|
267
|
+
port
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
const overlay = localFunctionUrlEnv(assigned.map(({ fn, port }) => ({
|
|
271
|
+
slug: fn.slug,
|
|
272
|
+
port
|
|
273
|
+
})));
|
|
274
|
+
const branchEnv = {
|
|
275
|
+
...neonEnv,
|
|
276
|
+
...overlay
|
|
277
|
+
};
|
|
278
|
+
return assigned.map(({ fn, port }) => plannedToUnit(fn, branchEnv, {
|
|
279
|
+
mode: "explicit",
|
|
280
|
+
port
|
|
281
|
+
}, overlay));
|
|
282
|
+
};
|
|
283
|
+
/**
|
|
284
|
+
* Local invocation URLs for functions `neon dev` is serving, so siblings call each
|
|
285
|
+
* other on localhost instead of the production compute host.
|
|
286
|
+
*/
|
|
287
|
+
const localFunctionUrlEnv = (functions) => {
|
|
288
|
+
const out = {};
|
|
289
|
+
for (const fn of functions) out[functionBaseUrlKey(fn.slug)] = `http://localhost:${fn.port}`;
|
|
290
|
+
return out;
|
|
291
|
+
};
|
|
292
|
+
/**
|
|
293
|
+
* `--source` serves one file. If that file is a neon.ts function, rewrite only that
|
|
294
|
+
* slug to localhost — this process is not listening for the others.
|
|
295
|
+
*/
|
|
296
|
+
const sourceFunctionUrlOverlay = async (match, cliPort, shellEnv = process.env) => {
|
|
297
|
+
const portSpec = portFromProps(cliPort, shellEnv);
|
|
298
|
+
if (match === void 0) return {
|
|
299
|
+
overlay: {},
|
|
300
|
+
port: portSpec
|
|
301
|
+
};
|
|
302
|
+
const port = portSpec.mode === "explicit" ? portSpec.port : match.port ?? await pickFreePort(portSpec.from);
|
|
303
|
+
return {
|
|
304
|
+
overlay: localFunctionUrlEnv([{
|
|
305
|
+
slug: match.slug,
|
|
306
|
+
port
|
|
307
|
+
}]),
|
|
308
|
+
port: {
|
|
309
|
+
mode: "explicit",
|
|
310
|
+
port
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
};
|
|
314
|
+
const pickFreePort = (from, reserved = /* @__PURE__ */ new Set()) => new Promise((resolvePort, reject) => {
|
|
315
|
+
const tryPort = (port) => {
|
|
316
|
+
if (port > 65535) {
|
|
317
|
+
reject(/* @__PURE__ */ new Error(`No free port at or above ${from}`));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
if (reserved.has(port)) {
|
|
321
|
+
tryPort(port + 1);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const server = createServer();
|
|
325
|
+
server.once("error", (err) => {
|
|
326
|
+
if (err.code === "EADDRINUSE") {
|
|
327
|
+
tryPort(port + 1);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
reject(err);
|
|
331
|
+
});
|
|
332
|
+
server.listen(port, "127.0.0.1", () => {
|
|
333
|
+
const addr = server.address();
|
|
334
|
+
const bound = typeof addr === "object" && addr !== null ? addr.port : port;
|
|
335
|
+
server.close((closeErr) => {
|
|
336
|
+
if (closeErr) reject(closeErr);
|
|
337
|
+
else resolvePort(bound);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
tryPort(from);
|
|
342
|
+
});
|
|
343
|
+
/**
|
|
344
|
+
* Resolve the selected branch id from props, if any. Best-effort: a failure here only
|
|
345
|
+
* means env injection is skipped, so it never throws.
|
|
346
|
+
*/
|
|
347
|
+
const resolveBranchId = async (props) => {
|
|
348
|
+
if (!props.apiClient || !props.projectId) return void 0;
|
|
349
|
+
const branch = props.branch ?? props.id;
|
|
350
|
+
if (!branch) return void 0;
|
|
351
|
+
try {
|
|
352
|
+
return await branchIdResolve({
|
|
353
|
+
branch,
|
|
354
|
+
apiClient: props.apiClient,
|
|
355
|
+
projectId: props.projectId
|
|
356
|
+
});
|
|
357
|
+
} catch (err) {
|
|
358
|
+
log.debug("dev: could not resolve branch id: %s", err instanceof Error ? err.message : String(err));
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const DEFAULT_PORT_BASE = 8787;
|
|
363
|
+
const portFromProps = (port, shellEnv) => {
|
|
364
|
+
if (port !== void 0) return {
|
|
365
|
+
mode: "explicit",
|
|
366
|
+
port
|
|
367
|
+
};
|
|
368
|
+
if (shellEnv.PORT !== void 0 && shellEnv.PORT !== "") return {
|
|
369
|
+
mode: "explicit",
|
|
370
|
+
port: Number(shellEnv.PORT)
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
mode: "search",
|
|
374
|
+
from: DEFAULT_PORT_BASE
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
/**
|
|
378
|
+
* Where a locally-served function's bundle is written.
|
|
379
|
+
*
|
|
380
|
+
* The location inside the project's own `node_modules` is load-bearing, not a tidiness
|
|
381
|
+
* choice. A function's `externalPackages` are left unbundled, and Node resolves an unbundled
|
|
382
|
+
* import by walking up from the importing file — so from here it reaches the project's real
|
|
383
|
+
* `node_modules` and finds them, at the host architecture that can actually run locally.
|
|
384
|
+
* Moving this directory anywhere outside `node_modules` breaks `neon dev` for those
|
|
385
|
+
* functions with a `Cannot find module`, so the path is pinned by a test.
|
|
386
|
+
*/
|
|
387
|
+
const devBundleDir = (cwd, slug) => slug === void 0 ? join(cwd, "node_modules", ".neon-dev") : join(cwd, "node_modules", ".neon-dev", slug);
|
|
388
|
+
/**
|
|
389
|
+
* Translate a {@link PlannedFunction} into a {@link ServedUnit}. The bind port is
|
|
390
|
+
* already chosen by the caller so sibling localhost URLs can be overlaid first.
|
|
391
|
+
* Per-function neon.ts env layers over the shared branch env.
|
|
392
|
+
*/
|
|
393
|
+
const plannedToUnit = (fn, branchEnv, port, overlay = {}) => {
|
|
394
|
+
const childEnv = buildChildEnv({
|
|
395
|
+
...branchEnv,
|
|
396
|
+
...fn.env
|
|
397
|
+
}, port);
|
|
398
|
+
return {
|
|
399
|
+
slug: fn.slug,
|
|
400
|
+
source: fn.source,
|
|
401
|
+
bundleDir: devBundleDir(process.cwd(), fn.slug),
|
|
402
|
+
childEnv,
|
|
403
|
+
label: fn.slug,
|
|
404
|
+
envSummary: {
|
|
405
|
+
neon: Object.keys(branchEnv),
|
|
406
|
+
fn: Object.keys(fn.env)
|
|
407
|
+
},
|
|
408
|
+
...fn.externalPackages ? { externalPackages: fn.externalPackages } : {},
|
|
409
|
+
...fn.bundler !== void 0 ? { bundler: fn.bundler } : {},
|
|
410
|
+
configKey: JSON.stringify({
|
|
411
|
+
source: fn.source,
|
|
412
|
+
port: fn.port ?? null,
|
|
413
|
+
env: fn.env,
|
|
414
|
+
overlay,
|
|
415
|
+
externalPackages: fn.externalPackages ?? null,
|
|
416
|
+
bundler: typeof fn.bundler === "function" ? "custom" : fn.bundler ?? null
|
|
417
|
+
})
|
|
418
|
+
};
|
|
419
|
+
};
|
|
420
|
+
/**
|
|
421
|
+
* Build a child's env. Neon vars layer over the inherited environment (so the branch's
|
|
422
|
+
* DATABASE_URL wins over a stale inherited value); a function that loads its own `.env`
|
|
423
|
+
* at runtime still overrides them. The port spec is encoded for the runtime via
|
|
424
|
+
* NEON_DEV_PORT (explicit) or NEON_DEV_PORT_BASE (search).
|
|
425
|
+
*/
|
|
426
|
+
const buildChildEnv = (neonEnv, port) => {
|
|
427
|
+
const env = {
|
|
428
|
+
...process.env,
|
|
429
|
+
...neonEnv
|
|
430
|
+
};
|
|
431
|
+
delete env.NEON_DEV_PORT;
|
|
432
|
+
delete env.NEON_DEV_PORT_BASE;
|
|
433
|
+
if (port.mode === "explicit") env.NEON_DEV_PORT = String(port.port);
|
|
434
|
+
else if (port.mode === "search") env.NEON_DEV_PORT_BASE = String(port.from);
|
|
435
|
+
return env;
|
|
436
|
+
};
|
|
437
|
+
const READY_PATTERN = /neon-dev:ready (\d+)/;
|
|
438
|
+
/**
|
|
439
|
+
* Supervise one or more {@link ServedUnit}s: bundle + start each in its own child, watch
|
|
440
|
+
* its inputs for hot reload, and tear everything down cleanly on shutdown. Units are
|
|
441
|
+
* independent — one crashing or failing to start does not stop the others (it is shown
|
|
442
|
+
* as errored and recovered on the next edit). A single SIGINT/SIGTERM shuts all of them
|
|
443
|
+
* down, tree-killing each child so no descendant it spawned is orphaned.
|
|
444
|
+
*
|
|
445
|
+
* In config mode, `reload` lets the supervisor watch `neon.ts` and reconcile the live set
|
|
446
|
+
* of units when it changes: a newly-declared function is hot-added (its own child, watcher,
|
|
447
|
+
* and port) and a removed one is torn down — all without disturbing the functions that
|
|
448
|
+
* stayed the same. A function whose config (env/port/source) changed is restarted
|
|
449
|
+
* in place; siblings are untouched.
|
|
450
|
+
*/
|
|
451
|
+
const runSupervisor = async (units, options = {}) => {
|
|
452
|
+
const { reload, envNote, envFiles } = options;
|
|
453
|
+
const runtimePath = resolveRuntimePath();
|
|
454
|
+
let shuttingDown = false;
|
|
455
|
+
const running = units.map(makeRunningUnit);
|
|
456
|
+
const bundleAndStart = async (r) => {
|
|
457
|
+
let bundlePath;
|
|
458
|
+
try {
|
|
459
|
+
bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages, r.unit.slug ?? void 0, r.unit.bundler);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
r.status = "error";
|
|
462
|
+
logUnit(r.unit, chalk.red("bundle failed: ") + (err instanceof Error ? err.message : String(err)));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (r.watcher) await r.watcher.sync();
|
|
466
|
+
const next = spawnChild(r.unit, runtimePath, bundlePath);
|
|
467
|
+
r.child = next;
|
|
468
|
+
const ready = waitForReady(next);
|
|
469
|
+
pipeChildOutput(next, r.unit.label);
|
|
470
|
+
next.on("exit", (code, signal) => {
|
|
471
|
+
if (shuttingDown || r.child !== next) return;
|
|
472
|
+
if (signal) {
|
|
473
|
+
log.debug("runtime for %s exited via %s", r.unit.slug ?? "(source)", signal);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (code && code !== 0 && r.everReady) {
|
|
477
|
+
r.status = "error";
|
|
478
|
+
logUnit(r.unit, chalk.red(`exited with code ${code} (waiting for a change)`));
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const port = await ready;
|
|
482
|
+
if (port !== null) {
|
|
483
|
+
r.boundPort = port;
|
|
484
|
+
r.everReady = true;
|
|
485
|
+
r.status = "ready";
|
|
486
|
+
} else r.status = "error";
|
|
487
|
+
};
|
|
488
|
+
const restart = (r) => {
|
|
489
|
+
if (shuttingDown) return;
|
|
490
|
+
if (r.restartTimer) clearTimeout(r.restartTimer);
|
|
491
|
+
r.restartTimer = setTimeout(() => {
|
|
492
|
+
(async () => {
|
|
493
|
+
logUnit(r.unit, chalk.dim("change detected, restarting…"));
|
|
494
|
+
if (r.child) await killTree(r.child);
|
|
495
|
+
if (shuttingDown) return;
|
|
496
|
+
await bundleAndStart(r);
|
|
497
|
+
if (r.status === "ready") logUnit(r.unit, chalk.green("ready") + ` ${urlFor(r.boundPort)}`);
|
|
498
|
+
})();
|
|
499
|
+
}, 150);
|
|
500
|
+
};
|
|
501
|
+
const startUnit = async (r) => {
|
|
502
|
+
r.watcher = await startWatcher(r.unit, () => {
|
|
503
|
+
restart(r);
|
|
504
|
+
});
|
|
505
|
+
await bundleAndStart(r);
|
|
506
|
+
};
|
|
507
|
+
const stopUnit = async (r) => {
|
|
508
|
+
if (r.restartTimer) clearTimeout(r.restartTimer);
|
|
509
|
+
await r.watcher?.close();
|
|
510
|
+
if (r.child) await killTree(r.child);
|
|
511
|
+
rmSync(r.unit.bundleDir, {
|
|
512
|
+
recursive: true,
|
|
513
|
+
force: true
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
await Promise.all(running.map((r) => startUnit(r)));
|
|
517
|
+
if (running.every((r) => r.status === "error")) {
|
|
518
|
+
await Promise.all(running.map((r) => stopUnit(r)));
|
|
519
|
+
throw new Error("No function started. See the output above for details.");
|
|
520
|
+
}
|
|
521
|
+
printBanner(running, envNote, envFiles);
|
|
522
|
+
let configWatcher = null;
|
|
523
|
+
if (reload) {
|
|
524
|
+
const ops = {
|
|
525
|
+
isShuttingDown: () => shuttingDown,
|
|
526
|
+
startUnit,
|
|
527
|
+
stopUnit,
|
|
528
|
+
restartUnit: restart
|
|
529
|
+
};
|
|
530
|
+
let inFlight = null;
|
|
531
|
+
let pending = false;
|
|
532
|
+
const drive = () => {
|
|
533
|
+
if (inFlight) {
|
|
534
|
+
pending = true;
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
inFlight = (async () => {
|
|
538
|
+
do {
|
|
539
|
+
pending = false;
|
|
540
|
+
await reconcileOnce(running, reload.replan, ops);
|
|
541
|
+
} while (pending && !shuttingDown);
|
|
542
|
+
})().finally(() => {
|
|
543
|
+
inFlight = null;
|
|
544
|
+
});
|
|
545
|
+
};
|
|
546
|
+
configWatcher = await startConfigWatcher(reload.configPath, drive);
|
|
547
|
+
}
|
|
548
|
+
await new Promise((resolveRun) => {
|
|
549
|
+
const shutdown = () => {
|
|
550
|
+
if (shuttingDown) return;
|
|
551
|
+
shuttingDown = true;
|
|
552
|
+
(async () => {
|
|
553
|
+
await configWatcher?.close();
|
|
554
|
+
await Promise.all(running.map((r) => stopUnit(r)));
|
|
555
|
+
log.info(chalk.dim("Stopped the dev server."));
|
|
556
|
+
resolveRun();
|
|
557
|
+
})();
|
|
558
|
+
};
|
|
559
|
+
process.on("SIGINT", shutdown);
|
|
560
|
+
process.on("SIGTERM", shutdown);
|
|
561
|
+
});
|
|
562
|
+
};
|
|
563
|
+
const makeRunningUnit = (unit) => ({
|
|
564
|
+
unit,
|
|
565
|
+
child: null,
|
|
566
|
+
boundPort: null,
|
|
567
|
+
everReady: false,
|
|
568
|
+
restartTimer: null,
|
|
569
|
+
watcher: null,
|
|
570
|
+
status: "starting"
|
|
571
|
+
});
|
|
572
|
+
/**
|
|
573
|
+
* Pure slug-keyed diff of the live units against the freshly-resolved desired set:
|
|
574
|
+
* - a slug present now but not before → **add** (new child + watcher + port),
|
|
575
|
+
* - a slug gone from neon.ts → **remove** (torn down),
|
|
576
|
+
* - a slug whose config (source/port/env) changed → **restart** in place,
|
|
577
|
+
* - an unchanged slug → left out of the plan entirely (never touched).
|
|
578
|
+
* Functions that stayed the same never die, so an edit that only adds a function is
|
|
579
|
+
* non-disruptive. `desired === null` (neon.ts deleted) is treated as "no functions".
|
|
580
|
+
*/
|
|
581
|
+
const diffUnits = (running, desired) => {
|
|
582
|
+
const desiredBySlug = /* @__PURE__ */ new Map();
|
|
583
|
+
for (const u of desired ?? []) if (u.slug !== null) desiredBySlug.set(u.slug, u);
|
|
584
|
+
const runningBySlug = /* @__PURE__ */ new Map();
|
|
585
|
+
for (const r of running) if (r.unit.slug !== null) runningBySlug.set(r.unit.slug, r);
|
|
586
|
+
const plan = {
|
|
587
|
+
remove: [],
|
|
588
|
+
restart: [],
|
|
589
|
+
add: []
|
|
590
|
+
};
|
|
591
|
+
for (const [slug, r] of runningBySlug) if (!desiredBySlug.has(slug)) plan.remove.push(r);
|
|
592
|
+
for (const [slug, want] of desiredBySlug) {
|
|
593
|
+
const r = runningBySlug.get(slug);
|
|
594
|
+
if (r) {
|
|
595
|
+
if (r.unit.configKey !== want.configKey) {
|
|
596
|
+
r.unit = want;
|
|
597
|
+
plan.restart.push(r);
|
|
598
|
+
}
|
|
599
|
+
} else plan.add.push(want);
|
|
600
|
+
}
|
|
601
|
+
return plan;
|
|
602
|
+
};
|
|
603
|
+
/**
|
|
604
|
+
* Run one reconcile: re-resolve neon.ts (ignoring the change with a clear message if it no
|
|
605
|
+
* longer loads), {@link diffUnits} against the live set, then apply the plan — tearing down
|
|
606
|
+
* removed functions, restarting changed ones in place, and hot-adding new ones. Mutates
|
|
607
|
+
* `running` in place so the surrounding supervisor sees the converged set.
|
|
608
|
+
*/
|
|
609
|
+
const reconcileOnce = async (running, replan, ops) => {
|
|
610
|
+
if (ops.isShuttingDown()) return;
|
|
611
|
+
let desired;
|
|
612
|
+
try {
|
|
613
|
+
desired = await replan(nextSearchBase(running), keepPortsFromRunning(running));
|
|
614
|
+
} catch (err) {
|
|
615
|
+
log.warning("neon.ts change ignored: " + (err instanceof Error ? err.message : String(err)) + chalk.dim(" (fix it and save again)"));
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
if (ops.isShuttingDown()) return;
|
|
619
|
+
const plan = diffUnits(running, desired);
|
|
620
|
+
for (const r of plan.remove) {
|
|
621
|
+
logUnit(r.unit, chalk.dim("removed from neon.ts, stopping…"));
|
|
622
|
+
await ops.stopUnit(r);
|
|
623
|
+
const idx = running.indexOf(r);
|
|
624
|
+
if (idx !== -1) running.splice(idx, 1);
|
|
625
|
+
}
|
|
626
|
+
for (const r of plan.restart) ops.restartUnit(r);
|
|
627
|
+
if (plan.add.length > 0) {
|
|
628
|
+
const added = plan.add.map((unit) => {
|
|
629
|
+
const r = makeRunningUnit(unit);
|
|
630
|
+
running.push(r);
|
|
631
|
+
logUnit(unit, chalk.dim("added in neon.ts, starting…"));
|
|
632
|
+
return r;
|
|
633
|
+
});
|
|
634
|
+
await Promise.all(added.map((r) => ops.startUnit(r)));
|
|
635
|
+
for (const r of added) if (r.status === "ready") {
|
|
636
|
+
const env = formatEnvSummary(r.unit.envSummary);
|
|
637
|
+
logUnit(r.unit, chalk.green("ready") + ` ${urlFor(r.boundPort)}` + (env ? chalk.dim(` ${env}`) : ""));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const keepPortsFromRunning = (running) => {
|
|
642
|
+
const keep = /* @__PURE__ */ new Map();
|
|
643
|
+
for (const r of running) {
|
|
644
|
+
if (r.unit.slug === null) continue;
|
|
645
|
+
const bound = r.boundPort;
|
|
646
|
+
if (bound !== null) {
|
|
647
|
+
keep.set(r.unit.slug, bound);
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
const pending = Number(r.unit.childEnv.NEON_DEV_PORT);
|
|
651
|
+
if (Number.isFinite(pending) && pending > 0) keep.set(r.unit.slug, pending);
|
|
652
|
+
}
|
|
653
|
+
return keep;
|
|
654
|
+
};
|
|
655
|
+
/**
|
|
656
|
+
* Start pickFreePort above every port live units already bound, so a hot-added
|
|
657
|
+
* function is not probed on a port a sibling is serving.
|
|
658
|
+
*/
|
|
659
|
+
const nextSearchBase = (running) => {
|
|
660
|
+
let max = 8786;
|
|
661
|
+
for (const r of running) if (r.boundPort !== null && r.boundPort > max) max = r.boundPort;
|
|
662
|
+
return max + 1;
|
|
663
|
+
};
|
|
664
|
+
/**
|
|
665
|
+
* Spawn the child for a unit: the bundled output run directly under `node`.
|
|
666
|
+
*
|
|
667
|
+
* Spawned detached (own process group) so killTree can reap the whole group.
|
|
668
|
+
*/
|
|
669
|
+
const spawnChild = (unit, runtimePath, bundlePath) => {
|
|
670
|
+
return spawn(process.execPath, [runtimePath, bundlePath], {
|
|
671
|
+
stdio: [
|
|
672
|
+
"ignore",
|
|
673
|
+
"pipe",
|
|
674
|
+
"pipe"
|
|
675
|
+
],
|
|
676
|
+
env: unit.childEnv,
|
|
677
|
+
detached: true
|
|
678
|
+
});
|
|
679
|
+
};
|
|
680
|
+
/**
|
|
681
|
+
* Findings already reported for a served unit, so an advisory that cannot change between
|
|
682
|
+
* saves is not reprinted on every one. A dev session on a project with a standing false
|
|
683
|
+
* positive would otherwise repeat the whole block for its lifetime.
|
|
684
|
+
*/
|
|
685
|
+
const reportedFindings = /* @__PURE__ */ new Map();
|
|
686
|
+
const writeBundle = async (source, bundleDir, externalPackages, label, bundler) => {
|
|
687
|
+
if (bundler !== void 0 && bundler !== "esbuild") {
|
|
688
|
+
const files = await bundleForDev(source, label, bundler);
|
|
689
|
+
return writeBundleFiles(bundleDir, files);
|
|
690
|
+
}
|
|
691
|
+
const entry = await resolveEsbuildEntry(source);
|
|
692
|
+
const { files, metafile, warnings } = await bundleEntry(entry, { ...externalPackages ? { externalPackages } : {} });
|
|
693
|
+
for (const warning of warnings) log.warning(warning);
|
|
694
|
+
const findings = findUndeclaredNativePackages({
|
|
695
|
+
metafile,
|
|
696
|
+
declared: externalPackages ?? [],
|
|
697
|
+
projectDir: dirname(entry)
|
|
698
|
+
});
|
|
699
|
+
const signature = findings.map((f) => f.name).join(",");
|
|
700
|
+
if (reportedFindings.get(source) !== signature) {
|
|
701
|
+
reportedFindings.set(source, signature);
|
|
702
|
+
for (const finding of findings) log.warning(describeNativeFinding(label ?? basename(source), finding));
|
|
703
|
+
}
|
|
704
|
+
return writeBundleFiles(bundleDir, files);
|
|
705
|
+
};
|
|
706
|
+
const bundleForDev = async (source, label, bundler) => {
|
|
707
|
+
const fn = {
|
|
708
|
+
slug: label ?? basename(source),
|
|
709
|
+
name: label ?? basename(source),
|
|
710
|
+
source,
|
|
711
|
+
env: {},
|
|
712
|
+
runtime: "nodejs24",
|
|
713
|
+
bundler
|
|
714
|
+
};
|
|
715
|
+
return bundler === "none" ? bundleAsIs(fn) : bundler(fn);
|
|
716
|
+
};
|
|
717
|
+
const writeBundleFiles = (bundleDir, files) => {
|
|
718
|
+
rmSync(bundleDir, {
|
|
719
|
+
recursive: true,
|
|
720
|
+
force: true
|
|
721
|
+
});
|
|
722
|
+
mkdirSync(bundleDir, { recursive: true });
|
|
723
|
+
for (const [name, contents] of Object.entries(files)) {
|
|
724
|
+
const dest = join(bundleDir, name);
|
|
725
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
726
|
+
writeFileSync(dest, contents);
|
|
727
|
+
}
|
|
728
|
+
const entry = ["index.mjs", "index.js"].find((name) => name in files);
|
|
729
|
+
return join(bundleDir, entry ?? "index.mjs");
|
|
730
|
+
};
|
|
731
|
+
const urlFor = (port) => port === null ? chalk.red("not running") : `http://localhost:${port}`;
|
|
732
|
+
const waitForReady = (child) => new Promise((resolveReady) => {
|
|
733
|
+
let settled = false;
|
|
734
|
+
let buffer = "";
|
|
735
|
+
const onData = (chunk) => {
|
|
736
|
+
buffer += chunk.toString();
|
|
737
|
+
const match = READY_PATTERN.exec(buffer);
|
|
738
|
+
if (match && !settled) {
|
|
739
|
+
settled = true;
|
|
740
|
+
child.stdout?.off("data", onData);
|
|
741
|
+
resolveReady(Number(match[1]));
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
child.stdout?.on("data", onData);
|
|
745
|
+
child.once("exit", () => {
|
|
746
|
+
if (!settled) {
|
|
747
|
+
settled = true;
|
|
748
|
+
resolveReady(null);
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
});
|
|
752
|
+
/**
|
|
753
|
+
* Forward the child's stdout/stderr to the parent, swallowing the machine-readable
|
|
754
|
+
* `neon-dev:ready` line. When `label` is set (multi-function mode), every line is
|
|
755
|
+
* prefixed with `[slug]` so concurrent servers' output stays readable.
|
|
756
|
+
*/
|
|
757
|
+
const pipeChildOutput = (child, label) => {
|
|
758
|
+
const prefix = label ? chalk.dim(`[${label}] `) : "";
|
|
759
|
+
const forward = (stream) => {
|
|
760
|
+
let buffer = "";
|
|
761
|
+
child[stream]?.on("data", (chunk) => {
|
|
762
|
+
buffer += chunk.toString();
|
|
763
|
+
const lines = buffer.split("\n");
|
|
764
|
+
buffer = lines.pop() ?? "";
|
|
765
|
+
for (const line of lines) {
|
|
766
|
+
if (READY_PATTERN.test(line)) continue;
|
|
767
|
+
process[stream].write(`${prefix}${line}\n`);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
};
|
|
771
|
+
forward("stdout");
|
|
772
|
+
forward("stderr");
|
|
773
|
+
};
|
|
774
|
+
const printBanner = (running, envNote, envFiles) => {
|
|
775
|
+
log.info("");
|
|
776
|
+
log.info(chalk.green.bold(" Neon Functions dev server"));
|
|
777
|
+
log.info("");
|
|
778
|
+
if (envFiles) {
|
|
779
|
+
log.info(` ${chalk.dim("env files".padEnd(20))} ${chalk.dim(envFiles)}`);
|
|
780
|
+
log.info("");
|
|
781
|
+
}
|
|
782
|
+
for (const r of running) {
|
|
783
|
+
const name = r.unit.label ?? "function";
|
|
784
|
+
const url = urlFor(r.boundPort);
|
|
785
|
+
log.info(` ${chalk.dim(name.padEnd(20))} ${url}`);
|
|
786
|
+
const env = formatEnvSummary(r.unit.envSummary);
|
|
787
|
+
if (env) log.info(` ${" ".repeat(20)} ${chalk.dim(env)}`);
|
|
788
|
+
}
|
|
789
|
+
if (envNote) {
|
|
790
|
+
log.info("");
|
|
791
|
+
log.info(` ${chalk.yellow("!")} ${chalk.dim(`Neon env: ${envNote}`)}`);
|
|
792
|
+
}
|
|
793
|
+
log.info("");
|
|
794
|
+
};
|
|
795
|
+
/**
|
|
796
|
+
* Render a unit's injected env into one transparent line for the banner, e.g.
|
|
797
|
+
* `env: DATABASE_URL, DATABASE_URL_UNPOOLED · neon.ts: RESEND_API_KEY`. Var **names** only
|
|
798
|
+
* (never values — they're secrets). Returns `''` when nothing is injected, so the caller can
|
|
799
|
+
* skip the line. Exported for unit testing.
|
|
800
|
+
*/
|
|
801
|
+
const formatEnvSummary = (summary) => {
|
|
802
|
+
if (!summary) return "";
|
|
803
|
+
const parts = [];
|
|
804
|
+
if (summary.neon.length > 0) parts.push(`env: ${[...summary.neon].sort().join(", ")}`);
|
|
805
|
+
if (summary.fn.length > 0) parts.push(`neon.ts: ${[...summary.fn].sort().join(", ")}`);
|
|
806
|
+
return parts.join(" · ");
|
|
807
|
+
};
|
|
808
|
+
const logUnit = (unit, message) => {
|
|
809
|
+
const prefix = unit.label ? chalk.dim(`[${unit.label}] `) : "";
|
|
810
|
+
log.info(`${prefix}${message}`);
|
|
811
|
+
};
|
|
812
|
+
const startWatcher = async (unit, restart) => {
|
|
813
|
+
const { default: chokidar } = await import("chokidar");
|
|
814
|
+
if (unit.bundler !== void 0 && unit.bundler !== "esbuild") {
|
|
815
|
+
const dir = statSync(unit.source, { throwIfNoEntry: false })?.isDirectory() === true ? unit.source : dirname(unit.source);
|
|
816
|
+
return startDirectoryWatcher(chokidar, dir, restart, { ignoreDist: false });
|
|
817
|
+
}
|
|
818
|
+
let entry = unit.source;
|
|
819
|
+
try {
|
|
820
|
+
entry = await resolveEsbuildEntry(unit.source);
|
|
821
|
+
} catch {}
|
|
822
|
+
const initialInputs = await resolveWatchInputs(entry);
|
|
823
|
+
if (initialInputs === null) return startDirectoryWatcher(chokidar, entry, restart, { ignoreDist: true });
|
|
824
|
+
return startInputWatcher(chokidar, entry, initialInputs, restart);
|
|
825
|
+
};
|
|
826
|
+
/**
|
|
827
|
+
* Watch the neon.ts file itself for changes, firing `onChange` on every save. Used by the
|
|
828
|
+
* supervisor (config mode only) to hot-add/remove/restart functions when the declared set
|
|
829
|
+
* changes. We watch the single config file rather than its import graph: editing neon.ts to
|
|
830
|
+
* add or remove a function is the case that matters, and a plain file watch is robust where
|
|
831
|
+
* the esbuild-based input resolution (built for function bundles) is not a fit for a
|
|
832
|
+
* jiti-loaded config.
|
|
833
|
+
*/
|
|
834
|
+
const startConfigWatcher = async (configPath, onChange) => {
|
|
835
|
+
const { default: chokidar } = await import("chokidar");
|
|
836
|
+
const watcher = chokidar.watch(configPath, { ignoreInitial: true });
|
|
837
|
+
await once(watcher, "ready");
|
|
838
|
+
watcher.on("all", () => {
|
|
839
|
+
onChange();
|
|
840
|
+
});
|
|
841
|
+
return {
|
|
842
|
+
sync: () => Promise.resolve(),
|
|
843
|
+
close: () => watcher.close()
|
|
844
|
+
};
|
|
845
|
+
};
|
|
846
|
+
const startInputWatcher = async (chokidar, source, initialInputs, restart) => {
|
|
847
|
+
const watched = /* @__PURE__ */ new Set([source, ...initialInputs]);
|
|
848
|
+
const watcher = chokidar.watch([...watched], { ignoreInitial: true });
|
|
849
|
+
await once(watcher, "ready");
|
|
850
|
+
watcher.on("all", () => {
|
|
851
|
+
restart();
|
|
852
|
+
});
|
|
853
|
+
const sync = async () => {
|
|
854
|
+
const next = await resolveWatchInputs(source);
|
|
855
|
+
if (next === null) return;
|
|
856
|
+
const desired = /* @__PURE__ */ new Set([source, ...next]);
|
|
857
|
+
for (const file of desired) if (!watched.has(file)) {
|
|
858
|
+
watcher.add(file);
|
|
859
|
+
watched.add(file);
|
|
860
|
+
}
|
|
861
|
+
for (const file of watched) if (!desired.has(file)) {
|
|
862
|
+
watcher.unwatch(file);
|
|
863
|
+
watched.delete(file);
|
|
864
|
+
}
|
|
865
|
+
};
|
|
866
|
+
return {
|
|
867
|
+
sync,
|
|
868
|
+
close: () => watcher.close()
|
|
869
|
+
};
|
|
870
|
+
};
|
|
871
|
+
const startDirectoryWatcher = async (chokidar, source, restart, options) => {
|
|
872
|
+
const watchedDir = statSync(source, { throwIfNoEntry: false })?.isDirectory() ? source : dirname(source);
|
|
873
|
+
const isIgnored = (p) => {
|
|
874
|
+
const rel = relative(watchedDir, p);
|
|
875
|
+
if (rel === "" || rel === ".") return false;
|
|
876
|
+
if (rel.startsWith("..")) return false;
|
|
877
|
+
const segments = rel.split(/[/\\]/);
|
|
878
|
+
if (segments.includes("node_modules") || segments.includes(".git")) return true;
|
|
879
|
+
return options.ignoreDist && segments.includes("dist");
|
|
880
|
+
};
|
|
881
|
+
const watcher = chokidar.watch(watchedDir, {
|
|
882
|
+
ignoreInitial: true,
|
|
883
|
+
ignored: (path) => isIgnored(path)
|
|
884
|
+
});
|
|
885
|
+
await once(watcher, "ready");
|
|
886
|
+
watcher.on("all", () => {
|
|
887
|
+
restart();
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
sync: () => Promise.resolve(),
|
|
891
|
+
close: () => watcher.close()
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
/**
|
|
895
|
+
* Terminate a child and every descendant it spawned. The child is started `detached`, so
|
|
896
|
+
* on POSIX it leads its own process group and a negative-PID signal reaps the group
|
|
897
|
+
* (covering the runtime and anything it spawned). On Windows there are no POSIX groups, so we
|
|
898
|
+
* shell out to `taskkill /T` to kill the tree. Escalates SIGTERM -> SIGKILL after 2s.
|
|
899
|
+
*/
|
|
900
|
+
const killTree = (child) => {
|
|
901
|
+
if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
|
|
902
|
+
const pid = child.pid;
|
|
903
|
+
return new Promise((resolveKill) => {
|
|
904
|
+
const timeout = setTimeout(() => {
|
|
905
|
+
forceKill(child, pid);
|
|
906
|
+
}, 2e3);
|
|
907
|
+
child.once("exit", () => {
|
|
908
|
+
clearTimeout(timeout);
|
|
909
|
+
resolveKill();
|
|
910
|
+
});
|
|
911
|
+
if (pid !== void 0 && process.platform !== "win32") try {
|
|
912
|
+
process.kill(-pid, "SIGTERM");
|
|
913
|
+
return;
|
|
914
|
+
} catch {}
|
|
915
|
+
child.kill("SIGTERM");
|
|
916
|
+
});
|
|
917
|
+
};
|
|
918
|
+
const forceKill = (child, pid) => {
|
|
919
|
+
if (pid === void 0) {
|
|
920
|
+
child.kill("SIGKILL");
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (process.platform === "win32") {
|
|
924
|
+
spawnSync("taskkill", [
|
|
925
|
+
"/pid",
|
|
926
|
+
String(pid),
|
|
927
|
+
"/T",
|
|
928
|
+
"/F"
|
|
929
|
+
]);
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
try {
|
|
933
|
+
process.kill(-pid, "SIGKILL");
|
|
934
|
+
} catch {
|
|
935
|
+
child.kill("SIGKILL");
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const resolveRuntimePath = () => {
|
|
939
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
940
|
+
const candidate = join(here, "..", "dev", "runtime.js");
|
|
941
|
+
if (!existsSync(candidate)) throw new Error(`Could not locate the dev runtime at ${candidate}`);
|
|
942
|
+
return candidate;
|
|
943
|
+
};
|
|
944
|
+
//#endregion
|
|
945
|
+
export { devBundleDir as a, diffUnits as c, handler as d, loadDevEnvFiles as f, sourceFunctionUrlOverlay as g, reportDevCredential as h, describe as i, formatEnvFiles as l, planFunctionsToUnits as m, captureShellEnv as n, devEnvContext as o, localFunctionUrlEnv as p, command as r, dev_exports as s, builder as t, formatEnvSummary as u };
|