diff --git a/.changeset/nice-sheep-juggle.md b/.changeset/nice-sheep-juggle.md new file mode 100644 index 000000000..262615098 --- /dev/null +++ b/.changeset/nice-sheep-juggle.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Mirror directory structure of input files if output is a directory to prevent overwriting the same file again and again. diff --git a/.gitignore b/.gitignore index 28dd70cdf..c81238d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ .astro dist node_modules + +packages/openapi-typescript/test/fixtures/cli-outputs/out diff --git a/packages/openapi-typescript/bin/cli.js b/packages/openapi-typescript/bin/cli.js index c73b445c3..0e568e46e 100755 --- a/packages/openapi-typescript/bin/cli.js +++ b/packages/openapi-typescript/bin/cli.js @@ -114,13 +114,13 @@ async function generateSchema(pathToSpec) { let outputFilePath = new URL(flags.output, CWD); // note: may be directory const isDir = fs.existsSync(outputFilePath) && fs.lstatSync(outputFilePath).isDirectory(); if (isDir) { - if (typeof flags.output === 'string' && !flags.output.endsWith('/')) { - outputFilePath = new URL(`${flags.output}/`, CWD) + if (typeof flags.output === "string" && !flags.output.endsWith("/")) { + outputFilePath = new URL(`${flags.output}/`, CWD); } - const filename = path.basename(pathToSpec).replace(EXT_RE, ".ts"); + const filename = pathToSpec.replace(EXT_RE, ".ts"); const originalOutputFilePath = outputFilePath; outputFilePath = new URL(filename, originalOutputFilePath); - if (outputFilePath.protocol !== 'file:') { + if (outputFilePath.protocol !== "file:") { outputFilePath = new URL(outputFilePath.host.replace(EXT_RE, ".ts"), originalOutputFilePath); } } @@ -174,6 +174,8 @@ async function main() { // handle local schema(s) const inputSpecPaths = await glob(pathToSpec); const isGlob = inputSpecPaths.length > 1; + const isDirUrl = outputDir.pathname === outputFile.pathname; + const isFile = fs.existsSync(outputDir) && fs.lstatSync(outputDir).isFile(); // error: no matches for glob if (inputSpecPaths.length === 0) { @@ -182,7 +184,7 @@ async function main() { } // error: tried to glob output to single file - if (isGlob && output === OUTPUT_FILE && fs.existsSync(outputDir) && fs.lstatSync(outputDir).isFile()) { + if (isGlob && output === OUTPUT_FILE && (isFile || !isDirUrl)) { error(`Expected directory for --output if using glob patterns. Received "${flags.output}".`); process.exit(1); } @@ -191,15 +193,14 @@ async function main() { await Promise.all( inputSpecPaths.map(async (specPath) => { if (flags.output !== "." && output === OUTPUT_FILE) { - if (isGlob) { - fs.mkdirSync(outputFile, { recursive: true }); // recursively make parent dirs - } - else { + if (isGlob || isDirUrl) { + fs.mkdirSync(new URL(path.dirname(specPath), outputDir), { recursive: true }); // recursively make parent dirs + } else { fs.mkdirSync(outputDir, { recursive: true }); // recursively make parent dirs } } await generateSchema(specPath); - }) + }), ); } diff --git a/packages/openapi-typescript/test/cli.test.ts b/packages/openapi-typescript/test/cli.test.ts index 8acf3537c..88ae34d3d 100644 --- a/packages/openapi-typescript/test/cli.test.ts +++ b/packages/openapi-typescript/test/cli.test.ts @@ -1,13 +1,22 @@ import { execa } from "execa"; +import glob from "fast-glob"; import fs from "node:fs"; +import path from "node:path/posix"; // prevent issues with `\` on windows import { URL, fileURLToPath } from "node:url"; import os from "node:os"; const root = new URL("../", import.meta.url); const cwd = os.platform() === "win32" ? fileURLToPath(root) : root; // execa bug: fileURLToPath required on Windows const cmd = "./bin/cli.js"; +const inputDir = "test/fixtures/cli-outputs/"; +const outputDir = path.join(inputDir, "out/"); const TIMEOUT = 90000; +// fast-glob does not sort results +async function getOutputFiles() { + return (await glob("**", { cwd: outputDir })).sort((a, b) => a.localeCompare(b, undefined, { numeric: true })); +} + describe("CLI", () => { // note: the snapshots in index.test.ts test the Node API; these test the CLI describe("snapshots", () => { @@ -76,4 +85,59 @@ describe("CLI", () => { expect(stdout).toEqual(expect.stringMatching(/^v[\d.]+(-.*)?$/)); }); }); + + describe("outputs", () => { + beforeEach(() => { + fs.rmSync(new URL(outputDir, root), { recursive: true, force: true }); + }); + + test("single file to file", async () => { + const inputFile = path.join(inputDir, "file-a.yaml"); + const outputFile = path.join(outputDir, "file-a.ts"); + await execa(cmd, [inputFile, "--output", outputFile], { cwd }); + const result = await getOutputFiles(); + expect(result).toEqual(["file-a.ts"]); + }); + + test("single file to directory", async () => { + const inputFile = path.join(inputDir, "file-a.yaml"); + await execa(cmd, [inputFile, "--output", outputDir], { cwd }); + const result = await getOutputFiles(); + expect(result).toEqual(["test/fixtures/cli-outputs/file-a.ts"]); + }); + + test("single file (glob) to file", async () => { + const inputFile = path.join(inputDir, "*-a.yaml"); + const outputFile = path.join(outputDir, "file-a.ts"); + await execa(cmd, [inputFile, "--output", outputFile], { cwd }); + const result = await getOutputFiles(); + expect(result).toEqual(["file-a.ts"]); + }); + + test("multiple files to file", async () => { + const inputFile = path.join(inputDir, "*.yaml"); + const outputFile = path.join(outputDir, "file-a.ts"); + await expect(execa(cmd, [inputFile, "--output", outputFile], { cwd })).rejects.toThrow(); + }); + + test("multiple files to directory", async () => { + const inputFile = path.join(inputDir, "*.yaml"); + await execa(cmd, [inputFile, "--output", outputDir], { cwd }); + const result = await getOutputFiles(); + expect(result).toEqual(["test/fixtures/cli-outputs/file-a.ts", "test/fixtures/cli-outputs/file-b.ts"]); + }); + + test("multiple nested files to directory", async () => { + const inputFile = path.join(inputDir, "**/*.yaml"); + await execa(cmd, [inputFile, "--output", outputDir], { cwd }); + const result = await getOutputFiles(); + expect(result).toEqual([ + "test/fixtures/cli-outputs/file-a.ts", + "test/fixtures/cli-outputs/file-b.ts", + "test/fixtures/cli-outputs/nested/deep/file-e.ts", + "test/fixtures/cli-outputs/nested/file-c.ts", + "test/fixtures/cli-outputs/nested/file-d.ts", + ]); + }); + }); }); diff --git a/packages/openapi-typescript/test/fixtures/cli-outputs/file-a.yaml b/packages/openapi-typescript/test/fixtures/cli-outputs/file-a.yaml new file mode 100644 index 000000000..7f4eaac81 --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/cli-outputs/file-a.yaml @@ -0,0 +1,8 @@ +openapi: "3.0" +info: + title: test file a + version: "1.0" +paths: + /endpoint: + get: + description: OK diff --git a/packages/openapi-typescript/test/fixtures/cli-outputs/file-b.yaml b/packages/openapi-typescript/test/fixtures/cli-outputs/file-b.yaml new file mode 100644 index 000000000..e41219cf3 --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/cli-outputs/file-b.yaml @@ -0,0 +1,8 @@ +openapi: "3.0" +info: + title: test file b + version: "1.0" +paths: + /endpoint: + get: + description: OK diff --git a/packages/openapi-typescript/test/fixtures/cli-outputs/nested/deep/file-e.yaml b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/deep/file-e.yaml new file mode 100644 index 000000000..fc56849d8 --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/deep/file-e.yaml @@ -0,0 +1,8 @@ +openapi: "3.0" +info: + title: test file e + version: "1.0" +paths: + /endpoint: + get: + description: OK diff --git a/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-c.yaml b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-c.yaml new file mode 100644 index 000000000..ccf725a95 --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-c.yaml @@ -0,0 +1,8 @@ +openapi: "3.0" +info: + title: test file c + version: "1.0" +paths: + /endpoint: + get: + description: OK diff --git a/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-d.yaml b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-d.yaml new file mode 100644 index 000000000..27079dd0c --- /dev/null +++ b/packages/openapi-typescript/test/fixtures/cli-outputs/nested/file-d.yaml @@ -0,0 +1,8 @@ +openapi: "3.0" +info: + title: test file d + version: "1.0" +paths: + /endpoint: + get: + description: OK