diff --git a/biome.json b/biome.json index b9b28d8..e714897 100644 --- a/biome.json +++ b/biome.json @@ -1,18 +1,20 @@ { - "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", - "organizeImports": { - "enabled": true - }, - "files": { - "ignore": ["dist", "examples/react-app/openapi"] - }, - "linter": { - "enabled": true, - "rules": { - "recommended": true - } - }, - "formatter": { - "enabled": true - } + "$schema": "https://biomejs.dev/schemas/1.7.0/schema.json", + "organizeImports": { + "enabled": true + }, + "files": { + "ignore": ["dist", "examples/react-app/openapi"] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + } } diff --git a/examples/react-app/package.json b/examples/react-app/package.json index 697596c..c6a5005 100644 --- a/examples/react-app/package.json +++ b/examples/react-app/package.json @@ -1,31 +1,31 @@ { - "name": "@7nohe/react-app", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "run-p dev:mock dev:client", - "dev:client": "vite --clearScreen=false", - "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", - "test:generated": "tsc -p ./tsconfig.openapi.json --noEmit" - }, - "dependencies": { - "@tanstack/react-query": "^5.18.1", - "axios": "^1.6.7", - "form-data": "~4.0.0", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@stoplight/prism-cli": "^5.5.2", - "@types/react": "^18.2.52", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react": "^4.2.1", - "npm-run-all": "^4.1.5", - "typescript": "^5.3.3", - "vite": "^5.0.12" - } + "name": "@7nohe/react-app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "run-p dev:mock dev:client", + "dev:client": "vite --clearScreen=false", + "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", + "test:generated": "tsc -p ./tsconfig.openapi.json --noEmit" + }, + "dependencies": { + "@tanstack/react-query": "^5.18.1", + "axios": "^1.6.7", + "form-data": "~4.0.0", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@stoplight/prism-cli": "^5.5.2", + "@types/react": "^18.2.52", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.1", + "npm-run-all": "^4.1.5", + "typescript": "^5.3.3", + "vite": "^5.0.12" + } } diff --git a/examples/react-app/request.ts b/examples/react-app/request.ts index 5d73aaf..dd12c47 100644 --- a/examples/react-app/request.ts +++ b/examples/react-app/request.ts @@ -9,83 +9,83 @@ import type { OpenAPIConfig } from "./OpenAPI"; const source = axios.CancelToken.source(); const axiosInstance = axios.create({ - // Your custom Axios instance config - baseURL: "http://localhost:4010", - headers: { - // Your custom headers - } satisfies RawAxiosRequestHeaders, + // Your custom Axios instance config + baseURL: "http://localhost:4010", + headers: { + // Your custom headers + } satisfies RawAxiosRequestHeaders, }); // Add a request interceptor axiosInstance.interceptors.request.use( - (config) => { - // Do something before request is sent - if (!config.url || !config.params) { - return config; - } + (config) => { + // Do something before request is sent + if (!config.url || !config.params) { + return config; + } - for (const [key, value] of Object.entries(config.params)) { - const stringToSearch = `{${key}}`; - if ( - config.url !== undefined && - config.url.search(stringToSearch) !== -1 - ) { - config.url = config.url.replace(`{${key}}`, encodeURIComponent(value)); - delete config.params[key]; - } - } + for (const [key, value] of Object.entries(config.params)) { + const stringToSearch = `{${key}}`; + if ( + config.url !== undefined && + config.url.search(stringToSearch) !== -1 + ) { + config.url = config.url.replace(`{${key}}`, encodeURIComponent(value)); + delete config.params[key]; + } + } - return config; - }, - (error) => { - // Do something with request error - return Promise.reject(error); - }, + return config; + }, + (error) => { + // Do something with request error + return Promise.reject(error); + }, ); // Add a response interceptor axiosInstance.interceptors.response.use( - (response) => { - // Any status code that lie within the range of 2xx cause this function to trigger - // Do something with response data - return response; - }, - (error) => { - // Any status codes that falls outside the range of 2xx cause this function to trigger - // Do something with response error - return Promise.reject(error); - }, + (response) => { + // Any status code that lie within the range of 2xx cause this function to trigger + // Do something with response data + return response; + }, + (error) => { + // Any status codes that falls outside the range of 2xx cause this function to trigger + // Do something with response error + return Promise.reject(error); + }, ); export const request = ( - config: OpenAPIConfig, - options: ApiRequestOptions, + config: OpenAPIConfig, + options: ApiRequestOptions, ): CancelablePromise => { - return new CancelablePromise((resolve, reject, onCancel) => { - onCancel(() => source.cancel("The user aborted a request.")); + return new CancelablePromise((resolve, reject, onCancel) => { + onCancel(() => source.cancel("The user aborted a request.")); - let formattedHeaders = options.headers as RawAxiosRequestHeaders; - if (options.mediaType) { - formattedHeaders = { - ...options.headers, - "Content-Type": options.mediaType, - } satisfies RawAxiosRequestHeaders; - } + let formattedHeaders = options.headers as RawAxiosRequestHeaders; + if (options.mediaType) { + formattedHeaders = { + ...options.headers, + "Content-Type": options.mediaType, + } satisfies RawAxiosRequestHeaders; + } - return axiosInstance - .request({ - url: options.url, - data: options.body, - method: options.method, - params: options.path, - headers: formattedHeaders, - cancelToken: source.token, - }) - .then((res) => { - resolve(res.data); - }) - .catch((error) => { - reject(error); - }); - }); + return axiosInstance + .request({ + url: options.url, + data: options.body, + method: options.method, + params: options.path, + headers: formattedHeaders, + cancelToken: source.token, + }) + .then((res) => { + resolve(res.data); + }) + .catch((error) => { + reject(error); + }); + }); }; diff --git a/examples/react-app/src/App.tsx b/examples/react-app/src/App.tsx index 36e80df..9f556a3 100644 --- a/examples/react-app/src/App.tsx +++ b/examples/react-app/src/App.tsx @@ -1,79 +1,79 @@ import { useState } from "react"; import { - useDefaultServiceAddPet, - useDefaultServiceFindPets, - useDefaultServiceFindPetsKey, - useDefaultServiceGetNotDefined, - useDefaultServicePostNotDefined, + useDefaultServiceAddPet, + useDefaultServiceFindPets, + useDefaultServiceFindPetsKey, + useDefaultServiceGetNotDefined, + useDefaultServicePostNotDefined, } from "../openapi/queries"; import "./App.css"; import { SuspenseParent } from "./components/SuspenseParent"; import { queryClient } from "./queryClient"; function App() { - const [tags, _setTags] = useState([]); - const [limit, _setLimit] = useState(10); + const [tags, _setTags] = useState([]); + const [limit, _setLimit] = useState(10); - const { data, error, refetch } = useDefaultServiceFindPets({ tags, limit }); - // This is an example of using a hook that has all parameters optional; - // Here we do not have to pass in an object - const { data: _ } = useDefaultServiceFindPets(); + const { data, error, refetch } = useDefaultServiceFindPets({ tags, limit }); + // This is an example of using a hook that has all parameters optional; + // Here we do not have to pass in an object + const { data: _ } = useDefaultServiceFindPets(); - // 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 } = useDefaultServiceGetNotDefined(); - const { mutate: mutateNotDefined } = - useDefaultServicePostNotDefined(); + // 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 } = useDefaultServiceGetNotDefined(); + const { mutate: mutateNotDefined } = + useDefaultServicePostNotDefined(); - const { mutate: addPet } = useDefaultServiceAddPet(); + const { mutate: addPet } = useDefaultServiceAddPet(); - if (error) - return ( -
-

Failed to fetch pets

- -
- ); + if (error) + return ( +
+

Failed to fetch pets

+ +
+ ); - return ( -
-

Pet List

-
    - {Array.isArray(data) && - data?.map((pet, index) => ( -
  • {pet.name}
  • - ))} -
- -
-

Suspense Components

- -
-
- ); + return ( +
+

Pet List

+
    + {Array.isArray(data) && + data?.map((pet, index) => ( +
  • {pet.name}
  • + ))} +
+ +
+

Suspense Components

+ +
+
+ ); } export default App; diff --git a/examples/react-app/src/components/SuspenseChild.tsx b/examples/react-app/src/components/SuspenseChild.tsx index c1cc6fd..0999316 100644 --- a/examples/react-app/src/components/SuspenseChild.tsx +++ b/examples/react-app/src/components/SuspenseChild.tsx @@ -1,17 +1,17 @@ import { useDefaultServiceFindPetsSuspense } from "../../openapi/queries/suspense"; export const SuspenseChild = () => { - const { data } = useDefaultServiceFindPetsSuspense({ tags: [], limit: 10 }); + const { data } = useDefaultServiceFindPetsSuspense({ tags: [], limit: 10 }); - if (!Array.isArray(data)) { - return
Error!
; - } + if (!Array.isArray(data)) { + return
Error!
; + } - return ( -
    - {data?.map((pet, index) => ( -
  • {pet.name}
  • - ))} -
- ); + return ( +
    + {data?.map((pet, index) => ( +
  • {pet.name}
  • + ))} +
+ ); }; diff --git a/examples/react-app/src/components/SuspenseParent.tsx b/examples/react-app/src/components/SuspenseParent.tsx index 69d4b35..3498100 100644 --- a/examples/react-app/src/components/SuspenseParent.tsx +++ b/examples/react-app/src/components/SuspenseParent.tsx @@ -2,9 +2,9 @@ import { Suspense } from "react"; import { SuspenseChild } from "./SuspenseChild"; export const SuspenseParent = () => { - return ( - loading...}> - - - ); + return ( + loading...}> + + + ); }; diff --git a/examples/react-app/src/main.tsx b/examples/react-app/src/main.tsx index b9fae9f..7b57301 100644 --- a/examples/react-app/src/main.tsx +++ b/examples/react-app/src/main.tsx @@ -6,9 +6,9 @@ import "./index.css"; import { queryClient } from "./queryClient"; ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - , + + + + + , ); diff --git a/examples/react-app/tsconfig.json b/examples/react-app/tsconfig.json index 9567105..374b672 100644 --- a/examples/react-app/tsconfig.json +++ b/examples/react-app/tsconfig.json @@ -1,24 +1,24 @@ { - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"], - "references": [ - { "path": "./tsconfig.node.json" }, - { "path": "./tsconfig.openapi.json" } - ] + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [ + { "path": "./tsconfig.node.json" }, + { "path": "./tsconfig.openapi.json" } + ] } diff --git a/examples/react-app/tsconfig.node.json b/examples/react-app/tsconfig.node.json index d3bf4b8..9d31e2a 100644 --- a/examples/react-app/tsconfig.node.json +++ b/examples/react-app/tsconfig.node.json @@ -1,9 +1,9 @@ { - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] } diff --git a/examples/react-app/tsconfig.openapi.json b/examples/react-app/tsconfig.openapi.json index a105ec7..89de92d 100644 --- a/examples/react-app/tsconfig.openapi.json +++ b/examples/react-app/tsconfig.openapi.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "composite": true, - "target": "ESNext", - "useDefineForClassFields": true, - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node10", - "resolveJsonModule": true, - "isolatedModules": true - }, - "include": ["openapi"] + "compilerOptions": { + "composite": true, + "target": "ESNext", + "useDefineForClassFields": true, + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node10", + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["openapi"] } diff --git a/examples/react-app/vite.config.ts b/examples/react-app/vite.config.ts index e29a791..1ff0da0 100644 --- a/examples/react-app/vite.config.ts +++ b/examples/react-app/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from "vite"; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()], }); diff --git a/package.json b/package.json index b8feb10..f9a9a27 100644 --- a/package.json +++ b/package.json @@ -1,56 +1,56 @@ { - "name": "@7nohe/openapi-react-query-codegen", - "version": "1.0.7", - "description": "OpenAPI React Query Codegen", - "bin": { - "openapi-rq": "dist/cli.mjs" - }, - "type": "module", - "workspaces": ["examples/*"], - "scripts": { - "build": "rimraf dist && tsc -p tsconfig.json", - "preview": "npm run build && npm -C examples/react-app run generate:api", - "prepublishOnly": "npm run build", - "release": "npx git-ensure -a && npx bumpp --commit --tag --push" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/7nohe/openapi-react-query-codegen.git" - }, - "homepage": "https://github.com/7nohe/openapi-react-query-codegen", - "bugs": "https://github.com/7nohe/openapi-react-query-codegen/issues", - "files": ["dist"], - "keywords": [ - "codegen", - "react-query", - "react", - "openapi", - "swagger", - "typescript", - "openapi-typescript-codegen", - "@hey-api/openapi-ts" - ], - "author": "Daiki Urata (@7nohe)", - "license": "MIT", - "devDependencies": { - "@biomejs/biome": "1.7.0", - "@hey-api/openapi-ts": "0.36.0", - "@types/node": "^20.10.6", - "commander": "^12.0.0", - "glob": "^10.3.10", - "lefthook": "^1.6.10", - "rimraf": "^5.0.5", - "ts-morph": "^22.0.0", - "typescript": "^5.3.3" - }, - "peerDependencies": { - "@hey-api/openapi-ts": "0.36.0", - "commander": ">= 11 < 13", - "glob": ">= 10", - "ts-morph": ">= 22 < 23", - "typescript": ">= 4.8.3" - }, - "engines": { - "node": ">=14" - } + "name": "@7nohe/openapi-react-query-codegen", + "version": "1.0.7", + "description": "OpenAPI React Query Codegen", + "bin": { + "openapi-rq": "dist/cli.mjs" + }, + "type": "module", + "workspaces": ["examples/*"], + "scripts": { + "build": "rimraf dist && tsc -p tsconfig.json", + "preview": "npm run build && npm -C examples/react-app run generate:api", + "prepublishOnly": "npm run build", + "release": "npx git-ensure -a && npx bumpp --commit --tag --push" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/7nohe/openapi-react-query-codegen.git" + }, + "homepage": "https://github.com/7nohe/openapi-react-query-codegen", + "bugs": "https://github.com/7nohe/openapi-react-query-codegen/issues", + "files": ["dist"], + "keywords": [ + "codegen", + "react-query", + "react", + "openapi", + "swagger", + "typescript", + "openapi-typescript-codegen", + "@hey-api/openapi-ts" + ], + "author": "Daiki Urata (@7nohe)", + "license": "MIT", + "devDependencies": { + "@biomejs/biome": "1.7.0", + "@hey-api/openapi-ts": "0.36.0", + "@types/node": "^20.10.6", + "commander": "^12.0.0", + "glob": "^10.3.10", + "lefthook": "^1.6.10", + "rimraf": "^5.0.5", + "ts-morph": "^22.0.0", + "typescript": "^5.3.3" + }, + "peerDependencies": { + "@hey-api/openapi-ts": "0.36.0", + "commander": ">= 11 < 13", + "glob": ">= 10", + "ts-morph": ">= 22 < 23", + "typescript": ">= 4.8.3" + }, + "engines": { + "node": ">=14" + } } diff --git a/src/cli.mts b/src/cli.mts index 9fad95d..5b07627 100644 --- a/src/cli.mts +++ b/src/cli.mts @@ -11,55 +11,55 @@ const program = new Command(); export type LimitedUserConfig = Omit; async function setupProgram() { - const __filename = fileURLToPath(import.meta.url); - const __dirname = dirname(__filename); - const file = await readFile(join(__dirname, "../package.json"), "utf-8"); - const packageJson = JSON.parse(file); - const version = packageJson.version; + const __filename = fileURLToPath(import.meta.url); + const __dirname = dirname(__filename); + const file = await readFile(join(__dirname, "../package.json"), "utf-8"); + const packageJson = JSON.parse(file); + const version = packageJson.version; - program - .name("openapi-rq") - .version(version) - .description("Generate React Query code based on OpenAPI") - .requiredOption( - "-i, --input ", - "OpenAPI specification, can be a path, url or string content (required)", - ) - .option("-o, --output ", "Output directory", "openapi") - .addOption( - new Option("-c, --client ", "HTTP client to generate") - .choices(["angular", "axios", "fetch", "node", "xhr"]) - .default("fetch"), - ) - .option("--request ", "Path to custom request file") - .option("--format", "Process output folder with formatter?") - .option("--lint", "Process output folder with linter?") - .option("--operationId", "Use operation ID to generate operation names?") - .addOption( - new Option( - "--serviceResponse ", - "Define shape of returned value from service calls", - ).choices(["body", "generics", "response"]), - ) - .option( - "--base ", - "Manually set base in OpenAPI config instead of inferring from server value", - ) - .addOption( - new Option( - "--enums ", - "Generate JavaScript objects from enum definitions?", - ).choices(["javascript", "typescript"]), - ) - .option( - "--useDateType", - "Use Date type instead of string for date types for models, this will not convert the data to a Date object", - ) - .parse(); + program + .name("openapi-rq") + .version(version) + .description("Generate React Query code based on OpenAPI") + .requiredOption( + "-i, --input ", + "OpenAPI specification, can be a path, url or string content (required)", + ) + .option("-o, --output ", "Output directory", "openapi") + .addOption( + new Option("-c, --client ", "HTTP client to generate") + .choices(["angular", "axios", "fetch", "node", "xhr"]) + .default("fetch"), + ) + .option("--request ", "Path to custom request file") + .option("--format", "Process output folder with formatter?") + .option("--lint", "Process output folder with linter?") + .option("--operationId", "Use operation ID to generate operation names?") + .addOption( + new Option( + "--serviceResponse ", + "Define shape of returned value from service calls", + ).choices(["body", "generics", "response"]), + ) + .option( + "--base ", + "Manually set base in OpenAPI config instead of inferring from server value", + ) + .addOption( + new Option( + "--enums ", + "Generate JavaScript objects from enum definitions?", + ).choices(["javascript", "typescript"]), + ) + .option( + "--useDateType", + "Use Date type instead of string for date types for models, this will not convert the data to a Date object", + ) + .parse(); - const options = program.opts(); + const options = program.opts(); - await generate(options, version); + await generate(options, version); } setupProgram(); diff --git a/src/common.mts b/src/common.mts index ee65e94..a567272 100644 --- a/src/common.mts +++ b/src/common.mts @@ -1,11 +1,11 @@ import type { PathLike } from "node:fs"; import { stat } from "node:fs/promises"; import type { - JSDoc, - MethodDeclaration, - ParameterDeclaration, - SourceFile, - Type, + JSDoc, + MethodDeclaration, + ParameterDeclaration, + SourceFile, + Type, } from "ts-morph"; import ts from "typescript"; @@ -14,40 +14,40 @@ export const TError = ts.factory.createIdentifier("TError"); export const TContext = ts.factory.createIdentifier("TContext"); export const queryKeyGenericType = - ts.factory.createTypeReferenceNode("TQueryKey"); + ts.factory.createTypeReferenceNode("TQueryKey"); export const queryKeyConstraint = ts.factory.createTypeReferenceNode("Array", [ - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), ]); export const capitalizeFirstLetter = (str: string) => { - return str.charAt(0).toUpperCase() + str.slice(1); + return str.charAt(0).toUpperCase() + str.slice(1); }; export const lowercaseFirstLetter = (str: string) => { - return str.charAt(0).toLowerCase() + str.slice(1); + return str.charAt(0).toLowerCase() + str.slice(1); }; export const getNameFromMethod = (method: MethodDeclaration) => { - return method.getName(); + return method.getName(); }; export type MethodDescription = { - className: string; - node: SourceFile; - method: MethodDeclaration; - methodBlock: ts.Block; - httpMethodName: string; - jsDoc: JSDoc[]; - isDeprecated: boolean; + className: string; + node: SourceFile; + method: MethodDeclaration; + methodBlock: ts.Block; + httpMethodName: string; + jsDoc: JSDoc[]; + isDeprecated: boolean; }; export async function exists(f: PathLike) { - try { - await stat(f); - return true; - } catch { - return false; - } + try { + await stat(f); + return true; + } catch { + return false; + } } const Common = "Common"; @@ -56,10 +56,10 @@ const Common = "Common"; * Build a common type name by prepending the Common namespace. */ export function BuildCommonTypeName(name: string | ts.Identifier) { - if (typeof name === "string") { - return ts.factory.createIdentifier(`${Common}.${name}`); - } - return ts.factory.createIdentifier(`${Common}.${name.text}`); + if (typeof name === "string") { + return ts.factory.createIdentifier(`${Common}.${name}`); + } + return ts.factory.createIdentifier(`${Common}.${name.text}`); } /** @@ -69,24 +69,24 @@ export function BuildCommonTypeName(name: string | ts.Identifier) { * @returns The parsed number or NaN if the value is not a valid number. */ export function safeParseNumber(value: unknown): number { - const parsed = Number(value); - if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { - return parsed; - } - return Number.NaN; + const parsed = Number(value); + if (!Number.isNaN(parsed) && Number.isFinite(parsed)) { + return parsed; + } + return Number.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() as Type, - })); - return paramNodes; + 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() as Type, + })); + return paramNodes; } diff --git a/src/createExports.mts b/src/createExports.mts index 6600284..3dfdf23 100644 --- a/src/createExports.mts +++ b/src/createExports.mts @@ -3,49 +3,49 @@ import { createUseQuery } from "./createUseQuery.mjs"; import type { Service } from "./service.mjs"; export const createExports = (service: Service) => { - const { klasses } = service; - const methods = klasses.flatMap((k) => k.methods); - - const allGet = methods.filter((m) => m.httpMethodName === "'GET'"); - const allPost = methods.filter((m) => m.httpMethodName === "'POST'"); - - const allQueries = allGet.map((m) => createUseQuery(m)); - const allMutations = allPost.map((m) => createUseMutation(m)); - - const commonInQueries = allQueries.flatMap( - ({ apiResponse, returnType, key }) => [apiResponse, returnType, key], - ); - const commonInMutations = allMutations.flatMap(({ mutationResult }) => [ - mutationResult, - ]); - - const allCommon = [...commonInQueries, ...commonInMutations]; - - const mainQueries = allQueries.flatMap(({ queryHook }) => [queryHook]); - const mainMutations = allMutations.flatMap(({ mutationHook }) => [ - mutationHook, - ]); - - const mainExports = [...mainQueries, ...mainMutations]; - - const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [ - suspenseQueryHook, - ]); - - const suspenseExports = [...suspenseQueries]; - - return { - /** - * Common types and variables between queries (regular and suspense) and mutations - */ - allCommon, - /** - * Main exports are the hooks that are used in the components - */ - mainExports, - /** - * Suspense exports are the hooks that are used in the suspense components - */ - suspenseExports, - }; + const { klasses } = service; + const methods = klasses.flatMap((k) => k.methods); + + const allGet = methods.filter((m) => m.httpMethodName === "'GET'"); + const allPost = methods.filter((m) => m.httpMethodName === "'POST'"); + + const allQueries = allGet.map((m) => createUseQuery(m)); + const allMutations = allPost.map((m) => createUseMutation(m)); + + const commonInQueries = allQueries.flatMap( + ({ apiResponse, returnType, key }) => [apiResponse, returnType, key], + ); + const commonInMutations = allMutations.flatMap(({ mutationResult }) => [ + mutationResult, + ]); + + const allCommon = [...commonInQueries, ...commonInMutations]; + + const mainQueries = allQueries.flatMap(({ queryHook }) => [queryHook]); + const mainMutations = allMutations.flatMap(({ mutationHook }) => [ + mutationHook, + ]); + + const mainExports = [...mainQueries, ...mainMutations]; + + const suspenseQueries = allQueries.flatMap(({ suspenseQueryHook }) => [ + suspenseQueryHook, + ]); + + const suspenseExports = [...suspenseQueries]; + + return { + /** + * Common types and variables between queries (regular and suspense) and mutations + */ + allCommon, + /** + * Main exports are the hooks that are used in the components + */ + mainExports, + /** + * Suspense exports are the hooks that are used in the suspense components + */ + suspenseExports, + }; }; diff --git a/src/createImports.mts b/src/createImports.mts index 0f659c2..7de85d3 100644 --- a/src/createImports.mts +++ b/src/createImports.mts @@ -5,143 +5,143 @@ import ts from "typescript"; const { join } = posix; export const createImports = ({ - serviceEndName, - project, + serviceEndName, + project, }: { - serviceEndName: string; - project: Project; + serviceEndName: string; + project: Project; }) => { - const modelsFile = project - .getSourceFiles() - .find((sourceFile) => sourceFile.getFilePath().includes("models.ts")); + const modelsFile = project + .getSourceFiles() + .find((sourceFile) => sourceFile.getFilePath().includes("models.ts")); - const serviceFile = project - .getSourceFiles() - .find((sourceFile) => sourceFile.getFilePath().includes("services.ts")); + const serviceFile = project + .getSourceFiles() + .find((sourceFile) => sourceFile.getFilePath().includes("services.ts")); - if (!modelsFile) { - console.warn(` + if (!modelsFile) { + console.warn(` ⚠️ WARNING: No models file found. This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`); - } + } - if (!serviceFile) { - throw new Error("No service file found"); - } + if (!serviceFile) { + throw new Error("No service file found"); + } - const modelNames = modelsFile - ? Array.from(modelsFile.getExportedDeclarations().keys()) - : []; + const modelNames = modelsFile + ? Array.from(modelsFile.getExportedDeclarations().keys()) + : []; - const serviceExports = Array.from( - serviceFile.getExportedDeclarations().keys(), - ); + const serviceExports = Array.from( + serviceFile.getExportedDeclarations().keys(), + ); - const serviceNames = serviceExports.filter((name) => - name.endsWith(serviceEndName), - ); + const serviceNames = serviceExports.filter((name) => + name.endsWith(serviceEndName), + ); - const serviceNamesData = serviceExports.filter((name) => - name.endsWith("Data"), - ); + const serviceNamesData = serviceExports.filter((name) => + name.endsWith("Data"), + ); - const imports = [ - ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("useQuery"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("useSuspenseQuery"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("useMutation"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("UseQueryResult"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("UseQueryOptions"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("UseMutationOptions"), - ), - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier("UseMutationResult"), - ), - ]), - ), - ts.factory.createStringLiteral("@tanstack/react-query"), - undefined, - ), - ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - // import all class names from service file - ...serviceNames.map((serviceName) => - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(serviceName), - ), - ), - // import all data objects from service file - ...serviceNamesData.map((dataName) => - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(dataName), - ), - ), - ]), - ), - ts.factory.createStringLiteral(join("../requests")), - undefined, - ), - ]; - if (modelsFile) { - // import all the models by name - imports.push( - ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - undefined, - ts.factory.createNamedImports([ - ...modelNames.map((modelName) => - ts.factory.createImportSpecifier( - false, - undefined, - ts.factory.createIdentifier(modelName), - ), - ), - ]), - ), - ts.factory.createStringLiteral(join("../requests/models")), - undefined, - ), - ); - } - return imports; + const imports = [ + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("useQuery"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("useSuspenseQuery"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("useMutation"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("UseQueryResult"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("UseQueryOptions"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("UseMutationOptions"), + ), + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier("UseMutationResult"), + ), + ]), + ), + ts.factory.createStringLiteral("@tanstack/react-query"), + undefined, + ), + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + // import all class names from service file + ...serviceNames.map((serviceName) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(serviceName), + ), + ), + // import all data objects from service file + ...serviceNamesData.map((dataName) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(dataName), + ), + ), + ]), + ), + ts.factory.createStringLiteral(join("../requests")), + undefined, + ), + ]; + if (modelsFile) { + // import all the models by name + imports.push( + ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + undefined, + ts.factory.createNamedImports([ + ...modelNames.map((modelName) => + ts.factory.createImportSpecifier( + false, + undefined, + ts.factory.createIdentifier(modelName), + ), + ), + ]), + ), + ts.factory.createStringLiteral(join("../requests/models")), + undefined, + ), + ); + } + return imports; }; diff --git a/src/createSource.mts b/src/createSource.mts index 35ce2cc..a00ac2c 100644 --- a/src/createSource.mts +++ b/src/createSource.mts @@ -6,172 +6,172 @@ import { createImports } from "./createImports.mjs"; import { getServices } from "./service.mjs"; const createSourceFile = async (outputPath: string, serviceEndName: string) => { - 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 = createImports({ - serviceEndName, - project, - }); - - const exports = createExports(service); - - const commonSource = ts.factory.createSourceFile( - [...imports, ...exports.allCommon], - ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), - ts.NodeFlags.None, - ); - - const commonImport = ts.factory.createImportDeclaration( - undefined, - ts.factory.createImportClause( - false, - ts.factory.createIdentifier("* as Common"), - undefined, - ), - ts.factory.createStringLiteral("./common"), - undefined, - ); - - const commonExport = ts.factory.createExportDeclaration( - undefined, - false, - undefined, - ts.factory.createStringLiteral("./common"), - undefined, - ); - - const queriesExport = ts.factory.createExportDeclaration( - undefined, - false, - undefined, - ts.factory.createStringLiteral("./queries"), - undefined, - ); - - const mainSource = ts.factory.createSourceFile( - [commonImport, ...imports, ...exports.mainExports], - ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), - ts.NodeFlags.None, - ); - - const suspenseSource = ts.factory.createSourceFile( - [commonImport, ...imports, ...exports.suspenseExports], - ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), - ts.NodeFlags.None, - ); - - const indexSource = ts.factory.createSourceFile( - [commonExport, queriesExport], - ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), - ts.NodeFlags.None, - ); - - return { - commonSource, - mainSource, - suspenseSource, - indexSource, - }; + 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 = createImports({ + serviceEndName, + project, + }); + + const exports = createExports(service); + + const commonSource = ts.factory.createSourceFile( + [...imports, ...exports.allCommon], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ); + + const commonImport = ts.factory.createImportDeclaration( + undefined, + ts.factory.createImportClause( + false, + ts.factory.createIdentifier("* as Common"), + undefined, + ), + ts.factory.createStringLiteral("./common"), + undefined, + ); + + const commonExport = ts.factory.createExportDeclaration( + undefined, + false, + undefined, + ts.factory.createStringLiteral("./common"), + undefined, + ); + + const queriesExport = ts.factory.createExportDeclaration( + undefined, + false, + undefined, + ts.factory.createStringLiteral("./queries"), + undefined, + ); + + const mainSource = ts.factory.createSourceFile( + [commonImport, ...imports, ...exports.mainExports], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ); + + const suspenseSource = ts.factory.createSourceFile( + [commonImport, ...imports, ...exports.suspenseExports], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ); + + const indexSource = ts.factory.createSourceFile( + [commonExport, queriesExport], + ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), + ts.NodeFlags.None, + ); + + return { + commonSource, + mainSource, + suspenseSource, + indexSource, + }; }; export const createSource = async ({ - outputPath, - version, - serviceEndName, + outputPath, + version, + serviceEndName, }: { - outputPath: string; - version: string; - serviceEndName: string; + outputPath: string; + version: string; + serviceEndName: string; }) => { - const queriesFile = ts.createSourceFile( - "queries.ts", - "", - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS, - ); - const commonFile = ts.createSourceFile( - "common.ts", - "", - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS, - ); - const suspenseFile = ts.createSourceFile( - "suspense.ts", - "", - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS, - ); - - const indexFile = ts.createSourceFile( - "index.ts", - "", - ts.ScriptTarget.Latest, - false, - ts.ScriptKind.TS, - ); - - const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed, - removeComments: false, - }); - - const { commonSource, mainSource, suspenseSource, indexSource } = - await createSourceFile(outputPath, serviceEndName); - - const commonResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( - ts.EmitHint.Unspecified, - commonSource, - commonFile, - )}`; - - const mainResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( - ts.EmitHint.Unspecified, - mainSource, - queriesFile, - )}`; - - const suspenseResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( - ts.EmitHint.Unspecified, - suspenseSource, - suspenseFile, - )}`; - - const indexResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( - ts.EmitHint.Unspecified, - indexSource, - indexFile, - )}`; - - return [ - { - name: "index.ts", - content: indexResult, - }, - { - name: "common.ts", - content: commonResult, - }, - { - name: "queries.ts", - content: mainResult, - }, - { - name: "suspense.ts", - content: suspenseResult, - }, - ]; + const queriesFile = ts.createSourceFile( + "queries.ts", + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS, + ); + const commonFile = ts.createSourceFile( + "common.ts", + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS, + ); + const suspenseFile = ts.createSourceFile( + "suspense.ts", + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS, + ); + + const indexFile = ts.createSourceFile( + "index.ts", + "", + ts.ScriptTarget.Latest, + false, + ts.ScriptKind.TS, + ); + + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + removeComments: false, + }); + + const { commonSource, mainSource, suspenseSource, indexSource } = + await createSourceFile(outputPath, serviceEndName); + + const commonResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( + ts.EmitHint.Unspecified, + commonSource, + commonFile, + )}`; + + const mainResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( + ts.EmitHint.Unspecified, + mainSource, + queriesFile, + )}`; + + const suspenseResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( + ts.EmitHint.Unspecified, + suspenseSource, + suspenseFile, + )}`; + + const indexResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n${printer.printNode( + ts.EmitHint.Unspecified, + indexSource, + indexFile, + )}`; + + return [ + { + name: "index.ts", + content: indexResult, + }, + { + name: "common.ts", + content: commonResult, + }, + { + name: "queries.ts", + content: mainResult, + }, + { + name: "suspense.ts", + content: suspenseResult, + }, + ]; }; diff --git a/src/createUseMutation.mts b/src/createUseMutation.mts index 8c6929f..90308a7 100644 --- a/src/createUseMutation.mts +++ b/src/createUseMutation.mts @@ -1,13 +1,13 @@ import ts from "typescript"; import { - BuildCommonTypeName, - type MethodDescription, - TContext, - TData, - TError, - capitalizeFirstLetter, - extractPropertiesFromObjectParam, - getNameFromMethod, + BuildCommonTypeName, + type MethodDescription, + TContext, + TData, + TError, + capitalizeFirstLetter, + extractPropertiesFromObjectParam, + getNameFromMethod, } from "./common.mjs"; import { addJSDocToNode } from "./util.mjs"; @@ -15,252 +15,252 @@ import { addJSDocToNode } from "./util.mjs"; * Awaited> */ function generateAwaitedReturnType({ - className, - methodName, + className, + methodName, }: { - className: string; - methodName: string; + className: string; + methodName: string; }) { - return ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Awaited"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("ReturnType"), - [ - ts.factory.createTypeQueryNode( - ts.factory.createQualifiedName( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName), - ), - undefined, - ), - ], - ), - ], - ); + return ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Awaited"), + [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("ReturnType"), + [ + ts.factory.createTypeQueryNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName), + ), + undefined, + ), + ], + ), + ], + ); } export const createUseMutation = ({ - node, - className, - method, - jsDoc = [], - isDeprecated = false, + node, + className, + method, + jsDoc = [], + isDeprecated = false, }: MethodDescription) => { - const methodName = getNameFromMethod(method); - const awaitedResponseDataType = generateAwaitedReturnType({ - className, - methodName, - }); + const methodName = getNameFromMethod(method); + const awaitedResponseDataType = generateAwaitedReturnType({ + className, + methodName, + }); - const mutationResult = ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier( - `${className}${capitalizeFirstLetter(methodName)}MutationResult`, - ), - undefined, - awaitedResponseDataType, - ); + const mutationResult = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier( + `${className}${capitalizeFirstLetter(methodName)}MutationResult`, + ), + undefined, + awaitedResponseDataType, + ); - const responseDataType = ts.factory.createTypeParameterDeclaration( - undefined, - TData, - undefined, - ts.factory.createTypeReferenceNode( - BuildCommonTypeName(mutationResult.name), - ), - ); + const responseDataType = ts.factory.createTypeParameterDeclaration( + undefined, + TData, + undefined, + ts.factory.createTypeReferenceNode( + BuildCommonTypeName(mutationResult.name), + ), + ); - const methodParameters = - method.getParameters().length !== 0 - ? ts.factory.createTypeLiteralNode( - method - .getParameters() - .flatMap((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), - ), - ), - ); - }), - // 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); + const methodParameters = + method.getParameters().length !== 0 + ? ts.factory.createTypeLiteralNode( + method + .getParameters() + .flatMap((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), + ), + ), + ); + }), + // 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); - const exportHook = ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier( - `use${className}${capitalizeFirstLetter(methodName)}`, - ), - undefined, - undefined, - ts.factory.createArrowFunction( - undefined, - ts.factory.createNodeArray([ - responseDataType, - ts.factory.createTypeParameterDeclaration( - undefined, - TError, - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ), - ts.factory.createTypeParameterDeclaration( - undefined, - TContext, - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ), - ]), - [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("options"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseMutationOptions"), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - methodParameters, - ts.factory.createTypeReferenceNode(TContext), - ], - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("mutationFn"), - ), - ], - ), - undefined, - ), - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createIdentifier("useMutation"), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - methodParameters, - ts.factory.createTypeReferenceNode(TContext), - ], - [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("mutationFn"), - ts.factory.createArrowFunction( - undefined, - undefined, - method.getParameters().length !== 0 - ? [ - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createObjectBindingPattern( - method.getParameters().flatMap((param) => { - const paramNodes = - extractPropertiesFromObjectParam(param); - return paramNodes.map((refParam) => - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier( - refParam.name, - ), - undefined, - ), - ); - }), - ), - undefined, - undefined, - undefined, - ), - ] - : [], - undefined, - ts.factory.createToken( - ts.SyntaxKind.EqualsGreaterThanToken, - ), - ts.factory.createAsExpression( - ts.factory.createAsExpression( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName), - ), - undefined, - method.getParameters().length !== 0 - ? [ - ts.factory.createObjectLiteralExpression( - method.getParameters().flatMap((params) => { - const paramNodes = - extractPropertiesFromObjectParam( - params, - ); - return paramNodes.map((refParam) => - ts.factory.createShorthandPropertyAssignment( - refParam.name, - ), - ); - }), - ), - ] - : [], - ), - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword, - ), - ), + const exportHook = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier( + `use${className}${capitalizeFirstLetter(methodName)}`, + ), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + ts.factory.createNodeArray([ + responseDataType, + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ), + ts.factory.createTypeParameterDeclaration( + undefined, + TContext, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ), + ]), + [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("options"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Omit"), + [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseMutationOptions"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + methodParameters, + ts.factory.createTypeReferenceNode(TContext), + ], + ), + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("mutationFn"), + ), + ], + ), + undefined, + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createIdentifier("useMutation"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + methodParameters, + ts.factory.createTypeReferenceNode(TContext), + ], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("mutationFn"), + ts.factory.createArrowFunction( + undefined, + undefined, + method.getParameters().length !== 0 + ? [ + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createObjectBindingPattern( + method.getParameters().flatMap((param) => { + const paramNodes = + extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier( + refParam.name, + ), + undefined, + ), + ); + }), + ), + undefined, + undefined, + undefined, + ), + ] + : [], + undefined, + ts.factory.createToken( + ts.SyntaxKind.EqualsGreaterThanToken, + ), + ts.factory.createAsExpression( + ts.factory.createAsExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName), + ), + undefined, + method.getParameters().length !== 0 + ? [ + ts.factory.createObjectLiteralExpression( + method.getParameters().flatMap((params) => { + const paramNodes = + extractPropertiesFromObjectParam( + params, + ); + return paramNodes.map((refParam) => + ts.factory.createShorthandPropertyAssignment( + refParam.name, + ), + ); + }), + ), + ] + : [], + ), + ts.factory.createKeywordTypeNode( + ts.SyntaxKind.UnknownKeyword, + ), + ), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Promise"), - [ts.factory.createTypeReferenceNode(TData)], - ), - ), - ), - ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier("options"), - ), - ]), - ], - ), - ), - ), - ], - ts.NodeFlags.Const, - ), - ); + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Promise"), + [ts.factory.createTypeReferenceNode(TData)], + ), + ), + ), + ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier("options"), + ), + ]), + ], + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ); - const hookWithJsDoc = addJSDocToNode(exportHook, node, isDeprecated, jsDoc); + const hookWithJsDoc = addJSDocToNode(exportHook, node, isDeprecated, jsDoc); - return { - mutationResult, - mutationHook: hookWithJsDoc, - }; + return { + mutationResult, + mutationHook: hookWithJsDoc, + }; }; diff --git a/src/createUseQuery.mts b/src/createUseQuery.mts index 45be6d9..7c98da2 100644 --- a/src/createUseQuery.mts +++ b/src/createUseQuery.mts @@ -1,75 +1,75 @@ import type { MethodDeclaration } from "ts-morph"; import ts from "typescript"; import { - BuildCommonTypeName, - TData, - TError, - capitalizeFirstLetter, - extractPropertiesFromObjectParam, - getNameFromMethod, - queryKeyConstraint, - queryKeyGenericType, + BuildCommonTypeName, + TData, + TError, + capitalizeFirstLetter, + extractPropertiesFromObjectParam, + getNameFromMethod, + queryKeyConstraint, + queryKeyGenericType, } from "./common.mjs"; import type { MethodDescription } from "./common.mjs"; import { addJSDocToNode } from "./util.mjs"; export const createApiResponseType = ({ - className, - methodName, + className, + methodName, }: { - className: string; - methodName: string; + className: string; + methodName: string; }) => { - /** Awaited> */ - const awaitedResponseDataType = ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Awaited"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("ReturnType"), - [ - ts.factory.createTypeQueryNode( - ts.factory.createQualifiedName( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName), - ), - undefined, - ), - ], - ), - ], - ); - /** DefaultResponseDataType - * export type MyClassMethodDefaultResponse = Awaited> - */ - const apiResponse = ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier( - `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( - methodName, - )}DefaultResponse`, - ), - undefined, - awaitedResponseDataType, - ); + /** Awaited> */ + const awaitedResponseDataType = ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Awaited"), + [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("ReturnType"), + [ + ts.factory.createTypeQueryNode( + ts.factory.createQualifiedName( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName), + ), + undefined, + ), + ], + ), + ], + ); + /** DefaultResponseDataType + * export type MyClassMethodDefaultResponse = Awaited> + */ + const apiResponse = ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier( + `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( + methodName, + )}DefaultResponse`, + ), + undefined, + awaitedResponseDataType, + ); - const responseDataType = ts.factory.createTypeParameterDeclaration( - undefined, - TData.text, - undefined, - ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name)), - ); + const responseDataType = ts.factory.createTypeParameterDeclaration( + undefined, + TData.text, + undefined, + ts.factory.createTypeReferenceNode(BuildCommonTypeName(apiResponse.name)), + ); - return { - /** DefaultResponseDataType - * export type MyClassMethodDefaultResponse = Awaited> - */ - apiResponse, - /** - * will be the name of the type of the response type of the method - * MyClassMethodDefaultResponse - */ - responseDataType, - }; + return { + /** DefaultResponseDataType + * export type MyClassMethodDefaultResponse = Awaited> + */ + apiResponse, + /** + * will be the name of the type of the response type of the method + * MyClassMethodDefaultResponse + */ + responseDataType, + }; }; /** @@ -78,60 +78,60 @@ export const createApiResponseType = ({ * we are already importing all the types from that file. */ function getShortType(type: string) { - return type.replaceAll(/import\("[a-zA-Z\/\.-]*"\)\./g, ""); + return type.replaceAll(/import\("[a-zA-Z\/\.-]*"\)\./g, ""); } export function getRequestParamFromMethod(method: MethodDeclaration) { - if (!method.getParameters().length) { - return null; - } + if (!method.getParameters().length) { + return null; + } - const params = method.getParameters().flatMap((param) => { - const paramNodes = extractPropertiesFromObjectParam(param); - return paramNodes.map((refParam) => ({ - name: refParam.name, - typeName: getShortType(refParam.type.getText()), - optional: refParam.optional, - })); - }); + const params = method.getParameters().flatMap((param) => { + const paramNodes = extractPropertiesFromObjectParam(param); + return paramNodes.map((refParam) => ({ + name: refParam.name, + typeName: getShortType(refParam.type.getText()), + optional: refParam.optional, + })); + }); - const areAllPropertiesOptional = params.every((param) => param.optional); + const areAllPropertiesOptional = params.every((param) => param.optional); - return ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createObjectBindingPattern( - params.map((refParam) => - ts.factory.createBindingElement( - undefined, - undefined, - ts.factory.createIdentifier(refParam.name), - undefined, - ), - ), - ), - undefined, - ts.factory.createTypeLiteralNode( - params.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.typeName), - ); - }), - ), - // if all params are optional, we create an empty object literal - // so the hook can be called without any parameters - areAllPropertiesOptional - ? ts.factory.createObjectLiteralExpression() - : undefined, - ); + return ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createObjectBindingPattern( + params.map((refParam) => + ts.factory.createBindingElement( + undefined, + undefined, + ts.factory.createIdentifier(refParam.name), + undefined, + ), + ), + ), + undefined, + ts.factory.createTypeLiteralNode( + params.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.typeName), + ); + }), + ), + // if all params are optional, we create an empty object literal + // so the hook can be called without any parameters + areAllPropertiesOptional + ? ts.factory.createObjectLiteralExpression() + : undefined, + ); } /** @@ -139,96 +139,96 @@ export function getRequestParamFromMethod(method: MethodDeclaration) { * export const classNameMethodNameQueryResult = UseQueryResult; */ export function createReturnTypeExport({ - className, - methodName, - defaultApiResponse, + className, + methodName, + defaultApiResponse, }: { - className: string; - methodName: string; - defaultApiResponse: ts.TypeAliasDeclaration; + className: string; + methodName: string; + defaultApiResponse: ts.TypeAliasDeclaration; }) { - return ts.factory.createTypeAliasDeclaration( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createIdentifier( - `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( - methodName, - )}QueryResult`, - ), - [ - ts.factory.createTypeParameterDeclaration( - undefined, - TData, - undefined, - ts.factory.createTypeReferenceNode(defaultApiResponse.name), - ), - ts.factory.createTypeParameterDeclaration( - undefined, - TError, - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ), - ], - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryResult"), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ], - ), - ); + return ts.factory.createTypeAliasDeclaration( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createIdentifier( + `${capitalizeFirstLetter(className)}${capitalizeFirstLetter( + methodName, + )}QueryResult`, + ), + [ + ts.factory.createTypeParameterDeclaration( + undefined, + TData, + undefined, + ts.factory.createTypeReferenceNode(defaultApiResponse.name), + ), + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ), + ], + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryResult"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + ), + ); } /** * QueryKey */ export function createQueryKeyExport({ - className, - methodName, - queryKey, + className, + methodName, + queryKey, }: { - className: string; - methodName: string; - queryKey: string; + className: string; + methodName: string; + queryKey: string; }) { - return ts.factory.createVariableStatement( - [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(queryKey), - undefined, - undefined, - ts.factory.createStringLiteral( - `${className}${capitalizeFirstLetter(methodName)}`, - ), - ), - ], - ts.NodeFlags.Const, - ), - ); + return ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(queryKey), + undefined, + undefined, + ts.factory.createStringLiteral( + `${className}${capitalizeFirstLetter(methodName)}`, + ), + ), + ], + ts.NodeFlags.Const, + ), + ); } function hookNameFromMethod({ - method, - className, + method, + className, }: { - method: MethodDeclaration; - className: string; + method: MethodDeclaration; + className: string; }) { - const methodName = getNameFromMethod(method); - return `use${className}${capitalizeFirstLetter(methodName)}`; + const methodName = getNameFromMethod(method); + return `use${className}${capitalizeFirstLetter(methodName)}`; } function createQueryKeyFromMethod({ - method, - className, + method, + className, }: { - method: MethodDeclaration; - className: string; + method: MethodDeclaration; + className: string; }) { - const customHookName = hookNameFromMethod({ method, className }); - const queryKey = `${customHookName}Key`; - return queryKey; + const customHookName = hookNameFromMethod({ method, className }); + const queryKey = `${customHookName}Key`; + return queryKey; } /** @@ -237,252 +237,252 @@ function createQueryKeyFromMethod({ * @param suffix The suffix to append to the hook name */ function createQueryHook({ - queryString, - suffix, - responseDataType, - requestParams, - method, - className, + queryString, + suffix, + responseDataType, + requestParams, + method, + className, }: { - queryString: "useSuspenseQuery" | "useQuery"; - suffix: string; - responseDataType: ts.TypeParameterDeclaration; - requestParams: ts.ParameterDeclaration[]; - method: MethodDeclaration; - className: string; + queryString: "useSuspenseQuery" | "useQuery"; + suffix: string; + responseDataType: ts.TypeParameterDeclaration; + requestParams: ts.ParameterDeclaration[]; + method: MethodDeclaration; + className: string; }) { - const methodName = getNameFromMethod(method); - const customHookName = hookNameFromMethod({ method, className }); - const queryKey = createQueryKeyFromMethod({ method, 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)], - ts.factory.createVariableDeclarationList( - [ - ts.factory.createVariableDeclaration( - ts.factory.createIdentifier(`${customHookName}${suffix}`), - undefined, - undefined, - ts.factory.createArrowFunction( - undefined, - ts.factory.createNodeArray([ - responseDataType, - ts.factory.createTypeParameterDeclaration( - undefined, - TError, - undefined, - ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), - ), - ts.factory.createTypeParameterDeclaration( - undefined, - "TQueryKey", - queryKeyConstraint, - ts.factory.createArrayTypeNode( - ts.factory.createKeywordTypeNode( - ts.SyntaxKind.UnknownKeyword, - ), - ), - ), - ]), - [ - ...requestParams, - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - queryKeyGenericType, - ), - ts.factory.createParameterDeclaration( - undefined, - undefined, - ts.factory.createIdentifier("options"), - ts.factory.createToken(ts.SyntaxKind.QuestionToken), - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("Omit"), - [ - ts.factory.createTypeReferenceNode( - ts.factory.createIdentifier("UseQueryOptions"), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ], - ), - ts.factory.createUnionTypeNode([ - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryKey"), - ), - ts.factory.createLiteralTypeNode( - ts.factory.createStringLiteral("queryFn"), - ), - ]), - ], - ), - ), - ], - undefined, - ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - ts.factory.createCallExpression( - ts.factory.createIdentifier(queryString), - [ - ts.factory.createTypeReferenceNode(TData), - ts.factory.createTypeReferenceNode(TError), - ], - [ - ts.factory.createObjectLiteralExpression([ - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("queryKey"), - ts.factory.createArrayLiteralExpression( - [ - BuildCommonTypeName(queryKey), - ts.factory.createSpreadElement( - ts.factory.createParenthesizedExpression( - ts.factory.createBinaryExpression( - ts.factory.createIdentifier("queryKey"), - ts.factory.createToken( - ts.SyntaxKind.QuestionQuestionToken, - ), - method.getParameters().length - ? ts.factory.createArrayLiteralExpression([ - ts.factory.createObjectLiteralExpression( - method - .getParameters() - .flatMap((param) => - extractPropertiesFromObjectParam( - param, - ).map((p) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier( - p.name, - ), - ), - ), - ), - ), - ]) - : ts.factory.createArrayLiteralExpression([]), - ), - ), - ), - ], - false, - ), - ), - ts.factory.createPropertyAssignment( - ts.factory.createIdentifier("queryFn"), - ts.factory.createArrowFunction( - undefined, - undefined, - [], - undefined, - ts.factory.createToken( - ts.SyntaxKind.EqualsGreaterThanToken, - ), - ts.factory.createAsExpression( - ts.factory.createCallExpression( - ts.factory.createPropertyAccessExpression( - ts.factory.createIdentifier(className), - ts.factory.createIdentifier(methodName), - ), - undefined, - method.getParameters().length - ? [ - ts.factory.createObjectLiteralExpression( - method - .getParameters() - .flatMap((param) => - extractPropertiesFromObjectParam( - param, - ).map((p) => - ts.factory.createShorthandPropertyAssignment( - ts.factory.createIdentifier(p.name), - ), - ), - ), - ), - ] - : undefined, - ), - ts.factory.createTypeReferenceNode(TData), - ), - ), - ), - ts.factory.createSpreadAssignment( - ts.factory.createIdentifier("options"), - ), - ]), - ], - ), - ), - ), - ], - ts.NodeFlags.Const, - ), - ); - return hookExport; + const hookExport = ts.factory.createVariableStatement( + [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], + ts.factory.createVariableDeclarationList( + [ + ts.factory.createVariableDeclaration( + ts.factory.createIdentifier(`${customHookName}${suffix}`), + undefined, + undefined, + ts.factory.createArrowFunction( + undefined, + ts.factory.createNodeArray([ + responseDataType, + ts.factory.createTypeParameterDeclaration( + undefined, + TError, + undefined, + ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), + ), + ts.factory.createTypeParameterDeclaration( + undefined, + "TQueryKey", + queryKeyConstraint, + ts.factory.createArrayTypeNode( + ts.factory.createKeywordTypeNode( + ts.SyntaxKind.UnknownKeyword, + ), + ), + ), + ]), + [ + ...requestParams, + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + queryKeyGenericType, + ), + ts.factory.createParameterDeclaration( + undefined, + undefined, + ts.factory.createIdentifier("options"), + ts.factory.createToken(ts.SyntaxKind.QuestionToken), + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("Omit"), + [ + ts.factory.createTypeReferenceNode( + ts.factory.createIdentifier("UseQueryOptions"), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + ), + ts.factory.createUnionTypeNode([ + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryKey"), + ), + ts.factory.createLiteralTypeNode( + ts.factory.createStringLiteral("queryFn"), + ), + ]), + ], + ), + ), + ], + undefined, + ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + ts.factory.createCallExpression( + ts.factory.createIdentifier(queryString), + [ + ts.factory.createTypeReferenceNode(TData), + ts.factory.createTypeReferenceNode(TError), + ], + [ + ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryKey"), + ts.factory.createArrayLiteralExpression( + [ + BuildCommonTypeName(queryKey), + ts.factory.createSpreadElement( + ts.factory.createParenthesizedExpression( + ts.factory.createBinaryExpression( + ts.factory.createIdentifier("queryKey"), + ts.factory.createToken( + ts.SyntaxKind.QuestionQuestionToken, + ), + method.getParameters().length + ? ts.factory.createArrayLiteralExpression([ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .flatMap((param) => + extractPropertiesFromObjectParam( + param, + ).map((p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier( + p.name, + ), + ), + ), + ), + ), + ]) + : ts.factory.createArrayLiteralExpression([]), + ), + ), + ), + ], + false, + ), + ), + ts.factory.createPropertyAssignment( + ts.factory.createIdentifier("queryFn"), + ts.factory.createArrowFunction( + undefined, + undefined, + [], + undefined, + ts.factory.createToken( + ts.SyntaxKind.EqualsGreaterThanToken, + ), + ts.factory.createAsExpression( + ts.factory.createCallExpression( + ts.factory.createPropertyAccessExpression( + ts.factory.createIdentifier(className), + ts.factory.createIdentifier(methodName), + ), + undefined, + method.getParameters().length + ? [ + ts.factory.createObjectLiteralExpression( + method + .getParameters() + .flatMap((param) => + extractPropertiesFromObjectParam( + param, + ).map((p) => + ts.factory.createShorthandPropertyAssignment( + ts.factory.createIdentifier(p.name), + ), + ), + ), + ), + ] + : undefined, + ), + ts.factory.createTypeReferenceNode(TData), + ), + ), + ), + ts.factory.createSpreadAssignment( + ts.factory.createIdentifier("options"), + ), + ]), + ], + ), + ), + ), + ], + ts.NodeFlags.Const, + ), + ); + return hookExport; } export const createUseQuery = ({ - node, - className, - method, - jsDoc = [], - isDeprecated: deprecated = false, + node, + className, + method, + jsDoc = [], + isDeprecated: deprecated = false, }: MethodDescription) => { - const methodName = getNameFromMethod(method); - const queryKey = createQueryKeyFromMethod({ method, className }); - const { apiResponse: defaultApiResponse, responseDataType } = - createApiResponseType({ - className, - methodName, - }); + const methodName = getNameFromMethod(method); + const queryKey = createQueryKeyFromMethod({ method, className }); + const { apiResponse: defaultApiResponse, responseDataType } = + createApiResponseType({ + className, + methodName, + }); - const requestParam = getRequestParamFromMethod(method); + const requestParam = getRequestParamFromMethod(method); - const requestParams = requestParam ? [requestParam] : []; + const requestParams = requestParam ? [requestParam] : []; - const queryHook = createQueryHook({ - queryString: "useQuery", - suffix: "", - responseDataType, - requestParams, - method, - className, - }); - const suspenseQueryHook = createQueryHook({ - queryString: "useSuspenseQuery", - suffix: "Suspense", - responseDataType, - requestParams, - method, - className, - }); + const queryHook = createQueryHook({ + queryString: "useQuery", + suffix: "", + responseDataType, + requestParams, + method, + className, + }); + const suspenseQueryHook = createQueryHook({ + queryString: "useSuspenseQuery", + suffix: "Suspense", + responseDataType, + requestParams, + method, + className, + }); - const hookWithJsDoc = addJSDocToNode(queryHook, node, deprecated, jsDoc); - const suspenseHookWithJsDoc = addJSDocToNode( - suspenseQueryHook, - node, - deprecated, - jsDoc, - ); + const hookWithJsDoc = addJSDocToNode(queryHook, node, deprecated, jsDoc); + const suspenseHookWithJsDoc = addJSDocToNode( + suspenseQueryHook, + node, + deprecated, + jsDoc, + ); - const returnTypeExport = createReturnTypeExport({ - className, - methodName, - defaultApiResponse, - }); + const returnTypeExport = createReturnTypeExport({ + className, + methodName, + defaultApiResponse, + }); - const queryKeyExport = createQueryKeyExport({ - className, - methodName, - queryKey, - }); + const queryKeyExport = createQueryKeyExport({ + className, + methodName, + queryKey, + }); - return { - apiResponse: defaultApiResponse, - returnType: returnTypeExport, - key: queryKeyExport, - queryHook: hookWithJsDoc, - suspenseQueryHook: suspenseHookWithJsDoc, - }; + return { + apiResponse: defaultApiResponse, + returnType: returnTypeExport, + key: queryKeyExport, + queryHook: hookWithJsDoc, + suspenseQueryHook: suspenseHookWithJsDoc, + }; }; diff --git a/src/generate.mts b/src/generate.mts index 483dda4..3e6bde4 100644 --- a/src/generate.mts +++ b/src/generate.mts @@ -6,50 +6,50 @@ import { createSource } from "./createSource.mjs"; import { print } from "./print.mjs"; export async function generate(options: UserConfig, version: string) { - const openApiOutputPath = path.join( - options.output ?? defaultOutputPath, - requestsOutputPath, - ); + const openApiOutputPath = path.join( + options.output ?? defaultOutputPath, + requestsOutputPath, + ); - // loop through properties on the options object - // if the property is a string of number then convert it to a number - // if the property is a string of boolean then convert it to a boolean - const formattedOptions = Object.entries(options).reduce( - (acc, [key, value]) => { - const typedKey = key as keyof UserConfig; - const typedValue = value as (typeof options)[keyof UserConfig]; - const parsedNumber = safeParseNumber(typedValue); - if (!Number.isNaN(parsedNumber)) { - (acc[typedKey] as unknown as number) = parsedNumber; - } else if (value === "true") { - (acc[typedKey] as unknown as boolean) = true; - } else if (value === "false") { - (acc[typedKey] as unknown as boolean) = false; - } else { - (acc[typedKey] as unknown as - | string - | boolean - | Record - | undefined) = typedValue; - } - return acc; - }, - options, - ); - const config: UserConfig = { - ...formattedOptions, - output: openApiOutputPath, - useOptions: true, - exportCore: true, - exportModels: true, - exportServices: true, - write: true, - }; - await createClient(config); - const source = await createSource({ - outputPath: openApiOutputPath, - version, - serviceEndName: "Service", // we are hard coding this because changing the service end name was depreciated in @hey-api/openapi-ts - }); - await print(source, formattedOptions); + // loop through properties on the options object + // if the property is a string of number then convert it to a number + // if the property is a string of boolean then convert it to a boolean + const formattedOptions = Object.entries(options).reduce( + (acc, [key, value]) => { + const typedKey = key as keyof UserConfig; + const typedValue = value as (typeof options)[keyof UserConfig]; + const parsedNumber = safeParseNumber(typedValue); + if (!Number.isNaN(parsedNumber)) { + (acc[typedKey] as unknown as number) = parsedNumber; + } else if (value === "true") { + (acc[typedKey] as unknown as boolean) = true; + } else if (value === "false") { + (acc[typedKey] as unknown as boolean) = false; + } else { + (acc[typedKey] as unknown as + | string + | boolean + | Record + | undefined) = typedValue; + } + return acc; + }, + options, + ); + const config: UserConfig = { + ...formattedOptions, + output: openApiOutputPath, + useOptions: true, + exportCore: true, + exportModels: true, + exportServices: true, + write: true, + }; + await createClient(config); + const source = await createSource({ + outputPath: openApiOutputPath, + version, + 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/print.mts b/src/print.mts index 2eb570b..3401a2f 100644 --- a/src/print.mts +++ b/src/print.mts @@ -5,36 +5,36 @@ import { exists } from "./common.mjs"; import { defaultOutputPath, queriesOutputPath } from "./constants.mjs"; async function printGeneratedTS( - result: { - name: string; - content: string; - }, - options: LimitedUserConfig, + result: { + name: string; + content: string; + }, + options: LimitedUserConfig, ) { - const dir = path.join(options.output ?? defaultOutputPath, queriesOutputPath); - const dirExists = await exists(dir); - if (!dirExists) { - await mkdir(dir, { recursive: true }); - } - await writeFile(path.join(dir, result.name), result.content); + const dir = path.join(options.output ?? defaultOutputPath, queriesOutputPath); + const dirExists = await exists(dir); + if (!dirExists) { + await mkdir(dir, { recursive: true }); + } + await writeFile(path.join(dir, result.name), result.content); } export async function print( - results: { - name: string; - content: string; - }[], - options: LimitedUserConfig, + results: { + name: string; + content: string; + }[], + options: LimitedUserConfig, ) { - const outputPath = options.output ?? defaultOutputPath; - const dirExists = await exists(outputPath); - if (!dirExists) { - await mkdir(outputPath); - } + const outputPath = options.output ?? defaultOutputPath; + const dirExists = await exists(outputPath); + if (!dirExists) { + await mkdir(outputPath); + } - const promises = results.map(async (result) => { - await printGeneratedTS(result, options); - }); + const promises = results.map(async (result) => { + await printGeneratedTS(result, options); + }); - await Promise.all(promises); + await Promise.all(promises); } diff --git a/src/service.mts b/src/service.mts index 3b3c842..5c6ba9e 100644 --- a/src/service.mts +++ b/src/service.mts @@ -3,124 +3,124 @@ import ts from "typescript"; import type { MethodDescription } from "./common.mjs"; export type Service = { - node: SourceFile; - klasses: Array<{ - className: string; - klass: ClassDeclaration; - methods: Array; - }>; + node: SourceFile; + klasses: Array<{ + className: string; + klass: ClassDeclaration; + methods: Array; + }>; }; 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, - klass, - methods: getMethodsFromService(node, klass), - })), - node, - } satisfies Service; + 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, + klass, + methods: getMethodsFromService(node, klass), + })), + node, + } satisfies Service; } function getClassesFromService(node: SourceFile) { - const klasses = node.getClasses(); - - if (!klasses.length) { - throw new Error("No classes found"); - } - - return klasses.map((klass) => { - const className = klass.getName(); - if (!className) { - throw new Error("Class name not found"); - } - return { - className, - klass, - }; - }); + const klasses = node.getClasses(); + + if (!klasses.length) { + throw new Error("No classes found"); + } + + return klasses.map((klass) => { + const className = klass.getName(); + if (!className) { + throw new Error("Class name not found"); + } + return { + className, + klass, + }; + }); } function getClassNameFromClassNode(klass: ClassDeclaration) { - const className = klass.getName(); + const className = klass.getName(); - if (!className) { - throw new Error("Class name not found"); - } - return className; + if (!className) { + throw new Error("Class name not found"); + } + return className; } 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.compilerNode - .getChildren(node.compilerNode) - .find((child) => child.kind === ts.SyntaxKind.Block); - - if (!methodBlockNode) { - throw new Error("Method block not found"); - } - const methodBlock = methodBlockNode as ts.Block; - const foundReturnStatement = methodBlock.statements.find( - (s) => s.kind === ts.SyntaxKind.ReturnStatement, - ); - if (!foundReturnStatement) { - throw new Error("Return statement not found"); - } - const returnStatement = foundReturnStatement as ts.ReturnStatement; - const foundCallExpression = returnStatement.expression; - if (!foundCallExpression) { - throw new Error("Call expression not found"); - } - const callExpression = foundCallExpression as ts.CallExpression; - const properties = ( - callExpression.arguments[1] as ts.ObjectLiteralExpression - ).properties as unknown as ts.PropertyAssignment[]; - const httpMethodName = properties - .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.compilerNode); - if (childItems.length) { - const allChildren = childItems.map(getAllChildren); - return [tsNode].concat(allChildren.flat()); - } - return [tsNode]; - }; - - const children = getAllChildren(method.compilerNode); - const jsDoc = method.getJsDocs().map((jsDoc) => jsDoc); - const isDeprecated = children.some( - (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag, - ); - - const className = getClassNameFromClassNode(klass); - - return { - className, - node, - method, - methodBlock, - httpMethodName, - jsDoc, - isDeprecated, - } satisfies MethodDescription; - }); + const methods = klass.getMethods(); + if (!methods.length) { + throw new Error("No methods found"); + } + return methods.map((method) => { + const methodBlockNode = method.compilerNode + .getChildren(node.compilerNode) + .find((child) => child.kind === ts.SyntaxKind.Block); + + if (!methodBlockNode) { + throw new Error("Method block not found"); + } + const methodBlock = methodBlockNode as ts.Block; + const foundReturnStatement = methodBlock.statements.find( + (s) => s.kind === ts.SyntaxKind.ReturnStatement, + ); + if (!foundReturnStatement) { + throw new Error("Return statement not found"); + } + const returnStatement = foundReturnStatement as ts.ReturnStatement; + const foundCallExpression = returnStatement.expression; + if (!foundCallExpression) { + throw new Error("Call expression not found"); + } + const callExpression = foundCallExpression as ts.CallExpression; + const properties = ( + callExpression.arguments[1] as ts.ObjectLiteralExpression + ).properties as unknown as ts.PropertyAssignment[]; + const httpMethodName = properties + .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.compilerNode); + if (childItems.length) { + const allChildren = childItems.map(getAllChildren); + return [tsNode].concat(allChildren.flat()); + } + return [tsNode]; + }; + + const children = getAllChildren(method.compilerNode); + const jsDoc = method.getJsDocs().map((jsDoc) => jsDoc); + const isDeprecated = children.some( + (c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag, + ); + + const className = getClassNameFromClassNode(klass); + + return { + className, + node, + method, + methodBlock, + httpMethodName, + jsDoc, + isDeprecated, + } satisfies MethodDescription; + }); } diff --git a/src/util.mts b/src/util.mts index 7e85137..f58ec1c 100644 --- a/src/util.mts +++ b/src/util.mts @@ -2,44 +2,44 @@ import type { JSDoc, SourceFile } from "ts-morph"; import ts from "typescript"; export function addJSDocToNode( - node: T, - sourceFile: SourceFile, - deprecated: boolean, - jsDoc: JSDoc[] = [], + node: T, + sourceFile: SourceFile, + deprecated: boolean, + jsDoc: JSDoc[] = [], ): T { - const deprecatedString = deprecated ? "@deprecated" : ""; + const deprecatedString = deprecated ? "@deprecated" : ""; - const jsDocString = [deprecatedString] - .concat( - jsDoc.map((comment) => { - if (typeof comment === "string") { - return comment; - } - if (Array.isArray(comment)) { - return comment.map((c) => c.getText(sourceFile)).join("\n"); - } - return ""; - }), - ) - // remove empty lines - .filter(Boolean) - // trim - .map((comment) => comment.trim()) - // add * to each line - .map((comment) => `* ${comment}`) - // join lines - .join("\n") - // replace new lines with \n * - .replace(/\n/g, "\n * "); + const jsDocString = [deprecatedString] + .concat( + jsDoc.map((comment) => { + if (typeof comment === "string") { + return comment; + } + if (Array.isArray(comment)) { + return comment.map((c) => c.getText(sourceFile)).join("\n"); + } + return ""; + }), + ) + // remove empty lines + .filter(Boolean) + // trim + .map((comment) => comment.trim()) + // add * to each line + .map((comment) => `* ${comment}`) + // join lines + .join("\n") + // replace new lines with \n * + .replace(/\n/g, "\n * "); - const nodeWithJSDoc = jsDocString - ? ts.addSyntheticLeadingComment( - node, - ts.SyntaxKind.MultiLineCommentTrivia, - `*\n ${jsDocString}\n `, - true, - ) - : node; + const nodeWithJSDoc = jsDocString + ? ts.addSyntheticLeadingComment( + node, + ts.SyntaxKind.MultiLineCommentTrivia, + `*\n ${jsDocString}\n `, + true, + ) + : node; - return nodeWithJSDoc; + return nodeWithJSDoc; } diff --git a/tsconfig.json b/tsconfig.json index dae60fc..864319e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,18 @@ { - "compilerOptions": { - "module": "NodeNext", - "moduleResolution": "NodeNext", - "allowSyntheticDefaultImports": true, - "strict": true, - "esModuleInterop": true, - "noImplicitAny": true, - "downlevelIteration": true, - "resolveJsonModule": true, - "outDir": "dist", - "lib": ["ESNext", "DOM"], - "target": "ESNext", - "baseUrl": ".", - "rootDir": "src" - }, - "include": ["src"] + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowSyntheticDefaultImports": true, + "strict": true, + "esModuleInterop": true, + "noImplicitAny": true, + "downlevelIteration": true, + "resolveJsonModule": true, + "outDir": "dist", + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "baseUrl": ".", + "rootDir": "src" + }, + "include": ["src"] }