import * as NodeServices from "@effect/platform-node/NodeServices" import { assert, describe, it } from "@effect/vitest" import { Effect, Layer, Stdio, Stream } from "effect" import * as Exit from "effect/Exit" import { TestConsole } from "effect/testing" import { CliOutput } from "effect/unstable/cli" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" const makeLayer = (args: ReadonlyArray) => Layer.mergeAll( TestConsole.layer, CliOutput.layer(CliOutput.defaultFormatter({ colors: false })), NodeServices.layer, Stdio.layerTest({ args: Effect.succeed(args) }) ) const fixturePath = (fileName: string) => `${import.meta.dirname}/fixtures/${fileName}` const cliProcessPath = `${import.meta.dirname}/../src/bin.ts` type CliMainModule = { readonly run: Effect.Effect } const runCli = Effect.fnUntraced(function*(args: ReadonlyArray) { const module = (yield* Effect.promise( () => import(new URL("../src/main.ts", import.meta.url).href) )) as CliMainModule return yield* Effect.gen(function*() { const exit = yield* Effect.exit(module.run) const stdoutLines = yield* TestConsole.logLines const stderrLines = yield* TestConsole.errorLines const stdout = stdoutLines.length > 0 ? String(stdoutLines[stdoutLines.length - 1]) : "" const stderr = stderrLines.map(String).join("\n") return { exit, stdout, stderr } as const }).pipe(Effect.provide(makeLayer(args))) }) const runCliProcess = Effect.fnUntraced(function*(args: ReadonlyArray) { const handle = yield* ChildProcess.make("node", [cliProcessPath, ...args]) return yield* Effect.all({ exitCode: handle.exitCode, stdout: Stream.mkString(Stream.decodeText(handle.stdout)), stderr: Stream.mkString(Stream.decodeText(handle.stderr)) }, { concurrency: "unbounded" }) }) describe("openapigen CLI", () => { it.effect("documents --format values and default in --help", () => Effect.gen(function*() { const result = yield* runCli(["--help"]) assert.isTrue(Exit.isSuccess(result.exit)) assert.include(result.stdout, "--format") assert.include(result.stdout, "httpclient") assert.include(result.stdout, "httpclient-type-only") assert.include(result.stdout, "httpapi") assert.include(result.stdout, "default: httpclient") assert.strictEqual(result.stderr, "") })) it.effect("routes --format values and defaults to httpclient", () => Effect.gen(function*() { const spec = fixturePath("cli-basic-spec.json") const defaultResult = yield* runCli(["--spec", spec, "--name", "CliClient"]) const httpclientResult = yield* runCli([ "--spec", spec, "--name", "CliClient", "--format", "httpclient" ]) const typeOnlyResult = yield* runCli([ "--spec", spec, "--name", "CliClient", "--format", "httpclient-type-only" ]) const httpapiResult = yield* runCli([ "--spec", spec, "--name", "CliClient", "--format", "httpapi" ]) assert.isTrue(Exit.isSuccess(defaultResult.exit)) assert.isTrue(Exit.isSuccess(httpclientResult.exit)) assert.isTrue(Exit.isSuccess(typeOnlyResult.exit)) assert.isTrue(Exit.isSuccess(httpapiResult.exit)) assert.strictEqual(defaultResult.stderr, "") assert.strictEqual(httpclientResult.stderr, "") assert.strictEqual(typeOnlyResult.stderr, "") assert.strictEqual(httpapiResult.stderr, "") assert.strictEqual(defaultResult.stdout, httpclientResult.stdout) assert.include(httpclientResult.stdout, "import * as Schema from \"effect/Schema\"") assert.notInclude(typeOnlyResult.stdout, "import * as Schema from \"effect/Schema\"") assert.include(typeOnlyResult.stdout, "import type * as HttpClient from \"effect/unstable/http/HttpClient\"") assert.include(httpapiResult.stdout, "export class CliClient extends HttpApi.make(\"CliClient\")") })) it.effect("rejects legacy --type-only flag", () => Effect.gen(function*() { const spec = fixturePath("cli-basic-spec.json") const result = yield* runCli(["--spec", spec, "--name", "CliClient", "--type-only"]) assert.isTrue(Exit.isFailure(result.exit)) assert.include(result.stdout, "USAGE") assert.include(result.stderr, "Unrecognized flag: --type-only") })) it.effect("writes warnings to stderr and keeps stdout as generated source", () => Effect.gen(function*() { const spec = fixturePath("cli-warning-spec.json") const result = yield* runCli(["--spec", spec, "--name", "CliClient"]) assert.isTrue(Exit.isSuccess(result.exit)) assert.include(result.stdout, "export const make = (") assert.include(result.stderr, "WARNING [cookie-parameter-dropped]") assert.include(result.stderr, "cookie-parameter-dropped") assert.notInclude(result.stdout, "cookie-parameter-dropped") assert.notInclude(result.stderr, "export const make = (") })) it.effect("separates generated source and warnings when spawned as a child process", () => Effect.gen(function*() { const spec = fixturePath("cli-warning-spec.json") const result = yield* runCliProcess(["--spec", spec, "--name", "CliClient"]) assert.strictEqual(result.exitCode, ChildProcessSpawner.ExitCode(0)) assert.include(result.stdout, "export const make = (") assert.notInclude(result.stdout, "WARNING [") assert.notInclude(result.stdout, "cookie-parameter-dropped") assert.include( result.stderr, "WARNING [cookie-parameter-dropped] GET /users/{id} (getUser): Cookie parameter \"session\" was dropped because non-security cookie parameters are not supported." ) assert.notInclude(result.stderr, "export const make = (") }).pipe(Effect.scoped, Effect.provide(NodeServices.layer))) })