@orkestrel/test 0.0.1 → 0.0.2
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 +40 -22
- 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 +106 -29
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +104 -13
- package/dist/src/server/index.d.ts +104 -13
- package/dist/src/server/index.js +106 -31
- package/dist/src/server/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["import type { InventoryOptions } 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\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reads files from selected directories below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param directories - The directories to visit below the root.\n * @param options - Optional file extension and exact-path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @remarks An absent extension filter includes every file. Exclusions match full root-relative keys.\n */\nexport function readInventory(\n\troot: URL | string,\n\tdirectories: 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 (directories.length === 0) return Object.fromEntries([])\n\n\tconst excluded = new Set(options?.exclude)\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const directory of directories) {\n\t\tconst candidate = resolveContained(base, directory)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Directory is a symbolic link: ${directory}`)\n\t\tif (!status.isDirectory()) throw new Error(`Not a directory: ${directory}`)\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(`Directory outside root: ${directory}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (excluded.has(key) || 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 (excluded.has(key)) 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\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 { ScratchInterface, ScratchOptions } from './types.js'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\trmSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve } from 'node:path'\nimport { resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional directory prefix and initial files.\n * @returns The scratch directory and its file operations.\n * @remarks The prefix defaults to `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst temporary = resolve(tmpdir())\n\tconst prefix = resolve(temporary, options?.prefix ?? 'orkestrel-test-')\n\tif (dirname(prefix) !== temporary)\n\t\tthrow new Error('Scratch prefix must stay within the temporary directory')\n\n\tconst path = mkdtempSync(prefix)\n\tconst allocation = statSync(path)\n\tconst outside = 'Path outside scratch directory'\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\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) throw new Error('Scratch directory does not exist')\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\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.exists(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\texists(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\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (\n\t\t\t\tstatus === undefined ||\n\t\t\t\tstatus.dev !== allocation.dev ||\n\t\t\t\tstatus.ino !== allocation.ino ||\n\t\t\t\tstatus.birthtimeMs !== allocation.birthtimeMs\n\t\t\t)\n\t\t\t\treturn\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 { 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\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\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,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Allocates an owned temporary directory with contained file operations.
|
|
3
3
|
*
|
|
4
|
-
* @param options - Optional directory prefix and initial files.
|
|
4
|
+
* @param options - Optional parent directory, name prefix, and initial files.
|
|
5
5
|
* @returns The scratch directory and its file operations.
|
|
6
|
-
* @
|
|
6
|
+
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
7
|
+
* `/` or `\`; or when allocation or seeding fails.
|
|
8
|
+
* @remarks The parent defaults to the host temporary directory. The prefix defaults to
|
|
9
|
+
* `orkestrel-test-`. Seed keys use root-relative paths.
|
|
7
10
|
*/
|
|
8
11
|
export declare function createScratch(options?: ScratchOptions): ScratchInterface;
|
|
9
12
|
|
|
@@ -11,20 +14,48 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
|
|
|
11
14
|
export declare interface InventoryOptions {
|
|
12
15
|
/** The file extensions to include, each written with its leading dot. */
|
|
13
16
|
readonly extensions?: readonly string[];
|
|
14
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The root-relative path keys to exclude. A key excludes itself and every key below it, matched
|
|
19
|
+
* on whole segments, so `excluded` drops `excluded/file.ts` and keeps `excluded-other/file.ts`.
|
|
20
|
+
*/
|
|
15
21
|
readonly exclude?: readonly string[];
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
|
-
*
|
|
25
|
+
* Reports whether a root-relative key matches an exclusion.
|
|
26
|
+
*
|
|
27
|
+
* @param key - The root-relative key to test.
|
|
28
|
+
* @param exclusions - The normalized root-relative exclusion keys.
|
|
29
|
+
* @returns Whether an exclusion names the key or one of its ancestors.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reports whether two directory identities name the same allocation.
|
|
35
|
+
*
|
|
36
|
+
* @param current - The identity read from the path now.
|
|
37
|
+
* @param allocation - The identity recorded when the directory was allocated.
|
|
38
|
+
* @returns Whether the device, the index node, and the creation time all match.
|
|
39
|
+
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
40
|
+
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
41
|
+
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
42
|
+
*/
|
|
43
|
+
export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads files from selected targets below a root directory.
|
|
20
47
|
*
|
|
21
48
|
* @param root - The root directory as a path or file URL.
|
|
22
|
-
* @param
|
|
23
|
-
* @param options - Optional file extension and
|
|
49
|
+
* @param targets - The files to read directly and directories to visit below the root.
|
|
50
|
+
* @param options - Optional file extension and path exclusions.
|
|
24
51
|
* @returns File contents keyed by sorted root-relative paths.
|
|
25
|
-
* @
|
|
52
|
+
* @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves
|
|
53
|
+
* outside the root.
|
|
54
|
+
* @remarks A named file is included regardless of the extension filter. An absent extension filter
|
|
55
|
+
* includes every walked file. An exclusion matches whole root-relative key segments and covers every
|
|
56
|
+
* key below it, and it applies to a named target and a walked entry alike.
|
|
26
57
|
*/
|
|
27
|
-
export declare function readInventory(root: URL | string,
|
|
58
|
+
export declare function readInventory(root: URL | string, targets: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
|
|
28
59
|
|
|
29
60
|
/**
|
|
30
61
|
* Resolves a target that stays below a root directory.
|
|
@@ -35,6 +66,16 @@ export declare function readInventory(root: URL | string, directories: readonly
|
|
|
35
66
|
*/
|
|
36
67
|
export declare function resolveContained(root: string, target: string): string | undefined;
|
|
37
68
|
|
|
69
|
+
/** The fields that together identify one allocated directory on its host. */
|
|
70
|
+
export declare interface ScratchIdentity {
|
|
71
|
+
/** The identifier of the device holding the directory. */
|
|
72
|
+
readonly device: number;
|
|
73
|
+
/** The number of the directory's index node on that device. */
|
|
74
|
+
readonly inode: number;
|
|
75
|
+
/** The directory's creation time in milliseconds. */
|
|
76
|
+
readonly birth: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
/** A temporary directory a test owns, writes into, reads back, and removes when it is done. */
|
|
39
80
|
export declare interface ScratchInterface {
|
|
40
81
|
/** The absolute path of the allocated directory. */
|
|
@@ -44,6 +85,8 @@ export declare interface ScratchInterface {
|
|
|
44
85
|
*
|
|
45
86
|
* @param target - A relative or absolute file path contained by the scratch directory.
|
|
46
87
|
* @param text - The file contents.
|
|
88
|
+
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
89
|
+
* link, or a file, or the host refuses to write the file.
|
|
47
90
|
*/
|
|
48
91
|
write(target: string, text: string): void;
|
|
49
92
|
/**
|
|
@@ -58,22 +101,70 @@ export declare interface ScratchInterface {
|
|
|
58
101
|
*/
|
|
59
102
|
read(target: string): string | undefined;
|
|
60
103
|
/**
|
|
61
|
-
* Reports whether a path exists.
|
|
104
|
+
* Reports whether a path exists without following its final symbolic link.
|
|
62
105
|
*
|
|
63
106
|
* @param target - A relative or absolute path contained by the scratch directory.
|
|
64
107
|
* @returns True when the entry exists, including a symbolic link whose target is missing.
|
|
65
108
|
* @throws When the path escapes the scratch directory or its root is a symbolic link or file.
|
|
66
109
|
*/
|
|
67
|
-
|
|
68
|
-
/**
|
|
110
|
+
has(target: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Lists the names directly inside a directory in sorted order.
|
|
113
|
+
*
|
|
114
|
+
* @param target - A relative or absolute directory path contained by the scratch directory. The
|
|
115
|
+
* scratch root is used when omitted.
|
|
116
|
+
* @returns The sorted entry names, without their parent paths.
|
|
117
|
+
* @throws When the path escapes the scratch directory, the target is missing or is not a
|
|
118
|
+
* directory, or the scratch root is a symbolic link or file.
|
|
119
|
+
*/
|
|
120
|
+
names(target?: string): readonly string[];
|
|
121
|
+
/**
|
|
122
|
+
* Creates a directory and every missing parent.
|
|
123
|
+
*
|
|
124
|
+
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
125
|
+
* @returns The absolute path of the directory.
|
|
126
|
+
* @throws When the path escapes the scratch directory, the target exists and is not a directory,
|
|
127
|
+
* or the scratch root is missing, a symbolic link, or a file.
|
|
128
|
+
*/
|
|
129
|
+
ensure(target: string): string;
|
|
130
|
+
/**
|
|
131
|
+
* Creates a symbolic link at a contained path, creating its missing parent directories.
|
|
132
|
+
*
|
|
133
|
+
* @param target - The relative or absolute contained path where the link is created. Unlike
|
|
134
|
+
* `node:fs`'s `symlinkSync(target, path)` vocabulary, this interface consistently calls the
|
|
135
|
+
* contained path the target.
|
|
136
|
+
* @param source - The link text naming the pointed-at path. It may name a path outside the scratch
|
|
137
|
+
* directory and is not containment-checked.
|
|
138
|
+
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
139
|
+
* link, or a file, or the host refuses to create the link.
|
|
140
|
+
*/
|
|
141
|
+
link(target: string, source: string): void;
|
|
142
|
+
/**
|
|
143
|
+
* Removes the allocated directory and everything in it when its identity still matches.
|
|
144
|
+
*
|
|
145
|
+
* @throws When the host refuses to inspect or remove the matching allocation.
|
|
146
|
+
*/
|
|
69
147
|
destroy(): void;
|
|
70
148
|
}
|
|
71
149
|
|
|
72
150
|
/** Options for allocating a scratch directory. */
|
|
73
151
|
export declare interface ScratchOptions {
|
|
74
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* The existing directory in which to create the allocation. Defaults to the host temporary
|
|
154
|
+
* directory. Allocation throws when this path is missing, a symbolic link, or not a directory.
|
|
155
|
+
*/
|
|
156
|
+
readonly parent?: string;
|
|
157
|
+
/**
|
|
158
|
+
* The name fragment that starts the generated directory name. Allocation throws when this value
|
|
159
|
+
* contains `/` or `\`. Both are refused on every host, so the rule does not vary by host. A
|
|
160
|
+
* fragment carrying no separator is one path segment and cannot steer the allocation, so
|
|
161
|
+
* `release-0..2-` allocates.
|
|
162
|
+
*/
|
|
75
163
|
readonly prefix?: string;
|
|
76
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Files to write on allocation, keyed by path below the scratch directory. Allocation removes its
|
|
166
|
+
* directory and rethrows when a key escapes or the host refuses a write.
|
|
167
|
+
*/
|
|
77
168
|
readonly files?: Readonly<Record<string, string>>;
|
|
78
169
|
}
|
|
79
170
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Allocates an owned temporary directory with contained file operations.
|
|
3
3
|
*
|
|
4
|
-
* @param options - Optional directory prefix and initial files.
|
|
4
|
+
* @param options - Optional parent directory, name prefix, and initial files.
|
|
5
5
|
* @returns The scratch directory and its file operations.
|
|
6
|
-
* @
|
|
6
|
+
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
7
|
+
* `/` or `\`; or when allocation or seeding fails.
|
|
8
|
+
* @remarks The parent defaults to the host temporary directory. The prefix defaults to
|
|
9
|
+
* `orkestrel-test-`. Seed keys use root-relative paths.
|
|
7
10
|
*/
|
|
8
11
|
export declare function createScratch(options?: ScratchOptions): ScratchInterface;
|
|
9
12
|
|
|
@@ -11,20 +14,48 @@ export declare function createScratch(options?: ScratchOptions): ScratchInterfac
|
|
|
11
14
|
export declare interface InventoryOptions {
|
|
12
15
|
/** The file extensions to include, each written with its leading dot. */
|
|
13
16
|
readonly extensions?: readonly string[];
|
|
14
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* The root-relative path keys to exclude. A key excludes itself and every key below it, matched
|
|
19
|
+
* on whole segments, so `excluded` drops `excluded/file.ts` and keeps `excluded-other/file.ts`.
|
|
20
|
+
*/
|
|
15
21
|
readonly exclude?: readonly string[];
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
/**
|
|
19
|
-
*
|
|
25
|
+
* Reports whether a root-relative key matches an exclusion.
|
|
26
|
+
*
|
|
27
|
+
* @param key - The root-relative key to test.
|
|
28
|
+
* @param exclusions - The normalized root-relative exclusion keys.
|
|
29
|
+
* @returns Whether an exclusion names the key or one of its ancestors.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reports whether two directory identities name the same allocation.
|
|
35
|
+
*
|
|
36
|
+
* @param current - The identity read from the path now.
|
|
37
|
+
* @param allocation - The identity recorded when the directory was allocated.
|
|
38
|
+
* @returns Whether the device, the index node, and the creation time all match.
|
|
39
|
+
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
40
|
+
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
41
|
+
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
42
|
+
*/
|
|
43
|
+
export declare function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reads files from selected targets below a root directory.
|
|
20
47
|
*
|
|
21
48
|
* @param root - The root directory as a path or file URL.
|
|
22
|
-
* @param
|
|
23
|
-
* @param options - Optional file extension and
|
|
49
|
+
* @param targets - The files to read directly and directories to visit below the root.
|
|
50
|
+
* @param options - Optional file extension and path exclusions.
|
|
24
51
|
* @returns File contents keyed by sorted root-relative paths.
|
|
25
|
-
* @
|
|
52
|
+
* @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves
|
|
53
|
+
* outside the root.
|
|
54
|
+
* @remarks A named file is included regardless of the extension filter. An absent extension filter
|
|
55
|
+
* includes every walked file. An exclusion matches whole root-relative key segments and covers every
|
|
56
|
+
* key below it, and it applies to a named target and a walked entry alike.
|
|
26
57
|
*/
|
|
27
|
-
export declare function readInventory(root: URL | string,
|
|
58
|
+
export declare function readInventory(root: URL | string, targets: readonly string[], options?: InventoryOptions): Readonly<Record<string, string>>;
|
|
28
59
|
|
|
29
60
|
/**
|
|
30
61
|
* Resolves a target that stays below a root directory.
|
|
@@ -35,6 +66,16 @@ export declare function readInventory(root: URL | string, directories: readonly
|
|
|
35
66
|
*/
|
|
36
67
|
export declare function resolveContained(root: string, target: string): string | undefined;
|
|
37
68
|
|
|
69
|
+
/** The fields that together identify one allocated directory on its host. */
|
|
70
|
+
export declare interface ScratchIdentity {
|
|
71
|
+
/** The identifier of the device holding the directory. */
|
|
72
|
+
readonly device: number;
|
|
73
|
+
/** The number of the directory's index node on that device. */
|
|
74
|
+
readonly inode: number;
|
|
75
|
+
/** The directory's creation time in milliseconds. */
|
|
76
|
+
readonly birth: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
38
79
|
/** A temporary directory a test owns, writes into, reads back, and removes when it is done. */
|
|
39
80
|
export declare interface ScratchInterface {
|
|
40
81
|
/** The absolute path of the allocated directory. */
|
|
@@ -44,6 +85,8 @@ export declare interface ScratchInterface {
|
|
|
44
85
|
*
|
|
45
86
|
* @param target - A relative or absolute file path contained by the scratch directory.
|
|
46
87
|
* @param text - The file contents.
|
|
88
|
+
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
89
|
+
* link, or a file, or the host refuses to write the file.
|
|
47
90
|
*/
|
|
48
91
|
write(target: string, text: string): void;
|
|
49
92
|
/**
|
|
@@ -58,22 +101,70 @@ export declare interface ScratchInterface {
|
|
|
58
101
|
*/
|
|
59
102
|
read(target: string): string | undefined;
|
|
60
103
|
/**
|
|
61
|
-
* Reports whether a path exists.
|
|
104
|
+
* Reports whether a path exists without following its final symbolic link.
|
|
62
105
|
*
|
|
63
106
|
* @param target - A relative or absolute path contained by the scratch directory.
|
|
64
107
|
* @returns True when the entry exists, including a symbolic link whose target is missing.
|
|
65
108
|
* @throws When the path escapes the scratch directory or its root is a symbolic link or file.
|
|
66
109
|
*/
|
|
67
|
-
|
|
68
|
-
/**
|
|
110
|
+
has(target: string): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Lists the names directly inside a directory in sorted order.
|
|
113
|
+
*
|
|
114
|
+
* @param target - A relative or absolute directory path contained by the scratch directory. The
|
|
115
|
+
* scratch root is used when omitted.
|
|
116
|
+
* @returns The sorted entry names, without their parent paths.
|
|
117
|
+
* @throws When the path escapes the scratch directory, the target is missing or is not a
|
|
118
|
+
* directory, or the scratch root is a symbolic link or file.
|
|
119
|
+
*/
|
|
120
|
+
names(target?: string): readonly string[];
|
|
121
|
+
/**
|
|
122
|
+
* Creates a directory and every missing parent.
|
|
123
|
+
*
|
|
124
|
+
* @param target - A relative or absolute directory path contained by the scratch directory.
|
|
125
|
+
* @returns The absolute path of the directory.
|
|
126
|
+
* @throws When the path escapes the scratch directory, the target exists and is not a directory,
|
|
127
|
+
* or the scratch root is missing, a symbolic link, or a file.
|
|
128
|
+
*/
|
|
129
|
+
ensure(target: string): string;
|
|
130
|
+
/**
|
|
131
|
+
* Creates a symbolic link at a contained path, creating its missing parent directories.
|
|
132
|
+
*
|
|
133
|
+
* @param target - The relative or absolute contained path where the link is created. Unlike
|
|
134
|
+
* `node:fs`'s `symlinkSync(target, path)` vocabulary, this interface consistently calls the
|
|
135
|
+
* contained path the target.
|
|
136
|
+
* @param source - The link text naming the pointed-at path. It may name a path outside the scratch
|
|
137
|
+
* directory and is not containment-checked.
|
|
138
|
+
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
139
|
+
* link, or a file, or the host refuses to create the link.
|
|
140
|
+
*/
|
|
141
|
+
link(target: string, source: string): void;
|
|
142
|
+
/**
|
|
143
|
+
* Removes the allocated directory and everything in it when its identity still matches.
|
|
144
|
+
*
|
|
145
|
+
* @throws When the host refuses to inspect or remove the matching allocation.
|
|
146
|
+
*/
|
|
69
147
|
destroy(): void;
|
|
70
148
|
}
|
|
71
149
|
|
|
72
150
|
/** Options for allocating a scratch directory. */
|
|
73
151
|
export declare interface ScratchOptions {
|
|
74
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* The existing directory in which to create the allocation. Defaults to the host temporary
|
|
154
|
+
* directory. Allocation throws when this path is missing, a symbolic link, or not a directory.
|
|
155
|
+
*/
|
|
156
|
+
readonly parent?: string;
|
|
157
|
+
/**
|
|
158
|
+
* The name fragment that starts the generated directory name. Allocation throws when this value
|
|
159
|
+
* contains `/` or `\`. Both are refused on every host, so the rule does not vary by host. A
|
|
160
|
+
* fragment carrying no separator is one path segment and cannot steer the allocation, so
|
|
161
|
+
* `release-0..2-` allocates.
|
|
162
|
+
*/
|
|
75
163
|
readonly prefix?: string;
|
|
76
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* Files to write on allocation, keyed by path below the scratch directory. Allocation removes its
|
|
166
|
+
* directory and rethrows when a key escapes or the host refuses a write.
|
|
167
|
+
*/
|
|
77
168
|
readonly files?: Readonly<Record<string, string>>;
|
|
78
169
|
}
|
|
79
170
|
|
package/dist/src/server/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
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
4
|
import { tmpdir } from "node:os";
|
|
@@ -17,36 +17,72 @@ function resolveContained(root, target) {
|
|
|
17
17
|
return candidate;
|
|
18
18
|
}
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Reports whether two directory identities name the same allocation.
|
|
21
|
+
*
|
|
22
|
+
* @param current - The identity read from the path now.
|
|
23
|
+
* @param allocation - The identity recorded when the directory was allocated.
|
|
24
|
+
* @returns Whether the device, the index node, and the creation time all match.
|
|
25
|
+
* @remarks All three fields are compared because none of them alone identifies an allocation. A
|
|
26
|
+
* device is shared by every directory on one filesystem, an index node is reused once its directory
|
|
27
|
+
* is removed, and a creation time repeats within the host's timestamp resolution.
|
|
28
|
+
*/
|
|
29
|
+
function matchesIdentity(current, allocation) {
|
|
30
|
+
return current.device === allocation.device && current.inode === allocation.inode && current.birth === allocation.birth;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reports whether a root-relative key matches an exclusion.
|
|
34
|
+
*
|
|
35
|
+
* @param key - The root-relative key to test.
|
|
36
|
+
* @param exclusions - The normalized root-relative exclusion keys.
|
|
37
|
+
* @returns Whether an exclusion names the key or one of its ancestors.
|
|
38
|
+
*/
|
|
39
|
+
function isExcluded(key, exclusions) {
|
|
40
|
+
return exclusions.some((rule) => rule === "" || key === rule || key.startsWith(`${rule}/`));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Reads files from selected targets below a root directory.
|
|
21
44
|
*
|
|
22
45
|
* @param root - The root directory as a path or file URL.
|
|
23
|
-
* @param
|
|
24
|
-
* @param options - Optional file extension and
|
|
46
|
+
* @param targets - The files to read directly and directories to visit below the root.
|
|
47
|
+
* @param options - Optional file extension and path exclusions.
|
|
25
48
|
* @returns File contents keyed by sorted root-relative paths.
|
|
26
|
-
* @
|
|
49
|
+
* @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves
|
|
50
|
+
* outside the root.
|
|
51
|
+
* @remarks A named file is included regardless of the extension filter. An absent extension filter
|
|
52
|
+
* includes every walked file. An exclusion matches whole root-relative key segments and covers every
|
|
53
|
+
* key below it, and it applies to a named target and a walked entry alike.
|
|
27
54
|
*/
|
|
28
|
-
function readInventory(root,
|
|
55
|
+
function readInventory(root, targets, options) {
|
|
29
56
|
const supplied = resolve(typeof root === "string" ? root : fileURLToPath(root));
|
|
30
57
|
const rootStatus = lstatSync(supplied);
|
|
31
58
|
if (rootStatus.isSymbolicLink()) throw new Error("Root is a symbolic link");
|
|
32
59
|
if (!rootStatus.isDirectory()) throw new Error("Root is not a directory");
|
|
33
60
|
const base = realpathSync.native(supplied);
|
|
34
|
-
if (
|
|
35
|
-
const
|
|
61
|
+
if (targets.length === 0) return Object.fromEntries([]);
|
|
62
|
+
const exclusions = (options?.exclude ?? []).map((rule) => {
|
|
63
|
+
const collapsed = (rule.startsWith("./") ? rule.slice(2) : rule).replace(/\/+/g, "/");
|
|
64
|
+
const untrailed = collapsed.endsWith("/") ? collapsed.slice(0, -1) : collapsed;
|
|
65
|
+
return untrailed === "." ? "" : untrailed;
|
|
66
|
+
});
|
|
36
67
|
const pending = [];
|
|
37
68
|
const queued = /* @__PURE__ */ new Set();
|
|
38
69
|
const contents = /* @__PURE__ */ new Map();
|
|
39
|
-
for (const
|
|
40
|
-
const candidate = resolveContained(base,
|
|
41
|
-
if (candidate === void 0) throw new Error(`
|
|
70
|
+
for (const target of targets) {
|
|
71
|
+
const candidate = resolveContained(base, target);
|
|
72
|
+
if (candidate === void 0) throw new Error(`Target outside root: ${target}`);
|
|
42
73
|
const status = lstatSync(candidate);
|
|
43
|
-
if (status.isSymbolicLink()) throw new Error(`
|
|
44
|
-
if (!status.isDirectory()) throw new Error(`
|
|
74
|
+
if (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`);
|
|
75
|
+
if (!status.isDirectory() && !status.isFile()) throw new Error(`Target is not a file or directory: ${target}`);
|
|
45
76
|
const physical = realpathSync.native(candidate);
|
|
46
77
|
const resolved = resolveContained(base, relative(base, physical));
|
|
47
|
-
if (resolved === void 0) throw new Error(`
|
|
78
|
+
if (resolved === void 0) throw new Error(`Target outside root: ${target}`);
|
|
48
79
|
const key = relative(base, resolved).split(sep).join("/");
|
|
49
|
-
if (
|
|
80
|
+
if (isExcluded(key, exclusions)) continue;
|
|
81
|
+
if (status.isFile()) {
|
|
82
|
+
contents.set(key, readFileSync(physical, "utf8"));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (queued.has(physical)) continue;
|
|
50
86
|
queued.add(physical);
|
|
51
87
|
pending.push(physical);
|
|
52
88
|
}
|
|
@@ -58,7 +94,7 @@ function readInventory(root, directories, options) {
|
|
|
58
94
|
const status = lstatSync(path);
|
|
59
95
|
if (status.isSymbolicLink()) continue;
|
|
60
96
|
const key = relative(base, path).split(sep).join("/");
|
|
61
|
-
if (
|
|
97
|
+
if (isExcluded(key, exclusions)) continue;
|
|
62
98
|
if (status.isDirectory()) {
|
|
63
99
|
const physical = realpathSync.native(path);
|
|
64
100
|
if (resolveContained(base, relative(base, physical)) === void 0 || queued.has(physical)) continue;
|
|
@@ -77,16 +113,28 @@ function readInventory(root, directories, options) {
|
|
|
77
113
|
/**
|
|
78
114
|
* Allocates an owned temporary directory with contained file operations.
|
|
79
115
|
*
|
|
80
|
-
* @param options - Optional directory prefix and initial files.
|
|
116
|
+
* @param options - Optional parent directory, name prefix, and initial files.
|
|
81
117
|
* @returns The scratch directory and its file operations.
|
|
82
|
-
* @
|
|
118
|
+
* @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains
|
|
119
|
+
* `/` or `\`; or when allocation or seeding fails.
|
|
120
|
+
* @remarks The parent defaults to the host temporary directory. The prefix defaults to
|
|
121
|
+
* `orkestrel-test-`. Seed keys use root-relative paths.
|
|
83
122
|
*/
|
|
84
123
|
function createScratch(options) {
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
88
|
-
|
|
89
|
-
|
|
124
|
+
const parent = resolve(options?.parent ?? tmpdir());
|
|
125
|
+
const parentStatus = lstatSync(parent, { throwIfNoEntry: false });
|
|
126
|
+
if (parentStatus === void 0) throw new Error("Scratch parent does not exist");
|
|
127
|
+
if (parentStatus.isSymbolicLink()) throw new Error("Scratch parent is a symbolic link");
|
|
128
|
+
if (!parentStatus.isDirectory()) throw new Error("Scratch parent is not a directory");
|
|
129
|
+
const prefix = options?.prefix ?? "orkestrel-test-";
|
|
130
|
+
if (prefix.includes("/") || prefix.includes("\\")) throw new Error("Scratch prefix must be a name fragment");
|
|
131
|
+
const path = mkdtempSync(`${parent}${sep}${prefix}`);
|
|
132
|
+
const allocated = statSync(path);
|
|
133
|
+
const allocation = {
|
|
134
|
+
birth: allocated.birthtimeMs,
|
|
135
|
+
device: allocated.dev,
|
|
136
|
+
inode: allocated.ino
|
|
137
|
+
};
|
|
90
138
|
const outside = "Path outside scratch directory";
|
|
91
139
|
try {
|
|
92
140
|
for (const [target, text] of Object.entries(options?.files ?? {})) {
|
|
@@ -107,23 +155,20 @@ function createScratch(options) {
|
|
|
107
155
|
write(target, text) {
|
|
108
156
|
const candidate = resolveContained(path, target);
|
|
109
157
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
110
|
-
|
|
111
|
-
if (rootStatus === void 0) throw new Error("Scratch directory does not exist");
|
|
112
|
-
if (rootStatus.isSymbolicLink()) throw new Error("Scratch directory is a symbolic link");
|
|
113
|
-
if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
|
|
158
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
114
159
|
mkdirSync(dirname(candidate), { recursive: true });
|
|
115
160
|
writeFileSync(candidate, text);
|
|
116
161
|
},
|
|
117
162
|
read(target) {
|
|
118
163
|
const candidate = resolveContained(path, target);
|
|
119
164
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
120
|
-
if (!scratch.
|
|
165
|
+
if (!scratch.has(target)) return void 0;
|
|
121
166
|
const status = statSync(candidate, { throwIfNoEntry: false });
|
|
122
167
|
if (status === void 0) return void 0;
|
|
123
168
|
if (status.isDirectory()) throw new Error(`Scratch path is a directory: ${target}`);
|
|
124
169
|
return readFileSync(candidate, "utf8");
|
|
125
170
|
},
|
|
126
|
-
|
|
171
|
+
has(target) {
|
|
127
172
|
const candidate = resolveContained(path, target);
|
|
128
173
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
129
174
|
const rootStatus = lstatSync(path, { throwIfNoEntry: false });
|
|
@@ -132,9 +177,39 @@ function createScratch(options) {
|
|
|
132
177
|
if (!rootStatus.isDirectory()) throw new Error("Scratch path is not a directory");
|
|
133
178
|
return lstatSync(candidate, { throwIfNoEntry: false }) !== void 0;
|
|
134
179
|
},
|
|
180
|
+
names(target = ".") {
|
|
181
|
+
const candidate = resolveContained(path, target);
|
|
182
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
183
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
184
|
+
const status = statSync(candidate, { throwIfNoEntry: false });
|
|
185
|
+
if (status === void 0) throw new Error(`Scratch path does not exist: ${target}`);
|
|
186
|
+
if (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`);
|
|
187
|
+
return readdirSync(candidate).sort();
|
|
188
|
+
},
|
|
189
|
+
ensure(target) {
|
|
190
|
+
const candidate = resolveContained(path, target);
|
|
191
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
192
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
193
|
+
const status = statSync(candidate, { throwIfNoEntry: false });
|
|
194
|
+
if (status !== void 0 && !status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`);
|
|
195
|
+
if (status === void 0) mkdirSync(candidate, { recursive: true });
|
|
196
|
+
return candidate;
|
|
197
|
+
},
|
|
198
|
+
link(target, source) {
|
|
199
|
+
const candidate = resolveContained(path, target);
|
|
200
|
+
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
201
|
+
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
202
|
+
mkdirSync(dirname(candidate), { recursive: true });
|
|
203
|
+
symlinkSync(source, candidate);
|
|
204
|
+
},
|
|
135
205
|
destroy() {
|
|
136
206
|
const status = lstatSync(path, { throwIfNoEntry: false });
|
|
137
|
-
if (status === void 0
|
|
207
|
+
if (status === void 0) return;
|
|
208
|
+
if (!matchesIdentity({
|
|
209
|
+
birth: status.birthtimeMs,
|
|
210
|
+
device: status.dev,
|
|
211
|
+
inode: status.ino
|
|
212
|
+
}, allocation)) return;
|
|
138
213
|
rmSync(path, {
|
|
139
214
|
force: true,
|
|
140
215
|
recursive: true
|
|
@@ -144,6 +219,6 @@ function createScratch(options) {
|
|
|
144
219
|
return scratch;
|
|
145
220
|
}
|
|
146
221
|
//#endregion
|
|
147
|
-
export { createScratch, readInventory, resolveContained };
|
|
222
|
+
export { createScratch, isExcluded, matchesIdentity, readInventory, resolveContained };
|
|
148
223
|
|
|
149
224
|
//# sourceMappingURL=index.js.map
|