1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6import * as Path from "effect/Path";
7import * as Schema from "effect/Schema";
8
9import { T3_PROJECT_FILE_NAME, type T3ProjectFile } from "@t3tools/contracts";
10import { T3ProjectFileFromJson } from "@t3tools/shared/t3ProjectFile";
11
12const decodeT3ProjectFileJson = Schema.decodeEffect(T3ProjectFileFromJson);
13
14export class T3ProjectFileLoadError extends Schema.TaggedError<T3ProjectFileLoadError>()(
15 "T3ProjectFileLoadError",
16 {
17 operation: Schema.Literals(["read", "decode"]),
18 workspaceRoot: Schema.String,
19 filePath: Schema.String,
20 cause: Schema.Defect(),
21 },
22) {
23 override get message(): string {
24 return `Failed to ${this.operation} ${T3_PROJECT_FILE_NAME} at ${this.filePath}.`;
25 }
26}
27
28export class T3ProjectFileLoader extends Context.Service<
29 T3ProjectFileLoader,
30 {
31 /** Never fails; missing/unreadable/invalid files return none, with warnings for invalid files. */
32 readonly load: (workspaceRoot: string) => Effect.Effect<Option.Option<T3ProjectFile>>;
33 }
34>()("t3/project/T3ProjectFileLoader") {}
35
36const logT3ProjectFileLoadError = (error: T3ProjectFileLoadError) =>
37 Effect.logWarning(error).pipe(
38 Effect.annotateLogs({
39 operation: error.operation,
40 workspaceRoot: error.workspaceRoot,
41 filePath: error.filePath,
42 errorTag: error._tag,
43 }),
44 );
45
46/** @public */
47export const make = Effect.gen(function* () {
48 const fileSystem = yield* FileSystem.FileSystem;
49 const path = yield* Path.Path;
50
51 const load: T3ProjectFileLoader["Service"]["load"] = Effect.fn("T3ProjectFileLoader.load")(
52 function* (workspaceRoot) {
53 const filePath = path.join(workspaceRoot, T3_PROJECT_FILE_NAME);
54 const raw = yield* fileSystem.readFileString(filePath).pipe(
55 Effect.asSome,
56 Effect.catchTags({
57 PlatformError: (error) =>
58 error.reason._tag === "NotFound"
59 ? Effect.succeed(Option.none<string>())
60 : logT3ProjectFileLoadError(
61 new T3ProjectFileLoadError({
62 operation: "read",
63 workspaceRoot,
64 filePath,
65 cause: error,
66 }),
67 ).pipe(Effect.as(Option.none<string>())),
68 }),
69 );
70 if (Option.isNone(raw)) {
71 return Option.none<T3ProjectFile>();
72 }
73 return yield* decodeT3ProjectFileJson(raw.value).pipe(
74 Effect.asSome,
75 Effect.catchTags({
76 SchemaError: (error) =>
77 logT3ProjectFileLoadError(
78 new T3ProjectFileLoadError({
79 operation: "decode",
80 workspaceRoot,
81 filePath,
82 cause: error,
83 }),
84 ).pipe(Effect.as(Option.none<T3ProjectFile>())),
85 }),
86 );
87 },
88 );
89
90 return T3ProjectFileLoader.of({ load });
91});
92
93export const layer = Layer.effect(T3ProjectFileLoader, make);