sproutboat 0.11.12 → 0.12.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 +32 -1
- package/SURFACE.md +2 -2
- package/package.json +9 -8
- package/src/bearssl.ts +2 -1
- package/src/build.ts +212 -119
- package/src/bundle.ts +29 -1
- package/src/compile-cache.ts +98 -0
- package/src/compile.ts +23 -19
- package/src/main.ts +1 -0
- package/src/node-shims.ts +378 -0
- package/src/standalone-build.ts +4 -14
- package/src/toolchain.ts +2 -2
- package/src/types-gen.ts +9 -5
- package/types/sproutboat.d.ts +11 -8
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,35 @@ a breaking change.
|
|
|
8
8
|
Reconstructed from git history on 2026-09-03 for everything through v0.4.11;
|
|
9
9
|
maintained going forward by the `release` skill.
|
|
10
10
|
|
|
11
|
+
## Unreleased
|
|
12
|
+
|
|
13
|
+
## [0.12.0] - 2026-09-28
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
- Run three original Cloudflare Worker examples unchanged: JSON responses,
|
|
17
|
+
cookie parsing, and redirects. The example suite now checks 21 native apps.
|
|
18
|
+
- Pass `(request, env, ctx)` to fetch handlers using the 2026-09-28
|
|
19
|
+
compatibility date while preserving the earlier two-argument form for older apps.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Update to Porffor alpha 10 via `@sproutboat/toolchain@^0.4.14` and use
|
|
23
|
+
`@sproutboat/runtime@^0.13.0` for native request decoding and redirects.
|
|
24
|
+
|
|
25
|
+
## [0.11.13] - 2026-09-26
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- Record compile duration in the artifact manifest so the platform can show it per deployment.
|
|
29
|
+
- Support selected `node:path`, `node:url`, and `node:querystring` APIs in handlers.
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
- Reuse checksum-verified native binaries for identical release builds and
|
|
33
|
+
refresh broker asset sidecars without recompiling the handler.
|
|
34
|
+
- Report native compilation time and binary cache status after `build`.
|
|
35
|
+
- Reject unsupported service bindings before starting a standalone build.
|
|
36
|
+
- Fetch the pinned Mozilla CA bundle from curl's dated archive so new upstream
|
|
37
|
+
revisions do not break fresh standalone builds.
|
|
38
|
+
- Update the pinned toolchain to `0.4.13` and the artifact contract to `0.3.0`.
|
|
39
|
+
|
|
11
40
|
## [0.11.12] - 2026-09-24
|
|
12
41
|
|
|
13
42
|
### Changed
|
|
@@ -610,7 +639,9 @@ its own package.
|
|
|
610
639
|
- Renamed the package to `sproutboat` (was `@sproutboat/cli`); dropped the
|
|
611
640
|
`sprout` bin alias in favour of a user-defined shell alias.
|
|
612
641
|
|
|
613
|
-
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.
|
|
642
|
+
[Unreleased]: https://github.com/baronunread/sproutboat-cli/compare/v0.12.0...HEAD
|
|
643
|
+
[0.12.0]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.13...v0.12.0
|
|
644
|
+
[0.11.13]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.12...v0.11.13
|
|
614
645
|
[0.11.12]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.11...v0.11.12
|
|
615
646
|
[0.11.11]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.10...v0.11.11
|
|
616
647
|
[0.11.10]: https://github.com/baronunread/sproutboat-cli/compare/v0.11.9...v0.11.10
|
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.12.0 · runs on Bun (use `bunx`, not `npx`)
|
|
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/08ac7ee+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.12.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",
|
|
@@ -61,26 +61,27 @@
|
|
|
61
61
|
"kitchen-sink:standalone": "bun examples/kitchen-sink/harness-standalone.ts"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@sproutboat/artifact": "^0.
|
|
64
|
+
"@sproutboat/artifact": "^0.3.0",
|
|
65
65
|
"@sproutboat/assets": "^0.3.0",
|
|
66
66
|
"@sproutboat/config": "^0.4.0",
|
|
67
|
-
"@sproutboat/runtime": "^0.
|
|
68
|
-
"@sproutboat/toolchain": "^0.4.
|
|
67
|
+
"@sproutboat/runtime": "^0.13.0",
|
|
68
|
+
"@sproutboat/toolchain": "^0.4.14",
|
|
69
69
|
"@sproutboat/wire": "^0.9.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@oxlint/plugins": "1.81.0",
|
|
73
73
|
"@types/bun": "1.4.0",
|
|
74
|
+
"cookie": "2.0.1",
|
|
74
75
|
"lefthook": "2.1.12",
|
|
75
76
|
"oxfmt": "0.66.0",
|
|
76
77
|
"oxlint": "1.81.0",
|
|
77
78
|
"typescript": "7.0.2"
|
|
78
79
|
},
|
|
79
80
|
"optionalDependencies": {
|
|
80
|
-
"@sproutboat/cli-darwin-arm64": "0.
|
|
81
|
-
"@sproutboat/cli-darwin-x64": "0.
|
|
82
|
-
"@sproutboat/cli-linux-arm64": "0.
|
|
83
|
-
"@sproutboat/cli-linux-x64": "0.
|
|
81
|
+
"@sproutboat/cli-darwin-arm64": "0.12.0",
|
|
82
|
+
"@sproutboat/cli-darwin-x64": "0.12.0",
|
|
83
|
+
"@sproutboat/cli-linux-arm64": "0.12.0",
|
|
84
|
+
"@sproutboat/cli-linux-x64": "0.12.0"
|
|
84
85
|
},
|
|
85
86
|
"engines": {
|
|
86
87
|
"node": ">=18"
|
package/src/bearssl.ts
CHANGED
|
@@ -34,7 +34,8 @@ const BEARSSL_SHA256 = "6705bba1714961b41a728dfc5debbe348d2966c117649392f8c8139e
|
|
|
34
34
|
* release is unknown until the next one. Fine while releases are frequent;
|
|
35
35
|
* revisit if a user is ever stuck on an old CLI for months.
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
// Use the dated archive so its bytes stay equal to the pinned digest.
|
|
38
|
+
const CACERT_URL = "https://curl.se/ca/cacert-2026-08-13.pem";
|
|
38
39
|
const CACERT_SHA256 = "f66dff1bdf8f96060b8177976f8b7d9254bc89bc4db933d769f7384d28480bc9";
|
|
39
40
|
|
|
40
41
|
const cacheDir = (): string =>
|
package/src/build.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { cp, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { walkAssets, type AssetManifest } from "./assets";
|
|
5
5
|
import { resourceRefs, type SproutboatConfig } from "./config";
|
|
6
6
|
import { ensureSqliteObject } from "./sqlite";
|
|
7
7
|
import { sqliteStamp } from "./sqlite";
|
|
8
8
|
import { bearsslStamp, ensureBearssl } from "./bearssl";
|
|
9
|
-
import { compileSprout, type Transport } from "./compile";
|
|
9
|
+
import { compileSprout, loadPrelude, type Transport } from "./compile";
|
|
10
|
+
import { compileCached } from "./compile-cache";
|
|
10
11
|
import {
|
|
11
12
|
ARTIFACT_SCHEMA_VERSION,
|
|
12
13
|
CAPABILITY_PROFILE,
|
|
@@ -16,6 +17,10 @@ import {
|
|
|
16
17
|
type ArtifactManifest,
|
|
17
18
|
} from "./manifest";
|
|
18
19
|
import { ensureZig, esbuildVersion, porfforVersion, toolchainStamp } from "./toolchain";
|
|
20
|
+
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_COMMIT_FULL } from "./porffor-toolchain";
|
|
21
|
+
import { version as toolchainPackageVersion } from "@sproutboat/toolchain/package.json" with { type: "json" };
|
|
22
|
+
import { version as cliVersion } from "../package.json" with { type: "json" };
|
|
23
|
+
import { wrapNativeFetchHandler } from "./wrap";
|
|
19
24
|
|
|
20
25
|
export type BuildInput = {
|
|
21
26
|
projectDir: string;
|
|
@@ -54,6 +59,8 @@ export type BuildInput = {
|
|
|
54
59
|
export type BuildOutput = {
|
|
55
60
|
artifactDir: string;
|
|
56
61
|
manifest: ArtifactManifest;
|
|
62
|
+
compileCache: "hit" | "miss" | "bypass";
|
|
63
|
+
compileMs: number;
|
|
57
64
|
};
|
|
58
65
|
|
|
59
66
|
/** Baking megabytes of assets into the module makes the Porffor compile crawl;
|
|
@@ -64,6 +71,45 @@ function digest(value: Uint8Array | string): `sha256:${string}` {
|
|
|
64
71
|
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
async function integrationIdentity(): Promise<string> {
|
|
75
|
+
// A published CLI has immutable bundled integration code and a release
|
|
76
|
+
// version. Source checkouts also hash their live files, so local edits never
|
|
77
|
+
// accidentally reuse a binary compiled by an earlier checkout state.
|
|
78
|
+
if (import.meta.url.includes("/$bunfs/")) {
|
|
79
|
+
const executable = await stat(process.execPath);
|
|
80
|
+
return `release:${cliVersion}:${executable.size}:${executable.mtimeMs}`;
|
|
81
|
+
}
|
|
82
|
+
const files = ["build.ts", "compile.ts", "sqlite.ts", "bearssl.ts", "toolchain.ts"].map((file) =>
|
|
83
|
+
resolve(import.meta.dir, file),
|
|
84
|
+
);
|
|
85
|
+
// Hash the patch implementation and its local modules so a Unicode patch
|
|
86
|
+
// edit cannot reuse a binary compiled with earlier builtin semantics.
|
|
87
|
+
const patchDir = dirname(Bun.resolveSync("@sproutboat/toolchain/patch", import.meta.dir));
|
|
88
|
+
const toolchainFiles = (await readdir(patchDir))
|
|
89
|
+
.filter((file) => file.endsWith(".ts") && !file.endsWith(".test.ts"))
|
|
90
|
+
.sort();
|
|
91
|
+
files.push(...toolchainFiles.map((file) => resolve(patchDir, file)));
|
|
92
|
+
return digest(Buffer.concat(await Promise.all(files.map((file) => readFile(file)))));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function acquireArtifactLock(artifactDir: string): Promise<string> {
|
|
96
|
+
const lock = `${artifactDir}.lock`;
|
|
97
|
+
await mkdir(dirname(lock), { recursive: true });
|
|
98
|
+
const deadline = Date.now() + 12 * 60_000;
|
|
99
|
+
while (true) {
|
|
100
|
+
try {
|
|
101
|
+
await mkdir(lock);
|
|
102
|
+
return lock;
|
|
103
|
+
} catch (error) {
|
|
104
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
105
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for artifact lock ${lock}`);
|
|
106
|
+
const info = await stat(lock).catch(() => null);
|
|
107
|
+
if (info && Date.now() - info.mtimeMs > 15 * 60_000) await rm(lock, { recursive: true, force: true });
|
|
108
|
+
else await Bun.sleep(100);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
67
113
|
/**
|
|
68
114
|
* Cross-compile a handler into a linux-x86_64 native-fetch server binary with
|
|
69
115
|
* Porffor + Zig — no Docker. The binary is not run here (the build host may not
|
|
@@ -75,30 +121,6 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
75
121
|
const sourceHash = digest(source);
|
|
76
122
|
const target = input.target ?? "linux-x86_64";
|
|
77
123
|
const embedded = input.transport === "embedded";
|
|
78
|
-
// The directory identity must change whenever an input that changes the
|
|
79
|
-
// generated C or link line changes. A source digest alone reused an artifact
|
|
80
|
-
// path across compiler pins, targets, and standalone ABI settings.
|
|
81
|
-
const artifactId = digest(
|
|
82
|
-
JSON.stringify({
|
|
83
|
-
sourceHash,
|
|
84
|
-
target,
|
|
85
|
-
transport: input.transport ?? "broker",
|
|
86
|
-
optimize: input.optimize ?? "release",
|
|
87
|
-
compatibilityDate: input.config.compatibility_date,
|
|
88
|
-
config: input.config,
|
|
89
|
-
toolchain: toolchainStamp(),
|
|
90
|
-
compiler:
|
|
91
|
-
target === "host"
|
|
92
|
-
? `host:${process.env.CC ?? "cc"}`
|
|
93
|
-
: process.env.SPROUTBOAT_ZIG
|
|
94
|
-
? `override:${process.env.SPROUTBOAT_ZIG}`
|
|
95
|
-
: "managed-zig",
|
|
96
|
-
native: embedded ? [sqliteStamp(), bearsslStamp()] : [],
|
|
97
|
-
}),
|
|
98
|
-
).slice("sha256:".length, 24);
|
|
99
|
-
const artifactDir = input.outputDirectory ?? resolve(input.projectDir, ".sproutboat/dist", artifactId);
|
|
100
|
-
const sproutPath = resolve(artifactDir, "sprout");
|
|
101
|
-
await mkdir(artifactDir, { recursive: true });
|
|
102
124
|
|
|
103
125
|
// #74 — split each storage-binding array into its binding-name list (the
|
|
104
126
|
// legacy shape the prelude/broker read) plus a `resources` map { binding ->
|
|
@@ -134,33 +156,34 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
134
156
|
resources,
|
|
135
157
|
};
|
|
136
158
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
159
|
+
const host = target === "host";
|
|
160
|
+
const assetDir = input.config.assets ? resolve(input.projectDir, input.config.assets.directory) : undefined;
|
|
161
|
+
if (
|
|
162
|
+
assetDir &&
|
|
163
|
+
!(await stat(assetDir)
|
|
164
|
+
.then((s) => s.isDirectory())
|
|
165
|
+
.catch(() => false))
|
|
166
|
+
) {
|
|
167
|
+
throw new Error(`assets.directory "${input.config.assets?.directory}" not found — run your site build first`);
|
|
168
|
+
}
|
|
169
|
+
const assetManifest: AssetManifest | undefined =
|
|
170
|
+
assetDir && input.config.assets
|
|
171
|
+
? {
|
|
172
|
+
notFound: input.config.assets.not_found_handling ?? "none",
|
|
173
|
+
runSproutFirst: input.config.assets.run_sprout_first ?? false,
|
|
174
|
+
files: walkAssets(assetDir),
|
|
175
|
+
}
|
|
176
|
+
: undefined;
|
|
148
177
|
// #15 — an embedded binary has no files beside it, so assets are baked into
|
|
149
178
|
// the module. Read them from the source directory: the artifact copy happens
|
|
150
179
|
// after the compile, and the compile is what needs them. Bytes travel as a
|
|
151
180
|
// latin1 string, one char per byte, which is what the asset shim hands back.
|
|
152
181
|
let bakedAssets: { manifest: AssetManifest; files: Record<string, string> } | undefined;
|
|
153
|
-
if (
|
|
154
|
-
const dir = resolve(input.projectDir, input.config.assets.directory);
|
|
155
|
-
const manifest: AssetManifest = {
|
|
156
|
-
notFound: input.config.assets.not_found_handling ?? "none",
|
|
157
|
-
runSproutFirst: input.config.assets.run_sprout_first ?? false,
|
|
158
|
-
files: walkAssets(dir),
|
|
159
|
-
};
|
|
182
|
+
if (embedded && assetDir && assetManifest) {
|
|
160
183
|
const files: Record<string, string> = {};
|
|
161
184
|
let total = 0;
|
|
162
|
-
for (const key of Object.keys(
|
|
163
|
-
const bytes = await readFile(resolve(
|
|
185
|
+
for (const key of Object.keys(assetManifest.files)) {
|
|
186
|
+
const bytes = await readFile(resolve(assetDir, `.${key}`));
|
|
164
187
|
total += bytes.byteLength;
|
|
165
188
|
if (total > MAX_BAKED_ASSET_BYTES) {
|
|
166
189
|
throw new Error(
|
|
@@ -170,86 +193,156 @@ export async function buildArtifact(input: BuildInput): Promise<BuildOutput> {
|
|
|
170
193
|
}
|
|
171
194
|
files[key] = bytes.toString("latin1");
|
|
172
195
|
}
|
|
173
|
-
bakedAssets = { manifest, files };
|
|
196
|
+
bakedAssets = { manifest: assetManifest, files };
|
|
174
197
|
}
|
|
175
198
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
199
|
+
// Version metadata is compiled into the executable. Its timestamp makes a
|
|
200
|
+
// new binary necessary on every build when this binding is configured.
|
|
178
201
|
const builtAt = new Date().toISOString();
|
|
202
|
+
const versionId = digest(
|
|
203
|
+
JSON.stringify({ sourceHash, target, config: input.config, assets: assetManifest, toolchain: toolchainStamp() }),
|
|
204
|
+
).slice("sha256:".length, 24);
|
|
179
205
|
const versionMetadata = input.config.version_metadata
|
|
180
|
-
? { binding: input.config.version_metadata, id:
|
|
206
|
+
? { binding: input.config.version_metadata, id: versionId, tag: input.config.name, timestamp: builtAt }
|
|
181
207
|
: undefined;
|
|
208
|
+
const generatedSource = wrapNativeFetchHandler(
|
|
209
|
+
source.toString(),
|
|
210
|
+
await loadPrelude(input.transport ?? "broker"),
|
|
211
|
+
input.config.vars ?? {},
|
|
212
|
+
bindings,
|
|
213
|
+
undefined,
|
|
214
|
+
input.config.compatibility_date,
|
|
215
|
+
input.config.name,
|
|
216
|
+
bakedAssets,
|
|
217
|
+
input.transport,
|
|
218
|
+
versionMetadata,
|
|
219
|
+
);
|
|
220
|
+
const compileKey = digest(
|
|
221
|
+
JSON.stringify({
|
|
222
|
+
generatedSource,
|
|
223
|
+
target,
|
|
224
|
+
optimize: input.optimize ?? "release",
|
|
225
|
+
toolchain: toolchainStamp(),
|
|
226
|
+
toolchainPackageVersion,
|
|
227
|
+
porffor: [PORFFOR_COMMIT_FULL, PORFFOR_ARCHIVE_SHA256],
|
|
228
|
+
native: embedded ? [sqliteStamp(), bearsslStamp()] : [],
|
|
229
|
+
integration: await integrationIdentity(),
|
|
230
|
+
}),
|
|
231
|
+
).slice("sha256:".length);
|
|
232
|
+
const artifactId = versionMetadata
|
|
233
|
+
? versionId
|
|
234
|
+
: digest(JSON.stringify({ compileKey, config: input.config, assets: assetManifest })).slice("sha256:".length, 24);
|
|
235
|
+
const artifactDir = input.outputDirectory ?? resolve(input.projectDir, ".sproutboat/dist", artifactId);
|
|
236
|
+
const sproutPath = resolve(artifactDir, "sprout");
|
|
237
|
+
const artifactLock = await acquireArtifactLock(artifactDir);
|
|
238
|
+
try {
|
|
239
|
+
await mkdir(artifactDir, { recursive: true });
|
|
182
240
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
porfforVersion: porfforVersion(),
|
|
212
|
-
esbuildVersion: esbuildVersion(),
|
|
213
|
-
buildImage: toolchainStamp(),
|
|
214
|
-
compatibilityDate: input.config.compatibility_date,
|
|
215
|
-
sourceHash,
|
|
216
|
-
binaryHash: digest(sprout),
|
|
217
|
-
binarySize: (await stat(sproutPath)).size,
|
|
218
|
-
builtAt,
|
|
219
|
-
};
|
|
220
|
-
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
221
|
-
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
|
222
|
-
// frozen at v2. The control plane reads this to configure the per-deployment
|
|
223
|
-
// broker (KV / D1 / R2 / queue names, secret names, outbound allowlist, cron
|
|
224
|
-
// schedules, Durable Object classes).
|
|
225
|
-
const hasBindings =
|
|
226
|
-
Object.values(bindings).some((value) => Array.isArray(value) && value.length > 0) ||
|
|
227
|
-
Object.keys(bindings.resources).length > 0 ||
|
|
228
|
-
Object.keys(bindings.vars).length > 0;
|
|
229
|
-
if (hasBindings) {
|
|
230
|
-
await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Static assets: copy the directory next to the artifact and record a manifest
|
|
234
|
-
// the edge serves from directly (assets-first) and the broker reads for
|
|
235
|
-
// `env.<ASSETS>.fetch()`.
|
|
236
|
-
if (input.config.assets) {
|
|
237
|
-
const srcDir = resolve(input.projectDir, input.config.assets.directory);
|
|
238
|
-
const outDir = resolve(artifactDir, "assets");
|
|
241
|
+
const compile = async (outPath: string) => {
|
|
242
|
+
// Acquire native inputs only after a cache miss. A host build never needs
|
|
243
|
+
// the cross-compiler; embedded builds link SQLite and BearSSL as well.
|
|
244
|
+
const zigBin = host ? undefined : await ensureZig();
|
|
245
|
+
const [sqliteObject, tls] = embedded
|
|
246
|
+
? await Promise.all([ensureSqliteObject({ target, zigBin }), ensureBearssl({ target, zigBin })])
|
|
247
|
+
: [null, null];
|
|
248
|
+
await compileSprout({
|
|
249
|
+
sourcePath: input.sourcePath,
|
|
250
|
+
generatedPath: input.generatedPath,
|
|
251
|
+
generatedSource,
|
|
252
|
+
outPath,
|
|
253
|
+
vars: input.config.vars ?? {},
|
|
254
|
+
bindings,
|
|
255
|
+
zigBin,
|
|
256
|
+
target: input.target,
|
|
257
|
+
compatibilityDate: input.config.compatibility_date,
|
|
258
|
+
transport: input.transport,
|
|
259
|
+
appName: input.config.name,
|
|
260
|
+
assets: bakedAssets,
|
|
261
|
+
extraLink: [...(sqliteObject ? [sqliteObject] : []), ...(tls ? tls.objects : [])],
|
|
262
|
+
extraCflags: tls ? ["-I", tls.includeDir] : [],
|
|
263
|
+
optimize: input.optimize,
|
|
264
|
+
versionMetadata,
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
let compileCache: BuildOutput["compileCache"] = "bypass";
|
|
268
|
+
const compileStartedAt = performance.now();
|
|
239
269
|
if (
|
|
240
|
-
!
|
|
241
|
-
|
|
242
|
-
|
|
270
|
+
!input.reuseSproutPath &&
|
|
271
|
+
!host &&
|
|
272
|
+
input.optimize !== "dev" &&
|
|
273
|
+
!input.outputDirectory &&
|
|
274
|
+
!versionMetadata &&
|
|
275
|
+
!process.env.SPROUTBOAT_PORFFOR_DIR &&
|
|
276
|
+
!process.env.SPROUTBOAT_ZIG &&
|
|
277
|
+
!process.env.SPROUTBOAT_UWS_TARBALL &&
|
|
278
|
+
!process.env.SPROUTBOAT_BUILD_UWS_FROM_SOURCE &&
|
|
279
|
+
!process.env.PORFFOR_VERSION
|
|
243
280
|
) {
|
|
244
|
-
|
|
281
|
+
compileCache = await compileCached(
|
|
282
|
+
resolve(input.projectDir, ".sproutboat/compile-cache"),
|
|
283
|
+
compileKey,
|
|
284
|
+
sproutPath,
|
|
285
|
+
compile,
|
|
286
|
+
);
|
|
287
|
+
} else {
|
|
288
|
+
const candidate = resolve(artifactDir, `.sprout-${randomUUID()}`);
|
|
289
|
+
try {
|
|
290
|
+
if (input.reuseSproutPath) await cp(input.reuseSproutPath, candidate);
|
|
291
|
+
else await compile(candidate);
|
|
292
|
+
await rename(candidate, sproutPath);
|
|
293
|
+
} finally {
|
|
294
|
+
await rm(candidate, { force: true });
|
|
295
|
+
}
|
|
245
296
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
297
|
+
const compileMs = Math.round(performance.now() - compileStartedAt);
|
|
298
|
+
|
|
299
|
+
const sprout = await readFile(sproutPath);
|
|
300
|
+
const manifest: ArtifactManifest & { compileMs: number } = {
|
|
301
|
+
schemaVersion: ARTIFACT_SCHEMA_VERSION,
|
|
302
|
+
project: input.config.name,
|
|
303
|
+
target: host ? hostTarget() : DEPLOY_TARGET,
|
|
304
|
+
runtime: RUNTIME,
|
|
305
|
+
capabilityProfile: CAPABILITY_PROFILE,
|
|
306
|
+
porfforVersion: porfforVersion(),
|
|
307
|
+
esbuildVersion: esbuildVersion(),
|
|
308
|
+
buildImage: toolchainStamp(),
|
|
309
|
+
compatibilityDate: input.config.compatibility_date,
|
|
310
|
+
sourceHash,
|
|
311
|
+
binaryHash: digest(sprout),
|
|
312
|
+
binarySize: (await stat(sproutPath)).size,
|
|
313
|
+
compileMs,
|
|
314
|
+
builtAt,
|
|
251
315
|
};
|
|
252
|
-
await writeFile(resolve(artifactDir, "
|
|
316
|
+
await writeFile(resolve(artifactDir, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
317
|
+
// Bindings live beside the manifest, not in it: the artifact manifest schema is
|
|
318
|
+
// frozen at v2. The control plane reads this to configure the per-deployment
|
|
319
|
+
// broker (KV / D1 / R2 / queue names, secret names, outbound allowlist, cron
|
|
320
|
+
// schedules, Durable Object classes).
|
|
321
|
+
const hasBindings =
|
|
322
|
+
Object.values(bindings).some((value) => Array.isArray(value) && value.length > 0) ||
|
|
323
|
+
Object.keys(bindings.resources).length > 0 ||
|
|
324
|
+
Object.keys(bindings.vars).length > 0;
|
|
325
|
+
if (hasBindings) {
|
|
326
|
+
await writeFile(resolve(artifactDir, "bindings.json"), `${JSON.stringify(bindings, null, 2)}\n`);
|
|
327
|
+
} else await rm(resolve(artifactDir, "bindings.json"), { force: true });
|
|
328
|
+
|
|
329
|
+
// Static assets: copy the directory next to the artifact and record a manifest
|
|
330
|
+
// the edge serves from directly (assets-first) and the broker reads for
|
|
331
|
+
// `env.<ASSETS>.fetch()`.
|
|
332
|
+
if (assetDir && assetManifest) {
|
|
333
|
+
const outDir = resolve(artifactDir, "assets");
|
|
334
|
+
await rm(outDir, { recursive: true, force: true });
|
|
335
|
+
await cp(assetDir, outDir, { recursive: true });
|
|
336
|
+
if (JSON.stringify(walkAssets(outDir)) !== JSON.stringify(assetManifest.files)) {
|
|
337
|
+
throw new Error("assets changed during the build; retry after the asset build finishes");
|
|
338
|
+
}
|
|
339
|
+
await writeFile(resolve(artifactDir, "assets.json"), `${JSON.stringify(assetManifest, null, 2)}\n`);
|
|
340
|
+
} else {
|
|
341
|
+
await rm(resolve(artifactDir, "assets.json"), { force: true });
|
|
342
|
+
await rm(resolve(artifactDir, "assets"), { recursive: true, force: true });
|
|
343
|
+
}
|
|
344
|
+
return { artifactDir, manifest, compileCache, compileMs };
|
|
345
|
+
} finally {
|
|
346
|
+
await rm(artifactLock, { recursive: true, force: true });
|
|
253
347
|
}
|
|
254
|
-
return { artifactDir, manifest };
|
|
255
348
|
}
|
package/src/bundle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* #89 — bundle the handler before Porffor sees it.
|
|
3
3
|
*
|
|
4
|
-
* Porffor
|
|
4
|
+
* Porffor supports ESM imports. We still bundle first so capability
|
|
5
5
|
* validation sees every dependency and the runtime wrapper receives one
|
|
6
6
|
* controlled default export.
|
|
7
7
|
*
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* reaching for a Node API fails exactly as user code would.
|
|
13
13
|
*/
|
|
14
14
|
import { relative } from "node:path";
|
|
15
|
+
import { NODE_SHIMS } from "./node-shims";
|
|
15
16
|
|
|
16
17
|
export type BundleResult = {
|
|
17
18
|
/** One self-contained ESM module: what gets validated, hashed, and compiled. */
|
|
@@ -27,6 +28,32 @@ export class BundleError extends Error {
|
|
|
27
28
|
|
|
28
29
|
const entryLabel = (entryPath: string, projectDir: string): string => relative(projectDir, entryPath) || entryPath;
|
|
29
30
|
|
|
31
|
+
const shimSpecifiers = new RegExp(
|
|
32
|
+
`^(${Object.keys(NODE_SHIMS)
|
|
33
|
+
.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
34
|
+
.join("|")})$`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bun already bundles working polyfills for most node:* built-ins under
|
|
39
|
+
* `target: "browser"` (verified for node:url, node:querystring). This
|
|
40
|
+
* plugin only overrides the ones in NODE_SHIMS — currently just node:path,
|
|
41
|
+
* whose bundled resolve() otherwise falls back to `process.cwd()`.
|
|
42
|
+
*/
|
|
43
|
+
const nodeShimsPlugin: import("bun").BunPlugin = {
|
|
44
|
+
name: "sproutboat-node-shims",
|
|
45
|
+
setup(build) {
|
|
46
|
+
build.onResolve({ filter: shimSpecifiers }, (args) => ({ path: args.path, namespace: "sb-node-shim" }));
|
|
47
|
+
build.onLoad({ filter: /.*/, namespace: "sb-node-shim" }, (args) => ({
|
|
48
|
+
// SAFETY: onResolve above only ever routes a specifier matched by
|
|
49
|
+
// shimSpecifiers (built from NODE_SHIMS's own keys) into this
|
|
50
|
+
// namespace, so args.path is always one of NODE_SHIMS's keys here.
|
|
51
|
+
contents: (NODE_SHIMS as Record<string, string>)[args.path],
|
|
52
|
+
loader: "js",
|
|
53
|
+
}));
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
30
57
|
/** Bun reports resolution failures on `AggregateError.errors`; its own message is just "Bundle failed". */
|
|
31
58
|
function bundleDetail(cause: unknown): string {
|
|
32
59
|
const errors = cause instanceof AggregateError ? cause.errors : [];
|
|
@@ -51,6 +78,7 @@ export async function bundleHandler(entryPath: string, projectDir: string): Prom
|
|
|
51
78
|
minify: false,
|
|
52
79
|
splitting: false,
|
|
53
80
|
sourcemap: "none",
|
|
81
|
+
plugins: [nodeShimsPlugin],
|
|
54
82
|
});
|
|
55
83
|
} catch (cause) {
|
|
56
84
|
// An unresolvable specifier arrives as an AggregateError whose `errors`
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
type CacheRecord = { key: string; hash: string; size: number };
|
|
6
|
+
|
|
7
|
+
function digest(bytes: Uint8Array): string {
|
|
8
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function verified(entry: string, key: string): Promise<Buffer | null> {
|
|
12
|
+
try {
|
|
13
|
+
// SAFETY: cache.json is private cache data; the key, metadata and binary
|
|
14
|
+
// digest are all checked before this entry can be used.
|
|
15
|
+
const record = JSON.parse(await readFile(resolve(entry, "cache.json"), "utf8")) as CacheRecord;
|
|
16
|
+
if (record.key !== key) return null;
|
|
17
|
+
const path = resolve(entry, "sprout");
|
|
18
|
+
const info = await stat(path);
|
|
19
|
+
if (!info.isFile() || (info.mode & 0o111) === 0 || info.size !== record.size) return null;
|
|
20
|
+
const bytes = await readFile(path);
|
|
21
|
+
return digest(bytes) === record.hash ? bytes : null;
|
|
22
|
+
} catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function publish(bytes: Uint8Array, outPath: string): Promise<void> {
|
|
28
|
+
await mkdir(dirname(outPath), { recursive: true });
|
|
29
|
+
const candidate = `${outPath}.${randomUUID()}.tmp`;
|
|
30
|
+
try {
|
|
31
|
+
await writeFile(candidate, bytes);
|
|
32
|
+
await chmod(candidate, 0o555);
|
|
33
|
+
await rename(candidate, outPath);
|
|
34
|
+
} finally {
|
|
35
|
+
await rm(candidate, { force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** A per-project binary cache. The lock covers compilation and publication, so
|
|
40
|
+
* another process can only read a complete, checksum-verified entry. */
|
|
41
|
+
export async function compileCached(
|
|
42
|
+
cacheRoot: string,
|
|
43
|
+
key: string,
|
|
44
|
+
outPath: string,
|
|
45
|
+
compile: (candidatePath: string) => Promise<void>,
|
|
46
|
+
): Promise<"hit" | "miss"> {
|
|
47
|
+
const entry = resolve(cacheRoot, key);
|
|
48
|
+
const cached = await verified(entry, key);
|
|
49
|
+
if (cached) {
|
|
50
|
+
await publish(cached, outPath);
|
|
51
|
+
return "hit";
|
|
52
|
+
}
|
|
53
|
+
await mkdir(cacheRoot, { recursive: true });
|
|
54
|
+
const lock = `${entry}.lock`;
|
|
55
|
+
const deadline = Date.now() + 12 * 60_000;
|
|
56
|
+
while (true) {
|
|
57
|
+
try {
|
|
58
|
+
await mkdir(lock);
|
|
59
|
+
break;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// SAFETY: Node filesystem errors expose code; unrelated failures are
|
|
62
|
+
// rethrown instead of being treated as a held lock.
|
|
63
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
64
|
+
const ready = await verified(entry, key);
|
|
65
|
+
if (ready) {
|
|
66
|
+
await publish(ready, outPath);
|
|
67
|
+
return "hit";
|
|
68
|
+
}
|
|
69
|
+
if (Date.now() > deadline) throw new Error(`timed out waiting for compile cache lock ${lock}`);
|
|
70
|
+
const lockInfo = await stat(lock).catch(() => null);
|
|
71
|
+
if (lockInfo && Date.now() - lockInfo.mtimeMs > 15 * 60_000) await rm(lock, { recursive: true, force: true });
|
|
72
|
+
else await Bun.sleep(100);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let stage: string | undefined;
|
|
76
|
+
try {
|
|
77
|
+
const ready = await verified(entry, key);
|
|
78
|
+
if (ready) {
|
|
79
|
+
await publish(ready, outPath);
|
|
80
|
+
return "hit";
|
|
81
|
+
}
|
|
82
|
+
await rm(entry, { recursive: true, force: true });
|
|
83
|
+
stage = await mkdtemp(resolve(cacheRoot, `.candidate-${key}-`));
|
|
84
|
+
const candidate = resolve(stage, "sprout");
|
|
85
|
+
await compile(candidate);
|
|
86
|
+
const bytes = await readFile(candidate);
|
|
87
|
+
const record: CacheRecord = { key, hash: digest(bytes), size: bytes.byteLength };
|
|
88
|
+
await chmod(candidate, 0o555);
|
|
89
|
+
await writeFile(resolve(stage, "cache.json"), JSON.stringify(record));
|
|
90
|
+
await rename(stage, entry);
|
|
91
|
+
stage = undefined;
|
|
92
|
+
await publish(bytes, outPath);
|
|
93
|
+
return "miss";
|
|
94
|
+
} finally {
|
|
95
|
+
if (stage) await rm(stage, { recursive: true, force: true });
|
|
96
|
+
await rm(lock, { recursive: true, force: true });
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/compile.ts
CHANGED
|
@@ -53,6 +53,8 @@ export type CompileInput = {
|
|
|
53
53
|
generatedPath?: string;
|
|
54
54
|
/** The bundled module (#89). Falls back to reading `sourcePath` verbatim. */
|
|
55
55
|
source?: string;
|
|
56
|
+
/** The exact wrapped module used to compute a verified compile-cache key. */
|
|
57
|
+
generatedSource?: string;
|
|
56
58
|
outPath: string;
|
|
57
59
|
vars: Record<string, string>;
|
|
58
60
|
bindings?: Bindings;
|
|
@@ -198,25 +200,27 @@ export async function compileSprout(input: CompileInput): Promise<void> {
|
|
|
198
200
|
await rm(input.outPath, { force: true });
|
|
199
201
|
const generatedPath = input.generatedPath ?? resolve(outDir, "sprout.generated.js");
|
|
200
202
|
await mkdir(dirname(generatedPath), { recursive: true });
|
|
201
|
-
const
|
|
202
|
-
input.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
203
|
+
const generatedSource =
|
|
204
|
+
input.generatedSource ??
|
|
205
|
+
(await (async () => {
|
|
206
|
+
const [source, prelude] = await Promise.all([
|
|
207
|
+
input.source === undefined ? readFile(input.sourcePath, "utf8") : Promise.resolve(input.source),
|
|
208
|
+
loadPrelude(input.transport ?? "broker"),
|
|
209
|
+
]);
|
|
210
|
+
return wrapNativeFetchHandler(
|
|
211
|
+
source,
|
|
212
|
+
prelude,
|
|
213
|
+
input.vars,
|
|
214
|
+
input.bindings ?? EMPTY_BINDINGS,
|
|
215
|
+
undefined,
|
|
216
|
+
input.compatibilityDate,
|
|
217
|
+
input.appName,
|
|
218
|
+
input.assets,
|
|
219
|
+
input.transport,
|
|
220
|
+
input.versionMetadata,
|
|
221
|
+
);
|
|
222
|
+
})());
|
|
223
|
+
await writeFile(generatedPath, generatedSource);
|
|
220
224
|
|
|
221
225
|
const launcher = resolve(porffor, "runtime/index.js");
|
|
222
226
|
// A musl build shells `zig` bare (Porffor hardcodes the literal command), so
|
package/src/main.ts
CHANGED
|
@@ -319,6 +319,7 @@ async function build(directory?: string, target: "linux-x86_64" | "host" = "linu
|
|
|
319
319
|
target,
|
|
320
320
|
});
|
|
321
321
|
console.log(ok(`built ${project.config.name}`));
|
|
322
|
+
console.log(dim(` compile ${artifact.compileMs} ms · binary cache ${artifact.compileCache}`));
|
|
322
323
|
if (target === "host")
|
|
323
324
|
console.log(dim(" host build — runs here, not deployable; drop --target host to build for a box"));
|
|
324
325
|
console.log(artifact.artifactDir);
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun's bundler ships full Node-compatible polyfills for node:path,
|
|
3
|
+
* node:url, and node:querystring under `target: "browser"`, but each has
|
|
4
|
+
* exactly one thing wrong with it for a Porffor-compiled handler — found
|
|
5
|
+
* by auditing bundled output against the capability check (source.ts) and,
|
|
6
|
+
* for node:url, by an actual failure through the real Porffor compiler.
|
|
7
|
+
* Auditing the bundle alone was not enough:
|
|
8
|
+
*
|
|
9
|
+
* - node:path's bundled `resolve()` falls back to `process.cwd()`, which
|
|
10
|
+
* trips the `process.` ban in `../packages/runtime/src/source.ts`.
|
|
11
|
+
* - node:querystring's bundled polyfill pulls in Bun's full Buffer
|
|
12
|
+
* implementation internally (for percent-decoding), tripping the
|
|
13
|
+
* `Buffer.` ban.
|
|
14
|
+
* - node:url's bundled `URL`/`URLSearchParams` exports are
|
|
15
|
+
* `var { URL, URLSearchParams } = globalThis;` — passes every capability
|
|
16
|
+
* check (no banned pattern in it) but Porffor's compiler rejects it at
|
|
17
|
+
* parse time as `Identifier 'URL' has already been declared`: Porffor
|
|
18
|
+
* pre-binds `URL` as a global, and a second top-level declaration of
|
|
19
|
+
* that exact name is a compile error, confirmed by actually building a
|
|
20
|
+
* handler through `sproutboat build` rather than just checking the
|
|
21
|
+
* bundle. `URL`/`URLSearchParams` are already global in a compiled
|
|
22
|
+
* handler (see `docs/concepts/handler.mdx`), so nothing needs importing
|
|
23
|
+
* — the fix is a shim that re-exports the globals under aliased local
|
|
24
|
+
* names instead of declaring anything literally named `URL`.
|
|
25
|
+
*
|
|
26
|
+
* None of these are the capability check being wrong — every case is a
|
|
27
|
+
* real thing (`process.cwd()`, `Buffer`, a name Porffor itself reserves)
|
|
28
|
+
* that doesn't exist the way a compiled handler needs it to, so the shims
|
|
29
|
+
* replace it rather than the check being loosened.
|
|
30
|
+
*/
|
|
31
|
+
const NODE_PATH_SHIM = `
|
|
32
|
+
function assertPath(path) {
|
|
33
|
+
if (typeof path !== "string")
|
|
34
|
+
throw TypeError("Path must be a string. Received " + JSON.stringify(path));
|
|
35
|
+
}
|
|
36
|
+
function normalizeStringPosix(path, allowAboveRoot) {
|
|
37
|
+
var res = "", lastSegmentLength = 0, lastSlash = -1, dots = 0, code;
|
|
38
|
+
for (var i = 0; i <= path.length; ++i) {
|
|
39
|
+
if (i < path.length) code = path.charCodeAt(i);
|
|
40
|
+
else if (code === 47) break;
|
|
41
|
+
else code = 47;
|
|
42
|
+
if (code === 47) {
|
|
43
|
+
if (lastSlash === i - 1 || dots === 1) {
|
|
44
|
+
} else if (lastSlash !== i - 1 && dots === 2) {
|
|
45
|
+
if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) {
|
|
46
|
+
if (res.length > 2) {
|
|
47
|
+
var lastSlashIndex = res.lastIndexOf("/");
|
|
48
|
+
if (lastSlashIndex !== res.length - 1) {
|
|
49
|
+
if (lastSlashIndex === -1) res = "", lastSegmentLength = 0;
|
|
50
|
+
else res = res.slice(0, lastSlashIndex), lastSegmentLength = res.length - 1 - res.lastIndexOf("/");
|
|
51
|
+
lastSlash = i, dots = 0;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
} else if (res.length === 2 || res.length === 1) {
|
|
55
|
+
res = "", lastSegmentLength = 0, lastSlash = i, dots = 0;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (allowAboveRoot) {
|
|
60
|
+
if (res.length > 0) res += "/..";
|
|
61
|
+
else res = "..";
|
|
62
|
+
lastSegmentLength = 2;
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
if (res.length > 0) res += "/" + path.slice(lastSlash + 1, i);
|
|
66
|
+
else res = path.slice(lastSlash + 1, i);
|
|
67
|
+
lastSegmentLength = i - lastSlash - 1;
|
|
68
|
+
}
|
|
69
|
+
lastSlash = i, dots = 0;
|
|
70
|
+
} else if (code === 46 && dots !== -1) ++dots;
|
|
71
|
+
else dots = -1;
|
|
72
|
+
}
|
|
73
|
+
return res;
|
|
74
|
+
}
|
|
75
|
+
function _format(sep, pathObject) {
|
|
76
|
+
var dir = pathObject.dir || pathObject.root, base = pathObject.base || (pathObject.name || "") + (pathObject.ext || "");
|
|
77
|
+
if (!dir) return base;
|
|
78
|
+
if (dir === pathObject.root) return dir + base;
|
|
79
|
+
return dir + sep + base;
|
|
80
|
+
}
|
|
81
|
+
function resolve() {
|
|
82
|
+
// Node falls back to process.cwd() here; a compiled handler has no real
|
|
83
|
+
// filesystem or cwd, so an all-relative resolve() resolves against a
|
|
84
|
+
// fixed virtual root instead.
|
|
85
|
+
var resolvedPath = "", resolvedAbsolute = false, cwd;
|
|
86
|
+
for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) {
|
|
87
|
+
var path;
|
|
88
|
+
if (i >= 0) path = arguments[i];
|
|
89
|
+
else {
|
|
90
|
+
if (cwd === undefined) cwd = "/";
|
|
91
|
+
path = cwd;
|
|
92
|
+
}
|
|
93
|
+
if (assertPath(path), path.length === 0) continue;
|
|
94
|
+
resolvedPath = path + "/" + resolvedPath, resolvedAbsolute = path.charCodeAt(0) === 47;
|
|
95
|
+
}
|
|
96
|
+
if (resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute), resolvedAbsolute)
|
|
97
|
+
if (resolvedPath.length > 0) return "/" + resolvedPath;
|
|
98
|
+
else return "/";
|
|
99
|
+
else if (resolvedPath.length > 0) return resolvedPath;
|
|
100
|
+
else return ".";
|
|
101
|
+
}
|
|
102
|
+
function normalize(path) {
|
|
103
|
+
if (assertPath(path), path.length === 0) return ".";
|
|
104
|
+
var isAbsolute = path.charCodeAt(0) === 47, trailingSeparator = path.charCodeAt(path.length - 1) === 47;
|
|
105
|
+
if (path = normalizeStringPosix(path, !isAbsolute), path.length === 0 && !isAbsolute) path = ".";
|
|
106
|
+
if (path.length > 0 && trailingSeparator) path += "/";
|
|
107
|
+
if (isAbsolute) return "/" + path;
|
|
108
|
+
return path;
|
|
109
|
+
}
|
|
110
|
+
function isAbsolute(path) {
|
|
111
|
+
return assertPath(path), path.length > 0 && path.charCodeAt(0) === 47;
|
|
112
|
+
}
|
|
113
|
+
function join() {
|
|
114
|
+
if (arguments.length === 0) return ".";
|
|
115
|
+
var joined;
|
|
116
|
+
for (var i = 0; i < arguments.length; ++i) {
|
|
117
|
+
var arg = arguments[i];
|
|
118
|
+
if (assertPath(arg), arg.length > 0)
|
|
119
|
+
if (joined === undefined) joined = arg;
|
|
120
|
+
else joined += "/" + arg;
|
|
121
|
+
}
|
|
122
|
+
if (joined === undefined) return ".";
|
|
123
|
+
return normalize(joined);
|
|
124
|
+
}
|
|
125
|
+
function relative(from, to) {
|
|
126
|
+
if (assertPath(from), assertPath(to), from === to) return "";
|
|
127
|
+
if (from = resolve(from), to = resolve(to), from === to) return "";
|
|
128
|
+
var fromStart = 1;
|
|
129
|
+
for (; fromStart < from.length; ++fromStart) if (from.charCodeAt(fromStart) !== 47) break;
|
|
130
|
+
var fromEnd = from.length, fromLen = fromEnd - fromStart, toStart = 1;
|
|
131
|
+
for (; toStart < to.length; ++toStart) if (to.charCodeAt(toStart) !== 47) break;
|
|
132
|
+
var toEnd = to.length, toLen = toEnd - toStart, length = fromLen < toLen ? fromLen : toLen, lastCommonSep = -1, i = 0;
|
|
133
|
+
for (; i <= length; ++i) {
|
|
134
|
+
if (i === length) {
|
|
135
|
+
if (toLen > length) {
|
|
136
|
+
if (to.charCodeAt(toStart + i) === 47) return to.slice(toStart + i + 1);
|
|
137
|
+
else if (i === 0) return to.slice(toStart + i);
|
|
138
|
+
} else if (fromLen > length) {
|
|
139
|
+
if (from.charCodeAt(fromStart + i) === 47) lastCommonSep = i;
|
|
140
|
+
else if (i === 0) lastCommonSep = 0;
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
var fromCode = from.charCodeAt(fromStart + i), toCode = to.charCodeAt(toStart + i);
|
|
145
|
+
if (fromCode !== toCode) break;
|
|
146
|
+
else if (fromCode === 47) lastCommonSep = i;
|
|
147
|
+
}
|
|
148
|
+
var out = "";
|
|
149
|
+
for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i)
|
|
150
|
+
if (i === fromEnd || from.charCodeAt(i) === 47)
|
|
151
|
+
if (out.length === 0) out += "..";
|
|
152
|
+
else out += "/..";
|
|
153
|
+
if (out.length > 0) return out + to.slice(toStart + lastCommonSep);
|
|
154
|
+
else {
|
|
155
|
+
if (toStart += lastCommonSep, to.charCodeAt(toStart) === 47) ++toStart;
|
|
156
|
+
return to.slice(toStart);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function dirname(path) {
|
|
160
|
+
if (assertPath(path), path.length === 0) return ".";
|
|
161
|
+
var code = path.charCodeAt(0), hasRoot = code === 47, end = -1, matchedSlash = true;
|
|
162
|
+
for (var i = path.length - 1; i >= 1; --i)
|
|
163
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
164
|
+
if (!matchedSlash) { end = i; break; }
|
|
165
|
+
} else matchedSlash = false;
|
|
166
|
+
if (end === -1) return hasRoot ? "/" : ".";
|
|
167
|
+
if (hasRoot && end === 1) return "//";
|
|
168
|
+
return path.slice(0, end);
|
|
169
|
+
}
|
|
170
|
+
function basename(path, ext) {
|
|
171
|
+
if (ext !== undefined && typeof ext !== "string") throw TypeError('"ext" argument must be a string');
|
|
172
|
+
assertPath(path);
|
|
173
|
+
var start = 0, end = -1, matchedSlash = true, i;
|
|
174
|
+
if (ext !== undefined && ext.length > 0 && ext.length <= path.length) {
|
|
175
|
+
if (ext.length === path.length && ext === path) return "";
|
|
176
|
+
var extIdx = ext.length - 1, firstNonSlashEnd = -1;
|
|
177
|
+
for (i = path.length - 1; i >= 0; --i) {
|
|
178
|
+
var code = path.charCodeAt(i);
|
|
179
|
+
if (code === 47) {
|
|
180
|
+
if (!matchedSlash) { start = i + 1; break; }
|
|
181
|
+
} else {
|
|
182
|
+
if (firstNonSlashEnd === -1) matchedSlash = false, firstNonSlashEnd = i + 1;
|
|
183
|
+
if (extIdx >= 0)
|
|
184
|
+
if (code === ext.charCodeAt(extIdx)) { if (--extIdx === -1) end = i; }
|
|
185
|
+
else extIdx = -1, end = firstNonSlashEnd;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (start === end) end = firstNonSlashEnd;
|
|
189
|
+
else if (end === -1) end = path.length;
|
|
190
|
+
return path.slice(start, end);
|
|
191
|
+
} else {
|
|
192
|
+
for (i = path.length - 1; i >= 0; --i)
|
|
193
|
+
if (path.charCodeAt(i) === 47) {
|
|
194
|
+
if (!matchedSlash) { start = i + 1; break; }
|
|
195
|
+
} else if (end === -1) matchedSlash = false, end = i + 1;
|
|
196
|
+
if (end === -1) return "";
|
|
197
|
+
return path.slice(start, end);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function extname(path) {
|
|
201
|
+
assertPath(path);
|
|
202
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0;
|
|
203
|
+
for (var i = path.length - 1; i >= 0; --i) {
|
|
204
|
+
var code = path.charCodeAt(i);
|
|
205
|
+
if (code === 47) {
|
|
206
|
+
if (!matchedSlash) { startPart = i + 1; break; }
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (end === -1) matchedSlash = false, end = i + 1;
|
|
210
|
+
if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; }
|
|
211
|
+
else if (startDot !== -1) preDotState = -1;
|
|
212
|
+
}
|
|
213
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) return "";
|
|
214
|
+
return path.slice(startDot, end);
|
|
215
|
+
}
|
|
216
|
+
function format(pathObject) {
|
|
217
|
+
if (pathObject === null || typeof pathObject !== "object")
|
|
218
|
+
throw TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject);
|
|
219
|
+
return _format("/", pathObject);
|
|
220
|
+
}
|
|
221
|
+
function parse(path) {
|
|
222
|
+
assertPath(path);
|
|
223
|
+
var ret = { root: "", dir: "", base: "", ext: "", name: "" };
|
|
224
|
+
if (path.length === 0) return ret;
|
|
225
|
+
var code = path.charCodeAt(0), isAbsolute2 = code === 47, start;
|
|
226
|
+
if (isAbsolute2) ret.root = "/", start = 1;
|
|
227
|
+
else start = 0;
|
|
228
|
+
var startDot = -1, startPart = 0, end = -1, matchedSlash = true, i = path.length - 1, preDotState = 0;
|
|
229
|
+
for (; i >= start; --i) {
|
|
230
|
+
if (code = path.charCodeAt(i), code === 47) {
|
|
231
|
+
if (!matchedSlash) { startPart = i + 1; break; }
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (end === -1) matchedSlash = false, end = i + 1;
|
|
235
|
+
if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; }
|
|
236
|
+
else if (startDot !== -1) preDotState = -1;
|
|
237
|
+
}
|
|
238
|
+
if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) {
|
|
239
|
+
if (end !== -1)
|
|
240
|
+
if (startPart === 0 && isAbsolute2) ret.base = ret.name = path.slice(1, end);
|
|
241
|
+
else ret.base = ret.name = path.slice(startPart, end);
|
|
242
|
+
} else {
|
|
243
|
+
if (startPart === 0 && isAbsolute2) ret.name = path.slice(1, startDot), ret.base = path.slice(1, end);
|
|
244
|
+
else ret.name = path.slice(startPart, startDot), ret.base = path.slice(startPart, end);
|
|
245
|
+
ret.ext = path.slice(startDot, end);
|
|
246
|
+
}
|
|
247
|
+
if (startPart > 0) ret.dir = path.slice(0, startPart - 1);
|
|
248
|
+
else if (isAbsolute2) ret.dir = "/";
|
|
249
|
+
return ret;
|
|
250
|
+
}
|
|
251
|
+
var sep = "/";
|
|
252
|
+
var delimiter = ":";
|
|
253
|
+
var path = { resolve, normalize, isAbsolute, join, relative, dirname, basename, extname, format, parse, sep, delimiter };
|
|
254
|
+
path.posix = path;
|
|
255
|
+
path.win32 = path;
|
|
256
|
+
export default path;
|
|
257
|
+
export { resolve, normalize, isAbsolute, join, relative, dirname, basename, extname, format, parse, sep, delimiter };
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* node:querystring's bundled polyfill pulls in Bun's full Buffer
|
|
262
|
+
* implementation (used internally for percent-decoding), which the
|
|
263
|
+
* capability check correctly rejects — Buffer isn't something Porffor's
|
|
264
|
+
* compiled output can rely on. This shim implements parse/stringify/
|
|
265
|
+
* escape/unescape (plus the encode/decode aliases Node exports) over
|
|
266
|
+
* plain string ops and encodeURIComponent/decodeURIComponent instead.
|
|
267
|
+
*/
|
|
268
|
+
const NODE_QUERYSTRING_SHIM = `
|
|
269
|
+
function qsUnescape(str) {
|
|
270
|
+
try { return decodeURIComponent(String(str).replace(/\\+/g, " ")); }
|
|
271
|
+
catch { return String(str); }
|
|
272
|
+
}
|
|
273
|
+
function qsEscape(str) {
|
|
274
|
+
return encodeURIComponent(String(str));
|
|
275
|
+
}
|
|
276
|
+
function parse(qs, sep, eq, options) {
|
|
277
|
+
sep = sep || "&";
|
|
278
|
+
eq = eq || "=";
|
|
279
|
+
var obj = Object.create(null);
|
|
280
|
+
if (typeof qs !== "string" || qs.length === 0) return obj;
|
|
281
|
+
var decode = (options && options.decodeURIComponent) || qsUnescape;
|
|
282
|
+
var maxKeys = options && typeof options.maxKeys === "number" ? options.maxKeys : 1000;
|
|
283
|
+
var pairs = qs.split(sep);
|
|
284
|
+
var limit = maxKeys > 0 ? Math.min(pairs.length, maxKeys) : pairs.length;
|
|
285
|
+
for (var i = 0; i < limit; i++) {
|
|
286
|
+
var pair = pairs[i];
|
|
287
|
+
var eqIdx = pair.indexOf(eq);
|
|
288
|
+
var key, value;
|
|
289
|
+
if (eqIdx >= 0) { key = pair.slice(0, eqIdx); value = pair.slice(eqIdx + eq.length); }
|
|
290
|
+
else { key = pair; value = ""; }
|
|
291
|
+
key = decode(key);
|
|
292
|
+
value = decode(value);
|
|
293
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
294
|
+
if (Array.isArray(obj[key])) obj[key].push(value);
|
|
295
|
+
else obj[key] = [obj[key], value];
|
|
296
|
+
} else obj[key] = value;
|
|
297
|
+
}
|
|
298
|
+
return obj;
|
|
299
|
+
}
|
|
300
|
+
function stringify(obj, sep, eq, options) {
|
|
301
|
+
sep = sep || "&";
|
|
302
|
+
eq = eq || "=";
|
|
303
|
+
obj = obj || {};
|
|
304
|
+
var encode = (options && options.encodeURIComponent) || qsEscape;
|
|
305
|
+
var parts = [];
|
|
306
|
+
for (var key in obj) {
|
|
307
|
+
if (!Object.prototype.hasOwnProperty.call(obj, key)) continue;
|
|
308
|
+
var value = obj[key];
|
|
309
|
+
var encodedKey = encode(key);
|
|
310
|
+
if (Array.isArray(value)) {
|
|
311
|
+
for (var i = 0; i < value.length; i++) parts.push(encodedKey + eq + encode(value[i]));
|
|
312
|
+
} else {
|
|
313
|
+
parts.push(encodedKey + eq + encode(value));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return parts.join(sep);
|
|
317
|
+
}
|
|
318
|
+
export { parse, stringify, qsEscape as escape, qsUnescape as unescape, stringify as encode, parse as decode };
|
|
319
|
+
export default { parse, stringify, escape: qsEscape, unescape: qsUnescape, encode: stringify, decode: parse };
|
|
320
|
+
`;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* `URL`/`URLSearchParams` are re-exported under aliased local names
|
|
324
|
+
* (`NodeURL`/`NodeURLSearchParams`) rather than declared as top-level
|
|
325
|
+
* `URL`/`URLSearchParams` — Porffor pre-binds `URL` as a global and
|
|
326
|
+
* rejects a second top-level declaration of that name. legacy
|
|
327
|
+
* parse/format/resolve are hand-written over the WHATWG URL rather than
|
|
328
|
+
* Bun's internal legacy-Url implementation, which isn't something this
|
|
329
|
+
* shim can import from; good enough for the common absolute-URL case,
|
|
330
|
+
* not a byte-for-byte port of Node's edge cases (relative/protocol-less
|
|
331
|
+
* input, unusual schemes).
|
|
332
|
+
*/
|
|
333
|
+
const NODE_URL_SHIM = `
|
|
334
|
+
var NodeURL = globalThis.URL;
|
|
335
|
+
var NodeURLSearchParams = globalThis.URLSearchParams;
|
|
336
|
+
function urlFormat(urlObject) {
|
|
337
|
+
if (typeof urlObject === "string") return urlObject;
|
|
338
|
+
if (urlObject instanceof NodeURL) return urlObject.href;
|
|
339
|
+
var protocol = urlObject.protocol || "";
|
|
340
|
+
var slashes = urlObject.slashes !== false && !!(urlObject.host || urlObject.hostname);
|
|
341
|
+
var auth = urlObject.auth ? urlObject.auth + "@" : "";
|
|
342
|
+
var host = urlObject.host || ((urlObject.hostname || "") + (urlObject.port ? ":" + urlObject.port : ""));
|
|
343
|
+
var pathname = urlObject.pathname || "";
|
|
344
|
+
var search = urlObject.search || (urlObject.query ? "?" + (typeof urlObject.query === "string" ? urlObject.query : "") : "");
|
|
345
|
+
var hash = urlObject.hash || "";
|
|
346
|
+
return protocol + (slashes ? "//" : "") + auth + host + pathname + search + hash;
|
|
347
|
+
}
|
|
348
|
+
function legacyFromURL(u) {
|
|
349
|
+
return {
|
|
350
|
+
protocol: u.protocol,
|
|
351
|
+
slashes: true,
|
|
352
|
+
auth: u.username ? u.username + (u.password ? ":" + u.password : "") : null,
|
|
353
|
+
host: u.host,
|
|
354
|
+
port: u.port || null,
|
|
355
|
+
hostname: u.hostname,
|
|
356
|
+
hash: u.hash || null,
|
|
357
|
+
search: u.search || null,
|
|
358
|
+
query: u.search ? u.search.slice(1) : null,
|
|
359
|
+
pathname: u.pathname,
|
|
360
|
+
path: u.pathname + (u.search || ""),
|
|
361
|
+
href: u.href,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function urlParse(input) {
|
|
365
|
+
return legacyFromURL(new NodeURL(input));
|
|
366
|
+
}
|
|
367
|
+
function urlResolve(from, to) {
|
|
368
|
+
return new NodeURL(to, new NodeURL(from)).href;
|
|
369
|
+
}
|
|
370
|
+
export { NodeURL as URL, NodeURLSearchParams as URLSearchParams, urlFormat as format, urlParse as parse, urlResolve as resolve };
|
|
371
|
+
export default { URL: NodeURL, URLSearchParams: NodeURLSearchParams, format: urlFormat, parse: urlParse, resolve: urlResolve };
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
export const NODE_SHIMS = {
|
|
375
|
+
"node:path": NODE_PATH_SHIM,
|
|
376
|
+
"node:querystring": NODE_QUERYSTRING_SHIM,
|
|
377
|
+
"node:url": NODE_URL_SHIM,
|
|
378
|
+
} satisfies Record<string, string>;
|
package/src/standalone-build.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* embedded transport already produces exactly the binary we want, so this
|
|
8
8
|
* copies it out and enforces the one thing a standalone binary cannot do.
|
|
9
9
|
*/
|
|
10
|
-
import { chmod, cp, mkdir
|
|
10
|
+
import { chmod, cp, mkdir } from "node:fs/promises";
|
|
11
11
|
import { resolve } from "node:path";
|
|
12
12
|
import { buildArtifact, type BuildInput } from "./build";
|
|
13
13
|
import type { Bindings } from "./wrap";
|
|
@@ -29,7 +29,7 @@ export type StandaloneBuildResult = { outPath: string; bytes: number };
|
|
|
29
29
|
* Outbound `fetch` is absent from this list: http and https both work, with
|
|
30
30
|
* BearSSL and the Mozilla root set compiled in.
|
|
31
31
|
*/
|
|
32
|
-
export function unsupportedBindings(bindings: Partial<Bindings>): string[] {
|
|
32
|
+
export function unsupportedBindings(bindings: Pick<Partial<Bindings>, "services" | "outbound">): string[] {
|
|
33
33
|
const reasons: string[] = [];
|
|
34
34
|
if ((bindings.services ?? []).length > 0) {
|
|
35
35
|
reasons.push("service bindings call another deployment through an edge, which a standalone binary has none of");
|
|
@@ -38,21 +38,11 @@ export function unsupportedBindings(bindings: Partial<Bindings>): string[] {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function buildStandalone(input: StandaloneBuildInput): Promise<StandaloneBuildResult> {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
let bindings: Partial<Bindings> = {};
|
|
44
|
-
try {
|
|
45
|
-
// SAFETY: bindings.json is written by buildArtifact from the validated
|
|
46
|
-
// config in this same call — our own output, not user input.
|
|
47
|
-
bindings = JSON.parse(await readFile(resolve(artifact.artifactDir, "bindings.json"), "utf8")) as Partial<Bindings>;
|
|
48
|
-
} catch {
|
|
49
|
-
bindings = {}; // a project with no bindings at all
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const blocked = unsupportedBindings(bindings);
|
|
41
|
+
const blocked = unsupportedBindings(input.config);
|
|
53
42
|
if (blocked.length > 0) {
|
|
54
43
|
throw new Error(`cannot build a standalone binary for this project:\n - ${blocked.join("\n - ")}`);
|
|
55
44
|
}
|
|
45
|
+
const artifact = await buildArtifact({ ...input, transport: "embedded" });
|
|
56
46
|
|
|
57
47
|
// The sprout *is* the binary: assets and bindings are compiled into it, and
|
|
58
48
|
// everything else it needs (port, data dir, secrets) arrives at run time.
|
package/src/toolchain.ts
CHANGED
|
@@ -38,7 +38,7 @@ const ZIG_SHA256 = {
|
|
|
38
38
|
// frozen reference handlers and update its COMPAT.md for any new mismatch before
|
|
39
39
|
// releasing — the alpha compiler's output can shift between pins. Checked by
|
|
40
40
|
// hand at bump time, not in CI.
|
|
41
|
-
// uWebSockets commit Porffor alpha-
|
|
41
|
+
// uWebSockets commit Porffor alpha-10 fetches for the native-fetch server.
|
|
42
42
|
const UWS_COMMIT = "360c276d";
|
|
43
43
|
const UWS_COMMIT_FULL = "360c276d609d59af56ae6932adb95154ace9f15f";
|
|
44
44
|
|
|
@@ -678,7 +678,7 @@ export function porfforVersion(): string {
|
|
|
678
678
|
|
|
679
679
|
export function esbuildVersion(): string {
|
|
680
680
|
// Legacy artifact field, retained so older control planes can read manifests.
|
|
681
|
-
// Handler bundling uses Bun.build and Porffor alpha
|
|
681
|
+
// Handler bundling uses Bun.build and Porffor alpha 10 needs no esbuild binary.
|
|
682
682
|
return "unused";
|
|
683
683
|
}
|
|
684
684
|
|
package/src/types-gen.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `sproutboat types` — the project's `env`, as TypeScript.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* types worth more here than they are there, not less.
|
|
4
|
+
* `env` remains available as a global in both compatibility modes. This file
|
|
5
|
+
* gives it the project's binding names and types. From 2026-09-28 it is also
|
|
6
|
+
* the second handler argument.
|
|
8
7
|
*
|
|
9
8
|
* The output declares `env` from `sproutboat.jsonc` and references the ambient
|
|
10
9
|
* types shipped in `types/sproutboat.d.ts`, so binding *shapes* version with
|
|
@@ -75,6 +74,11 @@ export function generateTypes(config: SproutboatConfig): string {
|
|
|
75
74
|
.join("\n")
|
|
76
75
|
: " // No bindings declared in sproutboat.jsonc yet.";
|
|
77
76
|
|
|
77
|
+
const handlerNote =
|
|
78
|
+
config.compatibility_date >= "2026-09-28"
|
|
79
|
+
? "The same object is passed as the second handler argument: fetch(request, env, ctx)."
|
|
80
|
+
: "The handler signature is fetch(request, ctx); env is only a global.";
|
|
81
|
+
|
|
78
82
|
return `${HEADER}
|
|
79
83
|
/// <reference types="sproutboat/types" />
|
|
80
84
|
|
|
@@ -82,7 +86,7 @@ declare global {
|
|
|
82
86
|
/**
|
|
83
87
|
* Bindings from sproutboat.jsonc, for project "${config.name}".
|
|
84
88
|
*
|
|
85
|
-
*
|
|
89
|
+
* ${handlerNote}
|
|
86
90
|
* Every call on these is synchronous, so \`await\` is allowed and does
|
|
87
91
|
* nothing.
|
|
88
92
|
*/
|
package/types/sproutboat.d.ts
CHANGED
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* generates a project's `sproutboat-env.d.ts`, which references this file and
|
|
7
7
|
* declares `env` with that project's bindings.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
9
|
+
* Bindings are synchronous. Before compatibility date 2026-09-28, `env` is a
|
|
10
|
+
* global only and `ctx` is the second handler argument. From that date, `env`
|
|
11
|
+
* is also passed as the second argument and `ctx` as the third.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
declare global {
|
|
@@ -300,11 +300,7 @@ declare global {
|
|
|
300
300
|
scheduledTime: number;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
* `env` is a global, not a parameter. `ctx` is the second argument, and its
|
|
305
|
-
* only member is `waitUntil`: work that outlives the response, drained
|
|
306
|
-
* in-process and capped at 25s.
|
|
307
|
-
*/
|
|
303
|
+
/** Work that outlives the response, drained in-process and capped at 25s. */
|
|
308
304
|
interface ExecutionContext {
|
|
309
305
|
waitUntil(promise: Promise<unknown>): void;
|
|
310
306
|
}
|
|
@@ -318,6 +314,13 @@ declare global {
|
|
|
318
314
|
queue?(batch: MessageBatch<QueueBody>, ctx: ExecutionContext): void | Promise<void>;
|
|
319
315
|
}
|
|
320
316
|
|
|
317
|
+
/** Handler signature for compatibility dates from 2026-09-28. */
|
|
318
|
+
interface WorkersHandler<Bindings = Record<string, unknown>, QueueBody = unknown> {
|
|
319
|
+
fetch(request: Request, env: Bindings, ctx: ExecutionContext): Response | Promise<Response>;
|
|
320
|
+
scheduled?(event: ScheduledEvent, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
|
|
321
|
+
queue?(batch: MessageBatch<QueueBody>, env: Bindings, ctx: ExecutionContext): void | Promise<void>;
|
|
322
|
+
}
|
|
323
|
+
|
|
321
324
|
/**
|
|
322
325
|
* What a Durable Object class implements. Declare it above the default
|
|
323
326
|
* export and name it in `durable_objects`.
|