sproutboat 0.9.0 → 0.10.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/CHANGELOG.md +80 -1
- package/README.md +20 -7
- package/SURFACE.md +6 -2
- package/bin/sproutboat.cjs +50 -0
- package/package.json +13 -6
- package/scripts/build-cli.ts +44 -0
- package/src/assets.ts +4 -121
- package/src/broker.ts +4 -1182
- package/src/build.ts +24 -17
- package/src/compile.ts +51 -19
- package/src/config.ts +4 -379
- package/src/dev.ts +263 -62
- package/src/json.ts +3 -38
- package/src/main.ts +39 -14
- package/src/manifest.ts +4 -167
- package/src/patch-porffor.ts +5 -144
- package/src/porffor-toolchain.ts +5 -0
- package/src/report.ts +8 -7
- package/src/source.ts +4 -44
- package/src/surface.ts +16 -0
- package/src/toolchain.ts +157 -44
- package/src/types-gen.ts +3 -0
- package/src/wrap.ts +5 -267
- package/types/sproutboat.d.ts +31 -0
- package/src/native-fetch-prelude.js +0 -950
- package/src/transport-broker.js +0 -299
- package/src/transport-embedded.js +0 -1571
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,82 @@ maintained going forward by the `release` skill.
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [0.10.1] - 2026-09-11
|
|
14
|
+
### Fixed
|
|
15
|
+
- `npm install sproutboat` / `bunx sproutboat` work again. 0.10.0's launcher
|
|
16
|
+
hard-required per-platform `@sproutboat/cli-*` packages that are not published
|
|
17
|
+
yet, so it always failed with "optional package … is missing". The launcher
|
|
18
|
+
now falls back to running `src/main.ts` with Bun when no platform binary is
|
|
19
|
+
installed (the v0.9.0 model), and `optionalDependencies` are dropped until the
|
|
20
|
+
binaries actually ship. 0.10.0 is deprecated on npm.
|
|
21
|
+
- `sproutboat --version` and the deploy banner report the real version instead
|
|
22
|
+
of a hard-coded `0.9.0` when run from the npm package rather than a
|
|
23
|
+
`bun build --compile` binary.
|
|
24
|
+
- `sproutboat build` / `deploy` locate `esbuild` via the installed dependency,
|
|
25
|
+
not just `process.execPath`'s directory, so bundling works on the Bun
|
|
26
|
+
fallback path.
|
|
27
|
+
- The launcher no longer hangs when it receives `SIGINT` / `SIGTERM` while
|
|
28
|
+
forwarding a signal to a running platform binary.
|
|
29
|
+
|
|
30
|
+
## [0.10.0] - 2026-09-11
|
|
31
|
+
### Added
|
|
32
|
+
- Per-platform native CLI: `npm install sproutboat` pulls a prebuilt binary for
|
|
33
|
+
the host (`@sproutboat/cli-{darwin,linux}-{arm64,x64}`) behind a thin launcher
|
|
34
|
+
that preserves signals and embeds its own version, so the CLI runs without Bun
|
|
35
|
+
on `PATH`.
|
|
36
|
+
- Porffor is fetched on the first build, not installed as a dependency: the
|
|
37
|
+
pinned commit is downloaded into `~/.cache/sproutboat`, SHA-256 verified, and
|
|
38
|
+
patched there. Installation clones nothing and runs no Git, Make or compiler.
|
|
39
|
+
- `request.cf.clientIp` in standalone builds, from a server-set
|
|
40
|
+
`x-sb-remote-addr`, with `SB_TRUSTED_PROXIES` for `X-Forwarded-For` resolution
|
|
41
|
+
behind a reverse proxy (baronunread/sproutboat#163).
|
|
42
|
+
- `env.<D1>.backup(name?)` — an online, integrity-checked single-file snapshot
|
|
43
|
+
of a D1 database via `VACUUM INTO`, on both the embedded and broker transports
|
|
44
|
+
(baronunread/sproutboat#164).
|
|
45
|
+
- Rate Limiting binding: `ratelimiters: [{ binding, limit, period }]` in
|
|
46
|
+
`sproutboat.jsonc` gives `env.<NAME>.limit({ key }) -> { success }`, a
|
|
47
|
+
fixed-window counter on both transports (baronunread/sproutboat#69).
|
|
48
|
+
- `crypto.subtle` subset: `digest` (SHA-256/384/512) and HMAC
|
|
49
|
+
`importKey` / `sign` / `verify`, backed by reference SHA-2 as inline C so it
|
|
50
|
+
works on both transports (baronunread/sproutboat#133). No ECDSA or AES yet.
|
|
51
|
+
- `crypto.scryptVerify(password, salt, expected, { N, r, p })` — a verify-only
|
|
52
|
+
scrypt (RFC 7914) for migrating password hashes made by Node/Bun `scrypt`
|
|
53
|
+
(baronunread/sproutboat#153). Not a blessed KDF for new credentials.
|
|
54
|
+
- `x-sb-cpu-ms`: per-invocation CPU time, carried on the handler response.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
- `303` (and every other status not in Porffor's table) no longer resets the
|
|
58
|
+
connection on standalone builds (baronunread/sproutboat#156).
|
|
59
|
+
- Handler `console.log` / `console.error` reach stderr, unbuffered, in
|
|
60
|
+
standalone builds instead of vanishing (baronunread/sproutboat#165).
|
|
61
|
+
- `sproutboat dev` no longer leaves the previous sprout running across a
|
|
62
|
+
rebuild, delivers no triggers to a candidate that failed to start, and cleans
|
|
63
|
+
up every failed setup path on rebuild and on shutdown.
|
|
64
|
+
- The Zig toolchain cache is hardened against a partial or concurrent download.
|
|
65
|
+
|
|
66
|
+
### Changed
|
|
67
|
+
- Porffor pin bumped alpha-4 → **alpha-5** (`1f4ae4ae`); the same `UWS_COMMIT`,
|
|
68
|
+
so no uWebSockets re-vendor.
|
|
69
|
+
- Config parsing, the artifact manifest, the binding broker, the wire assets and
|
|
70
|
+
the whole native-fetch runtime (prelude + transports) now come from published
|
|
71
|
+
`@sproutboat/*` packages; the CLI keeps thin re-export shims. The Porffor pin
|
|
72
|
+
and its source patches moved to `@sproutboat/toolchain`.
|
|
73
|
+
- Broker cron / queue / alarm delivery is gated by a local signal, so only a
|
|
74
|
+
promoted candidate runs timers.
|
|
75
|
+
|
|
76
|
+
### Performance
|
|
77
|
+
- Embedded transport caches prepared statements per database (FIFO, 32/db)
|
|
78
|
+
instead of recompiling the SQL on every binding op — the op boundary drops
|
|
79
|
+
from ~0.6 ms to tens of µs (baronunread/sproutboat#155).
|
|
80
|
+
- The broker's due-queue and due-alarm polls are indexed.
|
|
81
|
+
|
|
82
|
+
### Docs
|
|
83
|
+
- `docs/standalone.md` documents the single-threaded execution model and the
|
|
84
|
+
`SO_REUSEPORT` multi-process recipe for scaling past one core
|
|
85
|
+
(baronunread/sproutboat#154); the embedded transport also sets
|
|
86
|
+
`PRAGMA busy_timeout` so shared-data-dir writers wait instead of failing.
|
|
87
|
+
Also: the client-IP behaviour and backing up D1.
|
|
88
|
+
|
|
13
89
|
## [0.9.0] - 2026-09-07
|
|
14
90
|
### Added
|
|
15
91
|
- KV content management: `kv key get`, `put`, `delete`, and `list`; bounded
|
|
@@ -282,7 +358,10 @@ its own package.
|
|
|
282
358
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
283
359
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
284
360
|
|
|
285
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.
|
|
361
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.1...HEAD
|
|
362
|
+
[0.10.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.10.0...v0.10.1
|
|
363
|
+
[0.10.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.9.0...v0.10.0
|
|
364
|
+
[0.9.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.8.0...v0.9.0
|
|
286
365
|
[0.8.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.7.0...v0.8.0
|
|
287
366
|
[0.7.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.1...v0.7.0
|
|
288
367
|
[0.6.1]: https://github.com/baronunread/sproutboat-cli/compare/v0.6.0...v0.6.1
|
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@ https://sproutboat.com
|
|
|
8
8
|
The CLI for [Sproutboat](https://github.com/baronunread/sproutboat). Compiles a
|
|
9
9
|
`fetch` handler to a native binary and ships it to any Sproutboat control plane.
|
|
10
10
|
|
|
11
|
+
## Issues
|
|
12
|
+
|
|
13
|
+
Tracked centrally in [baronunread/sproutboat](https://github.com/baronunread/sproutboat/issues)
|
|
14
|
+
(label `area:cli`). Please file there.
|
|
15
|
+
|
|
11
16
|
## Overview
|
|
12
17
|
|
|
13
18
|
Wrangler-shaped, MIT licensed. `build` and `deploy` cross-compile your handler
|
|
@@ -19,16 +24,23 @@ for agents: [sproutboat.com/llms.txt](https://sproutboat.com/llms.txt)).
|
|
|
19
24
|
|
|
20
25
|
## Using
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
The npm package installs a small Node launcher and the matching native
|
|
28
|
+
platform package, so `npm install -g sproutboat` and `npm exec sproutboat` work
|
|
29
|
+
without Bun on `darwin` and `linux` for `arm64` and `x64`. The self-hosted
|
|
30
|
+
runtime exports remain in the root package for Bun-based platform integrations.
|
|
25
31
|
Nothing is needed on the machine that *runs* a sprout: that gets a static
|
|
26
32
|
binary.
|
|
27
33
|
|
|
34
|
+
The first build downloads the pinned Porffor source into
|
|
35
|
+
`~/.cache/sproutboat`, verifies its SHA-256, applies Sproutboat's compiler
|
|
36
|
+
patches there, and publishes the cache entry atomically. Installation itself
|
|
37
|
+
does not clone Porffor or invoke Git, Make, or a compiler. Warm-cache builds can
|
|
38
|
+
run offline.
|
|
39
|
+
|
|
28
40
|
```sh
|
|
29
|
-
|
|
41
|
+
npm exec sproutboat init hello
|
|
30
42
|
cd hello
|
|
31
|
-
|
|
43
|
+
npm exec sproutboat dev # runs it right here, no control plane needed
|
|
32
44
|
```
|
|
33
45
|
|
|
34
46
|
Happy with it? Ship it:
|
|
@@ -38,10 +50,10 @@ bunx sproutboat login --api-url https://control.example.com # one browser appr
|
|
|
38
50
|
bunx sproutboat deploy
|
|
39
51
|
```
|
|
40
52
|
|
|
41
|
-
Or install it once
|
|
53
|
+
Or install it once and drop the `npm exec`:
|
|
42
54
|
|
|
43
55
|
```sh
|
|
44
|
-
|
|
56
|
+
npm install -g sproutboat # then: sproutboat deploy, sproutboat tail, ...
|
|
45
57
|
```
|
|
46
58
|
|
|
47
59
|
`login` is one-time. It writes a long-lived token to
|
|
@@ -113,6 +125,7 @@ complete export. Existing output files require `--force`.
|
|
|
113
125
|
"queues": ["JOBS"],
|
|
114
126
|
"analytics_engine_datasets": ["METRICS"], // bare name only, no id
|
|
115
127
|
"durable_objects": { "COUNTER": "Counter" },
|
|
128
|
+
"ratelimiters": [{ "binding": "API", "limit": 100, "period": 60 }], // env.API.limit({ key })
|
|
116
129
|
"outbound": ["api.example.com"],
|
|
117
130
|
"triggers": { "crons": ["*/5 * * * *"] },
|
|
118
131
|
"assets": { "directory": "public", "binding": "ASSETS", "run_sprout_first": ["/api/*"] }
|
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.10.1 · runs on Bun (use `bunx`, not `npx`)
|
|
7
7
|
|
|
8
8
|
## Commands
|
|
9
9
|
|
|
@@ -44,6 +44,10 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
44
44
|
| `CC` | C compiler used to build uSockets for a host build (default `cc`). A host build needs one regardless: it is what Porffor compiles its own generated C with. |
|
|
45
45
|
| `AR` | Archiver used to assemble uSockets.a for a host build (default `ar`). |
|
|
46
46
|
| `SPROUTBOAT_COMPILE_TIMEOUT_MS` | Porffor compile timeout in ms (default 600000). |
|
|
47
|
+
| `SPROUTBOAT_TOOLCHAIN_CACHE` | Managed Porffor and compiler cache root (default ~/.cache/sproutboat). |
|
|
48
|
+
| `SPROUTBOAT_PORFFOR_DIR` | Contributor override for an existing Porffor source checkout. Required files are validated before use. |
|
|
49
|
+
| `SPROUTBOAT_BUILD_UWS_FROM_SOURCE` | Set to 1 to explicitly allow the contributor-only Git and Make uWebSockets fallback. |
|
|
50
|
+
| `SPROUTBOAT_CLI_VERSION` | CLI version embedded by the release executable build. Not normally set by users. |
|
|
47
51
|
| `SPROUTBOAT_VARS_JSON` | JSON object of baked `vars` (UPPER_SNAKE -> string), read by the wrapper when generating the sprout module. |
|
|
48
52
|
| `SPROUTBOAT_BINDINGS_JSON` | The artifact's bindings.json, read by the wrapper to emit the `__sbInstallBindings` line. |
|
|
49
53
|
| `SPROUTBOAT_CONFIG_DIR` | Directory for credentials.json (default ~/.config/sproutboat). |
|
|
@@ -67,7 +71,7 @@ usage: sproutboat <init [name] | check [project-dir] | types [project-dir] | dev
|
|
|
67
71
|
| | |
|
|
68
72
|
| --- | --- |
|
|
69
73
|
| Zig | `0.16.0` (`zig cc -target x86_64-linux-musl`, static) |
|
|
70
|
-
| Provenance stamp | `zig-musl/0.16.0+porffor/
|
|
74
|
+
| Provenance stamp | `zig-musl/0.16.0+porffor/1f4ae4a+uws/360c276d` |
|
|
71
75
|
| Artifact schema | `2` |
|
|
72
76
|
| Runtime | `native-fetch` |
|
|
73
77
|
| Capability profile | `http-sync-v0` |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// npm selects the packaged executable for this host when it is installed; that
|
|
3
|
+
// is a native single-file binary that needs no Bun. When it is absent (it is
|
|
4
|
+
// not published yet — see issue #134), fall back to running the TypeScript
|
|
5
|
+
// entry point with Bun, which is how the CLI shipped through v0.9.0.
|
|
6
|
+
const { spawn } = require("node:child_process");
|
|
7
|
+
const { join } = require("node:path");
|
|
8
|
+
|
|
9
|
+
const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
|
|
10
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
11
|
+
|
|
12
|
+
let command;
|
|
13
|
+
let args;
|
|
14
|
+
let viaBun = false;
|
|
15
|
+
try {
|
|
16
|
+
if (!platform || !arch) throw new Error(`unsupported host ${process.platform}/${process.arch}`);
|
|
17
|
+
command = require.resolve(`@sproutboat/cli-${platform}-${arch}/bin/sproutboat`);
|
|
18
|
+
args = process.argv.slice(2);
|
|
19
|
+
} catch {
|
|
20
|
+
viaBun = true;
|
|
21
|
+
// SPROUTBOAT_BUN is an escape hatch for an unusual install; otherwise PATH.
|
|
22
|
+
command = process.env.SPROUTBOAT_BUN || "bun";
|
|
23
|
+
args = [join(__dirname, "..", "src", "main.ts"), ...process.argv.slice(2)];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const child = spawn(command, args, { stdio: "inherit", windowsHide: true });
|
|
27
|
+
|
|
28
|
+
child.once("error", (error) => {
|
|
29
|
+
if (viaBun && error.code === "ENOENT") {
|
|
30
|
+
console.error("sproutboat: this build needs Bun on PATH. Install it: https://bun.sh");
|
|
31
|
+
} else {
|
|
32
|
+
console.error(`sproutboat: could not start: ${error.message}`);
|
|
33
|
+
}
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
38
|
+
process.on(signal, () => child.kill(signal));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
child.once("exit", (code, signal) => {
|
|
42
|
+
if (signal) {
|
|
43
|
+
// Re-raise so the parent's exit reflects the signal — but drop our own
|
|
44
|
+
// handler first, or the re-raise re-enters it and the process hangs.
|
|
45
|
+
process.removeAllListeners(signal);
|
|
46
|
+
process.kill(process.pid, signal);
|
|
47
|
+
} else {
|
|
48
|
+
process.exit(code ?? 1);
|
|
49
|
+
}
|
|
50
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sproutboat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
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",
|
|
@@ -19,12 +19,14 @@
|
|
|
19
19
|
"url": "git+https://github.com/baronunread/sproutboat-cli.git"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
|
-
"sproutboat": "
|
|
22
|
+
"sproutboat": "bin/sproutboat.cjs"
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"CHANGELOG.md",
|
|
26
26
|
"SURFACE.md",
|
|
27
27
|
"src",
|
|
28
|
+
"bin/sproutboat.cjs",
|
|
29
|
+
"scripts",
|
|
28
30
|
"types",
|
|
29
31
|
"vendor"
|
|
30
32
|
],
|
|
@@ -37,7 +39,6 @@
|
|
|
37
39
|
"./runtime/assets": "./src/assets.ts",
|
|
38
40
|
"./runtime/broker": "./src/broker.ts",
|
|
39
41
|
"./runtime/wrap": "./src/wrap.ts",
|
|
40
|
-
"./runtime/prelude": "./src/native-fetch-prelude.js",
|
|
41
42
|
"./package.json": "./package.json",
|
|
42
43
|
"./types": "./types/sproutboat.d.ts"
|
|
43
44
|
},
|
|
@@ -46,6 +47,7 @@
|
|
|
46
47
|
"typecheck": "tsc --noEmit",
|
|
47
48
|
"lint": "oxlint .",
|
|
48
49
|
"examples": "bun examples/smoke.ts",
|
|
50
|
+
"build:cli": "bun scripts/build-cli.ts",
|
|
49
51
|
"fmt": "oxfmt .",
|
|
50
52
|
"fmt:check": "oxfmt --check .",
|
|
51
53
|
"test": "bun test",
|
|
@@ -58,8 +60,13 @@
|
|
|
58
60
|
"kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
|
|
59
61
|
},
|
|
60
62
|
"dependencies": {
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
+
"@sproutboat/artifact": "^0.2.0",
|
|
64
|
+
"@sproutboat/assets": "^0.2.0",
|
|
65
|
+
"@sproutboat/config": "^0.3.0",
|
|
66
|
+
"@sproutboat/runtime": "^0.4.0",
|
|
67
|
+
"@sproutboat/toolchain": "^0.3.0",
|
|
68
|
+
"@sproutboat/wire": "^0.4.0",
|
|
69
|
+
"esbuild": "^0.28.2"
|
|
63
70
|
},
|
|
64
71
|
"devDependencies": {
|
|
65
72
|
"@oxlint/plugins": "1.81.0",
|
|
@@ -70,6 +77,6 @@
|
|
|
70
77
|
"typescript": "7.0.2"
|
|
71
78
|
},
|
|
72
79
|
"engines": {
|
|
73
|
-
"
|
|
80
|
+
"node": ">=18"
|
|
74
81
|
}
|
|
75
82
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Build one platform package executable. npm's tiny root launcher resolves it. */
|
|
2
|
+
import { chmod, copyFile, mkdir } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
const platform = process.platform === "darwin" ? "darwin" : process.platform === "linux" ? "linux" : null;
|
|
6
|
+
const arch = process.arch === "arm64" ? "arm64" : process.arch === "x64" ? "x64" : null;
|
|
7
|
+
if (!platform || !arch) throw new Error(`unsupported release host ${process.platform}/${process.arch}`);
|
|
8
|
+
const packageDir = resolve(import.meta.dir, "..", "platform-packages", `${platform}-${arch}`);
|
|
9
|
+
const out = resolve(packageDir, "bin", "sproutboat");
|
|
10
|
+
await mkdir(resolve(out, ".."), { recursive: true });
|
|
11
|
+
// SAFETY: package.json is this release's manifest and npm requires its version.
|
|
12
|
+
const version = ((await Bun.file(resolve(import.meta.dir, "..", "package.json")).json()) as { version: string })
|
|
13
|
+
.version;
|
|
14
|
+
// SAFETY: every checked-in platform package manifest owns a required string version.
|
|
15
|
+
const platformVersion = ((await Bun.file(resolve(packageDir, "package.json")).json()) as { version: string }).version;
|
|
16
|
+
if (platformVersion !== version)
|
|
17
|
+
throw new Error(`platform package version ${platformVersion} does not match root ${version}`);
|
|
18
|
+
const child = Bun.spawn(
|
|
19
|
+
[
|
|
20
|
+
process.execPath,
|
|
21
|
+
"build",
|
|
22
|
+
"--compile",
|
|
23
|
+
"--target=bun",
|
|
24
|
+
`--define:process.env.SPROUTBOAT_CLI_VERSION=${JSON.stringify(version)}`,
|
|
25
|
+
"--outfile",
|
|
26
|
+
out,
|
|
27
|
+
"src/main.ts",
|
|
28
|
+
],
|
|
29
|
+
{
|
|
30
|
+
cwd: resolve(import.meta.dir, ".."),
|
|
31
|
+
stdout: "inherit",
|
|
32
|
+
stderr: "inherit",
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
if ((await child.exited) !== 0) process.exit(1);
|
|
36
|
+
let esbuild: string;
|
|
37
|
+
try {
|
|
38
|
+
esbuild = Bun.resolveSync(`@esbuild/${platform}-${arch}/bin/esbuild`, import.meta.dir);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error("matching esbuild binary is missing; run bun install before building a platform package");
|
|
41
|
+
}
|
|
42
|
+
const packagedEsbuild = resolve(packageDir, "bin", "esbuild");
|
|
43
|
+
await copyFile(esbuild, packagedEsbuild);
|
|
44
|
+
await chmod(packagedEsbuild, 0o755);
|
package/src/assets.ts
CHANGED
|
@@ -1,121 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
*
|
|
5
|
-
* broker's `assets.get` op backs `env.<ASSETS>.fetch(request)` for the paths
|
|
6
|
-
* the sprout chooses to serve itself.
|
|
7
|
-
*/
|
|
8
|
-
import { createHash } from "node:crypto";
|
|
9
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
10
|
-
import { join, posix } from "node:path";
|
|
11
|
-
|
|
12
|
-
export type AssetEntry = { hash: string; size: number; type: string };
|
|
13
|
-
/** `/absolute/posix/path` -> entry. Serialised into `assets.json`. */
|
|
14
|
-
export type AssetFiles = { [path: string]: AssetEntry };
|
|
15
|
-
export type AssetManifest = {
|
|
16
|
-
/** Fallback for requests that match no file, applied by the broker. */
|
|
17
|
-
notFound: "none" | "single-page-application" | "404-page";
|
|
18
|
-
/** `true` = every path to the sprout first; string[] = selective (leading `!` negates). */
|
|
19
|
-
runSproutFirst: boolean | string[];
|
|
20
|
-
files: AssetFiles;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const TYPES = new Map<string, string>([
|
|
24
|
-
["html", "text/html; charset=utf-8"],
|
|
25
|
-
["css", "text/css; charset=utf-8"],
|
|
26
|
-
["js", "text/javascript; charset=utf-8"],
|
|
27
|
-
["mjs", "text/javascript; charset=utf-8"],
|
|
28
|
-
["json", "application/json; charset=utf-8"],
|
|
29
|
-
["map", "application/json; charset=utf-8"],
|
|
30
|
-
["txt", "text/plain; charset=utf-8"],
|
|
31
|
-
["xml", "application/xml; charset=utf-8"],
|
|
32
|
-
["svg", "image/svg+xml"],
|
|
33
|
-
["png", "image/png"],
|
|
34
|
-
["jpg", "image/jpeg"],
|
|
35
|
-
["jpeg", "image/jpeg"],
|
|
36
|
-
["gif", "image/gif"],
|
|
37
|
-
["webp", "image/webp"],
|
|
38
|
-
["avif", "image/avif"],
|
|
39
|
-
["ico", "image/x-icon"],
|
|
40
|
-
["woff2", "font/woff2"],
|
|
41
|
-
["woff", "font/woff"],
|
|
42
|
-
["ttf", "font/ttf"],
|
|
43
|
-
["wasm", "application/wasm"],
|
|
44
|
-
["webmanifest", "application/manifest+json"],
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
export function contentType(name: string): string {
|
|
48
|
-
const dot = name.lastIndexOf(".");
|
|
49
|
-
return (dot >= 0 ? TYPES.get(name.slice(dot + 1).toLowerCase()) : undefined) ?? "application/octet-stream";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Resolve a request path to a manifest key the way a static host does:
|
|
54
|
-
* - an exact hit wins;
|
|
55
|
-
* - a directory path (`/docs/`) tries `/docs/index.html`;
|
|
56
|
-
* - an extensionless path (`/docs`) tries `/docs.html`, then `/docs/index.html`.
|
|
57
|
-
* Returns the matched key, or `null`. This only picks which file to serve — no
|
|
58
|
-
* canonical redirects, and the caller still owns not-found handling. Mirrors
|
|
59
|
-
* Cloudflare's `html_handling: "auto-trailing-slash"` minus the 3xx responses.
|
|
60
|
-
*/
|
|
61
|
-
export function resolveAssetKey(path: string, has: (key: string) => boolean): string | null {
|
|
62
|
-
if (!path.startsWith("/")) path = `/${path}`;
|
|
63
|
-
if (path.endsWith("/")) {
|
|
64
|
-
const index = `${path}index.html`;
|
|
65
|
-
return has(index) ? index : null;
|
|
66
|
-
}
|
|
67
|
-
if (has(path)) return path;
|
|
68
|
-
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
69
|
-
if (!base.includes(".")) {
|
|
70
|
-
if (has(`${path}.html`)) return `${path}.html`;
|
|
71
|
-
if (has(`${path}/index.html`)) return `${path}/index.html`;
|
|
72
|
-
}
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Walk `dir` recursively, returning `{ "/path": {hash,size,type} }`. */
|
|
77
|
-
export function walkAssets(dir: string) {
|
|
78
|
-
const out: AssetFiles = {};
|
|
79
|
-
const recurse = (abs: string, rel: string): void => {
|
|
80
|
-
for (const ent of readdirSync(abs, { withFileTypes: true })) {
|
|
81
|
-
if (ent.name.startsWith(".")) continue;
|
|
82
|
-
const childAbs = join(abs, ent.name);
|
|
83
|
-
const childRel = posix.join(rel, ent.name);
|
|
84
|
-
if (ent.isDirectory()) {
|
|
85
|
-
recurse(childAbs, childRel);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (!ent.isFile()) continue;
|
|
89
|
-
const body = readFileSync(childAbs);
|
|
90
|
-
out[`/${childRel}`] = {
|
|
91
|
-
hash: createHash("sha256").update(body).digest("hex"),
|
|
92
|
-
size: body.byteLength,
|
|
93
|
-
type: contentType(ent.name),
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
};
|
|
97
|
-
recurse(dir, "");
|
|
98
|
-
return out;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Does `pathname` hit the sprout before assets, given a `runSproutFirst` spec? */
|
|
102
|
-
export function isSproutFirst(spec: boolean | string[], pathname: string): boolean {
|
|
103
|
-
if (!Array.isArray(spec)) return spec;
|
|
104
|
-
let matched = false;
|
|
105
|
-
for (const raw of spec) {
|
|
106
|
-
const negate = raw.startsWith("!");
|
|
107
|
-
const pattern = negate ? raw.slice(1) : raw;
|
|
108
|
-
if (globMatch(pattern, pathname)) matched = !negate;
|
|
109
|
-
}
|
|
110
|
-
return matched;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** `*` matches within a segment, `**` across segments. Anchored both ends. */
|
|
114
|
-
function globMatch(pattern: string, path: string): boolean {
|
|
115
|
-
const rx = pattern
|
|
116
|
-
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
117
|
-
.replace(/\*\*/g, " ")
|
|
118
|
-
.replace(/\*/g, "[^/]*")
|
|
119
|
-
.replace(/ /g, ".*");
|
|
120
|
-
return new RegExp(`^${rx}$`).test(path);
|
|
121
|
-
}
|
|
1
|
+
// Re-exported from @sproutboat/assets (moved verbatim). This shim keeps
|
|
2
|
+
// every `./assets` importer and the `sproutboat/runtime/assets` export path
|
|
3
|
+
// working; the implementation lives in the shared package.
|
|
4
|
+
export * from "@sproutboat/assets";
|