@sproutboat/runtime 0.17.1 → 0.18.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @sproutboat/runtime
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 145e3dc: `HTMLRewriter`, matching Cloudflare's API and lol-html's output.
8
+
9
+ - **Handlers:** `on(selector, { element, text, comments })` and `onDocument({ doctype, text, comments, end })`.
10
+ - **Element methods:** attributes, `tagName`, `before`/`after`/`prepend`/`append`/`replace`/`setInnerContent` with `{ html }`, `remove`, `removeAndKeepContent` and `onEndTag`.
11
+ - **Selectors:** type, `*`, `#id`, `.class`, the attribute operators, descendant and child combinators, and lists.
12
+ - **Async:** handlers may be async.
13
+
14
+ It reads the whole body before rewriting rather than streaming it. 37 cases are checked against Bun's lol-html-based HTMLRewriter, including where lol-html keeps attributes byte for byte and does not imply end tags.
15
+ - 2541458: More of the Web platform, from sproutd's W2 conformance suite against workerd:
16
+
17
+ - `CompressionStream` and `DecompressionStream` for `gzip`, `deflate` and `deflate-raw`, in JavaScript, because the native runtime links no zlib. Output decompresses with zlib, and zlib's output (stored, fixed and dynamic blocks) decompresses here. A corrupt or truncated input errors with a `TypeError`.
18
+ - A `Response` or `Request` whose body is a `ReadableStream` is read to the end by `text()`, `json()`, `arrayBuffer()` and `blob()`. Before, these returned `"[object ReadableStream]"`. Chunks must be bytes, as in workerd.
19
+ - `JSON.parse` applies its reviver: children first, `undefined` deletes the property, and `this` is the holder.
20
+
21
+ ### Patch Changes
22
+
23
+ - 116ee47: sproutd builds the runtime again. In split builds Porffor cannot type a prelude function's parameters, and the compression checksums' `let c = ~crc` compiled to C that assigned a boxed value to a number. The checksums now start from the parameter itself.
24
+
3
25
  ## 0.17.1
4
26
 
5
27
  ### Patch Changes
package/WEB_STREAMS.md CHANGED
@@ -14,8 +14,14 @@ the status and headers go out at once and each chunk is written as it is produce
14
14
  (chunked encoding), so Server-Sent Events and long responses work. Chunks must be bytes.
15
15
  Hosts that do not stream yet (sproutd's workers) read the stream to the end and send it
16
16
  buffered, at most 32 MiB. Request bodies and outbound fetch remain buffered.
17
- TextEncoderStream and TextDecoderStream are included; CompressionStream and
18
- DecompressionStream are not.
17
+ TextEncoderStream and TextDecoderStream are included, and CompressionStream and
18
+ DecompressionStream ("gzip", "deflate", "deflate-raw") in JavaScript, since the native
19
+ runtime links no zlib. Compression writes one fixed-Huffman block per chunk as it arrives
20
+ (close to zlib on text: 252 bytes against 221 for 22.5 KB); decompression inflates all
21
+ block types once the input ends and checks the CRC-32 or Adler-32. Response and Request
22
+ bodies that are ReadableStreams are read by text(), json(), arrayBuffer() and blob(), and
23
+ their chunks must be bytes, as in workerd. A body given as a string, bytes or a Blob is
24
+ still exposed as that value, not as a ReadableStream.
19
25
  BYOB buffer detachment requires transfer support from the underlying engine;
20
26
  the polyfill cannot supply that engine primitive. Full Web Platform Tests
21
27
  conformance has not been established for Porffor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sproutboat/runtime",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -2305,6 +2305,706 @@
2305
2305
  /* oxlint-enable */
2306
2306
  // END WEB STREAMS
2307
2307
 
2308
+ // COMPRESSION STREAMS: CompressionStream and DecompressionStream ("gzip", "deflate",
2309
+ // "deflate-raw"), in JavaScript: the native runtime links no zlib. Compression is LZ77 with
2310
+ // fixed Huffman codes, one block per written chunk, so output follows input as it arrives;
2311
+ // it is smaller than the input for text, not as small as zlib's. Decompression inflates
2312
+ // stored, fixed and dynamic blocks once the input ends, and checks the CRC-32 or Adler-32.
2313
+ // A malformed or truncated stream errors with a TypeError, as in workerd.
2314
+
2315
+ const __sbCrcTable = (() => {
2316
+ const table = new Int32Array(256);
2317
+ for (let n = 0; n < 256; n++) {
2318
+ let c = n;
2319
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
2320
+ table[n] = c;
2321
+ }
2322
+ return table;
2323
+ })();
2324
+ // c starts as the parameter itself. In split builds (sproutd) a prelude function may be
2325
+ // called from app code, so Porffor cannot type its parameters; it typed c as a number
2326
+ // from the loop but boxed ~crc (crc might be a BigInt), and the C did not compile.
2327
+ function __sbCrc32(crc, bytes) {
2328
+ let c = crc;
2329
+ c = ~c;
2330
+ for (let i = 0; i < bytes.length; i++) c = __sbCrcTable[(c ^ bytes[i]) & 255] ^ (c >>> 8);
2331
+ return ~c >>> 0;
2332
+ }
2333
+ function __sbAdler32(adler, bytes) {
2334
+ let a = adler, b = adler;
2335
+ a = a & 0xffff;
2336
+ b = b >>> 16;
2337
+ for (let i = 0; i < bytes.length; ) {
2338
+ const end = Math.min(i + 3800, bytes.length);
2339
+ for (; i < end; i++) { a += bytes[i]; b += a; }
2340
+ a %= 65521; b %= 65521;
2341
+ }
2342
+ return ((b << 16) | a) >>> 0;
2343
+ }
2344
+
2345
+ // Length and distance codes (RFC 1951 3.2.5).
2346
+ const __sbLenBase = [3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258];
2347
+ const __sbLenExtra = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0];
2348
+ const __sbDistBase = [1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577];
2349
+ const __sbDistExtra = [0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13];
2350
+
2351
+ class __sbBitWriter {
2352
+ constructor() { this.out = new Uint8Array(1024); this.length = 0; this.bits = 0; this.count = 0; }
2353
+ byte(b) {
2354
+ if (this.length == this.out.length) { const grown = new Uint8Array(this.out.length * 2); grown.set(this.out); this.out = grown; }
2355
+ this.out[this.length++] = b;
2356
+ }
2357
+ // Values go out least significant bit first.
2358
+ write(value, n) {
2359
+ this.bits |= value << this.count;
2360
+ this.count += n;
2361
+ while (this.count >= 8) { this.byte(this.bits & 255); this.bits >>>= 8; this.count -= 8; }
2362
+ }
2363
+ // Huffman codes go out most significant bit first.
2364
+ code(code, n) {
2365
+ let reversed = 0;
2366
+ for (let i = 0; i < n; i++) { reversed = (reversed << 1) | (code & 1); code >>= 1; }
2367
+ this.write(reversed, n);
2368
+ }
2369
+ align() { if (this.count > 0) { this.byte(this.bits & 255); this.bits = 0; this.count = 0; } }
2370
+ take() { const out = this.out.slice(0, this.length); this.length = 0; return out; }
2371
+ }
2372
+ function __sbFixedLiteral(w, v) {
2373
+ if (v < 144) w.code(0x30 + v, 8);
2374
+ else if (v < 256) w.code(0x190 + v - 144, 9);
2375
+ else if (v < 280) w.code(v - 256, 7);
2376
+ else w.code(0xc0 + v - 280, 8);
2377
+ }
2378
+ // One fixed-Huffman block for data, matching within it (a hash chain of 3-byte prefixes).
2379
+ function __sbDeflateBlock(w, data, last) {
2380
+ w.write(last ? 1 : 0, 1);
2381
+ w.write(1, 2);
2382
+ const head = new Int32Array(1 << 15).fill(-1);
2383
+ const prev = new Int32Array(Math.max(data.length, 1));
2384
+ const hash = (i) => ((data[i] << 10) ^ (data[i + 1] << 5) ^ data[i + 2]) & 0x7fff;
2385
+ let i = 0;
2386
+ while (i < data.length) {
2387
+ let bestLen = 0, bestDist = 0;
2388
+ if (i + 2 < data.length) {
2389
+ const h = hash(i);
2390
+ let candidate = head[h], chain = 32;
2391
+ while (candidate >= 0 && i - candidate <= 32768 && chain-- > 0) {
2392
+ let len = 0;
2393
+ const max = Math.min(258, data.length - i);
2394
+ while (len < max && data[candidate + len] == data[i + len]) len++;
2395
+ if (len > bestLen) { bestLen = len; bestDist = i - candidate; if (len == max) break; }
2396
+ candidate = prev[candidate];
2397
+ }
2398
+ prev[i] = head[h];
2399
+ head[h] = i;
2400
+ }
2401
+ if (bestLen >= 3) {
2402
+ let code = 0;
2403
+ while (code < 28 && __sbLenBase[code + 1] <= bestLen) code++;
2404
+ __sbFixedLiteral(w, 257 + code);
2405
+ if (__sbLenExtra[code]) w.write(bestLen - __sbLenBase[code], __sbLenExtra[code]);
2406
+ let dcode = 0;
2407
+ while (dcode < 29 && __sbDistBase[dcode + 1] <= bestDist) dcode++;
2408
+ w.code(dcode, 5);
2409
+ if (__sbDistExtra[dcode]) w.write(bestDist - __sbDistBase[dcode], __sbDistExtra[dcode]);
2410
+ // Index the skipped positions so later matches can start inside this one.
2411
+ for (let k = 1; k < bestLen && i + k + 2 < data.length; k++) { const h = hash(i + k); prev[i + k] = head[h]; head[h] = i + k; }
2412
+ i += bestLen;
2413
+ } else {
2414
+ __sbFixedLiteral(w, data[i]);
2415
+ i++;
2416
+ }
2417
+ }
2418
+ __sbFixedLiteral(w, 256);
2419
+ }
2420
+
2421
+ class __sbHuffman {
2422
+ // Canonical code from code lengths, decoded bit by bit (RFC 1951 3.2.2).
2423
+ constructor(lengths) {
2424
+ this.counts = new Int32Array(16);
2425
+ this.symbols = new Int32Array(lengths.length);
2426
+ for (let i = 0; i < lengths.length; i++) this.counts[lengths[i]]++;
2427
+ this.counts[0] = 0;
2428
+ const offsets = new Int32Array(16);
2429
+ for (let i = 1; i < 16; i++) offsets[i] = offsets[i - 1] + this.counts[i - 1];
2430
+ for (let i = 0; i < lengths.length; i++) if (lengths[i]) this.symbols[offsets[lengths[i]]++] = i;
2431
+ }
2432
+ }
2433
+ class __sbBitReader {
2434
+ constructor(data, at) { this.data = data; this.at = at; this.bits = 0; this.count = 0; }
2435
+ need(n) {
2436
+ while (this.count < n) {
2437
+ if (this.at >= this.data.length) throw new TypeError("Decompression failed: unexpected end of data");
2438
+ this.bits |= this.data[this.at++] << this.count;
2439
+ this.count += 8;
2440
+ }
2441
+ }
2442
+ read(n) {
2443
+ if (n == 0) return 0;
2444
+ this.need(n);
2445
+ const v = this.bits & ((1 << n) - 1);
2446
+ this.bits >>>= n;
2447
+ this.count -= n;
2448
+ return v;
2449
+ }
2450
+ decode(h) {
2451
+ let code = 0, first = 0, index = 0;
2452
+ for (let len = 1; len < 16; len++) {
2453
+ code |= this.read(1);
2454
+ const count = h.counts[len];
2455
+ if (code - count < first) return h.symbols[index + (code - first)];
2456
+ index += count;
2457
+ first += count;
2458
+ first <<= 1;
2459
+ code <<= 1;
2460
+ }
2461
+ throw new TypeError("Decompression failed: invalid Huffman code");
2462
+ }
2463
+ // The byte after the last bit read; stored blocks and trailers start on a byte.
2464
+ byteAt() { return this.at - (this.count >> 3); }
2465
+ align() { this.bits = 0; this.count = 0; }
2466
+ }
2467
+ const __sbFixedLit = (() => {
2468
+ const l = new Uint8Array(288);
2469
+ for (let i = 0; i < 288; i++) l[i] = i < 144 ? 8 : i < 256 ? 9 : i < 280 ? 7 : 8;
2470
+ return new __sbHuffman(l);
2471
+ })();
2472
+ const __sbFixedDist = new __sbHuffman(new Uint8Array(30).fill(5));
2473
+ const __sbClOrder = [16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15];
2474
+ function __sbGrow(out, used, n) {
2475
+ let size = out.length * 2;
2476
+ while (size < used + n) size *= 2;
2477
+ const grown = new Uint8Array(size);
2478
+ grown.set(out.subarray(0, used));
2479
+ return grown;
2480
+ }
2481
+ // Inflates raw deflate data from at; answers the output and the byte after the last block.
2482
+ function __sbInflate(data, at) {
2483
+ const r = new __sbBitReader(data, at);
2484
+ // Not named length: Porffor loses writes to a local of that name (Alpha 18).
2485
+ let out = new Uint8Array(Math.max(1024, data.length * 4));
2486
+ let used = 0;
2487
+ let last = 0;
2488
+ while (!last) {
2489
+ last = r.read(1);
2490
+ const type = r.read(2);
2491
+ if (type == 0) {
2492
+ r.align();
2493
+ const p = r.at;
2494
+ if (p + 4 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2495
+ const len = data[p] | (data[p + 1] << 8), nlen = data[p + 2] | (data[p + 3] << 8);
2496
+ if ((len ^ 0xffff) != nlen) throw new TypeError("Decompression failed: invalid stored block");
2497
+ if (p + 4 + len > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2498
+ if (used + len > out.length) out = __sbGrow(out, used, len);
2499
+ out.set(data.subarray(p + 4, p + 4 + len), used); used += len;
2500
+ r.at = p + 4 + len;
2501
+ continue;
2502
+ }
2503
+ let lit = __sbFixedLit, dist = __sbFixedDist;
2504
+ if (type == 2) {
2505
+ const hlit = r.read(5) + 257, hdist = r.read(5) + 1, hclen = r.read(4) + 4;
2506
+ const cl = new Uint8Array(19);
2507
+ for (let i = 0; i < hclen; i++) cl[__sbClOrder[i]] = r.read(3);
2508
+ const clh = new __sbHuffman(cl);
2509
+ const lengths = new Uint8Array(hlit + hdist);
2510
+ for (let i = 0; i < hlit + hdist; ) {
2511
+ const sym = r.decode(clh);
2512
+ if (sym < 16) lengths[i++] = sym;
2513
+ else {
2514
+ let repeat = 0, value = 0;
2515
+ if (sym == 16) { if (i == 0) throw new TypeError("Decompression failed: invalid code lengths"); value = lengths[i - 1]; repeat = 3 + r.read(2); }
2516
+ else if (sym == 17) repeat = 3 + r.read(3);
2517
+ else repeat = 11 + r.read(7);
2518
+ if (i + repeat > hlit + hdist) throw new TypeError("Decompression failed: invalid code lengths");
2519
+ while (repeat--) lengths[i++] = value;
2520
+ }
2521
+ }
2522
+ lit = new __sbHuffman(lengths.subarray(0, hlit));
2523
+ dist = new __sbHuffman(lengths.subarray(hlit));
2524
+ } else if (type != 1) throw new TypeError("Decompression failed: invalid block type");
2525
+ for (;;) {
2526
+ const sym = r.decode(lit);
2527
+ if (sym < 256) { if (used + 1 > out.length) out = __sbGrow(out, used, 1); out[used++] = sym; continue; }
2528
+ if (sym == 256) break;
2529
+ const code = sym - 257;
2530
+ if (code >= 29) throw new TypeError("Decompression failed: invalid used code");
2531
+ const len = __sbLenBase[code] + r.read(__sbLenExtra[code]);
2532
+ const dcode = r.decode(dist);
2533
+ if (dcode >= 30) throw new TypeError("Decompression failed: invalid distance code");
2534
+ const d = __sbDistBase[dcode] + r.read(__sbDistExtra[dcode]);
2535
+ if (d > used) throw new TypeError("Decompression failed: distance too far back");
2536
+ if (used + len > out.length) out = __sbGrow(out, used, len);
2537
+ for (let k = 0; k < len; k++) { out[used] = out[used - d]; used++; }
2538
+ }
2539
+ }
2540
+ return { out: out.slice(0, used), end: r.byteAt() };
2541
+ }
2542
+
2543
+ function __sbFormat(format, what) {
2544
+ const f = String(format);
2545
+ if (f != "gzip" && f != "deflate" && f != "deflate-raw") throw new TypeError("Unsupported " + what + " format: " + f);
2546
+ return f;
2547
+ }
2548
+ function __sbChunkBytes(chunk) {
2549
+ if (chunk instanceof Uint8Array) return chunk;
2550
+ if (chunk instanceof ArrayBuffer) return new Uint8Array(chunk);
2551
+ if (ArrayBuffer.isView(chunk)) return new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
2552
+ throw new TypeError("The provided value is not of type '(ArrayBuffer or ArrayBufferView)'");
2553
+ }
2554
+
2555
+ class CompressionStream {
2556
+ constructor(format) {
2557
+ const kind = __sbFormat(format, "compression");
2558
+ const w = new __sbBitWriter();
2559
+ let check = kind == "deflate" ? 1 : 0, size = 0;
2560
+ if (kind == "gzip") for (const b of [0x1f, 0x8b, 8, 0, 0, 0, 0, 0, 0, 0xff]) w.byte(b);
2561
+ else if (kind == "deflate") { w.byte(0x78); w.byte(0x9c); }
2562
+ const t = new TransformStream({
2563
+ transform(chunk, controller) {
2564
+ const bytes = __sbChunkBytes(chunk);
2565
+ if (!bytes.length) return;
2566
+ if (kind == "gzip") check = __sbCrc32(check, bytes);
2567
+ else if (kind == "deflate") check = __sbAdler32(check, bytes);
2568
+ size = (size + bytes.length) >>> 0;
2569
+ __sbDeflateBlock(w, bytes, false);
2570
+ const out = w.take();
2571
+ if (out.length) controller.enqueue(out);
2572
+ },
2573
+ flush(controller) {
2574
+ __sbDeflateBlock(w, new Uint8Array(0), true);
2575
+ w.align();
2576
+ if (kind == "gzip") for (const v of [check, size]) for (let k = 0; k < 4; k++) w.byte((v >>> (8 * k)) & 255);
2577
+ else if (kind == "deflate") for (let k = 3; k >= 0; k--) w.byte((check >>> (8 * k)) & 255);
2578
+ controller.enqueue(w.take());
2579
+ },
2580
+ });
2581
+ this._t = t;
2582
+ }
2583
+ get readable() { return this._t.readable; }
2584
+ get writable() { return this._t.writable; }
2585
+ }
2586
+
2587
+ class DecompressionStream {
2588
+ constructor(format) {
2589
+ const kind = __sbFormat(format, "decompression");
2590
+ const parts = [];
2591
+ let total = 0;
2592
+ const t = new TransformStream({
2593
+ transform(chunk) { const bytes = __sbChunkBytes(chunk); parts.push(bytes.slice()); total += bytes.length; },
2594
+ flush(controller) {
2595
+ const data = new Uint8Array(total);
2596
+ let at = 0;
2597
+ for (const p of parts) { data.set(p, at); at += p.length; }
2598
+ let start = 0;
2599
+ if (kind == "gzip") {
2600
+ if (data.length < 18 || data[0] != 0x1f || data[1] != 0x8b || data[2] != 8) throw new TypeError("Decompression failed: not gzip data");
2601
+ const flags = data[3];
2602
+ start = 10;
2603
+ if (flags & 4) start += 2 + (data[start] | (data[start + 1] << 8));
2604
+ if (flags & 8) { while (start < data.length && data[start]) start++; start++; }
2605
+ if (flags & 16) { while (start < data.length && data[start]) start++; start++; }
2606
+ if (flags & 2) start += 2;
2607
+ } else if (kind == "deflate") {
2608
+ if (data.length < 6 || (data[0] & 15) != 8 || ((data[0] << 8) | data[1]) % 31 != 0 || data[1] & 32) throw new TypeError("Decompression failed: not zlib data");
2609
+ start = 2;
2610
+ }
2611
+ const { out, end } = __sbInflate(data, start);
2612
+ if (kind == "gzip") {
2613
+ if (end + 8 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2614
+ const crc = (data[end] | (data[end + 1] << 8) | (data[end + 2] << 16) | (data[end + 3] << 24)) >>> 0;
2615
+ const isize = (data[end + 4] | (data[end + 5] << 8) | (data[end + 6] << 16) | (data[end + 7] << 24)) >>> 0;
2616
+ if (crc != __sbCrc32(0, out) || isize != out.length >>> 0) throw new TypeError("Decompression failed: gzip checksum mismatch");
2617
+ } else if (kind == "deflate") {
2618
+ if (end + 4 > data.length) throw new TypeError("Decompression failed: unexpected end of data");
2619
+ const adler = ((data[end] << 24) | (data[end + 1] << 16) | (data[end + 2] << 8) | data[end + 3]) >>> 0;
2620
+ if (adler != __sbAdler32(1, out)) throw new TypeError("Decompression failed: deflate checksum mismatch");
2621
+ }
2622
+ if (out.length) controller.enqueue(out);
2623
+ },
2624
+ });
2625
+ this._t = t;
2626
+ }
2627
+ get readable() { return this._t.readable; }
2628
+ get writable() { return this._t.writable; }
2629
+ }
2630
+ // Static installs, so app code sees the names when it is compiled apart (see streams-entry.js).
2631
+ if (globalThis.CompressionStream == null) globalThis.CompressionStream = CompressionStream;
2632
+ if (globalThis.DecompressionStream == null) globalThis.DecompressionStream = DecompressionStream;
2633
+ // END COMPRESSION STREAMS
2634
+
2635
+ // HTML REWRITER: Cloudflare's HTMLRewriter in JavaScript (workerd uses lol-html). It reads
2636
+ // the whole body, then tokenizes it once: elements, text and comments are matched against
2637
+ // the selectors given to on() and onDocument(), handlers run in document order (and may be
2638
+ // async), and the changes they ask for are written as the document is re-emitted.
2639
+ // Selectors: type, *, #id, .class, [attr], [attr=v], [attr~=v], [attr^=v], [attr$=v],
2640
+ // [attr*=v], [attr|=v], descendant and child combinators, and lists. Not :nth-child and
2641
+ // other pseudo-classes, which throw a TypeError on on(), as an unsupported selector does.
2642
+
2643
+ const __sbVoidTags = new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]);
2644
+ const __sbRawTextTags = new Set(["script", "style", "textarea", "title", "xmp", "iframe", "noembed", "noframes", "plaintext"]);
2645
+ function __sbEscapeText(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
2646
+ function __sbEscapeAttr(s) { return String(s).replace(/&/g, "&amp;").replace(/"/g, "&quot;"); }
2647
+ function __sbContent(content, options) { return options && options.html ? String(content) : __sbEscapeText(content); }
2648
+
2649
+ // One compound selector: [{ tag, id, classes, attrs }] joined by combinators.
2650
+ function __sbParseSelector(text) {
2651
+ const list = [];
2652
+ for (const part of String(text).split(",")) {
2653
+ const steps = [];
2654
+ let s = part.trim(), combinator = " ";
2655
+ if (!s) throw new TypeError("Invalid selector: " + text);
2656
+ while (s.length) {
2657
+ const step = { tag: null, id: null, classes: [], attrs: [], combinator };
2658
+ let m;
2659
+ let star = false;
2660
+ if ((m = /^(\*|[a-zA-Z][\w-]*)/.exec(s))) { if (m[1] != "*") step.tag = m[1].toLowerCase(); else star = true; s = s.slice(m[0].length); }
2661
+ for (;;) {
2662
+ if ((m = /^#([\w-]+)/.exec(s))) { step.id = m[1]; s = s.slice(m[0].length); continue; }
2663
+ if ((m = /^\.([\w-]+)/.exec(s))) { step.classes.push(m[1]); s = s.slice(m[0].length); continue; }
2664
+ if ((m = /^\[\s*([\w:-]+)\s*(?:([~^$*|]?=)\s*(?:"([^"]*)"|'([^']*)'|([^\]\s]+))\s*)?(i)?\s*\]/.exec(s))) {
2665
+ step.attrs.push({ name: m[1].toLowerCase(), op: m[2] || null, value: m[3] ?? m[4] ?? m[5] ?? "", ci: !!m[6] });
2666
+ s = s.slice(m[0].length);
2667
+ continue;
2668
+ }
2669
+ break;
2670
+ }
2671
+ if (s.length && s[0] == ":") throw new TypeError("Unsupported pseudo-class selector: " + text);
2672
+ if (step.tag == null && step.id == null && !step.classes.length && !step.attrs.length && !star)
2673
+ throw new TypeError("Invalid selector: " + text);
2674
+ steps.push(step);
2675
+ const ws = /^\s*/.exec(s)[0];
2676
+ s = s.slice(ws.length);
2677
+ if (!s.length) break;
2678
+ if (s[0] == ">") { combinator = ">"; s = s.slice(1).trimStart(); }
2679
+ else if (ws.length) combinator = " ";
2680
+ else throw new TypeError("Invalid selector: " + text);
2681
+ }
2682
+ list.push(steps);
2683
+ }
2684
+ return list;
2685
+ }
2686
+ function __sbStepMatches(step, el) {
2687
+ if (step.tag != null && step.tag != el.tag) return false;
2688
+ if (step.id != null && el.attr("id") !== step.id) return false;
2689
+ if (step.classes.length) {
2690
+ const have = (el.attr("class") || "").split(/\s+/);
2691
+ for (const c of step.classes) if (!have.includes(c)) return false;
2692
+ }
2693
+ for (const a of step.attrs) {
2694
+ let v = el.attr(a.name);
2695
+ if (v == null) return false;
2696
+ if (!a.op) continue;
2697
+ let want = a.value;
2698
+ if (a.ci) { v = v.toLowerCase(); want = want.toLowerCase(); }
2699
+ if (a.op == "=" && v !== want) return false;
2700
+ if (a.op == "~=" && !v.split(/\s+/).includes(want)) return false;
2701
+ if (a.op == "^=" && (!want || !v.startsWith(want))) return false;
2702
+ if (a.op == "$=" && (!want || !v.endsWith(want))) return false;
2703
+ if (a.op == "*=" && (!want || !v.includes(want))) return false;
2704
+ if (a.op == "|=" && v !== want && !v.startsWith(want + "-")) return false;
2705
+ }
2706
+ return true;
2707
+ }
2708
+ // stack: the open elements, outermost first; the last is the candidate.
2709
+ function __sbSelectorMatches(steps, stack) {
2710
+ const match = (si, ei) => {
2711
+ if (!__sbStepMatches(steps[si], stack[ei])) return false;
2712
+ if (si == 0) return true;
2713
+ if (steps[si].combinator == ">") return ei > 0 && match(si - 1, ei - 1);
2714
+ for (let k = ei - 1; k >= 0; k--) if (match(si - 1, k)) return true;
2715
+ return false;
2716
+ };
2717
+ return match(steps.length - 1, stack.length - 1);
2718
+ }
2719
+
2720
+ // Content to write around a node, from before/after/prepend/append/replace calls.
2721
+ class __sbEdits {
2722
+ constructor() { this.before = ""; this.after = ""; this.replaced = null; this.removed = false; }
2723
+ }
2724
+ class __sbElement {
2725
+ constructor(state) { this._s = state; }
2726
+ get tagName() { return this._s.tag; }
2727
+ set tagName(name) { this._s.tag = String(name).toLowerCase(); this._s.renamed = true; }
2728
+ get namespaceURI() { return "http://www.w3.org/1999/xhtml"; }
2729
+ get attributes() { return this._s.attrs.map((a) => [a[0], a[1]])[Symbol.iterator](); }
2730
+ get removed() { return this._s.edits.removed; }
2731
+ get selfClosing() { return this._s.selfClosing; }
2732
+ get canHaveContent() { return !__sbVoidTags.has(this._s.tag); }
2733
+ getAttribute(name) { return this._s.attr(String(name).toLowerCase()); }
2734
+ hasAttribute(name) { return this._s.attr(String(name).toLowerCase()) != null; }
2735
+ setAttribute(name, value) {
2736
+ const n = String(name).toLowerCase();
2737
+ const at = this._s.attrs.findIndex((a) => a[0] == n);
2738
+ if (at >= 0) this._s.attrs[at] = [n, String(value), null]; else this._s.attrs.push([n, String(value), null]);
2739
+ this._s.attrsChanged = true;
2740
+ return this;
2741
+ }
2742
+ removeAttribute(name) {
2743
+ const n = String(name).toLowerCase();
2744
+ this._s.attrs = this._s.attrs.filter((a) => a[0] != n);
2745
+ this._s.attrsChanged = true;
2746
+ return this;
2747
+ }
2748
+ before(content, options) { this._s.edits.before += __sbContent(content, options); return this; }
2749
+ after(content, options) { this._s.edits.after = __sbContent(content, options) + this._s.edits.after; return this; }
2750
+ prepend(content, options) { this._s.prepend = __sbContent(content, options) + this._s.prepend; return this; }
2751
+ append(content, options) { this._s.append += __sbContent(content, options); return this; }
2752
+ setInnerContent(content, options) { this._s.inner = __sbContent(content, options); this._s.prepend = ""; this._s.append = ""; return this; }
2753
+ replace(content, options) { this._s.edits.replaced = __sbContent(content, options); this._s.edits.removed = true; return this; }
2754
+ remove() { this._s.edits.removed = true; this._s.edits.replaced = ""; return this; }
2755
+ removeAndKeepContent() { this._s.keepContent = true; this._s.edits.removed = true; return this; }
2756
+ onEndTag(handler) { this._s.endHandlers.push(handler); }
2757
+ }
2758
+ class __sbEndTag {
2759
+ constructor(state) { this._s = state; }
2760
+ get name() { return this._s.tag; }
2761
+ set name(n) { this._s.tag = String(n).toLowerCase(); }
2762
+ before(content, options) { this._s.endBefore += __sbContent(content, options); return this; }
2763
+ after(content, options) { this._s.endAfter = __sbContent(content, options) + this._s.endAfter; return this; }
2764
+ remove() { this._s.endRemoved = true; return this; }
2765
+ }
2766
+ class __sbTextChunk {
2767
+ constructor(text, last) { this.text = text; this.lastInTextNode = last; this._e = new __sbEdits(); }
2768
+ get removed() { return this._e.removed; }
2769
+ before(content, options) { this._e.before += __sbContent(content, options); return this; }
2770
+ after(content, options) { this._e.after = __sbContent(content, options) + this._e.after; return this; }
2771
+ replace(content, options) { this._e.replaced = __sbContent(content, options); this._e.removed = true; return this; }
2772
+ remove() { this._e.removed = true; this._e.replaced = ""; return this; }
2773
+ }
2774
+ class __sbComment {
2775
+ constructor(text) { this.text = text; this._e = new __sbEdits(); }
2776
+ get removed() { return this._e.removed; }
2777
+ before(content, options) { this._e.before += __sbContent(content, options); return this; }
2778
+ after(content, options) { this._e.after = __sbContent(content, options) + this._e.after; return this; }
2779
+ replace(content, options) { this._e.replaced = __sbContent(content, options); this._e.removed = true; return this; }
2780
+ remove() { this._e.removed = true; this._e.replaced = ""; return this; }
2781
+ }
2782
+ class __sbDocEnd {
2783
+ constructor(out) { this._out = out; }
2784
+ append(content, options) { this._out.push(__sbContent(content, options)); return this; }
2785
+ }
2786
+
2787
+ class __sbRewriteRun {
2788
+ constructor(handlers, documentHandlers, html) {
2789
+ this.handlers = handlers;
2790
+ this.documentHandlers = documentHandlers;
2791
+ this.html = html;
2792
+ this.out = [];
2793
+ this.stack = [];
2794
+ // Inner content of an element replaced by setInnerContent, remove or replace is skipped:
2795
+ // the depth in the stack below which output is suppressed, or -1.
2796
+ this.skipBelow = -1;
2797
+ }
2798
+ emit(s) { if (this.skipBelow < 0) this.out.push(s); }
2799
+ async call(handler, method, arg) {
2800
+ const f = handler && handler[method];
2801
+ if (__sbIsFn(f)) await f.call(handler, arg);
2802
+ }
2803
+ // Text and comment handlers run for content anywhere inside a matched element.
2804
+ inside(steps) {
2805
+ for (let i = 0; i < this.stack.length; i++) if (steps.some((s) => __sbSelectorMatches(s, this.stack.slice(0, i + 1)))) return true;
2806
+ return false;
2807
+ }
2808
+ // A text node arrives as its text, then an empty chunk with lastInTextNode, as lol-html does.
2809
+ async text(raw) {
2810
+ if (!raw.length || this.skipBelow >= 0) return;
2811
+ for (const [text, last] of [[raw, false], ["", true]]) {
2812
+ const chunk = new __sbTextChunk(text, last);
2813
+ for (const h of this.documentHandlers) await this.call(h, "text", chunk);
2814
+ for (const [steps, h] of this.handlers) if (this.inside(steps)) await this.call(h, "text", chunk);
2815
+ this.emit(chunk._e.removed ? chunk._e.before + (chunk._e.replaced ?? "") + chunk._e.after : chunk._e.before + text + chunk._e.after);
2816
+ }
2817
+ }
2818
+ async comment(raw) {
2819
+ if (this.skipBelow >= 0) return;
2820
+ const c = new __sbComment(raw);
2821
+ for (const h of this.documentHandlers) await this.call(h, "comments", c);
2822
+ for (const [steps, h] of this.handlers) if (this.inside(steps)) await this.call(h, "comments", c);
2823
+ this.emit(c._e.removed ? c._e.before + (c._e.replaced ?? "") + c._e.after : c._e.before + "<!--" + c.text + "-->" + c._e.after);
2824
+ }
2825
+ async doctype(raw) {
2826
+ const m = /^<!doctype\s+([^\s>]+)?/i.exec(raw);
2827
+ const d = { name: m && m[1] ? m[1].toLowerCase() : null, publicId: null, systemId: null };
2828
+ for (const h of this.documentHandlers) await this.call(h, "doctype", d);
2829
+ this.emit(raw);
2830
+ }
2831
+ startTag(state) {
2832
+ let s = "<" + (state.renamed ? state.tag : state.rawName);
2833
+ for (const [n, v, raw] of state.attrs) s += raw != null ? " " + raw : " " + n + '="' + __sbEscapeAttr(v) + '"';
2834
+ return s + (state.selfClosing ? " />" : ">");
2835
+ }
2836
+ async element(tag, rawName, attrs, selfClosing, rawStart) {
2837
+ const state = {
2838
+ tag, rawName, attrs, selfClosing, rawStart,
2839
+ attr(name) { const a = this.attrs.find((x) => x[0] == name); return a ? a[1] : null; },
2840
+ edits: new __sbEdits(), prepend: "", append: "", inner: null, keepContent: false, renamed: false, attrsChanged: false,
2841
+ endHandlers: [], endBefore: "", endAfter: "", endRemoved: false,
2842
+ };
2843
+ const skipped = this.skipBelow >= 0;
2844
+ this.stack.push(state);
2845
+ if (!skipped) {
2846
+ const el = new __sbElement(state);
2847
+ for (const [steps, h] of this.handlers) if (steps.some((s) => __sbSelectorMatches(s, this.stack))) await this.call(h, "element", el);
2848
+ }
2849
+ const e = state.edits;
2850
+ this.emit(e.before);
2851
+ const unchanged = !state.renamed && !state.attrsChanged;
2852
+ if (!e.removed || state.keepContent) {
2853
+ if (!e.removed) this.emit(unchanged ? rawStart : this.startTag(state));
2854
+ } else this.emit(e.replaced ?? "");
2855
+ const isVoid = __sbVoidTags.has(tag) || selfClosing;
2856
+ if (isVoid) {
2857
+ this.stack.pop();
2858
+ this.emit(e.after);
2859
+ return null;
2860
+ }
2861
+ if (!e.removed || state.keepContent) this.emit(state.prepend);
2862
+ if (state.inner != null) { this.emit(state.inner); if (this.skipBelow < 0) this.skipBelow = this.stack.length; }
2863
+ else if (e.removed && !state.keepContent && this.skipBelow < 0) this.skipBelow = this.stack.length;
2864
+ return state;
2865
+ }
2866
+ async endTag(state, rawEnd) {
2867
+ if (this.skipBelow == this.stack.length) this.skipBelow = -1;
2868
+ this.stack.pop();
2869
+ const e = state.edits;
2870
+ if (!e.removed || state.keepContent) this.emit(state.append);
2871
+ if (!e.removed) {
2872
+ const end = new __sbEndTag(state);
2873
+ for (const handler of state.endHandlers) await handler(end);
2874
+ this.emit(state.endBefore);
2875
+ if (!state.endRemoved) this.emit(state.renamed || end.name != state.tag ? "</" + state.tag + ">" : rawEnd);
2876
+ this.emit(state.endAfter);
2877
+ }
2878
+ this.emit(e.after);
2879
+ }
2880
+ async run() {
2881
+ const html = this.html;
2882
+ let i = 0, textStart = 0;
2883
+ const flushText = async (to) => { if (to > textStart) await this.text(html.slice(textStart, to)); };
2884
+ const open = [];
2885
+ while (i < html.length) {
2886
+ const lt = html.indexOf("<", i);
2887
+ if (lt < 0) break;
2888
+ const next = html[lt + 1];
2889
+ if (html.startsWith("<!--", lt)) {
2890
+ await flushText(lt);
2891
+ const end = html.indexOf("-->", lt + 4);
2892
+ const stop = end < 0 ? html.length : end;
2893
+ await this.comment(html.slice(lt + 4, stop));
2894
+ i = textStart = end < 0 ? html.length : end + 3;
2895
+ continue;
2896
+ }
2897
+ if (next == "!" || next == "?") {
2898
+ await flushText(lt);
2899
+ const end = html.indexOf(">", lt);
2900
+ const raw = html.slice(lt, end < 0 ? html.length : end + 1);
2901
+ if (/^<!doctype/i.test(raw)) await this.doctype(raw); else this.emit(raw);
2902
+ i = textStart = lt + raw.length;
2903
+ continue;
2904
+ }
2905
+ if (next == "/" && /[a-zA-Z]/.test(html[lt + 2] || "")) {
2906
+ await flushText(lt);
2907
+ const end = html.indexOf(">", lt);
2908
+ const raw = html.slice(lt, end < 0 ? html.length : end + 1);
2909
+ const name = /^<\/([^\s/>]+)/.exec(raw)[1].toLowerCase();
2910
+ // Close up to the matching open element; unmatched end tags pass through.
2911
+ let at = -1;
2912
+ for (let k = open.length - 1; k >= 0; k--) if (open[k].rawName.toLowerCase() == name) { at = k; break; }
2913
+ if (at < 0) this.emit(raw);
2914
+ else {
2915
+ while (open.length > at + 1) { const inner = open.pop(); await this.endTag(inner, ""); }
2916
+ await this.endTag(open.pop(), raw);
2917
+ }
2918
+ i = textStart = lt + raw.length;
2919
+ continue;
2920
+ }
2921
+ if (!/[a-zA-Z]/.test(next || "")) { i = lt + 1; continue; }
2922
+ await flushText(lt);
2923
+ // A start tag: name, then attributes (quoted, unquoted or bare) up to > or />.
2924
+ let p = lt + 1;
2925
+ while (p < html.length && !/[\s/>]/.test(html[p])) p++;
2926
+ const rawName = html.slice(lt + 1, p);
2927
+ const tag = rawName.toLowerCase();
2928
+ const attrs = [];
2929
+ let selfClosing = false;
2930
+ for (;;) {
2931
+ while (p < html.length && /\s/.test(html[p])) p++;
2932
+ if (p >= html.length) break;
2933
+ if (html[p] == ">") { p++; break; }
2934
+ if (html[p] == "/" && html[p + 1] == ">") { selfClosing = true; p += 2; break; }
2935
+ if (html[p] == "/") { p++; continue; }
2936
+ const start = p;
2937
+ let q = p;
2938
+ while (q < html.length && !/[\s=/>]/.test(html[q])) q++;
2939
+ const name = html.slice(p, q).toLowerCase();
2940
+ p = q;
2941
+ while (p < html.length && /\s/.test(html[p])) p++;
2942
+ if (html[p] == "=") {
2943
+ p++;
2944
+ while (p < html.length && /\s/.test(html[p])) p++;
2945
+ let value;
2946
+ if (html[p] == '"' || html[p] == "'") {
2947
+ const quote = html[p], close = html.indexOf(quote, p + 1);
2948
+ value = html.slice(p + 1, close < 0 ? html.length : close);
2949
+ p = close < 0 ? html.length : close + 1;
2950
+ } else {
2951
+ let r = p;
2952
+ while (r < html.length && !/[\s>]/.test(html[r])) r++;
2953
+ value = html.slice(p, r);
2954
+ p = r;
2955
+ }
2956
+ attrs.push([name, value.replace(/&amp;/g, "&").replace(/&quot;/g, '"'), html.slice(start, p)]);
2957
+ } else attrs.push([name, "", html.slice(start, p)]);
2958
+ }
2959
+ const rawStart = html.slice(lt, p);
2960
+ const state = await this.element(tag, rawName, attrs, selfClosing && !__sbVoidTags.has(tag) ? false : selfClosing, rawStart);
2961
+ if (state) open.push(state);
2962
+ i = textStart = p;
2963
+ // Raw text elements hold their content verbatim up to their own end tag.
2964
+ if (state && __sbRawTextTags.has(tag)) {
2965
+ const close = html.toLowerCase().indexOf("</" + tag, p);
2966
+ const stop = close < 0 ? html.length : close;
2967
+ await this.text(html.slice(p, stop));
2968
+ i = textStart = stop;
2969
+ }
2970
+ }
2971
+ await flushText(html.length);
2972
+ // An element whose end tag never came has not ended: nothing appended to it is written
2973
+ // (lol-html does not imply end tags).
2974
+ open.length = 0;
2975
+ const tail = [];
2976
+ for (const h of this.documentHandlers) if (__sbIsFn(h.end)) await h.end(new __sbDocEnd(tail));
2977
+ return this.out.join("") + tail.join("");
2978
+ }
2979
+ }
2980
+
2981
+ class HTMLRewriter {
2982
+ constructor() { this._handlers = []; this._documentHandlers = []; }
2983
+ on(selector, handlers) { this._handlers.push([__sbParseSelector(selector), handlers]); return this; }
2984
+ onDocument(handlers) { this._documentHandlers.push(handlers); return this; }
2985
+ transform(response) {
2986
+ const handlers = this._handlers, documentHandlers = this._documentHandlers;
2987
+ const headers = new Headers(response.headers);
2988
+ headers.delete("content-length");
2989
+ const body = new ReadableStream({
2990
+ async start(controller) {
2991
+ try {
2992
+ const html = await response.text();
2993
+ const out = await new __sbRewriteRun(handlers, documentHandlers, html).run();
2994
+ controller.enqueue(new TextEncoder().encode(out));
2995
+ controller.close();
2996
+ } catch (error) {
2997
+ controller.error(error);
2998
+ }
2999
+ },
3000
+ });
3001
+ return new Response(body, { status: response.status, statusText: response.statusText, headers });
3002
+ }
3003
+ }
3004
+ // A static install, so app code sees the name when it is compiled apart (see streams-entry.js).
3005
+ if (globalThis.HTMLRewriter == null) globalThis.HTMLRewriter = HTMLRewriter;
3006
+ // END HTML REWRITER
3007
+
2308
3008
  // Porffor exposes native timers lexically. Libraries also access them through
2309
3009
  // globalThis, including noble hashes 2.4's host-task yields.
2310
3010
  if (globalThis.setTimeout == null) globalThis.setTimeout = setTimeout;
@@ -18,6 +18,31 @@ const handler = `export default {async fetch(request) {
18
18
  if (path === "/stream-strings") return new Response(new ReadableStream({ start(c) { c.enqueue("text"); c.close(); } }));
19
19
  if (path === "/form") { const f = new FormData(); f.append("a", "1"); f.append("f", new Blob(["xy"]), "n.txt"); return new Response(f); }
20
20
  const cases = [
21
+ // HTMLRewriter, against Bun's (lol-html, as in workerd): the W2 cases and more.
22
+ () => typeof HTMLRewriter,
23
+ () => new HTMLRewriter().on("h1", { element(e) { e.setInnerContent("x"); } }).transform(new Response("<h1>a</h1><p>b</p>")).text(),
24
+ () => new HTMLRewriter().on("a", { element(e) { e.setAttribute("href", "/n"); } }).transform(new Response('<a href="/o" data-x=1>l</a>')).text(),
25
+ () => new HTMLRewriter().on("script", { element(e) { e.remove(); } }).transform(new Response("<p>a</p><script>if (a < b) x()</script>")).text(),
26
+ async () => { let t = ""; await new HTMLRewriter().on("p", { text(c) { t += c.text + (c.lastInTextNode ? "|" : ""); } }).transform(new Response("<p>hi <b>there</b></p>")).text(); return t; },
27
+ () => new HTMLRewriter().on("body", { element(e) { e.append("<i>z</i>", { html: true }).prepend("<&>"); } }).transform(new Response("<body>a</body>")).text(),
28
+ () => new HTMLRewriter().on("div > span.k, #m", { element(e) { e.replace("R"); } }).on("p", { comments(c) { c.remove(); } }).transform(new Response('<div><span class="k">1</span><p><!--c-->2<span class="k">3</span></p></div><i id="m">4</i>')).text(),
29
+ () => new HTMLRewriter().on("li", { async element(e) { await null; e.removeAndKeepContent(); } }).onDocument({ end(d) { d.append("<!--e-->", { html: true }); } }).transform(new Response("<ul><li>a</li><li>b</li></ul>")).text(),
30
+ // CompressionStream and DecompressionStream: our output round-trips, and zlib's decodes.
31
+ async () => { const out = []; for (const f of ["gzip", "deflate", "deflate-raw"]) out.push(await new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hello ".repeat(200))); c.close(); } }).pipeThrough(new CompressionStream(f)).pipeThrough(new DecompressionStream(f))).text() === "hello ".repeat(200)); return out.join(","); },
32
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(Uint8Array.from(atob("H4sIAAAAAAACEysuKMovLUnKTyxRKB5uTACdCbks3AAAAA=="), (ch) => ch.charCodeAt(0))); c.close(); } }).pipeThrough(new DecompressionStream("gzip"))).text(),
33
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(Uint8Array.from(atob("eJxLqcxLzM1MVsgoTUvLTcxTSBnlj4ZHJv3SQ2peCgCxCfLI"), (ch) => ch.charCodeAt(0))); c.close(); } }).pipeThrough(new DecompressionStream("deflate"))).text(),
34
+ async () => { const s = new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hello hello")); c.close(); } }).pipeThrough(new CompressionStream("gzip")).pipeThrough(new DecompressionStream("gzip")).pipeThrough(new TextDecoderStream()); let t = ""; for (const r = s.getReader(); ;) { const { value, done } = await r.read(); if (done) break; t += value; } return t; },
35
+ // workerd knows gzip, deflate and deflate-raw; Bun (the reference) also brotli.
36
+ () => { try { new CompressionStream("brotli"); return "workerd:no throw"; } catch (e) { return "workerd:" + e.name; } },
37
+ // A ReadableStream body is read to its end by text(), arrayBuffer(), json() and blob().
38
+ async () => new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("hé")); c.enqueue(new Uint8Array([33])); c.close(); } })).text(),
39
+ async () => Array.from(new Uint8Array(await new Response(new ReadableStream({ start(c) { c.enqueue(new Uint8Array([1, 2])); c.close(); } })).arrayBuffer())).join(","),
40
+ async () => JSON.stringify(await new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode('{"a":[1]}')); c.close(); } })).json()),
41
+ async () => (await new Response(new ReadableStream({ start(c) { c.enqueue(new Uint8Array([120])); c.close(); } })).blob()).size,
42
+ async () => new Request("http://a.test", { method: "POST", body: new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode("req")); c.close(); } }), duplex: "half" }).text(),
43
+ // Chunks must be bytes: workerd throws a TypeError, where Bun (the reference below) reads a
44
+ // string chunk as text. "workerd:" marks the answer to expect from workerd instead.
45
+ async () => { try { await new Response(new ReadableStream({ start(c) { c.enqueue("s"); c.close(); } })).text(); return "workerd:no throw"; } catch (e) { return "workerd:" + e.name; } },
21
46
  () => new Request("http://a.test/a/../b?x=1#f").url,
22
47
  async () => { const r = new Request("http://a.test", { method: "POST", body: "x" }); await r.text(); try { await r.text(); return "no throw"; } catch (e) { return r.bodyUsed + ":" + e.name; } },
23
48
  () => new Request("http://a.test").redirect,
@@ -125,7 +150,7 @@ try {
125
150
  for (const run of reference) {
126
151
  try { want.push(String(await run())); } catch (e) { want.push("THROWS " + (e instanceof Error ? e.name : String(e))); }
127
152
  }
128
- assert.deepEqual(got, want);
153
+ assert.deepEqual(got, want.map((w) => (w.startsWith("workerd:") ? "workerd:TypeError" : w)));
129
154
  console.log(`Native bodies are sent as bytes and ${want.length} Fetch class cases match Bun`);
130
155
  } finally {
131
156
  if (server) {
@@ -17,6 +17,11 @@ const CASES = [
17
17
  `JSON.stringify({ n: { toJSON() { return 42; } } })`,
18
18
  `JSON.stringify({ nested: { at: new Date(0), list: [new Date(0)] } })`,
19
19
  `JSON.stringify({ plain: { x: 1 } })`,
20
+ // The reviver: children first, undefined deletes, `this` is the holder, arrays by index.
21
+ `JSON.stringify(JSON.parse('{"a":1,"b":{"c":2}}', (k, v) => (typeof v === "number" ? v * 10 : v)))`,
22
+ `JSON.stringify(JSON.parse('{"a":1,"drop":2,"l":[1,2,3]}', (k, v) => (k === "drop" ? undefined : v)))`,
23
+ `JSON.stringify(JSON.parse('[5,6]', function (k, v) { return k === "" ? v : k + ":" + v + ":" + Array.isArray(this); }))`,
24
+ `JSON.stringify(JSON.parse('{"x":1}', (k, v) => (k === "" ? "root:" + typeof v : v)))`,
20
25
  ];
21
26
  const handler = `export default {fetch() {
22
27
  const U = URL;