| 1 | import * as NodeServices from "@effect/platform-node/NodeServices" |
| 2 | import { assert, describe, it } from "@effect/vitest" |
| 3 | import { Effect, Layer, Stdio, Stream } from "effect" |
| 4 | import { CliOutput } from "effect/cli" |
| 5 | import * as Exit from "effect/Exit" |
| 6 | import { ChildProcess, ChildProcessSpawner } from "effect/process" |
| 7 | import { TestConsole } from "effect/testing" |
| 8 | |
| 9 | const makeLayer = (args: ReadonlyArray<string>) => |
| 10 | Layer.mergeAll( |
| 11 | TestConsole.layer, |
| 12 | CliOutput.layer(CliOutput.defaultFormatter({ colors: false })), |
| 13 | NodeServices.layer, |
| 14 | Stdio.layerTest({ args: Effect.succeed(args) }) |
| 15 | ) |
| 16 | |
| 17 | const fixturePath = (fileName: string) => `${import.meta.dirname}/fixtures/${fileName}` |
| 18 | const cliProcessPath = `${import.meta.dirname}/../src/bin.ts` |
| 19 | |
| 20 | type CliMainModule = { |
| 21 | readonly run: Effect.Effect<void> |
| 22 | } |
| 23 | |
| 24 | const runCli = Effect.fnUntraced(function*(args: ReadonlyArray<string>) { |
| 25 | const module = (yield* Effect.promise( |
| 26 | () => import(new URL("../src/main.ts", import.meta.url).href) |
| 27 | )) as CliMainModule |
| 28 | |
| 29 | return yield* Effect.gen(function*() { |
| 30 | const exit = yield* Effect.exit(module.run) |
| 31 | const stdoutLines = yield* TestConsole.logLines |
| 32 | const stderrLines = yield* TestConsole.errorLines |
| 33 | const stdout = stdoutLines.length > 0 ? String(stdoutLines[stdoutLines.length - 1]) : "" |
| 34 | const stderr = stderrLines.map(String).join("\n") |
| 35 | return { exit, stdout, stderr } as const |
| 36 | }).pipe(Effect.provide(makeLayer(args))) |
| 37 | }) |
| 38 | |
| 39 | const runCliProcess = Effect.fnUntraced(function*(args: ReadonlyArray<string>) { |
| 40 | const handle = yield* ChildProcess.make("node", [cliProcessPath, ...args]) |
| 41 | return yield* Effect.all({ |
| 42 | exitCode: handle.exitCode, |
| 43 | stdout: Stream.mkString(Stream.decodeText(handle.stdout)), |
| 44 | stderr: Stream.mkString(Stream.decodeText(handle.stderr)) |
| 45 | }, { concurrency: "unbounded" }) |
| 46 | }) |
| 47 | |
| 48 | describe("openapigen CLI", () => { |
| 49 | it.effect("documents --format values and default in --help", () => |
| 50 | Effect.gen(function*() { |
| 51 | const result = yield* runCli(["--help"]) |
| 52 | |
| 53 | assert.isTrue(Exit.isSuccess(result.exit)) |
| 54 | assert.include(result.stdout, "--format") |
| 55 | assert.include(result.stdout, "httpclient") |
| 56 | assert.include(result.stdout, "httpclient-type-only") |
| 57 | assert.include(result.stdout, "httpapi") |
| 58 | assert.include(result.stdout, "default: httpclient") |
| 59 | assert.strictEqual(result.stderr, "") |
| 60 | })) |
| 61 | |
| 62 | it.effect("routes --format values and defaults to httpclient", () => |
| 63 | Effect.gen(function*() { |
| 64 | const spec = fixturePath("cli-basic-spec.json") |
| 65 | |
| 66 | const defaultResult = yield* runCli(["--spec", spec, "--name", "CliClient"]) |
| 67 | const httpclientResult = yield* runCli([ |
| 68 | "--spec", |
| 69 | spec, |
| 70 | "--name", |
| 71 | "CliClient", |
| 72 | "--format", |
| 73 | "httpclient" |
| 74 | ]) |
| 75 | const typeOnlyResult = yield* runCli([ |
| 76 | "--spec", |
| 77 | spec, |
| 78 | "--name", |
| 79 | "CliClient", |
| 80 | "--format", |
| 81 | "httpclient-type-only" |
| 82 | ]) |
| 83 | const httpapiResult = yield* runCli([ |
| 84 | "--spec", |
| 85 | spec, |
| 86 | "--name", |
| 87 | "CliClient", |
| 88 | "--format", |
| 89 | "httpapi" |
| 90 | ]) |
| 91 | |
| 92 | assert.isTrue(Exit.isSuccess(defaultResult.exit)) |
| 93 | assert.isTrue(Exit.isSuccess(httpclientResult.exit)) |
| 94 | assert.isTrue(Exit.isSuccess(typeOnlyResult.exit)) |
| 95 | assert.isTrue(Exit.isSuccess(httpapiResult.exit)) |
| 96 | |
| 97 | assert.strictEqual(defaultResult.stderr, "") |
| 98 | assert.strictEqual(httpclientResult.stderr, "") |
| 99 | assert.strictEqual(typeOnlyResult.stderr, "") |
| 100 | assert.strictEqual(httpapiResult.stderr, "") |
| 101 | |
| 102 | assert.strictEqual(defaultResult.stdout, httpclientResult.stdout) |
| 103 | assert.include(httpclientResult.stdout, "import * as Schema from \"effect/Schema\"") |
| 104 | assert.notInclude(typeOnlyResult.stdout, "import * as Schema from \"effect/Schema\"") |
| 105 | assert.include(typeOnlyResult.stdout, "import type * as HttpClient from \"effect/http/HttpClient\"") |
| 106 | assert.include(httpapiResult.stdout, "export class CliClient extends HttpApi.make(\"CliClient\")") |
| 107 | })) |
| 108 | |
| 109 | it.effect("rejects legacy --type-only flag", () => |
| 110 | Effect.gen(function*() { |
| 111 | const spec = fixturePath("cli-basic-spec.json") |
| 112 | const result = yield* runCli(["--spec", spec, "--name", "CliClient", "--type-only"]) |
| 113 | |
| 114 | assert.isTrue(Exit.isFailure(result.exit)) |
| 115 | assert.include(result.stdout, "USAGE") |
| 116 | assert.include(result.stderr, "Unrecognized flag: --type-only") |
| 117 | })) |
| 118 | |
| 119 | it.effect("writes warnings to stderr and keeps stdout as generated source", () => |
| 120 | Effect.gen(function*() { |
| 121 | const spec = fixturePath("cli-warning-spec.json") |
| 122 | const result = yield* runCli(["--spec", spec, "--name", "CliClient"]) |
| 123 | |
| 124 | assert.isTrue(Exit.isSuccess(result.exit)) |
| 125 | assert.include(result.stdout, "export const make = (") |
| 126 | assert.include(result.stderr, "WARNING [cookie-parameter-dropped]") |
| 127 | assert.include(result.stderr, "cookie-parameter-dropped") |
| 128 | assert.notInclude(result.stdout, "cookie-parameter-dropped") |
| 129 | assert.notInclude(result.stderr, "export const make = (") |
| 130 | })) |
| 131 | |
| 132 | it.effect("separates generated source and warnings when spawned as a child process", () => |
| 133 | Effect.gen(function*() { |
| 134 | const spec = fixturePath("cli-warning-spec.json") |
| 135 | const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"]) |
| 136 | |
| 137 | assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0)) |
| 138 | assert.include(result.stdout, "export const make = (") |
| 139 | assert.notInclude(result.stdout, "WARNING [") |
| 140 | assert.notInclude(result.stdout, "cookie-parameter-dropped") |
| 141 | assert.include( |
| 142 | result.stderr, |
| 143 | "WARNING [cookie-parameter-dropped] GET /users/{id} (getUser): Cookie parameter \"session\" was dropped because non-security cookie parameters are not supported." |
| 144 | ) |
| 145 | assert.notInclude(result.stderr, "export const make = (") |
| 146 | }).pipe(Effect.scoped, Effect.provide(NodeServices.layer))) |
| 147 | }) |