neon 2.46.0 → 3.0.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.
@@ -1,13 +1,16 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { once } from "node:events";
3
3
  import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { basename, dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { describeNativeFinding, findUndeclaredNativePackages, } from "@neon/config-runtime";
6
7
  import chalk from "chalk";
7
8
  import { resolveDevEnv } from "../dev/env.js";
8
9
  import { resolveFunctionsFromConfig, } from "../dev/functions.js";
9
10
  import { resolveWatchInputs } from "../dev/inputs.js";
11
+ import { readEnvFile, resolveEnvFilePath } from "../env_file.js";
10
12
  import { log } from "../log.js";
13
+ import { getCliName } from "../utils/cli_name.js";
11
14
  import { branchIdResolve } from "../utils/enrichers.js";
12
15
  import { bundleEntry } from "../utils/esbuild.js";
13
16
  export const command = "dev";
@@ -30,7 +33,68 @@ export const builder = (argv) => argv
30
33
  type: "number",
31
34
  },
32
35
  })
36
+ .epilogue([
37
+ "",
38
+ "Functions run with the linked branch's Neon env injected, the same set the",
39
+ "deployed runtime gives them: DATABASE_URL, plus Neon Auth, the Data API,",
40
+ "object storage and the AI Gateway where the branch has them. A neon.ts in",
41
+ "this directory decides instead, exactly as it does for `env pull`.",
42
+ "",
43
+ "`dev` reads your .env / .env.local to reuse the branch credential behind the",
44
+ "AI Gateway and object storage, and never writes to them. With no such file it",
45
+ "issues a credential on every start, so run `env pull` once if you restart often.",
46
+ ].join("\n"))
33
47
  .strict();
48
+ /**
49
+ * The resolver context for a `neon dev` run.
50
+ *
51
+ * Two things here are what make local dev match the deployed runtime, which injects a
52
+ * branch's whole env into a function:
53
+ *
54
+ * - **The AI Gateway is asked for**, like `env pull` does, because nothing can detect it.
55
+ * Without this, a function that works deployed fails locally with no `NEON_AI_GATEWAY_*`,
56
+ * which is exactly the difference `dev` exists to eliminate.
57
+ * - **The local dotenv file is layered in**, so the branch credential behind the gateway and
58
+ * object storage is *reused* rather than re-minted. `dev` writes no file of its own, so
59
+ * without a source of persisted secrets every start would mint a credential and leave the
60
+ * last one live — one orphan per restart. `neon-env run` already reads the file for this
61
+ * reason; `dev` was the one that didn't.
62
+ */
63
+ export const devEnvContext = (props, branchId, cwd) => {
64
+ const envFile = resolveEnvFilePath(cwd);
65
+ return {
66
+ cwd,
67
+ implyAiGateway: true,
68
+ env: {
69
+ ...process.env,
70
+ ...(existsSync(envFile) ? readEnvFile(envFile) : {}),
71
+ },
72
+ ...(props.projectId ? { projectId: props.projectId } : {}),
73
+ ...(branchId ? { branchId } : {}),
74
+ ...(props.apiKey ? { apiKey: props.apiKey } : {}),
75
+ ...(props.apiHost ? { apiHost: props.apiHost } : {}),
76
+ };
77
+ };
78
+ /**
79
+ * Say when a run issued a branch credential.
80
+ *
81
+ * `dev` has nowhere to persist one — it writes no file — so on a branch with nothing to reuse
82
+ * it mints per start and cannot name the previous one to revoke it. Every other command that
83
+ * mints says so; this is the one that runs dozens of times a day, and the server banner listing
84
+ * `NEON_AI_GATEWAY_TOKEN` reads as "fetched", not "just created, and the last one is still
85
+ * live". The note names the one action that stops it, so it disappears once followed.
86
+ */
87
+ export const reportDevCredential = (credential) => {
88
+ if (!credential?.issued)
89
+ return;
90
+ if (credential.revoked.length > 0) {
91
+ log.info("Issued a new branch credential — %s changed. Revoked the one it replaced (%s).", credential.keys.join(", "), credential.revoked.join(", "));
92
+ return;
93
+ }
94
+ log.warning("Issued a branch credential for this run (%s) and left any previous one live — a dev " +
95
+ `server has nowhere to keep it. Run \`${getCliName()} env pull\` once to write it to ` +
96
+ "your .env, and restarts will reuse it instead of issuing another.", credential.keys.join(", "));
97
+ };
34
98
  export const handler = async (props) => {
35
99
  if (props.source !== undefined) {
36
100
  await runSingleSource(props);
@@ -51,17 +115,12 @@ const runSingleSource = async (props) => {
51
115
  throw new Error(`Source file not found: ${source}`);
52
116
  }
53
117
  const branchId = await resolveBranchId(props);
54
- const { vars: neonEnv, skipped } = await resolveDevEnv({
55
- cwd: process.cwd(),
56
- ...(props.projectId ? { projectId: props.projectId } : {}),
57
- ...(branchId ? { branchId } : {}),
58
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
59
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
60
- });
118
+ const { vars: neonEnv, skipped, credential, } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
119
+ reportDevCredential(credential);
61
120
  const unit = {
62
121
  slug: null,
63
122
  source,
64
- bundleDir: join(process.cwd(), "node_modules", ".neon-dev"),
123
+ bundleDir: devBundleDir(process.cwd()),
65
124
  childEnv: buildChildEnv(neonEnv, portFromProps(props.port)),
66
125
  label: null,
67
126
  envSummary: { neon: Object.keys(neonEnv), fn: [] },
@@ -89,13 +148,8 @@ const runFromConfig = async (props) => {
89
148
  throw new Error("neon.ts has no functions to serve. Add at least one under " +
90
149
  "`preview.functions`, or pass --source <path>.");
91
150
  }
92
- const { vars: neonEnv, skipped } = await resolveDevEnv({
93
- cwd: process.cwd(),
94
- ...(props.projectId ? { projectId: props.projectId } : {}),
95
- ...(branchId ? { branchId } : {}),
96
- ...(props.apiKey ? { apiKey: props.apiKey } : {}),
97
- ...(props.apiHost ? { apiHost: props.apiHost } : {}),
98
- });
151
+ const { vars: neonEnv, skipped, credential, } = await resolveDevEnv(devEnvContext(props, branchId, process.cwd()));
152
+ reportDevCredential(credential);
99
153
  const units = planFunctionsToUnits(functions, neonEnv, DEFAULT_PORT_BASE);
100
154
  // Re-derive the units from neon.ts on demand so the config watcher can hot-add/remove
101
155
  // functions without restarting the dev server. `searchBase` lets a freshly-added unit
@@ -160,6 +214,19 @@ const portFromProps = (port) => {
160
214
  }
161
215
  return { mode: "search", from: DEFAULT_PORT_BASE };
162
216
  };
217
+ /**
218
+ * Where a locally-served function's bundle is written.
219
+ *
220
+ * The location inside the project's own `node_modules` is load-bearing, not a tidiness
221
+ * choice. A function's `externalPackages` are left unbundled, and Node resolves an unbundled
222
+ * import by walking up from the importing file — so from here it reaches the project's real
223
+ * `node_modules` and finds them, at the host architecture that can actually run locally.
224
+ * Moving this directory anywhere outside `node_modules` breaks `neon dev` for those
225
+ * functions with a `Cannot find module`, so the path is pinned by a test.
226
+ */
227
+ export const devBundleDir = (cwd, slug) => slug === undefined
228
+ ? join(cwd, "node_modules", ".neon-dev")
229
+ : join(cwd, "node_modules", ".neon-dev", slug);
163
230
  /**
164
231
  * Translate a {@link PlannedFunction} into a {@link ServedUnit}. Port rules:
165
232
  * - explicit `dev.port`: bind exactly, fail if taken.
@@ -174,7 +241,7 @@ const plannedToUnit = (fn, branchEnv, searchBase) => {
174
241
  return {
175
242
  slug: fn.slug,
176
243
  source: fn.source,
177
- bundleDir: join(process.cwd(), "node_modules", ".neon-dev", fn.slug),
244
+ bundleDir: devBundleDir(process.cwd(), fn.slug),
178
245
  childEnv,
179
246
  label: fn.slug,
180
247
  envSummary: { neon: Object.keys(branchEnv), fn: Object.keys(fn.env) },
@@ -233,7 +300,9 @@ const runSupervisor = async (units, options = {}) => {
233
300
  const bundleAndStart = async (r) => {
234
301
  let bundlePath;
235
302
  try {
236
- bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages);
303
+ bundlePath = await writeBundle(r.unit.source, r.unit.bundleDir, r.unit.externalPackages,
304
+ // The `neon.ts` key, so a function is named the same way here as at deploy.
305
+ r.unit.slug ?? undefined);
237
306
  }
238
307
  catch (err) {
239
308
  r.status = "error";
@@ -481,10 +550,35 @@ const spawnChild = (unit, runtimePath, bundlePath) => {
481
550
  detached: true,
482
551
  });
483
552
  };
484
- const writeBundle = async (source, bundleDir, externalPackages) => {
485
- const files = await bundleEntry(source, {
553
+ /**
554
+ * Findings already reported for a served unit, so an advisory that cannot change between
555
+ * saves is not reprinted on every one. A dev session on a project with a standing false
556
+ * positive would otherwise repeat the whole block for its lifetime.
557
+ */
558
+ const reportedFindings = new Map();
559
+ const writeBundle = async (source, bundleDir, externalPackages, label) => {
560
+ // Left unbundled only. `bundleDir` sits inside the project's node_modules, so an
561
+ // externalized package resolves from the real tree at the host architecture — no install
562
+ // or copy is needed or wanted locally, whatever `includeFiles` says for a deploy.
563
+ const { files, metafile, warnings } = await bundleEntry(source, {
486
564
  ...(externalPackages ? { externalPackages } : {}),
487
565
  });
566
+ for (const warning of warnings)
567
+ log.warning(warning);
568
+ // Local runs resolve native packages from the real tree, so a missing declaration is
569
+ // invisible until deploy. Reporting it here is the only signal before then.
570
+ const findings = findUndeclaredNativePackages({
571
+ metafile,
572
+ declared: externalPackages ?? [],
573
+ projectDir: dirname(source),
574
+ });
575
+ const signature = findings.map((f) => f.name).join(",");
576
+ if (reportedFindings.get(source) !== signature) {
577
+ reportedFindings.set(source, signature);
578
+ for (const finding of findings) {
579
+ log.warning(describeNativeFinding(label ?? basename(source), finding));
580
+ }
581
+ }
488
582
  mkdirSync(bundleDir, { recursive: true });
489
583
  // bundleEntry emits a single `index.mjs` (no source map). The `.mjs` extension makes Node
490
584
  // load it as ESM directly, so no `package.json` `"type": "module"` marker is needed.
@@ -1,10 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
- import { NEON_ENV_VAR_KEYS } from "@neon/env";
3
2
  import chalk from "chalk";
3
+ import { NEON_ENV_VAR_KEYS } from "../_shared/env-core/env.js";
4
4
  import { ensureGitignored } from "../context.js";
5
5
  import { resolveNeonEnvVars } from "../dev/env.js";
6
6
  import { mergeEnvFile, readEnvFile, resolveEnvFilePath } from "../env_file.js";
7
+ import { ENV_PULL_SERVICES, ENV_PULL_UNAVAILABLE, envServiceKeys, ownedEnvServiceKeys, } from "../env_services.js";
7
8
  import { log } from "../log.js";
9
+ import { deprecatedServiceMessage, parseServices, servicesFlagValue, servicesOption, } from "../neon_services.js";
8
10
  import { warnAiGateway } from "../utils/ai_gateway_notice.js";
9
11
  import { announceTargetBranch } from "../utils/branch_notice.js";
10
12
  import { getCliName } from "../utils/cli_name.js";
@@ -34,14 +36,51 @@ export const builder = (argv) => argv
34
36
  "lines are preserved.",
35
37
  type: "string",
36
38
  },
39
+ service: servicesOption({
40
+ key: "service",
41
+ allowed: ENV_PULL_SERVICES,
42
+ describe: "Pull only these services' variables",
43
+ also: "Overrides neon.ts, and prunes only within the services you name.",
44
+ }),
37
45
  })
46
+ .epilogue([
47
+ "",
48
+ "What gets pulled, in precedence order:",
49
+ " 1. --service, when given — exactly those, ignoring neon.ts.",
50
+ " 2. neon.ts, when this directory has one.",
51
+ " 3. Otherwise everything the branch has, plus the AI Gateway —",
52
+ " which mints a branch credential for it.",
53
+ "",
54
+ "The pull bundled into link / checkout / config apply follows 2 and 3",
55
+ "without the AI Gateway, so it never mints a credential you did not ask",
56
+ "for. Run `env pull` to add it.",
57
+ ].join("\n"))
38
58
  .example("$0 env pull", "Write the linked branch's Neon vars into .env.local (or .env if present)")
39
- .example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file"), async (args) => {
59
+ .example("$0 env pull --branch preview --file .env.preview", "Pull a specific branch into a specific file")
60
+ .example("$0 env pull -s ai-gateway -s postgres", "Pull only the AI Gateway and Postgres variables"), async (args) => {
61
+ const raw = servicesFlagValue(args.service);
40
62
  // Explicit `env pull` announces the branch it's reading from up front so the user
41
63
  // can catch "pulled env from the wrong branch" before it overwrites their .env. The
42
64
  // bundled auto-pull (link / checkout / apply) stays quiet — those already report the
43
65
  // branch they pinned/applied to.
44
- await pull(args, { announce: true });
66
+ //
67
+ // It also implies the AI Gateway when there is no neon.ts, so a bare `env pull`
68
+ // really does write everything the branch can give you. The bundled auto-pull does
69
+ // not: minting a credential for a service the user never named is not something a
70
+ // side effect of `link` / `checkout` / `apply` should do.
71
+ await pull({
72
+ ...args,
73
+ ...(raw
74
+ ? {
75
+ services: parseServices(raw, {
76
+ allowed: ENV_PULL_SERVICES,
77
+ whyUnavailable: ENV_PULL_UNAVAILABLE,
78
+ flag: "--service",
79
+ onDeprecated: (used, canonical) => log.warning(deprecatedServiceMessage(used, canonical)),
80
+ }),
81
+ }
82
+ : {}),
83
+ }, { announce: true, implyAiGateway: raw === undefined });
45
84
  })
46
85
  .demandCommand(1);
47
86
  export const handler = (args) => args;
@@ -83,16 +122,18 @@ export const pull = async (props, opts = {}) => {
83
122
  // Reuse `neon dev`'s tiered resolver (neon.ts policy -> plan gate -> fetchEnv, else
84
123
  // pullConfig -> fetchEnv). Unlike dev, an unresolved context or failure is surfaced —
85
124
  // `env pull` is an explicit action, so it should error rather than write nothing.
86
- const { vars, credential } = await resolveNeonEnvVars({
125
+ const { vars, credential, skipped } = await resolveNeonEnvVars({
87
126
  cwd,
88
127
  projectId: props.projectId,
89
128
  branchId,
90
129
  env: { ...process.env, ...existingEnv },
130
+ ...(props.services ? { services: props.services } : {}),
131
+ ...(opts.implyAiGateway ? { implyAiGateway: true } : {}),
91
132
  ...(props.apiKey ? { apiKey: props.apiKey } : {}),
92
133
  ...(props.apiHost ? { apiHost: props.apiHost } : {}),
93
134
  ...(props.runtimeApi ? { api: props.runtimeApi } : {}),
94
135
  });
95
- const neonVars = pickNeonVars(vars);
136
+ const neonVars = pickServiceVars(pickNeonVars(vars), props.services);
96
137
  if (Object.keys(neonVars).length === 0) {
97
138
  log.info("No Neon env variables to pull for this branch (no DATABASE_URL or " +
98
139
  "enabled Auth / Data API).");
@@ -102,7 +143,7 @@ export const pull = async (props, opts = {}) => {
102
143
  // Neon-owned vars the branch no longer has (e.g. NEON_AUTH_* / NEON_DATA_API_* carried over
103
144
  // from a previous project/branch). Non-Neon lines are always preserved.
104
145
  const { written, removed } = mergeEnvFile(targetPath, neonVars, {
105
- managedKeys: NEON_OWNED_ENV_KEYS,
146
+ managedKeys: managedKeysFor(props.services, unreachedButCurrent(skipped, existingEnv, branchId)),
106
147
  });
107
148
  log.info("Pulled %d Neon variable%s into %s: %s", written.length, written.length === 1 ? "" : "s", targetPath, written.join(", "));
108
149
  if (removed.length > 0) {
@@ -116,6 +157,17 @@ export const pull = async (props, opts = {}) => {
116
157
  if (credential.revoked.length > 0) {
117
158
  log.info("Revoked the credential it replaced (%s).", credential.revoked.join(", "));
118
159
  }
160
+ else if (credential.superseded.length > 0) {
161
+ // An unscoped pull revokes what it supersedes and says so above. A scoped one
162
+ // cannot — it may not be the only service on that credential — so it leaves the
163
+ // old one live. Say that too, rather than letting the identical-looking output
164
+ // imply the branch is not accumulating credentials. Driven by what the resolver
165
+ // actually declined to revoke, so a first pull (which supersedes nothing) does
166
+ // not send the user hunting for a credential that was never there.
167
+ log.info("Left the credential it replaced live (%s): a pull scoped with --service " +
168
+ "can't tell which other services still use it. Revoke it in the Neon " +
169
+ "Console if nothing does.", credential.superseded.join(", "));
170
+ }
119
171
  }
120
172
  // A dotenv file *we* create holds live branch credentials (DATABASE_URL, Auth keys, service
121
173
  // tokens), so ignore it the same way the `.neon` context file is — otherwise a fresh repo is
@@ -143,8 +195,75 @@ export const pull = async (props, opts = {}) => {
143
195
  written,
144
196
  file: targetPath,
145
197
  ...(credential && credential.keys.length > 0 ? { credential } : {}),
198
+ ...(skipped && skipped.length > 0 ? { skipped } : {}),
146
199
  };
147
200
  };
201
+ /**
202
+ * The keys this pull is allowed to prune, i.e. the ones it is authoritative for.
203
+ *
204
+ * A `--service` selection narrows that to the services it named: `env pull -s ai-gateway`
205
+ * says nothing about `DATABASE_URL`, so it must not read that variable's absence from this
206
+ * pull as "the branch no longer has it". `unreached` is subtracted for the same reason — see
207
+ * {@link unreachedButCurrent}.
208
+ */
209
+ const managedKeysFor = (services, unreached) => {
210
+ const owned = services
211
+ ? ownedEnvServiceKeys(services)
212
+ : [...NEON_OWNED_ENV_KEYS];
213
+ if (unreached.length === 0)
214
+ return owned;
215
+ const keep = new Set(ownedEnvServiceKeys(unreached));
216
+ return owned.filter((key) => !keep.has(key));
217
+ };
218
+ /**
219
+ * Of the services this pull could not reach, the ones whose variables already on disk belong
220
+ * to the branch being pulled — the only ones worth keeping.
221
+ *
222
+ * Failing to reach a service is not evidence that the branch stopped having it:
223
+ * `PLATFORM_FEATURE_UNAVAILABLE` covers a transient incident as well as a project that
224
+ * genuinely lacks the feature, and pruning would delete a token whose secret exists nowhere
225
+ * else and strand the live credential behind it. But that only argues for keeping *this
226
+ * branch's* values. Variables left over from another branch are stale by definition, and
227
+ * keeping those would leave an app pointed at the wrong branch's gateway — a worse failure
228
+ * than losing a token, because it is silent.
229
+ *
230
+ * The gateway is the only service that can be unreached (only it is implied rather than
231
+ * observed), and its base URL is branch-scoped, so the persisted URL is what tells the two
232
+ * cases apart. Anything that does not resolve to this branch's gateway host is pruned, which
233
+ * is the safe direction: a stale entry costs a re-pull, a wrongly-kept one silently misroutes
234
+ * traffic.
235
+ */
236
+ const unreachedButCurrent = (skipped, existingEnv, branchId) => {
237
+ if (!skipped?.includes("ai-gateway"))
238
+ return [];
239
+ const baseUrl = existingEnv[NEON_ENV_VAR_KEYS.aiGateway.baseUrl];
240
+ return baseUrl !== undefined && isBranchGatewayUrl(baseUrl, branchId)
241
+ ? ["ai-gateway"]
242
+ : [];
243
+ };
244
+ /**
245
+ * Whether a persisted `NEON_AI_GATEWAY_BASE_URL` addresses `branchId`'s gateway.
246
+ *
247
+ * Checks the parsed **hostname** against the shape `@neon/env` builds
248
+ * (`<branchId>-api.ai.<host suffix>`), not the raw string: a prefix comparison is satisfied
249
+ * by a URL whose userinfo carries the branch id (`https://<branchId>-api.ai.@other-host/`)
250
+ * while the request actually goes elsewhere. An unparseable value is not this branch's
251
+ * gateway either, which is an answer rather than a swallowed failure.
252
+ */
253
+ const isBranchGatewayUrl = (baseUrl, branchId) => URL.canParse(baseUrl) &&
254
+ new URL(baseUrl).hostname.startsWith(`${branchId}-api.ai.`);
255
+ /**
256
+ * Narrow the resolved vars to the selected services (plus `NEON_BRANCH`, which every pull
257
+ * refreshes). Needed because the two `DATABASE_URL*` vars are always resolved — `fetchEnv`
258
+ * reads both connection URIs regardless, since the AI Gateway host is derived from the direct
259
+ * one — so `--service ai-gateway` has to drop them here rather than avoid fetching them.
260
+ */
261
+ const pickServiceVars = (vars, services) => {
262
+ if (!services)
263
+ return vars;
264
+ const wanted = envServiceKeys(services);
265
+ return Object.fromEntries(Object.entries(vars).filter(([key]) => wanted.has(key)));
266
+ };
148
267
  /**
149
268
  * Pull a freshly-pinned branch's Neon env vars into a local `.env`, bundled into `link` and
150
269
  * `checkout` so the branch-first loop is just *link + checkout* — `env pull` runs for you.
@@ -183,6 +302,8 @@ export const renderAgentPullNote = (result) => {
183
302
  const credential = result.credential?.issued
184
303
  ? ` Issued a new branch credential, so ${result.credential.keys.join(", ")} changed.`
185
304
  : "";
305
+ // No `skipped` note: only the implied AI Gateway can be skipped, and the auto-pull
306
+ // this renders never implies it.
186
307
  return ` Pulled ${result.written.length} Neon env var${result.written.length === 1 ? "" : "s"} into ${result.file}.${credential}`;
187
308
  }
188
309
  case "empty":
@@ -1,5 +1,6 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
- import { join } from "node:path";
2
+ import { dirname, join } from "node:path";
3
+ import { describeNativeFinding, findUndeclaredNativePackages, } from "@neon/config-runtime";
3
4
  import { isNeonApiError, retryOnLock } from "../api.js";
4
5
  import { createDeployment, deleteFunction, getFunction, listFunctions, } from "../functions_api.js";
5
6
  import { log } from "../log.js";
@@ -194,7 +195,20 @@ const deploy = async (props) => {
194
195
  throw new Error(`No entry file found in ${src}. Expected one of: ${ENTRY_CANDIDATES.join(", ")}.`);
195
196
  }
196
197
  // Bundle before any network round-trip so a bundling failure fails fast.
197
- const zip = zipBundle(await bundleEntry(source));
198
+ const bundled = await bundleEntry(source);
199
+ for (const warning of bundled.warnings)
200
+ log.warning(warning);
201
+ // `--src` bypasses `neon.ts`, so there is no policy to declare anything in and no way to
202
+ // stage files here. The advisory still runs: this is the shortest path to a function that
203
+ // deploys clean and then fails at invoke, so it is the last place to stay silent.
204
+ for (const finding of findUndeclaredNativePackages({
205
+ metafile: bundled.metafile,
206
+ declared: [],
207
+ projectDir: dirname(source),
208
+ })) {
209
+ log.warning(describeNativeFinding(props.slug, finding));
210
+ }
211
+ const zip = zipBundle(bundled.files);
198
212
  const branchId = await branchIdFromProps(props);
199
213
  // Snapshot the current version before deploy so we can detect the new one
200
214
  // afterward. A missing function (404) or no deployment yet → undefined.
@@ -1,3 +1,4 @@
1
+ import { NEON_SERVICES } from "./neon_services.js";
1
2
  /**
2
3
  * The published npm packages a `neon.ts` project needs — the `@neon/*` org names.
3
4
  *
@@ -11,55 +12,32 @@ export const CONFIG_PACKAGE = "@neon/config";
11
12
  export const ENV_PACKAGE = "@neon/env";
12
13
  export const REQUIRED_PACKAGES = [CONFIG_PACKAGE, ENV_PACKAGE];
13
14
  /**
14
- * A Neon service `config init` can declare in the `neon.ts` it scaffolds, spelled the way a
15
- * user types it in `--services`. Kebab-case rather than the `neon.ts` field names (`aiGateway`,
16
- * `buckets`) so the flag reads like a flag; {@link renderNeonConfig} owns the mapping.
15
+ * The services `config init` can declare in the `neon.ts` it scaffolds — the subset of
16
+ * {@link NEON_SERVICES} a policy has a field for. {@link renderNeonConfig} owns the mapping
17
+ * from these names to the `neon.ts` fields (`aiGateway`, `buckets`).
17
18
  *
18
- * Postgres is absent because every branch has it, and `dataApi` is absent because enabling it
19
- * with the default `authProvider: "neon"` requires `auth` — a pairing the picker would have to
20
- * enforce rather than offer.
19
+ * Postgres is absent because every branch has it, so there is nothing to declare. `data-api`
20
+ * is absent because enabling it with the default `authProvider: "neon"` requires `auth` — a
21
+ * pairing the picker would have to enforce rather than offer.
21
22
  */
22
- export const NEON_SERVICES = [
23
- "auth",
24
- "functions",
25
- "storage",
26
- "ai-gateway",
27
- ];
28
- /** `--services none`: declare nothing, i.e. scaffold the bare starter policy. */
29
- export const NO_SERVICES = "none";
23
+ export const CONFIG_INIT_SERVICES = NEON_SERVICES.filter((service) => service !== "postgres" && service !== "data-api");
24
+ /**
25
+ * What `config init --services none` produces. One constant because it is both the help text
26
+ * and what tells the parser `none` is a value here — passing the literal at each call site
27
+ * lets the two drift into documenting something the parser does not accept.
28
+ */
29
+ export const CONFIG_INIT_NONE_MEANS = "the bare starter policy";
30
+ /** Why the two a policy cannot declare are not selectable, for the refusal message. */
31
+ export const CONFIG_INIT_UNAVAILABLE = {
32
+ postgres: "every branch has Postgres, so a policy has nothing to declare for it",
33
+ "data-api": "enabling it with the default provider requires auth, so declare auth here and turn the Data API on with `neon data-api create`",
34
+ };
30
35
  /** Slug, display name, and source path of the function scaffolded for `functions`. */
31
36
  export const FUNCTION_SLUG = "hello";
32
37
  export const FUNCTION_NAME = "Hello World";
33
38
  export const FUNCTION_FILENAME = "hello.ts";
34
39
  /** Name of the bucket scaffolded for `storage`. */
35
40
  export const BUCKET_NAME = "assets";
36
- /**
37
- * Parse a `--services` value into a canonical service list: comma-separated
38
- * {@link NEON_SERVICES} names, or {@link NO_SERVICES} on its own for none.
39
- *
40
- * Unknown names are rejected here rather than silently dropped — a typo'd service would
41
- * otherwise scaffold a policy missing exactly the service the user asked for. The result is
42
- * deduplicated and ordered by {@link NEON_SERVICES} so the rendered file doesn't depend on the
43
- * order they were typed in.
44
- */
45
- export const parseServices = (raw) => {
46
- const names = raw
47
- .split(",")
48
- .map((name) => name.trim())
49
- .filter((name) => name !== "");
50
- if (names.includes(NO_SERVICES)) {
51
- if (names.length > 1) {
52
- throw new Error(`--services ${NO_SERVICES} cannot be combined with other services.`);
53
- }
54
- return [];
55
- }
56
- const unknown = names.filter((name) => !NEON_SERVICES.includes(name));
57
- if (unknown.length > 0) {
58
- throw new Error(`Unknown service${unknown.length === 1 ? "" : "s"} ${unknown.join(", ")}. ` +
59
- `Supported values: ${NEON_SERVICES.join(", ")}, ${NO_SERVICES}.`);
60
- }
61
- return NEON_SERVICES.filter((service) => names.includes(service));
62
- };
63
41
  /**
64
42
  * One indentation level in the emitted `neon.ts`. Two spaces, which is what every renderer
65
43
  * here produces and what `config_template.format.test.ts` holds them to.
@@ -88,7 +66,7 @@ const renderPreview = (services) => {
88
66
  ...at(3, `${FUNCTION_SLUG}: { name: "${FUNCTION_NAME}", source: "./${FUNCTION_FILENAME}" },`),
89
67
  ]));
90
68
  }
91
- if (services.includes("storage")) {
69
+ if (services.includes("object-storage")) {
92
70
  lines.push(...block(2, "buckets", [
93
71
  ...at(3, `// "private" is the default; use "public_read" for anonymous reads`, `${BUCKET_NAME}: { access: "private" },`),
94
72
  ]));