sproutboat 0.6.0 → 0.7.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/SURFACE.md +2 -2
- package/package.json +2 -2
- package/src/api-version.ts +32 -0
- package/src/build.ts +2 -0
- package/src/compile.ts +21 -2
- package/src/main.ts +15 -0
- package/src/manifest.ts +20 -0
- package/src/surface.ts +1 -1
- package/src/update-check.ts +1 -1
- package/src/wrap.ts +19 -1
package/SURFACE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> Generated by `src/surface.test.ts` from `src/surface.ts` + the pinned
|
|
4
4
|
> toolchain constants. Do not edit by hand — run `UPDATE_SURFACE=1 bun test`.
|
|
5
5
|
|
|
6
|
-
**Package:** `sproutboat` 0.
|
|
6
|
+
**Package:** `sproutboat` 0.7.0 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
| `kv` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | KV namespaces. `create` prints the id to bind from sproutboat.jsonc. |
|
|
21
21
|
| `d1` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | D1 databases. `create` prints the id to bind from sproutboat.jsonc. |
|
|
22
22
|
| `r2` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | R2 buckets. `create` prints the id to bind from sproutboat.jsonc. |
|
|
23
|
-
| `queues` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | Queues. `create` prints the id to bind from sproutboat.jsonc; consumers
|
|
23
|
+
| `queues` | `<list | create <name> | info <name> | rename <name> <new> | delete <name>>` | Queues. `create` prints the id to bind from sproutboat.jsonc; consumers deliver in batches with retries, and stop after 5 attempts. |
|
|
24
24
|
| `domains` | `<list | add <host> | verify <host> | delete <host>> [project-dir]` | Attach a custom domain to the project (TXT-verified). No sub-command lists. |
|
|
25
25
|
| `secrets` | `<list | put <NAME> [--value <value>] | delete <NAME>> [project-dir]` | Manage encrypted project secrets (read as env.NAME). `put` reads the value from stdin unless --value is given, so it stays out of shell history; applies on next deploy. |
|
|
26
26
|
| `delete` | `[project-dir] [--name <project>] --yes` | Delete the project, every version, and its route. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"lefthook": "2.1.12",
|
|
62
62
|
"oxfmt": "0.66.0",
|
|
63
63
|
"oxlint": "1.81.0",
|
|
64
|
-
"typescript": "
|
|
64
|
+
"typescript": "7.0.2"
|
|
65
65
|
},
|
|
66
66
|
"engines": {
|
|
67
67
|
"bun": ">=1.4.0"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version skew between a CLI and a control plane.
|
|
3
|
+
*
|
|
4
|
+
* The two update independently — the CLI from npm, a self-hosted node from its
|
|
5
|
+
* own `SB_REF` — so a user can easily point a months-old CLI at a fresh box or
|
|
6
|
+
* the reverse. Without a handshake that shows up as a 400 from a route that
|
|
7
|
+
* moved, which tells them nothing.
|
|
8
|
+
*
|
|
9
|
+
* The exchange is response-only: every `/api/` response carries the control
|
|
10
|
+
* plane's version and the oldest CLI it supports, and the CLI checks those on
|
|
11
|
+
* responses it was already reading. Nothing is added to requests, so an old
|
|
12
|
+
* control plane simply sends no headers and nothing happens.
|
|
13
|
+
*/
|
|
14
|
+
import { isNewer } from "./update-check";
|
|
15
|
+
|
|
16
|
+
export const CONTROL_VERSION_HEADER = "x-sproutboat-control";
|
|
17
|
+
export const MIN_CLI_HEADER = "x-sproutboat-min-cli";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The warning to print for this response, or null when the pair is fine.
|
|
21
|
+
* Pure so it can be tested without a server; `main.ts` prints what it returns.
|
|
22
|
+
*/
|
|
23
|
+
export function controlVersionWarning(response: Response, cliVersion: string): string | null {
|
|
24
|
+
const min = response.headers.get(MIN_CLI_HEADER);
|
|
25
|
+
// No header: a control plane older than this handshake. Nothing to say.
|
|
26
|
+
if (!min || !isNewer(min, cliVersion)) return null;
|
|
27
|
+
const control = response.headers.get(CONTROL_VERSION_HEADER);
|
|
28
|
+
return (
|
|
29
|
+
`this control plane${control ? ` (${control})` : ""} needs sproutboat ${min} or newer, ` +
|
|
30
|
+
`and you are on ${cliVersion} — upgrade with \`bun add -g sproutboat@latest\``
|
|
31
|
+
);
|
|
32
|
+
}
|
package/src/build.ts
CHANGED
|
@@ -99,6 +99,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
99
99
|
bindings,
|
|
100
100
|
zigBin,
|
|
101
101
|
target: input.target,
|
|
102
|
+
compatibilityDate: input.config.compatibility_date,
|
|
102
103
|
});
|
|
103
104
|
|
|
104
105
|
const sprout = await readFile(sproutPath);
|
|
@@ -111,6 +112,7 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
111
112
|
porfforVersion: porfforVersion(),
|
|
112
113
|
esbuildVersion: esbuildVersion(),
|
|
113
114
|
buildImage: toolchainStamp(),
|
|
115
|
+
compatibilityDate: input.config.compatibility_date,
|
|
114
116
|
sourceHash,
|
|
115
117
|
binaryHash: digest(sprout),
|
|
116
118
|
binarySize: (await stat(sproutPath)).size,
|
package/src/compile.ts
CHANGED
|
@@ -15,7 +15,13 @@ import { ensurePorfforPatched } from "./patch-porffor";
|
|
|
15
15
|
import { ensureUWebSockets, porfforRoot, UwsUnavailableError } from "./toolchain";
|
|
16
16
|
import { EMPTY_BINDINGS, preludePath, wrapNativeFetchHandler, type Bindings } from "./wrap";
|
|
17
17
|
|
|
18
|
-
export {
|
|
18
|
+
export {
|
|
19
|
+
BASELINE_COMPATIBILITY_DATE,
|
|
20
|
+
EMPTY_BINDINGS,
|
|
21
|
+
preludePath,
|
|
22
|
+
wrapNativeFetchHandler,
|
|
23
|
+
type Bindings,
|
|
24
|
+
} from "./wrap";
|
|
19
25
|
|
|
20
26
|
const COMPILE_TIMEOUT_MS = Number(process.env.SPROUTBOAT_COMPILE_TIMEOUT_MS || 600_000);
|
|
21
27
|
|
|
@@ -26,6 +32,9 @@ export type CompileInput = {
|
|
|
26
32
|
outPath: string;
|
|
27
33
|
vars: Record<string, string>;
|
|
28
34
|
bindings?: Bindings;
|
|
35
|
+
/** The project's `compatibility_date`, baked in so the runtime can gate a
|
|
36
|
+
* behaviour change on it. Defaults to the baseline when absent. */
|
|
37
|
+
compatibilityDate?: string;
|
|
29
38
|
/** Cross-compiler for `linux-x86_64`. Not needed, and not used, for `host`. */
|
|
30
39
|
zigBin?: string;
|
|
31
40
|
/**
|
|
@@ -76,7 +85,17 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
76
85
|
input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
|
|
77
86
|
readFile(preludePath, "utf8"),
|
|
78
87
|
]);
|
|
79
|
-
await writeFile(
|
|
88
|
+
await writeFile(
|
|
89
|
+
generatedPath,
|
|
90
|
+
wrapNativeFetchHandler(
|
|
91
|
+
source,
|
|
92
|
+
prelude,
|
|
93
|
+
input.vars,
|
|
94
|
+
input.bindings ?? EMPTY_BINDINGS,
|
|
95
|
+
undefined,
|
|
96
|
+
input.compatibilityDate,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
80
99
|
|
|
81
100
|
const porffor = porfforRoot();
|
|
82
101
|
const launcher = resolve(porffor, "runtime/index.js");
|
package/src/main.ts
CHANGED
|
@@ -13,11 +13,26 @@ import { CLI_VERSION, printDeployReport } from "./report";
|
|
|
13
13
|
import { activeApiUrl, forgetToken, savedToken, saveToken } from "./credentials";
|
|
14
14
|
import { helpText, STORAGE_PRODUCTS, STORAGE_VERBS, type StorageProduct } from "./surface";
|
|
15
15
|
import { notifyIfOutdated } from "./update-check";
|
|
16
|
+
import { controlVersionWarning } from "./api-version";
|
|
16
17
|
import { amber, bold, dim, leaf, ok, rose } from "./style";
|
|
17
18
|
|
|
18
19
|
const defaultApiUrl = "https://dashboard.sproutboat.com";
|
|
19
20
|
|
|
21
|
+
/** Warn at most once per run: every API response carries the headers, and one
|
|
22
|
+
* command makes several calls. */
|
|
23
|
+
let skewWarned = false;
|
|
24
|
+
|
|
20
25
|
async function responseText(response: Response, failure: string): Promise<string> {
|
|
26
|
+
// Checked here rather than at each call site: every control-plane response
|
|
27
|
+
// funnels through this helper, including the failures — and a version-skew
|
|
28
|
+
// 400 is exactly when the user most needs to be told which side is old.
|
|
29
|
+
if (!skewWarned) {
|
|
30
|
+
const warning = controlVersionWarning(response, CLI_VERSION);
|
|
31
|
+
if (warning) {
|
|
32
|
+
skewWarned = true;
|
|
33
|
+
console.warn(amber(`! ${warning}`));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
21
36
|
if (response.ok) return response.text();
|
|
22
37
|
fail(`${failure} (${response.status}): ${await response.text()}`);
|
|
23
38
|
}
|
package/src/manifest.ts
CHANGED
|
@@ -24,6 +24,16 @@ export type ArtifactManifest = {
|
|
|
24
24
|
esbuildVersion: string;
|
|
25
25
|
/** Build provenance, e.g. `zig-musl/0.16.0+porffor/a415d19+uws/360c276d`. */
|
|
26
26
|
buildImage: string;
|
|
27
|
+
/**
|
|
28
|
+
* The project's `compatibility_date`, as `YYYY-MM-DD`. Optional: artifacts
|
|
29
|
+
* built before this field existed have none, and every reader must treat that
|
|
30
|
+
* as the baseline (see `BASELINE_COMPATIBILITY_DATE` in `wrap.ts`). This is
|
|
31
|
+
* how a runtime behaviour change ships without breaking deployed apps — new
|
|
32
|
+
* builds opt in by moving their date, old binaries keep the old semantics —
|
|
33
|
+
* so it is deliberately *not* part of `schemaVersion`, which stays frozen
|
|
34
|
+
* at 2.
|
|
35
|
+
*/
|
|
36
|
+
compatibilityDate?: string;
|
|
27
37
|
sourceHash: `sha256:${string}`;
|
|
28
38
|
binaryHash: `sha256:${string}`;
|
|
29
39
|
binarySize: number;
|
|
@@ -110,6 +120,15 @@ export function validateManifest(value: ManifestInput): ManifestValidation {
|
|
|
110
120
|
if (binarySize === null) errors.push("binarySize must be a positive integer");
|
|
111
121
|
const builtAt = isString(value.builtAt) && !Number.isNaN(Date.parse(value.builtAt)) ? value.builtAt : null;
|
|
112
122
|
if (builtAt === null) errors.push("builtAt must be an ISO-8601 timestamp");
|
|
123
|
+
// Optional by design: an artifact from before the field existed is valid and
|
|
124
|
+
// must stay deployable, so "absent" is not an error — only "present and
|
|
125
|
+
// malformed" is.
|
|
126
|
+
let compatibilityDate: string | undefined;
|
|
127
|
+
if (value.compatibilityDate !== undefined) {
|
|
128
|
+
if (isString(value.compatibilityDate) && /^\d{4}-\d{2}-\d{2}$/.test(value.compatibilityDate))
|
|
129
|
+
compatibilityDate = value.compatibilityDate;
|
|
130
|
+
else errors.push("compatibilityDate must be YYYY-MM-DD");
|
|
131
|
+
}
|
|
113
132
|
if (
|
|
114
133
|
errors.length ||
|
|
115
134
|
schemaVersion === null ||
|
|
@@ -142,6 +161,7 @@ export function validateManifest(value: ManifestInput): ManifestValidation {
|
|
|
142
161
|
binaryHash,
|
|
143
162
|
binarySize,
|
|
144
163
|
builtAt,
|
|
164
|
+
compatibilityDate,
|
|
145
165
|
},
|
|
146
166
|
};
|
|
147
167
|
}
|
package/src/surface.ts
CHANGED
|
@@ -59,7 +59,7 @@ const storageCommands: readonly Command[] = STORAGE_PRODUCTS.map((product) => ({
|
|
|
59
59
|
emoji: product.emoji,
|
|
60
60
|
args: STORAGE_ARGS,
|
|
61
61
|
brief: `<${STORAGE_VERBS.join(" | ")}>`,
|
|
62
|
-
summary: `${product.plural[0].toUpperCase()}${product.plural.slice(1)}. \`create\` prints the id to bind from sproutboat.jsonc${product.name === "queues" ? "; consumers
|
|
62
|
+
summary: `${product.plural[0].toUpperCase()}${product.plural.slice(1)}. \`create\` prints the id to bind from sproutboat.jsonc${product.name === "queues" ? "; consumers deliver in batches with retries, and stop after 5 attempts" : ""}.`,
|
|
63
63
|
}));
|
|
64
64
|
|
|
65
65
|
export const COMMANDS: readonly Command[] = [
|
package/src/update-check.ts
CHANGED
|
@@ -28,7 +28,7 @@ function cachePath(): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** Numeric x.y.z compare; a trailing `-tag` (prerelease) sorts before its release. */
|
|
31
|
-
function isNewer(latest: string, current: string): boolean {
|
|
31
|
+
export function isNewer(latest: string, current: string): boolean {
|
|
32
32
|
const parts = (v: string) =>
|
|
33
33
|
v
|
|
34
34
|
.split("-")[0]
|
package/src/wrap.ts
CHANGED
|
@@ -17,6 +17,20 @@ export const preludePath = new URL("./native-fetch-prelude.js", import.meta.url)
|
|
|
17
17
|
// value is only a fallback for a directly-run binary.
|
|
18
18
|
const DEFAULT_PORT = 8080;
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* What an artifact with no `compatibilityDate` means. Artifacts built before
|
|
22
|
+
* the field existed keep the semantics of that day forever, because the binary
|
|
23
|
+
* is immutable and `rollback` can reactivate it at any time.
|
|
24
|
+
*
|
|
25
|
+
* How to use it: when a runtime behaviour has to change in a way that would
|
|
26
|
+
* break a deployed handler, don't change it unconditionally — gate it in the
|
|
27
|
+
* prelude on `__sbCompat >= "YYYY-MM-DD"` (ISO dates compare correctly as
|
|
28
|
+
* strings) and document the flip date. Old binaries carry their old date and
|
|
29
|
+
* keep the old behaviour; a project opts in by moving `compatibility_date` in
|
|
30
|
+
* its `sproutboat.jsonc` and rebuilding.
|
|
31
|
+
*/
|
|
32
|
+
export const BASELINE_COMPATIBILITY_DATE = "2026-08-26";
|
|
33
|
+
|
|
20
34
|
/**
|
|
21
35
|
* Binding names a project declares. `do` maps a binding name to a Durable Object
|
|
22
36
|
* class name; `crons` are schedule expressions with no name.
|
|
@@ -127,6 +141,7 @@ export function wrapNativeFetchHandler(
|
|
|
127
141
|
vars: Record<string, string> = {},
|
|
128
142
|
bindings: Bindings = EMPTY_BINDINGS,
|
|
129
143
|
port: number = DEFAULT_PORT,
|
|
144
|
+
compatibilityDate: string = BASELINE_COMPATIBILITY_DATE,
|
|
130
145
|
): string {
|
|
131
146
|
const neutralised = neutraliseExports(source);
|
|
132
147
|
if (neutralised === null || !/\bfetch\s*\(/.test(source)) {
|
|
@@ -134,13 +149,16 @@ export function wrapNativeFetchHandler(
|
|
|
134
149
|
}
|
|
135
150
|
|
|
136
151
|
const env = `const env = ${JSON.stringify(vars)};\nglobalThis.env = env;\n`;
|
|
152
|
+
// Baked, not a binding: the date belongs to the artifact, and a handler must
|
|
153
|
+
// not be able to change the semantics it was compiled against at runtime.
|
|
154
|
+
const compat = `globalThis.__sbCompat = ${JSON.stringify(compatibilityDate)};\n`;
|
|
137
155
|
const wire = hasBindings(bindings) ? `__sbInstallBindings(env, ${JSON.stringify(bindings)});\n` : "";
|
|
138
156
|
const registerDO = bindings.do.length
|
|
139
157
|
? `__sbRegisterDO({ ${bindings.do.map((d) => `${d.className}: ${d.className}`).join(", ")} });\n`
|
|
140
158
|
: "";
|
|
141
159
|
|
|
142
160
|
return (
|
|
143
|
-
`${prelude}\n${env}${wire}` +
|
|
161
|
+
`${prelude}\n${compat}${env}${wire}` +
|
|
144
162
|
`${neutralised}\n` +
|
|
145
163
|
`${registerDO}` +
|
|
146
164
|
`export default {\n port: ${port},\n fetch(request) { return __sbEntry(__sbHandlers, request); }\n};\n`
|