@orkestrel/test 0.0.1 → 0.0.3
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 +39 -21
- package/dist/src/core/index.cjs +5 -4
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +38 -4
- package/dist/src/core/index.d.ts +38 -4
- package/dist/src/core/index.js +5 -4
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +125 -29
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +114 -13
- package/dist/src/server/index.d.ts +114 -13
- package/dist/src/server/index.js +125 -31
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# @orkestrel/test
|
|
2
2
|
|
|
3
|
-
The test helpers
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The test helpers the `@orkestrel` fleet kept rewriting, published once. A call recorder that is a
|
|
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 scratch
|
|
6
|
+
directory the test owns and destroys, and a symlink-refusing source-file walker. A helper ships here
|
|
7
|
+
only when enough packages had already written their own; the guide's
|
|
8
|
+
[Limits](guides/test.md#limits) section states that rule and what it excluded. Add it as a
|
|
7
9
|
devDependency; nothing here runs in production code. Part of the `@orkestrel` line.
|
|
8
10
|
|
|
9
11
|
It has **zero runtime dependencies**, and no exported signature names an `@orkestrel/*` type. Both
|
|
@@ -48,16 +50,20 @@ recorder.clear() // truncates in place, so a `calls` reference captured earlier
|
|
|
48
50
|
captureError(() => loader.read('missing.txt')) // the thrown value, or undefined
|
|
49
51
|
requireValue(scratch.read('input.txt')) // 'hello' — narrows `string | undefined` without `!`
|
|
50
52
|
|
|
53
|
+
scratch.remove('input.txt') // one contained entry, subtree and all; a missing target is a no-op
|
|
51
54
|
scratch.destroy() // idempotent, and it removes only the directory it allocated
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
The rest of core is `collect` (drains an async iterable), `collectStream` (drains a
|
|
55
|
-
`ReadableStream`), `roundTripJSON` (copies
|
|
56
|
-
|
|
58
|
+
`ReadableStream`), `roundTripJSON` (copies any value `JSONSafe` accepts, including an
|
|
59
|
+
interface-typed one, and throws rather than turning a non-finite number into `null`), and
|
|
60
|
+
`resolveRoot` (the directory above the calling module, from `import.meta`).
|
|
57
61
|
|
|
58
62
|
The server face adds `readInventory`, which reads a checkout into a map of root-relative path to
|
|
59
|
-
file text that a parity suite can assert against, plus
|
|
60
|
-
|
|
63
|
+
file text that a parity suite can assert against, plus the three pure leaves behind it and
|
|
64
|
+
`createScratch`: `resolveContained`, the lexical check both refuse escapes with; `isExcluded`, the
|
|
65
|
+
exclusion rule the walk applies; and `matchesIdentity`, the comparison `destroy()` makes before it
|
|
66
|
+
removes anything.
|
|
61
67
|
|
|
62
68
|
```ts
|
|
63
69
|
import { resolveRoot } from '@orkestrel/test'
|
|
@@ -66,7 +72,7 @@ import { readInventory } from '@orkestrel/test/server'
|
|
|
66
72
|
// A suite in tests/ is one directory below the workspace root, which is what `resolveRoot` returns.
|
|
67
73
|
const root = resolveRoot(import.meta)
|
|
68
74
|
|
|
69
|
-
// Nothing is walked by default, so the
|
|
75
|
+
// Nothing is walked by default, so the targets are a required argument rather than an option.
|
|
70
76
|
const sources = readInventory(root, ['src/core', 'src/server'], { extensions: ['.ts'] })
|
|
71
77
|
|
|
72
78
|
Object.keys(sources)
|
|
@@ -77,7 +83,12 @@ Object.keys(sources)
|
|
|
77
83
|
sources['src/core/index.ts']
|
|
78
84
|
// "export * from './types.js'\nexport * from './helpers.js'\nexport * from './factories.js'\n"
|
|
79
85
|
|
|
80
|
-
//
|
|
86
|
+
// A target is a file or a directory. A named file is read whatever the extension filter says.
|
|
87
|
+
Object.keys(readInventory(root, ['package.json', 'src/core'], { extensions: ['.ts'] }))
|
|
88
|
+
// ['package.json', 'src/core/factories.ts', 'src/core/helpers.ts', 'src/core/index.ts',
|
|
89
|
+
// 'src/core/types.ts']
|
|
90
|
+
|
|
91
|
+
// An `exclude` entry matches whole key segments. A file key drops that file.
|
|
81
92
|
Object.keys(
|
|
82
93
|
readInventory(root, ['src/core'], { extensions: ['.ts'], exclude: ['src/core/index.ts'] }),
|
|
83
94
|
)
|
|
@@ -94,17 +105,24 @@ conversion. Keys are inserted in sorted order, and a plain object reads that ord
|
|
|
94
105
|
key that is not integer-like.
|
|
95
106
|
|
|
96
107
|
Two boundaries are worth stating up front, because the two filesystem helpers promise different
|
|
97
|
-
things. `createScratch` allocates its own directory at POSIX mode `0700`
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
things. `createScratch` allocates its own directory at POSIX mode `0700` — under the host temporary
|
|
109
|
+
directory, or under a `parent` you name — and refuses a path that lexically escapes it. The suite
|
|
110
|
+
asserts those bits on POSIX and proves nothing about a host that emulates them. The mode keeps
|
|
111
|
+
another uid out, and neither a sibling test worker nor the code under test is another uid. It does
|
|
112
|
+
not walk segments for symbolic links. A link inside its own allocation was created by the test
|
|
113
|
+
process, by the code the test drives, or by this package's own `link` — handing that code
|
|
114
|
+
`scratch.path` is the ordinary use of this helper — and a contained path reaches outside the
|
|
115
|
+
allocation through one. The guide's [traversal](guides/test.md#traversal) section states what each
|
|
116
|
+
member does with a link it meets. Naming a `parent` inside a package tree costs one more thing:
|
|
117
|
+
while the allocation exists, everything that walks that tree sees it. `destroy()` is unaffected by
|
|
118
|
+
where the allocation sits, because it matches the allocation's identity rather than its path. One
|
|
119
|
+
field of that identity is the host's to supply: where a host reports no real creation time, libuv
|
|
120
|
+
reports `ctime` in its place, the first write moves it, and `destroy()` then removes nothing and
|
|
121
|
+
returns as if it had. `readInventory` walks a checkout you supply, usually one the test did not
|
|
122
|
+
create, so it throws on a symlinked root or named target, throws on a named target whose real path
|
|
123
|
+
leaves the root through a link in the middle, and skips a symlink met while walking. Neither is a
|
|
124
|
+
sandbox against hostile filesystem content: those refusals stop accidental escape, not an adversary
|
|
125
|
+
who can create hard links where the test process already writes.
|
|
108
126
|
|
|
109
127
|
## Guide
|
|
110
128
|
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -69,14 +69,16 @@ async function collectStream(stream) {
|
|
|
69
69
|
/**
|
|
70
70
|
* Copies a JSON value through serialization and parsing.
|
|
71
71
|
*
|
|
72
|
-
* @typeParam T - The
|
|
73
|
-
* @param value - The value to copy.
|
|
72
|
+
* @typeParam T - The copied value's type, which the copy keeps.
|
|
73
|
+
* @param value - The value to copy, bounded by its own `JSONSafe` projection.
|
|
74
74
|
* @returns The parsed JSON copy.
|
|
75
75
|
* @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
|
|
76
|
-
* normalized to zero by JSON serialization.
|
|
76
|
+
* normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than
|
|
77
|
+
* constraining `T` to `JSONValue`, so an interface-typed value round-trips.
|
|
77
78
|
*/
|
|
78
79
|
function roundTripJSON(value) {
|
|
79
80
|
const serialized = JSON.stringify(value, (_key, current) => {
|
|
81
|
+
if (current === void 0 || typeof current === "function" || typeof current === "symbol") throw new Error("JSON values must not contain undefined, functions, or symbols");
|
|
80
82
|
if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
|
|
81
83
|
return current;
|
|
82
84
|
});
|
|
@@ -84,7 +86,6 @@ function roundTripJSON(value) {
|
|
|
84
86
|
const pending = [parsed];
|
|
85
87
|
while (pending.length > 0) {
|
|
86
88
|
const current = pending.pop();
|
|
87
|
-
if (current === void 0) continue;
|
|
88
89
|
if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
|
|
89
90
|
if (Array.isArray(current)) for (const child of current) pending.push(child);
|
|
90
91
|
else if (typeof current === "object" && current !== null) for (const child of Object.values(current)) pending.push(child);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type {
|
|
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;;;;;;;;;AC/GA,SAAgB,iBAA6E;CAC5F,MAAM,QAAiB,CAAC;CACxB,OAAO;EACN;EACA,IAAI,QAAQ;GACX,OAAO,MAAM;EACd;EACA,QAAQ,GAAG,MAAM;GAChB,MAAM,KAAK,IAAI;EAChB;EACA,QAAQ;GACP,MAAM,SAAS;EAChB;CACD;AACD"}
|
|
@@ -32,6 +32,39 @@ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<rea
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every
|
|
37
|
+
* member it does not, mapped to `never`.
|
|
38
|
+
*
|
|
39
|
+
* @typeParam T - The type to project.
|
|
40
|
+
* @remarks Intersect a parameter with this rather than constraining it to `JSONValue`. A `JSONValue`
|
|
41
|
+
* constraint rejects every `interface`, because TypeScript grants an implicit index signature to a
|
|
42
|
+
* type alias and never to an interface, and interfaces are what this project's public types are. A
|
|
43
|
+
* value whose type survives the projection satisfies the intersection unchanged; one that carries a
|
|
44
|
+
* method, a `Date`, a `Map`, the opaque `object` type, or a symbol-keyed member meets `never` at that
|
|
45
|
+
* member and is rejected there. A member typed `undefined` is rejected the same way, because
|
|
46
|
+
* serialization drops it from an object and rewrites it to `null` in an array, so the returned type
|
|
47
|
+
* would claim a member the copy does not carry. An optional member survives, since its declared type
|
|
48
|
+
* still narrows to what JSON keeps. A member declared `?: X | undefined` and passed an explicit
|
|
49
|
+
* `undefined` is refused; declare it `?: X` and omit the member instead. `unknown` passes through
|
|
50
|
+
* unvetted by the projection. For an `unknown` member, `roundTripJSON` refuses `undefined`,
|
|
51
|
+
* functions, symbols, and non-finite numbers at runtime; JSON otherwise may silently reshape the
|
|
52
|
+
* value, such as a `Date` to a string or a `Map` to `{}`.
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* interface Snapshot {
|
|
56
|
+
* readonly id: string
|
|
57
|
+
* readonly turns: number
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // { readonly id: string; readonly turns: number } — every member survives.
|
|
61
|
+
* type Safe = JSONSafe<Snapshot>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare type JSONSafe<T> = unknown extends T ? T : T extends string | number | boolean | null ? T : T extends ReadonlyArray<infer E> ? ReadonlyArray<JSONSafe<E>> : T extends (...args: never[]) => unknown ? never : T extends object ? object extends T ? never : {
|
|
65
|
+
readonly [K in keyof T]: K extends symbol ? never : JSONSafe<T[K]>;
|
|
66
|
+
} : never;
|
|
67
|
+
|
|
35
68
|
/** Any value JSON can represent, so a round trip through JSON preserves the type. */
|
|
36
69
|
export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
|
|
37
70
|
readonly [key: string]: JSONValue;
|
|
@@ -75,13 +108,14 @@ export declare function resolveRoot(meta: ImportMeta): URL;
|
|
|
75
108
|
/**
|
|
76
109
|
* Copies a JSON value through serialization and parsing.
|
|
77
110
|
*
|
|
78
|
-
* @typeParam T - The
|
|
79
|
-
* @param value - The value to copy.
|
|
111
|
+
* @typeParam T - The copied value's type, which the copy keeps.
|
|
112
|
+
* @param value - The value to copy, bounded by its own `JSONSafe` projection.
|
|
80
113
|
* @returns The parsed JSON copy.
|
|
81
114
|
* @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
|
|
82
|
-
* normalized to zero by JSON serialization.
|
|
115
|
+
* normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than
|
|
116
|
+
* constraining `T` to `JSONValue`, so an interface-typed value round-trips.
|
|
83
117
|
*/
|
|
84
|
-
export declare function roundTripJSON<T
|
|
118
|
+
export declare function roundTripJSON<T>(value: T & JSONSafe<T>): T;
|
|
85
119
|
|
|
86
120
|
/**
|
|
87
121
|
* Waits for a host timer to elapse.
|
package/dist/src/core/index.d.ts
CHANGED
|
@@ -32,6 +32,39 @@ export declare function collectStream<T>(stream: ReadableStream<T>): Promise<rea
|
|
|
32
32
|
*/
|
|
33
33
|
export declare function createRecorder<TArgs extends readonly unknown[]>(): RecorderInterface<TArgs>;
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* The JSON-safe projection of a type: every member JSON preserves, mapped to itself, and every
|
|
37
|
+
* member it does not, mapped to `never`.
|
|
38
|
+
*
|
|
39
|
+
* @typeParam T - The type to project.
|
|
40
|
+
* @remarks Intersect a parameter with this rather than constraining it to `JSONValue`. A `JSONValue`
|
|
41
|
+
* constraint rejects every `interface`, because TypeScript grants an implicit index signature to a
|
|
42
|
+
* type alias and never to an interface, and interfaces are what this project's public types are. A
|
|
43
|
+
* value whose type survives the projection satisfies the intersection unchanged; one that carries a
|
|
44
|
+
* method, a `Date`, a `Map`, the opaque `object` type, or a symbol-keyed member meets `never` at that
|
|
45
|
+
* member and is rejected there. A member typed `undefined` is rejected the same way, because
|
|
46
|
+
* serialization drops it from an object and rewrites it to `null` in an array, so the returned type
|
|
47
|
+
* would claim a member the copy does not carry. An optional member survives, since its declared type
|
|
48
|
+
* still narrows to what JSON keeps. A member declared `?: X | undefined` and passed an explicit
|
|
49
|
+
* `undefined` is refused; declare it `?: X` and omit the member instead. `unknown` passes through
|
|
50
|
+
* unvetted by the projection. For an `unknown` member, `roundTripJSON` refuses `undefined`,
|
|
51
|
+
* functions, symbols, and non-finite numbers at runtime; JSON otherwise may silently reshape the
|
|
52
|
+
* value, such as a `Date` to a string or a `Map` to `{}`.
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* interface Snapshot {
|
|
56
|
+
* readonly id: string
|
|
57
|
+
* readonly turns: number
|
|
58
|
+
* }
|
|
59
|
+
*
|
|
60
|
+
* // { readonly id: string; readonly turns: number } — every member survives.
|
|
61
|
+
* type Safe = JSONSafe<Snapshot>
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export declare type JSONSafe<T> = unknown extends T ? T : T extends string | number | boolean | null ? T : T extends ReadonlyArray<infer E> ? ReadonlyArray<JSONSafe<E>> : T extends (...args: never[]) => unknown ? never : T extends object ? object extends T ? never : {
|
|
65
|
+
readonly [K in keyof T]: K extends symbol ? never : JSONSafe<T[K]>;
|
|
66
|
+
} : never;
|
|
67
|
+
|
|
35
68
|
/** Any value JSON can represent, so a round trip through JSON preserves the type. */
|
|
36
69
|
export declare type JSONValue = string | number | boolean | null | readonly JSONValue[] | {
|
|
37
70
|
readonly [key: string]: JSONValue;
|
|
@@ -75,13 +108,14 @@ export declare function resolveRoot(meta: ImportMeta): URL;
|
|
|
75
108
|
/**
|
|
76
109
|
* Copies a JSON value through serialization and parsing.
|
|
77
110
|
*
|
|
78
|
-
* @typeParam T - The
|
|
79
|
-
* @param value - The value to copy.
|
|
111
|
+
* @typeParam T - The copied value's type, which the copy keeps.
|
|
112
|
+
* @param value - The value to copy, bounded by its own `JSONSafe` projection.
|
|
80
113
|
* @returns The parsed JSON copy.
|
|
81
114
|
* @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
|
|
82
|
-
* normalized to zero by JSON serialization.
|
|
115
|
+
* normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than
|
|
116
|
+
* constraining `T` to `JSONValue`, so an interface-typed value round-trips.
|
|
83
117
|
*/
|
|
84
|
-
export declare function roundTripJSON<T
|
|
118
|
+
export declare function roundTripJSON<T>(value: T & JSONSafe<T>): T;
|
|
85
119
|
|
|
86
120
|
/**
|
|
87
121
|
* Waits for a host timer to elapse.
|
package/dist/src/core/index.js
CHANGED
|
@@ -68,14 +68,16 @@ async function collectStream(stream) {
|
|
|
68
68
|
/**
|
|
69
69
|
* Copies a JSON value through serialization and parsing.
|
|
70
70
|
*
|
|
71
|
-
* @typeParam T - The
|
|
72
|
-
* @param value - The value to copy.
|
|
71
|
+
* @typeParam T - The copied value's type, which the copy keeps.
|
|
72
|
+
* @param value - The value to copy, bounded by its own `JSONSafe` projection.
|
|
73
73
|
* @returns The parsed JSON copy.
|
|
74
74
|
* @remarks Non-finite numbers throw because JSON would replace them with `null`. Negative zero is
|
|
75
|
-
* normalized to zero by JSON serialization.
|
|
75
|
+
* normalized to zero by JSON serialization. The bound intersects `JSONSafe<T>` rather than
|
|
76
|
+
* constraining `T` to `JSONValue`, so an interface-typed value round-trips.
|
|
76
77
|
*/
|
|
77
78
|
function roundTripJSON(value) {
|
|
78
79
|
const serialized = JSON.stringify(value, (_key, current) => {
|
|
80
|
+
if (current === void 0 || typeof current === "function" || typeof current === "symbol") throw new Error("JSON values must not contain undefined, functions, or symbols");
|
|
79
81
|
if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
|
|
80
82
|
return current;
|
|
81
83
|
});
|
|
@@ -83,7 +85,6 @@ function roundTripJSON(value) {
|
|
|
83
85
|
const pending = [parsed];
|
|
84
86
|
while (pending.length > 0) {
|
|
85
87
|
const current = pending.pop();
|
|
86
|
-
if (current === void 0) continue;
|
|
87
88
|
if (typeof current === "number" && !Number.isFinite(current)) throw new Error("JSON values must contain finite numbers");
|
|
88
89
|
if (Array.isArray(current)) for (const child of current) pending.push(child);
|
|
89
90
|
else if (typeof current === "object" && current !== null) for (const child of Object.values(current)) pending.push(child);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/core/helpers.ts","../../../src/core/factories.ts"],"sourcesContent":["import type {
|
|
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;;;;;;;;;AC/GA,SAAgB,iBAA6E;CAC5F,MAAM,QAAiB,CAAC;CACxB,OAAO;EACN;EACA,IAAI,QAAQ;GACX,OAAO,MAAM;EACd;EACA,QAAQ,GAAG,MAAM;GAChB,MAAM,KAAK,IAAI;EAChB;EACA,QAAQ;GACP,MAAM,SAAS;EAChB;CACD;AACD"}
|
|
@@ -18,36 +18,72 @@ function resolveContained(root, target) {
|
|
|
18
18
|
return candidate;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* Reports whether two directory identities name the same allocation.
|
|
22
|
+
*
|
|
23
|
+
* @param current - The identity read from the path now.
|
|
24
|
+
* @param allocation - The identity recorded when the directory was allocated.
|
|
25
|
+
* @returns Whether the device, the index node, and the creation time all match.
|
|
26
|
+
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
27
|
+
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
28
|
+
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
29
|
+
*/
|
|
30
|
+
function matchesIdentity(current, allocation) {
|
|
31
|
+
return current.device === allocation.device && current.inode === allocation.inode && current.birth === allocation.birth;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Reports whether a root-relative key matches an exclusion.
|
|
35
|
+
*
|
|
36
|
+
* @param key - The root-relative key to test.
|
|
37
|
+
* @param exclusions - The normalized root-relative exclusion keys.
|
|
38
|
+
* @returns Whether an exclusion names the key or one of its ancestors.
|
|
39
|
+
*/
|
|
40
|
+
function isExcluded(key, exclusions) {
|
|
41
|
+
return exclusions.some((rule) => rule === "" || key === rule || key.startsWith(`${rule}/`));
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Reads files from selected targets below a root directory.
|
|
22
45
|
*
|
|
23
46
|
* @param root - The root directory as a path or file URL.
|
|
24
|
-
* @param
|
|
25
|
-
* @param options - Optional file extension and
|
|
47
|
+
* @param targets - The files to read directly and directories to visit below the root.
|
|
48
|
+
* @param options - Optional file extension and path exclusions.
|
|
26
49
|
* @returns File contents keyed by sorted root-relative paths.
|
|
27
|
-
* @
|
|
50
|
+
* @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves
|
|
51
|
+
* outside the root.
|
|
52
|
+
* @remarks A named file is included regardless of the extension filter. An absent extension filter
|
|
53
|
+
* includes every walked file. An exclusion matches whole root-relative key segments and covers every
|
|
54
|
+
* key below it, and it applies to a named target and a walked entry alike.
|
|
28
55
|
*/
|
|
29
|
-
function readInventory(root,
|
|
56
|
+
function readInventory(root, targets, options) {
|
|
30
57
|
const supplied = (0, node_path.resolve)(typeof root === "string" ? root : (0, node_url.fileURLToPath)(root));
|
|
31
58
|
const rootStatus = (0, node_fs.lstatSync)(supplied);
|
|
32
59
|
if (rootStatus.isSymbolicLink()) throw new Error("Root is a symbolic link");
|
|
33
60
|
if (!rootStatus.isDirectory()) throw new Error("Root is not a directory");
|
|
34
61
|
const base = node_fs.realpathSync.native(supplied);
|
|
35
|
-
if (
|
|
36
|
-
const
|
|
62
|
+
if (targets.length === 0) return Object.fromEntries([]);
|
|
63
|
+
const exclusions = (options?.exclude ?? []).map((rule) => {
|
|
64
|
+
const collapsed = (rule.startsWith("./") ? rule.slice(2) : rule).replace(/\/+/g, "/");
|
|
65
|
+
const untrailed = collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
66
|
+
return untrailed === "." ? "" : untrailed;
|
|
67
|
+
});
|
|
37
68
|
const pending = [];
|
|
38
69
|
const queued = /* @__PURE__ */ new Set();
|
|
39
70
|
const contents = /* @__PURE__ */ new Map();
|
|
40
|
-
for (const
|
|
41
|
-
const candidate = resolveContained(base,
|
|
42
|
-
if (candidate === void 0) throw new Error(`
|
|
71
|
+
for (const target of targets) {
|
|
72
|
+
const candidate = resolveContained(base, target);
|
|
73
|
+
if (candidate === void 0) throw new Error(`Target outside root: ${target}`);
|
|
43
74
|
const status = (0, node_fs.lstatSync)(candidate);
|
|
44
|
-
if (status.isSymbolicLink()) throw new Error(`
|
|
45
|
-
if (!status.isDirectory()) throw new Error(`
|
|
75
|
+
if (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`);
|
|
76
|
+
if (!status.isDirectory() && !status.isFile()) throw new Error(`Target is not a file or directory: ${target}`);
|
|
46
77
|
const physical = node_fs.realpathSync.native(candidate);
|
|
47
78
|
const resolved = resolveContained(base, (0, node_path.relative)(base, physical));
|
|
48
|
-
if (resolved === void 0) throw new Error(`
|
|
79
|
+
if (resolved === void 0) throw new Error(`Target outside root: ${target}`);
|
|
49
80
|
const key = (0, node_path.relative)(base, resolved).split(node_path.sep).join("/");
|
|
50
|
-
if (
|
|
81
|
+
if (isExcluded(key, exclusions)) continue;
|
|
82
|
+
if (status.isFile()) {
|
|
83
|
+
contents.set(key, (0, node_fs.readFileSync)(physical, "utf8"));
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (queued.has(physical)) continue;
|
|
51
87
|
queued.add(physical);
|
|
52
88
|
pending.push(physical);
|
|
53
89
|
}
|
|
@@ -59,7 +95,7 @@ function readInventory(root, directories, options) {
|
|
|
59
95
|
const status = (0, node_fs.lstatSync)(path);
|
|
60
96
|
if (status.isSymbolicLink()) continue;
|
|
61
97
|
const key = (0, node_path.relative)(base, path).split(node_path.sep).join("/");
|
|
62
|
-
if (
|
|
98
|
+
if (isExcluded(key, exclusions)) continue;
|
|
63
99
|
if (status.isDirectory()) {
|
|
64
100
|
const physical = node_fs.realpathSync.native(path);
|
|
65
101
|
if (resolveContained(base, (0, node_path.relative)(base, physical)) === void 0 || queued.has(physical)) continue;
|
|
@@ -78,17 +114,30 @@ function readInventory(root, directories, options) {
|
|
|
78
114
|
/**
|
|
79
115
|
* Allocates an owned temporary directory with contained file operations.
|
|
80
116
|
*
|
|
81
|
-
* @param options - Optional directory prefix and initial files.
|
|
117
|
+
* @param options - Optional parent directory, name prefix, and initial files.
|
|
82
118
|
* @returns The scratch directory and its file operations.
|
|
83
|
-
* @
|
|
119
|
+
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
120
|
+
* `/` or `\`; or when allocation or seeding fails.
|
|
121
|
+
* @remarks The parent defaults to the host temporary directory. The prefix defaults to
|
|
122
|
+
* `orkestrel-test-`. Seed keys use root-relative paths.
|
|
84
123
|
*/
|
|
85
124
|
function createScratch(options) {
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
125
|
+
const parent = (0, node_path.resolve)(options?.parent ?? (0, node_os.tmpdir)());
|
|
126
|
+
const parentStatus = (0, node_fs.lstatSync)(parent, { throwIfNoEntry: false });
|
|
127
|
+
if (parentStatus === void 0) throw new Error("Scratch parent does not exist");
|
|
128
|
+
if (parentStatus.isSymbolicLink()) throw new Error("Scratch parent is a symbolic link");
|
|
129
|
+
if (!parentStatus.isDirectory()) throw new Error("Scratch parent is not a directory");
|
|
130
|
+
const prefix = options?.prefix ?? "orkestrel-test-";
|
|
131
|
+
if (prefix.includes("/") || prefix.includes("\\")) throw new Error("Scratch prefix must be a name fragment");
|
|
132
|
+
const path = (0, node_fs.mkdtempSync)(`${parent}${node_path.sep}${prefix}`);
|
|
133
|
+
const allocated = (0, node_fs.statSync)(path);
|
|
134
|
+
const allocation = {
|
|
135
|
+
birth: allocated.birthtimeMs,
|
|
136
|
+
device: allocated.dev,
|
|
137
|
+
inode: allocated.ino
|
|
138
|
+
};
|
|
91
139
|
const outside = "Path outside scratch directory";
|
|
140
|
+
const unremovable = "Scratch directory is not a removable target";
|
|
92
141
|
try {
|
|
93
142
|
for (const [target, text] of Object.entries(options?.files ?? {})) {
|
|
94
143
|
const candidate = resolveContained(path, target);
|
|
@@ -108,23 +157,20 @@ function createScratch(options) {
|
|
|
108
157
|
write(target, text) {
|
|
109
158
|
const candidate = resolveContained(path, target);
|
|
110
159
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
111
|
-
|
|
112
|
-
if (rootStatus === void 0) throw new Error("Scratch directory does not exist");
|
|
113
|
-
if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
|
|
114
|
-
if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
|
|
160
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
115
161
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(candidate), { recursive: true });
|
|
116
162
|
(0, node_fs.writeFileSync)(candidate, text);
|
|
117
163
|
},
|
|
118
164
|
read(target) {
|
|
119
165
|
const candidate = resolveContained(path, target);
|
|
120
166
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
121
|
-
if (!scratch.
|
|
167
|
+
if (!scratch.has(target)) return void 0;
|
|
122
168
|
const status = (0, node_fs.statSync)(candidate, { throwIfNoEntry: false });
|
|
123
169
|
if (status === void 0) return void 0;
|
|
124
170
|
if (status.isDirectory()) throw new Error(`Scratch path is a directory: ${target}`);
|
|
125
171
|
return (0, node_fs.readFileSync)(candidate, "utf8");
|
|
126
172
|
},
|
|
127
|
-
|
|
173
|
+
has(target) {
|
|
128
174
|
const candidate = resolveContained(path, target);
|
|
129
175
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
130
176
|
const rootStatus = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
|
|
@@ -133,9 +179,57 @@ function createScratch(options) {
|
|
|
133
179
|
if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
|
|
134
180
|
return (0, node_fs.lstatSync)(candidate, { throwIfNoEntry: false }) !== void 0;
|
|
135
181
|
},
|
|
182
|
+
names(target = ".") {
|
|
183
|
+
const candidate = resolveContained(path, target);
|
|
184
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
185
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
186
|
+
const status = (0, node_fs.statSync)(candidate, { throwIfNoEntry: false });
|
|
187
|
+
if (status === void 0) throw new Error(`Scratch path does not exist: ${target}`);
|
|
188
|
+
if (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`);
|
|
189
|
+
return (0, node_fs.readdirSync)(candidate).sort();
|
|
190
|
+
},
|
|
191
|
+
ensure(target) {
|
|
192
|
+
const candidate = resolveContained(path, target);
|
|
193
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
194
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
195
|
+
const status = (0, node_fs.statSync)(candidate, { throwIfNoEntry: false });
|
|
196
|
+
if (status !== void 0 && !status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`);
|
|
197
|
+
if (status === void 0) (0, node_fs.mkdirSync)(candidate, { recursive: true });
|
|
198
|
+
return candidate;
|
|
199
|
+
},
|
|
200
|
+
link(target, source) {
|
|
201
|
+
const candidate = resolveContained(path, target);
|
|
202
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
203
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
204
|
+
(0, node_fs.mkdirSync)((0, node_path.dirname)(candidate), { recursive: true });
|
|
205
|
+
(0, node_fs.symlinkSync)(source, candidate);
|
|
206
|
+
},
|
|
207
|
+
remove(target) {
|
|
208
|
+
const candidate = resolveContained(path, target);
|
|
209
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
210
|
+
if (candidate === path) throw new Error(`${unremovable}: ${target}`);
|
|
211
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
212
|
+
const status = (0, node_fs.lstatSync)(candidate, { throwIfNoEntry: false });
|
|
213
|
+
if (status !== void 0) {
|
|
214
|
+
if (matchesIdentity({
|
|
215
|
+
birth: status.birthtimeMs,
|
|
216
|
+
device: status.dev,
|
|
217
|
+
inode: status.ino
|
|
218
|
+
}, allocation)) throw new Error(`${unremovable}: ${target}`);
|
|
219
|
+
}
|
|
220
|
+
(0, node_fs.rmSync)(candidate, {
|
|
221
|
+
force: true,
|
|
222
|
+
recursive: true
|
|
223
|
+
});
|
|
224
|
+
},
|
|
136
225
|
destroy() {
|
|
137
226
|
const status = (0, node_fs.lstatSync)(path, { throwIfNoEntry: false });
|
|
138
|
-
if (status === void 0
|
|
227
|
+
if (status === void 0) return;
|
|
228
|
+
if (!matchesIdentity({
|
|
229
|
+
birth: status.birthtimeMs,
|
|
230
|
+
device: status.dev,
|
|
231
|
+
inode: status.ino
|
|
232
|
+
}, allocation)) return;
|
|
139
233
|
(0, node_fs.rmSync)(path, {
|
|
140
234
|
force: true,
|
|
141
235
|
recursive: true
|
|
@@ -146,6 +240,8 @@ function createScratch(options) {
|
|
|
146
240
|
}
|
|
147
241
|
//#endregion
|
|
148
242
|
exports.createScratch = createScratch;
|
|
243
|
+
exports.isExcluded = isExcluded;
|
|
244
|
+
exports.matchesIdentity = matchesIdentity;
|
|
149
245
|
exports.readInventory = readInventory;
|
|
150
246
|
exports.resolveContained = resolveContained;
|
|
151
247
|
|