Add an option to modify generated type names via a builder or a suffix
Summary: I would like to request a feature that allows users to customize the names of the generated types through a builder or by adding a suffix. This enhancement would provide more flexibility and clarity, especially when integrating generated types into existing projects where specific naming conventions are required.
Use Case: In scenarios where the generated types need to adhere to particular naming conventions or to avoid naming conflicts within a project, the ability to modify the names of these types is essential. For example, adding a suffix like Dto to the generated types can help maintain consistency and improve readability.
Proposed Solution: • Introduce an option in the configuration to allow users to specify a suffix or a custom name pattern for generated types. • Provide clear documentation and examples on how to use this new feature.
Benefits: • Enhances the flexibility of type generation. • Allows better integration with projects that have strict naming conventions. • Reduces potential conflicts and confusion in large codebases.
I believe this feature would significantly improve the developer experience and extend the utility of the type generation tool. Thank you for considering this request!
@MrLepage This will be definitely supported in the future. Let me put a vote on this to see how important it is compared to other features!
A temporary solution to this issue could be the following script that we started using to be able to migrate to openapi-ts while waiting for this feature. Just run it with npm after calling openapi-ts.
E.g.:
openapi-ts && node scripts/add-api-suffix.cjs --suffix '__api' --dir 'src/api/'
const fs = require("fs");
const path = require("path");
const process = require("process");
const TYPES_FILE = "types.gen.ts";
const INDEX_FILE = "index.ts";
function parseProcessArgs() {
const cmdArgRegex = /^--(\w+)$/;
const valueRegex = /^['"]?(.*?)['"]?$/;
const argsMap = {};
let lastCmdArg = null;
for (const arg of process.argv.slice(2)) {
const match = arg.match(cmdArgRegex);
if (match) {
argsMap[match[1]] = true;
lastCmdArg = match[1];
}
else {
const value = arg.match(valueRegex)[1];
if (lastCmdArg) {
if (argsMap[lastCmdArg] && argsMap[lastCmdArg] !== true) {
argsMap[lastCmdArg] = [...argsMap[lastCmdArg], value];
continue;
}
argsMap[lastCmdArg] = value;
}
}
}
return argsMap;
}
const parsedArgs = parseProcessArgs();
// Get process arguments
const suffix = parsedArgs.suffix;
let dir = parsedArgs.dir;
const encoding = parsedArgs.encoding ?? "utf8";
const exportTanstackQueryFromIndex = parsedArgs.exportTanstackQueryFromIndex ?? false;
if (!suffix) {
throw new Error("No suffix provided, exiting");
}
if (!dir) {
console.info("No dir provided, using cwd");
dir = process.cwd();
}
const typesFilePath = path.join(dir, TYPES_FILE);
const indexFilePath = path.join(dir, INDEX_FILE);
if (!fs.existsSync(typesFilePath)) {
throw new Error(`File ${typesFilePath} does not exist`);
}
const exportTypesRegExp = /^export (type|enum) (\w+)/gm;
const singleImportNameRegExp = /(\s*(\w+),?\s*)/g;
function makeImportTypesRegExp(currentDir) {
const relativeTypesFilePath = path.relative(currentDir, typesFilePath);
const parsedFilePath = path.parse(relativeTypesFilePath);
let escapedFilePath = path.join(parsedFilePath.dir, parsedFilePath.name).replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
if (path.resolve(currentDir) === path.resolve(path.parse(typesFilePath).dir)) {
escapedFilePath = `\\./${escapedFilePath}`;
}
return new RegExp(`^import type {((\\s*(\\w+),?\\s*)+)} from ["']${escapedFilePath}["'];`, "m");
}
// First, replace all occurences of exported types with the same type name plus the suffix
const typesFileContent = fs.readFileSync(typesFilePath, encoding);
const matches = typesFileContent.matchAll(exportTypesRegExp);
const exportNames = [];
for (const match of matches) {
exportNames.push(match[2]);
}
let newTypesFileContent = typesFileContent.replace(exportTypesRegExp, `export $1 $2${suffix}`);
for (const exportName of exportNames) {
const exportNameRegExp = new RegExp(`\\b${exportName}\\b`, "g");
newTypesFileContent = newTypesFileContent.replace(exportNameRegExp, `${exportName}${suffix}`);
}
fs.writeFileSync(typesFilePath, newTypesFileContent);
console.log(`\u{2705} Added suffix to exported types in '${typesFilePath}'`);
function adjustImportedTypeNames(content, dir) {
let newContent = content;
const importTypesRegExp = makeImportTypesRegExp(dir);
const match = content.match(importTypesRegExp);
if (match === null) {
return { newContent, gotAdjusted: false };
}
const matchedString = match[1];
const singleImportMatches = matchedString.matchAll(singleImportNameRegExp);
const importNames = [];
for (const singleImportMatch of singleImportMatches) {
importNames.push(singleImportMatch[2]);
}
for (const importName of importNames) {
const importNameRegExp = new RegExp(`\\b${importName}\\b`, "g");
newContent = newContent.replace(importNameRegExp, `${importName}${suffix}`);
}
return { newContent, gotAdjusted: true };
}
// Then, replace all occurences of imported types within all files in the given directory with the same type name plus the suffix
function recursivelyAdjustImportedTypeNames(dir) {
const filesAndDirs = fs.readdirSync(dir);
for (const fileOrDir of filesAndDirs) {
const fileOrDirPath = path.join(dir, fileOrDir);
if (fs.statSync(fileOrDirPath).isDirectory()) {
recursivelyAdjustImportedTypeNames(fileOrDirPath);
continue;
}
const fileContent = fs.readFileSync(fileOrDirPath, encoding);
const { newContent, gotAdjusted } = adjustImportedTypeNames(fileContent, dir);
fs.writeFileSync(fileOrDirPath, newContent);
if (gotAdjusted) {
console.log(`\u{2705} Added suffix to imported types in '${fileOrDirPath}'`);
}
}
}
recursivelyAdjustImportedTypeNames(dir);
// Finally, re-export all TanstackQuery exports from the index file
if (exportTanstackQueryFromIndex) {
let indexFileContent = fs.readFileSync(indexFilePath, encoding);
indexFileContent += `\nexport * from './@tanstack/react-query.gen';\n`;
fs.writeFileSync(indexFilePath, indexFileContent);
console.log(`\u{2705} Added re-export statement for tanstack query exports in '${indexFilePath}'`);
}
Hey all, v0.79.0 will add a bunch of new customization options to the TypeScript plugin. Let me know if you're still missing certain capabilities once you try it!
hey @mrlubos , this seems like it also addresses https://github.com/hey-api/openapi-ts/issues/558 ? Right?
@IAL32 No, that's a separate issue
@MrLepage This will be definitely supported in the future. Let me put a vote on this to see how important it is compared to other features!
hi @mrlubos , the current case config is for generated enum values. Can we have a option to config for the const name itself? The use case is like:
export const THE_KIND = {...} as const
export type TheKind = (typeof THE_KIND)[keyof typeof THE_KIND]
so we can differentiate the const and type when importing.
@kid1412621 can you describe the issue more? I've never felt the confusion/need to differentiate between them
@kid1412621 can you describe the issue more? I've never felt the confusion/need to differentiate between them
hi, imagine this usage:
import { type TheKind } from '@/types'
import { TheKind } from '@/types' // it conflicts
let x: TheKind | null = null
x = TheKind.KIND_1
but if we can rename the const:
import { type TheKind } from '@/types'
import { THE_KIND } from '@/types' // currently we use alias: import { TheKind as THE_KIND } from '@types'
let x: TheKind | null = null
x = THE_KIND.KIND_1
Do you have any suggestion for naming this option?
Do you have any suggestion for naming this option?
I think we need a way to configure renaming the const itself. Like SNAKE_CASE for const and PascalCase for type. Or able to add appendix like XXXEnum or sth.
A custom plugin could be implemented.
// openapi-ts.config.ts
import {
UserConfig,
defineConfig,
DefinePlugin,
definePluginConfig,
} from "@hey-api/openapi-ts";
type MyEndpointPlugin = DefinePlugin<{
omitControllerName?: boolean;
name: string;
}>;
export const MyEndpointPlugin = definePluginConfig({
config: { omitControllerName: true },
dependencies: ["@hey-api/typescript"],
handler: (({ plugin }) => {
plugin.forEach("operation", (arg) => {
arg.operation.id = arg.operation.id + "Suffix"
});
}) as MyEndpointPlugin["Handler"],
name: "my-plugin",
output: "my-plugin",
} as MyEndpointPlugin["Config"]);
const config = await defineConfig({
input: "http://localhost/openapi-spec-json", // or a remote URL
output: {
path: "./app/lib/api/gen/", // where generated files go
format: "prettier", // auto-format output
lint: "eslint", // run eslint on generated files
// optional: single file vs modular structure
// target: "typescript", // defaults to TS
clean: true,
case: "camelCase",
},
// interactive: true,
// dryRun: true,
parser: {},
plugins: [
MyEndpointPlugin({ omitControllerName: true }),
"@hey-api/client-axios",
"@tanstack/react-query",
],
});
export default config;