@sproutboat/toolchain 0.7.0 → 0.8.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.
package/src/pin.ts CHANGED
@@ -11,11 +11,11 @@
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-17";
15
- export const PORFFOR_COMMIT_FULL = "731f1b2a150d441ed22da3d14f8b1e0daaae8ee4";
14
+ export const PORFFOR_CHANNEL = "alpha-18";
15
+ export const PORFFOR_COMMIT_FULL = "deda4945a526d3974ff79e499e8c1dcef0873f3a";
16
16
  export const PORFFOR_COMMIT = PORFFOR_COMMIT_FULL.slice(0, 7);
17
17
  export const PORFFOR_ARCHIVE_SHA256 =
18
- "8eb65a3e77d58a23bfd2a1a60328672ad50e1ced7931f8d0ca3108e29d6cf846";
18
+ "4626ffba0d5bd417ba2c5b96506d40a152dbbeeaa8a698c904054bdef6bc6e64";
19
19
  export const PORFFOR_ARCHIVE_URL = `https://codeload.github.com/CanadaHonk/porffor/tar.gz/${PORFFOR_COMMIT_FULL}`;
20
20
 
21
21
  /** A compact identity string for a manifest / report (`alpha-17 (731f1b2)`). */
@@ -0,0 +1,34 @@
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
+ // sproutd B16: the doubling String.prototype.repeat matches Bun on edge cases and
8
+ // repeats 600 KB in well under a millisecond instead of ~2.4 ms per call.
9
+ const source = `const cases = [["ab", 0], ["ab", 1], ["ab", 2], ["ab", 3], ["abc", 7], ["x", 1000], ["é", 5], ["😀", 3], ["", 5], ["héllo", 33]];
10
+ const out = cases.map(([s, n]) => { const r = s.repeat(n); return [r.length, r.slice(0, 12), r.slice(-6)]; });
11
+ const t = performance.now(); let n = 0; for (let i = 0; i < 50; i++) n += "ab".repeat(300000).length + "é".repeat(200000).length;
12
+ console.log(JSON.stringify({ out, n }));
13
+ console.log(Math.round(performance.now() - t));`;
14
+
15
+ test("B16: String.prototype.repeat matches Bun and is linear in copies, not repetitions", async () => {
16
+ const dir = await mkdtemp(resolve(tmpdir(), "sb-repeat-"));
17
+ try {
18
+ const porffor = await ensurePorffor();
19
+ const input = resolve(dir, "case.js"), binary = resolve(dir, "case");
20
+ await writeFile(input, source);
21
+ const compile = Bun.spawnSync(["bun", resolve(porffor, "runtime/index.js"), "native", input, "-o", binary, "-s", "-O0"], { stdout: "pipe", stderr: "pipe" });
22
+ expect(compile.exitCode, new TextDecoder().decode(compile.stderr)).toBe(0);
23
+ const run = Bun.spawnSync([binary], { stdout: "pipe", stderr: "pipe", timeout: 10_000 });
24
+ expect(run.exitCode, new TextDecoder().decode(run.stderr)).toBe(0);
25
+ const reference = Bun.spawnSync(["bun", input], { stdout: "pipe" });
26
+ const lines = (bytes: Uint8Array) => new TextDecoder().decode(bytes).replaceAll("\x1b[33m", "").replaceAll("\x1b[0m", "").trim().split("\n");
27
+ const [got, ms] = lines(run.stdout);
28
+ expect(JSON.parse(got)).toEqual(JSON.parse(lines(reference.stdout)[0]));
29
+ // Per-repetition copying took ~180 ms here at -O0; doubling takes a few.
30
+ expect(Number(ms)).toBeLessThan(60);
31
+ } finally {
32
+ await rm(dir, { recursive: true, force: true });
33
+ }
34
+ }, 60_000);
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Streamed responses for standalone sprouts (sproutd B19), patched into Porffor's
3
+ * uWebSockets shim (compiler/uwebsockets.js) by patchStreamingResponses.
4
+ *
5
+ * The prelude opens a stream (sb_stream_open), marks its Response with the internal
6
+ * x-sb-stream: <id> header and returns at once with an empty body. The writer sees the
7
+ * marker, sends the status and headers, and attaches the uWS response to the stream
8
+ * instead of ending it. The prelude then pumps the ReadableStream: each chunk goes out with
9
+ * sb_stream_write (chunked transfer encoding), and sb_stream_end finishes the response.
10
+ * Chunks that arrive before the writer attaches (they can, while microtasks drain) are kept
11
+ * and flushed on attach. A client that goes away marks the stream aborted, and the next
12
+ * write returns -1 so the prelude cancels the source.
13
+ *
14
+ * Hosts without this shim (sproutd's workers) get the weak STREAM_STUBS: sb_stream_supported
15
+ * returns 0 and the prelude keeps buffering the body.
16
+ */
17
+
18
+ /** Inserted before write_response_value. C++ inside a JS template literal: no backticks, no ${. */
19
+ export const STREAM_REGISTRY = String.raw`// sproutboat B19: streamed responses
20
+ struct SbStream {
21
+ uWS::HttpResponse<false>* res = nullptr;
22
+ std::string pending;
23
+ bool attached = false, ended = false, aborted = false;
24
+ };
25
+ static std::unordered_map<u32, SbStream> sb_streams;
26
+ static u32 sb_stream_next = 1;
27
+
28
+ extern "C" int sb_stream_supported(void) { return 1; }
29
+
30
+ extern "C" u32 sb_stream_open(void) {
31
+ const u32 id = sb_stream_next++;
32
+ sb_streams[id] = SbStream();
33
+ return id;
34
+ }
35
+
36
+ extern "C" int sb_stream_write(u32 id, const char* data, size_t len) {
37
+ auto it = sb_streams.find(id);
38
+ if (it == sb_streams.end() || it->second.aborted) return -1;
39
+ SbStream& s = it->second;
40
+ if (!s.attached) { s.pending.append(data, len); return 0; }
41
+ auto* res = s.res;
42
+ res->cork([res, data, len]() { res->write(std::string_view(data, len)); });
43
+ return 0;
44
+ }
45
+
46
+ extern "C" void sb_stream_end(u32 id) {
47
+ auto it = sb_streams.find(id);
48
+ if (it == sb_streams.end()) return;
49
+ SbStream& s = it->second;
50
+ if (!s.attached && !s.aborted) { s.ended = true; return; }
51
+ if (!s.aborted) {
52
+ auto* res = s.res;
53
+ res->cork([res]() { res->end(); });
54
+ }
55
+ sb_streams.erase(it);
56
+ }
57
+
58
+ static void sb_stream_attach(u32 id, uWS::HttpResponse<false>* res) {
59
+ auto it = sb_streams.find(id);
60
+ if (it == sb_streams.end()) { res->end(); return; }
61
+ SbStream& s = it->second;
62
+ s.res = res;
63
+ s.attached = true;
64
+ res->onAborted([id]() {
65
+ auto found = sb_streams.find(id);
66
+ if (found == sb_streams.end()) return;
67
+ found->second.aborted = true;
68
+ found->second.res = nullptr;
69
+ });
70
+ if (!s.pending.empty()) { res->write(std::string_view(s.pending)); s.pending.clear(); }
71
+ if (s.ended) { res->end(); sb_streams.erase(it); }
72
+ }
73
+
74
+ `;
75
+
76
+ export const STREAM_FORBIDDEN_ANCHOR =
77
+ ' key == "x-sb-raw-body"; // sproutboat #176: internal signal, never sent\n';
78
+ export const STREAM_FORBIDDEN_INJECT =
79
+ ' key == "x-sb-raw-body" || // sproutboat #176: internal signal, never sent\n' +
80
+ ' key == "x-sb-stream"; // sproutboat B19: internal signal, never sent\n';
81
+
82
+ export const STREAM_SCAN_ANCHOR =
83
+ ' if (std::string_view(scan_buf, scan_bytes) == "x-sb-raw-body") raw_body = true;\n';
84
+ export const STREAM_SCAN_INJECT =
85
+ STREAM_SCAN_ANCHOR +
86
+ ' if (std::string_view(scan_buf, scan_bytes) == "x-sb-stream") {\n' +
87
+ " const struct jsval stream_value = unpack_jsval(*((u64*)(porf_mem + scan_entries + i * 16 + 8)));\n" +
88
+ " const char* id_buf = nullptr;\n" +
89
+ " size_t id_len = 0;\n" +
90
+ " char* id_owned = nullptr;\n" +
91
+ " porf_native_fetch_read_value(stream_value, &id_buf, &id_len, &id_owned);\n" +
92
+ " stream_id = (u32)strtoul(std::string(id_buf, id_len).c_str(), nullptr, 10);\n" +
93
+ " if (id_owned) free(id_owned);\n" +
94
+ " }\n";
95
+
96
+ /** The scan loop above stops at the raw-body header; the stream id needs its own flag. */
97
+ export const STREAM_FLAG_ANCHOR = " bool raw_body = false;\n";
98
+ export const STREAM_FLAG_INJECT = " bool raw_body = false;\n u32 stream_id = 0; // sproutboat B19\n";
99
+
100
+ export const STREAM_CORK_ANCHOR = " res->cork([res, status, headers_entries_ptr, body_buf, body_len]() {\n";
101
+ export const STREAM_CORK_INJECT = " res->cork([res, status, headers_entries_ptr, body_buf, body_len, stream_id]() {\n";
102
+
103
+ export const STREAM_END_ANCHOR = " res->end(std::string_view(body_buf, body_len));\n";
104
+ export const STREAM_END_INJECT =
105
+ " if (stream_id) sb_stream_attach(stream_id, res); // sproutboat B19: the prelude pumps the body\n" +
106
+ " else res->end(std::string_view(body_buf, body_len));\n";
107
+
108
+ export const STREAM_REGISTRY_ANCHOR =
109
+ "static void write_response_value(uWS::HttpResponse<false>* res, struct jsval response, bool* aborted) {\n";
110
+
111
+ /**
112
+ * Weak fallbacks for the runtime C that render.js emits (compiler/render.js), inserted before
113
+ * porf_native_fetch_read_raw_bytes. The shim's strong definitions above replace them in a
114
+ * standalone server; everywhere else (sproutd's workers) sb_stream_supported() returns 0 and
115
+ * the prelude buffers. Weak definitions, not weak undefined references: Mach-O will not link
116
+ * an undefined weak reference without -U, while a weak definition links on ELF and Mach-O.
117
+ */
118
+ export const STREAM_STUBS =
119
+ "// sproutboat B19: streaming fallbacks, overridden by the native server's shim\n" +
120
+ "__attribute__((weak)) int sb_stream_supported(void) { return 0; }\n" +
121
+ "__attribute__((weak)) unsigned int sb_stream_open(void) { return 0; }\n" +
122
+ "__attribute__((weak)) int sb_stream_write(unsigned int id, const char* data, size_t len) { (void)id; (void)data; (void)len; return -1; }\n" +
123
+ "__attribute__((weak)) void sb_stream_end(unsigned int id) { (void)id; }\n\n";
124
+
125
+ export const STREAM_STUBS_ANCHOR = "// sproutboat #176: zero-copy passthrough";
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The WHATWG UTF-8 TextDecoder that replaces Porffor's (see patchTextDecoder). Porffor's
3
+ * decoded three bytes at most (astral characters came out as garbage), never checked
4
+ * continuation bytes, and ignored fatal, ignoreBOM and { stream }. Found by sproutd's W2
5
+ * conformance suite against workerd.
6
+ */
7
+ export const TEXT_DECODER_SOURCE = String.raw`class TextDecoder {
8
+ // sproutboat: WHATWG UTF-8 decoder
9
+ constructor(label = 'utf-8', options = undefined) {
10
+ const name = String(label).trim().toLowerCase();
11
+ if (name != 'utf-8' && name != 'utf8' && name != 'unicode-1-1-utf-8') throw new RangeError('TextDecoder: unsupported encoding ' + label);
12
+ this.encoding = 'utf-8';
13
+ this.fatal = !!(options && options.fatal);
14
+ this.ignoreBOM = !!(options && options.ignoreBOM);
15
+ this._pending = [];
16
+ this._started = false;
17
+ }
18
+
19
+ decode(input = undefined, options = undefined) {
20
+ const stream = !!(options && options.stream);
21
+ let view;
22
+ if (input == null) view = new Uint8Array(0);
23
+ else if (input instanceof ArrayBuffer) view = new Uint8Array(input);
24
+ else if (ArrayBuffer.isView(input)) view = new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
25
+ else throw new TypeError('TextDecoder.decode: input must be an ArrayBuffer or a view of one');
26
+ let bytes = view;
27
+ if (this._pending.length > 0) {
28
+ bytes = new Uint8Array(this._pending.length + view.length);
29
+ for (let i = 0; i < this._pending.length; i++) bytes[i] = this._pending[i];
30
+ bytes.set(view, this._pending.length);
31
+ this._pending = [];
32
+ }
33
+ const n = bytes.length;
34
+ let out = '';
35
+ let i = 0;
36
+ while (i < n) {
37
+ const a = bytes[i];
38
+ let cp = -1;
39
+ let next = i + 1;
40
+ if (a < 0x80) cp = a;
41
+ else {
42
+ let need = 0, lower = 0x80, upper = 0xbf;
43
+ if (a >= 0xc2 && a <= 0xdf) { need = 1; cp = a & 0x1f; }
44
+ else if (a >= 0xe0 && a <= 0xef) { need = 2; cp = a & 0x0f; if (a == 0xe0) lower = 0xa0; if (a == 0xed) upper = 0x9f; }
45
+ else if (a >= 0xf0 && a <= 0xf4) { need = 3; cp = a & 0x07; if (a == 0xf0) lower = 0x90; if (a == 0xf4) upper = 0x8f; }
46
+ if (need == 0) cp = -1;
47
+ else {
48
+ let k = 0;
49
+ while (k < need) {
50
+ if (next >= n) break;
51
+ const c = bytes[next];
52
+ if (c < lower || c > upper) break;
53
+ lower = 0x80; upper = 0xbf;
54
+ cp = (cp << 6) | (c & 0x3f);
55
+ next++; k++;
56
+ }
57
+ if (k < need) {
58
+ if (next >= n && stream) {
59
+ // An incomplete sequence at the end of a chunk waits for the next one.
60
+ for (let j = i; j < n; j++) this._pending.push(bytes[j]);
61
+ break;
62
+ }
63
+ cp = -1;
64
+ }
65
+ }
66
+ }
67
+ if (cp < 0) {
68
+ if (this.fatal) {
69
+ this._pending = [];
70
+ this._started = false;
71
+ throw new TypeError('The encoded data was not valid utf-8');
72
+ }
73
+ cp = 0xfffd;
74
+ }
75
+ if (!this._started) {
76
+ this._started = true;
77
+ if (cp == 0xfeff && !this.ignoreBOM) { i = next; continue; }
78
+ }
79
+ if (cp > 0xffff) {
80
+ cp -= 0x10000;
81
+ out += String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff));
82
+ } else out += String.fromCharCode(cp);
83
+ i = next;
84
+ }
85
+ if (!stream) {
86
+ this._pending = [];
87
+ this._started = false;
88
+ }
89
+ return out;
90
+ }
91
+ }
92
+ `;
@@ -0,0 +1,342 @@
1
+ /**
2
+ * The URL class patchURLClass puts into Porffor's runtime/fetch-globals.js. Porffor's own
3
+ * exposes href, origin, pathname and search only, with no setters but pathname, no base
4
+ * argument and no hash, so Workers code that rewrites a URL (`url.hostname = ...`,
5
+ * `url.searchParams.set(...)`, `new URL(path, request.url)`) failed or went stale.
6
+ * Plain JavaScript compiled by Porffor; checked against WHATWG URL behaviour (Bun's) by
7
+ * packages/runtime/test/native-url.ts. searchParams is the prelude's URLSearchParams,
8
+ * which writes back through `_url` and is re-read through `_reset`.
9
+ */
10
+ export const URL_CLASS_SOURCE = String.raw`class URL {
11
+ // sproutboat: WHATWG-style URL. Porffor's had href/origin/pathname/search only, no
12
+ // setters but pathname, no base argument, no hash.
13
+ constructor(input, base = undefined) {
14
+ const raw = input != null && typeof input == 'object' && input.href != null ? String(input.href) : String(input);
15
+ if (this._parse(raw)) return;
16
+ if (base == null) throw new TypeError('Invalid URL: ' + raw);
17
+ this._resolve(raw.trim(), base instanceof URL ? base : new URL(base));
18
+ }
19
+
20
+ static _defaultPort(protocol) {
21
+ if (protocol == 'http:' || protocol == 'ws:') return '80';
22
+ if (protocol == 'https:' || protocol == 'wss:') return '443';
23
+ if (protocol == 'ftp:') return '21';
24
+ return '';
25
+ }
26
+
27
+ static canParse(input, base = undefined) {
28
+ try {
29
+ new URL(input, base);
30
+ return true;
31
+ } catch (e) {
32
+ return false;
33
+ }
34
+ }
35
+
36
+ _clear() {
37
+ this._username = '';
38
+ this._password = '';
39
+ this._hostname = '';
40
+ this._port = '';
41
+ this._pathname = '';
42
+ this._query = null;
43
+ this._fragment = null;
44
+ this._params = null;
45
+ }
46
+
47
+ // Splits off the fragment and the query (null when absent, '' when empty); returns the rest.
48
+ _splitTail(rest) {
49
+ const hashAt = rest.indexOf('#');
50
+ this._fragment = hashAt == -1 ? null : URL._escape(rest.slice(hashAt + 1));
51
+ if (hashAt != -1) rest = rest.slice(0, hashAt);
52
+ const queryAt = rest.indexOf('?');
53
+ this._query = queryAt == -1 ? null : URL._escape(rest.slice(queryAt + 1), this._isSpecial());
54
+ if (queryAt != -1) rest = rest.slice(0, queryAt);
55
+ return rest;
56
+ }
57
+
58
+ // false when raw has no scheme (a relative reference).
59
+ _parse(raw) {
60
+ raw = raw.trim();
61
+ const first = raw.charCodeAt(0);
62
+ if (!((first >= 65 && first <= 90) || (first >= 97 && first <= 122))) return false;
63
+ let i = 1;
64
+ while (i < raw.length) {
65
+ const c = raw.charCodeAt(i);
66
+ if ((c >= 65 && c <= 90) || (c >= 97 && c <= 122) || (c >= 48 && c <= 57) || c == 43 || c == 45 || c == 46) i++;
67
+ else break;
68
+ }
69
+ if (raw.charCodeAt(i) != 58) return false;
70
+ this._clear();
71
+ this._protocol = raw.slice(0, i + 1).toLowerCase();
72
+ let rest = this._splitTail(raw.slice(i + 1));
73
+ if (rest.startsWith('//')) {
74
+ rest = rest.slice(2);
75
+ let end = rest.indexOf('/');
76
+ if (end == -1) end = rest.length;
77
+ this._setAuthority(rest.slice(0, end));
78
+ if (this._hostname == '' && this._protocol != 'file:') throw new TypeError('Invalid URL: ' + raw);
79
+ this._pathname = URL._path(rest.slice(end) || '/');
80
+ } else if (this._isSpecial()) {
81
+ throw new TypeError('Invalid URL: ' + raw);
82
+ } else {
83
+ this._pathname = rest;
84
+ }
85
+ return true;
86
+ }
87
+
88
+ _isSpecial() {
89
+ const p = this._protocol;
90
+ return p == 'http:' || p == 'https:' || p == 'ws:' || p == 'wss:' || p == 'ftp:' || p == 'file:';
91
+ }
92
+
93
+ _resolve(raw, base) {
94
+ if (raw.startsWith('//')) {
95
+ if (!this._parse(base._protocol + raw)) throw new TypeError('Invalid URL: ' + raw);
96
+ return;
97
+ }
98
+ this._clear();
99
+ this._protocol = base._protocol;
100
+ this._username = base._username;
101
+ this._password = base._password;
102
+ this._hostname = base._hostname;
103
+ this._port = base._port;
104
+ const path = this._splitTail(raw);
105
+ if (path == '') {
106
+ this._pathname = base._pathname;
107
+ if (raw.indexOf('?') == -1) this._query = base._query;
108
+ } else if (path.startsWith('/')) {
109
+ this._pathname = URL._path(path);
110
+ } else {
111
+ const dir = base._pathname.slice(0, base._pathname.lastIndexOf('/') + 1);
112
+ this._pathname = URL._path(dir + path);
113
+ }
114
+ }
115
+
116
+ _setAuthority(auth) {
117
+ const at = auth.lastIndexOf('@');
118
+ if (at != -1) {
119
+ const info = auth.slice(0, at);
120
+ const colon = info.indexOf(':');
121
+ this._username = colon == -1 ? info : info.slice(0, colon);
122
+ this._password = colon == -1 ? '' : info.slice(colon + 1);
123
+ auth = auth.slice(at + 1);
124
+ }
125
+ this._setHost(auth);
126
+ }
127
+
128
+ _setHost(host) {
129
+ const close = host.charCodeAt(0) == 91 ? host.indexOf(']') : -1;
130
+ const colon = host.indexOf(':', close == -1 ? 0 : close);
131
+ this._hostname = URL._toASCII((colon == -1 ? host : host.slice(0, colon)).toLowerCase());
132
+ if (colon != -1) this.port = host.slice(colon + 1);
133
+ }
134
+
135
+ // An internationalized host in its ASCII form: each non-ASCII label as xn-- plus its
136
+ // Punycode (RFC 3492), as fetch and DNS need it. Lowercased first; no full UTS 46 mapping.
137
+ static _toASCII(host) {
138
+ if (host.charCodeAt(0) == 91) return host;
139
+ let ascii = true;
140
+ for (let i = 0; i < host.length; i++) if (host.charCodeAt(i) > 127) { ascii = false; break; }
141
+ if (ascii) return host;
142
+ const labels = host.split('.');
143
+ for (let i = 0; i < labels.length; i++) {
144
+ const label = labels[i];
145
+ for (let j = 0; j < label.length; j++) if (label.charCodeAt(j) > 127) { labels[i] = 'xn--' + URL._punycode(label); break; }
146
+ }
147
+ return labels.join('.');
148
+ }
149
+
150
+ static _punycode(input) {
151
+ const base = 36, tMin = 1, tMax = 26, skew = 38, damp = 700;
152
+ const points = [];
153
+ for (let i = 0; i < input.length; i++) {
154
+ const c = input.codePointAt(i);
155
+ points.push(c);
156
+ if (c > 0xffff) i++;
157
+ }
158
+ let out = '';
159
+ for (let i = 0; i < points.length; i++) if (points[i] < 0x80) out += String.fromCharCode(points[i]);
160
+ const basic = out.length;
161
+ let handled = basic;
162
+ if (basic > 0) out += '-';
163
+ const digit = (d) => String.fromCharCode(d < 26 ? d + 97 : d + 22);
164
+ const adapt = (delta, count, first) => {
165
+ delta = first ? Math.floor(delta / damp) : delta >> 1;
166
+ delta += Math.floor(delta / count);
167
+ let k = 0;
168
+ while (delta > ((base - tMin) * tMax) >> 1) { delta = Math.floor(delta / (base - tMin)); k += base; }
169
+ return k + Math.floor(((base - tMin + 1) * delta) / (delta + skew));
170
+ };
171
+ let n = 128, delta = 0, bias = 72;
172
+ while (handled < points.length) {
173
+ let m = 0x110000;
174
+ for (let i = 0; i < points.length; i++) if (points[i] >= n && points[i] < m) m = points[i];
175
+ delta += (m - n) * (handled + 1);
176
+ n = m;
177
+ for (let i = 0; i < points.length; i++) {
178
+ const c = points[i];
179
+ if (c < n) delta++;
180
+ if (c == n) {
181
+ let q = delta;
182
+ for (let k = base; ; k += base) {
183
+ const t = k <= bias ? tMin : k >= bias + tMax ? tMax : k - bias;
184
+ if (q < t) break;
185
+ out += digit(t + ((q - t) % (base - t)));
186
+ q = Math.floor((q - t) / (base - t));
187
+ }
188
+ out += digit(q);
189
+ bias = adapt(delta, handled + 1, handled == basic);
190
+ delta = 0;
191
+ handled++;
192
+ }
193
+ }
194
+ delta++;
195
+ n++;
196
+ }
197
+ return out;
198
+ }
199
+
200
+ // Dot segments resolved, spaces escaped.
201
+ static _path(p) {
202
+ p = URL._escape(p, false, true);
203
+ if (p.indexOf('.') == -1 && p.indexOf('%2') == -1) return p;
204
+ const parts = p.split('/');
205
+ const out = [];
206
+ for (let i = 1; i < parts.length; i++) {
207
+ const s = parts[i];
208
+ const last = i == parts.length - 1;
209
+ const l = s.toLowerCase();
210
+ if (l == '.' || l == '%2e') {
211
+ if (last) out.push('');
212
+ } else if (l == '..' || l == '.%2e' || l == '%2e.' || l == '%2e%2e') {
213
+ if (out.length > 0) out.pop();
214
+ if (last) out.push('');
215
+ } else {
216
+ out.push(s);
217
+ }
218
+ }
219
+ return '/' + out.join('/');
220
+ }
221
+
222
+ // The WHATWG percent-encode sets that matter here: C0 controls, space, " < > and backtick, non-ASCII;
223
+ // plus ' in a query, { } in a path. Existing %XX sequences stay as they are.
224
+ static _escape(s, query = false, path = false) {
225
+ let out = '';
226
+ for (let i = 0; i < s.length; i++) {
227
+ const c = s.charCodeAt(i);
228
+ if (c <= 32 || c == 34 || c == 60 || c == 62 || c == 96 || c == 127 || (query && c == 39) || (path && (c == 123 || c == 125))) {
229
+ out += '%' + (c < 16 ? '0' : '') + c.toString(16).toUpperCase();
230
+ } else if (c > 127) {
231
+ const pair = c >= 0xd800 && c <= 0xdbff && i + 1 < s.length;
232
+ out += encodeURIComponent(pair ? s.slice(i, i + 2) : s[i]);
233
+ if (pair) i++;
234
+ } else {
235
+ out += s[i];
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+
241
+ get protocol() { return this._protocol; }
242
+ set protocol(value) {
243
+ let v = String(value);
244
+ const colon = v.indexOf(':');
245
+ if (colon != -1) v = v.slice(0, colon);
246
+ if (v == '') return;
247
+ this._protocol = v.toLowerCase() + ':';
248
+ if (this._port == URL._defaultPort(this._protocol)) this._port = '';
249
+ }
250
+
251
+ get username() { return this._username; }
252
+ set username(value) { if (this._hostname != '') this._username = URL._escape(String(value)); }
253
+ get password() { return this._password; }
254
+ set password(value) { if (this._hostname != '') this._password = URL._escape(String(value)); }
255
+
256
+ get hostname() { return this._hostname; }
257
+ set hostname(value) {
258
+ const v = String(value);
259
+ if (v != '' && this._hostname != '') this._hostname = URL._toASCII(v.toLowerCase());
260
+ }
261
+
262
+ get port() { return this._port; }
263
+ set port(value) {
264
+ const v = String(value);
265
+ let digits = '';
266
+ for (let i = 0; i < v.length; i++) {
267
+ const c = v.charCodeAt(i);
268
+ if (c < 48 || c > 57) break;
269
+ digits += v[i];
270
+ }
271
+ if (digits == '' && v != '') return;
272
+ if (digits != '') digits = String(Number(digits));
273
+ if (Number(digits) > 65535) return;
274
+ this._port = digits == URL._defaultPort(this._protocol) ? '' : digits;
275
+ }
276
+
277
+ get host() { return this._port == '' ? this._hostname : this._hostname + ':' + this._port; }
278
+ set host(value) {
279
+ const v = String(value);
280
+ if (v != '' && this._hostname != '') this._setHost(v);
281
+ }
282
+
283
+ get origin() {
284
+ return this._isSpecial() && this._protocol != 'file:' ? this._protocol + '//' + this.host : 'null';
285
+ }
286
+
287
+ get pathname() { return this._pathname; }
288
+ set pathname(value) {
289
+ if (this._hostname == '' && !this._isSpecial()) return;
290
+ const v = String(value);
291
+ this._pathname = URL._path(v.startsWith('/') ? v : '/' + v);
292
+ }
293
+
294
+ get search() { return this._query == null || this._query == '' ? '' : '?' + this._query; }
295
+ set search(value) {
296
+ let v = String(value);
297
+ if (v.startsWith('?')) v = v.slice(1);
298
+ this._query = v == '' ? null : URL._escape(v, this._isSpecial());
299
+ if (this._params != null) this._params._reset(this.search);
300
+ }
301
+
302
+ get hash() { return this._fragment == null || this._fragment == '' ? '' : '#' + this._fragment; }
303
+ set hash(value) {
304
+ let v = String(value);
305
+ if (v.startsWith('#')) v = v.slice(1);
306
+ this._fragment = v == '' ? null : URL._escape(v);
307
+ }
308
+
309
+ get searchParams() {
310
+ if (this._params == null) {
311
+ this._params = new globalThis.URLSearchParams(this.search);
312
+ this._params._url = this;
313
+ }
314
+ return this._params;
315
+ }
316
+
317
+ get href() {
318
+ let out = this._protocol;
319
+ if (this._hostname != '' || this._protocol == 'file:') {
320
+ out += '//';
321
+ if (this._username != '' || this._password != '') {
322
+ out += this._username;
323
+ if (this._password != '') out += ':' + this._password;
324
+ out += '@';
325
+ }
326
+ out += this.host;
327
+ }
328
+ out += this._pathname;
329
+ if (this._query != null) out += '?' + this._query;
330
+ if (this._fragment != null) out += '#' + this._fragment;
331
+ return out;
332
+ }
333
+ set href(value) {
334
+ const v = String(value);
335
+ if (!this._parse(v)) throw new TypeError('Invalid URL: ' + v);
336
+ }
337
+
338
+ toString() { return this.href; }
339
+ toJSON() { return this.href; }
340
+ }
341
+
342
+ `;
Binary file
Binary file