1import * as Console from "effect/Console";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Path from "effect/Path";
5import * as Stream from "effect/Stream";
6import * as Command from "effect/cli/Command";
7import * as Flag from "effect/cli/Flag";
8import { ChildProcess } from "effect/process";
9import { generate, loadGeneratorConfig } from "../../Prisma/ORM/Generate.ts";
10import { CliError } from "../../Prisma/ORM/internal.ts";
11import { exec } from "../../Util/exec.ts";
12
13const generateCommand = Command.make(
14 "generate",
15 {
16 config: Flag.String("config").pipe(Flag.withDefault("./prisma.config.ts")),
17 watch: Flag.Boolean("watch").pipe(Flag.withDefault(false)),
18 },
19 Effect.fn(function* ({ config, watch }) {
20 if (!watch) {
21 const result = yield* generate(config);
22 yield* Console.log(
23 `Generated ${result.files.length} files in ${result.directory}`,
24 );
25 return;
26 }
27 const fs = yield* FileSystem.FileSystem;
28 const path = yield* Path.Path;
29 const loaded = yield* loadGeneratorConfig(config);
30 const bin = yield* path.fromFileUrl(
31 new URL("../../../bin/cli.js", import.meta.url),
32 );
33 const run = Effect.gen(function* () {
34 const result = yield* exec(
35 ChildProcess.make(process.execPath, [
36 bin,
37 "prisma",
38 "generate",
39 "--config",
40 loaded.absolute,
41 "--no-input",
42 ]),
43 );
44 if (result.exitCode !== 0)
45 return yield* Effect.fail(
46 new CliError({ message: `${result.stdout}\n${result.stderr}` }),
47 );
48 yield* Console.log(result.stdout);
49 }).pipe(
50 Effect.scoped,
51 Effect.catch((error) => Console.error(error)),
52 );
53 yield* run;
54 yield* Console.log(
55 `Watching ${loaded.directory}; each generation uses a fresh process.`,
56 );
57 yield* fs.watch(loaded.directory, { recursive: true }).pipe(
58 Stream.filter((event) => {
59 const file = path.resolve(loaded.directory, event.path);
60 const relative = path.relative(loaded.directory, file);
61 const output = path.dirname(loaded.output);
62 return (
63 !relative
64 .split(path.sep)
65 .some(
66 (part) =>
67 part === "node_modules" ||
68 part === ".git" ||
69 part === ".alchemy",
70 ) &&
71 !(file === output || file.startsWith(output + path.sep)) &&
72 /\.(?:ts|mts|cts|js|mjs|cjs|prisma|psl)$/.test(file)
73 );
74 }),
75 Stream.filterEffect((event) =>
76 Effect.gen(function* () {
77 const file = path.resolve(loaded.directory, event.path);
78 if (!(yield* fs.exists(file))) return true;
79 const contents = yield* fs.readFileString(file);
80 return (
81 !contents.startsWith("// Generated by alchemy prisma generate.") &&
82 !contents.includes(
83 "This file is automatically generated by 'prisma contract emit'",
84 )
85 );
86 }).pipe(Effect.catch(() => Effect.succeed(false))),
87 ),
88 Stream.debounce("200 millis"),
89 Stream.runForEach(() => run),
90 );
91 }),
92).pipe(
93 Command.withDescription(
94 "Generate Effect bindings and standalone schemas from a Prisma contract",
95 ),
96);
97
98export const prismaCommand = Command.make("prisma").pipe(
99 Command.withDescription("Prisma contract tooling"),
100 Command.withSubcommands([generateCommand]),
101);