|
@@ -8,22 +8,100 @@ import path from "path"
|
|
|
/**
|
|
/**
|
|
|
* Simulation replacement for `FSUtil`.
|
|
* Simulation replacement for `FSUtil`.
|
|
|
*
|
|
*
|
|
|
- * The real `FSUtil` layer builds most helpers on the injected
|
|
|
|
|
- * `FileSystem.FileSystem`, but `readDirectoryEntries`, `glob`, and `globUp`
|
|
|
|
|
- * reach for node `fs/promises` and the `glob` package directly, and `resolve`
|
|
|
|
|
- * canonicalizes through the host filesystem. This wraps the real layer and
|
|
|
|
|
- * reroutes those through the injected `FileSystem`/lexical path resolution so
|
|
|
|
|
- * every read observes the in-memory tree.
|
|
|
|
|
|
|
+ * This implementation is intentionally self-contained and only uses the
|
|
|
|
|
+ * injected simulated `FileSystem.FileSystem`. The default FSUtil layer has a
|
|
|
|
|
+ * few helpers that reach host-node APIs directly; depending on it here makes it
|
|
|
|
|
+ * easy for mutation paths to escape or miss the in-memory project tree.
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
const layer = Layer.effect(
|
|
const layer = Layer.effect(
|
|
|
FSUtil.Service,
|
|
FSUtil.Service,
|
|
|
Effect.gen(function* () {
|
|
Effect.gen(function* () {
|
|
|
- const base = yield* FSUtil.Service
|
|
|
|
|
const fs = yield* FileSystem.FileSystem
|
|
const fs = yield* FileSystem.FileSystem
|
|
|
|
|
|
|
|
- const resolve = Effect.fn("SimulationFSUtil.resolve")(function* (input: string) {
|
|
|
|
|
- return input
|
|
|
|
|
|
|
+ const existsSafe = Effect.fn("SimulationFSUtil.existsSafe")(function* (file: string) {
|
|
|
|
|
+ return yield* fs.exists(file).pipe(Effect.orElseSucceed(() => false))
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const isDir = Effect.fn("SimulationFSUtil.isDir")(function* (file: string) {
|
|
|
|
|
+ const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
|
|
|
+ return info?.type === "Directory"
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const isFile = Effect.fn("SimulationFSUtil.isFile")(function* (file: string) {
|
|
|
|
|
+ const info = yield* fs.stat(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
|
|
|
+ return info?.type === "File"
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const realPath = Effect.fn("SimulationFSUtil.realPath")(function* (file: string) {
|
|
|
|
|
+ return yield* fs.realPath(file)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const stat = Effect.fn("SimulationFSUtil.stat")(function* (file: string) {
|
|
|
|
|
+ return yield* fs.stat(file)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const readFile = Effect.fn("SimulationFSUtil.readFile")(function* (file: string) {
|
|
|
|
|
+ return yield* fs.readFile(file)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const readFileString = Effect.fn("SimulationFSUtil.readFileString")(function* (file: string) {
|
|
|
|
|
+ return yield* fs.readFileString(file)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const readFileStringSafe = Effect.fn("SimulationFSUtil.readFileStringSafe")(function* (file: string) {
|
|
|
|
|
+ return yield* fs
|
|
|
|
|
+ .readFileString(file)
|
|
|
|
|
+ .pipe(Effect.catchReason("PlatformError", "NotFound", () => Effect.succeed(undefined)))
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const readJson = Effect.fn("SimulationFSUtil.readJson")(function* (file: string) {
|
|
|
|
|
+ const text = yield* readFileString(file)
|
|
|
|
|
+ return JSON.parse(text) as unknown
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const writeFile = Effect.fn("SimulationFSUtil.writeFile")(function* (
|
|
|
|
|
+ file: string,
|
|
|
|
|
+ data: Uint8Array,
|
|
|
|
|
+ options?: Parameters<typeof fs.writeFile>[2],
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return yield* fs.writeFile(file, data, options)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const writeFileString = Effect.fn("SimulationFSUtil.writeFileString")(function* (
|
|
|
|
|
+ file: string,
|
|
|
|
|
+ data: string,
|
|
|
|
|
+ options?: Parameters<typeof fs.writeFileString>[2],
|
|
|
|
|
+ ) {
|
|
|
|
|
+ return yield* fs.writeFileString(file, data, options)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const makeDirectory: FileSystem.FileSystem["makeDirectory"] = (file, options) => fs.makeDirectory(file, options)
|
|
|
|
|
+
|
|
|
|
|
+ const ensureDir = Effect.fn("SimulationFSUtil.ensureDir")(function* (file: string) {
|
|
|
|
|
+ yield* fs.makeDirectory(file, { recursive: true })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const writeWithDirs = Effect.fn("SimulationFSUtil.writeWithDirs")(function* (
|
|
|
|
|
+ file: string,
|
|
|
|
|
+ content: string | Uint8Array,
|
|
|
|
|
+ mode?: number,
|
|
|
|
|
+ ) {
|
|
|
|
|
+ const write =
|
|
|
|
|
+ typeof content === "string"
|
|
|
|
|
+ ? fs.writeFileString(file, content)
|
|
|
|
|
+ : fs.writeFile(file, content)
|
|
|
|
|
+ yield* write.pipe(
|
|
|
|
|
+ Effect.catchReason("PlatformError", "NotFound", () =>
|
|
|
|
|
+ fs.makeDirectory(path.dirname(file), { recursive: true }).pipe(Effect.andThen(write)),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+ if (mode !== undefined) yield* fs.chmod(file, mode)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const writeJson = Effect.fn("SimulationFSUtil.writeJson")(function* (file: string, data: unknown, mode?: number) {
|
|
|
|
|
+ yield* writeFileString(file, JSON.stringify(data, null, 2))
|
|
|
|
|
+ if (mode !== undefined) yield* fs.chmod(file, mode)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const readDirectoryEntries = Effect.fn("SimulationFSUtil.readDirectoryEntries")(function* (dirPath: string) {
|
|
const readDirectoryEntries = Effect.fn("SimulationFSUtil.readDirectoryEntries")(function* (dirPath: string) {
|
|
@@ -48,6 +126,10 @@ const layer = Layer.effect(
|
|
|
)
|
|
)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+ const resolve = Effect.fn("SimulationFSUtil.resolve")(function* (input: string) {
|
|
|
|
|
+ return path.resolve(input)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
const glob = Effect.fn("SimulationFSUtil.glob")(function* (pattern: string, options?: Glob.Options) {
|
|
const glob = Effect.fn("SimulationFSUtil.glob")(function* (pattern: string, options?: Glob.Options) {
|
|
|
const cwd = path.resolve(options?.cwd ?? process.cwd())
|
|
const cwd = path.resolve(options?.cwd ?? process.cwd())
|
|
|
const entries = yield* fs
|
|
const entries = yield* fs
|
|
@@ -80,9 +162,53 @@ const layer = Layer.effect(
|
|
|
return result
|
|
return result
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- return FSUtil.Service.of({ ...base, readDirectoryEntries, resolve, glob, globUp })
|
|
|
|
|
|
|
+ const up = Effect.fn("SimulationFSUtil.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
|
|
|
|
|
+ const result: string[] = []
|
|
|
|
|
+ let current = path.resolve(options.start)
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ for (const target of options.targets) {
|
|
|
|
|
+ const search = path.join(current, target)
|
|
|
|
|
+ if (yield* fs.exists(search)) result.push(search)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (options.stop === current) break
|
|
|
|
|
+ const parent = path.dirname(current)
|
|
|
|
|
+ if (parent === current) break
|
|
|
|
|
+ current = parent
|
|
|
|
|
+ }
|
|
|
|
|
+ return result
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const findUp = Effect.fn("SimulationFSUtil.findUp")(function* (target: string, start: string, stop?: string) {
|
|
|
|
|
+ return yield* up({ targets: [target], start, stop })
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ return FSUtil.Service.of({
|
|
|
|
|
+ ...fs,
|
|
|
|
|
+ realPath,
|
|
|
|
|
+ stat,
|
|
|
|
|
+ readFile,
|
|
|
|
|
+ readFileString,
|
|
|
|
|
+ writeFile,
|
|
|
|
|
+ writeFileString,
|
|
|
|
|
+ makeDirectory,
|
|
|
|
|
+ isDir,
|
|
|
|
|
+ isFile,
|
|
|
|
|
+ existsSafe,
|
|
|
|
|
+ readFileStringSafe,
|
|
|
|
|
+ readJson,
|
|
|
|
|
+ writeJson,
|
|
|
|
|
+ ensureDir,
|
|
|
|
|
+ writeWithDirs,
|
|
|
|
|
+ readDirectoryEntries,
|
|
|
|
|
+ resolve,
|
|
|
|
|
+ findUp,
|
|
|
|
|
+ up,
|
|
|
|
|
+ globUp,
|
|
|
|
|
+ glob,
|
|
|
|
|
+ globMatch: Glob.match,
|
|
|
|
|
+ })
|
|
|
}),
|
|
}),
|
|
|
-).pipe(Layer.provide(FSUtil.layer))
|
|
|
|
|
|
|
+)
|
|
|
|
|
|
|
|
export const node = makeGlobalNode({ service: FSUtil.Service, layer, deps: [filesystem] })
|
|
export const node = makeGlobalNode({ service: FSUtil.Service, layer, deps: [filesystem] })
|
|
|
|
|
|