@7nohe/openapi-react-query-codegen 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  > Node.js library that generates [React Query (also called TanStack Query)](https://tanstack.com/query) hooks based on an OpenAPI specification file.
4
4
 
5
+ [![npm version](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen.svg)](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen)
6
+
5
7
  ## Features
6
8
 
7
- - Supports generation of custom react hooks that use React Query's `useQuery` and `useMutation` hooks
8
- - Supports generation of query keys for query caching
9
- - Supports the option to use pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
9
+ - Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery` and `useMutation` hooks
10
+ - Generates query keys and functions for query caching
11
+ - Generates pure TypeScript clients generated by [@hey-api/openapi-ts](https://github.com/hey-api/openapi-ts)
10
12
 
11
- ## Install
13
+ ## Installation
12
14
 
13
15
  ```
14
16
  $ npm install -D @7nohe/openapi-react-query-codegen
@@ -45,37 +47,45 @@ Options:
45
47
  -o, --output <value> Output directory (default: "openapi")
46
48
  -c, --client <value> HTTP client to generate [fetch, xhr, node, axios, angular] (default: "fetch")
47
49
  --request <value> Path to custom request file
48
- --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
49
- --enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
50
- --base <value> Manually set base in OpenAPI config instead of inferring from server value
51
- --serviceResponse <value> Define shape of returned value from service calls ['body', 'generics', 'response']
50
+ --format <value> Process output folder with formatter? ['biome', 'prettier']
51
+ --lint <value> Process output folder with linter? ['eslint', 'biome']
52
52
  --operationId Use operation ID to generate operation names?
53
- --lint Process output folder with linter?
54
- --format Process output folder with formatter?
53
+ --serviceResponse <value> Define shape of returned value from service calls ['body', 'response'] (default: "body")
54
+ --base <value> Manually set base in OpenAPI config instead of inferring from server value
55
+ --enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript']
56
+ --useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
57
+ --debug Enable debug mode
58
+ --noSchemas Disable generating schemas for request and response objects
59
+ --schemaTypes <value> Define the type of schema generation ['form', 'json'] (default: "json")
55
60
  -h, --help display help for command
56
61
  ```
57
62
 
58
- ## Example Usage
63
+ ### Example Usage
59
64
 
60
- ### Command
65
+ #### Command
61
66
 
62
67
  ```
63
68
  $ openapi-rq -i ./petstore.yaml
64
69
  ```
65
70
 
66
- ### Output directory structure
71
+ #### Output directory structure
67
72
 
68
73
  ```
69
74
  - openapi
70
75
  - queries
71
- - index.ts <- main file that exports common types, variables, and hooks
76
+ - index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
72
77
  - common.ts <- common types
73
78
  - queries.ts <- generated query hooks
74
79
  - suspenses.ts <- generated suspense hooks
80
+ - prefetch.ts <- generated prefetch hooks learn more about prefetching in in link below
75
81
  - requests <- output code generated by @hey-api/openapi-ts
76
82
  ```
77
83
 
78
- ### In your app
84
+ - [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
85
+
86
+ #### In your app
87
+
88
+ ##### Using the generated hooks
79
89
 
80
90
  ```tsx
81
91
  // App.tsx
@@ -86,11 +96,7 @@ function App() {
86
96
  return (
87
97
  <div className="App">
88
98
  <h1>Pet List</h1>
89
- <ul>
90
- {data?.map((pet) => (
91
- <li key={pet.id}>{pet.name}</li>
92
- ))}
93
- </ul>
99
+ <ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
94
100
  </div>
95
101
  );
96
102
  }
@@ -98,7 +104,7 @@ function App() {
98
104
  export default App;
99
105
  ```
100
106
 
101
- You can also use pure TS clients.
107
+ ##### Using the generated typescript client
102
108
 
103
109
  ```tsx
104
110
  import { useQuery } from "@tanstack/react-query";
@@ -121,7 +127,7 @@ function App() {
121
127
  export default App;
122
128
  ```
123
129
 
124
- You can also use suspense hooks.
130
+ ##### Using Suspense Hooks
125
131
 
126
132
  ```tsx
127
133
  // App.tsx
@@ -129,13 +135,7 @@ import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
129
135
  function ChildComponent() {
130
136
  const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
131
137
 
132
- return (
133
- <ul>
134
- {data?.map((pet, index) => (
135
- <li key={pet.id}>{pet.name}</li>
136
- ))}
137
- </ul>
138
- );
138
+ return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
139
139
  }
140
140
 
141
141
  function ParentComponent() {
@@ -160,6 +160,34 @@ function App() {
160
160
  export default App;
161
161
  ```
162
162
 
163
+ ##### Runtime Configuration
164
+
165
+ You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
166
+
167
+ It's default location is `openapi/requests/core/OpenAPI.ts` and it is also exported from `openapi/index.ts`
168
+
169
+ Import the constant into your runtime and modify it before setting up the react app.
170
+
171
+ ```typescript
172
+ /** main.tsx */
173
+ import { OpenAPI as OpenAPIConfig } from './openapi/requests/core/OpenAPI';
174
+ ...
175
+ OpenAPIConfig.BASE = 'www.domain.com/api';
176
+ OpenAPIConfig.HEADERS = {
177
+ 'x-header-1': 'value-1',
178
+ 'x-header-2': 'value-2',
179
+ };
180
+ ...
181
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
182
+ <React.StrictMode>
183
+ <QueryClientProvider client={queryClient}>
184
+ <App />
185
+ </QueryClientProvider>
186
+ </React.StrictMode>
187
+ );
188
+
189
+ ```
190
+
163
191
  ## License
164
192
 
165
193
  MIT
package/dist/cli.mjs CHANGED
@@ -4,6 +4,7 @@ import { Command, Option } from "commander";
4
4
  import { readFile } from "fs/promises";
5
5
  import { dirname, join } from "path";
6
6
  import { fileURLToPath } from "node:url";
7
+ import { defaultOutputPath } from "./constants.mjs";
7
8
  const program = new Command();
8
9
  async function setupProgram() {
9
10
  const __filename = fileURLToPath(import.meta.url);
@@ -16,18 +17,23 @@ async function setupProgram() {
16
17
  .version(version)
17
18
  .description("Generate React Query code based on OpenAPI")
18
19
  .requiredOption("-i, --input <value>", "OpenAPI specification, can be a path, url or string content (required)")
19
- .option("-o, --output <value>", "Output directory", "openapi")
20
+ .option("-o, --output <value>", "Output directory", defaultOutputPath)
20
21
  .addOption(new Option("-c, --client <value>", "HTTP client to generate")
21
22
  .choices(["angular", "axios", "fetch", "node", "xhr"])
22
23
  .default("fetch"))
23
24
  .option("--request <value>", "Path to custom request file")
24
- .option("--format", "Process output folder with formatter?")
25
- .option("--lint", "Process output folder with linter?")
25
+ .addOption(new Option("--format <value>", "Process output folder with formatter?").choices(["biome", "prettier"]))
26
+ .addOption(new Option("--lint <value>", "Process output folder with linter?").choices(["biome", "eslint"]))
26
27
  .option("--operationId", "Use operation ID to generate operation names?")
27
- .addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls").choices(["body", "generics", "response"]))
28
+ .addOption(new Option("--serviceResponse <value>", "Define shape of returned value from service calls")
29
+ .choices(["body", "response"])
30
+ .default("body"))
28
31
  .option("--base <value>", "Manually set base in OpenAPI config instead of inferring from server value")
29
32
  .addOption(new Option("--enums <value>", "Generate JavaScript objects from enum definitions?").choices(["javascript", "typescript"]))
30
33
  .option("--useDateType", "Use Date type instead of string for date types for models, this will not convert the data to a Date object")
34
+ .option("--debug", "Run in debug mode?")
35
+ .option("--noSchemas", "Disable generating JSON schemas")
36
+ .addOption(new Option("--schemaType <value>", "Type of JSON schema [Default: 'json']").choices(["form", "json"]))
31
37
  .parse();
32
38
  const options = program.opts();
33
39
  await generate(options, version);
package/dist/common.mjs CHANGED
@@ -1,5 +1,7 @@
1
1
  import { stat } from "fs/promises";
2
2
  import ts from "typescript";
3
+ import path from "path";
4
+ import { queriesOutputPath, requestsOutputPath } from "./constants.mjs";
3
5
  export const TData = ts.factory.createIdentifier("TData");
4
6
  export const TError = ts.factory.createIdentifier("TError");
5
7
  export const TContext = ts.factory.createIdentifier("TContext");
@@ -14,7 +16,11 @@ export const lowercaseFirstLetter = (str) => {
14
16
  return str.charAt(0).toLowerCase() + str.slice(1);
15
17
  };
16
18
  export const getNameFromMethod = (method) => {
17
- return method.getName();
19
+ const methodName = method.getName();
20
+ if (!methodName) {
21
+ throw new Error("Method name not found");
22
+ }
23
+ return methodName;
18
24
  };
19
25
  export async function exists(f) {
20
26
  try {
@@ -62,3 +68,68 @@ export function extractPropertiesFromObjectParam(param) {
62
68
  }));
63
69
  return paramNodes;
64
70
  }
71
+ /**
72
+ * Replace the import("...") surrounding the type if there is one.
73
+ * This can happen when the type is imported from another file, but
74
+ * we are already importing all the types from that file.
75
+ *
76
+ * https://regex101.com/r/3DyHaQ/1
77
+ *
78
+ * TODO: Replace with a more robust solution.
79
+ */
80
+ export function getShortType(type) {
81
+ return type.replaceAll(/import\(".*"\)\./g, "");
82
+ }
83
+ export function getClassesFromService(node) {
84
+ const klasses = node.getClasses();
85
+ if (!klasses.length) {
86
+ throw new Error("No classes found");
87
+ }
88
+ return klasses.map((klass) => {
89
+ const className = klass.getName();
90
+ if (!className) {
91
+ throw new Error("Class name not found");
92
+ }
93
+ return {
94
+ className,
95
+ klass,
96
+ };
97
+ });
98
+ }
99
+ export function getClassNameFromClassNode(klass) {
100
+ const className = klass.getName();
101
+ if (!className) {
102
+ throw new Error("Class name not found");
103
+ }
104
+ return className;
105
+ }
106
+ export function formatOptions(options) {
107
+ // loop through properties on the options object
108
+ // if the property is a string of number then convert it to a number
109
+ // if the property is a string of boolean then convert it to a boolean
110
+ const formattedOptions = Object.entries(options).reduce((acc, [key, value]) => {
111
+ const typedKey = key;
112
+ const typedValue = value;
113
+ const parsedNumber = safeParseNumber(typedValue);
114
+ if (value === "true" || value === true) {
115
+ acc[typedKey] = true;
116
+ }
117
+ else if (value === "false" || value === false) {
118
+ acc[typedKey] = false;
119
+ }
120
+ else if (!isNaN(parsedNumber)) {
121
+ acc[typedKey] = parsedNumber;
122
+ }
123
+ else {
124
+ acc[typedKey] = typedValue;
125
+ }
126
+ return acc;
127
+ }, options);
128
+ return formattedOptions;
129
+ }
130
+ export function buildRequestsOutputPath(outputPath) {
131
+ return path.join(outputPath, requestsOutputPath);
132
+ }
133
+ export function buildQueriesOutputPath(outputPath) {
134
+ return path.join(outputPath, queriesOutputPath);
135
+ }
@@ -1,3 +1,12 @@
1
1
  export const defaultOutputPath = "openapi";
2
2
  export const queriesOutputPath = "queries";
3
3
  export const requestsOutputPath = "requests";
4
+ export const serviceFileName = "services.gen";
5
+ export const modalsFileName = "types.gen";
6
+ export const OpenApiRqFiles = {
7
+ queries: "queries",
8
+ common: "common",
9
+ suspense: "suspense",
10
+ index: "index",
11
+ prefetch: "prefetch",
12
+ };
@@ -1,5 +1,6 @@
1
1
  import { createUseQuery } from "./createUseQuery.mjs";
2
2
  import { createUseMutation } from "./createUseMutation.mjs";
3
+ import { createPrefetch } from "./createPrefetch.mjs";
3
4
  export const createExports = (service) => {
4
5
  const { klasses } = service;
5
6
  const methods = klasses.map((k) => k.methods).flat();
@@ -9,6 +10,7 @@ export const createExports = (service) => {
9
10
  const allPatch = methods.filter((m) => m.httpMethodName.toUpperCase().includes("PATCH"));
10
11
  const allDelete = methods.filter((m) => m.httpMethodName.toUpperCase().includes("DELETE"));
11
12
  const allGetQueries = allGet.map((m) => createUseQuery(m));
13
+ const allPrefetchQueries = allGet.map((m) => createPrefetch(m));
12
14
  const allPostMutations = allPost.map((m) => createUseMutation(m));
13
15
  const allPutMutations = allPut.map((m) => createUseMutation(m));
14
16
  const allPatchMutations = allPatch.map((m) => createUseMutation(m));
@@ -36,6 +38,10 @@ export const createExports = (service) => {
36
38
  .map(({ suspenseQueryHook }) => [suspenseQueryHook])
37
39
  .flat();
38
40
  const suspenseExports = [...suspenseQueries];
41
+ const allPrefetches = allPrefetchQueries
42
+ .map(({ prefetchHook }) => [prefetchHook])
43
+ .flat();
44
+ const allPrefetchExports = [...allPrefetches];
39
45
  return {
40
46
  /**
41
47
  * Common types and variables between queries (regular and suspense) and mutations
@@ -49,5 +55,9 @@ export const createExports = (service) => {
49
55
  * Suspense exports are the hooks that are used in the suspense components
50
56
  */
51
57
  suspenseExports,
58
+ /**
59
+ * Prefetch exports are the hooks that are used in the prefetch components
60
+ */
61
+ allPrefetchExports,
52
62
  };
53
63
  };
@@ -1,27 +1,22 @@
1
1
  import ts from "typescript";
2
2
  import { posix } from "path";
3
+ import { modalsFileName, serviceFileName } from "./constants.mjs";
3
4
  const { join } = posix;
4
5
  export const createImports = ({ serviceEndName, project, }) => {
5
6
  const modelsFile = project
6
7
  .getSourceFiles()
7
- .find((sourceFile) => sourceFile.getFilePath().includes("models.ts"));
8
- const serviceFile = project
9
- .getSourceFiles()
10
- .find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
8
+ .find((sourceFile) => sourceFile.getFilePath().includes(modalsFileName));
9
+ const serviceFile = project.getSourceFileOrThrow(`${serviceFileName}.ts`);
11
10
  if (!modelsFile) {
12
11
  console.warn(`
13
12
  ⚠️ WARNING: No models file found.
14
13
  This may be an error if \`.components.schemas\` or \`.components.parameters\` is defined in your OpenAPI input.`);
15
14
  }
16
- if (!serviceFile) {
17
- throw new Error("No service file found");
18
- }
19
15
  const modelNames = modelsFile
20
16
  ? Array.from(modelsFile.getExportedDeclarations().keys())
21
17
  : [];
22
18
  const serviceExports = Array.from(serviceFile.getExportedDeclarations().keys());
23
19
  const serviceNames = serviceExports.filter((name) => name.endsWith(serviceEndName));
24
- const serviceNamesData = serviceExports.filter((name) => name.endsWith("Data"));
25
20
  const imports = [
26
21
  ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
27
22
  ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier("useQuery")),
@@ -35,15 +30,13 @@ export const createImports = ({ serviceEndName, project, }) => {
35
30
  ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
36
31
  // import all class names from service file
37
32
  ...serviceNames.map((serviceName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(serviceName))),
38
- // import all data objects from service file
39
- ...serviceNamesData.map((dataName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(dataName))),
40
- ])), ts.factory.createStringLiteral(join("../requests")), undefined),
33
+ ])), ts.factory.createStringLiteral(join("../requests", serviceFileName)), undefined),
41
34
  ];
42
35
  if (modelsFile) {
43
36
  // import all the models by name
44
37
  imports.push(ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, undefined, ts.factory.createNamedImports([
45
38
  ...modelNames.map((modelName) => ts.factory.createImportSpecifier(false, undefined, ts.factory.createIdentifier(modelName))),
46
- ])), ts.factory.createStringLiteral(join("../requests/models")), undefined));
39
+ ])), ts.factory.createStringLiteral(join("../requests/", modalsFileName)), undefined));
47
40
  }
48
41
  return imports;
49
42
  };
@@ -0,0 +1,58 @@
1
+ import ts from "typescript";
2
+ import { BuildCommonTypeName, extractPropertiesFromObjectParam, getNameFromMethod, } from "./common.mjs";
3
+ import { createQueryKeyFromMethod, getRequestParamFromMethod, hookNameFromMethod, } from "./createUseQuery.mjs";
4
+ import { addJSDocToNode } from "./util.mjs";
5
+ /**
6
+ * Creates a prefetch function for a query
7
+ */
8
+ function createPrefetchHook({ requestParams, method, className, }) {
9
+ const methodName = getNameFromMethod(method);
10
+ const queryName = hookNameFromMethod({ method, className });
11
+ const customHookName = `prefetch${queryName.charAt(0).toUpperCase() + queryName.slice(1)}`;
12
+ const queryKey = createQueryKeyFromMethod({ method, className });
13
+ // const
14
+ const hookExport = ts.factory.createVariableStatement(
15
+ // export
16
+ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
17
+ ts.factory.createVariableDeclaration(ts.factory.createIdentifier(customHookName), undefined, undefined, ts.factory.createArrowFunction(undefined, undefined, [
18
+ ts.factory.createParameterDeclaration(undefined, undefined, "queryClient", undefined, ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("QueryClient"))),
19
+ ...requestParams,
20
+ ], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createIdentifier("queryClient.prefetchQuery"), undefined, [
21
+ ts.factory.createObjectLiteralExpression([
22
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryKey"), ts.factory.createArrayLiteralExpression([
23
+ BuildCommonTypeName(queryKey),
24
+ method.getParameters().length
25
+ ? ts.factory.createArrayLiteralExpression([
26
+ ts.factory.createObjectLiteralExpression(method
27
+ .getParameters()
28
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
29
+ .flat()),
30
+ ])
31
+ : ts.factory.createArrayLiteralExpression([]),
32
+ ], false)),
33
+ ts.factory.createPropertyAssignment(ts.factory.createIdentifier("queryFn"), ts.factory.createArrowFunction(undefined, undefined, [], undefined, ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(className), ts.factory.createIdentifier(methodName)), undefined, method.getParameters().length
34
+ ? [
35
+ ts.factory.createObjectLiteralExpression(method
36
+ .getParameters()
37
+ .map((param) => extractPropertiesFromObjectParam(param).map((p) => ts.factory.createShorthandPropertyAssignment(ts.factory.createIdentifier(p.name))))
38
+ .flat()),
39
+ ]
40
+ : undefined))),
41
+ ]),
42
+ ]))),
43
+ ], ts.NodeFlags.Const));
44
+ return hookExport;
45
+ }
46
+ export const createPrefetch = ({ className, method, jsDoc, }) => {
47
+ const requestParam = getRequestParamFromMethod(method);
48
+ const requestParams = requestParam ? [requestParam] : [];
49
+ const prefetchHook = createPrefetchHook({
50
+ requestParams,
51
+ method,
52
+ className,
53
+ });
54
+ const hookWithJsDoc = addJSDocToNode(prefetchHook, jsDoc);
55
+ return {
56
+ prefetchHook: hookWithJsDoc,
57
+ };
58
+ };
@@ -1,9 +1,10 @@
1
1
  import ts from "typescript";
2
+ import { Project } from "ts-morph";
3
+ import { join } from "path";
4
+ import { OpenApiRqFiles } from "./constants.mjs";
2
5
  import { createImports } from "./createImports.mjs";
3
6
  import { createExports } from "./createExports.mjs";
4
7
  import { getServices } from "./service.mjs";
5
- import { Project } from "ts-morph";
6
- import { join } from "path";
7
8
  const createSourceFile = async (outputPath, serviceEndName) => {
8
9
  const project = new Project({
9
10
  // Optionally specify compiler options, tsconfig.json, in-memory file system, and more here.
@@ -21,53 +22,63 @@ const createSourceFile = async (outputPath, serviceEndName) => {
21
22
  });
22
23
  const exports = createExports(service);
23
24
  const commonSource = ts.factory.createSourceFile([...imports, ...exports.allCommon], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
24
- const commonImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier("* as Common"), undefined), ts.factory.createStringLiteral("./common"), undefined);
25
- const commonExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral("./common"), undefined);
26
- const queriesExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral("./queries"), undefined);
25
+ const commonImport = ts.factory.createImportDeclaration(undefined, ts.factory.createImportClause(false, ts.factory.createIdentifier("* as Common"), undefined), ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
26
+ const commonExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.common}`), undefined);
27
+ const queriesExport = ts.factory.createExportDeclaration(undefined, false, undefined, ts.factory.createStringLiteral(`./${OpenApiRqFiles.queries}`), undefined);
27
28
  const mainSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.mainExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
28
29
  const suspenseSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.suspenseExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
29
30
  const indexSource = ts.factory.createSourceFile([commonExport, queriesExport], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
31
+ const prefetchSource = ts.factory.createSourceFile([commonImport, ...imports, ...exports.allPrefetchExports], ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
30
32
  return {
31
33
  commonSource,
32
34
  mainSource,
33
35
  suspenseSource,
34
36
  indexSource,
37
+ prefetchSource,
35
38
  };
36
39
  };
37
40
  export const createSource = async ({ outputPath, version, serviceEndName, }) => {
38
- const queriesFile = ts.createSourceFile("queries.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
39
- const commonFile = ts.createSourceFile("common.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
40
- const suspenseFile = ts.createSourceFile("suspense.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
41
- const indexFile = ts.createSourceFile("index.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
41
+ const queriesFile = ts.createSourceFile(`${OpenApiRqFiles.queries}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
42
+ const commonFile = ts.createSourceFile(`${OpenApiRqFiles.common}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
43
+ const suspenseFile = ts.createSourceFile(`${OpenApiRqFiles.suspense}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
44
+ const indexFile = ts.createSourceFile(`${OpenApiRqFiles.index}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
45
+ const prefetchFile = ts.createSourceFile(`${OpenApiRqFiles.prefetch}.ts`, "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
42
46
  const printer = ts.createPrinter({
43
47
  newLine: ts.NewLineKind.LineFeed,
44
48
  removeComments: false,
45
49
  });
46
- const { commonSource, mainSource, suspenseSource, indexSource } = await createSourceFile(outputPath, serviceEndName);
47
- const commonResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
50
+ const { commonSource, mainSource, suspenseSource, indexSource, prefetchSource, } = await createSourceFile(outputPath, serviceEndName);
51
+ const comment = `// generated with @7nohe/openapi-react-query-codegen@${version} \n\n`;
52
+ const commonResult = comment +
48
53
  printer.printNode(ts.EmitHint.Unspecified, commonSource, commonFile);
49
- const mainResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
54
+ const mainResult = comment +
50
55
  printer.printNode(ts.EmitHint.Unspecified, mainSource, queriesFile);
51
- const suspenseResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
56
+ const suspenseResult = comment +
52
57
  printer.printNode(ts.EmitHint.Unspecified, suspenseSource, suspenseFile);
53
- const indexResult = `// generated with @7nohe/openapi-react-query-codegen@${version} \n` +
58
+ const indexResult = comment +
54
59
  printer.printNode(ts.EmitHint.Unspecified, indexSource, indexFile);
60
+ const prefetchResult = comment +
61
+ printer.printNode(ts.EmitHint.Unspecified, prefetchSource, prefetchFile);
55
62
  return [
56
63
  {
57
- name: "index.ts",
64
+ name: `${OpenApiRqFiles.index}.ts`,
58
65
  content: indexResult,
59
66
  },
60
67
  {
61
- name: "common.ts",
68
+ name: `${OpenApiRqFiles.common}.ts`,
62
69
  content: commonResult,
63
70
  },
64
71
  {
65
- name: "queries.ts",
72
+ name: `${OpenApiRqFiles.queries}.ts`,
66
73
  content: mainResult,
67
74
  },
68
75
  {
69
- name: "suspense.ts",
76
+ name: `${OpenApiRqFiles.suspense}.ts`,
70
77
  content: suspenseResult,
71
78
  },
79
+ {
80
+ name: `${OpenApiRqFiles.prefetch}.ts`,
81
+ content: prefetchResult,
82
+ },
72
83
  ];
73
84
  };
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { BuildCommonTypeName, TContext, TData, TError, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, } from "./common.mjs";
2
+ import { BuildCommonTypeName, TContext, TData, TError, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, getShortType, } from "./common.mjs";
3
3
  import { addJSDocToNode } from "./util.mjs";
4
4
  /**
5
5
  * Awaited<ReturnType<typeof myClass.myMethod>>
@@ -11,7 +11,7 @@ function generateAwaitedReturnType({ className, methodName, }) {
11
11
  ]),
12
12
  ]);
13
13
  }
14
- export const createUseMutation = ({ node, className, method, jsDoc = [], isDeprecated = false, }) => {
14
+ export const createUseMutation = ({ className, method, jsDoc, }) => {
15
15
  const methodName = getNameFromMethod(method);
16
16
  const awaitedResponseDataType = generateAwaitedReturnType({
17
17
  className,
@@ -26,22 +26,9 @@ export const createUseMutation = ({ node, className, method, jsDoc = [], isDepre
26
26
  const paramNodes = extractPropertiesFromObjectParam(param);
27
27
  return paramNodes.map((refParam) => ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(refParam.name), refParam.optional
28
28
  ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
29
- : undefined,
30
- // refParam.questionToken ?? refParam.initializer
31
- // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
32
- // : refParam.questionToken,
33
- ts.factory.createTypeReferenceNode(refParam.type.getText(param))));
29
+ : undefined, ts.factory.createTypeReferenceNode(getShortType(refParam.type.getText(param)))));
34
30
  })
35
- .flat()
36
- // return ts.factory.createPropertySignature(
37
- // undefined,
38
- // ts.factory.createIdentifier(param.getName()),
39
- // param.compilerNode.questionToken ?? param.compilerNode.initializer
40
- // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
41
- // : param.compilerNode.questionToken,
42
- // param.compilerNode.type
43
- // );
44
- )
31
+ .flat())
45
32
  : ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
46
33
  const exportHook = ts.factory.createVariableStatement([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], ts.factory.createVariableDeclarationList([
47
34
  ts.factory.createVariableDeclaration(ts.factory.createIdentifier(`use${className}${capitalizeFirstLetter(methodName)}`), undefined, undefined, ts.factory.createArrowFunction(undefined, ts.factory.createNodeArray([
@@ -90,7 +77,7 @@ export const createUseMutation = ({ node, className, method, jsDoc = [], isDepre
90
77
  ]),
91
78
  ]))),
92
79
  ], ts.NodeFlags.Const));
93
- const hookWithJsDoc = addJSDocToNode(exportHook, node, isDeprecated, jsDoc);
80
+ const hookWithJsDoc = addJSDocToNode(exportHook, jsDoc);
94
81
  return {
95
82
  mutationResult,
96
83
  mutationHook: hookWithJsDoc,
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript";
2
- import { BuildCommonTypeName, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, queryKeyConstraint, queryKeyGenericType, TData, TError, } from "./common.mjs";
2
+ import { BuildCommonTypeName, capitalizeFirstLetter, extractPropertiesFromObjectParam, getNameFromMethod, getShortType, queryKeyConstraint, queryKeyGenericType, TData, TError, } from "./common.mjs";
3
3
  import { addJSDocToNode } from "./util.mjs";
4
4
  export const createApiResponseType = ({ className, methodName, }) => {
5
5
  /** Awaited<ReturnType<typeof myClass.myMethod>> */
@@ -25,14 +25,6 @@ export const createApiResponseType = ({ className, methodName, }) => {
25
25
  responseDataType,
26
26
  };
27
27
  };
28
- /**
29
- * Replace the import("...") surrounding the type if there is one.
30
- * This can happen when the type is imported from another file, but
31
- * we are already importing all the types from that file.
32
- */
33
- function getShortType(type) {
34
- return type.replaceAll(/import\("[a-zA-Z\/\.-]*"\)\./g, "");
35
- }
36
28
  export function getRequestParamFromMethod(method) {
37
29
  if (!method.getParameters().length) {
38
30
  return null;
@@ -52,11 +44,7 @@ export function getRequestParamFromMethod(method) {
52
44
  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) => {
53
45
  return ts.factory.createPropertySignature(undefined, ts.factory.createIdentifier(refParam.name), refParam.optional
54
46
  ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
55
- : undefined,
56
- // param.hasQuestionToken() ?? param.getInitializer()?.compilerNode
57
- // ? ts.factory.createToken(ts.SyntaxKind.QuestionToken)
58
- // : param.getQuestionTokenNode()?.compilerNode,
59
- ts.factory.createTypeReferenceNode(refParam.typeName));
47
+ : undefined, ts.factory.createTypeReferenceNode(refParam.typeName));
60
48
  })),
61
49
  // if all params are optional, we create an empty object literal
62
50
  // so the hook can be called without any parameters
@@ -85,11 +73,11 @@ export function createQueryKeyExport({ className, methodName, queryKey, }) {
85
73
  ts.factory.createVariableDeclaration(ts.factory.createIdentifier(queryKey), undefined, undefined, ts.factory.createStringLiteral(`${className}${capitalizeFirstLetter(methodName)}`)),
86
74
  ], ts.NodeFlags.Const));
87
75
  }
88
- function hookNameFromMethod({ method, className, }) {
76
+ export function hookNameFromMethod({ method, className, }) {
89
77
  const methodName = getNameFromMethod(method);
90
78
  return `use${className}${capitalizeFirstLetter(methodName)}`;
91
79
  }
92
- function createQueryKeyFromMethod({ method, className, }) {
80
+ export function createQueryKeyFromMethod({ method, className, }) {
93
81
  const customHookName = hookNameFromMethod({ method, className });
94
82
  const queryKey = `${customHookName}Key`;
95
83
  return queryKey;
@@ -99,7 +87,7 @@ function createQueryKeyFromMethod({ method, className, }) {
99
87
  * @param queryString The type of query to use from react-query
100
88
  * @param suffix The suffix to append to the hook name
101
89
  */
102
- function createQueryHook({ queryString, suffix, responseDataType, requestParams, method, className, }) {
90
+ export function createQueryHook({ queryString, suffix, responseDataType, requestParams, method, className, }) {
103
91
  const methodName = getNameFromMethod(method);
104
92
  const customHookName = hookNameFromMethod({ method, className });
105
93
  const queryKey = createQueryKeyFromMethod({ method, className });
@@ -151,7 +139,7 @@ function createQueryHook({ queryString, suffix, responseDataType, requestParams,
151
139
  ], ts.NodeFlags.Const));
152
140
  return hookExport;
153
141
  }
154
- export const createUseQuery = ({ node, className, method, jsDoc = [], isDeprecated: deprecated = false, }) => {
142
+ export const createUseQuery = ({ className, method, jsDoc, }) => {
155
143
  const methodName = getNameFromMethod(method);
156
144
  const queryKey = createQueryKeyFromMethod({ method, className });
157
145
  const { apiResponse: defaultApiResponse, responseDataType } = createApiResponseType({
@@ -176,8 +164,8 @@ export const createUseQuery = ({ node, className, method, jsDoc = [], isDeprecat
176
164
  method,
177
165
  className,
178
166
  });
179
- const hookWithJsDoc = addJSDocToNode(queryHook, node, deprecated, jsDoc);
180
- const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, node, deprecated, jsDoc);
167
+ const hookWithJsDoc = addJSDocToNode(queryHook, jsDoc);
168
+ const suspenseHookWithJsDoc = addJSDocToNode(suspenseQueryHook, jsDoc);
181
169
  const returnTypeExport = createReturnTypeExport({
182
170
  className,
183
171
  methodName,
@@ -0,0 +1,21 @@
1
+ import { IndentationText, NewLineKind, Project, QuoteKind } from "ts-morph";
2
+ export const formatOutput = async (outputPath) => {
3
+ const project = new Project({
4
+ skipAddingFilesFromTsConfig: true,
5
+ manipulationSettings: {
6
+ indentationText: IndentationText.TwoSpaces,
7
+ newLineKind: NewLineKind.LineFeed,
8
+ quoteKind: QuoteKind.Double,
9
+ usePrefixAndSuffixTextForRename: false,
10
+ useTrailingCommas: true,
11
+ },
12
+ });
13
+ const sourceFiles = project.addSourceFilesAtPaths(`${outputPath}/**/*`);
14
+ const tasks = sourceFiles.map((sourceFile) => {
15
+ sourceFile.formatText();
16
+ sourceFile.fixMissingImports();
17
+ sourceFile.organizeImports();
18
+ return sourceFile.save();
19
+ });
20
+ await Promise.all(tasks);
21
+ };
package/dist/generate.mjs CHANGED
@@ -1,40 +1,36 @@
1
1
  import { createClient } from "@hey-api/openapi-ts";
2
2
  import { print } from "./print.mjs";
3
- import path from "path";
4
3
  import { createSource } from "./createSource.mjs";
5
- import { defaultOutputPath, requestsOutputPath } from "./constants.mjs";
6
- import { safeParseNumber } from "./common.mjs";
4
+ import { buildQueriesOutputPath, buildRequestsOutputPath, formatOptions, } from "./common.mjs";
5
+ import { formatOutput } from "./format.mjs";
7
6
  export async function generate(options, version) {
8
- const openApiOutputPath = path.join(options.output ?? defaultOutputPath, requestsOutputPath);
9
- // loop through properties on the options object
10
- // if the property is a string of number then convert it to a number
11
- // if the property is a string of boolean then convert it to a boolean
12
- const formattedOptions = Object.entries(options).reduce((acc, [key, value]) => {
13
- const typedKey = key;
14
- const typedValue = value;
15
- const parsedNumber = safeParseNumber(typedValue);
16
- if (!isNaN(parsedNumber)) {
17
- acc[typedKey] = parsedNumber;
18
- }
19
- else if (value === "true") {
20
- acc[typedKey] = true;
21
- }
22
- else if (value === "false") {
23
- acc[typedKey] = false;
24
- }
25
- else {
26
- acc[typedKey] = typedValue;
27
- }
28
- return acc;
29
- }, options);
7
+ const openApiOutputPath = buildRequestsOutputPath(options.output);
8
+ const formattedOptions = formatOptions(options);
30
9
  const config = {
31
- ...formattedOptions,
10
+ base: formattedOptions.base,
11
+ client: formattedOptions.client,
12
+ debug: formattedOptions.debug,
13
+ dryRun: false,
14
+ enums: formattedOptions.enums,
15
+ exportCore: true,
16
+ format: formattedOptions.format,
17
+ input: formattedOptions.input,
18
+ lint: formattedOptions.lint,
32
19
  output: openApiOutputPath,
20
+ request: formattedOptions.request,
21
+ schemas: {
22
+ export: !formattedOptions.noSchemas,
23
+ type: formattedOptions.schemaType,
24
+ },
25
+ services: {
26
+ export: true,
27
+ response: formattedOptions.serviceResponse,
28
+ },
29
+ types: {
30
+ dates: formattedOptions.useDateType,
31
+ export: true,
32
+ },
33
33
  useOptions: true,
34
- exportCore: true,
35
- exportModels: true,
36
- exportServices: true,
37
- write: true,
38
34
  };
39
35
  await createClient(config);
40
36
  const source = await createSource({
@@ -43,4 +39,6 @@ export async function generate(options, version) {
43
39
  serviceEndName: "Service", // we are hard coding this because changing the service end name was depreciated in @hey-api/openapi-ts
44
40
  });
45
41
  await print(source, formattedOptions);
42
+ const queriesOutputPath = buildQueriesOutputPath(options.output);
43
+ await formatOutput(queriesOutputPath);
46
44
  }
package/dist/print.mjs CHANGED
@@ -1,9 +1,8 @@
1
1
  import { mkdir, writeFile } from "fs/promises";
2
2
  import path from "path";
3
- import { defaultOutputPath, queriesOutputPath } from "./constants.mjs";
4
- import { exists } from "./common.mjs";
3
+ import { buildQueriesOutputPath, exists } from "./common.mjs";
5
4
  async function printGeneratedTS(result, options) {
6
- const dir = path.join(options.output ?? defaultOutputPath, queriesOutputPath);
5
+ const dir = buildQueriesOutputPath(options.output);
7
6
  const dirExists = await exists(dir);
8
7
  if (!dirExists) {
9
8
  await mkdir(dir, { recursive: true });
@@ -11,7 +10,7 @@ async function printGeneratedTS(result, options) {
11
10
  await writeFile(path.join(dir, result.name), result.content);
12
11
  }
13
12
  export async function print(results, options) {
14
- const outputPath = options.output ?? defaultOutputPath;
13
+ const outputPath = options.output;
15
14
  const dirExists = await exists(outputPath);
16
15
  if (!dirExists) {
17
16
  await mkdir(outputPath);
package/dist/service.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import ts from "typescript";
2
+ import { getClassNameFromClassNode, getClassesFromService, } from "./common.mjs";
3
+ import { serviceFileName } from "./constants.mjs";
2
4
  export async function getServices(project) {
3
5
  const node = project
4
6
  .getSourceFiles()
5
- .find((sourceFile) => sourceFile.getFilePath().includes("services.ts"));
7
+ .find((sourceFile) => sourceFile.getFilePath().includes(serviceFileName));
6
8
  if (!node) {
7
9
  throw new Error("No service node found");
8
10
  }
@@ -16,29 +18,6 @@ export async function getServices(project) {
16
18
  node,
17
19
  };
18
20
  }
19
- function getClassesFromService(node) {
20
- const klasses = node.getClasses();
21
- if (!klasses.length) {
22
- throw new Error("No classes found");
23
- }
24
- return klasses.map((klass) => {
25
- const className = klass.getName();
26
- if (!className) {
27
- throw new Error("Class name not found");
28
- }
29
- return {
30
- className,
31
- klass,
32
- };
33
- });
34
- }
35
- function getClassNameFromClassNode(klass) {
36
- const className = klass.getName();
37
- if (!className) {
38
- throw new Error("Class name not found");
39
- }
40
- return className;
41
- }
42
21
  function getMethodsFromService(node, klass) {
43
22
  const methods = klass.getMethods();
44
23
  if (!methods.length) {
@@ -78,7 +57,13 @@ function getMethodsFromService(node, klass) {
78
57
  return [tsNode];
79
58
  };
80
59
  const children = getAllChildren(method.compilerNode);
81
- const jsDoc = method.getJsDocs().map((jsDoc) => jsDoc);
60
+ // get all JSDoc comments
61
+ // this should be an array of 1 or 0
62
+ const jsDocs = children
63
+ .filter((c) => c.kind === ts.SyntaxKind.JSDoc)
64
+ .map((c) => c.getText(node.compilerNode));
65
+ // get the first JSDoc comment
66
+ const jsDoc = jsDocs?.[0];
82
67
  const isDeprecated = children.some((c) => c.kind === ts.SyntaxKind.JSDocDeprecatedTag);
83
68
  const className = getClassNameFromClassNode(klass);
84
69
  return {
package/dist/util.mjs CHANGED
@@ -1,28 +1,16 @@
1
1
  import ts from "typescript";
2
- export function addJSDocToNode(node, sourceFile, deprecated, jsDoc = []) {
3
- const deprecatedString = deprecated ? "@deprecated" : "";
4
- const jsDocString = [deprecatedString]
5
- .concat(jsDoc.map((comment) => {
6
- if (typeof comment === "string") {
7
- return comment;
8
- }
9
- if (Array.isArray(comment)) {
10
- return comment.map((c) => c.getText(sourceFile)).join("\n");
11
- }
12
- return "";
13
- }))
14
- // remove empty lines
15
- .filter(Boolean)
16
- // trim
17
- .map((comment) => comment.trim())
18
- // add * to each line
19
- .map((comment) => `* ${comment}`)
20
- // join lines
21
- .join("\n")
22
- // replace new lines with \n *
23
- .replace(/\n/g, "\n * ");
24
- const nodeWithJSDoc = jsDocString
25
- ? ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, `*\n ${jsDocString}\n `, true)
26
- : node;
2
+ export function addJSDocToNode(node, jsDoc) {
3
+ if (!jsDoc) {
4
+ return node;
5
+ }
6
+ // replace the first /** with *
7
+ // we do this because ts.addSyntheticLeadingComment will add /* to the beginning but we want /**
8
+ const removedFirstLine = jsDoc.trim().replace(/^\/\*\*/, "*");
9
+ // remove the last */ because ts.addSyntheticLeadingComment will add it
10
+ const removedSecondLine = removedFirstLine.replace(/\*\/$/, "");
11
+ const split = removedSecondLine.split("\n");
12
+ const trimmed = split.map((line) => line.trim());
13
+ const joined = trimmed.join("\n");
14
+ const nodeWithJSDoc = ts.addSyntheticLeadingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, joined, true);
27
15
  return nodeWithJSDoc;
28
16
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@7nohe/openapi-react-query-codegen",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "OpenAPI React Query Codegen",
5
5
  "bin": {
6
6
  "openapi-rq": "dist/cli.mjs"
@@ -31,20 +31,22 @@
31
31
  "author": "Daiki Urata (@7nohe)",
32
32
  "license": "MIT",
33
33
  "devDependencies": {
34
- "@hey-api/openapi-ts": "0.36.0",
34
+ "@hey-api/openapi-ts": "0.42.1",
35
35
  "@types/node": "^20.10.6",
36
+ "@vitest/coverage-v8": "^1.5.0",
36
37
  "commander": "^12.0.0",
37
38
  "glob": "^10.3.10",
38
39
  "rimraf": "^5.0.5",
39
40
  "ts-morph": "^22.0.0",
40
- "typescript": "^5.3.3"
41
+ "typescript": "^5.3.3",
42
+ "vitest": "^1.5.0"
41
43
  },
42
44
  "peerDependencies": {
43
- "@hey-api/openapi-ts": "0.36.0",
44
- "commander": ">= 11 < 13",
45
- "glob": ">= 10",
46
- "ts-morph": ">= 22 < 23",
47
- "typescript": ">= 4.8.3"
45
+ "@hey-api/openapi-ts": "0.42.1",
46
+ "commander": "12.x",
47
+ "glob": "10.x",
48
+ "ts-morph": "22.x",
49
+ "typescript": "5.x"
48
50
  },
49
51
  "engines": {
50
52
  "node": ">=14"
@@ -52,6 +54,7 @@
52
54
  "scripts": {
53
55
  "build": "rimraf dist && tsc -p tsconfig.json",
54
56
  "preview": "npm run build && npm -C examples/react-app run generate:api",
55
- "release": "npx git-ensure -a && npx bumpp --commit --tag --push"
57
+ "release": "npx git-ensure -a && npx bumpp --commit --tag --push",
58
+ "test": "vitest --coverage.enabled true"
56
59
  }
57
60
  }