@sproutboat/toolchain 0.6.0 → 0.7.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 +12 -0
- package/PORFFOR_ALPHA17.md +59 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +2 -0
- package/src/acquire.ts +1 -1
- package/src/alpha17-native.test.ts +42 -0
- package/src/patch.test.ts +38 -1
- package/src/patch.ts +40 -0
- package/src/pin.ts +4 -4
- package/vendor/porffor-731f1b2.tar.gz +0 -0
- package/vendor/porffor-43087d4.tar.gz +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 14546a0: Upgrade the verified vendored Porffor pin to Alpha 17, including correct Math.round ties and signed zero, native number parsing and formatting, full console number precision, and class-field capture fixes. Keep downstream compatibility patches until equivalent upstream changes ship.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- 14546a0: Register direct-only local function expressions in their lexical scope so type inference does not borrow an unrelated same-named function's return type. Fixes Better Auth's model-name resolver incorrectly treating an existing user schema as missing.
|
|
12
|
+
|
|
13
|
+
Correct Uint8Array base64 option string tags and complete-chunk truncation, unblocking base64url signatures and fixing padding and decoding.
|
|
14
|
+
|
|
3
15
|
## 0.6.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Porffor Alpha 17 evaluation
|
|
2
|
+
|
|
3
|
+
Verified on 2026-10-11. Released 2026-10-10:
|
|
4
|
+
https://github.com/CanadaHonk/porffor/releases/tag/alpha-17
|
|
5
|
+
|
|
6
|
+
Pinned commit: `731f1b2a150d441ed22da3d14f8b1e0daaae8ee4`.
|
|
7
|
+
Vendored archive SHA256:
|
|
8
|
+
`8eb65a3e77d58a23bfd2a1a60328672ad50e1ced7931f8d0ca3108e29d6cf846`.
|
|
9
|
+
|
|
10
|
+
## Changes since Alpha 16
|
|
11
|
+
|
|
12
|
+
- Number conversion moves to C, covering Number, parseInt, parseFloat and
|
|
13
|
+
formatting methods, including nondecimal radix conversion.
|
|
14
|
+
- Math.round implements JavaScript tie behavior and preserves negative zero.
|
|
15
|
+
- Console prints full number precision and signed zero.
|
|
16
|
+
- Class-field initializers ignore captures local to the initializer.
|
|
17
|
+
- Test262 history is updated. No new Proxy or Web Streams implementation ships.
|
|
18
|
+
|
|
19
|
+
The native regression compares dynamic numeric conversion, rounding, signed
|
|
20
|
+
zero, formatting and class-field captures against Bun. Existing stream,
|
|
21
|
+
base64 and lexical model-resolver regressions also run on the new pin.
|
|
22
|
+
|
|
23
|
+
## Downstream fixes retained
|
|
24
|
+
|
|
25
|
+
Web Streams still need the embedded ponyfill and compiler adaptations.
|
|
26
|
+
Better Auth's local model resolver needs lexical function registration and
|
|
27
|
+
correct return-type inference. Uint8Array base64 needs option string tag and
|
|
28
|
+
complete-chunk corrections. Alpha 17 does not contain these fixes.
|
|
29
|
+
|
|
30
|
+
Upstream main is one commit ahead at
|
|
31
|
+
`0342e06d1c893c15eb9d22c5fc2bfe2c5965c81b`:
|
|
32
|
+
https://github.com/CanadaHonk/porffor/commit/0342e06d1c893c15eb9d22c5fc2bfe2c5965c81b
|
|
33
|
+
It fixes TextEncoder astral and lone-surrogate encoding and encodeInto's short
|
|
34
|
+
buffer behavior. This overlaps our Unicode encoder patch and may let us remove
|
|
35
|
+
that patch after the change ships. Its unpatched encoder already passes our
|
|
36
|
+
native Unicode, lone-surrogate and short-buffer regression in an isolated copy. We keep the released pin and our verified patch for now.
|
|
37
|
+
|
|
38
|
+
## Better Auth
|
|
39
|
+
|
|
40
|
+
Version 1.7.1 passes native signup, signed-cookie sessions, wrong-password
|
|
41
|
+
rejection, signin, signout and session revocation using an explicit memory
|
|
42
|
+
adapter and its default password hashing. Optimized credential operations take
|
|
43
|
+
about 6 to 8 seconds on the development machine. The memory adapter is a probe,
|
|
44
|
+
not persistent storage. Production adapters and OAuth/plugins remain untested.
|
|
45
|
+
Database-free sessions select JWE and require AES, which native crypto does not
|
|
46
|
+
support. See the runtime's WEB_STREAMS.md for the stream transport limits.
|
|
47
|
+
|
|
48
|
+
## Verification
|
|
49
|
+
|
|
50
|
+
The development branches pass 183 package tests and 114 CLI tests; broker kitchen
|
|
51
|
+
sink passes 37 checks, standalone kitchen sink passes 40 checks, and all 21
|
|
52
|
+
small examples pass. The isolated package release passes 177 tests. Native
|
|
53
|
+
crypto, Unicode, live iterators, streams, base64,
|
|
54
|
+
model resolution and global timer checks pass. Native timer functions are also
|
|
55
|
+
exposed on globalThis for libraries using host-task yields.
|
|
56
|
+
|
|
57
|
+
The stream ponyfill IIFE still fails with a prototype TypeError on Alpha 17;
|
|
58
|
+
its module-scope adaptation remains necessary. The saved standalone auth probe
|
|
59
|
+
also passes with fresh noble hashes 2.4 and zod 4.6.5 dependencies.
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -128,7 +128,9 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
128
128
|
"}\n",
|
|
129
129
|
"compiler/builtins/json.ts": "// sb_json_utf16_v1\n",
|
|
130
130
|
"runtime/fetch-globals.js": "// sb_text_encoder_scalar_v1\n",
|
|
131
|
+
"compiler/builtins/uint8array_base64.ts": "let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;\n",
|
|
131
132
|
"compiler/codegen.js":
|
|
133
|
+
"if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();\n}\nconst func = resolveNamedFunction(scope, name) ?? funcByName(name);\n" +
|
|
132
134
|
" stmt(scope, Store(ctype, addr, 4, ctype === 'f64' || ctype === 'f32' ? f : toUint32(scope, f)));\n" +
|
|
133
135
|
" const taGet = (ctype, size, signed = true) => () => {\n" +
|
|
134
136
|
" const loaded = Load(ctype, taAddr(size), 4);\n" +
|
package/src/acquire.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
// @ts-expect-error Bun's file loader supplies a path; see the pinned-commit
|
|
7
7
|
// check below for why this is safe to trust unconditionally.
|
|
8
|
-
import vendoredPorfforArchive from "../vendor/porffor-
|
|
8
|
+
import vendoredPorfforArchive from "../vendor/porffor-731f1b2.tar.gz" with { type: "file" };
|
|
9
9
|
import { ensurePorfforPatched } from "./patch";
|
|
10
10
|
import { PORFFOR_ARCHIVE_SHA256, PORFFOR_ARCHIVE_URL, PORFFOR_COMMIT_FULL } from "./pin";
|
|
11
11
|
// Folded into the cache manifest below so a toolchain release that changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
import { ensurePorffor } from "./acquire";
|
|
6
|
+
|
|
7
|
+
const values = [-2.5, -1.5, -0.5, -0.1, 0, 0.5, 1.5, 2.5];
|
|
8
|
+
const strings = ["", " 42 ", "0xff", "0b101", "0o17", "1.2345678901234567", "1e-7", "-0", "12junk"];
|
|
9
|
+
const expected = {
|
|
10
|
+
rounded: values.map((value) => [Math.round(value), Object.is(Math.round(value), -0)]),
|
|
11
|
+
parsed: strings.map((value) => [Number(value), Object.is(Number(value), -0)]),
|
|
12
|
+
formatted: [1.2345678901234567, 1e-7, 1e21, 255].map((value) => [value.toString(), value.toString(16), value.toFixed(3)]),
|
|
13
|
+
field: [1, 2],
|
|
14
|
+
};
|
|
15
|
+
const source = `const values=${JSON.stringify(values)}, strings=${JSON.stringify(strings)};
|
|
16
|
+
class Field { value = (() => { const data = [1,2]; return () => data; })()(); }
|
|
17
|
+
console.log(JSON.stringify({
|
|
18
|
+
rounded:values.map(value=>[Math.round(value),Object.is(Math.round(value),-0)]),
|
|
19
|
+
parsed:strings.map(value=>[Number(value),Object.is(Number(value),-0)]),
|
|
20
|
+
formatted:[1.2345678901234567,1e-7,1e21,255].map(value=>[value.toString(),value.toString(16),value.toFixed(3)]),
|
|
21
|
+
field:new Field().value
|
|
22
|
+
}));
|
|
23
|
+
console.log(1.2345678901234567);`;
|
|
24
|
+
|
|
25
|
+
test("alpha-17: dynamic number conversion, rounding, precision and class fields match Bun", async () => {
|
|
26
|
+
const dir = await mkdtemp(resolve(tmpdir(), "sb-alpha17-"));
|
|
27
|
+
try {
|
|
28
|
+
const porffor = await ensurePorffor();
|
|
29
|
+
const input = resolve(dir, "numbers.js"), binary = resolve(dir, "numbers");
|
|
30
|
+
await writeFile(input, source);
|
|
31
|
+
await rm(binary, { force: true });
|
|
32
|
+
const compile = Bun.spawnSync(["bun", resolve(porffor, "runtime/index.js"), "native", input, "-o", binary, "-s", "-O0"], { stdout: "pipe", stderr: "pipe" });
|
|
33
|
+
expect(compile.exitCode, new TextDecoder().decode(compile.stderr)).toBe(0);
|
|
34
|
+
const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe", timeout: 10_000 });
|
|
35
|
+
expect(run.exitCode, new TextDecoder().decode(run.stderr)).toBe(0);
|
|
36
|
+
const lines = new TextDecoder().decode(run.stdout).replaceAll("\x1b[33m", "").replaceAll("\x1b[0m", "").trim().split("\n");
|
|
37
|
+
expect(JSON.parse(lines[0])).toEqual(JSON.parse(JSON.stringify(expected)));
|
|
38
|
+
expect(lines[1]).toBe("1.2345678901234567");
|
|
39
|
+
} finally {
|
|
40
|
+
await rm(dir, { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
}, 60_000);
|
package/src/patch.test.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { EMPTY_BINDINGS, wrapNativeFetchHandler } from "../../runtime/src/wrap";
|
|
6
|
-
import { patchRenderJs, patchUwebsockets } from "./patch";
|
|
6
|
+
import { patchLocalFunctionTypes, patchUint8ArrayBase64, patchRenderJs, patchUwebsockets } from "./patch";
|
|
7
7
|
|
|
8
8
|
// The parts of Porffor's compiler/render.js the patch anchors to.
|
|
9
9
|
const RENDER = `void porf_native_fetch_runtime_init(void) {
|
|
@@ -299,3 +299,40 @@ test("#165: render.js routes console output to stderr, unbuffered, idempotently"
|
|
|
299
299
|
await rm(root, { recursive: true, force: true });
|
|
300
300
|
}
|
|
301
301
|
});
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
test("local function type patch registers lexical bindings and is idempotent", async () => {
|
|
305
|
+
const root = await mkdtemp(join(tmpdir(), "sb-local-functions-"));
|
|
306
|
+
try {
|
|
307
|
+
await mkdir(join(root, "compiler"));
|
|
308
|
+
const file = join(root, "compiler/codegen.js");
|
|
309
|
+
await writeFile(file, "if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();\n}\nconst func = resolveNamedFunction(scope, name) ?? funcByName(name);");
|
|
310
|
+
await patchLocalFunctionTypes(root);
|
|
311
|
+
const patched = await readFile(file, "utf8");
|
|
312
|
+
expect(patched).toContain("bindNamedFunction(scope, name, func);");
|
|
313
|
+
expect(patched).not.toContain("?? funcByName(name)");
|
|
314
|
+
await patchLocalFunctionTypes(root);
|
|
315
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
316
|
+
} finally {
|
|
317
|
+
await rm(root, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
test("base64 patch preserves option tags and truncates full chunks, idempotently", async () => {
|
|
323
|
+
const root = await mkdtemp(join(tmpdir(), "sb-base64-patch-"));
|
|
324
|
+
try {
|
|
325
|
+
await mkdir(join(root, "compiler/builtins"), { recursive: true });
|
|
326
|
+
const file = join(root, "compiler/builtins/uint8array_base64.ts");
|
|
327
|
+
await writeFile(file, "let alphabet: string = 'base64';\nlet lastChunkHandling: string = 'loose';\nconst fullChunks: i32 = (len / 3) * 3;");
|
|
328
|
+
await patchUint8ArrayBase64(root);
|
|
329
|
+
const patched = await readFile(file, "utf8");
|
|
330
|
+
expect(patched).toContain("let alphabet: any =");
|
|
331
|
+
expect(patched).toContain("let lastChunkHandling: any =");
|
|
332
|
+
expect(patched).toContain("((len / 3) | 0) * 3");
|
|
333
|
+
await patchUint8ArrayBase64(root);
|
|
334
|
+
expect(await readFile(file, "utf8")).toBe(patched);
|
|
335
|
+
} finally {
|
|
336
|
+
await rm(root, { recursive: true, force: true });
|
|
337
|
+
}
|
|
338
|
+
});
|
package/src/patch.ts
CHANGED
|
@@ -1249,6 +1249,44 @@ export async function patchTypedArrayStore(root: string): Promise<void> {
|
|
|
1249
1249
|
if (changed) await writeFile(file, src);
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
|
+
/** Register elided local function expressions so inference uses their lexical binding. */
|
|
1253
|
+
export async function patchLocalFunctionTypes(root: string): Promise<void> {
|
|
1254
|
+
const file = resolve(root, "compiler/codegen.js");
|
|
1255
|
+
let src = await readFile(file, "utf8");
|
|
1256
|
+
const marker = "sproutboat: preserve lexical types for direct-only local functions";
|
|
1257
|
+
if (src.includes(marker)) return;
|
|
1258
|
+
const anchor = "if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n return valUndefined();";
|
|
1259
|
+
if (!src.includes(anchor)) throw new Error(`could not patch Porffor local function types: anchor not found in ${file}`);
|
|
1260
|
+
src = src.replace(anchor,
|
|
1261
|
+
"if (!hoistRead && directCallOnlyFunctionBinding(scope, kind, name, pattern, func)) {\n" +
|
|
1262
|
+
` // ${marker}\n` +
|
|
1263
|
+
" bindNamedFunction(scope, name, func);\n return valUndefined();");
|
|
1264
|
+
// resolveNamedFunction already handles unambiguous global fallbacks. Falling
|
|
1265
|
+
// back again can infer a type from an unrelated same-named function.
|
|
1266
|
+
const fallback = "const func = resolveNamedFunction(scope, name) ?? funcByName(name);";
|
|
1267
|
+
if (!src.includes(fallback)) throw new Error(`could not patch Porffor local function type fallback: anchor not found in ${file}`);
|
|
1268
|
+
src = src.replace(fallback, "const func = resolveNamedFunction(scope, name);");
|
|
1269
|
+
await writeFile(file, src);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** Keep base64 option tags accurate and truncate the complete-chunk count. */
|
|
1273
|
+
export async function patchUint8ArrayBase64(root: string): Promise<void> {
|
|
1274
|
+
const file = resolve(root, "compiler/builtins/uint8array_base64.ts");
|
|
1275
|
+
let src = await readFile(file, "utf8");
|
|
1276
|
+
const marker = "sproutboat: base64 option tags and complete chunks";
|
|
1277
|
+
if (src.includes(marker)) return;
|
|
1278
|
+
const edits = [
|
|
1279
|
+
["let alphabet: string =", "let alphabet: any ="],
|
|
1280
|
+
["let lastChunkHandling: string =", "let lastChunkHandling: any ="],
|
|
1281
|
+
["const fullChunks: i32 = (len / 3) * 3;", "const fullChunks: i32 = ((len / 3) | 0) * 3;"],
|
|
1282
|
+
] as const;
|
|
1283
|
+
for (const [anchor, replacement] of edits) {
|
|
1284
|
+
if (!src.includes(anchor)) throw new Error(`could not patch Porffor base64: anchor not found in ${file}`);
|
|
1285
|
+
src = src.replaceAll(anchor, replacement);
|
|
1286
|
+
}
|
|
1287
|
+
await writeFile(file, `// ${marker}\n${src}`);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1252
1290
|
export async function precompileBuiltins(root: string): Promise<void> {
|
|
1253
1291
|
const table = resolve(root, "compiler/builtins_precompiled.js");
|
|
1254
1292
|
// Porffor only writes the table when import.meta.url matches argv[1].
|
|
@@ -1319,6 +1357,8 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
1319
1357
|
await patchPromiseResolveNull(root);
|
|
1320
1358
|
await patchReplaceAll(root);
|
|
1321
1359
|
await patchTypedArrayStore(root);
|
|
1360
|
+
await patchLocalFunctionTypes(root);
|
|
1361
|
+
await patchUint8ArrayBase64(root);
|
|
1322
1362
|
// Last of the builtin edits: its precompile picks up the date, promise and
|
|
1323
1363
|
// replaceAll patches above too.
|
|
1324
1364
|
await precompileBuiltins(root);
|
package/src/pin.ts
CHANGED
|
@@ -11,14 +11,14 @@
|
|
|
11
11
|
* uWebSockets archive in `sproutboat-cli/vendor/` — then run both repos'
|
|
12
12
|
* test + conformance suites. See sproutboat-cli/MIGRATION.md.
|
|
13
13
|
*/
|
|
14
|
-
export const PORFFOR_CHANNEL = "alpha-
|
|
15
|
-
export const PORFFOR_COMMIT_FULL = "
|
|
14
|
+
export const PORFFOR_CHANNEL = "alpha-17";
|
|
15
|
+
export const PORFFOR_COMMIT_FULL = "731f1b2a150d441ed22da3d14f8b1e0daaae8ee4";
|
|
16
16
|
export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
|
|
17
17
|
export const PORFFOR_ARCHIVE_SHA256 =
|
|
18
|
-
"
|
|
18
|
+
"8eb65a3e77d58a23bfd2a1a60328672ad50e1ced7931f8d0ca3108e29d6cf846";
|
|
19
19
|
export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
|
|
20
20
|
|
|
21
|
-
/** A compact identity string for a manifest / report (`alpha-
|
|
21
|
+
/** A compact identity string for a manifest / report (`alpha-17 (731f1b2)`). */
|
|
22
22
|
export function porfforVersion(): string {
|
|
23
23
|
return process.env.PORFFOR_VERSION || `${PORFFOR_CHANNEL} (${PORFFOR_COMMIT})`;
|
|
24
24
|
}
|
|
Binary file
|
|
Binary file
|