@orkestrel/test 0.0.3 → 0.0.5
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 +59 -12
- package/dist/src/core/index.cjs +83 -0
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +68 -0
- package/dist/src/core/index.d.ts +68 -0
- package/dist/src/core/index.js +82 -1
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +31 -0
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +29 -0
- package/dist/src/server/index.d.ts +29 -0
- package/dist/src/server/index.js +31 -1
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
The test helpers the `@orkestrel` fleet kept rewriting, published once. A call recorder that is a
|
|
4
4
|
real callback rather than a spy. A real host delay. A throw-to-value converter and a presence
|
|
5
|
-
narrower, so `!` and `as` stay banned in tests. Two async collectors and a JSON copier. A
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
narrower, so `!` and `as` stay banned in tests. Two async collectors and a JSON copier. A frozen
|
|
6
|
+
hostile-value corpus for proving guards are total. A cleanup list that gives every owned resource
|
|
7
|
+
back, newest first. A scratch directory the test owns and destroys, a loopback port for a server the
|
|
8
|
+
test built, and a symlink-refusing source-file walker. A helper ships here only when enough packages
|
|
9
|
+
had already written their own; the guide's
|
|
8
10
|
[Limits](guides/test.md#limits) section states that rule and what it excluded. Add it as a
|
|
9
11
|
devDependency; nothing here runs in production code. Part of the `@orkestrel` line.
|
|
10
12
|
|
|
@@ -29,11 +31,19 @@ npm install -D @orkestrel/test
|
|
|
29
31
|
touches neither `node:*` nor the DOM, so a browser test project imports it unchanged.
|
|
30
32
|
|
|
31
33
|
```ts
|
|
32
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
captureError,
|
|
36
|
+
createRecorder,
|
|
37
|
+
createTeardown,
|
|
38
|
+
requireValue,
|
|
39
|
+
waitForDelay,
|
|
40
|
+
} from '@orkestrel/test'
|
|
33
41
|
import { createScratch } from '@orkestrel/test/server'
|
|
34
42
|
|
|
35
|
-
// A
|
|
43
|
+
// A cleanup list the test adds to as it goes, and a temporary directory seeded with the input.
|
|
44
|
+
const teardown = createTeardown()
|
|
36
45
|
const scratch = createScratch({ prefix: 'loader-', files: { 'input.txt': 'hello' } })
|
|
46
|
+
teardown.add(() => scratch.destroy()) // idempotent, and it removes only the directory it allocated
|
|
37
47
|
|
|
38
48
|
// A real callback, not a spy: hand `handler` to the code under test.
|
|
39
49
|
const recorder = createRecorder<[path: string]>()
|
|
@@ -51,19 +61,26 @@ captureError(() => loader.read('missing.txt')) // the thrown value, or undefined
|
|
|
51
61
|
requireValue(scratch.read('input.txt')) // 'hello' — narrows `string | undefined` without `!`
|
|
52
62
|
|
|
53
63
|
scratch.remove('input.txt') // one contained entry, subtree and all; a missing target is a no-op
|
|
54
|
-
|
|
64
|
+
await teardown.destroy() // newest registration first, and every handler runs even after one throws
|
|
55
65
|
```
|
|
56
66
|
|
|
67
|
+
The list registers no hook of its own. The consumer writes `afterEach(() => teardown.destroy())`
|
|
68
|
+
once, in its own setup, because registering it here would take a runtime dependency on the test
|
|
69
|
+
runner. A handler that throws does not stop the run: one failure is rethrown by identity, so the
|
|
70
|
+
test can assert on the value it threw, and several arrive as an `AggregateError` in run order.
|
|
71
|
+
|
|
57
72
|
The rest of core is `collect` (drains an async iterable), `collectStream` (drains a
|
|
58
73
|
`ReadableStream`), `roundTripJSON` (copies any value `JSONSafe` accepts, including an
|
|
59
|
-
interface-typed one, and throws rather than turning a non-finite number into `null`),
|
|
60
|
-
|
|
74
|
+
interface-typed one, and throws rather than turning a non-finite number into `null`), `resolveRoot`
|
|
75
|
+
(the directory above the calling module, from `import.meta`), and `createHostileValues` (a frozen
|
|
76
|
+
array of fresh values that each make a naive reader throw).
|
|
61
77
|
|
|
62
78
|
The server face adds `readInventory`, which reads a checkout into a map of root-relative path to
|
|
63
|
-
file text that a parity suite can assert against,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
removes
|
|
79
|
+
file text that a parity suite can assert against, and `createLoopback`, which binds a server the
|
|
80
|
+
test built to an ephemeral loopback port. Behind `readInventory` and `createScratch` sit three pure
|
|
81
|
+
leaves: `resolveContained`, the lexical check both refuse escapes with; `isExcluded`, the exclusion
|
|
82
|
+
rule the walk applies; and `matchesIdentity`, the comparison `destroy()` makes before it removes
|
|
83
|
+
anything.
|
|
67
84
|
|
|
68
85
|
```ts
|
|
69
86
|
import { resolveRoot } from '@orkestrel/test'
|
|
@@ -124,6 +141,36 @@ leaves the root through a link in the middle, and skips a symlink met while walk
|
|
|
124
141
|
sandbox against hostile filesystem content: those refusals stop accidental escape, not an adversary
|
|
125
142
|
who can create hard links where the test process already writes.
|
|
126
143
|
|
|
144
|
+
`createLoopback` binds a server the test built. The caller constructs it and keeps every route,
|
|
145
|
+
header, and status on it; this package supplies the bind and the release and nothing else.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import { createServer } from 'node:http'
|
|
149
|
+
import { createLoopback } from '@orkestrel/test/server'
|
|
150
|
+
|
|
151
|
+
const server = createServer((_request, response) => {
|
|
152
|
+
response.end('ok')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const loopback = await createLoopback(server)
|
|
156
|
+
|
|
157
|
+
loopback.port // the number the host assigned; this package neither picks a port nor reserves one
|
|
158
|
+
loopback.url // `http://127.0.0.1:${loopback.port}` — IPv4 loopback, no trailing slash
|
|
159
|
+
|
|
160
|
+
const response = await fetch(loopback.url) // a real request, answered by the caller's own server
|
|
161
|
+
await response.text() // 'ok'
|
|
162
|
+
|
|
163
|
+
await loopback.destroy() // drops every live connection, then closes; idempotent
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
It listens on port `0` at `127.0.0.1`, so the host assigns the number and the address is IPv4
|
|
167
|
+
loopback rather than `::1`, which a host resolving `localhost` can hand back instead. The scheme in
|
|
168
|
+
`url` is spelled `http` unconditionally, so a TLS server's origin is `port` plus a scheme the caller
|
|
169
|
+
writes. `destroy()` drops live connections on a server that carries `closeAllConnections` — both
|
|
170
|
+
`node:http`'s and `node:https`'s do — so a keep-alive client cannot hold the port past the test that
|
|
171
|
+
opened it. A plain `node:net` server has no such method, so `destroy()` waits for its open sockets
|
|
172
|
+
to end.
|
|
173
|
+
|
|
127
174
|
## Guide
|
|
128
175
|
|
|
129
176
|
For the full surface — every export, the behavioral contract each one holds to, and the measured
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -105,6 +105,59 @@ function resolveRoot(meta) {
|
|
|
105
105
|
//#endregion
|
|
106
106
|
//#region src/core/factories.ts
|
|
107
107
|
/**
|
|
108
|
+
* Creates values that make common object readers throw or violate their assumptions.
|
|
109
|
+
*
|
|
110
|
+
* @returns A frozen array whose six values are fresh on every call.
|
|
111
|
+
* @remarks Every member makes a naive reader throw. A total guard survives every member without
|
|
112
|
+
* throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in
|
|
113
|
+
* a release, so test the whole returned set in a loop and include the index in each failure.
|
|
114
|
+
* @example
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { expect } from 'vitest'
|
|
117
|
+
* import { createHostileValues } from '@orkestrel/test'
|
|
118
|
+
*
|
|
119
|
+
* function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {
|
|
120
|
+
* if (typeof value !== 'object' || value === null) return false
|
|
121
|
+
* try {
|
|
122
|
+
* if (Object.getPrototypeOf(value) !== Object.prototype) return false
|
|
123
|
+
* Reflect.get(value, 'value')
|
|
124
|
+
* if (Reflect.ownKeys(value).length === 0) return false
|
|
125
|
+
* return Object.values(value).every((member) => typeof member === 'string')
|
|
126
|
+
* } catch {
|
|
127
|
+
* return false
|
|
128
|
+
* }
|
|
129
|
+
* }
|
|
130
|
+
*
|
|
131
|
+
* for (const [index, value] of createHostileValues().entries()) {
|
|
132
|
+
* let accepted: boolean | undefined
|
|
133
|
+
* expect(() => {
|
|
134
|
+
* accepted = isWireRecord(value)
|
|
135
|
+
* }, `hostile value ${index}`).not.toThrow()
|
|
136
|
+
* expect(accepted, `hostile value ${index}`).toBe(false)
|
|
137
|
+
* }
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
function createHostileValues() {
|
|
141
|
+
const cyclic = {};
|
|
142
|
+
cyclic.self = cyclic;
|
|
143
|
+
const revoked = Proxy.revocable({}, {});
|
|
144
|
+
revoked.revoke();
|
|
145
|
+
return Object.freeze([
|
|
146
|
+
cyclic,
|
|
147
|
+
revoked.proxy,
|
|
148
|
+
new Proxy({}, { get() {
|
|
149
|
+
throw new Error("Hostile property read");
|
|
150
|
+
} }),
|
|
151
|
+
new Proxy({}, { ownKeys() {
|
|
152
|
+
throw new Error("Hostile key enumeration");
|
|
153
|
+
} }),
|
|
154
|
+
new Proxy({}, { getPrototypeOf() {
|
|
155
|
+
throw new Error("Hostile prototype read");
|
|
156
|
+
} }),
|
|
157
|
+
Object.create(null)
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
108
161
|
* Creates a recorder for callback arguments.
|
|
109
162
|
*
|
|
110
163
|
* @typeParam TArgs - The argument tuple to record.
|
|
@@ -125,11 +178,41 @@ function createRecorder() {
|
|
|
125
178
|
}
|
|
126
179
|
};
|
|
127
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Creates a teardown list that runs registered handlers newest-first.
|
|
183
|
+
*
|
|
184
|
+
* @returns A teardown list that awaits every handler and collects failures.
|
|
185
|
+
*/
|
|
186
|
+
function createTeardown() {
|
|
187
|
+
let handlers = [];
|
|
188
|
+
return {
|
|
189
|
+
get count() {
|
|
190
|
+
return handlers.length;
|
|
191
|
+
},
|
|
192
|
+
add(handler) {
|
|
193
|
+
handlers.push(handler);
|
|
194
|
+
},
|
|
195
|
+
async destroy() {
|
|
196
|
+
const snapshot = handlers;
|
|
197
|
+
handlers = [];
|
|
198
|
+
const failures = [];
|
|
199
|
+
for (const handler of snapshot.reverse()) try {
|
|
200
|
+
await handler();
|
|
201
|
+
} catch (error) {
|
|
202
|
+
failures.push(error);
|
|
203
|
+
}
|
|
204
|
+
if (failures.length === 1) throw failures[0];
|
|
205
|
+
if (failures.length > 1) throw new AggregateError(failures);
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
128
209
|
//#endregion
|
|
129
210
|
exports.captureError = captureError;
|
|
130
211
|
exports.collect = collect;
|
|
131
212
|
exports.collectStream = collectStream;
|
|
213
|
+
exports.createHostileValues = createHostileValues;
|
|
132
214
|
exports.createRecorder = createRecorder;
|
|
215
|
+
exports.createTeardown = createTeardown;
|
|
133
216
|
exports.requireValue = requireValue;
|
|
134
217
|
exports.resolveRoot = resolveRoot;
|
|
135
218
|
exports.roundTripJSON = roundTripJSON;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONSafe } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface } from './types.js'\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,aAAa,KAAK,GAAkB;CACnD,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACxD;;;;;;;AAQA,SAAgB,aAAa,OAA+B;CAC3D,IAAI;EACH,MAAM;CACP,SAAS,OAAO;EACf,OAAO;CACR;AAED;;;;;;;;;AAUA,SAAgB,aAAgB,OAA6B,UAAU,qBAAwB;CAC9F,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,MAAM,IAAI,MAAM,OAAO;CAClE,OAAO;AACR;;;;;;;;AASA,eAAsB,QAAW,QAAiD;CACjF,MAAM,SAAc,CAAC;CACrB,WAAW,MAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;CACnD,OAAO;AACR;;;;;;;;AASA,eAAsB,cAAiB,QAAkD;CACxF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAc,CAAC;CACrB,IAAI;EACH,OAAO,MAAM;GACZ,MAAM,SAAS,MAAM,OAAO,KAAK;GACjC,IAAI,OAAO,MAAM,OAAO;GACxB,OAAO,KAAK,OAAO,KAAK;EACzB;CACD,UAAU;EACT,OAAO,YAAY;CACpB;AACD;;;;;;;;;;;AAYA,SAAgB,cAAiB,OAA2B;CAC3D,MAAM,aAAa,KAAK,UAAU,QAAQ,MAAM,YAAY;EAC3D,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,cAAc,OAAO,YAAY,UAChF,MAAM,IAAI,MAAM,+DAA+D;EAEhF,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,OAAO;CACR,CAAC;CACD,MAAM,SAAY,KAAK,MAAM,UAAU;CACvC,MAAM,UAAqB,CAAC,MAAM;CAClC,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,UAAU,QAAQ,IAAI;EAC5B,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,IAAI,MAAM,QAAQ,OAAO,GACxB,KAAK,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;OACzC,IAAI,OAAO,YAAY,YAAY,YAAY,MACrD,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,QAAQ,KAAK,KAAK;CAEhE;CACA,OAAO;AACR;;;;;;;;AASA,SAAgB,YAAY,MAAuB;CAClD,OAAO,IAAI,IAAI,OAAO,KAAK,GAAG;AAC/B
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONSafe } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface, TeardownHandler, TeardownInterface } from './types.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose six values are fresh on every call.\n * @remarks Every member makes a naive reader throw. A total guard survives every member without\n * throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in\n * a release, so test the whole returned set in a loop and include the index in each failure.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t])\n}\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -24,6 +24,41 @@ export declare function collect<T>(source: AsyncIterable<T>): Promise<readonly T
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]>;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Creates values that make common object readers throw or violate their assumptions.
|
|
29
|
+
*
|
|
30
|
+
* @returns A frozen array whose six values are fresh on every call.
|
|
31
|
+
* @remarks Every member makes a naive reader throw. A total guard survives every member without
|
|
32
|
+
* throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in
|
|
33
|
+
* a release, so test the whole returned set in a loop and include the index in each failure.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { expect } from 'vitest'
|
|
37
|
+
* import { createHostileValues } from '@orkestrel/test'
|
|
38
|
+
*
|
|
39
|
+
* function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {
|
|
40
|
+
* if (typeof value !== 'object' || value === null) return false
|
|
41
|
+
* try {
|
|
42
|
+
* if (Object.getPrototypeOf(value) !== Object.prototype) return false
|
|
43
|
+
* Reflect.get(value, 'value')
|
|
44
|
+
* if (Reflect.ownKeys(value).length === 0) return false
|
|
45
|
+
* return Object.values(value).every((member) => typeof member === 'string')
|
|
46
|
+
* } catch {
|
|
47
|
+
* return false
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* for (const [index, value] of createHostileValues().entries()) {
|
|
52
|
+
* let accepted: boolean | undefined
|
|
53
|
+
* expect(() => {
|
|
54
|
+
* accepted = isWireRecord(value)
|
|
55
|
+
* }, `hostile value ${index}`).not.toThrow()
|
|
56
|
+
* expect(accepted, `hostile value ${index}`).toBe(false)
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function createHostileValues(): readonly unknown[];
|
|
61
|
+
|
|
27
62
|
/**
|
|
28
63
|
* Creates a recorder for callback arguments.
|
|
29
64
|
*
|
|
@@ -32,6 +67,13 @@ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<rea
|
|
|
32
67
|
*/
|
|
33
68
|
export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
|
|
34
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Creates a teardown list that runs registered handlers newest-first.
|
|
72
|
+
*
|
|
73
|
+
* @returns A teardown list that awaits every handler and collects failures.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createTeardown(): TeardownInterface;
|
|
76
|
+
|
|
35
77
|
/**
|
|
36
78
|
* The JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every
|
|
37
79
|
* member it does not, mapped to `never`.
|
|
@@ -117,6 +159,32 @@ export declare function resolveRoot(meta: ImportMeta): URL;
|
|
|
117
159
|
*/
|
|
118
160
|
export declare function roundTripJSON<T>(value: T & JSONSafe<T>): T;
|
|
119
161
|
|
|
162
|
+
/** The work one teardown entry performs when the list is destroyed. */
|
|
163
|
+
export declare type TeardownHandler = () => void | Promise<void>;
|
|
164
|
+
|
|
165
|
+
/** The cleanup a test adds as it goes and runs once, newest first, when it is done. */
|
|
166
|
+
export declare interface TeardownInterface {
|
|
167
|
+
/** How many handlers are registered. */
|
|
168
|
+
readonly count: number;
|
|
169
|
+
/**
|
|
170
|
+
* Registers a handler to run when the list is destroyed.
|
|
171
|
+
*
|
|
172
|
+
* @param handler - The work to perform.
|
|
173
|
+
*/
|
|
174
|
+
add(handler: TeardownHandler): void;
|
|
175
|
+
/**
|
|
176
|
+
* Runs every registered handler in reverse registration order, awaiting each in turn, and empties
|
|
177
|
+
* the list.
|
|
178
|
+
*
|
|
179
|
+
* @throws The value the failed handler threw, by identity, when exactly one handler failed. An
|
|
180
|
+
* `AggregateError` carrying every thrown value in run order, when several did.
|
|
181
|
+
* @remarks Every handler runs, including after an earlier one throws or rejects. A handler
|
|
182
|
+
* registered while the run is in progress stays registered for the next call rather than joining
|
|
183
|
+
* this one. The snapshot it ran is discarded, so a repeated call runs nothing that already ran.
|
|
184
|
+
*/
|
|
185
|
+
destroy(): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
|
|
120
188
|
/**
|
|
121
189
|
* Waits for a host timer to elapse.
|
|
122
190
|
*
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -24,6 +24,41 @@ export declare function collect<T>(source: AsyncIterable<T>): Promise<readonly T
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]>;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Creates values that make common object readers throw or violate their assumptions.
|
|
29
|
+
*
|
|
30
|
+
* @returns A frozen array whose six values are fresh on every call.
|
|
31
|
+
* @remarks Every member makes a naive reader throw. A total guard survives every member without
|
|
32
|
+
* throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in
|
|
33
|
+
* a release, so test the whole returned set in a loop and include the index in each failure.
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { expect } from 'vitest'
|
|
37
|
+
* import { createHostileValues } from '@orkestrel/test'
|
|
38
|
+
*
|
|
39
|
+
* function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {
|
|
40
|
+
* if (typeof value !== 'object' || value === null) return false
|
|
41
|
+
* try {
|
|
42
|
+
* if (Object.getPrototypeOf(value) !== Object.prototype) return false
|
|
43
|
+
* Reflect.get(value, 'value')
|
|
44
|
+
* if (Reflect.ownKeys(value).length === 0) return false
|
|
45
|
+
* return Object.values(value).every((member) => typeof member === 'string')
|
|
46
|
+
* } catch {
|
|
47
|
+
* return false
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* for (const [index, value] of createHostileValues().entries()) {
|
|
52
|
+
* let accepted: boolean | undefined
|
|
53
|
+
* expect(() => {
|
|
54
|
+
* accepted = isWireRecord(value)
|
|
55
|
+
* }, `hostile value ${index}`).not.toThrow()
|
|
56
|
+
* expect(accepted, `hostile value ${index}`).toBe(false)
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function createHostileValues(): readonly unknown[];
|
|
61
|
+
|
|
27
62
|
/**
|
|
28
63
|
* Creates a recorder for callback arguments.
|
|
29
64
|
*
|
|
@@ -32,6 +67,13 @@ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<rea
|
|
|
32
67
|
*/
|
|
33
68
|
export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
|
|
34
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Creates a teardown list that runs registered handlers newest-first.
|
|
72
|
+
*
|
|
73
|
+
* @returns A teardown list that awaits every handler and collects failures.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createTeardown(): TeardownInterface;
|
|
76
|
+
|
|
35
77
|
/**
|
|
36
78
|
* The JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every
|
|
37
79
|
* member it does not, mapped to `never`.
|
|
@@ -117,6 +159,32 @@ export declare function resolveRoot(meta: ImportMeta): URL;
|
|
|
117
159
|
*/
|
|
118
160
|
export declare function roundTripJSON<T>(value: T & JSONSafe<T>): T;
|
|
119
161
|
|
|
162
|
+
/** The work one teardown entry performs when the list is destroyed. */
|
|
163
|
+
export declare type TeardownHandler = () => void | Promise<void>;
|
|
164
|
+
|
|
165
|
+
/** The cleanup a test adds as it goes and runs once, newest first, when it is done. */
|
|
166
|
+
export declare interface TeardownInterface {
|
|
167
|
+
/** How many handlers are registered. */
|
|
168
|
+
readonly count: number;
|
|
169
|
+
/**
|
|
170
|
+
* Registers a handler to run when the list is destroyed.
|
|
171
|
+
*
|
|
172
|
+
* @param handler - The work to perform.
|
|
173
|
+
*/
|
|
174
|
+
add(handler: TeardownHandler): void;
|
|
175
|
+
/**
|
|
176
|
+
* Runs every registered handler in reverse registration order, awaiting each in turn, and empties
|
|
177
|
+
* the list.
|
|
178
|
+
*
|
|
179
|
+
* @throws The value the failed handler threw, by identity, when exactly one handler failed. An
|
|
180
|
+
* `AggregateError` carrying every thrown value in run order, when several did.
|
|
181
|
+
* @remarks Every handler runs, including after an earlier one throws or rejects. A handler
|
|
182
|
+
* registered while the run is in progress stays registered for the next call rather than joining
|
|
183
|
+
* this one. The snapshot it ran is discarded, so a repeated call runs nothing that already ran.
|
|
184
|
+
*/
|
|
185
|
+
destroy(): Promise<void>;
|
|
186
|
+
}
|
|
187
|
+
|
|
120
188
|
/**
|
|
121
189
|
* Waits for a host timer to elapse.
|
|
122
190
|
*
|
package/dist/src/core/index.js
CHANGED
|
@@ -104,6 +104,59 @@ function resolveRoot(meta) {
|
|
|
104
104
|
//#endregion
|
|
105
105
|
//#region src/core/factories.ts
|
|
106
106
|
/**
|
|
107
|
+
* Creates values that make common object readers throw or violate their assumptions.
|
|
108
|
+
*
|
|
109
|
+
* @returns A frozen array whose six values are fresh on every call.
|
|
110
|
+
* @remarks Every member makes a naive reader throw. A total guard survives every member without
|
|
111
|
+
* throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in
|
|
112
|
+
* a release, so test the whole returned set in a loop and include the index in each failure.
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* import { expect } from 'vitest'
|
|
116
|
+
* import { createHostileValues } from '@orkestrel/test'
|
|
117
|
+
*
|
|
118
|
+
* function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {
|
|
119
|
+
* if (typeof value !== 'object' || value === null) return false
|
|
120
|
+
* try {
|
|
121
|
+
* if (Object.getPrototypeOf(value) !== Object.prototype) return false
|
|
122
|
+
* Reflect.get(value, 'value')
|
|
123
|
+
* if (Reflect.ownKeys(value).length === 0) return false
|
|
124
|
+
* return Object.values(value).every((member) => typeof member === 'string')
|
|
125
|
+
* } catch {
|
|
126
|
+
* return false
|
|
127
|
+
* }
|
|
128
|
+
* }
|
|
129
|
+
*
|
|
130
|
+
* for (const [index, value] of createHostileValues().entries()) {
|
|
131
|
+
* let accepted: boolean | undefined
|
|
132
|
+
* expect(() => {
|
|
133
|
+
* accepted = isWireRecord(value)
|
|
134
|
+
* }, `hostile value ${index}`).not.toThrow()
|
|
135
|
+
* expect(accepted, `hostile value ${index}`).toBe(false)
|
|
136
|
+
* }
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
function createHostileValues() {
|
|
140
|
+
const cyclic = {};
|
|
141
|
+
cyclic.self = cyclic;
|
|
142
|
+
const revoked = Proxy.revocable({}, {});
|
|
143
|
+
revoked.revoke();
|
|
144
|
+
return Object.freeze([
|
|
145
|
+
cyclic,
|
|
146
|
+
revoked.proxy,
|
|
147
|
+
new Proxy({}, { get() {
|
|
148
|
+
throw new Error("Hostile property read");
|
|
149
|
+
} }),
|
|
150
|
+
new Proxy({}, { ownKeys() {
|
|
151
|
+
throw new Error("Hostile key enumeration");
|
|
152
|
+
} }),
|
|
153
|
+
new Proxy({}, { getPrototypeOf() {
|
|
154
|
+
throw new Error("Hostile prototype read");
|
|
155
|
+
} }),
|
|
156
|
+
Object.create(null)
|
|
157
|
+
]);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
107
160
|
* Creates a recorder for callback arguments.
|
|
108
161
|
*
|
|
109
162
|
* @typeParam TArgs - The argument tuple to record.
|
|
@@ -124,7 +177,35 @@ function createRecorder() {
|
|
|
124
177
|
}
|
|
125
178
|
};
|
|
126
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Creates a teardown list that runs registered handlers newest-first.
|
|
182
|
+
*
|
|
183
|
+
* @returns A teardown list that awaits every handler and collects failures.
|
|
184
|
+
*/
|
|
185
|
+
function createTeardown() {
|
|
186
|
+
let handlers = [];
|
|
187
|
+
return {
|
|
188
|
+
get count() {
|
|
189
|
+
return handlers.length;
|
|
190
|
+
},
|
|
191
|
+
add(handler) {
|
|
192
|
+
handlers.push(handler);
|
|
193
|
+
},
|
|
194
|
+
async destroy() {
|
|
195
|
+
const snapshot = handlers;
|
|
196
|
+
handlers = [];
|
|
197
|
+
const failures = [];
|
|
198
|
+
for (const handler of snapshot.reverse()) try {
|
|
199
|
+
await handler();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
failures.push(error);
|
|
202
|
+
}
|
|
203
|
+
if (failures.length === 1) throw failures[0];
|
|
204
|
+
if (failures.length > 1) throw new AggregateError(failures);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
127
208
|
//#endregion
|
|
128
|
-
export { captureError, collect, collectStream, createRecorder, requireValue, resolveRoot, roundTripJSON, waitForDelay };
|
|
209
|
+
export { captureError, collect, collectStream, createHostileValues, createRecorder, createTeardown, requireValue, resolveRoot, roundTripJSON, waitForDelay };
|
|
129
210
|
|
|
130
211
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONSafe } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface } from './types.js'\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,aAAa,KAAK,GAAkB;CACnD,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACxD;;;;;;;AAQA,SAAgB,aAAa,OAA+B;CAC3D,IAAI;EACH,MAAM;CACP,SAAS,OAAO;EACf,OAAO;CACR;AAED;;;;;;;;;AAUA,SAAgB,aAAgB,OAA6B,UAAU,qBAAwB;CAC9F,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,MAAM,IAAI,MAAM,OAAO;CAClE,OAAO;AACR;;;;;;;;AASA,eAAsB,QAAW,QAAiD;CACjF,MAAM,SAAc,CAAC;CACrB,WAAW,MAAM,SAAS,QAAQ,OAAO,KAAK,KAAK;CACnD,OAAO;AACR;;;;;;;;AASA,eAAsB,cAAiB,QAAkD;CACxF,MAAM,SAAS,OAAO,UAAU;CAChC,MAAM,SAAc,CAAC;CACrB,IAAI;EACH,OAAO,MAAM;GACZ,MAAM,SAAS,MAAM,OAAO,KAAK;GACjC,IAAI,OAAO,MAAM,OAAO;GACxB,OAAO,KAAK,OAAO,KAAK;EACzB;CACD,UAAU;EACT,OAAO,YAAY;CACpB;AACD;;;;;;;;;;;AAYA,SAAgB,cAAiB,OAA2B;CAC3D,MAAM,aAAa,KAAK,UAAU,QAAQ,MAAM,YAAY;EAC3D,IAAI,YAAY,KAAA,KAAa,OAAO,YAAY,cAAc,OAAO,YAAY,UAChF,MAAM,IAAI,MAAM,+DAA+D;EAEhF,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,OAAO;CACR,CAAC;CACD,MAAM,SAAY,KAAK,MAAM,UAAU;CACvC,MAAM,UAAqB,CAAC,MAAM;CAClC,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,UAAU,QAAQ,IAAI;EAC5B,IAAI,OAAO,YAAY,YAAY,CAAC,OAAO,SAAS,OAAO,GAC1D,MAAM,IAAI,MAAM,yCAAyC;EAE1D,IAAI,MAAM,QAAQ,OAAO,GACxB,KAAK,MAAM,SAAS,SAAS,QAAQ,KAAK,KAAK;OACzC,IAAI,OAAO,YAAY,YAAY,YAAY,MACrD,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,QAAQ,KAAK,KAAK;CAEhE;CACA,OAAO;AACR;;;;;;;;AASA,SAAgB,YAAY,MAAuB;CAClD,OAAO,IAAI,IAAI,OAAO,KAAK,GAAG;AAC/B
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type { JSONSafe } from './types.js'\n\n/**\n * Waits for a host timer to elapse.\n *\n * @param ms - The delay in milliseconds.\n * @returns A promise that resolves after the timer fires.\n */\nexport function waitForDelay(ms = 0): Promise<void> {\n\treturn new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Captures the value thrown by a thunk.\n *\n * @param thunk - The work whose thrown value to capture.\n * @returns The thrown value, or `undefined` when the thunk completes.\n */\nexport function captureError(thunk: () => unknown): unknown {\n\ttry {\n\t\tthunk()\n\t} catch (error) {\n\t\treturn error\n\t}\n\treturn undefined\n}\n\n/**\n * Requires a value to be present.\n *\n * @typeParam T - The required value type.\n * @param value - The value to check.\n * @param message - The error message used when the value is absent.\n * @returns The present value.\n */\nexport function requireValue<T>(value: T | null | undefined, message = 'Value is required'): T {\n\tif (value === null || value === undefined) throw new Error(message)\n\treturn value\n}\n\n/**\n * Collects every value from an async iterable.\n *\n * @typeParam T - The yielded value type.\n * @param source - The async iterable to drain.\n * @returns The yielded values in iteration order.\n */\nexport async function collect<T>(source: AsyncIterable<T>): Promise<readonly T[]> {\n\tconst values: T[] = []\n\tfor await (const value of source) values.push(value)\n\treturn values\n}\n\n/**\n * Collects every value from a readable stream.\n *\n * @typeParam T - The streamed value type.\n * @param stream - The readable stream to drain.\n * @returns The streamed values in read order.\n */\nexport async function collectStream<T>(stream: ReadableStream<T>): Promise<readonly T[]> {\n\tconst reader = stream.getReader()\n\tconst values: T[] = []\n\ttry {\n\t\twhile (true) {\n\t\t\tconst result = await reader.read()\n\t\t\tif (result.done) return values\n\t\t\tvalues.push(result.value)\n\t\t}\n\t} finally {\n\t\treader.releaseLock()\n\t}\n}\n\n/**\n * Copies a JSON value through serialization and parsing.\n *\n * @typeParam T - The copied value's type, which the copy keeps.\n * @param value - The value to copy, bounded by its own `JSONSafe` projection.\n * @returns The parsed JSON copy.\n * @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is\n * normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than\n * constraining `T` to `JSONValue`, so an interface-typed value round-trips.\n */\nexport function roundTripJSON<T>(value: T & JSONSafe<T>): T {\n\tconst serialized = JSON.stringify(value, (_key, current) => {\n\t\tif (current === undefined || typeof current === 'function' || typeof current === 'symbol') {\n\t\t\tthrow new Error('JSON values must not contain undefined, functions, or symbols')\n\t\t}\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\treturn current\n\t})\n\tconst parsed: T = JSON.parse(serialized)\n\tconst pending: unknown[] = [parsed]\n\twhile (pending.length > 0) {\n\t\tconst current = pending.pop()\n\t\tif (typeof current === 'number' && !Number.isFinite(current)) {\n\t\t\tthrow new Error('JSON values must contain finite numbers')\n\t\t}\n\t\tif (Array.isArray(current)) {\n\t\t\tfor (const child of current) pending.push(child)\n\t\t} else if (typeof current === 'object' && current !== null) {\n\t\t\tfor (const child of Object.values(current)) pending.push(child)\n\t\t}\n\t}\n\treturn parsed\n}\n\n/**\n * Resolves the parent directory of a calling module, which is the workspace root when called from\n * the conventional `tests/setup.ts` location.\n *\n * @param meta - The calling module metadata.\n * @returns The root URL one directory above the calling file.\n */\nexport function resolveRoot(meta: ImportMeta): URL {\n\treturn new URL('../', meta.url)\n}\n","import type { RecorderInterface, TeardownHandler, TeardownInterface } from './types.js'\n\n/**\n * Creates values that make common object readers throw or violate their assumptions.\n *\n * @returns A frozen array whose six values are fresh on every call.\n * @remarks Every member makes a naive reader throw. A total guard survives every member without\n * throwing. Whether it accepts or refuses one is that guard's own contract. Membership may grow in\n * a release, so test the whole returned set in a loop and include the index in each failure.\n * @example\n * ```ts\n * import { expect } from 'vitest'\n * import { createHostileValues } from '@orkestrel/test'\n *\n * function isWireRecord(value: unknown): value is Readonly<Record<string, string>> {\n * \tif (typeof value !== 'object' || value === null) return false\n * \ttry {\n * \t\tif (Object.getPrototypeOf(value) !== Object.prototype) return false\n * \t\tReflect.get(value, 'value')\n * \t\tif (Reflect.ownKeys(value).length === 0) return false\n * \t\treturn Object.values(value).every((member) => typeof member === 'string')\n * \t} catch {\n * \t\treturn false\n * \t}\n * }\n *\n * for (const [index, value] of createHostileValues().entries()) {\n * \tlet accepted: boolean | undefined\n * \texpect(() => {\n * \t\taccepted = isWireRecord(value)\n * \t}, `hostile value ${index}`).not.toThrow()\n * \texpect(accepted, `hostile value ${index}`).toBe(false)\n * }\n * ```\n */\nexport function createHostileValues(): readonly unknown[] {\n\tconst cyclic: Record<string, unknown> = {}\n\tcyclic.self = cyclic\n\tconst revoked = Proxy.revocable({}, {})\n\trevoked.revoke()\n\n\treturn Object.freeze([\n\t\tcyclic,\n\t\trevoked.proxy,\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tget() {\n\t\t\t\t\tthrow new Error('Hostile property read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\townKeys() {\n\t\t\t\t\tthrow new Error('Hostile key enumeration')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tnew Proxy(\n\t\t\t{},\n\t\t\t{\n\t\t\t\tgetPrototypeOf() {\n\t\t\t\t\tthrow new Error('Hostile prototype read')\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t\tObject.create(null),\n\t])\n}\n\n/**\n * Creates a recorder for callback arguments.\n *\n * @typeParam TArgs - The argument tuple to record.\n * @returns A recorder whose handler appends calls in order.\n */\nexport function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs> {\n\tconst calls: TArgs[] = []\n\treturn {\n\t\tcalls,\n\t\tget count() {\n\t\t\treturn calls.length\n\t\t},\n\t\thandler(...args) {\n\t\t\tcalls.push(args)\n\t\t},\n\t\tclear() {\n\t\t\tcalls.length = 0\n\t\t},\n\t}\n}\n\n/**\n * Creates a teardown list that runs registered handlers newest-first.\n *\n * @returns A teardown list that awaits every handler and collects failures.\n */\nexport function createTeardown(): TeardownInterface {\n\tlet handlers: TeardownHandler[] = []\n\treturn {\n\t\tget count() {\n\t\t\treturn handlers.length\n\t\t},\n\t\tadd(handler) {\n\t\t\thandlers.push(handler)\n\t\t},\n\t\tasync destroy() {\n\t\t\tconst snapshot = handlers\n\t\t\thandlers = []\n\t\t\tconst failures: unknown[] = []\n\t\t\tfor (const handler of snapshot.reverse()) {\n\t\t\t\ttry {\n\t\t\t\t\tawait handler()\n\t\t\t\t} catch (error) {\n\t\t\t\t\tfailures.push(error)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (failures.length === 1) throw failures[0]\n\t\t\tif (failures.length > 1) throw new AggregateError(failures)\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let node_fs = require("node:fs");
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
|
+
let node_events = require("node:events");
|
|
5
6
|
let node_os = require("node:os");
|
|
6
7
|
//#region src/server/helpers.ts
|
|
7
8
|
/**
|
|
@@ -238,7 +239,37 @@ function createScratch(options) {
|
|
|
238
239
|
};
|
|
239
240
|
return scratch;
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Starts a server on an ephemeral IPv4 loopback port.
|
|
244
|
+
*
|
|
245
|
+
* @param server - The unstarted server to bind.
|
|
246
|
+
* @returns The bound origin, assigned port, and asynchronous teardown.
|
|
247
|
+
* @throws When the server cannot bind or reports an address without a numeric port.
|
|
248
|
+
*/
|
|
249
|
+
async function createLoopback(server) {
|
|
250
|
+
server.listen(0, "127.0.0.1");
|
|
251
|
+
await (0, node_events.once)(server, "listening");
|
|
252
|
+
const address = server.address();
|
|
253
|
+
if (typeof address !== "object" || address === null || !("port" in address) || typeof address.port !== "number") throw new Error(`Loopback address must have a numeric port; found ${String(address)}`);
|
|
254
|
+
const port = address.port;
|
|
255
|
+
let destruction;
|
|
256
|
+
return {
|
|
257
|
+
url: `http://127.0.0.1:${port}`,
|
|
258
|
+
port,
|
|
259
|
+
destroy() {
|
|
260
|
+
if (destruction === void 0) destruction = new Promise((resolveClose, rejectClose) => {
|
|
261
|
+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") server.closeAllConnections();
|
|
262
|
+
server.close((error) => {
|
|
263
|
+
if (error === void 0 || "code" in error && error.code === "ERR_SERVER_NOT_RUNNING") resolveClose();
|
|
264
|
+
else rejectClose(error);
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
return destruction;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
241
271
|
//#endregion
|
|
272
|
+
exports.createLoopback = createLoopback;
|
|
242
273
|
exports.createScratch = createScratch;
|
|
243
274
|
exports.isExcluded = isExcluded;
|
|
244
275
|
exports.matchesIdentity = matchesIdentity;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { ScratchIdentity, ScratchInterface, ScratchOptions } from './types.js'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { Server } from 'node:net';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts a server on an ephemeral IPv4 loopback port.
|
|
5
|
+
*
|
|
6
|
+
* @param server - The unstarted server to bind.
|
|
7
|
+
* @returns The bound origin, assigned port, and asynchronous teardown.
|
|
8
|
+
* @throws When the server cannot bind or reports an address without a numeric port.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createLoopback(server: Server): Promise<LoopbackInterface>;
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* Allocates an owned temporary directory with contained file operations.
|
|
3
14
|
*
|
|
@@ -30,6 +41,24 @@ export declare interface InventoryOptions {
|
|
|
30
41
|
*/
|
|
31
42
|
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
32
43
|
|
|
44
|
+
/** A server a test owns, listening on an ephemeral loopback port until the test releases it. */
|
|
45
|
+
export declare interface LoopbackInterface {
|
|
46
|
+
/**
|
|
47
|
+
* The `http` origin for the assigned port, without a trailing slash. A TLS server answers on the
|
|
48
|
+
* same port under `https`.
|
|
49
|
+
*/
|
|
50
|
+
readonly url: string;
|
|
51
|
+
/** The ephemeral port the host assigned. */
|
|
52
|
+
readonly port: number;
|
|
53
|
+
/**
|
|
54
|
+
* Drops every live connection on a server that carries `closeAllConnections`, stops listening, and
|
|
55
|
+
* releases the port.
|
|
56
|
+
*
|
|
57
|
+
* @remarks Idempotent. A plain `net.Server` waits for its open sockets to end.
|
|
58
|
+
*/
|
|
59
|
+
destroy(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
/**
|
|
34
63
|
* Reports whether two directory identities name the same allocation.
|
|
35
64
|
*
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { Server } from 'node:net';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Starts a server on an ephemeral IPv4 loopback port.
|
|
5
|
+
*
|
|
6
|
+
* @param server - The unstarted server to bind.
|
|
7
|
+
* @returns The bound origin, assigned port, and asynchronous teardown.
|
|
8
|
+
* @throws When the server cannot bind or reports an address without a numeric port.
|
|
9
|
+
*/
|
|
10
|
+
export declare function createLoopback(server: Server): Promise<LoopbackInterface>;
|
|
11
|
+
|
|
1
12
|
/**
|
|
2
13
|
* Allocates an owned temporary directory with contained file operations.
|
|
3
14
|
*
|
|
@@ -30,6 +41,24 @@ export declare interface InventoryOptions {
|
|
|
30
41
|
*/
|
|
31
42
|
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
32
43
|
|
|
44
|
+
/** A server a test owns, listening on an ephemeral loopback port until the test releases it. */
|
|
45
|
+
export declare interface LoopbackInterface {
|
|
46
|
+
/**
|
|
47
|
+
* The `http` origin for the assigned port, without a trailing slash. A TLS server answers on the
|
|
48
|
+
* same port under `https`.
|
|
49
|
+
*/
|
|
50
|
+
readonly url: string;
|
|
51
|
+
/** The ephemeral port the host assigned. */
|
|
52
|
+
readonly port: number;
|
|
53
|
+
/**
|
|
54
|
+
* Drops every live connection on a server that carries `closeAllConnections`, stops listening, and
|
|
55
|
+
* releases the port.
|
|
56
|
+
*
|
|
57
|
+
* @remarks Idempotent. A plain `net.Server` waits for its open sockets to end.
|
|
58
|
+
*/
|
|
59
|
+
destroy(): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
/**
|
|
34
63
|
* Reports whether two directory identities name the same allocation.
|
|
35
64
|
*
|
package/dist/src/server/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { once } from "node:events";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
//#region src/server/helpers.ts
|
|
6
7
|
/**
|
|
@@ -237,7 +238,36 @@ function createScratch(options) {
|
|
|
237
238
|
};
|
|
238
239
|
return scratch;
|
|
239
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Starts a server on an ephemeral IPv4 loopback port.
|
|
243
|
+
*
|
|
244
|
+
* @param server - The unstarted server to bind.
|
|
245
|
+
* @returns The bound origin, assigned port, and asynchronous teardown.
|
|
246
|
+
* @throws When the server cannot bind or reports an address without a numeric port.
|
|
247
|
+
*/
|
|
248
|
+
async function createLoopback(server) {
|
|
249
|
+
server.listen(0, "127.0.0.1");
|
|
250
|
+
await once(server, "listening");
|
|
251
|
+
const address = server.address();
|
|
252
|
+
if (typeof address !== "object" || address === null || !("port" in address) || typeof address.port !== "number") throw new Error(`Loopback address must have a numeric port; found ${String(address)}`);
|
|
253
|
+
const port = address.port;
|
|
254
|
+
let destruction;
|
|
255
|
+
return {
|
|
256
|
+
url: `http://127.0.0.1:${port}`,
|
|
257
|
+
port,
|
|
258
|
+
destroy() {
|
|
259
|
+
if (destruction === void 0) destruction = new Promise((resolveClose, rejectClose) => {
|
|
260
|
+
if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") server.closeAllConnections();
|
|
261
|
+
server.close((error) => {
|
|
262
|
+
if (error === void 0 || "code" in error && error.code === "ERR_SERVER_NOT_RUNNING") resolveClose();
|
|
263
|
+
else rejectClose(error);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
return destruction;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
240
270
|
//#endregion
|
|
241
|
-
export { createScratch, isExcluded, matchesIdentity, readInventory, resolveContained };
|
|
271
|
+
export { createLoopback, createScratch, isExcluded, matchesIdentity, readInventory, resolveContained };
|
|
242
272
|
|
|
243
273
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { ScratchIdentity, ScratchInterface, ScratchOptions } from './types.js'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,iBAAiB,MAAc,QAAoC;CAClF,MAAM,YAAY,QAAQ,MAAM,MAAM;CACtC,MAAM,YAAY,SAAS,MAAM,SAAS;CAG1C,IAAI,cAAc,QAAQ,UAAU,WAAW,KAAK,KAAK,KAAK,WAAW,SAAS,GACjF;CAED,OAAO;AACR;;;;;;;;;;;AAYA,SAAgB,gBAAgB,SAA0B,YAAsC;CAC/F,OACC,QAAQ,WAAW,WAAW,UAC9B,QAAQ,UAAU,WAAW,SAC7B,QAAQ,UAAU,WAAW;AAE/B;;;;;;;;AASA,SAAgB,WAAW,KAAa,YAAwC;CAC/E,OAAO,WAAW,MAAM,SAAS,SAAS,MAAM,QAAQ,QAAQ,IAAI,WAAW,GAAG,KAAK,EAAE,CAAC;AAC3F;;;;;;;;;;;;;;AAeA,SAAgB,cACf,MACA,SACA,SACmC;CACnC,MAAM,WAAW,QAAQ,OAAO,SAAS,WAAW,OAAO,cAAc,IAAI,CAAC;CAC9E,MAAM,aAAa,UAAU,QAAQ;CACrC,IAAI,WAAW,eAAe,GAAG,MAAM,IAAI,MAAM,yBAAyB;CAC1E,IAAI,CAAC,WAAW,YAAY,GAAG,MAAM,IAAI,MAAM,yBAAyB;CAExE,MAAM,OAAO,aAAa,OAAO,QAAQ;CACzC,IAAI,QAAQ,WAAW,GAAG,OAAO,OAAO,YAAY,CAAC,CAAC;CAEtD,MAAM,cAAc,SAAS,WAAW,CAAC,EAAA,CAAG,KAAK,SAAS;EAEzD,MAAM,aADa,KAAK,WAAW,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,KAAA,CAC9B,QAAQ,QAAQ,GAAG;EAChD,MAAM,YAAY,UAAU,SAAS,GAAG,IAAI,UAAU,MAAM,GAAG,EAAE,IAAI;EACrE,OAAO,cAAc,MAAM,KAAK;CACjC,CAAC;CACD,MAAM,UAAoB,CAAC;CAC3B,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,2BAAW,IAAI,IAAoB;CAEzC,KAAK,MAAM,UAAU,SAAS;EAC7B,MAAM,YAAY,iBAAiB,MAAM,MAAM;EAC/C,IAAI,cAAc,KAAA,GACjB,MAAM,IAAI,MAAM,wBAAwB,QAAQ;EAGjD,MAAM,SAAS,UAAU,SAAS;EAClC,IAAI,OAAO,eAAe,GAAG,MAAM,IAAI,MAAM,8BAA8B,QAAQ;EACnF,IAAI,CAAC,OAAO,YAAY,KAAK,CAAC,OAAO,OAAO,GAC3C,MAAM,IAAI,MAAM,sCAAsC,QAAQ;EAG/D,MAAM,WAAW,aAAa,OAAO,SAAS;EAC9C,MAAM,WAAW,iBAAiB,MAAM,SAAS,MAAM,QAAQ,CAAC;EAChE,IAAI,aAAa,KAAA,GAChB,MAAM,IAAI,MAAM,wBAAwB,QAAQ;EAGjD,MAAM,MAAM,SAAS,MAAM,QAAQ,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;EACxD,IAAI,WAAW,KAAK,UAAU,GAAG;EACjC,IAAI,OAAO,OAAO,GAAG;GACpB,SAAS,IAAI,KAAK,aAAa,UAAU,MAAM,CAAC;GAChD;EACD;EACA,IAAI,OAAO,IAAI,QAAQ,GAAG;EAC1B,OAAO,IAAI,QAAQ;EACnB,QAAQ,KAAK,QAAQ;CACtB;CAEA,OAAO,QAAQ,SAAS,GAAG;EAC1B,MAAM,YAAY,QAAQ,IAAI;EAC9B,IAAI,cAAc,KAAA,GAAW;EAE7B,KAAK,MAAM,SAAS,YAAY,WAAW,EAAE,eAAe,KAAK,CAAC,GAAG;GACpE,MAAM,OAAO,QAAQ,WAAW,MAAM,IAAI;GAC1C,MAAM,SAAS,UAAU,IAAI;GAC7B,IAAI,OAAO,eAAe,GAAG;GAE7B,MAAM,MAAM,SAAS,MAAM,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,KAAK,GAAG;GACpD,IAAI,WAAW,KAAK,UAAU,GAAG;GAEjC,IAAI,OAAO,YAAY,GAAG;IACzB,MAAM,WAAW,aAAa,OAAO,IAAI;IAIzC,IAHiB,iBAAiB,MAAM,SAAS,MAAM,QAAQ,CAG3D,MAAa,KAAA,KAAa,OAAO,IAAI,QAAQ,GAChD;IAED,OAAO,IAAI,QAAQ;IACnB,QAAQ,KAAK,QAAQ;IACrB;GACD;GAEA,IACC,CAAC,OAAO,OAAO,KACd,SAAS,eAAe,KAAA,KACxB,CAAC,QAAQ,WAAW,MAAM,cAAc,MAAM,KAAK,SAAS,SAAS,CAAC,GAEvE;GAED,SAAS,IAAI,KAAK,aAAa,MAAM,MAAM,CAAC;EAC7C;CACD;CAEA,OAAO,OAAO,YACb,MAAM,KAAK,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,WAAY,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI,CAAE,CAC1F;AACD;;;;;;;;;;;;;AClIA,SAAgB,cAAc,SAA4C;CACzE,MAAM,SAAS,QAAQ,SAAS,UAAU,OAAO,CAAC;CAClD,MAAM,eAAe,UAAU,QAAQ,EAAE,gBAAgB,MAAM,CAAC;CAChE,IAAI,iBAAiB,KAAA,GAAW,MAAM,IAAI,MAAM,+BAA+B;CAC/E,IAAI,aAAa,eAAe,GAAG,MAAM,IAAI,MAAM,mCAAmC;CACtF,IAAI,CAAC,aAAa,YAAY,GAAG,MAAM,IAAI,MAAM,mCAAmC;CAEpF,MAAM,SAAS,SAAS,UAAU;CAClC,IAAI,OAAO,SAAS,GAAG,KAAK,OAAO,SAAS,IAAI,GAC/C,MAAM,IAAI,MAAM,wCAAwC;CAGzD,MAAM,OAAO,YAAY,GAAG,SAAS,MAAM,QAAQ;CACnD,MAAM,YAAY,SAAS,IAAI;CAC/B,MAAM,aAA8B;EACnC,OAAO,UAAU;EACjB,QAAQ,UAAU;EAClB,OAAO,UAAU;CAClB;CACA,MAAM,UAAU;CAChB,MAAM,cAAc;CACpB,IAAI;EACH,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,SAAS,SAAS,CAAC,CAAC,GAAG;GAClE,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GAEpE,UAAU,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,cAAc,WAAW,IAAI;EAC9B;CACD,SAAS,OAAO;EACf,OAAO,MAAM;GAAE,OAAO;GAAM,WAAW;EAAK,CAAC;EAC7C,MAAM;CACP;CAEA,MAAM,UAA4B;EACjC;EACA,MAAM,QAAQ,MAAM;GACnB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GAEpE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,UAAU,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,cAAc,WAAW,IAAI;EAC9B;EACA,KAAK,QAAQ;GACZ,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GACpE,IAAI,CAAC,QAAQ,IAAI,MAAM,GAAG,OAAO,KAAA;GACjC,MAAM,SAAS,SAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;GACjC,IAAI,OAAO,YAAY,GACtB,MAAM,IAAI,MAAM,gCAAgC,QAAQ;GAEzD,OAAO,aAAa,WAAW,MAAM;EACtC;EACA,IAAI,QAAQ;GACX,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GAEpE,MAAM,aAAa,UAAU,MAAM,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,eAAe,KAAA,GAAW,OAAO;GACrC,IAAI,WAAW,eAAe,GAAG,MAAM,IAAI,MAAM,sCAAsC;GACvF,IAAI,CAAC,WAAW,YAAY,GAAG,MAAM,IAAI,MAAM,iCAAiC;GAEhF,OAAO,UAAU,WAAW,EAAE,gBAAgB,MAAM,CAAC,MAAM,KAAA;EAC5D;EACA,MAAM,SAAS,KAAK;GACnB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GACpE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,SAAS,SAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,GAAW,MAAM,IAAI,MAAM,gCAAgC,QAAQ;GAClF,IAAI,CAAC,OAAO,YAAY,GAAG,MAAM,IAAI,MAAM,oCAAoC,QAAQ;GACvF,OAAO,YAAY,SAAS,CAAC,CAAC,KAAK;EACpC;EACA,OAAO,QAAQ;GACd,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GACpE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,SAAS,SAAS,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC5D,IAAI,WAAW,KAAA,KAAa,CAAC,OAAO,YAAY,GAC/C,MAAM,IAAI,MAAM,oCAAoC,QAAQ;GAE7D,IAAI,WAAW,KAAA,GAAW,UAAU,WAAW,EAAE,WAAW,KAAK,CAAC;GAClE,OAAO;EACR;EACA,KAAK,QAAQ,QAAQ;GACpB,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GACpE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,UAAU,QAAQ,SAAS,GAAG,EAAE,WAAW,KAAK,CAAC;GACjD,YAAY,QAAQ,SAAS;EAC9B;EACA,OAAO,QAAQ;GACd,MAAM,YAAY,iBAAiB,MAAM,MAAM;GAC/C,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,GAAG,QAAQ,IAAI,QAAQ;GACpE,IAAI,cAAc,MAAM,MAAM,IAAI,MAAM,GAAG,YAAY,IAAI,QAAQ;GACnE,IAAI,CAAC,QAAQ,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kCAAkC;GAEzE,MAAM,SAAS,UAAU,WAAW,EAAE,gBAAgB,MAAM,CAAC;GAC7D,IAAI,WAAW,KAAA,GAMd;QAAI,gBAAgB;KAJnB,OAAO,OAAO;KACd,QAAQ,OAAO;KACf,OAAO,OAAO;IAEK,GAAU,UAAU,GACvC,MAAM,IAAI,MAAM,GAAG,YAAY,IAAI,QAAQ;GAAA;GAG7C,OAAO,WAAW;IAAE,OAAO;IAAM,WAAW;GAAK,CAAC;EACnD;EACA,UAAU;GACT,MAAM,SAAS,UAAU,MAAM,EAAE,gBAAgB,MAAM,CAAC;GACxD,IAAI,WAAW,KAAA,GAAW;GAM1B,IAAI,CAAC,gBAAgB;IAJpB,OAAO,OAAO;IACd,QAAQ,OAAO;IACf,OAAO,OAAO;GAEM,GAAU,UAAU,GAAG;GAC5C,OAAO,MAAM;IAAE,OAAO;IAAM,WAAW;GAAK,CAAC;EAC9C;CACD;CACA,OAAO;AACR"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\trmSync(path, { force: true, recursive: true })\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\trmSync(candidate, { force: true, recursive: true })\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orkestrel/test",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "The test helpers the Orkestrel fleet repeats — a call recorder, a real delay, JSON and async collectors, and an owned scratch directory with a source-file walker. Zero runtime dependencies. Part of the @orkestrel line.",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/orkestrel/test#readme",
|
|
@@ -69,7 +69,6 @@
|
|
|
69
69
|
"build:src:server": "vite build --config configs/src/vite.server.config.ts && npm run copy dist/src/server/index.d.ts dist/src/server/index.d.cts",
|
|
70
70
|
"prepublishOnly": "npm run format:check && npm run lint:check && npm run check && npm run build && npm test"
|
|
71
71
|
},
|
|
72
|
-
"dependencies": {},
|
|
73
72
|
"devDependencies": {
|
|
74
73
|
"@microsoft/api-extractor": "^7.58.12",
|
|
75
74
|
"@orkestrel/guide": "^0.0.10",
|