tape-six 1.7.13 → 1.8.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/README.md +3 -1
- package/TESTING.md +246 -47
- package/bin/tape6-server.js +19 -19
- package/index.d.ts +67 -0
- package/llms-full.txt +102 -0
- package/llms.txt +66 -0
- package/package.json +13 -4
- package/skills/write-tests/SKILL.md +63 -16
- package/src/State.js +26 -21
- package/src/Tester.js +59 -27
- package/src/deep6/env.d.ts +174 -0
- package/src/deep6/env.js +4 -4
- package/src/deep6/index.d.ts +86 -0
- package/src/deep6/index.js +10 -7
- package/src/deep6/traverse/assemble.d.ts +59 -0
- package/src/deep6/traverse/assemble.js +4 -3
- package/src/deep6/traverse/clone.d.ts +57 -0
- package/src/deep6/traverse/clone.js +4 -2
- package/src/deep6/traverse/deref.d.ts +59 -0
- package/src/deep6/traverse/deref.js +3 -2
- package/src/deep6/traverse/preprocess.d.ts +65 -0
- package/src/deep6/traverse/preprocess.js +2 -1
- package/src/deep6/traverse/walk.d.ts +219 -0
- package/src/deep6/traverse/walk.js +9 -4
- package/src/deep6/unifiers/matchCondition.d.ts +45 -0
- package/src/deep6/unifiers/matchCondition.js +1 -0
- package/src/deep6/unifiers/matchInstanceOf.d.ts +37 -0
- package/src/deep6/unifiers/matchInstanceOf.js +1 -0
- package/src/deep6/unifiers/matchString.d.ts +56 -0
- package/src/deep6/unifiers/matchString.js +1 -0
- package/src/deep6/unifiers/matchTypeOf.d.ts +37 -0
- package/src/deep6/unifiers/matchTypeOf.js +1 -0
- package/src/deep6/unifiers/ref.d.ts +52 -0
- package/src/deep6/unifiers/ref.js +1 -0
- package/src/deep6/unify.d.ts +95 -0
- package/src/deep6/unify.js +130 -66
- package/src/deep6/utils/replaceVars.d.ts +25 -0
- package/src/deep6/utils/replaceVars.js +23 -19
- package/src/response.d.ts +43 -0
- package/src/response.js +57 -0
- package/src/server.d.ts +81 -0
- package/src/server.js +69 -0
- package/src/test.js +26 -53
package/src/deep6/unify.js
CHANGED
|
@@ -24,18 +24,17 @@ class Wrap extends Unifier {
|
|
|
24
24
|
value = isWrapped ? val.object : val;
|
|
25
25
|
if (!value || typeof value != 'object' || Array.isArray(this.object) !== Array.isArray(value)) return false;
|
|
26
26
|
if (Array.isArray(this.object)) {
|
|
27
|
-
if (!Array.isArray(value)) return false;
|
|
28
27
|
return isWrapped
|
|
29
28
|
? unifyObjects(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
|
|
30
29
|
: unifyObjects(this.object, this.type, this, val, env.openArrays ? 'open' : 'exact', null, ls, rs, env);
|
|
31
30
|
}
|
|
32
|
-
if (
|
|
31
|
+
if (this.object instanceof Map) {
|
|
33
32
|
if (!(value instanceof Map)) return false;
|
|
34
33
|
return isWrapped
|
|
35
34
|
? unifyMaps(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
|
|
36
35
|
: unifyMaps(this.object, this.type, this, val, env.openMaps ? 'open' : 'exact', null, ls, rs, env);
|
|
37
36
|
}
|
|
38
|
-
if (
|
|
37
|
+
if (this.object instanceof Set) {
|
|
39
38
|
if (!(value instanceof Set)) return false;
|
|
40
39
|
return isWrapped
|
|
41
40
|
? unifySets(this.object, this.type, this, val.object, val.type, val, ls, rs, env)
|
|
@@ -61,11 +60,12 @@ const registry = [
|
|
|
61
60
|
Date,
|
|
62
61
|
(l, r) => l instanceof Date && r instanceof Date && l.getTime() == r.getTime(),
|
|
63
62
|
RegExp,
|
|
64
|
-
(l, r) =>
|
|
65
|
-
l instanceof RegExp && r instanceof RegExp && l.source == r.source && l.global == r.global && l.multiline == r.multiline && l.ignoreCase == r.ignoreCase
|
|
63
|
+
(l, r) => l instanceof RegExp && r instanceof RegExp && l.source == r.source && l.flags == r.flags
|
|
66
64
|
],
|
|
67
65
|
filters = [];
|
|
68
66
|
|
|
67
|
+
typeof URL == 'function' && registry.push(URL, (l, r) => l instanceof URL && r instanceof URL && l.href == r.href);
|
|
68
|
+
|
|
69
69
|
// possible well-known constructors
|
|
70
70
|
|
|
71
71
|
const unifyTypedArrays = Type => (l, r, ls, rs, env) => {
|
|
@@ -99,9 +99,10 @@ const unifyDataView = (l, r, ls, rs, env) => {
|
|
|
99
99
|
};
|
|
100
100
|
typeof DataView == 'function' && registry.push(DataView, unifyDataView);
|
|
101
101
|
|
|
102
|
+
const unifyUint8Array = unifyTypedArrays(Uint8Array);
|
|
102
103
|
const unifyArrayBuffer = (l, r, ls, rs, env) => {
|
|
103
104
|
if (!(l instanceof ArrayBuffer) || !(r instanceof ArrayBuffer) || l.byteLength != r.byteLength) return false;
|
|
104
|
-
return
|
|
105
|
+
return unifyUint8Array(new Uint8Array(l), new Uint8Array(r), ls, rs, env);
|
|
105
106
|
};
|
|
106
107
|
typeof ArrayBuffer == 'function' && typeof Uint8Array == 'function' && registry.push(ArrayBuffer, unifyArrayBuffer);
|
|
107
108
|
|
|
@@ -161,22 +162,28 @@ const mapOps = {
|
|
|
161
162
|
}
|
|
162
163
|
}
|
|
163
164
|
};
|
|
164
|
-
mapOps.exact.exact.compare =
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
165
|
+
mapOps.exact.exact.compare =
|
|
166
|
+
mapOps.exact.open.compare =
|
|
167
|
+
mapOps.exact.soft.compare =
|
|
168
|
+
(l, r, ls, rs) => {
|
|
169
|
+
for (const [key, value] of r) {
|
|
170
|
+
if (!l.has(key)) return false;
|
|
171
|
+
ls.push(l.get(key));
|
|
172
|
+
rs.push(value);
|
|
173
|
+
}
|
|
174
|
+
return true;
|
|
175
|
+
};
|
|
176
|
+
mapOps.open.open.compare =
|
|
177
|
+
mapOps.open.soft.compare =
|
|
178
|
+
mapOps.soft.soft.compare =
|
|
179
|
+
(l, r, ls, rs) => {
|
|
180
|
+
for (const [key, value] of r) {
|
|
181
|
+
if (!l.has(key)) continue;
|
|
182
|
+
ls.push(l.get(key));
|
|
183
|
+
rs.push(value);
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
};
|
|
180
187
|
mapOps.exact.soft.update = mapOps.open.soft.update = function () {
|
|
181
188
|
for (const [key, value] of this.l) {
|
|
182
189
|
!this.r.has(key) && this.r.set(key, value);
|
|
@@ -186,7 +193,19 @@ mapOps.exact.soft.update = mapOps.open.soft.update = function () {
|
|
|
186
193
|
const unifyMaps = (l, lt, lm, r, rt, rm, ls, rs, env) => {
|
|
187
194
|
const ols = ls;
|
|
188
195
|
if (lt > rt) {
|
|
189
|
-
|
|
196
|
+
let tmp;
|
|
197
|
+
tmp = l;
|
|
198
|
+
l = r;
|
|
199
|
+
r = tmp;
|
|
200
|
+
tmp = lt;
|
|
201
|
+
lt = rt;
|
|
202
|
+
rt = tmp;
|
|
203
|
+
tmp = lm;
|
|
204
|
+
lm = rm;
|
|
205
|
+
rm = tmp;
|
|
206
|
+
tmp = ls;
|
|
207
|
+
ls = rs;
|
|
208
|
+
rs = tmp;
|
|
190
209
|
}
|
|
191
210
|
const ops = mapOps[lt][rt];
|
|
192
211
|
if (ops.precheck && !ops.precheck(l, r)) return false;
|
|
@@ -231,8 +250,13 @@ const setOps = {
|
|
|
231
250
|
}
|
|
232
251
|
}
|
|
233
252
|
};
|
|
234
|
-
setOps.exact.exact.compare =
|
|
235
|
-
|
|
253
|
+
setOps.exact.exact.compare =
|
|
254
|
+
setOps.exact.open.compare =
|
|
255
|
+
setOps.exact.soft.compare =
|
|
256
|
+
setOps.open.open.compare =
|
|
257
|
+
setOps.open.soft.compare =
|
|
258
|
+
setOps.soft.soft.compare =
|
|
259
|
+
() => true;
|
|
236
260
|
setOps.exact.soft.update = setOps.open.soft.update = function () {
|
|
237
261
|
for (const key of this.l) {
|
|
238
262
|
this.r.add(key);
|
|
@@ -242,7 +266,19 @@ setOps.exact.soft.update = setOps.open.soft.update = function () {
|
|
|
242
266
|
const unifySets = (l, lt, lm, r, rt, rm, ls, rs, env) => {
|
|
243
267
|
const ols = ls;
|
|
244
268
|
if (lt > rt) {
|
|
245
|
-
|
|
269
|
+
let tmp;
|
|
270
|
+
tmp = l;
|
|
271
|
+
l = r;
|
|
272
|
+
r = tmp;
|
|
273
|
+
tmp = lt;
|
|
274
|
+
lt = rt;
|
|
275
|
+
rt = tmp;
|
|
276
|
+
tmp = lm;
|
|
277
|
+
lm = rm;
|
|
278
|
+
rm = tmp;
|
|
279
|
+
tmp = ls;
|
|
280
|
+
ls = rs;
|
|
281
|
+
rs = tmp;
|
|
246
282
|
}
|
|
247
283
|
const ops = setOps[lt][rt];
|
|
248
284
|
if (ops.precheck && !ops.precheck(l, r)) return false;
|
|
@@ -267,7 +303,10 @@ const objectOps = {
|
|
|
267
303
|
rKeys = rKeys.concat(Object.getOwnPropertySymbols(r));
|
|
268
304
|
}
|
|
269
305
|
if (lKeys.length != rKeys.length) return false;
|
|
270
|
-
|
|
306
|
+
for (let i = 0; i < lKeys.length; ++i) {
|
|
307
|
+
if (!hasOwnProperty.call(r, lKeys[i])) return false;
|
|
308
|
+
}
|
|
309
|
+
return true;
|
|
271
310
|
}
|
|
272
311
|
},
|
|
273
312
|
open: {},
|
|
@@ -298,29 +337,34 @@ const objectOps = {
|
|
|
298
337
|
}
|
|
299
338
|
}
|
|
300
339
|
};
|
|
301
|
-
objectOps.exact.exact.compare =
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
340
|
+
objectOps.exact.exact.compare =
|
|
341
|
+
objectOps.exact.open.compare =
|
|
342
|
+
objectOps.exact.soft.compare =
|
|
343
|
+
(l, r, ls, rs, env) => {
|
|
344
|
+
let keys = Object.keys(r);
|
|
345
|
+
if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
|
|
346
|
+
for (let i = 0; i < keys.length; ++i) {
|
|
347
|
+
const k = keys[i];
|
|
348
|
+
if (!hasOwnProperty.call(l, k)) return false;
|
|
349
|
+
ls.push(l[k]);
|
|
350
|
+
rs.push(r[k]);
|
|
351
|
+
}
|
|
308
352
|
return true;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
};
|
|
353
|
+
};
|
|
354
|
+
objectOps.open.open.compare =
|
|
355
|
+
objectOps.open.soft.compare =
|
|
356
|
+
objectOps.soft.soft.compare =
|
|
357
|
+
(l, r, ls, rs, env) => {
|
|
358
|
+
let keys = Object.keys(r);
|
|
359
|
+
if (env.symbols) keys = keys.concat(Object.getOwnPropertySymbols(r));
|
|
360
|
+
for (const k of keys) {
|
|
361
|
+
if (hasOwnProperty.call(l, k)) {
|
|
362
|
+
ls.push(l[k]);
|
|
363
|
+
rs.push(r[k]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return true;
|
|
367
|
+
};
|
|
324
368
|
objectOps.exact.soft.update = objectOps.open.soft.update = function () {
|
|
325
369
|
let keys = Object.keys(this.l);
|
|
326
370
|
if (this.e.symbols) keys = keys.concat(Object.getOwnPropertySymbols(this.l));
|
|
@@ -332,7 +376,19 @@ objectOps.exact.soft.update = objectOps.open.soft.update = function () {
|
|
|
332
376
|
const unifyObjects = (l, lt, lm, r, rt, rm, ls, rs, env) => {
|
|
333
377
|
const ols = ls;
|
|
334
378
|
if (lt > rt) {
|
|
335
|
-
|
|
379
|
+
let tmp;
|
|
380
|
+
tmp = l;
|
|
381
|
+
l = r;
|
|
382
|
+
r = tmp;
|
|
383
|
+
tmp = lt;
|
|
384
|
+
lt = rt;
|
|
385
|
+
rt = tmp;
|
|
386
|
+
tmp = lm;
|
|
387
|
+
lm = rm;
|
|
388
|
+
rm = tmp;
|
|
389
|
+
tmp = ls;
|
|
390
|
+
ls = rs;
|
|
391
|
+
rs = tmp;
|
|
336
392
|
}
|
|
337
393
|
const ops = objectOps[lt][rt];
|
|
338
394
|
if (ops.precheck && !ops.precheck(l, r, env)) return false;
|
|
@@ -354,36 +410,39 @@ const unify = (l, r, env, options) => {
|
|
|
354
410
|
env = Object.assign(env, options);
|
|
355
411
|
// options: openObjects, openArrays, openMaps, openSets, circular, loose, ignoreFunctions, signedZero, symbols.
|
|
356
412
|
const ls = [l],
|
|
357
|
-
rs = [r]
|
|
358
|
-
|
|
413
|
+
rs = [r];
|
|
414
|
+
let lSeen, rSeen;
|
|
415
|
+
if (env.circular) {
|
|
416
|
+
lSeen = new Map();
|
|
359
417
|
rSeen = new Map();
|
|
418
|
+
}
|
|
360
419
|
main: while (ls.length) {
|
|
361
420
|
// perform a command, or extract a pair
|
|
362
421
|
l = ls.pop();
|
|
363
|
-
if (l instanceof Command) {
|
|
422
|
+
if (typeof l == 'object' && l instanceof Command) {
|
|
364
423
|
l.f();
|
|
365
424
|
continue;
|
|
366
425
|
}
|
|
367
426
|
r = rs.pop();
|
|
368
427
|
// direct equality
|
|
369
428
|
if (l === r) {
|
|
370
|
-
if (
|
|
429
|
+
if (lSeen && l && typeof l == 'object' && lSeen.has(l) ^ rSeen.has(r)) return null;
|
|
371
430
|
if (env.signedZero && l === 0 && 1 / l !== 1 / r) return null;
|
|
372
431
|
continue;
|
|
373
432
|
}
|
|
374
433
|
// anyvar
|
|
375
434
|
if (l === _ || r === _) continue;
|
|
376
435
|
// process variables (variables have priority)
|
|
377
|
-
if (l instanceof Variable) {
|
|
436
|
+
if (typeof l == 'object' && l instanceof Variable) {
|
|
378
437
|
if (l.unify(r, ls, rs, env)) continue;
|
|
379
438
|
return null;
|
|
380
439
|
}
|
|
381
|
-
if (r instanceof Variable) {
|
|
440
|
+
if (typeof r == 'object' && r instanceof Variable) {
|
|
382
441
|
if (r.unify(l, ls, rs, env)) continue;
|
|
383
442
|
return null;
|
|
384
443
|
}
|
|
385
444
|
// process circular dependencies
|
|
386
|
-
if (
|
|
445
|
+
if (lSeen) {
|
|
387
446
|
const lIndex = lSeen.get(l);
|
|
388
447
|
if (typeof lIndex == 'number') {
|
|
389
448
|
if (lIndex === rSeen.get(r)) continue main;
|
|
@@ -396,11 +455,11 @@ const unify = (l, r, env, options) => {
|
|
|
396
455
|
r && typeof r == 'object' && rSeen.set(r, rSeen.size);
|
|
397
456
|
}
|
|
398
457
|
// invoke custom unifiers
|
|
399
|
-
if (l instanceof Unifier) {
|
|
458
|
+
if (typeof l == 'object' && l instanceof Unifier) {
|
|
400
459
|
if (l.unify(r, ls, rs, env)) continue;
|
|
401
460
|
return null;
|
|
402
461
|
}
|
|
403
|
-
if (r instanceof Unifier) {
|
|
462
|
+
if (typeof r == 'object' && r instanceof Unifier) {
|
|
404
463
|
if (r.unify(l, ls, rs, env)) continue;
|
|
405
464
|
return null;
|
|
406
465
|
}
|
|
@@ -414,12 +473,17 @@ const unify = (l, r, env, options) => {
|
|
|
414
473
|
if (typeof l == 'number' && isNaN(l) && isNaN(r)) continue;
|
|
415
474
|
// cut off impossible combinations
|
|
416
475
|
if (typeof l != 'object' || !l || !r) return null;
|
|
417
|
-
//
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
476
|
+
// fast path: plain objects and arrays skip registry
|
|
477
|
+
const lp = Object.getPrototypeOf(l),
|
|
478
|
+
rp = Object.getPrototypeOf(r);
|
|
479
|
+
if (!((lp === Object.prototype || lp === null || lp === Array.prototype) && (rp === Object.prototype || rp === null || rp === Array.prototype))) {
|
|
480
|
+
// process registered constructors
|
|
481
|
+
const registry = unify.registry;
|
|
482
|
+
for (let i = 0; i < registry.length; i += 2) {
|
|
483
|
+
if (l instanceof registry[i] || r instanceof registry[i]) {
|
|
484
|
+
if (registry[i + 1](l, r, ls, rs, env)) continue main;
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
423
487
|
}
|
|
424
488
|
}
|
|
425
489
|
// process registered filters
|
|
@@ -442,5 +506,5 @@ const unify = (l, r, env, options) => {
|
|
|
442
506
|
unify.registry = registry;
|
|
443
507
|
unify.filters = filters;
|
|
444
508
|
|
|
445
|
-
export {_, Env, Unifier, isUnifier, Variable, variable, isVariable, _ as any, open, soft, isOpen, isSoft, isWrapped};
|
|
509
|
+
export {_, Env, Unifier, isUnifier, Variable, variable, isVariable, _ as any, open, soft, isOpen, isSoft, isWrapped, unify};
|
|
446
510
|
export default unify;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Type definitions for deep6 replaceVars utility
|
|
2
|
+
// Generated from src/utils/replaceVars.js
|
|
3
|
+
|
|
4
|
+
import type {Env, Variable} from '../env.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Creates a tagged template function that substitutes variables from an environment
|
|
8
|
+
*
|
|
9
|
+
* Template interpolations can be Variable instances (resolved via `.get(env)`),
|
|
10
|
+
* strings/numbers/symbols (looked up in `env.values`), or other values (coerced to string).
|
|
11
|
+
*
|
|
12
|
+
* @param env - Unification environment with variable bindings
|
|
13
|
+
* @returns A tagged template literal function
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const x = variable('x');
|
|
18
|
+
* const env = unify({a: x}, {a: 42});
|
|
19
|
+
* const t = replaceVars(env);
|
|
20
|
+
* t`The answer is ${x}!`; // "The answer is 42!"
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const replaceVars: (env: Env) => (strings: TemplateStringsArray, ...vars: unknown[]) => string;
|
|
24
|
+
|
|
25
|
+
export default replaceVars;
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import {isVariable} from '../env.js';
|
|
2
2
|
|
|
3
|
-
const replaceVars =
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
3
|
+
const replaceVars =
|
|
4
|
+
env =>
|
|
5
|
+
(strings, ...vars) => {
|
|
6
|
+
let buffer = strings[0];
|
|
7
|
+
for (let i = 0; i < vars.length; ) {
|
|
8
|
+
const v = vars[i];
|
|
9
|
+
if (isVariable(v)) {
|
|
10
|
+
buffer += v.get(env);
|
|
11
|
+
} else
|
|
12
|
+
switch (typeof v) {
|
|
13
|
+
case 'string':
|
|
14
|
+
case 'number':
|
|
15
|
+
case 'symbol':
|
|
16
|
+
buffer += env.values[v];
|
|
17
|
+
break;
|
|
18
|
+
default:
|
|
19
|
+
buffer += v;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
buffer += strings[++i];
|
|
18
23
|
}
|
|
19
|
-
buffer
|
|
20
|
-
}
|
|
21
|
-
return buffer;
|
|
22
|
-
};
|
|
24
|
+
return buffer;
|
|
25
|
+
};
|
|
23
26
|
|
|
27
|
+
export {replaceVars};
|
|
24
28
|
export default replaceVars;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type {IncomingMessage} from 'node:http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Anything `asText` / `asJson` / `asBytes` / `header` / `headers` accept.
|
|
5
|
+
* Either a W3C `Response` (from `fetch`) or a Node `http.IncomingMessage`.
|
|
6
|
+
*/
|
|
7
|
+
export type ResponseLike = Response | IncomingMessage;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Read the response body as a UTF-8 string. Works with both `Response`
|
|
11
|
+
* (delegates to `res.text()`) and `http.IncomingMessage` (drains the
|
|
12
|
+
* stream and decodes).
|
|
13
|
+
*/
|
|
14
|
+
export function asText(res: ResponseLike): Promise<string>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read the response body and parse as JSON. Equivalent to
|
|
18
|
+
* `JSON.parse(await asText(res))`; uses `res.json()` directly when
|
|
19
|
+
* available.
|
|
20
|
+
*/
|
|
21
|
+
export function asJson<T = unknown>(res: ResponseLike): Promise<T>;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the response body as raw bytes (`Uint8Array`). For
|
|
25
|
+
* `http.IncomingMessage`, drains the stream; for `Response`, delegates
|
|
26
|
+
* to `arrayBuffer()`.
|
|
27
|
+
*/
|
|
28
|
+
export function asBytes(res: ResponseLike): Promise<Uint8Array>;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read a single response header by name (case-insensitive). Returns
|
|
32
|
+
* `null` if the header isn't present (matching `Response.headers.get`
|
|
33
|
+
* semantics). For `http.IncomingMessage` headers that are arrays
|
|
34
|
+
* (e.g. `set-cookie`), values are joined with `, `.
|
|
35
|
+
*/
|
|
36
|
+
export function header(res: ResponseLike, name: string): string | null;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return all response headers as a plain object with lowercase keys.
|
|
40
|
+
* Array-valued headers (e.g. `set-cookie` on `IncomingMessage`) are
|
|
41
|
+
* joined with `, ` to match `Response.headers` shape.
|
|
42
|
+
*/
|
|
43
|
+
export function headers(res: ResponseLike): Record<string, string>;
|
package/src/response.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @ts-self-types="./response.d.ts"
|
|
2
|
+
|
|
3
|
+
const isWebResponse = res =>
|
|
4
|
+
typeof res?.text === 'function' && typeof res?.headers?.get === 'function';
|
|
5
|
+
|
|
6
|
+
const drainAsBuffer = res =>
|
|
7
|
+
new Promise((resolve, reject) => {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
10
|
+
res.on('end', () => {
|
|
11
|
+
try {
|
|
12
|
+
resolve(Buffer.concat(chunks));
|
|
13
|
+
} catch (error) {
|
|
14
|
+
reject(error);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
res.on('error', reject);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const asText = async res => {
|
|
21
|
+
if (isWebResponse(res)) return await res.text();
|
|
22
|
+
const buffer = await drainAsBuffer(res);
|
|
23
|
+
return buffer.toString('utf8');
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const asJson = async res => {
|
|
27
|
+
if (isWebResponse(res)) return await res.json();
|
|
28
|
+
return JSON.parse(await asText(res));
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const asBytes = async res => {
|
|
32
|
+
if (isWebResponse(res)) {
|
|
33
|
+
const ab = await res.arrayBuffer();
|
|
34
|
+
return new Uint8Array(ab);
|
|
35
|
+
}
|
|
36
|
+
const buffer = await drainAsBuffer(res);
|
|
37
|
+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const header = (res, name) => {
|
|
41
|
+
if (isWebResponse(res)) return res.headers.get(name);
|
|
42
|
+
const value = res.headers?.[name.toLowerCase()];
|
|
43
|
+
return value === undefined ? null : Array.isArray(value) ? value.join(', ') : value;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const headers = res => {
|
|
47
|
+
const out = {};
|
|
48
|
+
if (isWebResponse(res)) {
|
|
49
|
+
for (const [k, v] of res.headers) out[k.toLowerCase()] = v;
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
if (!res.headers) return out;
|
|
53
|
+
for (const [k, v] of Object.entries(res.headers)) {
|
|
54
|
+
out[k.toLowerCase()] = Array.isArray(v) ? v.join(', ') : v;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
};
|
package/src/server.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type {RequestListener, Server} from 'node:http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of starting a server. Properties reflect the actual bound address
|
|
5
|
+
* (e.g. `port` is the OS-assigned port when `0` was requested).
|
|
6
|
+
*/
|
|
7
|
+
export interface ServerLifecycle {
|
|
8
|
+
/** The underlying `http.Server` instance. */
|
|
9
|
+
server: Server;
|
|
10
|
+
/** Base URL of the bound server, e.g. `"http://127.0.0.1:54321"`. */
|
|
11
|
+
base: string;
|
|
12
|
+
/** Actual port the server is listening on. */
|
|
13
|
+
port: number;
|
|
14
|
+
/** Host the server is bound to. */
|
|
15
|
+
host: string;
|
|
16
|
+
/**
|
|
17
|
+
* Close the server. Idempotent. Calls `server.closeAllConnections()`
|
|
18
|
+
* (when available) so keep-alive sockets don't delay teardown.
|
|
19
|
+
*/
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for `startServer` / `withServer` / `setupServer`.
|
|
25
|
+
*/
|
|
26
|
+
export interface ServerOptions {
|
|
27
|
+
/**
|
|
28
|
+
* Host to bind to. Defaults to `'127.0.0.1'` (explicit IPv4 — avoids
|
|
29
|
+
* dual-stack ambiguity on macOS where `'localhost'` may resolve to `::1`).
|
|
30
|
+
*/
|
|
31
|
+
host?: string;
|
|
32
|
+
/** Port to bind to. Defaults to `0` (OS-assigned). */
|
|
33
|
+
port?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Procedural primitive: start an `http.Server`, return a lifecycle handle.
|
|
38
|
+
*
|
|
39
|
+
* Races `'listening'` against `'error'` — port-busy / `EACCES` rejects,
|
|
40
|
+
* never hangs.
|
|
41
|
+
*/
|
|
42
|
+
export function startServer(server: Server, opts?: ServerOptions): Promise<ServerLifecycle>;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Scoped resource: spin up a server with `serverHandler`, run `clientHandler`
|
|
46
|
+
* with the base URL, tear the server down in `finally`. Cleanup runs whether
|
|
47
|
+
* `clientHandler` resolves, rejects, or throws synchronously.
|
|
48
|
+
*
|
|
49
|
+
* `serverHandler` is the per-request callback (Node calls it once per
|
|
50
|
+
* incoming request). `clientHandler` is the per-scope test body (called once,
|
|
51
|
+
* with the base URL). Either side may be the SUT — naming reflects role on
|
|
52
|
+
* the wire, not which side is being tested.
|
|
53
|
+
*/
|
|
54
|
+
export function withServer<T>(
|
|
55
|
+
serverHandler: RequestListener,
|
|
56
|
+
clientHandler: (base: string, lifecycle: ServerLifecycle) => Promise<T> | T,
|
|
57
|
+
opts?: ServerOptions
|
|
58
|
+
): Promise<T>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Hook-registering helper for suite-shared servers. Registers `beforeAll` to
|
|
62
|
+
* start the server and `afterAll` to close it; returns a live-getter context
|
|
63
|
+
* whose properties reflect the running server's state at access time.
|
|
64
|
+
*
|
|
65
|
+
* Per-test state reset (e.g. clearing a `recorded[]` array) stays user-side:
|
|
66
|
+
* compose your own `beforeEach`. `setupServer` owns the suite lifecycle; the
|
|
67
|
+
* caller owns suite state.
|
|
68
|
+
*
|
|
69
|
+
* Don't destructure the returned object at module load time — its properties
|
|
70
|
+
* read the live lifecycle on each access; destructuring captures stale
|
|
71
|
+
* `undefined` values.
|
|
72
|
+
*/
|
|
73
|
+
export function setupServer(
|
|
74
|
+
serverHandler: RequestListener,
|
|
75
|
+
opts?: ServerOptions
|
|
76
|
+
): Readonly<{
|
|
77
|
+
readonly server: Server | undefined;
|
|
78
|
+
readonly base: string | undefined;
|
|
79
|
+
readonly port: number | undefined;
|
|
80
|
+
readonly host: string | undefined;
|
|
81
|
+
}>;
|
package/src/server.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// @ts-self-types="./server.d.ts"
|
|
2
|
+
|
|
3
|
+
import http from 'node:http';
|
|
4
|
+
import {once} from 'node:events';
|
|
5
|
+
|
|
6
|
+
import {beforeAll, afterAll} from './test.js';
|
|
7
|
+
|
|
8
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
9
|
+
|
|
10
|
+
export const startServer = async (server, {host = DEFAULT_HOST, port = 0} = {}) => {
|
|
11
|
+
const listening = once(server, 'listening');
|
|
12
|
+
const failure = once(server, 'error').then(([err]) => {
|
|
13
|
+
throw err;
|
|
14
|
+
});
|
|
15
|
+
server.listen(port, host);
|
|
16
|
+
await Promise.race([listening, failure]);
|
|
17
|
+
|
|
18
|
+
const addr = server.address();
|
|
19
|
+
const actualPort = typeof addr === 'object' && addr ? addr.port : port;
|
|
20
|
+
const base = `http://${host}:${actualPort}`;
|
|
21
|
+
|
|
22
|
+
let closed = false;
|
|
23
|
+
const close = async () => {
|
|
24
|
+
if (closed) return;
|
|
25
|
+
closed = true;
|
|
26
|
+
server.closeAllConnections?.();
|
|
27
|
+
server.close();
|
|
28
|
+
await once(server, 'close');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
return {server, base, port: actualPort, host, close};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const withServer = async (serverHandler, clientHandler, opts) => {
|
|
35
|
+
const server = http.createServer(serverHandler);
|
|
36
|
+
const lifecycle = await startServer(server, opts);
|
|
37
|
+
try {
|
|
38
|
+
return await clientHandler(lifecycle.base, lifecycle);
|
|
39
|
+
} finally {
|
|
40
|
+
await lifecycle.close();
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const setupServer = (serverHandler, opts) => {
|
|
45
|
+
let lifecycle = null;
|
|
46
|
+
beforeAll(async () => {
|
|
47
|
+
lifecycle = await startServer(http.createServer(serverHandler), opts);
|
|
48
|
+
});
|
|
49
|
+
afterAll(async () => {
|
|
50
|
+
if (lifecycle) {
|
|
51
|
+
await lifecycle.close();
|
|
52
|
+
lifecycle = null;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
return Object.freeze({
|
|
56
|
+
get server() {
|
|
57
|
+
return lifecycle?.server;
|
|
58
|
+
},
|
|
59
|
+
get base() {
|
|
60
|
+
return lifecycle?.base;
|
|
61
|
+
},
|
|
62
|
+
get port() {
|
|
63
|
+
return lifecycle?.port;
|
|
64
|
+
},
|
|
65
|
+
get host() {
|
|
66
|
+
return lifecycle?.host;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|