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 { expandHomePath } from "@t3tools/provider-core/server/pathExpansion"; import * as HostProcess from "@t3tools/shared/HostProcess"; export class WorkspaceRootNotExistsError extends Schema.TaggedError()( "WorkspaceRootNotExistsError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, }, ) { override get message(): string { return `Workspace root does not exist: ${this.normalizedWorkspaceRoot}`; } } export class WorkspaceRootCreateFailedError extends Schema.TaggedError()( "WorkspaceRootCreateFailedError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to create workspace root: ${this.normalizedWorkspaceRoot}`; } } export class WorkspaceRootStatFailedError extends Schema.TaggedError()( "WorkspaceRootStatFailedError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, phase: Schema.Literals(["validate-existing", "verify-created"]), cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to stat workspace root '${this.normalizedWorkspaceRoot}' during '${this.phase}'.`; } } export class WorkspaceRootNotDirectoryError extends Schema.TaggedError()( "WorkspaceRootNotDirectoryError", { workspaceRoot: Schema.String, normalizedWorkspaceRoot: Schema.String, }, ) { override get message(): string { return `Workspace root is not a directory: ${this.normalizedWorkspaceRoot}`; } } export class WorkspacePathOutsideRootError extends Schema.TaggedError()( "WorkspacePathOutsideRootError", { workspaceRoot: Schema.String, relativePath: Schema.String, }, ) { override get message(): string { return `Workspace file path must be relative to the project root: ${this.relativePath}`; } } export const WorkspacePathsError = Schema.Union([ WorkspaceRootNotExistsError, WorkspaceRootCreateFailedError, WorkspaceRootStatFailedError, WorkspaceRootNotDirectoryError, WorkspacePathOutsideRootError, ]); export type WorkspacePathsError = typeof WorkspacePathsError.Type; export class WorkspacePaths extends Context.Service< WorkspacePaths, { readonly normalizeWorkspaceRoot: ( workspaceRoot: string, options?: { readonly createIfMissing?: boolean }, ) => Effect.Effect< string, | WorkspaceRootNotExistsError | WorkspaceRootCreateFailedError | WorkspaceRootStatFailedError | WorkspaceRootNotDirectoryError >; readonly resolveRelativePathWithinRoot: (input: { workspaceRoot: string; relativePath: string; }) => Effect.Effect< { absolutePath: string; relativePath: string }, WorkspacePathOutsideRootError >; } >()("t3/workspace/WorkspacePaths") {} const make = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const path = yield* Path.Path; const statWorkspaceRoot = Effect.fn("WorkspacePaths.statWorkspaceRoot")(function* ( workspaceRoot: string, normalizedWorkspaceRoot: string, phase: WorkspaceRootStatFailedError["phase"], ) { return yield* fileSystem.stat(normalizedWorkspaceRoot).pipe( Effect.matchEffect({ onFailure: (cause) => cause.reason._tag === "NotFound" ? Effect.succeed(null) : Effect.fail( new WorkspaceRootStatFailedError({ workspaceRoot, normalizedWorkspaceRoot, phase, cause, }), ), onSuccess: Effect.succeed, }), ); }); const normalizeWorkspaceRoot: WorkspacePaths["Service"]["normalizeWorkspaceRoot"] = Effect.fn( "WorkspacePaths.normalizeWorkspaceRoot", )(function* (workspaceRoot, options) { const normalizedWorkspaceRoot = path.resolve( expandHomePath(workspaceRoot.trim(), yield* HostProcess.HomeDirectory), ); let workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, "validate-existing", ); if (!workspaceStat && options?.createIfMissing) { yield* fileSystem.makeDirectory(normalizedWorkspaceRoot, { recursive: true }).pipe( Effect.mapError( (cause) => new WorkspaceRootCreateFailedError({ workspaceRoot, normalizedWorkspaceRoot, cause, }), ), ); workspaceStat = yield* statWorkspaceRoot( workspaceRoot, normalizedWorkspaceRoot, "verify-created", ); } if (!workspaceStat) { return yield* new WorkspaceRootNotExistsError({ workspaceRoot, normalizedWorkspaceRoot, }); } if (workspaceStat.type !== "Directory") { return yield* new WorkspaceRootNotDirectoryError({ workspaceRoot, normalizedWorkspaceRoot, }); } return normalizedWorkspaceRoot; }); const resolveRelativePathWithinRoot: WorkspacePaths["Service"]["resolveRelativePathWithinRoot"] = Effect.fn("WorkspacePaths.resolveRelativePathWithinRoot")(function* (input) { const normalizedInputPath = input.relativePath.trim(); if (path.isAbsolute(normalizedInputPath)) { return yield* new WorkspacePathOutsideRootError({ workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, }); } const absolutePath = path.resolve(input.workspaceRoot, normalizedInputPath); // Only the host separator becomes `/`: on POSIX `\` is a file name character. const relativeToRoot = path .relative(input.workspaceRoot, absolutePath) .replaceAll(path.sep, "/"); if ( relativeToRoot.length === 0 || relativeToRoot === "." || relativeToRoot.startsWith("../") || relativeToRoot === ".." || path.isAbsolute(relativeToRoot) ) { return yield* new WorkspacePathOutsideRootError({ workspaceRoot: input.workspaceRoot, relativePath: input.relativePath, }); } return { absolutePath, relativePath: relativeToRoot, }; }); return WorkspacePaths.of({ normalizeWorkspaceRoot, resolveRelativePathWithinRoot }); }); export const layer = Layer.effect(WorkspacePaths, make);