Extracted from alkhub_ts packages/core/operations/ and packages/core/mcp/. - Runtime-agnostic (injected fs/env deps, no Deno globals) - Direct @logtape/logtape import instead of logger wrapper - PendingRequestMap with pubsub-wired call protocol - Peer-dep isolation for MCP adapter (sub-path export) - Schema const naming convention (XSchema + X type alias) - 68 tests passing, build + lint + test all green
2.4 KiB
ADR-002: Inject Filesystem Dependencies for Runtime Agnosticism
Status: Accepted Date: 2026-04-30
Context
The operations package must work in both Node.js and Deno. Two functions need filesystem access:
scanOperations(dirPath, fs)— recursive directory scan for.tsoperation filesFromOpenAPIFile(path, config, fs?)— read OpenAPI JSON spec from filesystem
In Node.js, these use node:fs/promises and node:path. In Deno, they would use Deno.readDir() and Deno.cwd(). Direct use of Node APIs would break Deno; direct use of Deno globals would break Node.
Decision
Inject filesystem dependencies through interfaces, not global imports.
interface ScannerFS {
readdir(path: string): AsyncIterable<{ name: string; isFile: boolean; isDirectory: boolean }>
cwd(): string
}
interface OpenAPIFS {
readFile(path: string): Promise<string>
}
Callers provide the FS implementation. When OpenAPIFS is not provided, FromOpenAPIFile falls back to node:fs/promises via dynamic import.
Rationale
-
No platform globals in source — no
Deno.*calls anywhere insrc/. Both Node and Deno consumers work by providing the right FS interface. -
Testability — tests provide mock FS implementations. No filesystem mocking libraries needed.
-
Consistent pattern —
ScannerFSandOpenAPIFSfollow the same pattern: minimal interface, consumer-provided implementation, optional Node fallback. -
Deno path module — the original alkhub scanner used
@std/path(Deno standard library path module) forresolve()andextname(). The extracted version avoids this dependency by using simple string operations (endsWith(".ts"), path construction with/). -
Node fallback is dynamic —
FromOpenAPIFileusesawait import("node:fs/promises")as a fallback when nofsis provided. This keeps the Node path out of the module graph when a custom FS is injected, and avoids top-level Node imports that would break Deno.
Consequences
- Callers in Deno must provide
ScannerFSandOpenAPIFSimplementations usingDeno.readDir()andDeno.readTextFile() - Callers in Node can omit the
fsparameter forFromOpenAPIFile(Node fallback) but must provideScannerFSforscanOperations - The
pathToFileURLhelper in scanner uses a simplefile://prefix construction rather thanurl.pathToFileURL()to avoid importing Node'surlmodule