@sproutboat/runtime 0.14.0 → 0.15.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 +6 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +27 -9
- package/src/source.ts +1 -8
- package/test/native-iterators.ts +103 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Pin and vendor Porffor alpha-16 (43087d4). Enable Proxy and revocable Proxy in handler validation, add native regression coverage for traps and iterators, and drop three typed-array patches now implemented upstream. URLSearchParams and FormData now provide live, self-iterable keys, values and entries iterators. See PORFFOR_ALPHA16.md for the complete change audit and remaining package compatibility limits.
|
|
8
|
+
|
|
3
9
|
## 0.14.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -93,6 +93,22 @@ function __sbTagCpu(res, t0) {
|
|
|
93
93
|
return res;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// Live iterators use the current collection on each next(), including mutations.
|
|
97
|
+
function __sbCollectionIterator(owner, kind, formData) {
|
|
98
|
+
let index = 0;
|
|
99
|
+
return {
|
|
100
|
+
next() {
|
|
101
|
+
const length = formData ? owner._entries.length : owner._keys.length;
|
|
102
|
+
if (index >= length) return { value: undefined, done: true };
|
|
103
|
+
const key = formData ? owner._entries[index][0] : owner._keys[index];
|
|
104
|
+
const value = formData ? owner._entries[index][1] : owner._vals[index];
|
|
105
|
+
index++;
|
|
106
|
+
return { value: kind === 0 ? key : kind === 1 ? value : [key, value], done: false };
|
|
107
|
+
},
|
|
108
|
+
[Symbol.iterator]() { return this; },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
class __SproutboatURLSearchParams {
|
|
97
113
|
constructor(init) {
|
|
98
114
|
this._keys = [];
|
|
@@ -165,11 +181,15 @@ class __SproutboatURLSearchParams {
|
|
|
165
181
|
this._vals = idx.map((i) => this._vals[i]);
|
|
166
182
|
}
|
|
167
183
|
keys() {
|
|
168
|
-
return this
|
|
184
|
+
return __sbCollectionIterator(this, 0, false);
|
|
169
185
|
}
|
|
170
186
|
values() {
|
|
171
|
-
return this
|
|
187
|
+
return __sbCollectionIterator(this, 1, false);
|
|
188
|
+
}
|
|
189
|
+
entries() {
|
|
190
|
+
return __sbCollectionIterator(this, 2, false);
|
|
172
191
|
}
|
|
192
|
+
[Symbol.iterator]() { return this.entries(); }
|
|
173
193
|
get size() {
|
|
174
194
|
return this._keys.length;
|
|
175
195
|
}
|
|
@@ -318,24 +338,22 @@ if (globalThis.FormData == null) {
|
|
|
318
338
|
for (let i = 0; i < this._entries.length; i++) cb(this._entries[i][1], this._entries[i][0], this);
|
|
319
339
|
}
|
|
320
340
|
keys() {
|
|
321
|
-
return this
|
|
341
|
+
return __sbCollectionIterator(this, 0, true);
|
|
322
342
|
}
|
|
323
343
|
values() {
|
|
324
|
-
return this
|
|
344
|
+
return __sbCollectionIterator(this, 1, true);
|
|
325
345
|
}
|
|
326
346
|
entries() {
|
|
327
|
-
return this
|
|
347
|
+
return __sbCollectionIterator(this, 2, true);
|
|
328
348
|
}
|
|
329
349
|
}
|
|
350
|
+
__SproutboatFormData.prototype[Symbol.iterator] = __SproutboatFormData.prototype.entries;
|
|
330
351
|
globalThis.FormData = __SproutboatFormData;
|
|
331
352
|
}
|
|
332
353
|
|
|
333
354
|
function __sbParseUrlencodedFormData(text) {
|
|
334
355
|
const fd = new FormData();
|
|
335
|
-
// forEach
|
|
336
|
-
// arrays, and this shim's own URLSearchParams.keys()/values() return
|
|
337
|
-
// arrays as an implementation shortcut, so relying on either shape here
|
|
338
|
-
// would break against whichever kind actually ends up installed.
|
|
356
|
+
// forEach handles both the native API and the local shim.
|
|
339
357
|
new URLSearchParams(text).forEach((value, key) => fd.append(key, value));
|
|
340
358
|
return fd;
|
|
341
359
|
}
|
package/src/source.ts
CHANGED
|
@@ -25,14 +25,7 @@ const alwaysForbidden: Array<[RegExp, string]> = [
|
|
|
25
25
|
// match would also flag it appearing in an ordinary string a dependency
|
|
26
26
|
// happens to construct (a doc link, a log message).
|
|
27
27
|
[/['"`]node:/, "Node, Bun, and Deno APIs are not supported"],
|
|
28
|
-
|
|
29
|
-
// trapped property reads back as `undefined`, with no throw. Rejecting it
|
|
30
|
-
// here is the difference between a build error and a 502 nobody can explain.
|
|
31
|
-
// It is why itty-router and other Proxy-based routers do not work yet.
|
|
32
|
-
[
|
|
33
|
-
/\bnew\s+Proxy\s*\(|\bProxy\s*\.\s*revocable\s*\(/,
|
|
34
|
-
"Proxy is not supported by the compiler: its traps are silently ignored and the property reads back as undefined",
|
|
35
|
-
],
|
|
28
|
+
|
|
36
29
|
];
|
|
37
30
|
|
|
38
31
|
export function validateHttpSyncSource(source: string): SourceValidation {
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
const expected = {
|
|
9
|
+
query: [["a", "1"], ["a", "2"], ["b", "3"]],
|
|
10
|
+
keys: ["a", "a", "b"], values: ["1", "2", "3"],
|
|
11
|
+
live: ["x", "y", true], form: [["x", "1"], ["x", "2"]],
|
|
12
|
+
formLive: ["x", "y", true], self: true,
|
|
13
|
+
};
|
|
14
|
+
const handler = `export default { fetch(request) {
|
|
15
|
+
if (new URL(request.url).pathname === "/null") return new Response(null);
|
|
16
|
+
const params = new URLSearchParams("a=1&a=2&b=3");
|
|
17
|
+
const query = [];
|
|
18
|
+
for (const pair of params) query.push(pair);
|
|
19
|
+
const liveParams = new URLSearchParams("x=1");
|
|
20
|
+
const iterator = liveParams.keys();
|
|
21
|
+
const first = iterator.next().value;
|
|
22
|
+
liveParams.append("y", "2");
|
|
23
|
+
const second = iterator.next().value;
|
|
24
|
+
const done = iterator.next().done;
|
|
25
|
+
const fd = new FormData(); fd.append("x", "1"); fd.append("x", "2");
|
|
26
|
+
const form = []; for (const pair of fd) form.push(pair);
|
|
27
|
+
const liveForm = new FormData(); liveForm.append("x", "1");
|
|
28
|
+
const formIterator = liveForm.keys(); const f1 = formIterator.next().value;
|
|
29
|
+
liveForm.append("y", "2"); const f2 = formIterator.next().value;
|
|
30
|
+
return Response.json({ query, keys: Array.from(params.keys()), values: Array.from(params.values()),
|
|
31
|
+
live: [first, second, done], form, formLive: [f1, f2, formIterator.next().done],
|
|
32
|
+
self: iterator[Symbol.iterator]() === iterator });
|
|
33
|
+
} };`;
|
|
34
|
+
|
|
35
|
+
function freePort(): number {
|
|
36
|
+
const listener = Bun.listen({ hostname: "127.0.0.1", port: 0, socket: { data() {} } });
|
|
37
|
+
const port = listener.port;
|
|
38
|
+
listener.stop(true);
|
|
39
|
+
return port;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const workdir = await mkdtemp(join(tmpdir(), "sb-native-iterators-"));
|
|
43
|
+
let server: ReturnType<typeof Bun.spawn> | undefined;
|
|
44
|
+
try {
|
|
45
|
+
const porffor = await ensurePorffor({ cacheRoot: workdir });
|
|
46
|
+
const node = Bun.which("node");
|
|
47
|
+
if (!node) throw new Error("native iterators test needs node on PATH");
|
|
48
|
+
|
|
49
|
+
const [core, broker] = await Promise.all([
|
|
50
|
+
readFile(preludePath, "utf8"),
|
|
51
|
+
readFile(transportPath("broker"), "utf8"),
|
|
52
|
+
]);
|
|
53
|
+
assert(core.includes(TRANSPORT_MARKER), "runtime prelude is missing its transport marker");
|
|
54
|
+
const prelude = core.replace(TRANSPORT_MARKER, broker);
|
|
55
|
+
const generated = join(workdir, "iterators.generated.js");
|
|
56
|
+
const binary = join(workdir, "iterators-native");
|
|
57
|
+
await writeFile(generated, wrapNativeFetchHandler(handler, prelude));
|
|
58
|
+
await rm(binary, { force: true });
|
|
59
|
+
|
|
60
|
+
const compile = Bun.spawn([node, resolve(porffor, "runtime/index.js"), "native", generated, "-o", binary, "-s", "-O0"], {
|
|
61
|
+
cwd: workdir,
|
|
62
|
+
env: process.env,
|
|
63
|
+
stdout: "pipe",
|
|
64
|
+
stderr: "pipe",
|
|
65
|
+
});
|
|
66
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
67
|
+
compile.exited,
|
|
68
|
+
new Response(compile.stdout).text(),
|
|
69
|
+
new Response(compile.stderr).text(),
|
|
70
|
+
]);
|
|
71
|
+
assert.equal(exitCode, 0, `Porffor native compile failed:\n${stderr}\n${stdout}`);
|
|
72
|
+
|
|
73
|
+
const port = freePort();
|
|
74
|
+
server = Bun.spawn([binary], {
|
|
75
|
+
cwd: workdir,
|
|
76
|
+
env: { ...process.env, PORT: String(port) },
|
|
77
|
+
stdout: "ignore",
|
|
78
|
+
stderr: "inherit",
|
|
79
|
+
});
|
|
80
|
+
let response: Response | undefined;
|
|
81
|
+
for (let attempt = 0; attempt < 50; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
response = await fetch(`http://127.0.0.1:${port}/`, { signal: AbortSignal.timeout(500) });
|
|
84
|
+
break;
|
|
85
|
+
} catch {
|
|
86
|
+
if (server.exitCode !== null) break;
|
|
87
|
+
await Bun.sleep(100);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
assert(response, "compiled iterators handler did not start");
|
|
91
|
+
assert.equal(response.status, 200);
|
|
92
|
+
assert.deepEqual(await response.json(), expected);
|
|
93
|
+
const empty = await fetch(`http://127.0.0.1:${port}/null`);
|
|
94
|
+
assert.equal(empty.status, 200);
|
|
95
|
+
assert.equal(await empty.text(), "");
|
|
96
|
+
console.log("Native Web API iterators preserve entries, duplicate keys and live mutations");
|
|
97
|
+
} finally {
|
|
98
|
+
if (server) {
|
|
99
|
+
server.kill(9);
|
|
100
|
+
await server.exited;
|
|
101
|
+
}
|
|
102
|
+
await rm(workdir, { recursive: true, force: true });
|
|
103
|
+
}
|