From 75b5e1aa89f1d0756332f6539167295864880416 Mon Sep 17 00:00:00 2001 From: Drew Powers Date: Fri, 4 Nov 2022 12:59:10 -0600 Subject: [PATCH] Add --version flag, add CLI tests --- README.md | 12 ++++----- bin/cli.js | 36 ++++++++++++++++----------- package.json | 3 ++- pnpm-lock.yaml | 65 ++++++++++++++++++++++++++++++++++++++++++++++++ test/cli.test.ts | 41 ++++++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 23 deletions(-) create mode 100644 test/cli.test.ts diff --git a/README.md b/README.md index eb11e7a66..a830cddef 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,8 @@ The following flags can be appended to the CLI command. | Option | Alias | Default | Description | | :----------------------------- | :---- | :------: | :--------------------------------------------------------------------------------------------------------------------------- | +| `--help` | | | Display inline help message and exit | +| `--version` | | | Display this library’s version and exit | | `--output [location]` | `-o` | (stdout) | Where should the output file be saved? | | `--auth [token]` | | | Provide an auth token to be passed along in the request (only if accessing a private schema) | | `--header` | `-x` | | Provide an array of or singular headers as an alternative to a JSON object. Each header must follow the `key: value` pattern | @@ -271,16 +273,10 @@ By default, openapiTS will generate `updated_at?: string;` because it’s not su ```js const types = openapiTS(mySchema, { - /** transform: runs before Schema Object is converted into a TypeScript type */ transform(node: SchemaObject, options): string { if ("format" in node && node.format === "date-time") { - return "Date"; // return the TypeScript “Date” type, as a string + return "Date"; } - // if no string returned, handle it normally - }, - /** post-transform: runs after TypeScript type has been transformed */ - postTransform(type: string, options): string { - // if no string returned, keep TypeScript type as-is }, }); ``` @@ -289,6 +285,8 @@ This will generate `updated_at?: Date` instead. Note that you will still have to There are many other uses for this besides checking `format`. Because this must return a **string** you can produce any arbitrary TypeScript code you’d like (even your own custom types). +✨ Don’t forget about `postTransform()` as well! It works the same way, but runs _after_ the TypeScript transformation so you can extend/modify types as-needed. + ## 🏅 Project Goals 1. Support converting any valid OpenAPI schema to TypeScript types, no matter how complicated. diff --git a/bin/cli.js b/bin/cli.js index e26396829..a5eea4956 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -14,7 +14,8 @@ const HELP = `Usage $ openapi-typescript [input] [options] Options - --help display this + --help Display this + --version Display the version --output, -o Specify output file (default: stdout) --auth (optional) Provide an authentication token for private URL --headersObject, -h (optional) Provide a JSON object as string of HTTP headers for remote schema request @@ -42,13 +43,15 @@ function errorAndExit(errorMessage) { throw new Error(errorMessage); } -const [, , input, ...args] = process.argv; +const [, , ...args] = process.argv; if (args.includes("-ap")) errorAndExit(`The -ap alias has been deprecated. Use "--additional-properties" instead.`); if (args.includes("-it")) errorAndExit(`The -it alias has been deprecated. Use "--immutable-types" instead.`); const flags = parser(args, { array: ["header"], boolean: [ + "help", + "version", "defaultNonNullable", "immutableTypes", "contentNever", @@ -57,7 +60,6 @@ const flags = parser(args, { "pathParamsAsTypes", "alphabetize", ], - number: ["version"], string: ["auth", "header", "headersObject", "httpMethod"], alias: { header: ["x"], @@ -131,33 +133,37 @@ async function generateSchema(pathToSpec) { } async function main() { - if (flags.help) { + if ("help" in flags) { console.info(HELP); process.exit(0); } + const packageJSON = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")); + if ("version" in flags) { + console.info(`v${packageJSON.version}`); + process.exit(0); + } let output = flags.output ? OUTPUT_FILE : OUTPUT_STDOUT; // FILE or STDOUT let outputFile = new URL(flags.output, CWD); let outputDir = new URL(".", outputFile); - const pathToSpec = input; - if (output === OUTPUT_FILE) { - const packageJSON = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")); console.info(`✨ ${BOLD}openapi-typescript ${packageJSON.version}${RESET}`); // only log if we’re NOT writing to stdout } - // handle remote schema, exit - if (HTTP_RE.test(pathToSpec)) { + const pathToSpec = flags._[0]; + + // handle stdin schema, exit + if (!pathToSpec) { if (output !== "." && output === OUTPUT_FILE) fs.mkdirSync(outputDir, { recursive: true }); - await generateSchema(pathToSpec); + await generateSchema(process.stdin); return; } - // handle stdin schema, exit - if (pathToSpec === "-") { + // handle remote schema, exit + if (HTTP_RE.test(pathToSpec)) { if (output !== "." && output === OUTPUT_FILE) fs.mkdirSync(outputDir, { recursive: true }); - await generateSchema(process.stdin); + await generateSchema(pathToSpec); return; } @@ -167,12 +173,12 @@ async function main() { // error: no matches for glob if (inputSpecPaths.length === 0) { - errorAndExit(`❌ Could not find any specs matching "${pathToSpec}". Please check that the path is correct.`); + errorAndExit(`✘ Could not find any specs matching "${pathToSpec}". Please check that the path is correct.`); } // error: tried to glob output to single file if (isGlob && output === OUTPUT_FILE && fs.existsSync(outputDir) && fs.lstatSync(outputDir).isFile()) { - errorAndExit(`❌ Expected directory for --output if using glob patterns. Received "${flags.output}".`); + errorAndExit(`✘ Expected directory for --output if using glob patterns. Received "${flags.output}".`); } // generate schema(s) in parallel diff --git a/package.json b/package.json index f50c9b04a..32dc097a1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "openapi-typescript", "description": "Generate TypeScript types from Swagger OpenAPI specs", - "version": "5.4.0", + "version": "6.0.0", "author": "drew@pow.rs", "license": "MIT", "bin": { @@ -66,6 +66,7 @@ "eslint": "^8.26.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-prettier": "^4.2.1", + "execa": "^6.1.0", "prettier": "^2.7.1", "typescript": "^4.8.4", "vitest": "^0.24.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e0e55c77a..22fc9670f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,7 @@ specifiers: eslint: ^8.26.0 eslint-config-prettier: ^8.5.0 eslint-plugin-prettier: ^4.2.1 + execa: ^6.1.0 fast-glob: ^3.2.12 js-yaml: ^4.1.0 mime: ^3.0.0 @@ -40,6 +41,7 @@ devDependencies: eslint: 8.26.0 eslint-config-prettier: 8.5.0_eslint@8.26.0 eslint-plugin-prettier: 4.2.1_aniwkeyvlpmwkidetuytnokvcm + execa: 6.1.0 prettier: 2.7.1 typescript: 4.8.4 vitest: 0.24.5 @@ -1042,6 +1044,21 @@ packages: engines: {node: '>=0.10.0'} dev: true + /execa/6.1.0: + resolution: {integrity: sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 3.0.1 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.1.0 + onetime: 6.0.0 + signal-exit: 3.0.7 + strip-final-newline: 3.0.0 + dev: true + /fast-deep-equal/3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true @@ -1139,6 +1156,11 @@ packages: resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} dev: true + /get-stream/6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + dev: true + /glob-parent/5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1234,6 +1256,11 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true + /human-signals/3.0.1: + resolution: {integrity: sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ==} + engines: {node: '>=12.20.0'} + dev: true + /ignore/5.2.0: resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} engines: {node: '>= 4'} @@ -1317,6 +1344,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-stream/3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dev: true + /isexe/2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true @@ -1450,6 +1482,10 @@ packages: yargs-parser: 20.2.9 dev: true + /merge-stream/2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + dev: true + /merge2/1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1467,6 +1503,11 @@ packages: hasBin: true dev: false + /mimic-fn/4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + dev: true + /min-indent/1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -1515,12 +1556,26 @@ packages: validate-npm-package-license: 3.0.4 dev: true + /npm-run-path/5.1.0: + resolution: {integrity: sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + dependencies: + path-key: 4.0.0 + dev: true + /once/1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 dev: true + /onetime/6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + dependencies: + mimic-fn: 4.0.0 + dev: true + /optionator/0.9.1: resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} engines: {node: '>= 0.8.0'} @@ -1586,6 +1641,11 @@ packages: engines: {node: '>=8'} dev: true + /path-key/4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + dev: true + /path-parse/1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} dev: true @@ -1809,6 +1869,11 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-final-newline/3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + dev: true + /strip-indent/4.0.0: resolution: {integrity: sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==} engines: {node: '>=12'} diff --git a/test/cli.test.ts b/test/cli.test.ts new file mode 100644 index 000000000..33bdccec6 --- /dev/null +++ b/test/cli.test.ts @@ -0,0 +1,41 @@ +import { execa } from "execa"; +import fs from "node:fs"; +import { URL } from "node:url"; + +const cwd = new URL("../", import.meta.url); +const cmd = "./bin/cli.js"; + +describe("CLI", () => { + describe("snapshots", () => { + test("GitHub API", async () => { + const expected = fs.readFileSync(new URL("./examples/github-api.ts", cwd), "utf8").trim(); + const { stdout } = await execa(cmd, ["./test/fixtures/github-api.yaml"], { cwd }); + expect(stdout).toBe(expected); + }); + test("Stripe API", async () => { + const expected = fs.readFileSync(new URL("./examples/stripe-api.ts", cwd), "utf8").trim(); + const { stdout } = await execa(cmd, ["./test/fixtures/stripe-api.yaml"], { + cwd, + }); + expect(stdout).toBe(expected); + }); + test("stdin", async () => { + const expected = fs.readFileSync(new URL("./examples/stripe-api.ts", cwd), "utf8").trim(); + const input = fs.readFileSync(new URL("./test/fixtures/stripe-api.yaml", cwd)); + const { stdout } = await execa(cmd, { input }); + expect(stdout).toBe(expected); + }); + }); + + describe("flags", () => { + test("--help", async () => { + const { stdout } = await execa(cmd, ["--help"], { cwd }); + expect(stdout).toEqual(expect.stringMatching(/^Usage\n\s+\$ openapi-typescript \[input\] \[options\]/)); + }); + + test("--version", async () => { + const { stdout } = await execa(cmd, ["--version"], { cwd }); + expect(stdout).toEqual(expect.stringMatching(/^v[\d.]+(-.*)?$/)); + }); + }); +});