| 1 | import * as Console from "effect/Console"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as FileSystem from "effect/FileSystem"; |
| 4 | import * as Path from "effect/Path"; |
| 5 | import * as Stream from "effect/Stream"; |
| 6 | import * as Command from "effect/cli/Command"; |
| 7 | import * as Flag from "effect/cli/Flag"; |
| 8 | import { ChildProcess } from "effect/process"; |
| 9 | import { generate, loadGeneratorConfig } from "../../Prisma/ORM/Generate.ts"; |
| 10 | import { CliError } from "../../Prisma/ORM/internal.ts"; |
| 11 | import { exec } from "../../Util/exec.ts"; |
| 12 | |
| 13 | const 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 | |
| 98 | export const prismaCommand = Command.make("prisma").pipe( |
| 99 | Command.withDescription("Prisma contract tooling"), |
| 100 | Command.withSubcommands([generateCommand]), |
| 101 | ); |