neon 2.1.2 → 2.29.1
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/LICENSE.md +178 -0
- package/README.md +601 -0
- package/dist/analytics.js +156 -0
- package/dist/api.js +665 -0
- package/dist/auth.js +127 -0
- package/dist/callback.html +51 -0
- package/dist/cli.js +9 -0
- package/dist/commands/auth.js +214 -0
- package/dist/commands/bootstrap.js +481 -0
- package/dist/commands/branches.js +481 -0
- package/dist/commands/bucket.js +543 -0
- package/dist/commands/checkout.js +289 -0
- package/dist/commands/config.js +544 -0
- package/dist/commands/connection_string.js +172 -0
- package/dist/commands/data_api.js +285 -0
- package/dist/commands/databases.js +82 -0
- package/dist/commands/deploy.js +26 -0
- package/dist/commands/dev.js +698 -0
- package/dist/commands/env.js +166 -0
- package/dist/commands/functions.js +373 -0
- package/dist/commands/index.js +54 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/ip_allow.js +137 -0
- package/dist/commands/link.js +1121 -0
- package/dist/commands/neon_auth.js +1028 -0
- package/dist/commands/operations.js +28 -0
- package/dist/commands/orgs.js +24 -0
- package/dist/commands/projects.js +372 -0
- package/dist/commands/psql.js +62 -0
- package/dist/commands/roles.js +65 -0
- package/dist/commands/schema_diff.js +151 -0
- package/dist/commands/set_context.js +29 -0
- package/dist/commands/status.js +40 -0
- package/dist/commands/user.js +15 -0
- package/dist/commands/vpc_endpoints.js +134 -0
- package/dist/config.js +11 -0
- package/dist/config_format.js +72 -0
- package/dist/context.js +177 -0
- package/dist/current_branch_fast_path.js +55 -0
- package/dist/dev/env.js +240 -0
- package/dist/dev/functions.js +70 -0
- package/dist/dev/inputs.js +63 -0
- package/dist/dev/runtime.js +146 -0
- package/dist/env.js +36 -0
- package/dist/env_file.js +159 -0
- package/dist/errors.js +80 -0
- package/dist/functions_api.js +44 -0
- package/dist/help.js +146 -0
- package/dist/index.js +234 -0
- package/dist/log.js +18 -0
- package/dist/parameters.gen.js +480 -0
- package/dist/pkg.js +25 -0
- package/dist/psql/cli.js +53 -0
- package/dist/psql/command/cmd_cond.js +437 -0
- package/dist/psql/command/cmd_connect.js +820 -0
- package/dist/psql/command/cmd_copy.js +1035 -0
- package/dist/psql/command/cmd_describe.js +1815 -0
- package/dist/psql/command/cmd_format.js +922 -0
- package/dist/psql/command/cmd_io.js +2193 -0
- package/dist/psql/command/cmd_lo.js +393 -0
- package/dist/psql/command/cmd_meta.js +970 -0
- package/dist/psql/command/cmd_misc.js +187 -0
- package/dist/psql/command/cmd_pipeline.js +1148 -0
- package/dist/psql/command/cmd_restrict.js +171 -0
- package/dist/psql/command/cmd_show.js +766 -0
- package/dist/psql/command/dispatch.js +343 -0
- package/dist/psql/command/inputQueue.js +42 -0
- package/dist/psql/command/shared.js +71 -0
- package/dist/psql/complete/filenames.js +139 -0
- package/dist/psql/complete/index.js +104 -0
- package/dist/psql/complete/matcher.js +315 -0
- package/dist/psql/complete/psqlVars.js +247 -0
- package/dist/psql/complete/queries.js +493 -0
- package/dist/psql/complete/rules.js +2424 -0
- package/dist/psql/core/common.js +1253 -0
- package/dist/psql/core/help.js +576 -0
- package/dist/psql/core/mainloop.js +1360 -0
- package/dist/psql/core/prompt.js +440 -0
- package/dist/psql/core/settings.js +684 -0
- package/dist/psql/core/sqlHelp.js +1066 -0
- package/dist/psql/core/startup.js +846 -0
- package/dist/psql/core/syncVars.js +116 -0
- package/dist/psql/core/variables.js +287 -0
- package/dist/psql/describe/formatters.js +1290 -0
- package/dist/psql/describe/processNamePattern.js +270 -0
- package/dist/psql/describe/queries.js +2378 -0
- package/dist/psql/describe/versionGate.js +43 -0
- package/dist/psql/index.js +2030 -0
- package/dist/psql/io/history.js +299 -0
- package/dist/psql/io/input.js +120 -0
- package/dist/psql/io/lineEditor/buffer.js +325 -0
- package/dist/psql/io/lineEditor/complete.js +227 -0
- package/dist/psql/io/lineEditor/filename.js +159 -0
- package/dist/psql/io/lineEditor/index.js +894 -0
- package/dist/psql/io/lineEditor/keymap.js +745 -0
- package/dist/psql/io/lineEditor/vt100.js +363 -0
- package/dist/psql/io/pgpass.js +202 -0
- package/dist/psql/io/pgservice.js +194 -0
- package/dist/psql/io/psqlrc.js +422 -0
- package/dist/psql/print/aligned.js +1765 -0
- package/dist/psql/print/asciidoc.js +248 -0
- package/dist/psql/print/crosstab.js +463 -0
- package/dist/psql/print/csv.js +95 -0
- package/dist/psql/print/html.js +258 -0
- package/dist/psql/print/json.js +96 -0
- package/dist/psql/print/latex.js +396 -0
- package/dist/psql/print/pager.js +267 -0
- package/dist/psql/print/troff.js +258 -0
- package/dist/psql/print/unaligned.js +119 -0
- package/dist/psql/print/units.js +135 -0
- package/dist/psql/scanner/slash.js +515 -0
- package/dist/psql/scanner/sql.js +914 -0
- package/dist/psql/scanner/stringutils.js +394 -0
- package/dist/psql/types/backslash.js +1 -0
- package/dist/psql/types/connection.js +1 -0
- package/dist/psql/types/index.js +7 -0
- package/dist/psql/types/printer.js +1 -0
- package/dist/psql/types/repl.js +1 -0
- package/dist/psql/types/scanner.js +24 -0
- package/dist/psql/types/settings.js +1 -0
- package/dist/psql/types/variables.js +1 -0
- package/dist/psql/wire/connection.js +2858 -0
- package/dist/psql/wire/copy.js +108 -0
- package/dist/psql/wire/notify.js +59 -0
- package/dist/psql/wire/pipeline.js +521 -0
- package/dist/psql/wire/protocol.js +466 -0
- package/dist/psql/wire/sasl.js +296 -0
- package/dist/psql/wire/tls.js +602 -0
- package/dist/storage_api.js +147 -0
- package/dist/test_utils/fixtures.js +122 -0
- package/dist/test_utils/oauth_server.js +9 -0
- package/dist/types.js +1 -0
- package/dist/utils/api_enums.js +33 -0
- package/dist/utils/auth.js +5 -0
- package/dist/utils/branch_notice.js +22 -0
- package/dist/utils/branch_picker.js +103 -0
- package/dist/utils/compute_units.js +28 -0
- package/dist/utils/enrichers.js +161 -0
- package/dist/utils/esbuild.js +158 -0
- package/dist/utils/formats.js +18 -0
- package/dist/utils/middlewares.js +20 -0
- package/dist/utils/package_manager.js +68 -0
- package/dist/utils/point_in_time.js +56 -0
- package/dist/utils/psql.js +120 -0
- package/dist/utils/string.js +5 -0
- package/dist/utils/ui.js +59 -0
- package/dist/utils/zip.js +4 -0
- package/dist/writer.js +97 -0
- package/package.json +117 -14
- package/.jshintrc +0 -3
- package/CHANGELOG.markdown +0 -22
- package/LICENSE +0 -21
- package/README.markdown +0 -76
- package/bower.json +0 -23
- package/license.txt +0 -9
- package/neon.js +0 -234
- package/stdlib/bubbling_support.js +0 -32
- package/stdlib/custom_event.js +0 -54
- package/stdlib/custom_event_support.js +0 -195
- package/stdlib/index.js +0 -7
- package/stdlib/node_support.js +0 -123
- package/stdlib/widget.js +0 -340
- package/test/neon_browser.html +0 -11
- package/test/neon_stdlib_browser.html +0 -15
- package/test/neon_stdlib_test.js +0 -84
- package/test/neon_test.js +0 -64
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { contextBranch, currentContextFile, readContextFile, } from "./context.js";
|
|
2
|
+
import { log } from "./log.js";
|
|
3
|
+
/**
|
|
4
|
+
* Offline fast path for `(config) status --current-branch` (used by shell prompts).
|
|
5
|
+
*
|
|
6
|
+
* Reading the pinned branch out of the local `.neon` file does not need the CLI's
|
|
7
|
+
* full command tree, `@neondatabase/api-client`, or yargs — importing those is ~200ms,
|
|
8
|
+
* which dwarfs the actual work. So the entry point ({@link file://./cli.ts}) calls this
|
|
9
|
+
* BEFORE importing `index.js`, and only falls through to the full CLI when this returns
|
|
10
|
+
* `false`. On the fast path the process loads only this module + `context.js`/`log.js`
|
|
11
|
+
* (~25ms total incl. Node startup) instead of ~230ms.
|
|
12
|
+
*
|
|
13
|
+
* It mirrors the `--current-branch` short-circuit in `status()` (commands/config.ts):
|
|
14
|
+
* print the pinned branch to stdout and exit 0, or print nothing + a `neonctl checkout`
|
|
15
|
+
* hint on stderr and exit non-zero when no branch is pinned.
|
|
16
|
+
*
|
|
17
|
+
* Deliberately conservative: only the EXACT bare invocation is handled —
|
|
18
|
+
* `status --current-branch` or `config status --current-branch` with no other args.
|
|
19
|
+
* Anything else (extra flags like `--context-file`/`--output`, more args, etc.) returns
|
|
20
|
+
* `false` and flows through the normal yargs pipeline, so behavior can never diverge —
|
|
21
|
+
* the worst case is "not faster", never "wrong".
|
|
22
|
+
*
|
|
23
|
+
* @returns `true` if it handled the invocation (caller should NOT load the full CLI).
|
|
24
|
+
*/
|
|
25
|
+
export const tryCurrentBranchFastPath = (argv,
|
|
26
|
+
// `cwd` is overridable so tests can exercise the `.neon` walk-up without mutating
|
|
27
|
+
// `process.cwd()` (which isn't allowed in vitest workers), mirroring currentContextFile.
|
|
28
|
+
cwd = process.cwd()) => {
|
|
29
|
+
// argv is [execPath, scriptPath, ...userArgs].
|
|
30
|
+
if (!isExactCurrentBranchInvocation(argv.slice(2))) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const branch = contextBranch(readContextFile(currentContextFile(cwd)));
|
|
34
|
+
if (branch) {
|
|
35
|
+
process.stdout.write(`${branch}\n`);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
log.info("No branch pinned. Run `neonctl checkout <branch>` to pin a branch and pull its env vars.");
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* True only for `status --current-branch` or `config status --current-branch` with no
|
|
45
|
+
* other arguments. Any extra token (another flag, `--context-file`, `=`-style flags,
|
|
46
|
+
* positional args) makes this false so the full CLI handles it.
|
|
47
|
+
*/
|
|
48
|
+
const isExactCurrentBranchInvocation = (args) => {
|
|
49
|
+
const rest = args[0] === "status"
|
|
50
|
+
? args.slice(1)
|
|
51
|
+
: args[0] === "config" && args[1] === "status"
|
|
52
|
+
? args.slice(2)
|
|
53
|
+
: null;
|
|
54
|
+
return rest !== null && rest.length === 1 && rest[0] === "--current-branch";
|
|
55
|
+
};
|
package/dist/dev/env.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { loadConfigFromFile } from "@neon/config";
|
|
2
|
+
import { plan, pullConfig } from "@neon/config-runtime";
|
|
3
|
+
import { fetchEnv, toEntries } from "@neon/env";
|
|
4
|
+
import { log } from "../log.js";
|
|
5
|
+
/** The API-targeting options every runtime call forwards from the context. */
|
|
6
|
+
const apiOptions = (ctx) => ({
|
|
7
|
+
...(ctx.apiKey ? { apiKey: ctx.apiKey } : {}),
|
|
8
|
+
...(ctx.apiHost ? { apiHost: ctx.apiHost } : {}),
|
|
9
|
+
...(ctx.api ? { api: ctx.api } : {}),
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Thrown when a `neon.ts` policy declares a branch-level resource (Neon Auth,
|
|
13
|
+
* Data API, a bucket, the AI Gateway) that the linked remote branch does not
|
|
14
|
+
* have yet. Unlike every other failure in {@link resolveDevEnv} — which degrades
|
|
15
|
+
* to "run without injection" — this is a hard stop: the user's intent (a policy)
|
|
16
|
+
* cannot be honored, and silently dropping the secret would be more confusing
|
|
17
|
+
* than refusing to start. The fix is to provision the resource first.
|
|
18
|
+
*/
|
|
19
|
+
export class DevEnvMismatchError extends Error {
|
|
20
|
+
constructor() {
|
|
21
|
+
super(...arguments);
|
|
22
|
+
this.name = "DevEnvMismatchError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Signals that no project/branch context could be resolved, so there is nothing to
|
|
27
|
+
* resolve env from. `resolveDevEnv` degrades on this (dev runs without injection);
|
|
28
|
+
* `env pull` surfaces it (an explicit pull needs a branch).
|
|
29
|
+
*/
|
|
30
|
+
export class MissingBranchContextError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super(...arguments);
|
|
33
|
+
this.name = "MissingBranchContextError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the branch's Neon env vars (pooled / direct `DATABASE_URL`, plus Auth /
|
|
38
|
+
* Data API when enabled) into a `{ KEY: value }` map. Shared by `neon dev` (which
|
|
39
|
+
* injects them) and `neon env pull` (which writes them to a `.env` file).
|
|
40
|
+
*
|
|
41
|
+
* Tiered:
|
|
42
|
+
*
|
|
43
|
+
* 1. a `neon.ts` policy is found -> the policy is the source of truth. We first
|
|
44
|
+
* check it against the branch's live state (`plan`); if it declares a resource
|
|
45
|
+
* the branch is missing, we stop with a {@link DevEnvMismatchError} pointing at
|
|
46
|
+
* `neonctl deploy`. Otherwise `fetchEnv` evaluates the policy.
|
|
47
|
+
* 2. no `neon.ts`, but a project + branch are known -> `pullConfig` reads the
|
|
48
|
+
* branch's live state (incl. Auth / Data API enablement) into a config, then
|
|
49
|
+
* `fetchEnv` resolves what is actually enabled.
|
|
50
|
+
* 3. otherwise -> throw {@link MissingBranchContextError}.
|
|
51
|
+
*
|
|
52
|
+
* Unlike {@link resolveDevEnv}, this never swallows errors — callers decide how to
|
|
53
|
+
* handle them.
|
|
54
|
+
*/
|
|
55
|
+
export const resolveNeonEnvVars = async (ctx) => {
|
|
56
|
+
const config = await loadNeonConfig(ctx.cwd);
|
|
57
|
+
if (config) {
|
|
58
|
+
if (!ctx.projectId || !ctx.branchId) {
|
|
59
|
+
throw new MissingBranchContextError("Found a neon.ts but could not resolve the project/branch. " +
|
|
60
|
+
"Run `neonctl link` and `neonctl checkout <branch>`, or pass " +
|
|
61
|
+
"--project-id / --branch.");
|
|
62
|
+
}
|
|
63
|
+
// Resolve env from the policy with its `preview.functions` removed. Functions carry no
|
|
64
|
+
// branch-level secrets — their env comes from the local `neon.ts` `functions.<slug>.env`,
|
|
65
|
+
// layered per-function by the dev server — so env resolution never needs the functions
|
|
66
|
+
// API. Probing it (via `plan`/`fetchEnv`) only adds a failure mode: an undeployed
|
|
67
|
+
// function, or a project where the Functions Preview isn't enabled, would error and sink
|
|
68
|
+
// ALL injection (including DATABASE_URL). Stripping functions keeps env resolution honest
|
|
69
|
+
// while leaving buckets / AI Gateway / Auth / Data API fully checked — those DO carry
|
|
70
|
+
// secrets, so a declared-but-missing one still hard-stops (see assertPolicyMatchesBranch).
|
|
71
|
+
const envConfig = withoutPreviewFunctions(config);
|
|
72
|
+
await assertPolicyMatchesBranch(envConfig, ctx);
|
|
73
|
+
return await fetchAndProject(envConfig, ctx);
|
|
74
|
+
}
|
|
75
|
+
if (ctx.projectId && ctx.branchId) {
|
|
76
|
+
const pulled = await pullConfig({
|
|
77
|
+
projectId: ctx.projectId,
|
|
78
|
+
branchId: ctx.branchId,
|
|
79
|
+
...apiOptions(ctx),
|
|
80
|
+
});
|
|
81
|
+
// `pulled.config` is already a `Config` (static auth/dataApi toggles + a branch
|
|
82
|
+
// tuning closure), so it feeds straight into fetchEnv — no wrapping needed.
|
|
83
|
+
return await fetchAndProject(pulled.config, ctx);
|
|
84
|
+
}
|
|
85
|
+
throw new MissingBranchContextError("No project/branch context found. Link a branch (`neonctl link` / " +
|
|
86
|
+
"`neonctl checkout`) or pass --project-id and --branch.");
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* `neon dev`'s env resolver: {@link resolveNeonEnvVars} with graceful degradation.
|
|
90
|
+
*
|
|
91
|
+
* - Success → `{ vars }` (possibly just the always-present Postgres URLs).
|
|
92
|
+
* - No linked branch / project → `{ vars: {}, skipped }` with a "link a branch" hint; the
|
|
93
|
+
* function still runs locally, just without Neon env.
|
|
94
|
+
* - Any other failure (offline, transient API error) → `{ vars: {}, skipped }` naming the
|
|
95
|
+
* cause; again non-fatal.
|
|
96
|
+
* - {@link DevEnvMismatchError} (policy declares a secret-bearing service the branch lacks)
|
|
97
|
+
* is the one hard stop and is re-thrown for the caller to surface.
|
|
98
|
+
*/
|
|
99
|
+
export const resolveDevEnv = async (ctx) => {
|
|
100
|
+
try {
|
|
101
|
+
return { vars: await resolveNeonEnvVars(ctx) };
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
if (err instanceof DevEnvMismatchError)
|
|
105
|
+
throw err;
|
|
106
|
+
if (err instanceof MissingBranchContextError) {
|
|
107
|
+
log.debug("dev: %s; skipping env injection", err.message);
|
|
108
|
+
return {
|
|
109
|
+
vars: {},
|
|
110
|
+
skipped: {
|
|
111
|
+
reason: "no linked Neon branch — run `neonctl link`, then " +
|
|
112
|
+
"`neonctl checkout <branch>`, to inject DATABASE_URL and friends",
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
117
|
+
log.debug("dev: env resolution failed: %s", detail);
|
|
118
|
+
return {
|
|
119
|
+
vars: {},
|
|
120
|
+
skipped: {
|
|
121
|
+
reason: `could not reach Neon (${detail}); running without Neon env`,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Return the policy with its `preview.functions` removed, so the env path never enumerates
|
|
128
|
+
* functions against the Neon API. Functions are local-source-bundled and produce no
|
|
129
|
+
* branch-level secrets, so they are irrelevant to env resolution; probing them only risks
|
|
130
|
+
* failing the whole resolve (undeployed function, or Functions Preview disabled on the
|
|
131
|
+
* project). Buckets / AI Gateway and the top-level Auth / Data API toggles are preserved —
|
|
132
|
+
* they DO carry env, so they must still be checked and resolved. Returns the config
|
|
133
|
+
* unchanged when it declares no functions.
|
|
134
|
+
*/
|
|
135
|
+
const withoutPreviewFunctions = (config) => {
|
|
136
|
+
const preview = config.preview;
|
|
137
|
+
if (!preview?.functions)
|
|
138
|
+
return config;
|
|
139
|
+
const previewWithoutFunctions = { ...preview };
|
|
140
|
+
delete previewWithoutFunctions.functions;
|
|
141
|
+
return { ...config, preview: previewWithoutFunctions };
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Tier-1 guard. Dry-run the policy against the branch's live state and stop if
|
|
145
|
+
* it declares a branch-level resource the branch is missing. Built on `plan` so
|
|
146
|
+
* it covers every present and future provisionable resource for free: any
|
|
147
|
+
* `create` action is a resource `neonctl deploy` would provision.
|
|
148
|
+
*
|
|
149
|
+
* Called with functions already stripped (see {@link withoutPreviewFunctions}), so the
|
|
150
|
+
* `plan` probe never enumerates the functions API — an undeployed function, or a project
|
|
151
|
+
* without the Functions Preview, must never block local dev or sink env injection.
|
|
152
|
+
*/
|
|
153
|
+
const assertPolicyMatchesBranch = async (config, ctx) => {
|
|
154
|
+
const result = await plan(config, {
|
|
155
|
+
projectId: ctx.projectId,
|
|
156
|
+
branchId: ctx.branchId,
|
|
157
|
+
...apiOptions(ctx),
|
|
158
|
+
});
|
|
159
|
+
const missing = result.applied.filter(isMissingResource);
|
|
160
|
+
if (missing.length === 0)
|
|
161
|
+
return;
|
|
162
|
+
const names = missing.map((change) => change.identifier).join(", ");
|
|
163
|
+
throw new DevEnvMismatchError(`Your neon.ts declares ${names} for branch ${ctx.branchId}, but the branch ` +
|
|
164
|
+
"does not have it yet, so the matching env vars cannot be injected. " +
|
|
165
|
+
"Provision it first with `neonctl deploy` (or `neonctl config apply`), " +
|
|
166
|
+
"then re-run `neonctl dev`.");
|
|
167
|
+
};
|
|
168
|
+
/**
|
|
169
|
+
* A planned change that provisions a branch-level resource the branch lacks: a
|
|
170
|
+
* `create` on a service (Neon Auth, Data API, a bucket, the AI Gateway). Branch
|
|
171
|
+
* setting drift (`update`) and `noop`s are ignored — they don't block local dev
|
|
172
|
+
* — and functions are excluded (see {@link assertPolicyMatchesBranch}).
|
|
173
|
+
*/
|
|
174
|
+
const isMissingResource = (change) => change.kind === "service" &&
|
|
175
|
+
change.action === "create" &&
|
|
176
|
+
!change.identifier.startsWith("function:");
|
|
177
|
+
const fetchAndProject = async (config, ctx) => {
|
|
178
|
+
const env = await fetchEnv(config, {
|
|
179
|
+
projectId: ctx.projectId,
|
|
180
|
+
branch: ctx.branchId,
|
|
181
|
+
...apiOptions(ctx),
|
|
182
|
+
...(ctx.env ? { env: ctx.env } : {}),
|
|
183
|
+
});
|
|
184
|
+
return toEntries(env);
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Load a `neon.ts` policy if one exists on the path from `cwd` up to the repo
|
|
188
|
+
* root. Returns `null` when there is none (the common "no config" case), and
|
|
189
|
+
* surfaces real load errors (e.g. a syntax error in an existing file).
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* Substrings that mark a module-resolution failure while loading `neon.ts` —
|
|
193
|
+
* almost always because the project's dependencies aren't installed yet (the
|
|
194
|
+
* config imports `@neon/config` & friends). Deliberately specific:
|
|
195
|
+
* the generic "…or a missing dependency…" hint the loader always appends is
|
|
196
|
+
* NOT in here, so a real syntax/runtime error doesn't get mislabeled.
|
|
197
|
+
*/
|
|
198
|
+
const MISSING_DEPENDENCY_HINTS = [
|
|
199
|
+
"cannot find module",
|
|
200
|
+
"cannot find package",
|
|
201
|
+
"err_module_not_found",
|
|
202
|
+
"failed to resolve",
|
|
203
|
+
"could not resolve",
|
|
204
|
+
"module not found",
|
|
205
|
+
];
|
|
206
|
+
/** Flatten an error and its `cause` chain to one lowercased string for matching. */
|
|
207
|
+
const errorChainText = (err) => {
|
|
208
|
+
const parts = [];
|
|
209
|
+
let current = err;
|
|
210
|
+
for (let depth = 0; current instanceof Error && depth < 6; depth++) {
|
|
211
|
+
parts.push(current.message);
|
|
212
|
+
current = current.cause;
|
|
213
|
+
}
|
|
214
|
+
return parts.join("\n").toLowerCase();
|
|
215
|
+
};
|
|
216
|
+
const looksLikeMissingDependency = (err) => {
|
|
217
|
+
const text = errorChainText(err);
|
|
218
|
+
return MISSING_DEPENDENCY_HINTS.some((hint) => text.includes(hint));
|
|
219
|
+
};
|
|
220
|
+
const loadNeonConfig = async (cwd) => {
|
|
221
|
+
try {
|
|
222
|
+
const { config } = await loadConfigFromFile({ cwd });
|
|
223
|
+
return config;
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
227
|
+
if (/Could not find a Neon config file/i.test(message)) {
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
// A neon.ts that imports a package which isn't installed fails here with a
|
|
231
|
+
// cryptic "Cannot find module …". Turn that into the actionable thing to do.
|
|
232
|
+
if (looksLikeMissingDependency(err)) {
|
|
233
|
+
throw new Error("Could not load neon.ts: a package it imports is not installed. " +
|
|
234
|
+
"Did you run `npm install`? Install your dependencies " +
|
|
235
|
+
"(npm / pnpm / yarn / bun), then try again.\n" +
|
|
236
|
+
`Original error: ${message}`);
|
|
237
|
+
}
|
|
238
|
+
throw err;
|
|
239
|
+
}
|
|
240
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { loadConfigFromFile, resolveConfig, } from "@neon/config";
|
|
4
|
+
/**
|
|
5
|
+
* Load `neon.ts` (if any) and resolve the list of functions it declares into
|
|
6
|
+
* {@link PlannedFunction}s for `neon dev` to serve. Returns `null` when there is no
|
|
7
|
+
* `neon.ts` on the path from `cwd` up to the repo root — the caller turns that into a
|
|
8
|
+
* "no --source and no neon.ts" error.
|
|
9
|
+
*
|
|
10
|
+
* `branchName` is used only to evaluate a policy that switches on `branch.name`; the
|
|
11
|
+
* function list is otherwise branch-independent, so a placeholder is fine when unknown.
|
|
12
|
+
*/
|
|
13
|
+
export const resolveFunctionsFromConfig = async (cwd, branchName) => {
|
|
14
|
+
const loaded = await loadNeonConfig(cwd);
|
|
15
|
+
if (!loaded)
|
|
16
|
+
return null;
|
|
17
|
+
const { config, configDir, configPath } = loaded;
|
|
18
|
+
const resolved = resolveConfig(config, {
|
|
19
|
+
name: branchName ?? "local",
|
|
20
|
+
exists: branchName !== undefined,
|
|
21
|
+
});
|
|
22
|
+
const functions = resolved.preview?.functions ?? [];
|
|
23
|
+
const planned = functions.map((fn) => {
|
|
24
|
+
const source = isAbsolute(fn.source)
|
|
25
|
+
? fn.source
|
|
26
|
+
: resolve(configDir, fn.source);
|
|
27
|
+
if (!existsSync(source)) {
|
|
28
|
+
throw new Error(`Function "${fn.slug}" points at a source that does not exist: ${source} ` +
|
|
29
|
+
`(from neon.ts "${fn.source}"). Fix the source path and re-run.`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
slug: fn.slug,
|
|
33
|
+
name: fn.name,
|
|
34
|
+
source,
|
|
35
|
+
...(devPort(fn.dev) !== undefined
|
|
36
|
+
? { port: devPort(fn.dev) }
|
|
37
|
+
: {}),
|
|
38
|
+
env: { ...fn.env },
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return { configPath, functions: planned };
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Read the `port` off a {@link FunctionDevConfig}. `undefined` when no `dev.port` is set
|
|
45
|
+
* (the supervisor then searches for a free port).
|
|
46
|
+
*/
|
|
47
|
+
const devPort = (dev) => dev?.port;
|
|
48
|
+
/**
|
|
49
|
+
* Load a `neon.ts` policy if one exists, returning the loaded config, the resolved path to
|
|
50
|
+
* the config file (used by the dev server to watch it), and the directory it lives in (used
|
|
51
|
+
* to resolve each function's relative `source`). Returns `null` when no config file is
|
|
52
|
+
* found; surfaces real load errors (e.g. a syntax error).
|
|
53
|
+
*/
|
|
54
|
+
const loadNeonConfig = async (cwd) => {
|
|
55
|
+
try {
|
|
56
|
+
const { config, resolvedPath } = await loadConfigFromFile({ cwd });
|
|
57
|
+
return {
|
|
58
|
+
config,
|
|
59
|
+
configDir: dirname(resolvedPath),
|
|
60
|
+
configPath: resolvedPath,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
65
|
+
if (/Could not find a Neon config file/i.test(message)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
throw err;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
const defaultDeps = {
|
|
3
|
+
isPackaged: () => process.pkg !== undefined,
|
|
4
|
+
loadEsbuild: (name) => import(name),
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the exact set of files esbuild reads to produce the bundle for
|
|
8
|
+
* `source` — the entry plus every local module it imports (npm deps are left
|
|
9
|
+
* external, so they never appear). These are the files the dev watcher should
|
|
10
|
+
* watch, so a single edit triggers exactly one rebuild.
|
|
11
|
+
*
|
|
12
|
+
* Returns absolute paths, or `null` when the precise set cannot be computed —
|
|
13
|
+
* either inside the packaged binary (which cannot import esbuild as a module;
|
|
14
|
+
* it shells out to a binary that has no JSON-metafile equivalent here) or on a
|
|
15
|
+
* platform where the esbuild module won't load. Callers fall back to a coarser
|
|
16
|
+
* watch in that case.
|
|
17
|
+
*
|
|
18
|
+
* This performs a metafile-only pass (`write:false`, `metafile:true`) so it
|
|
19
|
+
* never emits output; the actual bundle bytes still come from `bundleEntry`.
|
|
20
|
+
*/
|
|
21
|
+
export const resolveWatchInputs = async (source, deps = defaultDeps) => {
|
|
22
|
+
if (deps.isPackaged())
|
|
23
|
+
return null;
|
|
24
|
+
// esbuild is resolved by a COMPUTED specifier, never the literal string
|
|
25
|
+
// 'esbuild', for the same reason as src/utils/esbuild.ts: rollup and
|
|
26
|
+
// @yao-pkg/pkg statically scan for literal import()/require() and would pull
|
|
27
|
+
// esbuild's native Go binary into the bundle/snapshot. Keep it invisible.
|
|
28
|
+
const name = ["es", "build"].join("");
|
|
29
|
+
let esbuild;
|
|
30
|
+
try {
|
|
31
|
+
esbuild = await deps.loadEsbuild(name);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
let metafile;
|
|
37
|
+
try {
|
|
38
|
+
// Mirrors bundleEntry's flags so the resolved input graph matches the real
|
|
39
|
+
// bundle. metafile:true + write:false makes this a pure analysis pass.
|
|
40
|
+
const result = await esbuild.build({
|
|
41
|
+
entryPoints: [source],
|
|
42
|
+
bundle: true,
|
|
43
|
+
write: false,
|
|
44
|
+
metafile: true,
|
|
45
|
+
format: "esm",
|
|
46
|
+
platform: "node",
|
|
47
|
+
packages: "external",
|
|
48
|
+
logLevel: "silent",
|
|
49
|
+
});
|
|
50
|
+
metafile = result.metafile;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// A bundle error here is non-fatal for watching: bundleEntry surfaces the
|
|
54
|
+
// real diagnostic. Fall back to the coarser watch so edits still rebuild.
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const inputs = metafile?.inputs;
|
|
58
|
+
if (!inputs)
|
|
59
|
+
return null;
|
|
60
|
+
// metafile input keys are paths relative to esbuild's cwd; resolve to absolute
|
|
61
|
+
// so they compare cleanly against chokidar's watched paths.
|
|
62
|
+
return Object.keys(inputs).map((p) => resolve(process.cwd(), p));
|
|
63
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { getRequestListener } from "@hono/node-server";
|
|
5
|
+
const isFunction = (value) => typeof value === "function";
|
|
6
|
+
const hasFetchMethod = (value) => typeof value === "object" &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
"fetch" in value &&
|
|
9
|
+
typeof value.fetch === "function";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve the user's exported handler to a single fetch callback.
|
|
12
|
+
*
|
|
13
|
+
* Resolution order (first match wins):
|
|
14
|
+
* 1. `export default { fetch }` — Workers / Neon Functions style
|
|
15
|
+
* 2. `export default function (req)` — bare (async) default function
|
|
16
|
+
*/
|
|
17
|
+
export const resolveFetchHandler = (mod) => {
|
|
18
|
+
const defaultExport = mod.default;
|
|
19
|
+
if (hasFetchMethod(defaultExport)) {
|
|
20
|
+
const target = defaultExport;
|
|
21
|
+
return (req) => target.fetch(req);
|
|
22
|
+
}
|
|
23
|
+
if (isFunction(defaultExport)) {
|
|
24
|
+
return defaultExport;
|
|
25
|
+
}
|
|
26
|
+
throw new Error("No request handler found in the source module. Export one of:\n" +
|
|
27
|
+
" export default { fetch(req) { /* ... */ } }\n" +
|
|
28
|
+
" export default function (req) { /* ... */ }");
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Wrap a fetch handler so user errors become a 500 response (with the message
|
|
32
|
+
* in the body during dev) instead of crashing the child process.
|
|
33
|
+
*/
|
|
34
|
+
export const withErrorBoundary = (handler) => {
|
|
35
|
+
return async (req) => {
|
|
36
|
+
try {
|
|
37
|
+
return await handler(req);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
const message = err instanceof Error ? (err.stack ?? err.message) : String(err);
|
|
41
|
+
process.stderr.write(`Request handler threw an error:\n${message}\n`);
|
|
42
|
+
return new Response(`Internal Server Error\n\n${message}`, {
|
|
43
|
+
status: 500,
|
|
44
|
+
headers: { "content-type": "text/plain; charset=utf-8" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
};
|
|
49
|
+
const isAddressInUse = (err) => typeof err === "object" &&
|
|
50
|
+
err !== null &&
|
|
51
|
+
err.code === "EADDRINUSE";
|
|
52
|
+
const DEFAULT_SEARCH_BASE = 8787;
|
|
53
|
+
const MAX_SEARCH_STEPS = 100;
|
|
54
|
+
const bindPort = async (server, selection, hostname) => {
|
|
55
|
+
if (selection.mode === "explicit") {
|
|
56
|
+
return listen(server, selection.port, hostname);
|
|
57
|
+
}
|
|
58
|
+
for (let step = 0; step < MAX_SEARCH_STEPS; step++) {
|
|
59
|
+
try {
|
|
60
|
+
return await listen(server, selection.from + step, hostname);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
if (!isAddressInUse(err))
|
|
64
|
+
throw err;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Could not find a free port in ${selection.from}-${selection.from + MAX_SEARCH_STEPS - 1}`);
|
|
68
|
+
};
|
|
69
|
+
const listen = (server, port, hostname) => new Promise((resolveListen, rejectListen) => {
|
|
70
|
+
const onError = (err) => {
|
|
71
|
+
server.off("listening", onListening);
|
|
72
|
+
rejectListen(err);
|
|
73
|
+
};
|
|
74
|
+
const onListening = () => {
|
|
75
|
+
server.off("error", onError);
|
|
76
|
+
resolveListen(server.address().port);
|
|
77
|
+
};
|
|
78
|
+
server.once("error", onError);
|
|
79
|
+
server.once("listening", onListening);
|
|
80
|
+
server.listen(port, hostname);
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Load the (already-bundled) user module, build the listener, and start an HTTP
|
|
84
|
+
* server. Announces the bound port on stdout as `neon-dev:ready <port>` so the
|
|
85
|
+
* parent can render the URL. Resolves with the bound port.
|
|
86
|
+
*/
|
|
87
|
+
export const startRuntime = async ({ source, port, hostname, }) => {
|
|
88
|
+
const absoluteSource = resolve(process.cwd(), source);
|
|
89
|
+
const mod = (await import(pathToFileURL(absoluteSource).href));
|
|
90
|
+
const handler = withErrorBoundary(resolveFetchHandler(mod));
|
|
91
|
+
const listener = getRequestListener(handler, { hostname });
|
|
92
|
+
const server = createServer((incoming, outgoing) => {
|
|
93
|
+
void listener(incoming, outgoing);
|
|
94
|
+
});
|
|
95
|
+
const boundPort = await bindPort(server, port, hostname);
|
|
96
|
+
process.stdout.write(`neon-dev:ready ${boundPort}\n`);
|
|
97
|
+
return boundPort;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Build a {@link PortSelection} from the environment. Precedence:
|
|
101
|
+
* 1. `NEON_DEV_PORT` -> explicit bind (crash if taken). Set by `neon dev` from an
|
|
102
|
+
* explicit `--port` / `dev.port`.
|
|
103
|
+
* 2. `PORT` -> explicit bind. A bare `PORT=3000 neon dev` sets this, so the
|
|
104
|
+
* runtime binds the port chosen for it.
|
|
105
|
+
* 3. otherwise -> search upward from `NEON_DEV_PORT_BASE` (or the default base).
|
|
106
|
+
*/
|
|
107
|
+
export const portSelectionFromEnv = (env) => {
|
|
108
|
+
const explicit = env.NEON_DEV_PORT;
|
|
109
|
+
if (explicit !== undefined && explicit !== "") {
|
|
110
|
+
return { mode: "explicit", port: parsePort(explicit, "NEON_DEV_PORT") };
|
|
111
|
+
}
|
|
112
|
+
const injected = env.PORT;
|
|
113
|
+
if (injected !== undefined && injected !== "") {
|
|
114
|
+
return { mode: "explicit", port: parsePort(injected, "PORT") };
|
|
115
|
+
}
|
|
116
|
+
const base = Number(env.NEON_DEV_PORT_BASE ?? DEFAULT_SEARCH_BASE);
|
|
117
|
+
return {
|
|
118
|
+
mode: "search",
|
|
119
|
+
from: Number.isInteger(base) ? base : DEFAULT_SEARCH_BASE,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
const parsePort = (value, varName) => {
|
|
123
|
+
const port = Number(value);
|
|
124
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
125
|
+
throw new Error(`Invalid ${varName}: "${value}"`);
|
|
126
|
+
}
|
|
127
|
+
return port;
|
|
128
|
+
};
|
|
129
|
+
const isDirectExecution = () => {
|
|
130
|
+
const entry = process.argv[1];
|
|
131
|
+
if (!entry)
|
|
132
|
+
return false;
|
|
133
|
+
return import.meta.url === pathToFileURL(entry).href;
|
|
134
|
+
};
|
|
135
|
+
if (isDirectExecution()) {
|
|
136
|
+
const source = process.env.NEON_DEV_SOURCE ?? process.argv[2];
|
|
137
|
+
if (!source) {
|
|
138
|
+
process.stderr.write("neon-dev runtime: missing source path\n");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
startRuntime({ source, port: portSelectionFromEnv(process.env) }).catch((err) => {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
process.stderr.write(`neon-dev runtime failed to start: ${msg}\n`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
}
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const isCi = () => {
|
|
2
|
+
return process.env.CI !== "false" && Boolean(process.env.CI);
|
|
3
|
+
};
|
|
4
|
+
export const isDebug = () => {
|
|
5
|
+
return Boolean(process.env.DEBUG);
|
|
6
|
+
};
|
|
7
|
+
export const getGithubEnvVars = (env) => {
|
|
8
|
+
const vars = [
|
|
9
|
+
// github action info
|
|
10
|
+
"GITHUB_ACTION_PATH",
|
|
11
|
+
// source github repository
|
|
12
|
+
"GITHUB_REPOSITORY",
|
|
13
|
+
// environment info
|
|
14
|
+
"GITHUB_RUN_ID",
|
|
15
|
+
"GITHUB_RUN_NUMBER",
|
|
16
|
+
"GITHUB_SERVER_URL",
|
|
17
|
+
"GITHUB_WORKFLOW_REF",
|
|
18
|
+
"RUNNER_ARCH",
|
|
19
|
+
"RUNNER_ENVIRONMENT",
|
|
20
|
+
"RUNNER_OS",
|
|
21
|
+
];
|
|
22
|
+
const map = new Map();
|
|
23
|
+
vars.forEach((v) => {
|
|
24
|
+
let value = env[v];
|
|
25
|
+
if (value === undefined || value === "") {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (v === "GITHUB_ACTION_PATH") {
|
|
29
|
+
value = value.includes("actions/")
|
|
30
|
+
? value.replace(/^.*actions\/(.+)$/, "$1")
|
|
31
|
+
: value;
|
|
32
|
+
}
|
|
33
|
+
map.set(v, value);
|
|
34
|
+
});
|
|
35
|
+
return Object.fromEntries(map);
|
|
36
|
+
};
|