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.
- package/README.md +54 -0
- package/dist/_shared/auth_selection.js +76 -79
- package/dist/_shared/credentials.js +166 -187
- package/dist/_shared/env-core/env.js +395 -0
- package/dist/_shared/env-core/reuse-secrets.js +179 -0
- package/dist/_shared/paths.js +129 -126
- package/dist/_shared/profiles.js +192 -242
- package/dist/_shared/secure_file.js +36 -38
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/analytics.js +163 -207
- package/dist/api.js +815 -758
- package/dist/auth.js +121 -141
- package/dist/auth_context.js +39 -53
- package/dist/cli.js +4 -7
- package/dist/commands/api.js +220 -250
- package/dist/commands/api_keys.js +251 -314
- package/dist/commands/auth.js +283 -328
- package/dist/commands/bootstrap.js +372 -437
- package/dist/commands/branches.js +304 -455
- package/dist/commands/bucket.js +374 -514
- package/dist/commands/checkout.js +213 -298
- package/dist/commands/config.js +575 -658
- package/dist/commands/connection_string.js +137 -165
- package/dist/commands/data_api.js +238 -260
- package/dist/commands/databases.js +67 -76
- package/dist/commands/deploy.js +31 -25
- package/dist/commands/dev.js +642 -681
- package/dist/commands/diff.js +156 -200
- package/dist/commands/env.js +243 -303
- package/dist/commands/functions.js +275 -341
- package/dist/commands/index.js +70 -65
- package/dist/commands/init.js +84 -119
- package/dist/commands/inspect.js +55 -55
- package/dist/commands/ip_allow.js +88 -120
- package/dist/commands/link.js +874 -1019
- package/dist/commands/logs.js +291 -0
- package/dist/commands/neon_auth.js +725 -933
- package/dist/commands/operations.js +34 -25
- package/dist/commands/orgs.js +28 -18
- package/dist/commands/profile.js +614 -845
- package/dist/commands/projects.js +313 -373
- package/dist/commands/psql.js +60 -58
- package/dist/commands/roles.js +55 -58
- package/dist/commands/schema_diff.js +87 -131
- package/dist/commands/set_context.js +34 -26
- package/dist/commands/snapshots.js +288 -413
- package/dist/commands/status.js +41 -37
- package/dist/commands/user.js +21 -10
- package/dist/commands/vpc_endpoints.js +85 -113
- package/dist/config.js +7 -6
- package/dist/config_format.js +50 -66
- package/dist/config_template.js +128 -157
- package/dist/context.js +183 -235
- package/dist/current_branch_fast_path.js +40 -49
- package/dist/dev/env.js +313 -394
- package/dist/dev/functions.js +54 -64
- package/dist/dev/inputs.js +46 -58
- package/dist/dev/runtime.js +135 -164
- package/dist/dev/websocket.js +766 -959
- package/dist/env.js +27 -33
- package/dist/env_file.js +118 -132
- package/dist/env_services.js +36 -38
- package/dist/errors.js +57 -68
- package/dist/functions_api.js +45 -43
- package/dist/help.js +189 -140
- package/dist/index.js +182 -257
- package/dist/init/agents.js +137 -118
- package/dist/init/auth.js +58 -68
- package/dist/init/bootstrap.js +325 -396
- package/dist/init/build_config.js +4 -2
- package/dist/init/detect_agent.js +56 -101
- package/dist/init/editors.js +35 -52
- package/dist/init/enrich_output.js +51 -66
- package/dist/init/extension.js +134 -171
- package/dist/init/inspect.js +179 -266
- package/dist/init/interactive.js +510 -622
- package/dist/init/neonctl.js +117 -168
- package/dist/init/orchestrate.js +157 -173
- package/dist/init/phases/auth.js +188 -202
- package/dist/init/phases/cleanup.js +23 -23
- package/dist/init/phases/db.js +251 -277
- package/dist/init/phases/getting_started.js +213 -223
- package/dist/init/phases/mcp.js +174 -224
- package/dist/init/phases/migrations.js +247 -248
- package/dist/init/phases/neon_auth.js +114 -133
- package/dist/init/phases/setup.js +546 -703
- package/dist/init/phases/skills.js +75 -86
- package/dist/init/phases/status.js +72 -67
- package/dist/init/resolve_context.js +102 -99
- package/dist/init/route_command.js +91 -98
- package/dist/init/skills.js +174 -218
- package/dist/init/vsix.js +77 -99
- package/dist/log.js +17 -16
- package/dist/neon_services.js +104 -129
- package/dist/parameters.gen.js +481 -471
- package/dist/pkg.js +17 -19
- package/dist/profile_keys.js +44 -47
- package/dist/psql/cli.js +44 -47
- package/dist/psql/command/cmd_cond.js +231 -406
- package/dist/psql/command/cmd_connect.js +557 -764
- package/dist/psql/command/cmd_copy.js +727 -983
- package/dist/psql/command/cmd_describe.js +1499 -1688
- package/dist/psql/command/cmd_format.js +733 -905
- package/dist/psql/command/cmd_io.js +1293 -2082
- package/dist/psql/command/cmd_lo.js +297 -359
- package/dist/psql/command/cmd_meta.js +727 -878
- package/dist/psql/command/cmd_misc.js +138 -172
- package/dist/psql/command/cmd_pipeline.js +547 -1099
- package/dist/psql/command/cmd_restrict.js +119 -155
- package/dist/psql/command/cmd_show.js +529 -688
- package/dist/psql/command/dispatch.js +261 -325
- package/dist/psql/command/inputQueue.js +35 -33
- package/dist/psql/command/shared.js +49 -63
- package/dist/psql/complete/filenames.js +90 -133
- package/dist/psql/complete/index.js +59 -97
- package/dist/psql/complete/matcher.js +236 -300
- package/dist/psql/complete/psqlVars.js +218 -223
- package/dist/psql/complete/queries.js +159 -177
- package/dist/psql/complete/rules.js +1493 -2299
- package/dist/psql/core/common.js +762 -1180
- package/dist/psql/core/help.js +456 -546
- package/dist/psql/core/mainloop.js +692 -1302
- package/dist/psql/core/prompt.js +391 -408
- package/dist/psql/core/settings.js +429 -644
- package/dist/psql/core/sqlHelp.js +480 -554
- package/dist/psql/core/startup.js +626 -815
- package/dist/psql/core/syncVars.js +67 -110
- package/dist/psql/core/variables.js +156 -278
- package/dist/psql/describe/formatters.js +884 -1285
- package/dist/psql/describe/processNamePattern.js +173 -260
- package/dist/psql/describe/queries.js +1368 -2403
- package/dist/psql/describe/versionGate.js +32 -41
- package/dist/psql/index.js +1414 -1927
- package/dist/psql/io/history.js +232 -271
- package/dist/psql/io/input.js +103 -108
- package/dist/psql/io/lineEditor/buffer.js +238 -319
- package/dist/psql/io/lineEditor/complete.js +135 -213
- package/dist/psql/io/lineEditor/filename.js +139 -148
- package/dist/psql/io/lineEditor/index.js +653 -870
- package/dist/psql/io/lineEditor/keymap.js +544 -702
- package/dist/psql/io/lineEditor/vt100.js +294 -341
- package/dist/psql/io/pgpass.js +158 -187
- package/dist/psql/io/pgservice.js +146 -183
- package/dist/psql/io/psqlrc.js +328 -403
- package/dist/psql/print/aligned.js +1020 -1683
- package/dist/psql/print/asciidoc.js +180 -214
- package/dist/psql/print/crosstab.js +281 -442
- package/dist/psql/print/csv.js +48 -70
- package/dist/psql/print/html.js +195 -226
- package/dist/psql/print/json.js +75 -88
- package/dist/psql/print/latex.js +291 -364
- package/dist/psql/print/pager.js +171 -242
- package/dist/psql/print/troff.js +194 -226
- package/dist/psql/print/unaligned.js +69 -95
- package/dist/psql/print/units.js +167 -169
- package/dist/psql/scanner/slash.js +428 -483
- package/dist/psql/scanner/sql.js +445 -889
- package/dist/psql/scanner/stringutils.js +309 -379
- package/dist/psql/types/index.js +2 -7
- package/dist/psql/types/scanner.js +25 -22
- package/dist/psql/wire/connection.js +2042 -2803
- package/dist/psql/wire/copy.js +84 -100
- package/dist/psql/wire/notify.js +39 -59
- package/dist/psql/wire/pipeline.js +305 -518
- package/dist/psql/wire/protocol.js +349 -417
- package/dist/psql/wire/sasl.js +180 -265
- package/dist/psql/wire/tls.js +400 -561
- package/dist/storage_api.js +115 -129
- package/dist/test_utils/fixtures.js +94 -113
- package/dist/test_utils/oauth_server.js +10 -7
- package/dist/test_utils/project_dir.js +33 -0
- package/dist/utils/ai_gateway_notice.js +131 -162
- package/dist/utils/api_enums.js +21 -28
- package/dist/utils/auth.js +10 -4
- package/dist/utils/branch_notice.js +20 -19
- package/dist/utils/branch_picker.js +83 -89
- package/dist/utils/cli_name.js +15 -12
- package/dist/utils/compute_units.js +20 -27
- package/dist/utils/config_diff.js +127 -158
- package/dist/utils/enrichers.js +95 -148
- package/dist/utils/esbuild.js +133 -147
- package/dist/utils/flags.js +35 -47
- package/dist/utils/formats.js +8 -15
- package/dist/utils/git_diff.js +69 -80
- package/dist/utils/inspect_db.js +101 -143
- package/dist/utils/inspect_queries.js +179 -142
- package/dist/utils/middlewares.js +37 -44
- package/dist/utils/openapi.js +87 -99
- package/dist/utils/package_manager.js +312 -110
- package/dist/utils/point_in_time.js +49 -53
- package/dist/utils/psql.js +89 -106
- package/dist/utils/service_picker.js +55 -58
- package/dist/utils/string.js +5 -5
- package/dist/utils/ui.js +38 -55
- package/dist/utils/write_sync.js +26 -35
- package/dist/utils/zip.js +4 -3
- package/dist/writer.js +67 -87
- package/package.json +9 -7
package/dist/commands/dev.js
CHANGED
|
@@ -1,757 +1,718 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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
|
-
|
|
631
|
-
|
|
594
|
+
const prefix = unit.label ? chalk.dim(`[${unit.label}] `) : "";
|
|
595
|
+
log.info(`${prefix}${message}`);
|
|
632
596
|
};
|
|
633
597
|
const startWatcher = async (source, restart) => {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
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
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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 };
|