Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions cli/src/commands/generate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import * as fs from "fs";
import { prompt } from "inquirer";
import inquirer from "inquirer";
import * as os from "os";
import * as path from "path";
import Generate from "./generate";

jest.mock("inquirer", () => ({ prompt: jest.fn() }));
// The command imports the default export, so that is what the mock stands in for.
jest.mock("inquirer", () => ({
__esModule: true,
default: { prompt: jest.fn() }
}));

const promptMock = prompt as unknown as jest.Mock;
const promptMock = inquirer.prompt as unknown as jest.Mock;

const CONTRACT = path.join(__dirname, "../../../test-fixtures/contract/api.ts");

Expand Down Expand Up @@ -101,6 +105,55 @@ describe("generate", () => {
`Generated ${written}`
);
});
test("inquirer resolves as CommonJS and still exposes the legacy prompt", () => {
// Every other case here runs against the mock, so the suite would pass
// against a module that cannot be loaded at all — which is the shape an
// ESM-only inquirer takes in a CommonJS package. `jest.requireActual`
// bypasses the factory above. The manifest is read from disk rather than
// required, because inquirer's `exports` map does not expose
// `./package.json`.
const actual = jest.requireActual("inquirer");
expect(typeof (actual.default ?? actual).prompt).toBe("function");

let dir = path.dirname(require.resolve("inquirer"));
while (!fs.existsSync(path.join(dir, "package.json"))) {
dir = path.dirname(dir);
}
const manifest = JSON.parse(
fs.readFileSync(path.join(dir, "package.json"), "utf8")
);
expect(manifest.type ?? "commonjs").not.toBe("module");
});

test("asks for each flag under the label the command fixes", async () => {
// The compiler already requires `message` and `type` to be present — both
// are non-optional on inquirer's question type. What it cannot check is the
// text, and the text is what a user reads, so it is pinned here.
withTerminal();
const outDir = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), "spot-generate-labels-"))
);
promptMock
.mockResolvedValueOnce({ Generator: "openapi3" })
.mockResolvedValueOnce({ Language: "yaml" })
.mockResolvedValueOnce({ "Output destination": outDir });
jest.spyOn(console, "log").mockImplementation(() => undefined);

await Generate.run(["-c", CONTRACT]);

expect(
promptMock.mock.calls.map(([question]) => [
question.name,
question.message,
question.type
])
).toEqual([
["Generator", "Generator:", "list"],
["Language", "Language:", "list"],
["Output destination", "Output destination:", "input"]
]);
});

test("prompts for the missing flags when there is a terminal", async () => {
// The other cases all sit on the no-terminal side of the guard, so without
// this one the condition itself is unconstrained: making it unconditional
Expand Down
14 changes: 10 additions & 4 deletions cli/src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Command, flags } from "@oclif/command";
import { prompt } from "inquirer";
import inquirer from "inquirer";
import YAML from "js-yaml";
import path from "path";
import { Contract } from "../../../lib/src/definitions";
Expand Down Expand Up @@ -71,10 +71,13 @@ export default class Generate extends Command {

if (!generator) {
generator = (
await prompt<{
await inquirer.prompt<{
Generator: string;
}>({
name: "Generator",
// The label a user sees. Fixed rather than derived from `name`, and
// its exact text is asserted in generate.spec.ts.
message: "Generator:",
type: "list",
choices: availableGenerators()
})
Expand All @@ -94,10 +97,11 @@ export default class Generate extends Command {

if (!language) {
language = (
await prompt<{
await inquirer.prompt<{
Language: string;
}>({
name: "Language",
message: "Language:",
type: "list",
choices: availableFormats(generator)
})
Expand All @@ -117,10 +121,12 @@ export default class Generate extends Command {

if (!outDir) {
outDir = (
await prompt<{
await inquirer.prompt<{
"Output destination": string;
}>({
name: "Output destination",
message: "Output destination:",
type: "input",
default: "."
})
)["Output destination"];
Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"eslint-config-prettier": "^10.1.8",
"express": "^5.2.1",
"fs-extra": "^11.4.0",
"inquirer": "^8.1.1",
"inquirer": "^11.1.0",
"js-yaml": "^5.2.3",
"prettier": "^3.9.6",
"qs": "^6.15.3",
Expand All @@ -37,7 +37,6 @@
"@types/cors": "^2.8.17",
"@types/express": "^5.0.6",
"@types/fs-extra": "^11.0.4",
"@types/inquirer": "^8.1.2",
"@types/jest": "^30.0.0",
"@types/node": "^22.20.1",
"@types/qs": "^6.15.1",
Expand Down
Loading
Loading