sproutboat 0.19.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 +58 -0
- package/SURFACE.md +2 -2
- package/package.json +8 -8
- package/scripts/with-local-packages.ts +193 -0
- package/src/bundle.ts +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,64 @@ 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
|
+
|
|
34
|
+
## [0.20.0] - 2026-10-21
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- Workers RPC over service bindings. A target whose default export extends
|
|
39
|
+
`WorkerEntrypoint` (from `cloudflare:workers`) exposes its methods:
|
|
40
|
+
`await env.SERVICE.add(2, 3)` runs `add(2, 3)` on it and resolves to the result.
|
|
41
|
+
- Arguments and results are copied as structured clone does (`Date`, binary data,
|
|
42
|
+
`Map`, `Set`, `undefined`, `NaN`, `-0`).
|
|
43
|
+
- An error the method throws rejects the call with the same class and message.
|
|
44
|
+
- `cloudflare:workers` also provides `DurableObject`, `RpcTarget` and `env`.
|
|
45
|
+
- Compatibility date `2026-10-22` changes how `fetch()` and `env.SERVICE.fetch()`
|
|
46
|
+
behave in a deployed app:
|
|
47
|
+
- They return promises and no longer block. The app keeps serving other requests
|
|
48
|
+
while a call is in flight, concurrent calls overlap, and a deployment can call
|
|
49
|
+
itself through a binding.
|
|
50
|
+
- Responses stream: the call resolves when the headers arrive, and `response.body`
|
|
51
|
+
receives chunks as they come.
|
|
52
|
+
- Redirects are followed as Fetch does (`redirect: "manual"` and `"error"` work).
|
|
53
|
+
- A service-binding target that throws makes the caller throw the same error, and
|
|
54
|
+
the target sees the URL its caller passed.
|
|
55
|
+
|
|
56
|
+
Apps with an earlier `compatibility_date` keep getting a `Response` right away, as
|
|
57
|
+
before. Move the date to `2026-10-22` and rebuild to opt in.
|
|
58
|
+
- `fetch()` and service bindings honour `init.signal`. Aborting stops the work
|
|
59
|
+
downstream too.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- Every `Set-Cookie` header of a fetched response is kept, not just the last one.
|
|
64
|
+
- A fetched response without a `Content-Type` no longer gains `text/plain`.
|
|
65
|
+
- `atob()` and `btoa()` were wrong for strings from `JSON.parse` or `TextDecoder`;
|
|
66
|
+
they are now correct for any string.
|
|
67
|
+
- A `fetch` or service call is no longer retried once the broker may have run it.
|
|
68
|
+
|
|
69
|
+
toolchain 0.11.0, runtime 0.21.1, wire 0.14.
|
|
70
|
+
|
|
13
71
|
## [0.19.0] - 2026-10-21
|
|
14
72
|
|
|
15
73
|
### 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.
|
|
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/
|
|
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.
|
|
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",
|
|
@@ -64,9 +64,9 @@
|
|
|
64
64
|
"@sproutboat/artifact": "^0.3.0",
|
|
65
65
|
"@sproutboat/assets": "^0.3.0",
|
|
66
66
|
"@sproutboat/config": "^0.5.0",
|
|
67
|
-
"@sproutboat/runtime": "^0.
|
|
68
|
-
"@sproutboat/toolchain": "^0.
|
|
69
|
-
"@sproutboat/wire": "^0.
|
|
67
|
+
"@sproutboat/runtime": "^0.21.1",
|
|
68
|
+
"@sproutboat/toolchain": "^0.12.0",
|
|
69
|
+
"@sproutboat/wire": "^0.14.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@oxlint/plugins": "1.85.0",
|
|
@@ -78,10 +78,10 @@
|
|
|
78
78
|
"typescript": "7.0.2"
|
|
79
79
|
},
|
|
80
80
|
"optionalDependencies": {
|
|
81
|
-
"@sproutboat/cli-darwin-arm64": "0.
|
|
82
|
-
"@sproutboat/cli-darwin-x64": "0.
|
|
83
|
-
"@sproutboat/cli-linux-arm64": "0.
|
|
84
|
-
"@sproutboat/cli-linux-x64": "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
|
+
}
|
package/src/bundle.ts
CHANGED
|
@@ -79,6 +79,9 @@ export async function bundleHandler(entryPath: string, projectDir: string): Prom
|
|
|
79
79
|
splitting: false,
|
|
80
80
|
sourcemap: "none",
|
|
81
81
|
plugins: [nodeShimsPlugin],
|
|
82
|
+
// WorkerEntrypoint, DurableObject, RpcTarget and env are runtime globals; the
|
|
83
|
+
// compiler turns this import into them, so it stays in the bundle as written.
|
|
84
|
+
external: ["cloudflare:workers"],
|
|
82
85
|
});
|
|
83
86
|
} catch (cause) {
|
|
84
87
|
// An unresolvable specifier arrives as an AggregateError whose `errors`
|