sproutboat 0.2.0 → 0.2.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/README.md CHANGED
@@ -70,9 +70,16 @@ binding, with an Astro UI and a runnable end-to-end harness.
70
70
  ## Requirements
71
71
 
72
72
  - [Bun](https://bun.sh) 1.4+
73
- - `git` and `make` on `PATH` (first build only — compiles uWebSockets once)
74
- - No Docker. Builds cross-compile to a static `linux-x86_64` binary with Porffor
75
- and Zig. Windows: build from WSL.
73
+
74
+ `build` / `deploy` cross-compile the handler to a static `linux-x86_64` binary
75
+ with Porffor and Zig (Zig is fetched automatically on first use). The package
76
+ ships a prebuilt uWebSockets, so nothing else is compiled from source. No Docker,
77
+ no root. On Windows, build from WSL.
78
+
79
+ If that prebuilt is unusable (a `porffor` pin bump before the archive is
80
+ refreshed), the first build falls back to compiling uWebSockets locally, which
81
+ needs `git` and `make` on `PATH`. `SPROUTBOAT_UWS_TARBALL=<archive>` overrides
82
+ the shipped one.
76
83
 
77
84
  ## Limits (v1)
78
85
 
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.2.0 · runs on Bun (use `bunx`, not `npx`)
6
+ **Package:** `sproutboat` 0.2.1 · runs on Bun (use `bunx`, not `npx`)
7
7
 
8
8
  ## Commands
9
9
 
@@ -30,6 +30,7 @@ usage: sproutboat <init [name] | check [project-dir] | build [project-dir] | dep
30
30
  | `SPROUTBOAT_API_URL` | Control-plane URL. Overrides the saved active endpoint. |
31
31
  | `SPROUTBOAT_TOKEN` | API token. Overrides the saved credential for the endpoint. |
32
32
  | `SPROUTBOAT_ZIG` | Path to a Zig binary to use instead of downloading the pinned one. |
33
+ | `SPROUTBOAT_UWS_TARBALL` | 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). |
33
34
  | `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
34
35
  | `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
35
36
  | `XDG_CONFIG_HOME` | Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sproutboat",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Wrangler-shaped CLI for Sproutboat. Deploys workers to any control plane via --api-url / SPROUTBOAT_API_URL.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "files": [
26
26
  "src",
27
27
  "!src/*.test.ts",
28
+ "vendor",
28
29
  "SURFACE.md"
29
30
  ],
30
31
  "engines": {
package/src/compile.ts CHANGED
@@ -4,14 +4,15 @@
4
4
  * `zig cc -target x86_64-linux-musl` and statically links, so the same command
5
5
  * works from macOS, Linux, or WSL with no Docker.
6
6
  *
7
- * One-time per machine: Porffor git-clones uWebSockets and builds `uSockets.a`
8
- * into ~/.cache/porffor/deps/ (needs `git` and `make` on PATH). Later builds
9
- * reuse it and take a few seconds.
7
+ * One-time per machine: the uWebSockets tree Porffor links is unpacked into
8
+ * ~/.cache/porffor/deps/. `ensureUWebSockets()` extracts the prebuilt archive
9
+ * shipped in `vendor/` so this needs no `git` or `make`; if that archive is
10
+ * unusable it falls back to Porffor's own git + make path (needs both on PATH).
10
11
  */
11
12
  import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
12
13
  import { dirname, resolve } from "node:path";
13
14
  import { ensurePorfforPatched } from "./patch-porffor";
14
- import { porfforRoot } from "./toolchain";
15
+ import { ensureUWebSockets, porfforRoot, UwsUnavailableError } from "./toolchain";
15
16
 
16
17
  const preludePath = new URL("./native-fetch-prelude.js", import.meta.url);
17
18
  // The server honours $PORT at runtime (patches/porffor-render.patch); this baked
@@ -99,6 +100,28 @@ export type CompileInput = {
99
100
  /** Compile `sourcePath` to a native binary at `outPath` (mode 0555). */
100
101
  export async function compileWorker(input: CompileInput): Promise<void> {
101
102
  await ensurePorfforPatched();
103
+
104
+ // Seed the Porffor uWebSockets cache from the prebuilt archive in `vendor/` so
105
+ // the first build needs no `git` / `make`. Fall back to Porffor's own git+make
106
+ // path if the archive is missing or fails its checksum.
107
+ try {
108
+ await ensureUWebSockets();
109
+ } catch (error) {
110
+ if (!(error instanceof UwsUnavailableError)) throw error;
111
+ const haveGit = Bun.which("git");
112
+ const haveMake = Bun.which("make");
113
+ if (haveGit && haveMake) {
114
+ console.warn(`prebuilt uWebSockets unusable (${error.message.split("\n")[0]}); falling back to git + make (slower, one-time)`);
115
+ } else {
116
+ const missing = [!haveGit && "git", !haveMake && "make"].filter(Boolean).join(" and ");
117
+ throw new Error(
118
+ `${error.message}\n\nThe prebuilt uWebSockets is unusable, and ${missing} ` +
119
+ `${missing.includes("and") ? "are" : "is"} not on PATH for the fallback build. ` +
120
+ `Install ${missing}, or set SPROUTBOAT_UWS_TARBALL to a valid archive.`,
121
+ );
122
+ }
123
+ }
124
+
102
125
  const outDir = dirname(input.outPath);
103
126
  await mkdir(outDir, { recursive: true });
104
127
  const generatedPath = resolve(outDir, "worker.generated.js");
package/src/surface.ts CHANGED
@@ -25,6 +25,7 @@ export const ENV_VARS: readonly EnvVar[] = [
25
25
  { name: "SPROUTBOAT_API_URL", purpose: "Control-plane URL. Overrides the saved active endpoint." },
26
26
  { name: "SPROUTBOAT_TOKEN", purpose: "API token. Overrides the saved credential for the endpoint." },
27
27
  { name: "SPROUTBOAT_ZIG", purpose: "Path to a Zig binary to use instead of downloading the pinned one." },
28
+ { 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)." },
28
29
  { name: "SPROUTBOAT_COMPILE_TIMEOUT_MS", purpose: "Porffor compile timeout in ms (default 600000)." },
29
30
  { name: "SPROUTBOAT_CONFIG_DIR", purpose: "Directory for credentials.json (default ~/.config/sproutboat)." },
30
31
  { name: "XDG_CONFIG_HOME", purpose: "Base for the default credentials dir when SPROUTBOAT_CONFIG_DIR is unset." },
package/src/toolchain.ts CHANGED
@@ -30,6 +30,14 @@ const PORFFOR_COMMIT = "a415d19";
30
30
  // uWebSockets commit Porffor alpha-4 fetches for the native-fetch server. Read
31
31
  // from node_modules/porffor at build time; this is the fallback for the stamp.
32
32
  const UWS_COMMIT = "360c276d";
33
+ const UWS_COMMIT_FULL = "360c276d609d59af56ae6932adb95154ace9f15f";
34
+
35
+ // `vendor/uwebsockets-<UWS_COMMIT>-musl.tar.xz` ships in the package: the
36
+ // checked-out, patched, `zig cc -target x86_64-linux-musl`-built uWebSockets
37
+ // tree (headers + `uSockets/uSockets.a`). Regenerate + re-pin the sha whenever
38
+ // the `porffor` pin (and thus UWS_COMMIT_FULL) changes — `bun tools/prebuild-uws.ts`
39
+ // or the `uws-prebuild` workflow.
40
+ const UWS_TARBALL_SHA256 = "e83736f3f8cf9d56a1ebe6ea61625a7af12386763374d47c14cff472ada7484a";
33
41
 
34
42
  function platformKey(): string {
35
43
  const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
@@ -82,6 +90,66 @@ export async function ensureZig(): Promise<string> {
82
90
  return bin;
83
91
  }
84
92
 
93
+ function uwsCommitFull(): string {
94
+ try {
95
+ const src = readFileSync(resolve(porfforRoot(), "compiler/uwebsockets.js"), "utf8");
96
+ return /UWS_COMMIT\s*=\s*['"]([0-9a-f]{40})/.exec(src)?.[1] ?? UWS_COMMIT_FULL;
97
+ } catch {
98
+ return UWS_COMMIT_FULL;
99
+ }
100
+ }
101
+
102
+ /** Thrown when the prebuilt uWebSockets archive is missing or fails its checksum. */
103
+ export class UwsUnavailableError extends Error {}
104
+
105
+ /** Path to the vendored prebuilt archive for the given short commit. */
106
+ export function uwsVendorArchive(short: string): string {
107
+ return resolve(import.meta.dir, "..", "vendor", `uwebsockets-${short}-musl.tar.xz`);
108
+ }
109
+
110
+ /**
111
+ * Seed `~/.cache/porffor/deps/uWebSockets-<commit>-musl/` with the checked-out,
112
+ * patched, `x86_64-linux-musl`-built uWebSockets tree so Porffor's own
113
+ * `ensureUWebSockets` / `ensureUSocketsBuilt` short-circuit — the first build
114
+ * then needs no `git` and no `make`, only Zig.
115
+ *
116
+ * The archive ships in the package (`vendor/`). No-ops if the cache is already
117
+ * populated. Throws `UwsUnavailableError` if the archive is missing or fails its
118
+ * checksum; the caller decides whether to fall back to Porffor's git + make path.
119
+ *
120
+ * `SPROUTBOAT_UWS_TARBALL=/path/to/archive.tar.xz` overrides the vendored one.
121
+ */
122
+ export async function ensureUWebSockets(): Promise<void> {
123
+ const commit = uwsCommitFull();
124
+ const short = commit.slice(0, 8);
125
+ const depsRoot = resolve(homedir(), ".cache/porffor/deps");
126
+ const dir = resolve(depsRoot, `uWebSockets-${commit}-musl`);
127
+ if (existsSync(resolve(dir, "src/App.h")) && existsSync(resolve(dir, "uSockets/uSockets.a"))) return;
128
+
129
+ const archive = process.env.SPROUTBOAT_UWS_TARBALL || uwsVendorArchive(short);
130
+ if (!existsSync(archive)) {
131
+ throw new UwsUnavailableError(
132
+ process.env.SPROUTBOAT_UWS_TARBALL
133
+ ? `SPROUTBOAT_UWS_TARBALL=${archive} does not exist`
134
+ : `no vendored uWebSockets archive at ${archive} (porffor pin moved? run \`bun tools/prebuild-uws.ts\`)`,
135
+ );
136
+ }
137
+ if (!process.env.SPROUTBOAT_UWS_TARBALL) {
138
+ const actual = await sha256File(archive);
139
+ if (actual !== UWS_TARBALL_SHA256) {
140
+ throw new UwsUnavailableError(`vendored uWebSockets sha256 mismatch\n expected ${UWS_TARBALL_SHA256}\n got ${actual}`);
141
+ }
142
+ }
143
+
144
+ await mkdir(dir, { recursive: true });
145
+ const untar = Bun.spawn(["tar", "-xJf", archive, "-C", dir, "--strip-components=1"], { stdout: "pipe", stderr: "pipe" });
146
+ const [code, err] = await Promise.all([untar.exited, new Response(untar.stderr).text()]);
147
+ if (code !== 0) {
148
+ await rm(dir, { recursive: true, force: true });
149
+ throw new UwsUnavailableError(`could not extract vendored uWebSockets: ${err.trim()}`);
150
+ }
151
+ }
152
+
85
153
  /** Directory holding node_modules/porffor (walks up from this file). */
86
154
  export function porfforRoot(start = import.meta.dir): string {
87
155
  let dir = start;