177 lines · 5.7 KB · TypeScript
RawDownload
import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Path from "effect/Path"; import {  WorkspaceFileSystem,  WorkspaceFileSystemError,  type WorkspaceFileSystemShape,} from "../Services/WorkspaceFileSystem.ts";import * as WorkspaceEntries from "../WorkspaceEntries.ts";import { WorkspacePaths } from "../Services/WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; function makeReadFileError(input: {  readonly cwd: string;  readonly relativePath: string;  readonly detail: string;  readonly cause?: unknown;}) {  return new WorkspaceFileSystemError({    cwd: input.cwd,    relativePath: input.relativePath,    operation: "workspaceFileSystem.readFile",    detail: input.detail,    ...(input.cause === undefined ? {} : { cause: input.cause }),  });} function causeMessage(cause: { readonly message: string }) {  return cause.message;} export const makeWorkspaceFileSystem = Effect.gen(function* () {  const fileSystem = yield* FileSystem.FileSystem;  const path = yield* Path.Path;  const workspacePaths = yield* WorkspacePaths;  const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;   const readFile: WorkspaceFileSystemShape["readFile"] = Effect.fn("WorkspaceFileSystem.readFile")(    function* (input) {      const target = yield* workspacePaths.resolveRelativePathWithinRoot({        workspaceRoot: input.cwd,        relativePath: input.relativePath,      });       const [realWorkspaceRoot, realTargetPath] = yield* Effect.all([        fileSystem.realPath(input.cwd),        fileSystem.realPath(target.absolutePath),      ]).pipe(        Effect.mapError((cause) =>          makeReadFileError({            cwd: input.cwd,            relativePath: input.relativePath,            detail: causeMessage(cause),            cause,          }),        ),      );       const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);      if (        relativeRealPath.startsWith(`..${path.sep}`) ||        relativeRealPath === ".." ||        path.isAbsolute(relativeRealPath)      ) {        return yield* makeReadFileError({          cwd: input.cwd,          relativePath: input.relativePath,          detail: "Workspace file path resolves outside the project root.",        });      }       const result = yield* Effect.scoped(        Effect.gen(function* () {          const handle = yield* fileSystem.open(realTargetPath, { flag: "r" }).pipe(            Effect.mapError((cause) =>              makeReadFileError({                cwd: input.cwd,                relativePath: input.relativePath,                detail: causeMessage(cause),                cause,              }),            ),          );          const stat = yield* handle.stat.pipe(            Effect.mapError((cause) =>              makeReadFileError({                cwd: input.cwd,                relativePath: input.relativePath,                detail: causeMessage(cause),                cause,              }),            ),          );          if (stat.type !== "File") {            return yield* makeReadFileError({              cwd: input.cwd,              relativePath: input.relativePath,              detail: "Workspace path is not a file.",            });          }          const truncated = stat.size > FileSystem.Size(PROJECT_READ_FILE_MAX_BYTES);          const bytesToRead = truncated ? PROJECT_READ_FILE_MAX_BYTES : Number(stat.size);          const buffer = new Uint8Array(bytesToRead);          const bytesRead = yield* handle.read(buffer).pipe(            Effect.mapError((cause) =>              makeReadFileError({                cwd: input.cwd,                relativePath: input.relativePath,                detail: causeMessage(cause),                cause,              }),            ),          );          const fileBytes = buffer.subarray(0, Number(bytesRead));          if (fileBytes.includes(0)) {            return yield* makeReadFileError({              cwd: input.cwd,              relativePath: input.relativePath,              detail: "Binary files cannot be previewed as text.",            });          }          const contents = new TextDecoder("utf-8").decode(fileBytes);          return {            relativePath: target.relativePath,            contents,            byteLength: Number(stat.size),            truncated,          };        }),      );       return result;    },  );   const writeFile: WorkspaceFileSystemShape["writeFile"] = Effect.fn(    "WorkspaceFileSystem.writeFile",  )(function* (input) {    const target = yield* workspacePaths.resolveRelativePathWithinRoot({      workspaceRoot: input.cwd,      relativePath: input.relativePath,    });     yield* fileSystem.makeDirectory(path.dirname(target.absolutePath), { recursive: true }).pipe(      Effect.mapError(        (cause) =>          new WorkspaceFileSystemError({            cwd: input.cwd,            relativePath: input.relativePath,            operation: "workspaceFileSystem.makeDirectory",            detail: cause.message,            cause,          }),      ),    );    yield* fileSystem.writeFileString(target.absolutePath, input.contents).pipe(      Effect.mapError(        (cause) =>          new WorkspaceFileSystemError({            cwd: input.cwd,            relativePath: input.relativePath,            operation: "workspaceFileSystem.writeFile",            detail: cause.message,            cause,          }),      ),    );    yield* workspaceEntries.refresh(input.cwd);    return { relativePath: target.relativePath };  });  return { readFile, writeFile } satisfies WorkspaceFileSystemShape;}); export const WorkspaceFileSystemLive = Layer.effect(WorkspaceFileSystem, makeWorkspaceFileSystem);