@sproutboat/runtime 0.35.0 → 0.37.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.
@@ -0,0 +1,123 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { ensurePorffor } from "../../toolchain/src/acquire";
6
+ import { preludePath, TRANSPORT_MARKER, transportPath, wrapNativeFetchHandler } from "../src/wrap";
7
+
8
+ // TextDecoder encodings (UTF-16LE/BE, windows-1252), Blob.stream and Response.url in a compiled handler, compared with Bun
9
+ // (found while testing third-party packages).
10
+ const CASES = [
11
+ `new TextDecoder("utf-16le").decode(new Uint8Array([104, 0, 105, 0, 0x3d, 0xd8, 0x00, 0xde]))`,
12
+ `new TextDecoder("utf-16be").decode(new Uint8Array([0, 104, 0, 105]))`,
13
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
14
+ `JSON.stringify(new TextDecoder("utf-16le", { ignoreBOM: true }).decode(new Uint8Array([0xff, 0xfe, 65, 0])))`,
15
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([0x00, 0xd8, 65, 0])))`,
16
+ `JSON.stringify(new TextDecoder("utf-16le").decode(new Uint8Array([65, 0, 66])))`,
17
+ `(() => { try { new TextDecoder("utf-16le", { fatal: true }).decode(new Uint8Array([0x00, 0xd8])); return "no throw"; } catch (e) { return e.name; } })()`,
18
+ `(() => { const d = new TextDecoder("utf-16le"); return [d.decode(new Uint8Array([104]), { stream: true }), d.decode(new Uint8Array([0, 105]), { stream: true }), d.decode(new Uint8Array([0x3d, 0xd8]), { stream: true }), d.decode(new Uint8Array([0x00, 0xde]))].join("|"); })()`,
19
+ `new TextDecoder("windows-1252").decode(new Uint8Array([0x80, 0x41, 0xe9, 0x9f, 0xff, 0x7f]))`,
20
+ `JSON.stringify(new TextDecoder("windows-1252").decode(new Uint8Array([0x81, 0x8d, 0x8f, 0x90, 0x9d])))`,
21
+ `[new TextDecoder("latin1").encoding, new TextDecoder("ISO-8859-1").encoding, new TextDecoder(" ascii ").encoding, new TextDecoder("utf-16").encoding, new TextDecoder("UTF8").encoding, new TextDecoder("unicodefffe").encoding].join()`,
22
+ `(() => { try { new TextDecoder("not-an-encoding"); return "no throw"; } catch (e) { return e.name; } })()`,
23
+ `new TextDecoder("utf-8").decode(new Uint8Array([0xe2, 0x82, 0xac]))`,
24
+ `new Response(new Blob(["abc", new Uint8Array([100, 101])]).stream()).text()`,
25
+ `(async () => { const r = new Blob(["hello"]).stream().getReader(); const a = await r.read(); const b = await r.read(); return [a.done, a.value.length, b.done].join(); })()`,
26
+ `JSON.stringify([new Response("x").url, new Response("x").redirected, new Response(null, { status: 429 }).url + "|"])`,
27
+ `(async () => { const r = new Blob([]).stream().getReader(); const a = await r.read(); return a.done; })()`,
28
+ ];
29
+ const handler = `export default {async fetch() {
30
+ const cases = [
31
+ ${CASES.map((c) => ` () => (${c}),`).join("\n")}
32
+ ];
33
+ const out = [];
34
+ for (const run of cases) {
35
+ try { out.push(String(await run())); } catch (e) { out.push("THROWS " + (e && e.message)); }
36
+ }
37
+ return new Response(JSON.stringify(out));
38
+ }};`;
39
+ const want = await Promise.all(
40
+ CASES.map(async (c) => {
41
+ try {
42
+ return String(await new Function("return " + c)());
43
+ } catch (e) {
44
+ return "THROWS " + (e instanceof Error ? e.message : String(e));
45
+ }
46
+ }),
47
+ );
48
+
49
+ function freePort(): number {
50
+ const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
51
+ const port = listener.port;
52
+ listener.stop(true);
53
+ return port;
54
+ }
55
+
56
+ const workdir = await mkdtemp(join(tmpdir(), "sb-native-textdecoder-encodings-"));
57
+ let server: ReturnType<typeof Bun.spawn> | undefined;
58
+ try {
59
+ const porffor = await ensurePorffor();
60
+ const node = Bun.which("node");
61
+ if (!node) throw new Error("native streams test needs node on PATH");
62
+
63
+ const [core, broker] = await Promise.all([readFile(preludePath, "utf8"), readFile(transportPath("broker"), "utf8")]);
64
+ assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
65
+ const prelude = core.replace(TRANSPORT_MARKER, broker);
66
+ const generated = join(workdir, "textdecoder-encodings.generated.js");
67
+ const binary = join(workdir, "textdecoder-encodings-native");
68
+ await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
69
+ await rm(binary, { force: true });
70
+
71
+ const compile = Bun.spawn(
72
+ [node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"],
73
+ {
74
+ cwd: workdir,
75
+ env: process.env,
76
+ stdout: "pipe",
77
+ stderr: "pipe",
78
+ },
79
+ );
80
+ const [exitCode, stdout, stderr] = await Promise.all([
81
+ compile.exited,
82
+ new Response(compile.stdout).text(),
83
+ new Response(compile.stderr).text(),
84
+ ]);
85
+ assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
86
+
87
+ const port = freePort();
88
+ server = Bun.spawn([binary], {
89
+ cwd: workdir,
90
+ env: { ...process.env, PORT: String(port) },
91
+ stdout: "ignore",
92
+ stderr: "inherit",
93
+ });
94
+ let ready = false;
95
+ for (let attempt = 0; attempt < 50 && !ready; attempt++) {
96
+ try {
97
+ await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
98
+ ready = true;
99
+ } catch {
100
+ if (server.exitCode !== null) break;
101
+ await Bun.sleep(100);
102
+ }
103
+ }
104
+ assert(ready, "compiled handler did not start");
105
+ // SAFETY: the handler above is the only producer of this body: a JSON array with one
106
+ // string per case; the comparison below reports any entry that differs.
107
+ const got = JSON.parse(await (await fetch(`http://127.0.0.1:${port}/`)).text()) as string[];
108
+ const diffs = CASES.flatMap((c, i) =>
109
+ got[i] === want[i] ? [] : [`${c}\n want ${JSON.stringify(want[i])}\n got ${JSON.stringify(got[i])}`],
110
+ );
111
+ assert.deepEqual(
112
+ diffs,
113
+ [],
114
+ `TextDecoder, Blob.stream and Response.url results differ from Bun in ${diffs.length} of ${CASES.length} cases:\n${diffs.join("\n")}`,
115
+ );
116
+ console.log(`Native TextDecoder encodings, Blob.stream and Response.url match Bun in all ${CASES.length} cases`);
117
+ } finally {
118
+ if (server) {
119
+ server.kill(9);
120
+ await server.exited;
121
+ }
122
+ await rm(workdir, { recursive: true, force: true });
123
+ }