@bagelink/sdk 1.2.153 → 1.3.3
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/bin/splitClientGen.ts +718 -0
- package/package.json +2 -1
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import type { TSESTree } from '@typescript-eslint/typescript-estree'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { parse, AST_NODE_TYPES } from '@typescript-eslint/typescript-estree'
|
|
5
|
+
import { formatAndWriteCode } from './utils'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Converts a string to camelCase format (first letter lowercase, rest unchanged)
|
|
9
|
+
* @param str - The string to convert
|
|
10
|
+
* @returns The camelCase version of the string
|
|
11
|
+
*/
|
|
12
|
+
function toCamelCase(str: string): string {
|
|
13
|
+
if (!str) return ''
|
|
14
|
+
return str.charAt(0).toLowerCase() + str.slice(1)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Configuration for the client code splitter
|
|
19
|
+
*/
|
|
20
|
+
export interface SplitClientOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Directory containing the generated files
|
|
23
|
+
*/
|
|
24
|
+
bagelinkDir: string
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether to organize into separate directories
|
|
28
|
+
*/
|
|
29
|
+
useDirectories?: boolean
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Strategy for grouping API and type declarations
|
|
33
|
+
* Default is to use the first PascalCase word in the name
|
|
34
|
+
*/
|
|
35
|
+
groupingStrategy?: GroupingStrategy
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Enhanced type for controlling how files are grouped
|
|
40
|
+
*/
|
|
41
|
+
export interface GroupingStrategy {
|
|
42
|
+
/**
|
|
43
|
+
* Extract the group name from a declaration name
|
|
44
|
+
* @param name The name of the type or API function
|
|
45
|
+
* @returns The group name to use
|
|
46
|
+
*/
|
|
47
|
+
getGroupName: (name: string) => string
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the file name for a group
|
|
51
|
+
* @param group The group name
|
|
52
|
+
* @param kind Whether this is a 'types' or 'api' file
|
|
53
|
+
* @returns The file name to use (without directory path)
|
|
54
|
+
*/
|
|
55
|
+
getFileName: (group: string, kind: 'types' | 'api') => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Default grouping strategy using the first PascalCase word
|
|
60
|
+
*/
|
|
61
|
+
export function createDefaultGroupingStrategy(groupingPattern = /^([A-Z][a-z]+)/): GroupingStrategy {
|
|
62
|
+
return {
|
|
63
|
+
getGroupName(name: string): string {
|
|
64
|
+
const match = name.match(groupingPattern)
|
|
65
|
+
return match ? match[1] : 'Common'
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
getFileName(group: string, kind: 'types' | 'api'): string {
|
|
69
|
+
// Use the utility function for consistent camelCase conversion
|
|
70
|
+
const suffix = kind === 'types' ? 'Types' : 'Api'
|
|
71
|
+
return `${toCamelCase(group)}${suffix}.ts`
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Group information for code organization
|
|
78
|
+
*/
|
|
79
|
+
interface GroupInfo {
|
|
80
|
+
types: Set<string>
|
|
81
|
+
apis: Set<string>
|
|
82
|
+
dependencies: Set<string>
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse TypeScript code with consistent options
|
|
87
|
+
*/
|
|
88
|
+
function parseTypeScript(code: string) {
|
|
89
|
+
return parse(code, {
|
|
90
|
+
ecmaVersion: 2020,
|
|
91
|
+
sourceType: 'module',
|
|
92
|
+
loc: true,
|
|
93
|
+
range: true, // Critical: This enables position tracking in the AST
|
|
94
|
+
tokens: true,
|
|
95
|
+
comment: true,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Splits the monolithic client code into organized files and directories
|
|
101
|
+
*/
|
|
102
|
+
export async function splitClientCode(options: SplitClientOptions): Promise<void> {
|
|
103
|
+
const { bagelinkDir, useDirectories = true, groupingStrategy = createDefaultGroupingStrategy() } = options
|
|
104
|
+
console.log(`Splitting client code in: ${bagelinkDir}`)
|
|
105
|
+
|
|
106
|
+
// Create output directories
|
|
107
|
+
const typesDir = useDirectories ? path.join(bagelinkDir, 'types') : bagelinkDir
|
|
108
|
+
const apisDir = useDirectories ? path.join(bagelinkDir, 'api') : bagelinkDir
|
|
109
|
+
|
|
110
|
+
if (useDirectories) {
|
|
111
|
+
if (!fs.existsSync(typesDir)) fs.mkdirSync(typesDir, { recursive: true })
|
|
112
|
+
if (!fs.existsSync(apisDir)) fs.mkdirSync(apisDir, { recursive: true })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Read the generated files
|
|
116
|
+
const typesFile = path.join(bagelinkDir, 'types.d.ts')
|
|
117
|
+
const apiFile = path.join(bagelinkDir, 'api.ts')
|
|
118
|
+
|
|
119
|
+
if (!fs.existsSync(typesFile) || !fs.existsSync(apiFile)) {
|
|
120
|
+
throw new Error('Generated client code files not found. Run the generator first.')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const typesContent = fs.readFileSync(typesFile, 'utf-8')
|
|
124
|
+
const apiContent = fs.readFileSync(apiFile, 'utf-8')
|
|
125
|
+
|
|
126
|
+
// Extract commonalities first - imports, utility functions, etc.
|
|
127
|
+
const { commonCode, axiosInstance } = extractCommonCode(apiContent)
|
|
128
|
+
|
|
129
|
+
// Parse and organize types
|
|
130
|
+
const typeGroups = organizeTypes(typesContent, groupingStrategy)
|
|
131
|
+
|
|
132
|
+
// Parse and organize API functions
|
|
133
|
+
const apiGroups = organizeApiFunctions(apiContent, groupingStrategy)
|
|
134
|
+
|
|
135
|
+
// Combine the groups information for dependency tracking
|
|
136
|
+
const groups = mergeGroupInfo(typeGroups, apiGroups)
|
|
137
|
+
|
|
138
|
+
// Write the common code
|
|
139
|
+
if (useDirectories) {
|
|
140
|
+
await formatAndWriteCode(
|
|
141
|
+
path.join(apisDir, 'common.ts'),
|
|
142
|
+
commonCode
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// Create proper axios export
|
|
146
|
+
await formatAndWriteCode(
|
|
147
|
+
path.join(apisDir, 'axios.ts'),
|
|
148
|
+
axiosInstance
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Generate and write files for each group
|
|
153
|
+
for (const [group, info] of Object.entries(groups)) {
|
|
154
|
+
// Write types file
|
|
155
|
+
if (info.types.size > 0) {
|
|
156
|
+
const dependencies = getDependencyImports(info.dependencies, groups, 'types')
|
|
157
|
+
const typesOutput = generateTypesFile(group, info.types, typesContent, dependencies)
|
|
158
|
+
const typesFileName = useDirectories
|
|
159
|
+
? path.join(typesDir, groupingStrategy.getFileName(group, 'types'))
|
|
160
|
+
: path.join(bagelinkDir, groupingStrategy.getFileName(group, 'types'))
|
|
161
|
+
|
|
162
|
+
await formatAndWriteCode(typesFileName, typesOutput)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Write API file
|
|
166
|
+
if (info.apis.size > 0) {
|
|
167
|
+
const dependencies = getDependencyImports(info.dependencies, groups, 'apis')
|
|
168
|
+
const apisOutput = generateApiFile(
|
|
169
|
+
group,
|
|
170
|
+
info.apis,
|
|
171
|
+
apiContent,
|
|
172
|
+
dependencies,
|
|
173
|
+
useDirectories
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
const apiFileName = path.join(
|
|
177
|
+
apisDir,
|
|
178
|
+
groupingStrategy.getFileName(group, 'api')
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
await formatAndWriteCode(apiFileName, apisOutput)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Create index files
|
|
186
|
+
if (useDirectories) {
|
|
187
|
+
// Types index
|
|
188
|
+
const typesIndexContent = Object.keys(groups)
|
|
189
|
+
.filter(group => groups[group].types.size > 0)
|
|
190
|
+
.map(group => `export * from './${toCamelCase(group)}Types';`)
|
|
191
|
+
.join('\n')
|
|
192
|
+
|
|
193
|
+
await formatAndWriteCode(
|
|
194
|
+
path.join(typesDir, 'index.ts'),
|
|
195
|
+
typesIndexContent
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// API index - Export axios only ONCE
|
|
199
|
+
const apiIndexContent
|
|
200
|
+
= `export { axios } from './axios';\n${
|
|
201
|
+
Object.keys(groups)
|
|
202
|
+
.filter(group => groups[group].apis.size > 0)
|
|
203
|
+
.map(group => `export * from './${toCamelCase(group)}Api';`)
|
|
204
|
+
.join('\n')}`
|
|
205
|
+
|
|
206
|
+
await formatAndWriteCode(
|
|
207
|
+
path.join(apisDir, 'index.ts'),
|
|
208
|
+
apiIndexContent
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
// Root index
|
|
212
|
+
await formatAndWriteCode(
|
|
213
|
+
path.join(bagelinkDir, 'index.ts'),
|
|
214
|
+
`export * from './types';\nexport * from './api';`
|
|
215
|
+
)
|
|
216
|
+
} else {
|
|
217
|
+
// Flat structure - create a single index file
|
|
218
|
+
const indexContent = Object.keys(groups)
|
|
219
|
+
.map((group) => {
|
|
220
|
+
const exports = []
|
|
221
|
+
if (groups[group].types.size > 0) {
|
|
222
|
+
exports.push(`export * from './${toCamelCase(group)}Types';`)
|
|
223
|
+
}
|
|
224
|
+
if (groups[group].apis.size > 0) {
|
|
225
|
+
exports.push(`export * from './${toCamelCase(group)}Api';`)
|
|
226
|
+
}
|
|
227
|
+
return exports.join('\n')
|
|
228
|
+
})
|
|
229
|
+
.join('\n')
|
|
230
|
+
|
|
231
|
+
await formatAndWriteCode(
|
|
232
|
+
path.join(bagelinkDir, 'index.ts'),
|
|
233
|
+
indexContent
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log('Client code successfully split into organized files!')
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extracts common code that should be shared across all API files
|
|
242
|
+
*/
|
|
243
|
+
function extractCommonCode(apiContent: string): { commonCode: string, axiosInstance: string } {
|
|
244
|
+
// Use the consistent parsing function with range tracking
|
|
245
|
+
const ast = parseTypeScript(apiContent)
|
|
246
|
+
|
|
247
|
+
const imports: string[] = []
|
|
248
|
+
const axiosDefinition: string[] = []
|
|
249
|
+
const utilityTypes: string[] = []
|
|
250
|
+
|
|
251
|
+
for (const node of ast.body) {
|
|
252
|
+
// Safely access range information with proper null checks
|
|
253
|
+
if (!node.range) {
|
|
254
|
+
console.warn(`Node missing range information: ${node.type}`)
|
|
255
|
+
continue
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Get imports
|
|
259
|
+
if (node.type === 'ImportDeclaration') {
|
|
260
|
+
imports.push(apiContent.substring(node.range[0], node.range[1]))
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Get axios setup
|
|
264
|
+
if (node.type === 'ExportNamedDeclaration'
|
|
265
|
+
&& node.declaration?.type === 'VariableDeclaration'
|
|
266
|
+
&& node.declaration.declarations.some(d => d.id.type === 'Identifier' && d.id.name === 'axios')) {
|
|
267
|
+
axiosDefinition.push(apiContent.substring(node.range[0], node.range[1]))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Get utility interfaces like UploadOptions
|
|
271
|
+
if (node.type === 'ExportNamedDeclaration'
|
|
272
|
+
&& node.declaration?.type === 'TSInterfaceDeclaration') {
|
|
273
|
+
utilityTypes.push(apiContent.substring(node.range[0], node.range[1]))
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const commonCode = [
|
|
278
|
+
...imports,
|
|
279
|
+
...utilityTypes
|
|
280
|
+
].join('\n\n')
|
|
281
|
+
|
|
282
|
+
const axiosInstance = [
|
|
283
|
+
...imports.filter(i => i.includes('axios')),
|
|
284
|
+
...axiosDefinition
|
|
285
|
+
].join('\n\n')
|
|
286
|
+
|
|
287
|
+
return { commonCode, axiosInstance }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Organizes type definitions into logical groups
|
|
292
|
+
*/
|
|
293
|
+
function organizeTypes(typesContent: string, groupingStrategy: GroupingStrategy): Record<string, GroupInfo> {
|
|
294
|
+
const ast = parseTypeScript(typesContent)
|
|
295
|
+
const groups: Record<string, GroupInfo> = {}
|
|
296
|
+
|
|
297
|
+
for (const node of ast.body) {
|
|
298
|
+
// Skip nodes without range info
|
|
299
|
+
if (!node.range) continue
|
|
300
|
+
|
|
301
|
+
if (node.type === 'ExportNamedDeclaration'
|
|
302
|
+
&& (node.declaration?.type === 'TSTypeAliasDeclaration'
|
|
303
|
+
|| node.declaration?.type === 'TSInterfaceDeclaration')) {
|
|
304
|
+
const typeName = node.declaration.id.name
|
|
305
|
+
const group = groupingStrategy.getGroupName(typeName)
|
|
306
|
+
|
|
307
|
+
if (!groups[group]) {
|
|
308
|
+
groups[group] = {
|
|
309
|
+
types: new Set(),
|
|
310
|
+
apis: new Set(),
|
|
311
|
+
dependencies: new Set()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
groups[group].types.add(typeName)
|
|
316
|
+
|
|
317
|
+
// Collect dependencies for this type
|
|
318
|
+
const dependencies = findTypeDependencies(node, typesContent)
|
|
319
|
+
for (const dep of dependencies) {
|
|
320
|
+
groups[group].dependencies.add(dep)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return groups
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Organizes API functions into logical groups with enhanced type dependency detection
|
|
330
|
+
*/
|
|
331
|
+
function organizeApiFunctions(apiContent: string, groupingStrategy: GroupingStrategy): Record<string, GroupInfo> {
|
|
332
|
+
const ast = parseTypeScript(apiContent)
|
|
333
|
+
const groups: Record<string, GroupInfo> = {}
|
|
334
|
+
|
|
335
|
+
// First pass: identify top-level exports
|
|
336
|
+
for (const node of ast.body) {
|
|
337
|
+
if (!node.range) continue
|
|
338
|
+
|
|
339
|
+
// Handle direct function exports
|
|
340
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
|
|
341
|
+
for (const declarator of node.declaration.declarations) {
|
|
342
|
+
if (declarator.id.type === 'Identifier') {
|
|
343
|
+
const functionName = declarator.id.name
|
|
344
|
+
|
|
345
|
+
// Skip utility exports
|
|
346
|
+
if (['axios', 'UploadOptions', 'baseURL', 'endpoints', 'schemas'].includes(functionName)) {
|
|
347
|
+
continue
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if this is a nested API object (like `auth: {...}` or `dataExports: {...}`)
|
|
351
|
+
if (declarator.init?.type === 'ObjectExpression') {
|
|
352
|
+
// For object groups, use the object name as the group name directly
|
|
353
|
+
// rather than applying the grouping strategy
|
|
354
|
+
const groupName = functionName
|
|
355
|
+
|
|
356
|
+
if (!groups[groupName]) {
|
|
357
|
+
groups[groupName] = {
|
|
358
|
+
types: new Set(),
|
|
359
|
+
apis: new Set(),
|
|
360
|
+
dependencies: new Set()
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
groups[groupName].apis.add(functionName)
|
|
365
|
+
|
|
366
|
+
// Extract types from the entire object
|
|
367
|
+
const objectCode = apiContent.substring(node.range[0], node.range[1])
|
|
368
|
+
const typeRefs = findTypeReferences(objectCode)
|
|
369
|
+
|
|
370
|
+
// Add AxiosResponse explicitly as a dependency
|
|
371
|
+
typeRefs.push('AxiosResponse')
|
|
372
|
+
|
|
373
|
+
for (const typeRef of typeRefs) {
|
|
374
|
+
groups[groupName].dependencies.add(typeRef)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
continue
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Regular function export - use grouping strategy
|
|
381
|
+
const group = groupingStrategy.getGroupName(functionName)
|
|
382
|
+
|
|
383
|
+
if (!groups[group]) {
|
|
384
|
+
groups[group] = {
|
|
385
|
+
types: new Set(),
|
|
386
|
+
apis: new Set(),
|
|
387
|
+
dependencies: new Set()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
groups[group].apis.add(functionName)
|
|
392
|
+
|
|
393
|
+
// Find type dependencies in the function
|
|
394
|
+
const functionCode = apiContent.substring(node.range[0], node.range[1])
|
|
395
|
+
const typeRefs = findTypeReferences(functionCode)
|
|
396
|
+
|
|
397
|
+
for (const typeRef of typeRefs) {
|
|
398
|
+
groups[group].dependencies.add(typeRef)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return groups
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Pre-compiled regex patterns for type reference detection
|
|
410
|
+
* Defining these outside functions prevents unnecessary recompilation
|
|
411
|
+
*/
|
|
412
|
+
const TYPE_PATTERNS = {
|
|
413
|
+
// PascalCase type identifiers (handles leading underscores and interface patterns)
|
|
414
|
+
PASCAL_CASE_TYPE: /(?<!['"$_.])\b_?[A-Z]\w*\b(?!\s*:)/g,
|
|
415
|
+
|
|
416
|
+
// Type parameters in generics (e.g., Promise<UserResponse>)
|
|
417
|
+
GENERIC_TYPE_PARAMS: /<(_?[A-Z]\w*(?:, *_?[A-Z]\w*)*)>/g,
|
|
418
|
+
|
|
419
|
+
// Return type annotations including AxiosResponse pattern
|
|
420
|
+
RETURN_TYPE_ANNOTATION: /\): *Promise<(?:AxiosResponse<)?(_?[A-Z]\w*)(?:\[\])?>+/g,
|
|
421
|
+
|
|
422
|
+
// Parameter type annotations
|
|
423
|
+
PARAM_TYPE_ANNOTATION: /: *(_?[A-Z]\w*)\b/g,
|
|
424
|
+
|
|
425
|
+
// Types used in extends, implements, etc.
|
|
426
|
+
TYPE_RELATION: /(?:extends|implements|keyof|typeof|infer)\s+(_?[A-Z]\w*)/g
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Type reference matcher that validates if a string is likely a proper type name
|
|
431
|
+
*/
|
|
432
|
+
const TYPE_NAME_VALIDATOR = /^_?[A-Z]\w*/
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Regex for cleaning up type names by removing brackets and other syntax characters
|
|
436
|
+
*/
|
|
437
|
+
const TYPE_CLEANUP_PATTERN = /[<>[\]]/g
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Enhanced type reference finder with more robust pattern matching
|
|
441
|
+
* using pre-compiled regex patterns
|
|
442
|
+
*/
|
|
443
|
+
function findTypeReferences(code: string): string[] {
|
|
444
|
+
const matches = new Set<string>()
|
|
445
|
+
|
|
446
|
+
// Process each pattern with a clean approach to avoid regex stateful issues
|
|
447
|
+
Object.values(TYPE_PATTERNS).forEach((pattern) => {
|
|
448
|
+
// Create a fresh copy of the pattern to avoid stateful regex issues
|
|
449
|
+
const freshPattern = new RegExp(pattern.source, pattern.flags)
|
|
450
|
+
|
|
451
|
+
// Use matchAll for cleaner iteration over all matches
|
|
452
|
+
const allMatches = [...code.matchAll(freshPattern)]
|
|
453
|
+
|
|
454
|
+
for (const matchArray of allMatches) {
|
|
455
|
+
// Get the captured group if it exists, otherwise use the full match
|
|
456
|
+
const matchValue = matchArray[1] || matchArray[0]
|
|
457
|
+
|
|
458
|
+
// Process potential comma-separated type lists (for generics)
|
|
459
|
+
matchValue.split(',').forEach((type) => {
|
|
460
|
+
const trimmedType = type.trim().replace(TYPE_CLEANUP_PATTERN, '')
|
|
461
|
+
|
|
462
|
+
// Validate it's a proper type name using our validator
|
|
463
|
+
if (trimmedType && TYPE_NAME_VALIDATOR.test(trimmedType)) {
|
|
464
|
+
matches.add(trimmedType)
|
|
465
|
+
}
|
|
466
|
+
})
|
|
467
|
+
}
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
// Always include AxiosResponse for API files
|
|
471
|
+
matches.add('AxiosResponse')
|
|
472
|
+
|
|
473
|
+
return [...matches]
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Merges type and API group information
|
|
478
|
+
*/
|
|
479
|
+
function mergeGroupInfo(
|
|
480
|
+
typeGroups: Record<string, GroupInfo>,
|
|
481
|
+
apiGroups: Record<string, GroupInfo>
|
|
482
|
+
): Record<string, GroupInfo> {
|
|
483
|
+
const result: Record<string, GroupInfo> = {}
|
|
484
|
+
|
|
485
|
+
// Start with all type groups
|
|
486
|
+
for (const [group, info] of Object.entries(typeGroups)) {
|
|
487
|
+
result[group] = {
|
|
488
|
+
types: new Set(info.types),
|
|
489
|
+
apis: new Set(),
|
|
490
|
+
dependencies: new Set(info.dependencies)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Add or merge API groups
|
|
495
|
+
for (const [group, info] of Object.entries(apiGroups)) {
|
|
496
|
+
if (!result[group]) {
|
|
497
|
+
result[group] = {
|
|
498
|
+
types: new Set(),
|
|
499
|
+
apis: new Set(info.apis),
|
|
500
|
+
dependencies: new Set(info.dependencies)
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
for (const api of info.apis) {
|
|
504
|
+
result[group].apis.add(api)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const dep of info.dependencies) {
|
|
508
|
+
result[group].dependencies.add(dep)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return result
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export type ProgramStatement = ReturnType<typeof parse>['body'][number]
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Finds type dependencies in a type declaration
|
|
520
|
+
*/
|
|
521
|
+
function findTypeDependencies(node: ProgramStatement, _typesContent: string): string[] {
|
|
522
|
+
const dependencies = new Set<string>()
|
|
523
|
+
|
|
524
|
+
// Type guard functions for type safety
|
|
525
|
+
function isTypeReference(node: unknown): node is TSESTree.TSTypeReference {
|
|
526
|
+
return node !== null && typeof node === 'object' && 'type' in node && node.type === AST_NODE_TYPES.TSTypeReference
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function isIdentifier(node: unknown): node is TSESTree.Identifier {
|
|
530
|
+
return node !== null && typeof node === 'object' && 'type' in node && node.type === AST_NODE_TYPES.Identifier
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Simple approach - extract all identifiers from type references
|
|
534
|
+
function visit(node: unknown) {
|
|
535
|
+
// Handle type references
|
|
536
|
+
if (isTypeReference(node)) {
|
|
537
|
+
if ('typeName' in node && isIdentifier(node.typeName)) {
|
|
538
|
+
dependencies.add(node.typeName.name)
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Recursively visit child nodes
|
|
543
|
+
if (node && typeof node === 'object') {
|
|
544
|
+
// Handle arrays
|
|
545
|
+
if (Array.isArray(node)) {
|
|
546
|
+
node.forEach((item) => { visit(item) })
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Skip certain properties
|
|
551
|
+
const skipProps = ['parent', 'range', 'loc']
|
|
552
|
+
|
|
553
|
+
// Visit each property
|
|
554
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
555
|
+
if (!skipProps.includes(key) && value && typeof value === 'object') {
|
|
556
|
+
visit(value)
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
visit(node)
|
|
563
|
+
return Array.from(dependencies)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Generates import statements for dependencies
|
|
568
|
+
*/
|
|
569
|
+
function getDependencyImports(
|
|
570
|
+
dependencies: Set<string>,
|
|
571
|
+
groups: Record<string, GroupInfo>,
|
|
572
|
+
_kind: 'types' | 'apis'
|
|
573
|
+
): Record<string, string[]> {
|
|
574
|
+
const imports: Record<string, string[]> = {}
|
|
575
|
+
|
|
576
|
+
// Find where each dependency is defined
|
|
577
|
+
for (const dep of dependencies) {
|
|
578
|
+
for (const [group, info] of Object.entries(groups)) {
|
|
579
|
+
if (info.types.has(dep)) {
|
|
580
|
+
if (!imports[group]) {
|
|
581
|
+
imports[group] = []
|
|
582
|
+
}
|
|
583
|
+
imports[group].push(dep)
|
|
584
|
+
break
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
return imports
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Generates a types file for a group
|
|
594
|
+
*/
|
|
595
|
+
function generateTypesFile(
|
|
596
|
+
group: string,
|
|
597
|
+
typeNames: Set<string>,
|
|
598
|
+
typesContent: string,
|
|
599
|
+
dependencies: Record<string, string[]>
|
|
600
|
+
): string {
|
|
601
|
+
const ast = parseTypeScript(typesContent)
|
|
602
|
+
|
|
603
|
+
// Generate imports for dependencies with consistent camelCase
|
|
604
|
+
const imports = Object.entries(dependencies)
|
|
605
|
+
.filter(([depGroup]) => depGroup !== group) // Don't import from self
|
|
606
|
+
.map(([depGroup, types]) => `import type { ${types.sort().join(', ')} } from './${toCamelCase(depGroup)}Types';`
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
// Extract type declarations
|
|
610
|
+
const declarations: string[] = []
|
|
611
|
+
for (const node of ast.body) {
|
|
612
|
+
// Skip nodes without range info
|
|
613
|
+
if (!node.range) continue
|
|
614
|
+
|
|
615
|
+
if (node.type === 'ExportNamedDeclaration'
|
|
616
|
+
&& (node.declaration?.type === 'TSTypeAliasDeclaration'
|
|
617
|
+
|| node.declaration?.type === 'TSInterfaceDeclaration')) {
|
|
618
|
+
const typeName = node.declaration.id.name
|
|
619
|
+
if (typeNames.has(typeName)) {
|
|
620
|
+
declarations.push(typesContent.substring(node.range[0], node.range[1]))
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return [
|
|
626
|
+
...imports,
|
|
627
|
+
'',
|
|
628
|
+
...declarations
|
|
629
|
+
].join('\n')
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Generates an API file for a group with improved handling of nested APIs
|
|
634
|
+
*/
|
|
635
|
+
function generateApiFile(
|
|
636
|
+
_group: string,
|
|
637
|
+
apiFunctionNames: Set<string>,
|
|
638
|
+
apiContent: string,
|
|
639
|
+
dependencies: Record<string, string[]>,
|
|
640
|
+
useDirectories: boolean
|
|
641
|
+
): string {
|
|
642
|
+
const ast = parseTypeScript(apiContent)
|
|
643
|
+
|
|
644
|
+
// Generate imports
|
|
645
|
+
const imports = [
|
|
646
|
+
useDirectories
|
|
647
|
+
? `import { axios } from './axios';`
|
|
648
|
+
: `import { axios } from './common';`,
|
|
649
|
+
`import type { AxiosResponse } from 'axios';`
|
|
650
|
+
]
|
|
651
|
+
|
|
652
|
+
// Add imports for the common utilities
|
|
653
|
+
if (useDirectories) {
|
|
654
|
+
imports.push(`import type { UploadOptions } from './common';`)
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Add imports for type dependencies with consistent camelCase
|
|
658
|
+
if (useDirectories) {
|
|
659
|
+
Object.entries(dependencies).forEach(([depGroup, types]) => {
|
|
660
|
+
// Filter out AxiosResponse as we're importing it directly
|
|
661
|
+
const filteredTypes = types.filter(t => t !== 'AxiosResponse')
|
|
662
|
+
if (filteredTypes.length > 0) {
|
|
663
|
+
imports.push(`import type { ${filteredTypes.sort().join(', ')} } from '../types/${toCamelCase(depGroup)}Types';`)
|
|
664
|
+
}
|
|
665
|
+
})
|
|
666
|
+
} else {
|
|
667
|
+
Object.entries(dependencies).forEach(([depGroup, types]) => {
|
|
668
|
+
// Filter out AxiosResponse as we're importing it directly
|
|
669
|
+
const filteredTypes = types.filter(t => t !== 'AxiosResponse')
|
|
670
|
+
if (filteredTypes.length > 0) {
|
|
671
|
+
imports.push(`import type { ${filteredTypes.sort().join(', ')} } from './${toCamelCase(depGroup)}Types';`)
|
|
672
|
+
}
|
|
673
|
+
})
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Extract API function declarations with their JSDoc comments
|
|
677
|
+
const declarations: string[] = []
|
|
678
|
+
|
|
679
|
+
for (const node of ast.body) {
|
|
680
|
+
if (!node.range) continue
|
|
681
|
+
|
|
682
|
+
// Handle export declarations
|
|
683
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration?.type === 'VariableDeclaration') {
|
|
684
|
+
for (const declarator of node.declaration.declarations) {
|
|
685
|
+
if (declarator.id.type === 'Identifier') {
|
|
686
|
+
const functionName = declarator.id.name
|
|
687
|
+
|
|
688
|
+
// Check if this function should be included
|
|
689
|
+
const shouldInclude = apiFunctionNames.has(functionName)
|
|
690
|
+
|
|
691
|
+
if (shouldInclude) {
|
|
692
|
+
// Get the range of the entire declaration including comments
|
|
693
|
+
// Find leading comments by using AST comment tokens instead of node properties
|
|
694
|
+
let startPos = node.range[0]
|
|
695
|
+
const comments = ast.comments || []
|
|
696
|
+
const nodeLeadingComments = comments.filter(
|
|
697
|
+
comment => comment.range[1] <= node.range[0]
|
|
698
|
+
&& !comments.some(c => comment.range[1] < c.range[1] && c.range[1] <= node.range[0])
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if (nodeLeadingComments.length > 0) {
|
|
702
|
+
startPos = nodeLeadingComments[0].range[0]
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
declarations.push(apiContent.substring(startPos, node.range[1]))
|
|
706
|
+
break
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
return [
|
|
714
|
+
...imports.filter(Boolean),
|
|
715
|
+
'',
|
|
716
|
+
...declarations
|
|
717
|
+
].join('\n')
|
|
718
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bagelink/sdk",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.3",
|
|
5
5
|
"description": "Bagel core sdk packages",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Neveh Allon",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"dist",
|
|
45
45
|
"src",
|
|
46
46
|
"bin/authClientCode.ts",
|
|
47
|
+
"bin/splitClientGen.ts",
|
|
47
48
|
"bin/utils.ts"
|
|
48
49
|
],
|
|
49
50
|
"publishConfig": {
|