diff --git a/README.md b/README.md index cc46df5..0319fcf 100644 --- a/README.md +++ b/README.md @@ -44,24 +44,14 @@ Options: -i, --input OpenAPI specification, can be a path, url or string content (required) -o, --output Output directory (default: "openapi") -c, --client HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch") - --useUnionTypes Unused, will be removed in the next major version - --exportSchemas Write schemas to disk (default: false) - --indent Unused, will be removed in the next major version - --postfixServices Service name postfix (default: "Service") - --postfixModels Unused, will be removed in the next major version --request Path to custom request file - --write Write the files to disk (true or false) --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object --enums Generate JavaScript objects from enum definitions? --base Manually set base in OpenAPI config instead of inferring from server value --serviceResponse Define shape of returned value from service calls ['body', 'generics', 'response'] --operationId Use operation ID to generate operation names? --lint Process output folder with linter? - --name Custom client class name --format Process output folder with formatter? - --exportCore Export core types - --exportModels Export models - --exportServices Export services -h, --help display help for command ``` @@ -158,6 +148,15 @@ function ParentComponent() { ); } +function App() { + return ( +
+

Pet List

+ +
+ ); +} + export default App; ``` diff --git a/examples/react-app/package.json b/examples/react-app/package.json index fb997a7..c6a5005 100644 --- a/examples/react-app/package.json +++ b/examples/react-app/package.json @@ -9,7 +9,7 @@ "dev:mock": "prism mock ./petstore.yaml --dynamic", "build": "tsc && vite build", "preview": "vite preview", - "generate:api": "node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts --postfixServices=Client", + "generate:api": "node ../../dist/cli.mjs -i ./petstore.yaml -c axios --request ./request.ts", "test:generated": "tsc -p ./tsconfig.openapi.json --noEmit" }, "dependencies": { diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx index 3c18510..007340d 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -1,10 +1,10 @@ import "./App.css"; import { - useDefaultClientAddPet, - useDefaultClientFindPets, - useDefaultClientFindPetsKey, - useDefaultClientGetNotDefined, - useDefaultClientPostNotDefined, + useDefaultServiceAddPet, + useDefaultServiceFindPets, + useDefaultServiceFindPetsKey, + useDefaultServiceGetNotDefined, + useDefaultServicePostNotDefined, } from "../openapi/queries"; import { useState } from "react"; import { queryClient } from "./queryClient"; @@ -14,18 +14,16 @@ function App() { const [tags, _setTags] = useState([]); const [limit, _setLimit] = useState(10); - const { data, error, refetch } = useDefaultClientFindPets({ - data: { tags, limit }, - }); + const { data, error, refetch } = useDefaultServiceFindPets({ tags, limit }); // This is an example of a query that is not defined in the OpenAPI spec // this defaults to any - here we are showing how to override the type // Note - this is marked as deprecated in the OpenAPI spec and being passed to the client - const { data: notDefined } = useDefaultClientGetNotDefined(); + const { data: notDefined } = useDefaultServiceGetNotDefined(); const { mutate: mutateNotDefined } = - useDefaultClientPostNotDefined(); + useDefaultServicePostNotDefined(); - const { mutate: addPet } = useDefaultClientAddPet(); + const { mutate: addPet } = useDefaultServiceAddPet(); if (error) return ( @@ -48,14 +46,12 @@ function App() { onClick={() => { addPet( { - data: { - requestBody: { name: "Duggy" }, - }, + requestBody: { name: "Duggy" }, }, { onSuccess: () => { queryClient.invalidateQueries({ - queryKey: [useDefaultClientFindPetsKey], + queryKey: [useDefaultServiceFindPetsKey], }); console.log("success"); }, diff --git a/examples/react-app/src/components/SuspenseChild.tsx b/examples/react-app/src/components/SuspenseChild.tsx index d2f482e..0999316 100644 --- a/examples/react-app/src/components/SuspenseChild.tsx +++ b/examples/react-app/src/components/SuspenseChild.tsx @@ -1,7 +1,7 @@ -import { useDefaultClientFindPetsSuspense } from "../../openapi/queries/suspense"; +import { useDefaultServiceFindPetsSuspense } from "../../openapi/queries/suspense"; export const SuspenseChild = () => { - const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 }); + const { data } = useDefaultServiceFindPetsSuspense({ tags: [], limit: 10 }); if (!Array.isArray(data)) { return
Error!
; diff --git a/examples/react-app/src/components/SuspenseParent.tsx b/examples/react-app/src/components/SuspenseParent.tsx index d86e793..3498100 100644 --- a/examples/react-app/src/components/SuspenseParent.tsx +++ b/examples/react-app/src/components/SuspenseParent.tsx @@ -3,10 +3,8 @@ import { SuspenseChild } from "./SuspenseChild"; export const SuspenseParent = () => { return ( - <> - loading...}> - - - + loading...}> + + ); }; diff --git a/examples/react-app/src/main.tsx b/examples/react-app/src/main.tsx index e0d6b27..b8a928c 100644 --- a/examples/react-app/src/main.tsx +++ b/examples/react-app/src/main.tsx @@ -1,13 +1,14 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' -import App from './App' -import './index.css' -import { QueryClientProvider } from '@tanstack/react-query' -import { queryClient } from './queryClient' -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "./queryClient"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( -) +); diff --git a/package.json b/package.json index c9097e9..cce2590 100644 --- a/package.json +++ b/package.json @@ -37,14 +37,15 @@ "author": "Daiki Urata (@7nohe)", "license": "MIT", "devDependencies": { - "@hey-api/openapi-ts": "0.34.5", + "@hey-api/openapi-ts": "0.36.0", "@types/node": "^20.10.6", "commander": "^12.0.0", "glob": "^10.3.10", + "ts-morph": "^22.0.0", "typescript": "^5.3.3" }, "peerDependencies": { - "@hey-api/openapi-ts": "0.34.5", + "@hey-api/openapi-ts": "0.36.0", "commander": ">= 11 < 13", "glob": ">= 10", "typescript": ">= 4.8.3" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32e3ff1..99b8192 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@hey-api/openapi-ts': - specifier: 0.34.5 - version: 0.34.5 + specifier: 0.36.0 + version: 0.36.0(typescript@5.4.3) '@types/node': specifier: ^20.10.6 version: 20.12.2 @@ -20,6 +20,9 @@ importers: glob: specifier: ^10.3.10 version: 10.3.12 + ts-morph: + specifier: ^22.0.0 + version: 22.0.0 typescript: specifier: ^5.3.3 version: 5.4.3 @@ -516,16 +519,19 @@ packages: engines: {node: '>=14.0.0', npm: '>=6.0.0'} dev: true - /@hey-api/openapi-ts@0.34.5: - resolution: {integrity: sha512-aW5Q2Mgm0vDZK42EBWHgDk8VLptGyREA23s8FyKeE37YaNsJB5PP+gbSM1jW0QuTGGXStHHP8dpxcVz6N4XmjA==} + /@hey-api/openapi-ts@0.36.0(typescript@5.4.3): + resolution: {integrity: sha512-vm7td6EAisXiIfaQSZmR/2T7jUt+7YGjXacQ2qSlVlJCQp+RSAhZ9xxx88fAzjYHRdv4V5BXqGl2Q2lSw2sCGg==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + peerDependencies: + typescript: ^5.x dependencies: '@apidevtools/json-schema-ref-parser': 11.5.4 c12: 1.10.0 camelcase: 8.0.0 commander: 12.0.0 handlebars: 4.7.8 + typescript: 5.4.3 dev: true /@isaacs/cliui@8.0.2: @@ -574,6 +580,27 @@ packages: resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} dev: true + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -938,6 +965,15 @@ packages: engines: {node: '>= 10'} dev: true + /@ts-morph/common@0.23.0: + resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==} + dependencies: + fast-glob: 3.3.2 + minimatch: 9.0.4 + mkdirp: 3.0.1 + path-browserify: 1.0.1 + dev: true + /@types/babel__core@7.20.5: resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} dependencies: @@ -1315,6 +1351,10 @@ packages: wrap-ansi: 7.0.0 dev: true + /code-block-writer@13.0.1: + resolution: {integrity: sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==} + dev: true + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -1671,6 +1711,17 @@ packages: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} dev: true + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + /fast-redact@3.5.0: resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==} engines: {node: '>=6'} @@ -1691,6 +1742,12 @@ packages: resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} dev: true + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + /figures@2.0.0: resolution: {integrity: sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==} engines: {node: '>=4'} @@ -2347,6 +2404,11 @@ packages: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + /micri@4.5.1: resolution: {integrity: sha512-AtvnSBGFglNr+iqs5gufpHT9xRXUabgu9vYEnQYPXSBs+nLSBvmUS5Mzg+3LJ9eQBrNA1o5M49WeqiX1f+d2sg==} engines: {node: '>= 12.0.0'} @@ -2354,6 +2416,14 @@ packages: handler-agent: 0.2.0 dev: true + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -2434,6 +2504,12 @@ packages: hasBin: true dev: true + /mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + dev: true + /mlly@1.6.1: resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} dependencies: @@ -2618,6 +2694,10 @@ packages: lodash.camelcase: 4.3.0 dev: true + /path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + /path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -2773,6 +2853,10 @@ packages: engines: {node: '>=6'} dev: true + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + /quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} dev: true @@ -2861,6 +2945,11 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + /rollup@4.13.2: resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -2886,6 +2975,12 @@ packages: fsevents: 2.3.3 dev: true + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + /safe-array-concat@1.1.2: resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} engines: {node: '>=0.4'} @@ -3200,6 +3295,13 @@ packages: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} dev: true + /ts-morph@22.0.0: + resolution: {integrity: sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==} + dependencies: + '@ts-morph/common': 0.23.0 + code-block-writer: 13.0.1 + dev: true + /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} dev: true diff --git a/src/cli.mts b/src/cli.mts index 95f90ad..cb93fc4 100644 --- a/src/cli.mts +++ b/src/cli.mts @@ -31,22 +31,9 @@ async function setupProgram() { .choices(["angular", "axios", "fetch", "node", "xhr"]) .default("fetch") ) - .option("--exportSchemas ", "Write schemas to disk") - .option("--postfixServices ", "Service name postfix", "Service") - .option( - "--postfixModels ", - "Depreciated - now unused - left for backwards compatibility" - ) .option("--request ", "Path to custom request file") - .option( - "--exportCore ", - "Export core - Generate Core client classes?" - ) - .option("--exportModels ", "Generate models?") - .option("--exportServices ", "Generate services?") .option("--format", "Process output folder with formatter?") .option("--lint", "Process output folder with linter?") - .option("--name", "Custom client class name") .option("--operationId", "Use operation ID to generate operation names?") .addOption( new Option( @@ -63,26 +50,6 @@ async function setupProgram() { "--useDateType", "Use Date type instead of string for date types for models, this will not convert the data to a Date object" ) - /* TODO: Implement this feature - useOptions - * currently this will not work because the options are new exports of the Services - * we new to be able to import any of the options from each service into the queries files - */ - // .option("--useOptions ", "Use options or arguments functions") - .option("--write", "Write the files to disk (true or false)") - // TODO: remove these options in the next major release - .addOption( - new Option( - "--indent ", - "Depreciated - now unused - left for backwards compatibility" - ).hideHelp() - ) - // TODO: remove these options in the next major release - .addOption( - new Option( - "--useUnionTypes ", - "Depreciated - now unused - left for backwards compatibility" - ).hideHelp() - ) .parse(); const options = program.opts(); diff --git a/src/common.mts b/src/common.mts index 59e9895..3d8acb2 100644 --- a/src/common.mts +++ b/src/common.mts @@ -1,6 +1,12 @@ import { type PathLike } from "fs"; import { stat } from "fs/promises"; -import ts, { JSDocComment, NodeArray, SourceFile } from "typescript"; +import ts from "typescript"; +import { + MethodDeclaration, + JSDoc, + SourceFile, + ParameterDeclaration, +} from "ts-morph"; export const TData = ts.factory.createIdentifier("TData"); export const TError = ts.factory.createIdentifier("TError"); @@ -20,20 +26,17 @@ export const lowercaseFirstLetter = (str: string) => { return str.charAt(0).toLowerCase() + str.slice(1); }; -export const getNameFromMethod = ( - method: ts.MethodDeclaration, - node: ts.SourceFile -) => { - return method.name.getText(node); +export const getNameFromMethod = (method: MethodDeclaration) => { + return method.getName(); }; export type MethodDescription = { className: string; node: SourceFile; - method: ts.MethodDeclaration; + method: MethodDeclaration; methodBlock: ts.Block; httpMethodName: string; - jsDoc: (string | NodeArray | undefined)[]; + jsDoc: JSDoc[]; isDeprecated: boolean; }; @@ -71,3 +74,18 @@ export function safeParseNumber(value: unknown): number { } return NaN; } + +export function extractPropertiesFromObjectParam(param: ParameterDeclaration) { + const referenced = param.findReferences()[0]; + const def = referenced.getDefinition(); + const paramNodes = def + .getNode() + .getType() + .getProperties() + .map((prop) => ({ + name: prop.getName(), + optional: prop.isOptional(), + type: prop.getValueDeclaration()?.getType()!, + })); + return paramNodes; +} diff --git a/src/createImports.mts b/src/createImports.mts index 4c1e8f3..af46878 100644 --- a/src/createImports.mts +++ b/src/createImports.mts @@ -2,6 +2,7 @@ import ts from "typescript"; import { glob } from "glob"; import { extname, basename, posix } from "path"; import { Service } from "./service.mjs"; +import { Project } from "ts-morph"; const { join } = posix; @@ -9,10 +10,12 @@ export const createImports = async ({ generatedClientsPath, service, serviceEndName, + project, }: { generatedClientsPath: string; service: Service; serviceEndName: string; + project: Project; }) => { const { klasses } = service; // get all class names @@ -20,6 +23,16 @@ export const createImports = async ({ // remove duplicates const uniqueClassNames = [...new Set(classNames)]; + const modelsFile = project + .getSourceFiles() + .find((sourceFile) => sourceFile.getFilePath().includes("models.ts")); + + if (!modelsFile) { + throw new Error("No models file found"); + } + + const modalNames = Array.from(modelsFile.getExportedDeclarations().keys()); + const modalsPath = join(generatedClientsPath, "models").replace(/\\/g, "/"); const servicesPath = join(generatedClientsPath, "services").replace( /\\/g, @@ -158,5 +171,24 @@ export const createImports = async ({ undefined ); }), + // import all the models by name + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ...modalNames.map((modelName) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(modelName) + ) + ), + ]) + ), + ts.factory.createStringLiteral(join("../requests/models")), + undefined + ), ]; }; diff --git a/src/createSource.mts b/src/createSource.mts index 3e5881f..3147e89 100644 --- a/src/createSource.mts +++ b/src/createSource.mts @@ -2,14 +2,28 @@ import ts from "typescript"; import { createImports } from "./createImports.mjs"; import { createExports } from "./createExports.mjs"; import { getServices } from "./service.mjs"; +import { Project } from "ts-morph"; +import { join } from "path"; const createSourceFile = async (outputPath: string, serviceEndName: string) => { - const service = await getServices(outputPath); + const project = new Project({ + // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here. + // If you initialize with a tsconfig.json, then it will automatically populate the project + // with the associated source files. + // Read more: https://ts-morph.com/setup/ + skipAddingFilesFromTsConfig: true, + }); + + const sourceFiles = join(process.cwd(), outputPath); + project.addSourceFilesAtPaths(`${sourceFiles}/**/*`); + + const service = await getServices(project); const imports = await createImports({ generatedClientsPath: outputPath, service, serviceEndName, + project, }); const exports = createExports(service); diff --git a/src/createUseMutation.mts b/src/createUseMutation.mts index a2e41aa..a302536 100644 --- a/src/createUseMutation.mts +++ b/src/createUseMutation.mts @@ -6,6 +6,7 @@ import { TData, TError, capitalizeFirstLetter, + extractPropertiesFromObjectParam, getNameFromMethod, } from "./common.mjs"; import { addJSDocToNode } from "./util.mjs"; @@ -46,7 +47,7 @@ export const createUseMutation = ({ jsDoc = [], isDeprecated = false, }: MethodDescription) => { - const methodName = getNameFromMethod(method, node); + const methodName = getNameFromMethod(method); const awaitedResponseDataType = generateAwaitedReturnType({ className, methodName, @@ -69,18 +70,37 @@ export const createUseMutation = ({ ); const methodParameters = - method.parameters.length !== 0 + method.getParameters().length !== 0 ? ts.factory.createTypeLiteralNode( - method.parameters.map((param) => { - return ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier(param.name.getText(node)), - param.questionToken ?? param.initializer - ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) - : param.questionToken, - param.type - ); - }) + method + .getParameters() + .map((param) => { + const paramNodes = extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => + ts.factory.createPropertySignature( + undefined, + ts.factory.createIdentifier(refParam.name), + refParam.optional + ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + : undefined, + // refParam.questionToken ?? refParam.initializer + // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + // : refParam.questionToken, + ts.factory.createTypeReferenceNode( + refParam.type.getText(param) + ) + ) + ); + }) + .flat() + // return ts.factory.createPropertySignature( + // undefined, + // ts.factory.createIdentifier(param.getName()), + // param.compilerNode.questionToken ?? param.compilerNode.initializer + // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + // : param.compilerNode.questionToken, + // param.compilerNode.type + // ); ) : ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); @@ -154,22 +174,29 @@ export const createUseMutation = ({ ts.factory.createArrowFunction( undefined, undefined, - method.parameters.length !== 0 + method.getParameters().length !== 0 ? [ ts.factory.createParameterDeclaration( undefined, undefined, ts.factory.createObjectBindingPattern( - method.parameters.map((param) => { - return ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier( - param.name.getText(node) - ), - undefined - ); - }) + method + .getParameters() + .map((param) => { + const paramNodes = + extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier( + refParam.name + ), + undefined + ) + ); + }) + .flat() ), undefined, undefined, @@ -189,11 +216,26 @@ export const createUseMutation = ({ ts.factory.createIdentifier(methodName) ), undefined, - method.parameters.map((params) => - ts.factory.createIdentifier( - params.name.getText(node) - ) - ) + method.getParameters().length !== 0 + ? [ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .map((params) => { + const paramNodes = + extractPropertiesFromObjectParam( + params + ); + return paramNodes.map((refParam) => + ts.factory.createShorthandPropertyAssignment( + refParam.name + ) + ); + }) + .flat() + ), + ] + : [] ), ts.factory.createKeywordTypeNode( ts.SyntaxKind.UnknownKeyword diff --git a/src/createUseQuery.mts b/src/createUseQuery.mts index 17be417..2d7e4af 100644 --- a/src/createUseQuery.mts +++ b/src/createUseQuery.mts @@ -1,7 +1,9 @@ import ts from "typescript"; +import { MethodDeclaration } from "ts-morph"; import { BuildCommonTypeName, capitalizeFirstLetter, + extractPropertiesFromObjectParam, getNameFromMethod, queryKeyConstraint, queryKeyGenericType, @@ -70,11 +72,8 @@ export const createApiResponseType = ({ }; }; -export function getRequestParamFromMethod( - method: ts.MethodDeclaration, - node: ts.SourceFile -) { - if (!method.parameters.length) { +export function getRequestParamFromMethod(method: MethodDeclaration) { + if (!method.getParameters().length) { return null; } @@ -84,41 +83,42 @@ export function getRequestParamFromMethod( undefined, undefined, ts.factory.createObjectBindingPattern( - method.parameters.map((param) => { - const type = param.type; - if (!type) { - throw new Error("No type found"); - } - const subTypes = type?.getChildren(); - if (!subTypes) { - throw new Error("No subType found"); - } - console.log(param.type as ts.TypeNode); - // console.log(param.type?.getText(node)); - // console.log(subType.getText(node)); - subTypes.forEach((subType) => { - console.log(subType.kind); - }); - return ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier(param.name.getText(node)), - undefined - ); - }) + method + .getParameters() + .map((param) => { + const paramNodes = extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier(refParam.name), + undefined + ) + ); + }) + .flat() ), undefined, ts.factory.createTypeLiteralNode( - method.parameters.map((param) => - ts.factory.createPropertySignature( - undefined, - ts.factory.createIdentifier(param.name.getText(node)), - param.questionToken ?? param.initializer - ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) - : param.questionToken, - param.type - ) - ) + method + .getParameters() + .map((param) => { + const paramNodes = extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => { + return ts.factory.createPropertySignature( + undefined, + ts.factory.createIdentifier(refParam.name), + refParam.optional + ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + : undefined, + // param.hasQuestionToken() ?? param.getInitializer()?.compilerNode + // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) + // : param.getQuestionTokenNode()?.compilerNode, + ts.factory.createTypeReferenceNode(refParam.type.getText()) + ); + }); + }) + .flat() ) ); } @@ -199,27 +199,23 @@ export function createQueryKeyExport({ function hookNameFromMethod({ method, - node, className, }: { - method: ts.MethodDeclaration; - node: ts.SourceFile; + method: MethodDeclaration; className: string; }) { - const methodName = getNameFromMethod(method, node); + const methodName = getNameFromMethod(method); return `use${className}${capitalizeFirstLetter(methodName)}`; } function createQueryKeyFromMethod({ method, - node, className, }: { - method: ts.MethodDeclaration; - node: ts.SourceFile; + method: MethodDeclaration; className: string; }) { - const customHookName = hookNameFromMethod({ method, node, className }); + const customHookName = hookNameFromMethod({ method, className }); const queryKey = `${customHookName}Key`; return queryKey; } @@ -235,20 +231,18 @@ function createQueryHook({ responseDataType, requestParams, method, - node, className, }: { queryString: "useSuspenseQuery" | "useQuery"; suffix: string; responseDataType: ts.TypeParameterDeclaration; requestParams: ts.ParameterDeclaration[]; - method: ts.MethodDeclaration; - node: ts.SourceFile; + method: MethodDeclaration; className: string; }) { - const methodName = getNameFromMethod(method, node); - const customHookName = hookNameFromMethod({ method, node, className }); - const queryKey = createQueryKeyFromMethod({ method, node, className }); + const methodName = getNameFromMethod(method); + const customHookName = hookNameFromMethod({ method, className }); + const queryKey = createQueryKeyFromMethod({ method, className }); const hookExport = ts.factory.createVariableStatement( [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], @@ -338,16 +332,23 @@ function createQueryHook({ ts.factory.createToken( ts.SyntaxKind.QuestionQuestionToken ), - method.parameters.length + method.getParameters().length ? ts.factory.createArrayLiteralExpression([ ts.factory.createObjectLiteralExpression( - method.parameters.map((param) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier( - param.name.getText(node) + method + .getParameters() + .map((param) => + extractPropertiesFromObjectParam( + param + ).map((p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier( + p.name + ) + ) ) ) - ) + .flat() ), ]) : ts.factory.createArrayLiteralExpression([]) @@ -375,11 +376,24 @@ function createQueryHook({ ts.factory.createIdentifier(methodName) ), undefined, - method.parameters.map((param) => - ts.factory.createIdentifier( - param.name.getText(node) - ) - ) + method.getParameters().length + ? [ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .map((param) => + extractPropertiesFromObjectParam( + param + ).map((p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier(p.name) + ) + ) + ) + .flat() + ), + ] + : undefined ), ts.factory.createTypeReferenceNode(TData) ) @@ -407,15 +421,15 @@ export const createUseQuery = ({ jsDoc = [], isDeprecated: deprecated = false, }: MethodDescription) => { - const methodName = getNameFromMethod(method, node); - const queryKey = createQueryKeyFromMethod({ method, node, className }); + const methodName = getNameFromMethod(method); + const queryKey = createQueryKeyFromMethod({ method, className }); const { apiResponse: defaultApiResponse, responseDataType } = createApiResponseType({ className, methodName, }); - const requestParam = getRequestParamFromMethod(method, node); + const requestParam = getRequestParamFromMethod(method); const requestParams = requestParam ? [requestParam] : []; @@ -425,7 +439,6 @@ export const createUseQuery = ({ responseDataType, requestParams, method, - node, className, }); const suspenseQueryHook = createQueryHook({ @@ -434,7 +447,6 @@ export const createUseQuery = ({ responseDataType, requestParams, method, - node, className, }); diff --git a/src/generate.mts b/src/generate.mts index b713944..bde4d22 100644 --- a/src/generate.mts +++ b/src/generate.mts @@ -30,16 +30,20 @@ export async function generate(options: UserConfig, version: string) { }, options ); - await createClient({ + const config: UserConfig = { ...formattedOptions, output: openApiOutputPath, useOptions: true, - }); - const { postfixServices } = formattedOptions; + exportCore: true, + exportModels: true, + exportServices: true, + write: true, + }; + await createClient(config); const source = await createSource({ outputPath: openApiOutputPath, version, - serviceEndName: postfixServices!, + serviceEndName: "Service", // we are hard coding this because changing the service end name was depreciated in @hey-api/openapi-ts }); await print(source, formattedOptions); } diff --git a/src/service.mts b/src/service.mts index a648bc0..14239ed 100644 --- a/src/service.mts +++ b/src/service.mts @@ -1,39 +1,29 @@ -import { readFile } from "fs/promises"; -import { join } from "path"; -import ts, { JSDoc } from "typescript"; +import ts from "typescript"; +import { ClassDeclaration, Project, SourceFile } from "ts-morph"; import { MethodDescription } from "./common.mjs"; export type Service = { - node: ts.SourceFile; + node: SourceFile; klasses: Array<{ className: string; - klass: ts.ClassDeclaration; + klass: ClassDeclaration; methods: Array; }>; }; -export async function getServices( - generatedClientsPath: string -): Promise { - const pathToService = join(generatedClientsPath, "services.ts").replace( - /\\/g, - "/" - ); - const servicesPath = await readFile( - join(process.cwd(), pathToService), - "utf-8" - ); - - const node = ts.createSourceFile( - pathToService, // fileName - servicesPath, - ts.ScriptTarget.Latest // languageVersion - ); +export async function getServices(project: Project): Promise { + const node = project + .getSourceFiles() + .find((sourceFile) => sourceFile.getFilePath().includes("services.ts")); + + if (!node) { + throw new Error("No service node found"); + } const klasses = getClassesFromService(node); return { klasses: klasses.map(({ klass, className }) => ({ - className: className, + className, klass, methods: getMethodsFromService(node, klass), })), @@ -41,24 +31,18 @@ export async function getServices( } satisfies Service; } -function getClassesFromService(node: ts.SourceFile) { - const nodeChildren = node.getChildren(); - if (!nodeChildren.length) { - throw new Error("No children found"); - } - - const subChildren = nodeChildren.map((child) => child.getChildren()).flat(); +function getClassesFromService(node: SourceFile) { + const klasses = node.getClasses(); - const foundKlasses = subChildren.filter( - (child) => child.kind === ts.SyntaxKind.ClassDeclaration - ); - - if (!foundKlasses.length) { + if (!klasses.length) { throw new Error("No classes found"); } - const klasses = foundKlasses as ts.ClassDeclaration[]; + return klasses.map((klass) => { - const className = getClassNameFromClassNode(klass); + const className = klass.getName(); + if (!className) { + throw new Error("Class name not found"); + } return { className, klass, @@ -66,8 +50,8 @@ function getClassesFromService(node: ts.SourceFile) { }); } -function getClassNameFromClassNode(klass: ts.ClassDeclaration) { - const className = String(klass.name?.escapedText); +function getClassNameFromClassNode(klass: ClassDeclaration) { + const className = klass.getName(); if (!className) { throw new Error("Class name not found"); @@ -75,19 +59,14 @@ function getClassNameFromClassNode(klass: ts.ClassDeclaration) { return className; } -function getMethodsFromService( - node: ts.SourceFile, - klass: ts.ClassDeclaration -) { - const methods = klass.members.filter( - (node) => node.kind === ts.SyntaxKind.MethodDeclaration - ) as ts.MethodDeclaration[]; +function getMethodsFromService(node: SourceFile, klass: ClassDeclaration) { + const methods = klass.getMethods(); if (!methods.length) { throw new Error("No methods found"); } return methods.map((method) => { - const methodBlockNode = method - .getChildren(node) + const methodBlockNode = method.compilerNode + .getChildren(node.compilerNode) .find((child) => child.kind === ts.SyntaxKind.Block); if (!methodBlockNode) { @@ -110,15 +89,15 @@ function getMethodsFromService( callExpression.arguments[1] as ts.ObjectLiteralExpression ).properties as unknown as ts.PropertyAssignment[]; const httpMethodName = properties - .find((p) => p.name?.getText(node) === "method") - ?.initializer?.getText(node); + .find((p) => p.name?.getText(node.compilerNode) === "method") + ?.initializer?.getText(node.compilerNode); if (!httpMethodName) { throw new Error("httpMethodName not found"); } const getAllChildren = (tsNode: ts.Node): Array => { - const childItems = tsNode.getChildren(node); + const childItems = tsNode.getChildren(node.compilerNode); if (childItems.length) { const allChildren = childItems.map(getAllChildren); return [tsNode].concat(allChildren.flat()); @@ -126,10 +105,8 @@ function getMethodsFromService( return [tsNode]; }; - const children = getAllChildren(method); - const jsDoc = children - .filter((c) => c.kind === ts.SyntaxKind.JSDoc) - .map((c) => (c as JSDoc).comment); + const children = getAllChildren(method.compilerNode); + const jsDoc = method.getJsDocs().map((jsDoc) => jsDoc); const isDeprecated = children.some( (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag ); diff --git a/src/util.mts b/src/util.mts index 8dd7f73..9de8417 100644 --- a/src/util.mts +++ b/src/util.mts @@ -1,10 +1,11 @@ -import ts from 'typescript'; +import ts from "typescript"; +import { JSDoc, SourceFile } from "ts-morph"; export function addJSDocToNode( node: T, - sourceFile: ts.SourceFile, + sourceFile: SourceFile, deprecated: boolean, - jsDoc: (string | ts.NodeArray | undefined)[] = [], + jsDoc: JSDoc[] = [] ): T { const deprecatedString = deprecated ? "@deprecated" : ""; @@ -39,6 +40,6 @@ export function addJSDocToNode( true ) : node; - + return nodeWithJSDoc; -} \ No newline at end of file +}