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
48 lines
2.4 KiB
Markdown
48 lines
2.4 KiB
Markdown
# 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:
|
|
|
|
1. `scanOperations(dirPath, fs)` — recursive directory scan for `.ts` operation files
|
|
2. `FromOpenAPIFile(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.
|
|
|
|
```ts
|
|
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
|
|
|
|
1. **No platform globals in source** — no `Deno.*` calls anywhere in `src/`. Both Node and Deno consumers work by providing the right FS interface.
|
|
|
|
2. **Testability** — tests provide mock FS implementations. No filesystem mocking libraries needed.
|
|
|
|
3. **Consistent pattern** — `ScannerFS` and `OpenAPIFS` follow the same pattern: minimal interface, consumer-provided implementation, optional Node fallback.
|
|
|
|
4. **Deno path module** — the original alkhub scanner used `@std/path` (Deno standard library path module) for `resolve()` and `extname()`. The extracted version avoids this dependency by using simple string operations (`endsWith(".ts")`, path construction with `/`).
|
|
|
|
5. **Node fallback is dynamic** — `FromOpenAPIFile` uses `await import("node:fs/promises")` as a fallback when no `fs` is 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 `ScannerFS` and `OpenAPIFS` implementations using `Deno.readDir()` and `Deno.readTextFile()`
|
|
- Callers in Node can omit the `fs` parameter for `FromOpenAPIFile` (Node fallback) but must provide `ScannerFS` for `scanOperations`
|
|
- The `pathToFileURL` helper in scanner uses a simple `file://` prefix construction rather than `url.pathToFileURL()` to avoid importing Node's `url` module |