as-test 1.3.0 → 1.4.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.
@@ -0,0 +1,122 @@
1
+ import { OBJECT, TOTAL_OVERHEAD } from "rt/common";
2
+
3
+ // Structural equality entry point. Modelled on as-pect's Reflect.equals,
4
+ // trimmed of memoisation cache and ignore-list bookkeeping — those live
5
+ // in the per-class methods emitted by the EqualsTransform.
6
+ //
7
+ // Per-call flow:
8
+ // * primitives, strings, booleans → `===`
9
+ // * nullables → resolve null on either side, then fall through
10
+ // * arrays → length + element-wise recursion with the same `stack`
11
+ // * managed values → identity short-circuit, optional `rtId` check
12
+ // for strict mode, cycle detection via `stack`, then dispatch to
13
+ // the class's `__AS_TEST_EQUALS(other, stack, ignore, strict)`
14
+ //
15
+ // `right` is forwarded with its full static type so the dispatched
16
+ // method's body reads fields directly via AS virtual dispatch.
17
+
18
+ export function reflectEquals<T>(
19
+ left: T,
20
+ right: T,
21
+ stack: usize[],
22
+ strict: bool,
23
+ ): bool {
24
+ if (isBoolean<T>() || isInteger<T>() || isFloat<T>() || isString<T>())
25
+ return left === right;
26
+
27
+ if (isNullable<T>()) {
28
+ const lp = changetype<usize>(left);
29
+ const rp = changetype<usize>(right);
30
+ if (lp == 0 || rp == 0) return lp == rp;
31
+ }
32
+
33
+ if (isArray<T>()) {
34
+ // @ts-expect-error: type
35
+ const aLen = (left as valueof<T>[]).length;
36
+ // @ts-expect-error: type
37
+ const bLen = (right as valueof<T>[]).length;
38
+ if (aLen != bLen) return false;
39
+ for (let i = 0; i < aLen; i++) {
40
+ if (
41
+ // @ts-expect-error: type
42
+ !reflectEquals<valueof<T>>(
43
+ // @ts-expect-error: type
44
+ unchecked((left as valueof<T>[])[i]),
45
+ // @ts-expect-error: type
46
+ unchecked((right as valueof<T>[])[i]),
47
+ stack,
48
+ strict,
49
+ )
50
+ ) {
51
+ return false;
52
+ }
53
+ }
54
+ return true;
55
+ }
56
+
57
+ if (isManaged<T>()) {
58
+ const lp = changetype<usize>(left);
59
+ const rp = changetype<usize>(right);
60
+ if (lp == rp) return true;
61
+ if (lp == 0 || rp == 0) return false;
62
+
63
+ if (strict) {
64
+ const lo = changetype<OBJECT>(lp - TOTAL_OVERHEAD);
65
+ const ro = changetype<OBJECT>(rp - TOTAL_OVERHEAD);
66
+ if (lo.rtId != ro.rtId) return false;
67
+ }
68
+
69
+ for (let i = 0; i < stack.length; i += 2) {
70
+ if (unchecked(stack[i]) == lp && unchecked(stack[i + 1]) == rp)
71
+ return true;
72
+ }
73
+
74
+ stack.push(lp);
75
+ stack.push(rp);
76
+
77
+ // Pass `right` with its full static type so the dispatched method's
78
+ // body can read fields directly. For nullable T, strip the
79
+ // nullability with `!` — we returned early above if either operand
80
+ // was null, so `right!` is safe at this point.
81
+ //
82
+ // The transform injects `__AS_TEST_EQUALS` on every user class the
83
+ // compiler reaches, but third-party generic types (Map, Set,
84
+ // Option<T>, …) don't carry it. Fall back to the class's `==`
85
+ // operator (which the user can overload via `@operator("==")`),
86
+ // collapsing to reference identity if no overload is present.
87
+ let passed: bool;
88
+ if (isNullable<T>()) {
89
+ // @ts-expect-error: optional method, presence is a compile-time check
90
+ if (isDefined(left!.__AS_TEST_EQUALS)) {
91
+ // @ts-expect-error: declared by transform
92
+ passed = left!.__AS_TEST_EQUALS(
93
+ right!,
94
+ stack,
95
+ [] as StaticArray<i64>,
96
+ strict,
97
+ );
98
+ } else {
99
+ passed = left == right;
100
+ }
101
+ } else {
102
+ // @ts-expect-error: optional method, presence is a compile-time check
103
+ if (isDefined(left.__AS_TEST_EQUALS)) {
104
+ // @ts-expect-error: declared by transform
105
+ passed = left.__AS_TEST_EQUALS(
106
+ right,
107
+ stack,
108
+ [] as StaticArray<i64>,
109
+ strict,
110
+ );
111
+ } else {
112
+ passed = left == right;
113
+ }
114
+ }
115
+
116
+ stack.pop();
117
+ stack.pop();
118
+ return passed;
119
+ }
120
+
121
+ return left === right;
122
+ }
@@ -0,0 +1,240 @@
1
+ // Tiny in-tree replacement for json-as's runtime. Used for two jobs:
2
+ //
3
+ // 1. Stringifying user values for the wipc assertion-report payload
4
+ // (was `JSON.stringify<T>(value)`).
5
+ // 2. JSON-escaping internal strings before splicing them into hand-built
6
+ // wire payloads (was `JSON.stringify<string>(s)`).
7
+ //
8
+ // Dispatch order in `stringify<T>`:
9
+ // * primitives / booleans / numbers → `.toString()`
10
+ // * strings → JSON-escape + quote
11
+ // * nullable null → "null"
12
+ // * Date → quoted ISO-8601 string
13
+ // * ArrayBuffer → array of unsigned byte values
14
+ // * TypedArray (ArrayBufferView) → array of element values
15
+ // * Array / StaticArray → element-wise recursion
16
+ // * Set → array of values
17
+ // * Map → object with stringified keys
18
+ // * managed with `toJSON(): string` → call it
19
+ // * managed without `toJSON()` → "<TypeName>" placeholder
20
+ //
21
+ // The Date/ArrayBuffer/typed-array/StaticArray/Set/Map branches use
22
+ // `value instanceof X` guards. In a generic function AssemblyScript resolves
23
+ // these statically: the branch is only compiled when `T` can actually be that
24
+ // type and pruned otherwise, so the recursive calls inside type-check against
25
+ // the real K/V/element types.
26
+ //
27
+ // Classes decorated with `@json` / `@serializable` are skipped by the
28
+ // EqualsTransform's toJSON injector. Users who want those classes to
29
+ // render prettily in reports can add their own one-line `toJSON()`
30
+ // (e.g. `return JSON.stringify(this);` if they've also wired up
31
+ // `--transform json-as`). as-test itself stays json-as-free.
32
+
33
+ export function stringify<T>(value: T): string {
34
+ if (isBoolean<T>()) return value ? "true" : "false";
35
+ if (isInteger<T>() || isFloat<T>()) {
36
+ // @ts-ignore: every numeric AS primitive carries toString()
37
+ return value.toString();
38
+ }
39
+ if (isString<T>()) {
40
+ return escape(value as string);
41
+ }
42
+
43
+ if (isNullable<T>() && changetype<usize>(value) == 0) return "null";
44
+
45
+ // Date → quoted ISO-8601 string (matches `JSON.stringify(new Date(...))`).
46
+ if (value instanceof Date) {
47
+ return escape((value as Date).toISOString());
48
+ }
49
+
50
+ // ArrayBuffer → array of its unsigned byte values. A raw buffer has no
51
+ // natural JSON form, so surface the bytes for debugging.
52
+ if (value instanceof ArrayBuffer) {
53
+ const view = Uint8Array.wrap(value as ArrayBuffer);
54
+ const len = view.length;
55
+ if (len == 0) return "[]";
56
+ let out = "[";
57
+ for (let i = 0; i < len; i++) {
58
+ if (i > 0) out += ",";
59
+ out += unchecked(view[i]).toString();
60
+ }
61
+ return out + "]";
62
+ }
63
+
64
+ // Typed arrays (Int32Array, Float64Array, …) all extend ArrayBufferView.
65
+ if (value instanceof ArrayBufferView) {
66
+ // @ts-ignore: every typed array carries a typesafe length + indexer
67
+ const len = value.length;
68
+ if (len == 0) return "[]";
69
+ let out = "[";
70
+ for (let i = 0; i < len; i++) {
71
+ if (i > 0) out += ",";
72
+ // @ts-ignore: element is the view's numeric valueof type
73
+ out += stringify(unchecked(value[i]));
74
+ }
75
+ return out + "]";
76
+ }
77
+
78
+ if (isArray<T>()) {
79
+ // @ts-ignore: typesafe length
80
+ const len = (value as valueof<T>[]).length;
81
+ if (len == 0) return "[]";
82
+ let out = "[";
83
+ for (let i = 0; i < len; i++) {
84
+ if (i > 0) out += ",";
85
+ out += stringify<valueof<T>>(
86
+ // @ts-ignore: bounds-checked above
87
+ unchecked((value as valueof<T>[])[i]),
88
+ );
89
+ }
90
+ return out + "]";
91
+ }
92
+
93
+ // StaticArray<V> → element-wise recursion, same shape as a regular array.
94
+ if (value instanceof StaticArray) {
95
+ // @ts-ignore: typesafe length + indexer
96
+ const len = value.length;
97
+ if (len == 0) return "[]";
98
+ let out = "[";
99
+ for (let i = 0; i < len; i++) {
100
+ if (i > 0) out += ",";
101
+ // @ts-ignore: element is the array's valueof type
102
+ out += stringify(unchecked(value[i]));
103
+ }
104
+ return out + "]";
105
+ }
106
+
107
+ // Set<V> → array of its values, in insertion order.
108
+ if (value instanceof Set) {
109
+ const vals = value.values();
110
+ const len = vals.length;
111
+ if (len == 0) return "[]";
112
+ let out = "[";
113
+ for (let i = 0; i < len; i++) {
114
+ if (i > 0) out += ",";
115
+ out += stringify(unchecked(vals[i]));
116
+ }
117
+ return out + "]";
118
+ }
119
+
120
+ // Map<K,V> → JSON object. JSON object keys must be strings, so non-string
121
+ // keys are coerced to their `.toString()` form and quoted (e.g. a numeric
122
+ // key `10` becomes `"10"`, matching `JSON.stringify({ 10: ... })`).
123
+ if (value instanceof Map) {
124
+ const keys = value.keys();
125
+ const vals = value.values();
126
+ const len = keys.length;
127
+ if (len == 0) return "{}";
128
+ let out = "{";
129
+ for (let i = 0; i < len; i++) {
130
+ if (i > 0) out += ",";
131
+ out += jsonKey(unchecked(keys[i])) + ":" + stringify(unchecked(vals[i]));
132
+ }
133
+ return out + "}";
134
+ }
135
+
136
+ if (isManaged<T>()) {
137
+ // @ts-ignore: hand-written or transform-generated serializer
138
+ if (isDefined(value.toJSON)) return value.toJSON();
139
+ return escape("<" + nameof<T>() + ">");
140
+ }
141
+
142
+ // Unreachable for well-typed AS code — but emit a valid JSON string so
143
+ // the surrounding payload stays parsable.
144
+ return escape("<" + nameof<T>() + ">");
145
+ }
146
+
147
+ // JSON string escape per RFC 8259, with explicit handling for UTF-16
148
+ // surrogates (matches json-as's serializeString behaviour).
149
+ //
150
+ // * `"`, `\`, control chars U+0000..U+001F → escape sequences
151
+ // * valid surrogate pair (high → low) → pass both code units through
152
+ // * lone surrogate (high not followed by → emit `\uXXXX` for the
153
+ // low, or low without a preceding high) single code unit
154
+ // * everything else → pass through
155
+ export function escape(s: string): string {
156
+ let out = '"';
157
+ const len = s.length;
158
+ for (let i = 0; i < len; i++) {
159
+ const c = s.charCodeAt(i);
160
+ if (c == 0x22 /* " */) {
161
+ out += '\\"';
162
+ continue;
163
+ }
164
+ if (c == 0x5c /* \ */) {
165
+ out += "\\\\";
166
+ continue;
167
+ }
168
+ if (c == 0x08) {
169
+ out += "\\b";
170
+ continue;
171
+ }
172
+ if (c == 0x09) {
173
+ out += "\\t";
174
+ continue;
175
+ }
176
+ if (c == 0x0a) {
177
+ out += "\\n";
178
+ continue;
179
+ }
180
+ if (c == 0x0c) {
181
+ out += "\\f";
182
+ continue;
183
+ }
184
+ if (c == 0x0d) {
185
+ out += "\\r";
186
+ continue;
187
+ }
188
+ if (c < 0x20) {
189
+ out += "\\u00" + hexNibble((c >>> 4) & 0xf) + hexNibble(c & 0xf);
190
+ continue;
191
+ }
192
+ if (c >= 0xd800 && c <= 0xdfff) {
193
+ // High surrogate followed by a low surrogate is a valid pair —
194
+ // emit both code units verbatim and skip the low surrogate index.
195
+ if (c <= 0xdbff && i + 1 < len) {
196
+ const next = s.charCodeAt(i + 1);
197
+ if (next >= 0xdc00 && next <= 0xdfff) {
198
+ out += String.fromCharCode(c);
199
+ out += String.fromCharCode(next);
200
+ i++;
201
+ continue;
202
+ }
203
+ }
204
+ // Lone surrogate — escape the single code unit.
205
+ out += "\\u" + hex4(<u32>c);
206
+ continue;
207
+ }
208
+ out += String.fromCharCode(c);
209
+ }
210
+ return out + '"';
211
+ }
212
+
213
+ // Render a Map key as a quoted JSON string. Primitive keys are coerced to
214
+ // their textual form first; anything else falls back to the value serializer,
215
+ // wrapped as a string so the surrounding object stays parsable.
216
+ function jsonKey<K>(key: K): string {
217
+ if (isString<K>()) return escape(key as string);
218
+ if (isBoolean<K>()) return escape(key ? "true" : "false");
219
+ if (isInteger<K>() || isFloat<K>()) {
220
+ // @ts-ignore: numeric AS primitives carry toString()
221
+ return escape(key.toString());
222
+ }
223
+ return escape(stringify<K>(key));
224
+ }
225
+
226
+
227
+ @inline function hexNibble(n: u32): string {
228
+ // 0..9 → '0'..'9'; 10..15 → 'a'..'f'.
229
+ return String.fromCharCode(n < 10 ? 0x30 + n : 0x61 + (n - 10));
230
+ }
231
+
232
+
233
+ @inline function hex4(c: u32): string {
234
+ return (
235
+ hexNibble((c >>> 12) & 0xf) +
236
+ hexNibble((c >>> 8) & 0xf) +
237
+ hexNibble((c >>> 4) & 0xf) +
238
+ hexNibble(c & 0xf)
239
+ );
240
+ }
@@ -2,9 +2,14 @@ import { Time } from "..";
2
2
  import { Expectation } from "./expectation";
3
3
  import { Tests } from "./tests";
4
4
  import { Log } from "./log";
5
- import { after_each_callback, before_each_callback } from "..";
5
+ import {
6
+ after_each_callback,
7
+ after_each_kinds,
8
+ before_each_callback,
9
+ before_each_kinds,
10
+ } from "..";
6
11
  import { sendSuiteEnd, sendSuiteStart } from "../util/wipc";
7
- import { JSON } from "json-as/assembly";
12
+ import { escape, stringify } from "./stringify";
8
13
 
9
14
  export class Suite {
10
15
  public file: string = "unknown";
@@ -49,10 +54,8 @@ export class Suite {
49
54
  }
50
55
 
51
56
  run(): void {
52
- // @ts-ignore
57
+ // @ts-ignore: current_suite is a @global, the parent for nested registration
53
58
  current_suite = this;
54
- // @ts-ignore
55
- depth++;
56
59
  this.time.start = performance.now();
57
60
  sendSuiteStart(this.file, this.depth, this.kind, this.description);
58
61
  const isSkippedCase =
@@ -73,8 +76,6 @@ export class Suite {
73
76
  if (isSkippedCase) {
74
77
  this.time.end = performance.now();
75
78
  this.verdict = "skip";
76
- // @ts-ignore
77
- depth--;
78
79
  sendSuiteEnd(
79
80
  this.file,
80
81
  this.depth,
@@ -85,14 +86,22 @@ export class Suite {
85
86
  return;
86
87
  }
87
88
 
88
- // @ts-ignore
89
- if (isTestCase && before_each_callback) before_each_callback();
89
+ // @ts-ignore: nullable function import resolved at runtime
90
+ if (
91
+ before_each_callback &&
92
+ hookFiresFor(this.kind, before_each_kinds, isTestCase)
93
+ ) {
94
+ before_each_callback();
95
+ }
90
96
  this.callback();
91
- // @ts-ignore
92
- if (isTestCase && after_each_callback) after_each_callback();
97
+ // @ts-ignore: nullable function import resolved at runtime
98
+ if (
99
+ after_each_callback &&
100
+ hookFiresFor(this.kind, after_each_kinds, isTestCase)
101
+ ) {
102
+ after_each_callback();
103
+ }
93
104
  this.time.end = performance.now();
94
- // @ts-ignore
95
- depth--;
96
105
 
97
106
  const hasOnlyChildren = this.hasOnlyChildren();
98
107
 
@@ -146,16 +155,12 @@ export class Suite {
146
155
  }
147
156
 
148
157
  skip(): void {
149
- // @ts-ignore
158
+ // @ts-ignore: current_suite is a @global
150
159
  current_suite = this;
151
- // @ts-ignore
152
- depth++;
153
160
  this.time.start = performance.now();
154
161
  this.time.end = this.time.start;
155
162
  this.verdict = "skip";
156
163
  sendSuiteStart(this.file, this.depth, this.kind, this.description);
157
- // @ts-ignore
158
- depth--;
159
164
  sendSuiteEnd(
160
165
  this.file,
161
166
  this.depth,
@@ -172,31 +177,47 @@ export class Suite {
172
177
  return false;
173
178
  }
174
179
 
175
- serialize(): string {
180
+ toJSON(): string {
176
181
  let out = "{";
177
182
  if (this.depth <= 0) {
178
- out += '"file":' + JSON.stringify<string>(this.file) + ",";
183
+ out += '"file":' + escape(this.file) + ",";
179
184
  }
180
185
  out += '"order":' + this.order.toString();
181
- out += ',"time":' + this.time.serialize();
182
- out += ',"description":' + JSON.stringify<string>(this.description);
186
+ out += ',"time":' + this.time.toJSON();
187
+ out += ',"description":' + escape(this.description);
183
188
  out += ',"depth":' + this.depth.toString();
184
189
  out += ',"suites":' + serializeSuites(this.suites);
185
190
  out += ',"tests":' + serializeTests(this.tests);
186
191
  out += ',"logs":' + serializeLogs(this.logs);
187
- out += ',"kind":' + JSON.stringify<string>(this.kind);
188
- out += ',"verdict":' + JSON.stringify<string>(this.verdict);
192
+ out += ',"kind":' + escape(this.kind);
193
+ out += ',"verdict":' + escape(this.verdict);
189
194
  out += "}";
190
195
  return out;
191
196
  }
192
197
  }
193
198
 
199
+ // Whether a beforeEach/afterEach hook should fire for a suite of `kind`. A
200
+ // `null` kinds list (the default) restricts the hook to test cases — the caller
201
+ // passes whether `kind` is one. An explicit list fires for exactly those kinds,
202
+ // which is how `beforeEach(fn, ["describe", "test"])` opts grouping blocks in.
203
+ function hookFiresFor(
204
+ kind: string,
205
+ kinds: string[] | null,
206
+ isTestCaseKind: bool,
207
+ ): bool {
208
+ if (kinds === null) return isTestCaseKind;
209
+ for (let i = 0; i < kinds.length; i++) {
210
+ if (unchecked(kinds[i]) == kind) return true;
211
+ }
212
+ return false;
213
+ }
214
+
194
215
  function serializeSuites(values: Suite[]): string {
195
216
  if (!values.length) return "[]";
196
217
  let out = "[";
197
218
  for (let i = 0; i < values.length; i++) {
198
219
  if (i) out += ",";
199
- out += unchecked(values[i]).serialize();
220
+ out += unchecked(values[i]).toJSON();
200
221
  }
201
222
  out += "]";
202
223
  return out;
@@ -207,7 +228,7 @@ function serializeTests(values: Tests[]): string {
207
228
  let out = "[";
208
229
  for (let i = 0; i < values.length; i++) {
209
230
  if (i) out += ",";
210
- out += unchecked(values[i]).serialize();
231
+ out += unchecked(values[i]).toJSON();
211
232
  }
212
233
  out += "]";
213
234
  return out;
@@ -218,7 +239,7 @@ function serializeLogs(values: Log[]): string {
218
239
  let out = "[";
219
240
  for (let i = 0; i < values.length; i++) {
220
241
  if (i) out += ",";
221
- out += unchecked(values[i]).serialize();
242
+ out += unchecked(values[i]).toJSON();
222
243
  }
223
244
  out += "]";
224
245
  return out;
@@ -1,4 +1,4 @@
1
- import { JSON } from "json-as/assembly";
1
+ import { escape, stringify } from "./stringify";
2
2
 
3
3
  export class Tests {
4
4
  public order: i32 = 0;
@@ -10,24 +10,24 @@ export class Tests {
10
10
  public message: string = "";
11
11
  public location: string = "";
12
12
 
13
- serialize(): string {
13
+ toJSON(): string {
14
14
  return (
15
15
  '{"order":' +
16
16
  this.order.toString() +
17
17
  ',"type":' +
18
- JSON.stringify<string>(this.type) +
18
+ escape(this.type) +
19
19
  ',"verdict":' +
20
- JSON.stringify<string>(this.verdict) +
20
+ escape(this.verdict) +
21
21
  ',"left":' +
22
22
  (this.left.length ? this.left : "null") +
23
23
  ',"right":' +
24
24
  (this.right.length ? this.right : "null") +
25
25
  ',"instr":' +
26
- JSON.stringify<string>(this.instr) +
26
+ escape(this.instr) +
27
27
  ',"message":' +
28
- JSON.stringify<string>(this.message) +
28
+ escape(this.message) +
29
29
  ',"location":' +
30
- JSON.stringify<string>(this.location) +
30
+ escape(this.location) +
31
31
  "}"
32
32
  );
33
33
  }
@@ -1,7 +1,7 @@
1
- import { JSON } from "json-as/assembly";
1
+ import { escape } from "../src/stringify";
2
2
 
3
3
  function q(s: string): string {
4
- return JSON.stringify<string>(s);
4
+ return escape(s);
5
5
  }
6
6
 
7
7
  // @ts-ignore
@@ -24,6 +24,7 @@ export class BuildWorkerPool {
24
24
  buildCommand: args.buildCommand,
25
25
  featureToggles,
26
26
  overrides,
27
+ onReads: args.onReads,
27
28
  resolve,
28
29
  reject,
29
30
  });
@@ -113,6 +114,13 @@ export class BuildWorkerPool {
113
114
  worker.busy = false;
114
115
  worker.task = null;
115
116
  if (message.type == "done") {
117
+ if (task.onReads && message.reads?.length) {
118
+ try {
119
+ task.onReads(message.reads);
120
+ } catch {
121
+ // a misbehaving sink shouldn't poison the build pipeline.
122
+ }
123
+ }
116
124
  task.resolve();
117
125
  } else {
118
126
  task.reject(deserializeError(message.error));
@@ -134,6 +142,7 @@ export class BuildWorkerPool {
134
142
  modeName: task.modeName,
135
143
  featureToggles: task.featureToggles,
136
144
  overrides: task.overrides,
145
+ recordReads: !!task.onReads,
137
146
  });
138
147
  }
139
148
  }
@@ -1,8 +1,13 @@
1
- import { build } from "./commands/build-core.js";
2
- process.env.AS_TEST_BUILD_API = "1";
1
+ import { build, buildRecorderStorage } from "./commands/build-core.js";
3
2
  process.on("message", async (message) => {
4
3
  if (!message || message.type != "build-file") return;
5
- try {
4
+ // Force the in-process API build path inside this worker so the readFile
5
+ // hook is reachable. We do it on first message rather than at module load
6
+ // so importing this file from a test doesn't mutate the parent env.
7
+ process.env.AS_TEST_BUILD_API = "1";
8
+ const seen = new Set();
9
+ const collected = [];
10
+ const runBuild = async () => {
6
11
  await build(
7
12
  message.configPath,
8
13
  [message.file],
@@ -10,9 +15,28 @@ process.on("message", async (message) => {
10
15
  message.featureToggles,
11
16
  message.overrides,
12
17
  );
18
+ };
19
+ try {
20
+ if (message.recordReads) {
21
+ const store = {
22
+ // asc commonly resolves the same source twice during a build (entry
23
+ // lookups, transform passes). Dedupe at record time so IPC payloads
24
+ // stay bounded — `(mode, spec)` is constant for the worker's lifetime
25
+ // of this task, so a file-keyed set is sufficient.
26
+ record: (mode, spec, file) => {
27
+ if (seen.has(file)) return;
28
+ seen.add(file);
29
+ collected.push({ mode, spec, file });
30
+ },
31
+ };
32
+ await buildRecorderStorage.run(store, runBuild);
33
+ } else {
34
+ await runBuild();
35
+ }
13
36
  send({
14
37
  type: "done",
15
38
  id: message.id,
39
+ reads: message.recordReads ? collected : undefined,
16
40
  });
17
41
  } catch (error) {
18
42
  send({