// @effect-diagnostics nodeBuiltinImport:off/** * WorkspaceFileSystem - Effect service contract for workspace file mutations. * * Owns workspace-root-relative file read/write operations and their associated * safety checks and cache invalidation hooks. * * @module WorkspaceFileSystem */import * as NodeFSP from "node:fs/promises"; import type { ProjectReadFileInput, ProjectReadFileResult, ProjectWriteFileInput, ProjectWriteFileResult,} from "@t3tools/contracts";import * as Context from "effect/Context";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 * as Schema from "effect/Schema"; import * as WorkspaceEntries from "./WorkspaceEntries.ts";import * as WorkspacePaths from "./WorkspacePaths.ts"; const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024; export class WorkspaceFileSystemError extends Schema.TaggedErrorClass<WorkspaceFileSystemError>()( "WorkspaceFileSystemError", { cwd: Schema.String, relativePath: Schema.optional(Schema.String), operation: Schema.Literals([ "workspaceFileSystem.readFile", "workspaceFileSystem.makeDirectory", "workspaceFileSystem.writeFile", ]), cause: Schema.Defect(), },) { override get message(): string { const target = this.relativePath ? `'${this.relativePath}' in '${this.cwd}'` : `'${this.cwd}'`; return `Workspace file operation '${this.operation}' failed for ${target}.`; }} /** Service tag for workspace file operations. */export class WorkspaceFileSystem extends Context.Service< WorkspaceFileSystem, { /** Read a UTF-8 text file relative to the workspace root. */ readonly readFile: ( input: ProjectReadFileInput, ) => Effect.Effect< ProjectReadFileResult, WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError >; /** * Write a file relative to the workspace root. * * Creates parent directories as needed and rejects paths that escape the * workspace root. */ readonly writeFile: ( input: ProjectWriteFileInput, ) => Effect.Effect< ProjectWriteFileResult, WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError >; }>()("t3/workspace/WorkspaceFileSystem") {} export const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const workspacePaths = yield* WorkspacePaths.WorkspacePaths; const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries; const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn( "WorkspaceFileSystem.readFile", )(function* (input) { const target = yield* workspacePaths.resolveRelativePathWithinRoot({ workspaceRoot: input.cwd, relativePath: input.relativePath, }); return yield* Effect.tryPromise({ try: async () => { const [realWorkspaceRoot, realTargetPath] = await Promise.all([ NodeFSP.realpath(input.cwd), NodeFSP.realpath(target.absolutePath), ]); const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath); if ( relativeRealPath.startsWith(`..${path.sep}`) || relativeRealPath === ".." || path.isAbsolute(relativeRealPath) ) { throw new Error("Workspace file path resolves outside the project root."); } const handle = await NodeFSP.open(realTargetPath, "r"); try { const stat = await handle.stat(); if (!stat.isFile()) { throw new Error("Workspace path is not a file."); } const bytesToRead = Math.min(stat.size, PROJECT_READ_FILE_MAX_BYTES); const buffer = Buffer.alloc(bytesToRead); const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0); const fileBytes = buffer.subarray(0, bytesRead); if (fileBytes.includes(0)) { throw new Error("Binary files cannot be previewed as text."); } const contents = new TextDecoder("utf-8").decode(fileBytes); return { relativePath: target.relativePath, contents, byteLength: stat.size, truncated: stat.size > PROJECT_READ_FILE_MAX_BYTES, }; } finally { await handle.close(); } }, catch: (cause) => new WorkspaceFileSystemError({ cwd: input.cwd, relativePath: input.relativePath, operation: "workspaceFileSystem.readFile", cause, }), }); }); const writeFile: WorkspaceFileSystem["Service"]["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", cause, }), ), ); yield* fileSystem.writeFileString(target.absolutePath, input.contents).pipe( Effect.mapError( (cause) => new WorkspaceFileSystemError({ cwd: input.cwd, relativePath: input.relativePath, operation: "workspaceFileSystem.writeFile", cause, }), ), ); yield* workspaceEntries.refresh(input.cwd); return { relativePath: target.relativePath }; }); return WorkspaceFileSystem.of({ readFile, writeFile });}); export const layer = Layer.effect(WorkspaceFileSystem, make);
// @effect-diagnostics nodeBuiltinImport:off
/**
* WorkspaceFileSystem - Effect service contract for workspace file mutations.
*
* Owns workspace-root-relative file read/write operations and their associated
* safety checks and cache invalidation hooks.
*
* @module WorkspaceFileSystem
*/
import * as NodeFSP from "node:fs/promises";
import type {
ProjectReadFileInput,
ProjectReadFileResult,
ProjectWriteFileInput,
ProjectWriteFileResult,
} from "@t3tools/contracts";
import * as Context from "effect/Context";
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 * as Schema from "effect/Schema";
import * as WorkspaceEntries from "./WorkspaceEntries.ts";
import * as WorkspacePaths from "./WorkspacePaths.ts";
const PROJECT_READ_FILE_MAX_BYTES = 1024 * 1024;
export class WorkspaceFileSystemError extends Schema.TaggedErrorClass<WorkspaceFileSystemError>()(
"WorkspaceFileSystemError",
{
cwd: Schema.String,
relativePath: Schema.optional(Schema.String),
operation: Schema.Literals([
"workspaceFileSystem.readFile",
"workspaceFileSystem.makeDirectory",
"workspaceFileSystem.writeFile",
]),
cause: Schema.Defect(),
},
) {
override get message(): string {
const target = this.relativePath ? `'${this.relativePath}' in '${this.cwd}'` : `'${this.cwd}'`;
return `Workspace file operation '${this.operation}' failed for ${target}.`;
}
}
/** Service tag for workspace file operations. */
export class WorkspaceFileSystem extends Context.Service<
WorkspaceFileSystem,
{
/** Read a UTF-8 text file relative to the workspace root. */
readonly readFile: (
input: ProjectReadFileInput,
) => Effect.Effect<
ProjectReadFileResult,
WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError
>;
/**
* Write a file relative to the workspace root.
*
* Creates parent directories as needed and rejects paths that escape the
* workspace root.
*/
readonly writeFile: (
input: ProjectWriteFileInput,
) => Effect.Effect<
ProjectWriteFileResult,
WorkspaceFileSystemError | WorkspacePaths.WorkspacePathOutsideRootError
>;
}
>()("t3/workspace/WorkspaceFileSystem") {}
export const make = Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const workspacePaths = yield* WorkspacePaths.WorkspacePaths;
const workspaceEntries = yield* WorkspaceEntries.WorkspaceEntries;
const readFile: WorkspaceFileSystem["Service"]["readFile"] = Effect.fn(
"WorkspaceFileSystem.readFile",
)(function* (input) {
const target = yield* workspacePaths.resolveRelativePathWithinRoot({
workspaceRoot: input.cwd,
relativePath: input.relativePath,
});
return yield* Effect.tryPromise({
try: async () => {
const [realWorkspaceRoot, realTargetPath] = await Promise.all([
NodeFSP.realpath(input.cwd),
NodeFSP.realpath(target.absolutePath),
]);
const relativeRealPath = path.relative(realWorkspaceRoot, realTargetPath);
if (
relativeRealPath.startsWith(`..${path.sep}`) ||
relativeRealPath === ".." ||
path.isAbsolute(relativeRealPath)
) {
throw new Error("Workspace file path resolves outside the project root.");
}
const handle = await NodeFSP.open(realTargetPath, "r");
try {
const stat = await handle.stat();
if (!stat.isFile()) {
throw new Error("Workspace path is not a file.");
}
const bytesToRead = Math.min(stat.size, PROJECT_READ_FILE_MAX_BYTES);
const buffer = Buffer.alloc(bytesToRead);
const { bytesRead } = await handle.read(buffer, 0, bytesToRead, 0);
const fileBytes = buffer.subarray(0, bytesRead);
if (fileBytes.includes(0)) {
throw new Error("Binary files cannot be previewed as text.");
}
const contents = new TextDecoder("utf-8").decode(fileBytes);
return {
relativePath: target.relativePath,
contents,
byteLength: stat.size,
truncated: stat.size > PROJECT_READ_FILE_MAX_BYTES,
};
} finally {
await handle.close();
}
},
catch: (cause) =>
new WorkspaceFileSystemError({
cwd: input.cwd,
relativePath: input.relativePath,
operation: "workspaceFileSystem.readFile",
cause,
}),
});
});
const writeFile: WorkspaceFileSystem["Service"]["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",
cause,
}),
),
);
yield* fileSystem.writeFileString(target.absolutePath, input.contents).pipe(
Effect.mapError(
(cause) =>
new WorkspaceFileSystemError({
cwd: input.cwd,
relativePath: input.relativePath,
operation: "workspaceFileSystem.writeFile",
cause,
}),
),
);
yield* workspaceEntries.refresh(input.cwd);
return { relativePath: target.relativePath };
});
return WorkspaceFileSystem.of({ readFile, writeFile });
});
export const layer = Layer.effect(WorkspaceFileSystem, make);