diff --git a/.all-contributorsrc b/.all-contributorsrc
index fa8a5f7..22f30e6 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -119,6 +119,13 @@
"avatar_url": "https://avatars.githubusercontent.com/u/172711?v=4",
"profile": "https://tverdohleb.com/",
"contributions": ["bug", "code", "ideas"]
+ },
+ {
+ "login": "hayawata3626",
+ "name": "Isco",
+ "avatar_url": "https://avatars.githubusercontent.com/u/15213369?v=4",
+ "profile": "https://zenn.dev/watahaya",
+ "contributions": ["code"]
}
],
"contributorsPerLine": 7
diff --git a/README.md b/README.md
index bfd617d..df2b0f8 100644
--- a/README.md
+++ b/README.md
@@ -284,6 +284,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
 Valeriy 🐛 💻 🤔 |
+  Isco 💻 |
diff --git a/plugins/typescript/src/core/analyzeImportUsage.test.ts b/plugins/typescript/src/core/analyzeImportUsage.test.ts
new file mode 100644
index 0000000..5c99d65
--- /dev/null
+++ b/plugins/typescript/src/core/analyzeImportUsage.test.ts
@@ -0,0 +1,346 @@
+import { describe, it, expect } from "vitest";
+import ts, { factory as f } from "typescript";
+import { analyzeImportUsage } from "./analyzeImportUsage";
+
+// Helper to set parent references for TypeScript AST nodes
+const setParentReferences = (node: T): T => {
+ function setParent(child: ts.Node, parent: ts.Node) {
+ Object.defineProperty(child, "parent", {
+ value: parent,
+ writable: true,
+ configurable: true,
+ });
+ child.forEachChild((grandChild) => setParent(grandChild, child));
+ }
+ node.forEachChild((child) => setParent(child, node));
+ return node;
+};
+
+describe("analyzeImportUsage", () => {
+ describe("type-only usage", () => {
+ it("should detect type reference usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyAlias"),
+ undefined,
+ f.createTypeReferenceNode(
+ f.createIdentifier("TestImport"),
+ undefined
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
+ });
+
+ it("should detect usage in type alias declaration", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("TestImport"),
+ undefined,
+ f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
+ });
+
+ it("should detect usage in interface declaration", () => {
+ const nodes = [
+ setParentReferences(
+ f.createInterfaceDeclaration(
+ undefined,
+ f.createIdentifier("TestImport"),
+ undefined,
+ undefined,
+ []
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
+ });
+
+ it("should detect indexed access type usage", () => {
+ const typeRef = f.createTypeReferenceNode(
+ f.createIdentifier("TestImport"),
+ undefined
+ );
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createIndexedAccessTypeNode(
+ typeRef,
+ f.createLiteralTypeNode(f.createStringLiteral("prop"))
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(true);
+ });
+ });
+
+ describe("value usage", () => {
+ it("should detect call expression usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createCallExpression(
+ f.createIdentifier("TestImport"),
+ undefined,
+ []
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+
+ it("should detect property access usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createPropertyAccessExpression(
+ f.createIdentifier("TestImport"),
+ f.createIdentifier("prop")
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+
+ it("should detect binary expression usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createBinaryExpression(
+ f.createIdentifier("TestImport"),
+ ts.SyntaxKind.EqualsEqualsToken,
+ f.createStringLiteral("test")
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+
+ it("should detect new expression usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createNewExpression(
+ f.createIdentifier("TestImport"),
+ undefined,
+ []
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+
+ it("should detect array literal usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createVariableStatement(
+ undefined,
+ f.createVariableDeclarationList([
+ f.createVariableDeclaration(
+ f.createIdentifier("arr"),
+ undefined,
+ undefined,
+ f.createArrayLiteralExpression([
+ f.createIdentifier("TestImport"),
+ ])
+ ),
+ ])
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+
+ it("should detect object literal usage", () => {
+ const objectLiteral = f.createObjectLiteralExpression([
+ f.createPropertyAssignment("key", f.createIdentifier("TestImport")),
+ ]);
+ const nodes = [
+ setParentReferences(
+ f.createVariableStatement(
+ undefined,
+ f.createVariableDeclarationList([
+ f.createVariableDeclaration(
+ f.createIdentifier("obj"),
+ undefined,
+ undefined,
+ objectLiteral
+ ),
+ ])
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+ });
+
+ describe("mixed usage", () => {
+ it("should detect mixed type and value usage", () => {
+ const nodes = [
+ // Type usage
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createTypeReferenceNode(
+ f.createIdentifier("TestImport"),
+ undefined
+ )
+ )
+ ),
+ // Value usage
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createCallExpression(
+ f.createIdentifier("TestImport"),
+ undefined,
+ []
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+ });
+
+ describe("unused imports", () => {
+ it("should default to type-only for unused imports", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "UnusedImport")).toBe(true);
+ });
+
+ it("should handle empty node array", () => {
+ expect(analyzeImportUsage([], "TestImport")).toBe(true);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("should handle nodes without parents", () => {
+ const identifier = f.createIdentifier("TestImport");
+ // Explicitly don't set parent references
+ expect(analyzeImportUsage([identifier], "TestImport")).toBe(true);
+ });
+
+ it("should be case sensitive", () => {
+ const nodes = [
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createCallExpression(
+ f.createIdentifier("testImport"), // lowercase
+ undefined,
+ []
+ )
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(true); // Should default to type-only
+ });
+
+ it("should handle deeply nested usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createBlock([
+ f.createIfStatement(
+ f.createBinaryExpression(
+ f.createTrue(),
+ ts.SyntaxKind.EqualsEqualsToken,
+ f.createTrue()
+ ),
+ f.createBlock([
+ f.createExpressionStatement(
+ f.createCallExpression(
+ f.createIdentifier("TestImport"),
+ undefined,
+ []
+ )
+ ),
+ ])
+ ),
+ ])
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "TestImport")).toBe(false);
+ });
+ });
+
+ describe("performance optimization", () => {
+ it("should return false for mixed usage", () => {
+ const nodes = [
+ // Type usage
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createTypeReferenceNode(
+ f.createIdentifier("TestImport"),
+ undefined
+ )
+ )
+ ),
+ // Value usage
+ setParentReferences(
+ f.createExpressionStatement(
+ f.createCallExpression(
+ f.createIdentifier("TestImport"),
+ undefined,
+ []
+ )
+ )
+ ),
+ // Additional nodes - should stop before processing these
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("AnotherType"),
+ undefined,
+ f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ )
+ ),
+ ];
+
+ const result = analyzeImportUsage(nodes, "TestImport");
+ expect(result).toBe(false);
+ });
+ });
+});
diff --git a/plugins/typescript/src/core/analyzeImportUsage.ts b/plugins/typescript/src/core/analyzeImportUsage.ts
new file mode 100644
index 0000000..3b0db51
--- /dev/null
+++ b/plugins/typescript/src/core/analyzeImportUsage.ts
@@ -0,0 +1,83 @@
+import ts from "typescript";
+
+/**
+ * Determines if a node represents a value usage context
+ */
+const isValueUsageContext = (parent: ts.Node): boolean => {
+ return (
+ ts.isCallExpression(parent) ||
+ ts.isObjectBindingPattern(parent) ||
+ ts.isPropertyAccessExpression(parent) ||
+ ts.isBinaryExpression(parent) ||
+ ts.isReturnStatement(parent) ||
+ ts.isExportSpecifier(parent) ||
+ ts.isImportSpecifier(parent) ||
+ ts.isAsExpression(parent) ||
+ ts.isNewExpression(parent) ||
+ ts.isArrayLiteralExpression(parent) ||
+ ts.isObjectLiteralExpression(parent) ||
+ ts.isPropertyAssignment(parent) ||
+ ts.isVariableDeclaration(parent)
+ );
+};
+
+/**
+ * Determines if a node represents a type usage context
+ */
+const isTypeUsageContext = (parent: ts.Node, currentNode: ts.Node): boolean => {
+ if (
+ ts.isTypeReferenceNode(parent) ||
+ ts.isTypeAliasDeclaration(parent) ||
+ ts.isInterfaceDeclaration(parent)
+ ) {
+ return true;
+ }
+
+ if (ts.isIndexedAccessTypeNode(parent)) {
+ return (
+ ts.isTypeReferenceNode(parent.objectType) &&
+ parent.objectType.typeName === currentNode
+ );
+ }
+
+ return false;
+};
+
+/**
+ * Analyzes how an imported symbol is used in the AST.
+ *
+ * @param nodes the AST nodes to analyze
+ * @param importName the name of the imported symbol
+ * @returns true if the import is used only as a type
+ */
+export const analyzeImportUsage = (
+ nodes: ts.Node[],
+ importName: string
+): boolean => {
+ const determineUsageType = (): "type-only" | "value" | "mixed" | "unused" => {
+ const usages = { type: false, value: false };
+
+ const checkNode = (node: ts.Node): boolean => {
+ // returns: should continue
+ if (ts.isIdentifier(node) && node.text === importName && node.parent) {
+ const parent = node.parent;
+
+ if (isValueUsageContext(parent)) usages.value = true;
+ if (isTypeUsageContext(parent, node)) usages.type = true;
+
+ return !(usages.type && usages.value);
+ }
+
+ return !node.forEachChild((child) => !checkNode(child));
+ };
+
+ nodes.every(checkNode);
+
+ if (!usages.type && !usages.value) return "unused";
+ if (usages.type && usages.value) return "mixed";
+ return usages.type ? "type-only" : "value";
+ };
+
+ const usage = determineUsageType();
+ return usage === "type-only" || usage === "unused";
+};
diff --git a/plugins/typescript/src/core/createNamedImport.test.ts b/plugins/typescript/src/core/createNamedImport.test.ts
new file mode 100644
index 0000000..d954b30
--- /dev/null
+++ b/plugins/typescript/src/core/createNamedImport.test.ts
@@ -0,0 +1,110 @@
+import { describe, it, expect } from "vitest";
+import { createNamedImport, shouldUseTypeImport } from "./createNamedImport";
+import { analyzeImportUsage } from "./analyzeImportUsage";
+import { factory as f } from "typescript";
+import * as ts from "typescript";
+
+// Helper function to set parent references in AST nodes
+const setParentReferences = (node: ts.Node): ts.Node => {
+ const setParent = (n: ts.Node, parent: ts.Node) => {
+ Object.defineProperty(n, "parent", {
+ value: parent,
+ writable: true,
+ configurable: true,
+ });
+ n.forEachChild((child) => setParent(child, n));
+ };
+
+ node.forEachChild((child) => setParent(child, node));
+ return node;
+};
+
+describe("createNamedImport", () => {
+ it("should create a regular import when isTypeOnly is false", () => {
+ const result = createNamedImport("MyType", "./types", false);
+ expect(result.importClause?.isTypeOnly).toBe(false);
+ });
+
+ it("should create a type-only import when isTypeOnly is true", () => {
+ const result = createNamedImport("MyType", "./types", true);
+ expect(result.importClause?.isTypeOnly).toBe(true);
+ });
+
+ it("should handle array of imports", () => {
+ const result = createNamedImport(["Type1", "Type2"], "./types", true);
+ expect(result.importClause?.isTypeOnly).toBe(true);
+ expect(result.importClause?.namedBindings).toBeDefined();
+ });
+});
+
+describe("shouldUseTypeImport", () => {
+ it("should return false when useTypeImports is false", () => {
+ expect(shouldUseTypeImport("User", false)).toBe(false);
+ expect(shouldUseTypeImport("fetchData", false)).toBe(false);
+ });
+
+ it("should return false when no AST nodes provided", () => {
+ expect(shouldUseTypeImport("User", true)).toBe(false);
+ expect(shouldUseTypeImport("fetchData", true)).toBe(false);
+ });
+});
+
+describe("analyzeImportUsage", () => {
+ it("should detect type-only usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createTypeReferenceNode(f.createIdentifier("User"), undefined)
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "User")).toBe(true);
+ });
+
+ it("should detect value usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createCallExpression(f.createIdentifier("fetchData"), undefined, [])
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "fetchData")).toBe(false);
+ });
+
+ it("should detect mixed usage", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createTypeReferenceNode(f.createIdentifier("User"), undefined)
+ )
+ ),
+ setParentReferences(
+ f.createCallExpression(f.createIdentifier("User"), undefined, [])
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "User")).toBe(false);
+ });
+
+ it("should default to type-only when not used", () => {
+ const nodes = [
+ setParentReferences(
+ f.createTypeAliasDeclaration(
+ undefined,
+ f.createIdentifier("MyType"),
+ undefined,
+ f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword)
+ )
+ ),
+ ];
+
+ expect(analyzeImportUsage(nodes, "UnusedImport")).toBe(true);
+ });
+});
diff --git a/plugins/typescript/src/core/createNamedImport.ts b/plugins/typescript/src/core/createNamedImport.ts
index 1b8007f..545b16a 100644
--- a/plugins/typescript/src/core/createNamedImport.ts
+++ b/plugins/typescript/src/core/createNamedImport.ts
@@ -1,4 +1,6 @@
import { factory as f } from "typescript";
+import type * as ts from "typescript";
+import { analyzeImportUsage } from "./analyzeImportUsage";
/**
* Helper to create named imports.
@@ -29,3 +31,58 @@ export const createNamedImport = (
undefined
);
};
+
+/**
+ * Helper to determine whether an import should be type-only based on actual usage.
+ *
+ * @param importName the name of the import
+ * @param useTypeImports whether to use type-only imports
+ * @param nodes AST nodes to analyze for usage
+ * @returns true if the import should be type-only
+ */
+export const shouldUseTypeImport = (
+ importName: string,
+ useTypeImports: boolean,
+ nodes?: ts.Node[]
+): boolean => {
+ if (!useTypeImports) {
+ return false;
+ }
+
+ if (nodes) {
+ const isTypeOnly = analyzeImportUsage(nodes, importName);
+ return isTypeOnly;
+ }
+
+ return false;
+};
+
+/**
+ * Helper to create named imports with types.
+ *
+ * @param typeImports array of import names that should be type-only
+ * @param valueImports array of import names that should be value imports
+ * @param filename path of the module
+ * @returns ts.Node of the merged import declaration
+ */
+export const createNamedImportWithTypes = (
+ typeImports: string[],
+ valueImports: string[],
+ filename: string
+) => {
+ const allImports = [
+ ...typeImports.map((name) =>
+ f.createImportSpecifier(true, undefined, f.createIdentifier(name))
+ ),
+ ...valueImports.map((name) =>
+ f.createImportSpecifier(false, undefined, f.createIdentifier(name))
+ ),
+ ];
+
+ return f.createImportDeclaration(
+ undefined,
+ f.createImportClause(false, undefined, f.createNamedImports(allImports)),
+ f.createStringLiteral(filename),
+ undefined
+ );
+};
diff --git a/plugins/typescript/src/core/getUsedImports.ts b/plugins/typescript/src/core/getUsedImports.ts
index 590fd47..3e76a3c 100644
--- a/plugins/typescript/src/core/getUsedImports.ts
+++ b/plugins/typescript/src/core/getUsedImports.ts
@@ -19,7 +19,8 @@ export const getUsedImports = (
parameters: string;
responses: string;
utils: string;
- }
+ },
+ useTypeImports = true
): { keys: string[]; nodes: ts.Node[] } => {
const imports: Record<
keyof typeof files,
@@ -92,7 +93,7 @@ export const getUsedImports = (
return createNamedImport(
Array.from(i.imports.values()),
`./${i.from}`,
- true
+ useTypeImports ?? true
);
}
}),
diff --git a/plugins/typescript/src/generators/generateFetchers.ts b/plugins/typescript/src/generators/generateFetchers.ts
index e205704..1e9c403 100644
--- a/plugins/typescript/src/generators/generateFetchers.ts
+++ b/plugins/typescript/src/generators/generateFetchers.ts
@@ -11,7 +11,10 @@ import { createOperationFetcherFnNodes } from "../core/createOperationFetcherFnN
import { isVerb } from "../core/isVerb";
import { isOperationObject } from "../core/isOperationObject";
import { getOperationTypes } from "../core/getOperationTypes";
-import { createNamedImport } from "../core/createNamedImport";
+import {
+ createNamedImport,
+ shouldUseTypeImport,
+} from "../core/createNamedImport";
import { getFetcher } from "../templates/fetcher";
import { getUtils } from "../templates/utils";
@@ -95,6 +98,7 @@ export const generateFetchers = async (context: Context, config: Config) => {
getFetcher({
prefix: filenamePrefix,
baseUrl: get(context.openAPIDocument, "servers.0.url"),
+ useTypeImports: config.useTypeImports,
})
);
} else {
@@ -222,7 +226,8 @@ export const generateFetchers = async (context: Context, config: Config) => {
{
...config.schemasFiles,
utils: utilsFilename,
- }
+ },
+ config.useTypeImports
);
if (
@@ -237,7 +242,14 @@ export const generateFetchers = async (context: Context, config: Config) => {
printNodes([
createWatermark(context.openAPIDocument.info),
createNamespaceImport("Fetcher", `./${fetcherFilename}`),
- createNamedImport(fetcherImports, `./${fetcherFilename}`),
+ createNamedImport(
+ fetcherImports,
+ `./${fetcherFilename}`,
+ config.useTypeImports &&
+ fetcherImports.some((name) =>
+ shouldUseTypeImport(name, config.useTypeImports || false, nodes)
+ )
+ ),
...usedImportsNodes,
...nodes,
])
diff --git a/plugins/typescript/src/generators/generateReactQueryComponents.test.ts b/plugins/typescript/src/generators/generateReactQueryComponents.test.ts
index 0aae751..7c83cc5 100644
--- a/plugins/typescript/src/generators/generateReactQueryComponents.test.ts
+++ b/plugins/typescript/src/generators/generateReactQueryComponents.test.ts
@@ -93,8 +93,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -259,8 +259,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -450,8 +450,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -656,8 +656,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -848,8 +848,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -1041,8 +1041,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -1248,8 +1248,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -1447,8 +1447,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -1654,8 +1654,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -1809,8 +1809,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2012,8 +2012,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2173,8 +2173,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2307,8 +2307,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2445,8 +2445,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2583,8 +2583,8 @@ describe("generateReactQueryComponents", () => {
*/
import * as reactQuery from "@tanstack/react-query";
import {
+ type PetstoreContext,
usePetstoreContext,
- PetstoreContext,
queryKeyFn,
} from "./petstoreContext";
import { deepMerge } from "./petstoreUtils";
@@ -2720,7 +2720,7 @@ describe("generateReactQueryComponents", () => {
* @version 1.0.0
*/
import * as reactQuery from "@tanstack/react-query";
- import { useContext, Context, queryKeyFn } from "./context";
+ import { type Context, useContext, queryKeyFn } from "./context";
import { deepMerge } from "./utils";
import type * as Fetcher from "./fetcher";
import { fetch } from "./fetcher";
diff --git a/plugins/typescript/src/generators/generateReactQueryComponents.ts b/plugins/typescript/src/generators/generateReactQueryComponents.ts
index 4cfbab0..7d75b41 100644
--- a/plugins/typescript/src/generators/generateReactQueryComponents.ts
+++ b/plugins/typescript/src/generators/generateReactQueryComponents.ts
@@ -11,7 +11,10 @@ import { createOperationFetcherFnNodes } from "../core/createOperationFetcherFnN
import { isVerb } from "../core/isVerb";
import { isOperationObject } from "../core/isOperationObject";
import { getOperationTypes } from "../core/getOperationTypes";
-import { createNamedImport } from "../core/createNamedImport";
+import {
+ createNamedImport,
+ createNamedImportWithTypes,
+} from "../core/createNamedImport";
import { getFetcher } from "../templates/fetcher";
import { getContext } from "../templates/context";
@@ -42,6 +45,8 @@ export const generateReactQueryComponents = async (
context: Context,
config: Config
) => {
+ const { useTypeImports = true, ...restConfig } = config;
+ const finalConfig = { useTypeImports, ...restConfig };
const sourceFile = ts.createSourceFile(
"index.ts",
"",
@@ -69,13 +74,14 @@ export const generateReactQueryComponents = async (
.join("\n");
const filenamePrefix =
- c.snake(config.filenamePrefix ?? context.openAPIDocument.info.title) + "-";
+ c.snake(finalConfig.filenamePrefix ?? context.openAPIDocument.info.title) +
+ "-";
const formatFilename =
- typeof config.formatFilename === "function"
- ? config.formatFilename
- : config.filenameCase
- ? c[config.filenameCase]
+ typeof finalConfig.formatFilename === "function"
+ ? finalConfig.formatFilename
+ : finalConfig.filenameCase
+ ? c[finalConfig.filenameCase]
: c.camel;
const filename = formatFilename(filenamePrefix + "-components");
@@ -97,6 +103,7 @@ export const generateReactQueryComponents = async (
prefix: filenamePrefix,
contextPath: contextFilename,
baseUrl: get(context.openAPIDocument, "servers.0.url"),
+ useTypeImports: finalConfig.useTypeImports,
})
);
}
@@ -104,7 +111,7 @@ export const generateReactQueryComponents = async (
if (!context.existsFile(`${contextFilename}.ts`)) {
context.writeFile(
`${contextFilename}.ts`,
- getContext(filenamePrefix, filename)
+ getContext(filenamePrefix, filename, finalConfig.useTypeImports)
);
}
@@ -160,7 +167,7 @@ export const generateReactQueryComponents = async (
operation,
operationId,
printNodes,
- injectedHeaders: config.injectedHeaders,
+ injectedHeaders: finalConfig.injectedHeaders,
pathParameters: verbs.parameters,
variablesExtraPropsType: f.createIndexedAccessTypeNode(
f.createTypeReferenceNode(
@@ -326,10 +333,14 @@ export const generateReactQueryComponents = async (
])
);
- const { nodes: usedImportsNodes } = getUsedImports(nodes, {
- ...config.schemasFiles,
- utils: utilsFilename,
- });
+ const { nodes: usedImportsNodes } = getUsedImports(
+ nodes,
+ {
+ ...finalConfig.schemasFiles,
+ utils: utilsFilename,
+ },
+ finalConfig.useTypeImports
+ );
if (!context.existsFile(`${utilsFilename}.ts`)) {
await context.writeFile(`${utilsFilename}.ts`, getUtils());
@@ -340,10 +351,21 @@ export const generateReactQueryComponents = async (
printNodes([
createWatermark(context.openAPIDocument.info),
createReactQueryImport(),
- createNamedImport(
- [contextHookName, contextTypeName, "queryKeyFn"],
- `./${contextFilename}`
- ),
+ ...(finalConfig.useTypeImports
+ ? [
+ createNamedImportWithTypes(
+ [contextTypeName],
+ [contextHookName, "queryKeyFn"],
+ `./${contextFilename}`
+ ),
+ ]
+ : [
+ createNamedImport(
+ [contextHookName, contextTypeName, "queryKeyFn"],
+ `./${contextFilename}`,
+ false
+ ),
+ ]),
createNamedImport("deepMerge", `./${utilsFilename}`),
createNamespaceImport("Fetcher", `./${fetcherFilename}`),
createNamedImport(fetcherFn, `./${fetcherFilename}`),
diff --git a/plugins/typescript/src/generators/generateSchemaTypes.ts b/plugins/typescript/src/generators/generateSchemaTypes.ts
index f361e47..c7f8846 100644
--- a/plugins/typescript/src/generators/generateSchemaTypes.ts
+++ b/plugins/typescript/src/generators/generateSchemaTypes.ts
@@ -27,6 +27,8 @@ export const generateSchemaTypes = async (
context: Context,
config: Config = {}
) => {
+ const { useTypeImports = true, ...restConfig } = config;
+ const finalConfig = { useTypeImports, ...restConfig };
const { components } = context.openAPIDocument;
if (!components) {
throw new Error("No components founds!");
@@ -67,7 +69,7 @@ export const generateSchemaTypes = async (
openAPIDocument: context.openAPIDocument,
currentComponent: currentComponent,
},
- config.useEnums
+ finalConfig.useEnums
),
],
[]
@@ -77,7 +79,7 @@ export const generateSchemaTypes = async (
componentSchemaEntries: [string, SchemaObject | ReferenceObject][],
currentComponent: OpenAPIComponentType
) => {
- if (config.useEnums) {
+ if (finalConfig.useEnums) {
const enumSchemaEntries = getEnumProperties(componentSchemaEntries);
const enumSchemas = enumSchemaEntries.reduce(
(mem, [name, schema]) => [
@@ -104,13 +106,14 @@ export const generateSchemaTypes = async (
};
const filenamePrefix =
- c.snake(config.filenamePrefix ?? context.openAPIDocument.info.title) + "-";
+ c.snake(finalConfig.filenamePrefix ?? context.openAPIDocument.info.title) +
+ "-";
const formatFilename =
- typeof config.formatFilename === "function"
- ? config.formatFilename
- : config.filenameCase
- ? c[config.filenameCase]
+ typeof finalConfig.formatFilename === "function"
+ ? finalConfig.formatFilename
+ : finalConfig.filenameCase
+ ? c[finalConfig.filenameCase]
: c.camel;
const files = {
requestBodies: formatFilename(filenamePrefix + "-request-bodies"),
@@ -132,7 +135,7 @@ export const generateSchemaTypes = async (
files.schemas + ".ts",
printNodes([
createWatermark(context.openAPIDocument.info),
- ...getUsedImports(schemas, files).nodes,
+ ...getUsedImports(schemas, files, finalConfig.useTypeImports).nodes,
...schemas,
])
);
@@ -163,7 +166,7 @@ export const generateSchemaTypes = async (
files.responses + ".ts",
printNodes([
createWatermark(context.openAPIDocument.info),
- ...getUsedImports(schemas, files).nodes,
+ ...getUsedImports(schemas, files, finalConfig.useTypeImports).nodes,
...schemas,
])
);
@@ -196,7 +199,7 @@ export const generateSchemaTypes = async (
files.requestBodies + ".ts",
printNodes([
createWatermark(context.openAPIDocument.info),
- ...getUsedImports(schemas, files).nodes,
+ ...getUsedImports(schemas, files, finalConfig.useTypeImports).nodes,
...schemas,
])
);
@@ -224,7 +227,7 @@ export const generateSchemaTypes = async (
files.parameters + ".ts",
printNodes([
createWatermark(context.openAPIDocument.info),
- ...getUsedImports(schemas, files).nodes,
+ ...getUsedImports(schemas, files, finalConfig.useTypeImports).nodes,
...schemas,
])
);
diff --git a/plugins/typescript/src/generators/types.ts b/plugins/typescript/src/generators/types.ts
index 0474c26..3494b74 100644
--- a/plugins/typescript/src/generators/types.ts
+++ b/plugins/typescript/src/generators/types.ts
@@ -36,4 +36,11 @@ export type ConfigBase = {
* @default false
*/
useEnums?: boolean;
+ /**
+ * Use type-only imports for types and interfaces.
+ * This is useful when using TypeScript's verbatimModuleSyntax option.
+ *
+ * @default true
+ */
+ useTypeImports?: boolean;
};
diff --git a/plugins/typescript/src/templates/context.ts b/plugins/typescript/src/templates/context.ts
index 7e1f1d6..1f3f872 100644
--- a/plugins/typescript/src/templates/context.ts
+++ b/plugins/typescript/src/templates/context.ts
@@ -1,6 +1,10 @@
import { pascal } from "case";
-export const getContext = (prefix: string, componentsFile: string) =>
+export const getContext = (
+ prefix: string,
+ componentsFile: string,
+ useTypeImports = true
+) =>
`import {
skipToken,
type DefaultError,
@@ -8,7 +12,7 @@ export const getContext = (prefix: string, componentsFile: string) =>
type QueryKey,
type UseQueryOptions,
} from "@tanstack/react-query";
- import { QueryOperation } from './${componentsFile}';
+ import ${useTypeImports ? "type { QueryOperation }" : "{ QueryOperation }"} from './${componentsFile}';
export type ${pascal(prefix)}Context<
TQueryFnData = unknown,
diff --git a/plugins/typescript/src/templates/fetcher.ts b/plugins/typescript/src/templates/fetcher.ts
index edc74c7..ff7d20c 100644
--- a/plugins/typescript/src/templates/fetcher.ts
+++ b/plugins/typescript/src/templates/fetcher.ts
@@ -9,14 +9,16 @@ export const getFetcher = ({
prefix,
contextPath,
baseUrl,
+ useTypeImports = true,
}: {
prefix: string;
contextPath?: string;
baseUrl?: string;
+ useTypeImports?: boolean;
}) =>
`${
contextPath
- ? `import { ${pascal(prefix)}Context } from "./${contextPath}";`
+ ? `import ${useTypeImports ? `type { ${pascal(prefix)}Context }` : `{ ${pascal(prefix)}Context }`} from "./${contextPath}";`
: `export type ${pascal(prefix)}FetcherExtraProps = {
/**
* You can add some extra props to your generated fetchers.