@sproutboat/toolchain 0.8.0 → 0.9.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 +16 -0
- package/package.json +1 -1
- package/src/acquire.test.ts +2 -0
- package/src/fetch-classes.ts +58 -8
- package/src/fingerprint.test.ts +16 -4
- package/src/fingerprint.ts +35 -39
- package/src/patch.ts +50 -0
- package/src/unicode-native.test.ts +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @sproutboat/toolchain
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 2541458: More of the Web platform, from sproutd's W2 conformance suite against workerd:
|
|
8
|
+
|
|
9
|
+
- `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`.
|
|
10
|
+
- 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.
|
|
11
|
+
- `JSON.parse` applies its reviver: children first, `undefined` deletes the property, and `this` is the holder.
|
|
12
|
+
|
|
13
|
+
## 0.8.1
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 742e61c: A CLI compiled with `bun build --compile` builds again. 0.8.0 embedded the patch sources with text imports to fingerprint them, but Bun bundles a text import of `patch.ts` as the module `patch.ts` is everywhere else, so compiling failed with `No matching export ... for import "default"`. The fingerprint now hashes the sources when they are on disk, and falls back to the fingerprint they were released with (`RELEASED_FINGERPRINT`, kept current by a test). The value is unchanged, so existing caches stay valid.
|
|
18
|
+
|
|
3
19
|
## 0.8.0
|
|
4
20
|
|
|
5
21
|
### Minor Changes
|
package/package.json
CHANGED
package/src/acquire.test.ts
CHANGED
|
@@ -151,6 +151,8 @@ async function fixture(): Promise<{ archive: string; sha256: string }> {
|
|
|
151
151
|
"compiler/builtins/json.ts":
|
|
152
152
|
"let __Porffor_json_bufLimit: i32 = 0;\n depth += 1;\n depth -= 1;\n depth += 1;\n depth -= 1;\n let out: i32 = __Porffor_json_serialize(buffer, value, 0, space);\n" +
|
|
153
153
|
"// sb_json_utf16_v1\n" +
|
|
154
|
+
// patchJsonReviver's anchor.
|
|
155
|
+
"export const __JSON_parse = (_: bytestring) => {\n};\n" +
|
|
154
156
|
"export const __Porffor_json_serialize = (_buffer: i32, value: any): i32 => {\n" +
|
|
155
157
|
" for (const x of (value as any[])) {\n }\n" +
|
|
156
158
|
" const val: any = value[key];\n" +
|
package/src/fetch-classes.ts
CHANGED
|
@@ -74,6 +74,40 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
|
|
|
74
74
|
return (Porffor.type(body) | 0b10000000) == Porffor.TYPES.bytestring ? body : String(body);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// A ReadableStream body is read to its end, and its chunks must be bytes, as in Fetch and
|
|
78
|
+
// workerd. Readers of such a body answer with a promise; other bodies keep answering at
|
|
79
|
+
// once, since the prelude reads them synchronously. The prelude defines ReadableStream on
|
|
80
|
+
// globalThis after this file is compiled.
|
|
81
|
+
static _isStream(body) {
|
|
82
|
+
const Stream = globalThis.ReadableStream;
|
|
83
|
+
return Stream != null && body instanceof Stream;
|
|
84
|
+
}
|
|
85
|
+
static async _streamBytes(stream) {
|
|
86
|
+
const reader = stream.getReader();
|
|
87
|
+
const chunks = [];
|
|
88
|
+
let size = 0;
|
|
89
|
+
for (;;) {
|
|
90
|
+
const step = await reader.read();
|
|
91
|
+
if (step.done) break;
|
|
92
|
+
const value = step.value;
|
|
93
|
+
if (!(value instanceof Uint8Array)) throw new TypeError('This ReadableStream did not return bytes.');
|
|
94
|
+
chunks.push(value);
|
|
95
|
+
size += value.length;
|
|
96
|
+
}
|
|
97
|
+
const out = new Uint8Array(size);
|
|
98
|
+
let at = 0;
|
|
99
|
+
for (const chunk of chunks) {
|
|
100
|
+
out.set(chunk, at);
|
|
101
|
+
at += chunk.length;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
static _read(message, finish) {
|
|
106
|
+
const body = Request._consume(message);
|
|
107
|
+
if (Request._isStream(body)) return Request._streamBytes(body).then(finish);
|
|
108
|
+
return finish(Request._bodyBytes(body));
|
|
109
|
+
}
|
|
110
|
+
|
|
77
111
|
constructor(input, init = undefined) {
|
|
78
112
|
let url = input;
|
|
79
113
|
let method = 'GET';
|
|
@@ -115,14 +149,22 @@ export const REQUEST_CLASS_SOURCE = String.raw`class Request {
|
|
|
115
149
|
get redirect() { return this._redirect ?? 'follow'; }
|
|
116
150
|
get bodyUsed() { return this._bodyUsed === true; }
|
|
117
151
|
|
|
118
|
-
arrayBuffer() { return Request.
|
|
119
|
-
blob() { return new Blob([
|
|
152
|
+
arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
|
|
153
|
+
blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
|
|
120
154
|
clone() {
|
|
121
155
|
if (this._bodyUsed === true) throw new TypeError('Body has already been used');
|
|
122
156
|
return new Request(this);
|
|
123
157
|
}
|
|
124
|
-
json() {
|
|
125
|
-
|
|
158
|
+
json() {
|
|
159
|
+
const body = Request._consume(this);
|
|
160
|
+
if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
|
|
161
|
+
return JSON.parse(Request._bodyText(body));
|
|
162
|
+
}
|
|
163
|
+
text() {
|
|
164
|
+
const body = Request._consume(this);
|
|
165
|
+
if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
|
|
166
|
+
return Request._bodyText(body);
|
|
167
|
+
}
|
|
126
168
|
}
|
|
127
169
|
|
|
128
170
|
`;
|
|
@@ -152,8 +194,8 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
|
|
|
152
194
|
get ok() { return this.status >= 200 && this.status < 300; }
|
|
153
195
|
get bodyUsed() { return this._bodyUsed === true; }
|
|
154
196
|
|
|
155
|
-
arrayBuffer() { return Request.
|
|
156
|
-
blob() { return new Blob([
|
|
197
|
+
arrayBuffer() { return Request._read(this, (bytes) => bytes.buffer); }
|
|
198
|
+
blob() { return Request._read(this, (bytes) => new Blob([bytes])); }
|
|
157
199
|
clone() {
|
|
158
200
|
if (this._bodyUsed === true) throw new TypeError('Body has already been used');
|
|
159
201
|
const copy = new Response(null, { status: this.status == 0 ? 200 : this.status, statusText: this._statusText, headers: this.headers });
|
|
@@ -162,8 +204,16 @@ export const RESPONSE_CLASS_SOURCE = String.raw`class Response {
|
|
|
162
204
|
if (this._type != null) copy._type = this._type;
|
|
163
205
|
return copy;
|
|
164
206
|
}
|
|
165
|
-
json() {
|
|
166
|
-
|
|
207
|
+
json() {
|
|
208
|
+
const body = Request._consume(this);
|
|
209
|
+
if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => JSON.parse(new TextDecoder().decode(bytes)));
|
|
210
|
+
return JSON.parse(Request._bodyText(body));
|
|
211
|
+
}
|
|
212
|
+
text() {
|
|
213
|
+
const body = Request._consume(this);
|
|
214
|
+
if (Request._isStream(body)) return Request._streamBytes(body).then((bytes) => new TextDecoder().decode(bytes));
|
|
215
|
+
return Request._bodyText(body);
|
|
216
|
+
}
|
|
167
217
|
}
|
|
168
218
|
`;
|
|
169
219
|
|
package/src/fingerprint.test.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expect, test } from "bun:test";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
|
-
import { PATCH_FINGERPRINT, PATCH_SOURCES } from "./fingerprint";
|
|
3
|
+
import { PATCH_FINGERPRINT, PATCH_SOURCES, RELEASED_FINGERPRINT, sourceFingerprint } from "./fingerprint";
|
|
4
4
|
|
|
5
5
|
// Not part of the patched compiler: acquisition, the barrel, generators and this module.
|
|
6
6
|
const NOT_PATCH_SOURCES = new Set(["acquire.ts", "index.ts", "fingerprint.ts"]);
|
|
@@ -9,8 +9,20 @@ test("the patch fingerprint covers every toolchain module that shapes the compil
|
|
|
9
9
|
const modules = (await readdir(import.meta.dir)).filter(
|
|
10
10
|
(name) => name.endsWith(".ts") && !name.endsWith(".test.ts") && !name.startsWith("gen-") && !NOT_PATCH_SOURCES.has(name),
|
|
11
11
|
);
|
|
12
|
-
expect(
|
|
13
|
-
const embedded = new Map<string, string>(Object.entries(PATCH_SOURCES));
|
|
14
|
-
for (const name of modules) expect(embedded.get(name)).toBe(await Bun.file(`${import.meta.dir}/${name}`).text());
|
|
12
|
+
expect(modules.sort()).toEqual(PATCH_SOURCES.map(String).sort());
|
|
15
13
|
expect(PATCH_FINGERPRINT).toMatch(/^[0-9a-f]{12}$/);
|
|
14
|
+
expect(PATCH_FINGERPRINT).toBe(sourceFingerprint(import.meta.dir)!);
|
|
15
|
+
expect(sourceFingerprint("/nonexistent")).toBeNull();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// A compiled CLI falls back to RELEASED_FINGERPRINT, so it must match the sources.
|
|
19
|
+
test("RELEASED_FINGERPRINT matches the sources", async () => {
|
|
20
|
+
const actual = sourceFingerprint(import.meta.dir)!;
|
|
21
|
+
if (process.env.UPDATE_FINGERPRINT && actual !== RELEASED_FINGERPRINT) {
|
|
22
|
+
const file = `${import.meta.dir}/fingerprint.ts`;
|
|
23
|
+
const text = await Bun.file(file).text();
|
|
24
|
+
await Bun.write(file, text.replace(`RELEASED_FINGERPRINT = "${RELEASED_FINGERPRINT}"`, `RELEASED_FINGERPRINT = "${actual}"`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
expect(RELEASED_FINGERPRINT, "run UPDATE_FINGERPRINT=1 bun test packages/toolchain/src/fingerprint.test.ts").toBe(actual);
|
|
16
28
|
});
|
package/src/fingerprint.ts
CHANGED
|
@@ -1,48 +1,44 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
// a module, not a string, hence the expect-errors.
|
|
5
|
-
// @ts-expect-error text import
|
|
6
|
-
import caseMap from "./case-map.ts" with { type: "text" };
|
|
7
|
-
// @ts-expect-error text import
|
|
8
|
-
import fetchClasses from "./fetch-classes.ts" with { type: "text" };
|
|
9
|
-
// @ts-expect-error text import
|
|
10
|
-
import headersClass from "./headers-class.ts" with { type: "text" };
|
|
11
|
-
// @ts-expect-error text import
|
|
12
|
-
import patch from "./patch.ts" with { type: "text" };
|
|
13
|
-
// @ts-expect-error text import
|
|
14
|
-
import pin from "./pin.ts" with { type: "text" };
|
|
15
|
-
// @ts-expect-error text import
|
|
16
|
-
import streaming from "./streaming.ts" with { type: "text" };
|
|
17
|
-
// @ts-expect-error text import
|
|
18
|
-
import textDecoder from "./text-decoder.ts" with { type: "text" };
|
|
19
|
-
// @ts-expect-error text import
|
|
20
|
-
import unicode from "./unicode.ts" with { type: "text" };
|
|
21
|
-
// @ts-expect-error text import
|
|
22
|
-
import urlClass from "./url-class.ts" with { type: "text" };
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
23
4
|
|
|
24
5
|
/** Every module that shapes the patched compiler. fingerprint.test.ts fails when one is missing. */
|
|
25
|
-
export const PATCH_SOURCES =
|
|
26
|
-
"case-map.ts"
|
|
27
|
-
"fetch-classes.ts"
|
|
28
|
-
"headers-class.ts"
|
|
29
|
-
"patch.ts"
|
|
30
|
-
"pin.ts"
|
|
31
|
-
"streaming.ts"
|
|
32
|
-
"text-decoder.ts"
|
|
33
|
-
"unicode.ts"
|
|
34
|
-
"url-class.ts"
|
|
35
|
-
|
|
6
|
+
export const PATCH_SOURCES = [
|
|
7
|
+
"case-map.ts",
|
|
8
|
+
"fetch-classes.ts",
|
|
9
|
+
"headers-class.ts",
|
|
10
|
+
"patch.ts",
|
|
11
|
+
"pin.ts",
|
|
12
|
+
"streaming.ts",
|
|
13
|
+
"text-decoder.ts",
|
|
14
|
+
"unicode.ts",
|
|
15
|
+
"url-class.ts",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The fingerprint of the sources as released. A compiled CLI (`bun build --compile`) has
|
|
20
|
+
* no source files to hash, and cannot embed them either: Bun bundles a text import of
|
|
21
|
+
* patch.ts as the module patch.ts is everywhere else. Kept current by fingerprint.test.ts
|
|
22
|
+
* (UPDATE_FINGERPRINT=1 bun test packages/toolchain/src/fingerprint.test.ts).
|
|
23
|
+
*/
|
|
24
|
+
export const RELEASED_FINGERPRINT = "5a243546eb20";
|
|
25
|
+
|
|
26
|
+
/** The fingerprint of the sources in dir, or null where they are not on disk. */
|
|
27
|
+
export function sourceFingerprint(dir: string): string | null {
|
|
28
|
+
const hash = createHash("sha256");
|
|
29
|
+
try {
|
|
30
|
+
for (const name of PATCH_SOURCES) hash.update(`${name}\0${readFileSync(join(dir, name), "utf8")}\0`);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return hash.digest("hex").slice(0, 12);
|
|
35
|
+
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Identifies the patch chain, not just the toolchain release. A cache entry is keyed by it, so
|
|
39
39
|
* an edited patch gets a fresh, fully patched compiler instead of one a same-version build
|
|
40
40
|
* already marked complete (B19: the prelude began calling sb_stream_*, which only the new
|
|
41
|
-
* chain defines, and every build against an older entry failed to link).
|
|
41
|
+
* chain defines, and every build against an older entry failed to link). From the sources
|
|
42
|
+
* when they are on disk, so an unreleased edit counts at once; else as released.
|
|
42
43
|
*/
|
|
43
|
-
export const PATCH_FINGERPRINT = (
|
|
44
|
-
const hash = createHash("sha256");
|
|
45
|
-
for (const [name, source] of Object.entries(PATCH_SOURCES).sort(([a], [b]) => a.localeCompare(b)))
|
|
46
|
-
hash.update(`${name}\0${source}\0`);
|
|
47
|
-
return hash.digest("hex").slice(0, 12);
|
|
48
|
-
})();
|
|
44
|
+
export const PATCH_FINGERPRINT = sourceFingerprint(import.meta.dir) ?? RELEASED_FINGERPRINT;
|
package/src/patch.ts
CHANGED
|
@@ -1938,6 +1938,55 @@ export async function patchJsonCycles(root: string): Promise<void> {
|
|
|
1938
1938
|
await writeFile(file, src);
|
|
1939
1939
|
}
|
|
1940
1940
|
|
|
1941
|
+
// JSON.parse ignored its reviver (W2 conformance: json/reviver). The parser keeps its body
|
|
1942
|
+
// under a new name, and __JSON_parse walks the result as the spec's InternalizeJSONProperty
|
|
1943
|
+
// does: children first, a reviver answer of undefined deletes the property.
|
|
1944
|
+
const JSON_REVIVER_MARKER = "sproutboat: JSON.parse reviver";
|
|
1945
|
+
// Porffor declares (_: bytestring); patchUnicode widens it to any later, on the renamed parser.
|
|
1946
|
+
const JSON_PARSE_ANCHOR = /export const __JSON_parse = \(_: (bytestring|any)\) => \{\n/;
|
|
1947
|
+
const JSON_REVIVER_SOURCE = `
|
|
1948
|
+
// ${JSON_REVIVER_MARKER}
|
|
1949
|
+
export const __Porffor_json_revive = (holder: any, key: any, reviver: any): any => {
|
|
1950
|
+
const value: any = holder[key];
|
|
1951
|
+
if (Porffor.type(value) == Porffor.TYPES.array) {
|
|
1952
|
+
const len: i32 = value.length;
|
|
1953
|
+
for (let i: i32 = 0; i < len; i++) {
|
|
1954
|
+
const next: any = __Porffor_json_revive(value, ecma262.ToString(i), reviver);
|
|
1955
|
+
if (next === undefined) delete value[i];
|
|
1956
|
+
else value[i] = next;
|
|
1957
|
+
}
|
|
1958
|
+
} else if (Porffor.type(value) == Porffor.TYPES.object) {
|
|
1959
|
+
for (const k: any in value) {
|
|
1960
|
+
const next: any = __Porffor_json_revive(value, k, reviver);
|
|
1961
|
+
if (next === undefined) delete value[k];
|
|
1962
|
+
else value[k] = next;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return reviver.call(holder, key, value);
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1968
|
+
export const __JSON_parse = (text: any, reviver: any) => {
|
|
1969
|
+
const value: any = __Porffor_json_parseText(text);
|
|
1970
|
+
if (Porffor.type(reviver) != Porffor.TYPES.function) return value;
|
|
1971
|
+
const root: object = {};
|
|
1972
|
+
root[''] = value;
|
|
1973
|
+
return __Porffor_json_revive(root, '', reviver);
|
|
1974
|
+
};
|
|
1975
|
+
`;
|
|
1976
|
+
|
|
1977
|
+
/** JSON.parse applies its reviver, as InternalizeJSONProperty does. */
|
|
1978
|
+
export async function patchJsonReviver(root: string): Promise<void> {
|
|
1979
|
+
const file = resolve(root, "compiler/builtins/json.ts");
|
|
1980
|
+
const src = await readFile(file, "utf8");
|
|
1981
|
+
if (src.includes(JSON_REVIVER_MARKER)) return;
|
|
1982
|
+
if ((src.match(new RegExp(JSON_PARSE_ANCHOR, "g")) ?? []).length !== 1)
|
|
1983
|
+
throw new Error(`could not patch Porffor's JSON.parse reviver: anchor not found in ${file}`);
|
|
1984
|
+
await writeFile(
|
|
1985
|
+
file,
|
|
1986
|
+
src.replace(JSON_PARSE_ANCHOR, (_line, type) => `export const __Porffor_json_parseText = (_: ${type}) => {\n`).trimEnd() + "\n" + JSON_REVIVER_SOURCE,
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1941
1990
|
// Object.prototype.toString gave "[object Object]" for typed arrays: their lazily
|
|
1942
1991
|
// built prototypes have no toStringTag, and the fallback list of builtin tags left
|
|
1943
1992
|
// them out (W2 conformance; V8 gives "[object Uint8Array]").
|
|
@@ -2251,6 +2300,7 @@ export async function ensurePorfforPatched(root: string): Promise<void> {
|
|
|
2251
2300
|
await patchPrivateFieldsHidden(root);
|
|
2252
2301
|
await patchRepeatDoubling(root);
|
|
2253
2302
|
await patchJsonCycles(root);
|
|
2303
|
+
await patchJsonReviver(root);
|
|
2254
2304
|
await patchTypedArrayTag(root);
|
|
2255
2305
|
await patchConsoleColors(root);
|
|
2256
2306
|
await patchCaseMapping(root);
|
|
@@ -110,4 +110,6 @@ test("native Unicode JSON and UTF-8 encoding preserve code points and buffer bou
|
|
|
110
110
|
}
|
|
111
111
|
await rm(work, { recursive: true, force: true });
|
|
112
112
|
}
|
|
113
|
-
|
|
113
|
+
// An optimised native build of the whole prelude: about 17 s warm, but 50 to 110 s cold
|
|
114
|
+
// (a new patch fingerprint or prelude recompiles everything), which 60 s did not cover.
|
|
115
|
+
}, 180_000);
|