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
Valeriy

🐛 💻 🤔 + Isco
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.