sproutboat 0.5.0 → 0.6.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.
@@ -29,12 +29,15 @@ export async function ensurePorfforPatched(): Promise<void> {
29
29
  if (done) return;
30
30
  const file = resolve(porfforRoot(), "compiler/render.js");
31
31
  const src = await readFile(file, "utf8");
32
- if (src.includes(MARKER)) { done = true; return; }
32
+ if (src.includes(MARKER)) {
33
+ done = true;
34
+ return;
35
+ }
33
36
  const anchorAt = src.indexOf(ANCHOR);
34
37
  if (anchorAt === -1) {
35
38
  throw new Error(
36
39
  `could not patch Porffor for $PORT: anchor not found in ${file}. ` +
37
- "Porffor's native-fetch renderer changed — check patches/UPSTREAM.md.",
40
+ "Porffor's native-fetch renderer changed — check patches/UPSTREAM.md.",
38
41
  );
39
42
  }
40
43
  const patched = src.slice(0, anchorAt + ANCHOR.length) + INJECT + src.slice(anchorAt + ANCHOR.length);
package/src/report.ts CHANGED
@@ -6,7 +6,9 @@ import { bold, dim, leaf, sprout } from "./style";
6
6
 
7
7
  // Read from package.json so the banner never drifts from the published version.
8
8
  // SAFETY: our own package.json, shipped beside src/ by the `files` field; npm requires `version`.
9
- const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { version: string };
9
+ const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
10
+ version: string;
11
+ };
10
12
  export const CLI_VERSION = packageJson.version;
11
13
 
12
14
  function bytes(n: number): string {
@@ -21,23 +23,38 @@ function table(headers: string[], rows: string[][], align: boolean[] = []): stri
21
23
  const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
22
24
  const rule = (l: string, m: string, r: string) => leaf(l + widths.map((w) => "─".repeat(w + 2)).join(m) + r);
23
25
  const row = (cells: string[], head = false) =>
24
- leaf("│ ") + cells.map((c, i) => {
25
- const cell = align[i] ? (c ?? "").padStart(widths[i]) : (c ?? "").padEnd(widths[i]);
26
- return head ? bold(cell) : cell;
27
- }).join(leaf(" ")) + leaf(" ");
28
- return [rule("┌", "┬", "┐"), row(headers, true), rule("├", "┼", "┤"), ...rows.map((r) => row(r)), rule("└", "┴", "┘")].join("\n");
26
+ leaf("│ ") +
27
+ cells
28
+ .map((c, i) => {
29
+ const cell = align[i] ? (c ?? "").padStart(widths[i]) : (c ?? "").padEnd(widths[i]);
30
+ return head ? bold(cell) : cell;
31
+ })
32
+ .join(leaf(" │ ")) +
33
+ leaf(" │");
34
+ return [
35
+ rule("┌", "┬", "┐"),
36
+ row(headers, true),
37
+ rule("├", "┼", "┤"),
38
+ ...rows.map((r) => row(r)),
39
+ rule("└", "┴", "┘"),
40
+ ].join("\n");
29
41
  }
30
42
 
31
43
  /** Every binding the compiled sprout will see, flattened to (name, type, detail) rows. */
32
44
  function bindingRows(config: SproutboatConfig): string[][] {
33
45
  const rows: string[][] = [];
34
- for (const [name, value] of Object.entries(config.vars ?? {})) rows.push([`env.${name}`, "var", JSON.stringify(value)]);
46
+ for (const [name, value] of Object.entries(config.vars ?? {}))
47
+ rows.push([`env.${name}`, "var", JSON.stringify(value)]);
35
48
  const list = (names: string[] | undefined, type: string, detail = "") => {
36
49
  for (const name of names ?? []) rows.push([`env.${name}`, type, detail]);
37
50
  };
38
51
  const resourceList = (refs: Parameters<typeof resourceRefs>[0], type: string) => {
39
52
  for (const ref of resourceRefs(refs)) {
40
- rows.push([`env.${ref.binding}`, type, ref.id ?? "deploy-scoped store (--no-provision) — drop the flag to auto-provision a persistent resource"]);
53
+ rows.push([
54
+ `env.${ref.binding}`,
55
+ type,
56
+ ref.id ?? "deploy-scoped store (--no-provision) — drop the flag to auto-provision a persistent resource",
57
+ ]);
41
58
  }
42
59
  };
43
60
  resourceList(config.kv_namespaces, "kv");
@@ -46,7 +63,8 @@ function bindingRows(config: SproutboatConfig): string[][] {
46
63
  resourceList(config.r2_buckets, "r2");
47
64
  resourceList(config.queues, "queue");
48
65
  list(config.analytics_engine_datasets, "analytics");
49
- for (const [name, className] of Object.entries(config.durable_objects ?? {})) rows.push([`env.${name}`, "durable object", className]);
66
+ for (const [name, className] of Object.entries(config.durable_objects ?? {}))
67
+ rows.push([`env.${name}`, "durable object", className]);
50
68
  for (const host of config.outbound ?? []) rows.push([`fetch()`, "outbound", host]);
51
69
  for (const cron of config.triggers?.crons ?? []) rows.push([`scheduled()`, "cron", cron]);
52
70
  if (config.assets?.binding) rows.push([`env.${config.assets.binding}`, "assets", config.assets.directory ?? ""]);
@@ -74,14 +92,16 @@ export function printDeployReport(
74
92
  console.log();
75
93
 
76
94
  console.log(bold("Artifact"));
77
- console.log(table(
78
- ["File", "Type", "Size"],
79
- [
80
- ["sprout", manifest.runtime, bytes(sproutBin.length)],
81
- ["manifest.json", "json", bytes(manifestBytes)],
82
- ],
83
- [false, false, true],
84
- ));
95
+ console.log(
96
+ table(
97
+ ["File", "Type", "Size"],
98
+ [
99
+ ["sprout", manifest.runtime, bytes(sproutBin.length)],
100
+ ["manifest.json", "json", bytes(manifestBytes)],
101
+ ],
102
+ [false, false, true],
103
+ ),
104
+ );
85
105
  console.log(`Total upload: ${bold(bytes(total))} ${dim(`(sprout gzip: ${bytes(gz)})`)}`);
86
106
  console.log();
87
107
 
package/src/source.ts CHANGED
@@ -16,7 +16,10 @@ const alwaysForbidden: Array<[RegExp, string]> = [
16
16
  // trapped property reads back as `undefined`, with no throw. Rejecting it
17
17
  // here is the difference between a build error and a 502 nobody can explain.
18
18
  // It is why itty-router and other Proxy-based routers do not work yet.
19
- [/\bnew\s+Proxy\s*\(|\bProxy\s*\.\s*revocable\s*\(/, "Proxy is not supported by the compiler: its traps are silently ignored and the property reads back as undefined"],
19
+ [
20
+ /\bnew\s+Proxy\s*\(|\bProxy\s*\.\s*revocable\s*\(/,
21
+ "Proxy is not supported by the compiler: its traps are silently ignored and the property reads back as undefined",
22
+ ],
20
23
  ];
21
24
 
22
25
  const fetchWithoutAllowlist: [RegExp, string] = [
package/src/style.ts CHANGED
@@ -3,14 +3,17 @@
3
3
  * or NO_COLOR is set (https://no-color.org), so piped/CI output stays plain.
4
4
  */
5
5
  const enabled = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
6
- const paint = (code: string) => (text: string): string => (enabled ? `\x1b[${code}m${text}\x1b[0m` : text);
6
+ const paint =
7
+ (code: string) =>
8
+ (text: string): string =>
9
+ enabled ? `\x1b[${code}m${text}\x1b[0m` : text;
7
10
 
8
- export const leaf = paint("32"); // green — headings, structure, the accent
9
- export const sprout = paint("92"); // bright green — success
10
- export const dim = paint("2"); // secondary detail
11
+ export const leaf = paint("32"); // green — headings, structure, the accent
12
+ export const sprout = paint("92"); // bright green — success
13
+ export const dim = paint("2"); // secondary detail
11
14
  export const bold = paint("1");
12
- export const amber = paint("33"); // warnings
13
- export const rose = paint("31"); // errors
15
+ export const amber = paint("33"); // warnings
16
+ export const rose = paint("31"); // errors
14
17
 
15
18
  /** "✓ message" with a green tick. */
16
19
  export const ok = (message: string): string => `${sprout("✓")} ${message}`;
package/src/surface.ts CHANGED
@@ -63,43 +63,121 @@ const storageCommands: readonly Command[] = STORAGE_PRODUCTS.map((product) => ({
63
63
  }));
64
64
 
65
65
  export const COMMANDS: readonly Command[] = [
66
- { name: "init", group: "Develop", emoji: "🌱", args: "[name]",
67
- summary: "Scaffold sproutboat.jsonc + src/index.js in ./<name>." },
68
- { name: "check", group: "Develop", emoji: "🔍", args: "[project-dir]",
69
- summary: "Validate the config and entry point without building." },
70
- { name: "dev", group: "Develop", emoji: "⚡", args: "[project-dir] [--port <n>] [--no-watch]", brief: "[project-dir] [--port <n>]",
71
- summary: "Run the project on this machine against a real broker, rebuilding on save." },
72
- { name: "build", group: "Develop", emoji: "🔨", args: "[project-dir] [--target host]", brief: "[project-dir]",
73
- summary: "Cross-compile the native-fetch sprout (Porffor + Zig). `--target host` builds for this machine instead, to run locally — not deployable." },
74
-
75
- { name: "deploy", group: "Ship", emoji: "🚀",
76
- args: "[project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision]", brief: "[project-dir] [--dry-run]",
77
- summary: "Build (unless --artifact), auto-provision id-less storage bindings and pin their ids into sproutboat.jsonc, print the report, upload, wait until the URL serves. The control plane skips an upload that matches the live artifact byte-for-byte. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores." },
78
- { name: "versions", group: "Ship", emoji: "📜", args: "<list | view <version-id>> [project-dir]", brief: "<list | view>",
79
- summary: "List the project's deployed versions, or show one version's artifact and bindings." },
80
- { name: "rollback", group: "Ship", emoji: "⏮", args: "<version-id> [project-dir]", brief: "<version-id>",
81
- summary: "Re-activate a previous version." },
82
- { name: "tail", group: "Ship", emoji: "📡", args: "[project-dir] [--sprout]",
83
- summary: "Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead." },
66
+ {
67
+ name: "init",
68
+ group: "Develop",
69
+ emoji: "🌱",
70
+ args: "[name]",
71
+ summary: "Scaffold sproutboat.jsonc + src/index.js in ./<name>.",
72
+ },
73
+ {
74
+ name: "check",
75
+ group: "Develop",
76
+ emoji: "🔍",
77
+ args: "[project-dir]",
78
+ summary: "Validate the config and entry point without building.",
79
+ },
80
+ {
81
+ name: "dev",
82
+ group: "Develop",
83
+ emoji: "",
84
+ args: "[project-dir] [--port <n>] [--no-watch]",
85
+ brief: "[project-dir] [--port <n>]",
86
+ summary: "Run the project on this machine against a real broker, rebuilding on save.",
87
+ },
88
+ {
89
+ name: "build",
90
+ group: "Develop",
91
+ emoji: "🔨",
92
+ args: "[project-dir] [--target host]",
93
+ brief: "[project-dir]",
94
+ summary:
95
+ "Cross-compile the native-fetch sprout (Porffor + Zig). `--target host` builds for this machine instead, to run locally — not deployable.",
96
+ },
97
+
98
+ {
99
+ name: "deploy",
100
+ group: "Ship",
101
+ emoji: "🚀",
102
+ args: "[project-dir] [--dry-run] [--artifact <dir>] [--no-wait] [--no-provision]",
103
+ brief: "[project-dir] [--dry-run]",
104
+ summary:
105
+ "Build (unless --artifact), auto-provision id-less storage bindings and pin their ids into sproutboat.jsonc, print the report, upload, wait until the URL serves. The control plane skips an upload that matches the live artifact byte-for-byte. --dry-run stops before upload; --no-wait skips the health check; --no-provision leaves id-less bindings as ephemeral deploy-scoped stores.",
106
+ },
107
+ {
108
+ name: "versions",
109
+ group: "Ship",
110
+ emoji: "📜",
111
+ args: "<list | view <version-id>> [project-dir]",
112
+ brief: "<list | view>",
113
+ summary: "List the project's deployed versions, or show one version's artifact and bindings.",
114
+ },
115
+ {
116
+ name: "rollback",
117
+ group: "Ship",
118
+ emoji: "⏮",
119
+ args: "<version-id> [project-dir]",
120
+ brief: "<version-id>",
121
+ summary: "Re-activate a previous version.",
122
+ },
123
+ {
124
+ name: "tail",
125
+ group: "Ship",
126
+ emoji: "📡",
127
+ args: "[project-dir] [--sprout]",
128
+ summary: "Print recent request logs; --sprout prints the running sprout + broker stdout/stderr instead.",
129
+ },
84
130
 
85
131
  ...storageCommands,
86
132
 
87
- { name: "domains", group: "Configure", emoji: "🌐",
88
- args: "<list | add <host> | verify <host> | delete <host>> [project-dir]", brief: "<list | add | verify | delete>",
89
- summary: "Attach a custom domain to the project (TXT-verified). No sub-command lists." },
90
- { name: "secrets", group: "Configure", emoji: "🔑",
91
- args: "<list | put <NAME> [--value <value>] | delete <NAME>> [project-dir]", brief: "<list | put | delete>",
92
- summary: "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." },
93
- { name: "delete", group: "Configure", emoji: "🗑",
94
- args: "[project-dir] [--name <project>] --yes", brief: "[project-dir] --yes",
95
- summary: "Delete the project, every version, and its route." },
96
-
97
- { name: "login", group: "Account", emoji: "🔓", args: "[--api-url <url>] [--token <token>]", brief: "[--token <token>]",
98
- summary: "Device-code browser flow, or store <token> for <url> directly." },
99
- { name: "logout", group: "Account", emoji: "🔒", args: "[--api-url <url>]",
100
- summary: "Forget the stored credential for the active endpoint, or for <url>." },
101
- { name: "whoami", group: "Account", emoji: "👤", args: "",
102
- summary: "Show the active endpoint and the account the stored token belongs to." },
133
+ {
134
+ name: "domains",
135
+ group: "Configure",
136
+ emoji: "🌐",
137
+ args: "<list | add <host> | verify <host> | delete <host>> [project-dir]",
138
+ brief: "<list | add | verify | delete>",
139
+ summary: "Attach a custom domain to the project (TXT-verified). No sub-command lists.",
140
+ },
141
+ {
142
+ name: "secrets",
143
+ group: "Configure",
144
+ emoji: "🔑",
145
+ args: "<list | put <NAME> [--value <value>] | delete <NAME>> [project-dir]",
146
+ brief: "<list | put | delete>",
147
+ summary:
148
+ "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.",
149
+ },
150
+ {
151
+ name: "delete",
152
+ group: "Configure",
153
+ emoji: "🗑",
154
+ args: "[project-dir] [--name <project>] --yes",
155
+ brief: "[project-dir] --yes",
156
+ summary: "Delete the project, every version, and its route.",
157
+ },
158
+
159
+ {
160
+ name: "login",
161
+ group: "Account",
162
+ emoji: "🔓",
163
+ args: "[--api-url <url>] [--token <token>]",
164
+ brief: "[--token <token>]",
165
+ summary: "Device-code browser flow, or store <token> for <url> directly.",
166
+ },
167
+ {
168
+ name: "logout",
169
+ group: "Account",
170
+ emoji: "🔒",
171
+ args: "[--api-url <url>]",
172
+ summary: "Forget the stored credential for the active endpoint, or for <url>.",
173
+ },
174
+ {
175
+ name: "whoami",
176
+ group: "Account",
177
+ emoji: "👤",
178
+ args: "",
179
+ summary: "Show the active endpoint and the account the stored token belongs to.",
180
+ },
103
181
  ];
104
182
 
105
183
  export type EnvVar = { name: string; purpose: string };
@@ -108,18 +186,48 @@ export const ENV_VARS: readonly EnvVar[] = [
108
186
  { name: "SPROUTBOAT_API_URL", purpose: "Control-plane URL. Overrides the saved active endpoint." },
109
187
  { name: "SPROUTBOAT_TOKEN", purpose: "API token. Overrides the saved credential for the endpoint." },
110
188
  { name: "SPROUTBOAT_ZIG", purpose: "Path to a Zig binary to use instead of downloading the pinned one." },
111
- { name: "SPROUTBOAT_UWS_TARBALL", purpose: "Path to a prebuilt uWebSockets (x86_64-linux-musl) tarball to seed the Porffor cache with, instead of downloading it (removes the first-build git + make need)." },
189
+ {
190
+ name: "SPROUTBOAT_UWS_TARBALL",
191
+ purpose:
192
+ "Path to a prebuilt uWebSockets (x86_64-linux-musl) tarball to seed the Porffor cache with, instead of downloading it (removes the first-build git + make need).",
193
+ },
112
194
  { name: "SPROUTBOAT_COMPILE_TIMEOUT_MS", purpose: "Porffor compile timeout in ms (default 600000)." },
113
- { name: "SPROUTBOAT_VARS_JSON", purpose: "JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module." },
114
- { name: "SPROUTBOAT_BINDINGS_JSON", purpose: "The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line." },
195
+ {
196
+ name: "SPROUTBOAT_VARS_JSON",
197
+ purpose:
198
+ "JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module.",
199
+ },
200
+ {
201
+ name: "SPROUTBOAT_BINDINGS_JSON",
202
+ purpose: "The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line.",
203
+ },
115
204
  { name: "SPROUTBOAT_CONFIG_DIR", purpose: "Directory for credentials.json (default ~/.config/sproutboat)." },
116
- { name: "NO_COLOR", purpose: "When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY." },
117
- { name: "SPROUTBOAT_NO_UPDATE_CHECK", purpose: "When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set)." },
205
+ {
206
+ name: "NO_COLOR",
207
+ purpose:
208
+ "When set, disables coloured terminal output (https://no-color.org). Output is also plain whenever stdout is not a TTY.",
209
+ },
210
+ {
211
+ name: "SPROUTBOAT_NO_UPDATE_CHECK",
212
+ purpose: "When set, skips the once-a-day npm check for a newer `sproutboat` release (also skipped when CI is set).",
213
+ },
118
214
  { name: "XDG_CONFIG_HOME", purpose: "Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset." },
119
215
  { name: "PORFFOR_VERSION", purpose: "Override the Porffor identity string recorded in the manifest." },
120
- { name: "SB_BROKER_PORT", purpose: "Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs)." },
121
- { name: "SB_BROKER_TOKEN", purpose: "Per-deployment auth token the sprout sends on every broker frame, and the broker sends back on scheduled/queue triggers (paired with SB_BROKER_PORT)." },
122
- { name: "SB_SPROUT_URL", purpose: "http://127.0.0.1:<PORT> of the sprout; when set, `src/broker.ts` runs the cron scheduler and queue consumer and delivers triggers to it." },
216
+ {
217
+ name: "SB_BROKER_PORT",
218
+ purpose:
219
+ "Loopback port of the binding broker, read by the compiled sprout at runtime (set by the control plane, or by `src/broker.ts` for local runs).",
220
+ },
221
+ {
222
+ name: "SB_BROKER_TOKEN",
223
+ purpose:
224
+ "Per-deployment auth token the sprout sends on every broker frame, and the broker sends back on scheduled/queue triggers (paired with SB_BROKER_PORT).",
225
+ },
226
+ {
227
+ name: "SB_SPROUT_URL",
228
+ purpose:
229
+ "http://127.0.0.1:<PORT> of the sprout; when set, `src/broker.ts` runs the cron scheduler and queue consumer and delivers triggers to it.",
230
+ },
123
231
  ];
124
232
 
125
233
  const GROUP_ORDER: readonly Group[] = ["Develop", "Ship", "Storage", "Configure", "Account"];
package/src/toolchain.ts CHANGED
@@ -49,7 +49,10 @@ const UWS_TARBALL_SHA256 = "e83736f3f8cf9d56a1ebe6ea61625a7af12386763374d47c14cf
49
49
  function platformKey(): ZigPlatform {
50
50
  const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
51
51
  const os = process.platform === "linux" ? "linux" : process.platform === "darwin" ? "macos" : null;
52
- if (!arch || !os) throw new Error(`no pinned Zig for ${process.platform}/${process.arch} — set SPROUTBOAT_ZIG to a zig ${ZIG_VERSION} binary`);
52
+ if (!arch || !os)
53
+ throw new Error(
54
+ `no pinned Zig for ${process.platform}/${process.arch} — set SPROUTBOAT_ZIG to a zig ${ZIG_VERSION} binary`,
55
+ );
53
56
  return `${arch}-${os}`;
54
57
  }
55
58
 
@@ -88,7 +91,10 @@ export async function ensureZig(): Promise<string> {
88
91
  }
89
92
 
90
93
  // `tar -xJ` (xz) works on macOS bsdtar and GNU tar with xz on PATH.
91
- const untar = Bun.spawn(["tar", "-xJf", archive, "-C", dir, "--strip-components=1"], { stdout: "pipe", stderr: "pipe" });
94
+ const untar = Bun.spawn(["tar", "-xJf", archive, "-C", dir, "--strip-components=1"], {
95
+ stdout: "pipe",
96
+ stderr: "pipe",
97
+ });
92
98
  const [code, err] = await Promise.all([untar.exited, new Response(untar.stderr).text()]);
93
99
  if (code !== 0) throw new Error(`could not extract Zig (needs \`tar\` with xz support): ${err.trim()}`);
94
100
  await rm(archive, { force: true });
@@ -144,12 +150,17 @@ export async function ensureUWebSockets(): Promise<void> {
144
150
  if (!process.env.SPROUTBOAT_UWS_TARBALL) {
145
151
  const actual = await sha256File(archive);
146
152
  if (actual !== UWS_TARBALL_SHA256) {
147
- throw new UwsUnavailableError(`vendored uWebSockets sha256 mismatch\n expected ${UWS_TARBALL_SHA256}\n got ${actual}`);
153
+ throw new UwsUnavailableError(
154
+ `vendored uWebSockets sha256 mismatch\n expected ${UWS_TARBALL_SHA256}\n got ${actual}`,
155
+ );
148
156
  }
149
157
  }
150
158
 
151
159
  await mkdir(dir, { recursive: true });
152
- const untar = Bun.spawn(["tar", "-xJf", archive, "-C", dir, "--strip-components=1"], { stdout: "pipe", stderr: "pipe" });
160
+ const untar = Bun.spawn(["tar", "-xJf", archive, "-C", dir, "--strip-components=1"], {
161
+ stdout: "pipe",
162
+ stderr: "pipe",
163
+ });
153
164
  const [code, err] = await Promise.all([untar.exited, new Response(untar.stderr).text()]);
154
165
  if (code !== 0) {
155
166
  await rm(dir, { recursive: true, force: true });
@@ -29,7 +29,11 @@ function cachePath(): string {
29
29
 
30
30
  /** Numeric x.y.z compare; a trailing `-tag` (prerelease) sorts before its release. */
31
31
  function isNewer(latest: string, current: string): boolean {
32
- const parts = (v: string) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
32
+ const parts = (v: string) =>
33
+ v
34
+ .split("-")[0]
35
+ .split(".")
36
+ .map((n) => Number.parseInt(n, 10) || 0);
33
37
  const [a, b] = [parts(latest), parts(current)];
34
38
  for (let i = 0; i < 3; i++) {
35
39
  if ((a[i] ?? 0) !== (b[i] ?? 0)) return (a[i] ?? 0) > (b[i] ?? 0);
@@ -41,16 +45,23 @@ async function latestVersion(): Promise<string | undefined> {
41
45
  try {
42
46
  const cached = parseCache(await readFile(cachePath(), "utf8"));
43
47
  if (cached && Date.now() - cached.checkedAt < CACHE_TTL_MS) return cached.latest;
44
- } catch { /* no cache yet, or unreadable — fetch below */ }
48
+ } catch {
49
+ /* no cache yet, or unreadable — fetch below */
50
+ }
45
51
 
46
52
  try {
47
- const response = await fetch(REGISTRY, { signal: AbortSignal.timeout(1000), headers: { accept: "application/json" } });
53
+ const response = await fetch(REGISTRY, {
54
+ signal: AbortSignal.timeout(1000),
55
+ headers: { accept: "application/json" },
56
+ });
48
57
  if (!response.ok) return undefined;
49
58
  const latest = jsonObject(parseJsonValue(await response.text()))?.version;
50
59
  if (!isString(latest)) return undefined;
51
60
  await writeFile(cachePath(), JSON.stringify({ checkedAt: Date.now(), latest } satisfies Cache)).catch(() => {});
52
61
  return latest;
53
- } catch { /* offline / slow / DNS — skip silently */ }
62
+ } catch {
63
+ /* offline / slow / DNS — skip silently */
64
+ }
54
65
  return undefined;
55
66
  }
56
67
 
@@ -59,6 +70,10 @@ export async function notifyIfOutdated(current: string): Promise<void> {
59
70
  if (process.env.SPROUTBOAT_NO_UPDATE_CHECK || process.env.CI) return;
60
71
  const latest = await latestVersion();
61
72
  if (latest && isNewer(latest, current)) {
62
- console.error(dim(` update available: sproutboat ${current} → ${latest} · bump the dependency or run \`bunx sproutboat@latest\``));
73
+ console.error(
74
+ dim(
75
+ ` update available: sproutboat ${current} → ${latest} · bump the dependency or run \`bunx sproutboat@latest\``,
76
+ ),
77
+ );
63
78
  }
64
79
  }
package/src/wrap.ts CHANGED
@@ -35,12 +35,30 @@ export type Bindings = {
35
35
  assets: string;
36
36
  };
37
37
 
38
- export const EMPTY_BINDINGS: Bindings = { kv: [], secrets: [], outbound: [], d1: [], r2: [], queues: [], analytics: [], do: [], crons: [], assets: "" };
38
+ export const EMPTY_BINDINGS: Bindings = {
39
+ kv: [],
40
+ secrets: [],
41
+ outbound: [],
42
+ d1: [],
43
+ r2: [],
44
+ queues: [],
45
+ analytics: [],
46
+ do: [],
47
+ crons: [],
48
+ assets: "",
49
+ };
39
50
 
40
51
  function hasBindings(b: Bindings): boolean {
41
52
  return (
42
- b.kv.length > 0 || b.secrets.length > 0 || b.outbound.length > 0 || b.d1.length > 0 || b.r2.length > 0 ||
43
- b.queues.length > 0 || b.analytics.length > 0 || b.do.length > 0 || b.assets !== ""
53
+ b.kv.length > 0 ||
54
+ b.secrets.length > 0 ||
55
+ b.outbound.length > 0 ||
56
+ b.d1.length > 0 ||
57
+ b.r2.length > 0 ||
58
+ b.queues.length > 0 ||
59
+ b.analytics.length > 0 ||
60
+ b.do.length > 0 ||
61
+ b.assets !== ""
44
62
  );
45
63
  }
46
64
 
@@ -67,7 +85,10 @@ export function neutraliseExports(source: string): string | null {
67
85
  if (block === undefined) return null;
68
86
  let handler: string | null = null;
69
87
  const aliases: string[] = [];
70
- for (const entry of block[1].split(",").map((part) => part.trim()).filter(Boolean)) {
88
+ for (const entry of block[1]
89
+ .split(",")
90
+ .map((part) => part.trim())
91
+ .filter(Boolean)) {
71
92
  const parts = entry.match(/^(\S+)(?:\s+as\s+(\S+))?$/);
72
93
  if (parts === null) continue;
73
94
  const local = parts[1];
@@ -140,10 +161,13 @@ export function readVarsFromEnv(): Record<string, string> {
140
161
  if (!raw) return {};
141
162
  const parsed: VarsJson = JSON.parse(raw);
142
163
  if (!isVarsObject(parsed)) throw new Error("SPROUTBOAT_VARS_JSON must be a JSON object");
143
- return Object.fromEntries(Object.entries(parsed).map(([key, value]): [string, string] => {
144
- if (!/^[A-Z][A-Z0-9_]*$/.test(key) || !isVarsString(value)) throw new Error(`SPROUTBOAT_VARS_JSON.${key} must map an UPPER_SNAKE name to a string`);
145
- return [key, value];
146
- }));
164
+ return Object.fromEntries(
165
+ Object.entries(parsed).map(([key, value]): [string, string] => {
166
+ if (!/^[A-Z][A-Z0-9_]*$/.test(key) || !isVarsString(value))
167
+ throw new Error(`SPROUTBOAT_VARS_JSON.${key} must map an UPPER_SNAKE name to a string`);
168
+ return [key, value];
169
+ }),
170
+ );
147
171
  }
148
172
 
149
173
  /**