sproutboat 0.20.0 → 0.21.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/CHANGELOG.md CHANGED
@@ -10,6 +10,27 @@ maintained going forward by the `release` skill.
10
10
 
11
11
  ## Unreleased
12
12
 
13
+ ## [0.21.0] - 2026-10-21
14
+
15
+ ### Changed
16
+
17
+ - Apps compile with Porffor Alpha 19 (`@sproutboat/toolchain` 0.12.0).
18
+ Binaries are about 1.5% smaller, and build times are unchanged.
19
+ - Alpha 19 brings JavaScript closer to V8, and the changes apply at every
20
+ compatibility date:
21
+ - `Error.prototype.stack` is an accessor;
22
+ - `instanceof` follows the spec, including `Symbol.hasInstance`;
23
+ - sloppy-mode functions box a primitive `this`;
24
+ - undeclared names are read from the global object;
25
+ - using a `let`, `const` or class binding before its declaration throws.
26
+
27
+ ### Fixed
28
+
29
+ - `JSON.parse` with a reviver now gives the reviver the right `context.source`
30
+ when the input contains non-ASCII text.
31
+ - `Map.groupBy` works, and private class fields no longer show up as
32
+ enumerable properties.
33
+
13
34
  ## [0.20.0] - 2026-10-21
14
35
 
15
36
  ### Added
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.20.0
6
+ **Package:** `sproutboat` 0.21.0
7
7
 
8
8
  ## Commands
9
9
 
@@ -76,7 +76,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
76
76
  | | |
77
77
  | --- | --- |
78
78
  | Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
79
- | Provenance stamp | `zig-musl/0.16.0+porffor/deda494+uws/360c276d` |
79
+ | Provenance stamp | `zig-musl/0.16.0+porffor/0c135ce+uws/360c276d` |
80
80
  | Artifact schema | `2` |
81
81
  | Runtime | `native-fetch` |
82
82
  | Capability profile | `http-sync-v0` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.20.0",
3
+ "version": "0.21.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",
@@ -65,7 +65,7 @@
65
65
  "@sproutboat/assets": "^0.3.0",
66
66
  "@sproutboat/config": "^0.5.0",
67
67
  "@sproutboat/runtime": "^0.21.1",
68
- "@sproutboat/toolchain": "^0.11.0",
68
+ "@sproutboat/toolchain": "^0.12.0",
69
69
  "@sproutboat/wire": "^0.14.0"
70
70
  },
71
71
  "devDependencies": {
@@ -78,10 +78,10 @@
78
78
  "typescript": "7.0.2"
79
79
  },
80
80
  "optionalDependencies": {
81
- "@sproutboat/cli-darwin-arm64": "0.20.0",
82
- "@sproutboat/cli-darwin-x64": "0.20.0",
83
- "@sproutboat/cli-linux-arm64": "0.20.0",
84
- "@sproutboat/cli-linux-x64": "0.20.0"
81
+ "@sproutboat/cli-darwin-arm64": "0.21.0",
82
+ "@sproutboat/cli-darwin-x64": "0.21.0",
83
+ "@sproutboat/cli-linux-arm64": "0.21.0",
84
+ "@sproutboat/cli-linux-x64": "0.21.0"
85
85
  },
86
86
  "engines": {
87
87
  "node": ">=18"
@@ -0,0 +1,193 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Run a command against local `@sproutboat/{runtime,toolchain,wire}` checkouts.
4
+ *
5
+ * bun scripts/with-local-packages.ts <path-to-sproutboat-packages> [--only runtime,toolchain,wire] -- <command...>
6
+ *
7
+ * Moves each `node_modules/@sproutboat/<pkg>` aside, symlinks
8
+ * `<packages>/packages/<pkg>` in its place, runs the command with inherited
9
+ * stdio, then puts the original entries back. The restore runs whatever the
10
+ * command does: a non-zero exit, a throw here, SIGINT or SIGTERM. Only SIGKILL
11
+ * can skip it, and the next run repairs that before it starts (the moved-aside
12
+ * entry is left in a known place for exactly that reason).
13
+ *
14
+ * Refuses to run in the main checkout when it has uncommitted changes. Work in
15
+ * progress lives there, and anything already running from it (a dev server, a
16
+ * watcher) would silently pick up the linked packages. Use a worktree instead.
17
+ */
18
+ import { existsSync, lstatSync, readlinkSync, renameSync, rmSync, symlinkSync } from "node:fs";
19
+ import { resolve } from "node:path";
20
+
21
+ const PACKAGES = ["runtime", "toolchain", "wire"] as const;
22
+ type Pkg = (typeof PACKAGES)[number];
23
+ const isPkg = (value: string): value is Pkg => PACKAGES.some((pkg) => pkg === value);
24
+
25
+ const USAGE =
26
+ "usage: bun scripts/with-local-packages.ts <path-to-sproutboat-packages> [--only runtime,toolchain,wire] -- <command...>";
27
+
28
+ function fail(message: string): never {
29
+ console.error(`with-local-packages: ${message}`);
30
+ process.exit(2);
31
+ }
32
+
33
+ // ---- arguments ----------------------------------------------------------
34
+
35
+ const argv = process.argv.slice(2);
36
+ const separator = argv.indexOf("--");
37
+ if (separator === -1) fail(`missing "--" before the command\n${USAGE}`);
38
+ const ours = argv.slice(0, separator);
39
+ const command = argv.slice(separator + 1);
40
+ if (command.length === 0) fail(`no command after "--"\n${USAGE}`);
41
+
42
+ let packagesRoot: string | undefined;
43
+ let only: Pkg[] | undefined;
44
+ for (let i = 0; i < ours.length; i++) {
45
+ const arg = ours[i];
46
+ if (arg === "--only" || arg.startsWith("--only=")) {
47
+ const value = arg === "--only" ? ours[++i] : arg.slice("--only=".length);
48
+ if (!value) fail(`--only needs a value\n${USAGE}`);
49
+ const names = value.split(",").map((name) => name.trim());
50
+ const unknown = names.filter((name) => !isPkg(name));
51
+ if (unknown.length) fail(`unknown package ${unknown.join(", ")} (expected ${PACKAGES.join(", ")})`);
52
+ only = names.filter(isPkg);
53
+ } else if (arg === "-h" || arg === "--help") {
54
+ console.log(USAGE);
55
+ process.exit(0);
56
+ } else if (arg.startsWith("-")) {
57
+ fail(`unknown option ${arg}\n${USAGE}`);
58
+ } else if (packagesRoot === undefined) {
59
+ packagesRoot = resolve(arg);
60
+ } else {
61
+ fail(`unexpected argument ${arg}\n${USAGE}`);
62
+ }
63
+ }
64
+ if (!packagesRoot) fail(`missing the path to a sproutboat-packages checkout\n${USAGE}`);
65
+ if (!existsSync(resolve(packagesRoot, "packages")))
66
+ fail(`${packagesRoot} has no packages/ directory: is it a sproutboat-packages checkout?`);
67
+
68
+ // ---- safety: never touch a dirty main checkout --------------------------
69
+
70
+ const cliRoot = resolve(import.meta.dir, "..");
71
+ function git(...args: string[]): string {
72
+ const result = Bun.spawnSync(["git", "-C", cliRoot, ...args], { stdout: "pipe", stderr: "pipe" });
73
+ if (result.exitCode !== 0) fail(`git ${args.join(" ")} failed: ${result.stderr.toString().trim()}`);
74
+ return result.stdout.toString().trim();
75
+ }
76
+ const gitDir = resolve(cliRoot, git("rev-parse", "--git-dir"));
77
+ const commonDir = resolve(cliRoot, git("rev-parse", "--git-common-dir"));
78
+ const linkedWorktree = gitDir !== commonDir;
79
+ if (!linkedWorktree && git("status", "--porcelain") !== "") {
80
+ fail(
81
+ `refusing to run: ${cliRoot} is the main checkout and has uncommitted changes.\n` +
82
+ "Relinking its node_modules would change what that work runs against. Use a worktree:\n" +
83
+ ` git -C ${cliRoot} worktree add /tmp/sb-cli-local origin/main\n` +
84
+ " cd /tmp/sb-cli-local && bun install --frozen-lockfile\n" +
85
+ ` bun scripts/with-local-packages.ts ${packagesRoot} -- <command...>`,
86
+ );
87
+ }
88
+
89
+ // ---- link ---------------------------------------------------------------
90
+
91
+ const scope = resolve(cliRoot, "node_modules/@sproutboat");
92
+ const entryPath = (pkg: Pkg) => resolve(scope, pkg);
93
+ const backupPath = (pkg: Pkg) => resolve(scope, `.${pkg}.with-local-packages-backup`);
94
+ const exists = (path: string) => {
95
+ try {
96
+ lstatSync(path);
97
+ return true;
98
+ } catch {
99
+ return false;
100
+ }
101
+ };
102
+
103
+ /** Put one package's original entry back. Synchronous so the exit hook can use it. */
104
+ function restore(pkg: Pkg): void {
105
+ const entry = entryPath(pkg);
106
+ const backup = backupPath(pkg);
107
+ if (!exists(backup)) return;
108
+ if (exists(entry)) {
109
+ // Only ever remove our own symlink, never a real directory.
110
+ if (!lstatSync(entry).isSymbolicLink()) {
111
+ console.error(`with-local-packages: ${entry} is not a symlink, leaving ${backup} for you to inspect`);
112
+ return;
113
+ }
114
+ rmSync(entry);
115
+ }
116
+ renameSync(backup, entry);
117
+ }
118
+
119
+ // A run killed with SIGKILL leaves a backup behind: repair that first.
120
+ for (const pkg of PACKAGES) {
121
+ if (exists(backupPath(pkg))) {
122
+ console.error(`with-local-packages: restoring node_modules/@sproutboat/${pkg} left over from an interrupted run`);
123
+ restore(pkg);
124
+ }
125
+ }
126
+
127
+ const requested = only ?? PACKAGES.filter((pkg) => existsSync(resolve(packagesRoot!, "packages", pkg, "package.json")));
128
+ if (requested.length === 0) fail(`no runtime, toolchain or wire package under ${packagesRoot}/packages`);
129
+ for (const pkg of requested) {
130
+ if (!existsSync(resolve(packagesRoot, "packages", pkg, "package.json")))
131
+ fail(`${packagesRoot}/packages/${pkg} has no package.json`);
132
+ if (!exists(entryPath(pkg))) fail(`node_modules/@sproutboat/${pkg} is missing: run bun install first`);
133
+ }
134
+ if (!existsSync(resolve(packagesRoot, "node_modules")))
135
+ console.error(
136
+ `with-local-packages: warning: ${packagesRoot} has no node_modules; ` +
137
+ "the linked packages resolve their own imports from there, so run bun install in it if anything fails to resolve",
138
+ );
139
+
140
+ const linked: Pkg[] = [];
141
+ let restored = false;
142
+ function restoreAll(): void {
143
+ if (restored) return;
144
+ restored = true;
145
+ for (const pkg of linked.toReversed()) {
146
+ try {
147
+ restore(pkg);
148
+ } catch (error) {
149
+ console.error(`with-local-packages: could not restore ${pkg}: ${String(error)}`);
150
+ process.exitCode = 1;
151
+ }
152
+ }
153
+ const leftover = linked.filter((pkg) => exists(backupPath(pkg)));
154
+ if (leftover.length === 0) console.error(`with-local-packages: restored ${linked.join(", ")}`);
155
+ }
156
+ // The last line of defence: runs on any exit this process gets to see.
157
+ process.on("exit", restoreAll);
158
+
159
+ let child: ReturnType<typeof Bun.spawn> | undefined;
160
+ let signalled: NodeJS.Signals | undefined;
161
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
162
+ process.on(signal, () => {
163
+ // A second signal means "stop now": kill the command, the restore still runs.
164
+ if (signalled && child) child.kill("SIGKILL");
165
+ signalled = signal;
166
+ // Ctrl-C already reached the command through the process group; SIGTERM
167
+ // and SIGHUP sent to this process alone are passed on.
168
+ if (child && signal !== "SIGINT") child.kill(signal);
169
+ if (!child) {
170
+ restoreAll();
171
+ process.exit(128 + (signal === "SIGINT" ? 2 : signal === "SIGTERM" ? 15 : 1));
172
+ }
173
+ });
174
+ }
175
+
176
+ try {
177
+ for (const pkg of requested) {
178
+ const target = resolve(packagesRoot, "packages", pkg);
179
+ renameSync(entryPath(pkg), backupPath(pkg));
180
+ linked.push(pkg);
181
+ symlinkSync(target, entryPath(pkg), "dir");
182
+ console.error(`with-local-packages: @sproutboat/${pkg} -> ${readlinkSync(entryPath(pkg))}`);
183
+ }
184
+
185
+ child = Bun.spawn(command, { cwd: process.cwd(), stdio: ["inherit", "inherit", "inherit"] });
186
+ const code = await child.exited;
187
+ restoreAll();
188
+ if (signalled) process.exit(128 + (signalled === "SIGINT" ? 2 : signalled === "SIGTERM" ? 15 : 1));
189
+ process.exit(child.signalCode ? 1 : code);
190
+ } catch (error) {
191
+ restoreAll();
192
+ fail(error instanceof Error ? error.message : String(error));
193
+ }