@toktokhan-dev/cli-plugin-gen-api-react-query 0.2.0 → 0.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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from '@toktokhan-dev/cli';
2
- import { createPackageRoot, prettierString, withLoading, cwd } from '@toktokhan-dev/node';
2
+ import { createPackageRoot, withLoading, cwd } from '@toktokhan-dev/node';
3
3
  import omit from 'lodash/omit.js';
4
4
  import path from 'path';
5
5
  import { generateApi } from 'swagger-typescript-api';
@@ -43,6 +43,8 @@ const parseSwagger = (config) => generateApi({
43
43
  input: config.swaggerSchemaUrl,
44
44
  httpClientType: config.httpClientType, // "axios" or "fetch"
45
45
  typeSuffix: 'Type',
46
+ sortTypes: true,
47
+ sortRoutes: true,
46
48
  prettier: {
47
49
  printWidth: 120,
48
50
  },
@@ -394,12 +396,12 @@ const writeSwaggerApiFile = async (params) => {
394
396
  processedContent = rewriteDataContractsImport(content, filename, classificationResult);
395
397
  }
396
398
  const { apiContents, hookParts } = splitHookContents(filename, processedContent);
397
- await generatePretty(path.resolve(targetFolder, `${filename}.api.ts`), apiContents);
399
+ generate(path.resolve(targetFolder, `${filename}.api.ts`), apiContents);
398
400
  if (config.includeReactQuery) {
399
- await generatePretty(path.resolve(targetFolder, `${filename}.query.ts`), hookParts[0]);
401
+ generate(path.resolve(targetFolder, `${filename}.query.ts`), hookParts[0]);
400
402
  }
401
403
  if (config.includeReactSuspenseQuery) {
402
- await generatePretty(path.resolve(targetFolder, `${filename}.suspenseQuery.ts`), hookParts[1]);
404
+ generate(path.resolve(targetFolder, `${filename}.suspenseQuery.ts`), hookParts[1]);
403
405
  }
404
406
  continue;
405
407
  }
@@ -498,23 +500,6 @@ function extractImportedTypeNames(content) {
498
500
  .map((s) => s.trim())
499
501
  .filter(Boolean));
500
502
  }
501
- async function generatePretty(path, contents) {
502
- let organized = contents;
503
- try {
504
- organized = await prettierString(contents, {
505
- parser: 'babel-ts',
506
- plugins: ['prettier-plugin-organize-imports'],
507
- });
508
- }
509
- catch (err) {
510
- console.warn('⚠️ prettier-plugin-organize-imports not found, skipping import organization');
511
- }
512
- const formatted = await prettierString(organized, {
513
- parser: 'typescript',
514
- configPath: 'auto',
515
- });
516
- generate(path, formatted);
517
- }
518
503
  function generate(path, contents) {
519
504
  let existingContent = '';
520
505
  try {
@@ -654,7 +639,9 @@ const genApi = defineCommand({
654
639
  await withLoading('Prettier format', covered.output, async () => {
655
640
  const fs = await import('fs');
656
641
  const pathMod = await import('path');
657
- const { prettierString } = await import('@toktokhan-dev/node');
642
+ const { prettierString, findFileToTop } = await import('@toktokhan-dev/node');
643
+ // output 디렉토리 기준으로 prettier config를 탐색 (cwd 의존 제거)
644
+ const configPath = findFileToTop(covered.output, '.prettierrc.js') || 'auto';
658
645
  const listTsFiles = (dir) => {
659
646
  const entries = fs.readdirSync(dir, { withFileTypes: true });
660
647
  const files = [];
@@ -673,9 +660,19 @@ const genApi = defineCommand({
673
660
  for (const file of files) {
674
661
  try {
675
662
  const raw = fs.readFileSync(file, 'utf8');
676
- const formatted = await prettierString(raw, {
663
+ let organized = raw;
664
+ try {
665
+ organized = await prettierString(raw, {
666
+ parser: 'babel-ts',
667
+ plugins: ['prettier-plugin-organize-imports'],
668
+ });
669
+ }
670
+ catch {
671
+ // prettier-plugin-organize-imports not installed, skip
672
+ }
673
+ const formatted = await prettierString(organized, {
677
674
  parser: 'typescript',
678
- configPath: 'auto',
675
+ configPath,
679
676
  });
680
677
  fs.writeFileSync(file, formatted);
681
678
  }
@@ -693,8 +690,9 @@ const genApi = defineCommand({
693
690
  // 스마트 타입 병합 함수
694
691
  function mergeTypeScriptContent(existing, newContent) {
695
692
  // 1) import 구문 보존 및 병합 (양쪽 모두에서 수집)
696
- const importRegex = /^\s*import\s+[^;]*;\s*$/gm;
697
- const sideEffectImportRegex = /^\s*import\s+['"][^'"]+['"];\s*$/gm;
693
+ // 멀티라인 import도 매칭: import { \n A, \n B \n } from '...';
694
+ const importRegex = /^\s*import\s+[\s\S]*?from\s*['"][^'"]*['"];?/gm;
695
+ const sideEffectImportRegex = /^\s*import\s*['"][^'"]+['"];\s*$/gm;
698
696
  const collectImports = (content) => {
699
697
  const imports = new Set();
700
698
  const matchedA = content.match(importRegex) ?? [];
@@ -710,14 +708,17 @@ function mergeTypeScriptContent(existing, newContent) {
710
708
  // import 병합: 새 파일 기준 우선 순서 + 기존에만 있는 import 추가
711
709
  const mergedImportSet = new Set(newImports);
712
710
  existingImports.forEach((imp) => mergedImportSet.add(imp));
713
- const mergedImports = Array.from(mergedImportSet);
711
+ const mergedImports = Array.from(mergedImportSet).sort();
714
712
  // 2) 타입 선언 병합 (중복 제거)
715
713
  const existingTypes = parseTypeDefinitions(existingBody);
716
714
  const newTypes = parseTypeDefinitions(newBody);
717
715
  // 새 타입 기준으로 병합: 새 버전이 source of truth (swagger 스키마)
718
716
  // 기존에만 있는 타입은 보존 (사용자가 수동 추가한 것)
719
717
  const mergedTypes = { ...existingTypes, ...newTypes };
720
- const mergedTypesString = Object.values(mergedTypes).join('\n\n');
718
+ const mergedTypesString = Object.entries(mergedTypes)
719
+ .sort(([a], [b]) => a.localeCompare(b))
720
+ .map(([, v]) => v)
721
+ .join('\n\n');
721
722
  // 3) 기타 코드(타입/임포트 외)는 "새로운 내용"을 기준으로 유지
722
723
  const removeHeaderComment = (content) => {
723
724
  // 파일 상단의 모든 블록 코멘트와 라인 코멘트를 제거 (재귀적 처리)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toktokhan-dev/cli-plugin-gen-api-react-query",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "A CLI plugin for generating API hooks with React Query built by TOKTOKHAN.DEV",
5
5
  "author": "TOKTOKHAN.DEV <fe-system@toktokhan.dev>",
6
6
  "license": "ISC",
@@ -38,8 +38,8 @@
38
38
  "lodash": "^4.17.21",
39
39
  "prettier-plugin-organize-imports": "^3.2.4",
40
40
  "swagger-typescript-api": "13.0.22",
41
- "@toktokhan-dev/node": "0.0.10",
42
- "@toktokhan-dev/cli": "0.0.11"
41
+ "@toktokhan-dev/cli": "0.0.11",
42
+ "@toktokhan-dev/node": "0.0.10"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/lodash": "^4.17.0",
@@ -36,7 +36,7 @@
36
36
  return `${keyName} : "${enumNames?.[idx]}"`;
37
37
  }).join(", ");
38
38
 
39
- return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\r\n${map} \r\n } as const`;
39
+ return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\n${map} \n } as const`;
40
40
 
41
41
  },
42
42
  interface: (contract) => {
@@ -49,7 +49,7 @@
49
49
  content = content.replace(field, `${field} | null`)
50
50
  })
51
51
 
52
- return `interface ${contract.name} {\r\n${content}}`;
52
+ return `interface ${contract.name} {\n${content}}`;
53
53
  },
54
54
  type: (contract) => {
55
55
  return `type ${contract.name} = ${contract.content}`;
@@ -83,8 +83,7 @@
83
83
  <% if (description.length) { %>
84
84
  /**
85
85
  <%~ description.map(part => `* ${part}`).join("\n") %>
86
-
87
- */
86
+ */
88
87
  <% } %>
89
88
  export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
90
89
 
@@ -36,7 +36,7 @@
36
36
  return `${keyName} : "${enumNames?.[idx]}"`;
37
37
  }).join(", ");
38
38
 
39
- return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\r\n${map} \r\n } as const`;
39
+ return `type ${contract.name} = keyof typeof ${mapName}; export const ${mapName} = {\n${map} \n } as const`;
40
40
 
41
41
  },
42
42
  interface: (contract) => {
@@ -49,7 +49,7 @@
49
49
  content = content.replace(field, `${field} | null`)
50
50
  })
51
51
 
52
- return `interface ${contract.name} {\r\n${content}}`;
52
+ return `interface ${contract.name} {\n${content}}`;
53
53
  },
54
54
  type: (contract) => {
55
55
  return `type ${contract.name} = ${contract.content}`;
@@ -85,8 +85,7 @@
85
85
  <% if (description.length) { %>
86
86
  /**
87
87
  <%~ description.map(part => `* ${part}`).join("\n") %>
88
-
89
- */
88
+ */
90
89
  <% } %>
91
90
  export <%~ (dataContractTemplates[contract.typeIdentifier] || dataContractTemplates.type)(contract) %>
92
91
 
@@ -60,7 +60,7 @@ export enum ContentType {
60
60
  }
61
61
 
62
62
  export class HttpClient<SecurityDataType = unknown> {
63
- public baseUrl: string = "<%~ apiConfig.baseUrl %>";
63
+ public baseUrl: string = "<%~ apiConfig.baseUrl.replace(/\/$/, '') %>";
64
64
  private securityData: SecurityDataType | null = null;
65
65
  private securityWorker?: ApiConfig<SecurityDataType>["securityWorker"];
66
66
  private abortControllers = new Map<CancelToken, AbortController>();
@@ -44,7 +44,7 @@ export type InfiniteQueryHookParams<
44
44
  > = {
45
45
  options?: Partial<
46
46
  Omit<
47
- UseInfiniteQueryOptions<OriginData, Error, TData, OriginData, any, TPageParam>,
47
+ UseInfiniteQueryOptions<OriginData, Error, TData, any, TPageParam>,
48
48
  'queryKey' | 'queryFn'
49
49
  >
50
50
  >;
@@ -85,7 +85,7 @@ export type SuspenseInfiniteQueryHookParams<
85
85
  > = {
86
86
  options?: Partial<
87
87
  Omit<
88
- UseSuspenseInfiniteQueryOptions<OriginData, Error, TData, OriginData, any, TPageParam>,
88
+ UseSuspenseInfiniteQueryOptions<OriginData, Error, TData, any, TPageParam>,
89
89
  'queryKey' | 'queryFn'
90
90
  >
91
91
  >;