openapi-remote-codegen 0.1.0

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.
@@ -0,0 +1,521 @@
1
+ import type { GeneratorConfig } from '../config.js';
2
+ import type { OperationInfo, ParameterInfo, ParsedSpec } from '../types.js';
3
+ import {
4
+ operationIdToFunctionName,
5
+ tagToFileName,
6
+ resolveInvalidation,
7
+ } from '../utils/naming.js';
8
+ import { getClientPropertyName } from '../utils/client-mapping.js';
9
+
10
+ export function generateRemoteFunctions(parsed: ParsedSpec, config: GeneratorConfig): Map<string, string> {
11
+ const fileContents = new Map<string, string>();
12
+
13
+ // Group operations by tag
14
+ const byTag = new Map<string, OperationInfo[]>();
15
+ for (const op of parsed.operations) {
16
+ const existing = byTag.get(op.tag) ?? [];
17
+ existing.push(op);
18
+ byTag.set(op.tag, existing);
19
+ }
20
+
21
+ // Generate file for each tag
22
+ for (const [tag, operations] of byTag) {
23
+ const fileName = `${tagToFileName(tag)}.generated.remote.ts`;
24
+ const content = generateTagFile(tag, operations, config);
25
+ fileContents.set(fileName, content);
26
+ }
27
+
28
+ // Generate barrel export (pass operations to detect name collisions)
29
+ const indexContent = generateIndexFile(byTag);
30
+ fileContents.set('index.ts', indexContent);
31
+
32
+ return fileContents;
33
+ }
34
+
35
+ function generateTagFile(tag: string, operations: OperationInfo[], config: GeneratorConfig): string {
36
+ const schemaImports = new Set<string>();
37
+ const typeImports = new Set<string>();
38
+ const enumImports = new Set<string>();
39
+
40
+ // Collect imports
41
+ for (const op of operations) {
42
+ if (op.requestBodySchema) {
43
+ schemaImports.add(op.requestBodySchema);
44
+ const typeName = op.requestBodySchema.replace(/Schema$/, '');
45
+ typeImports.add(typeName);
46
+ }
47
+ for (const param of op.parameters) {
48
+ if (param.enumName) {
49
+ enumImports.add(param.enumName);
50
+ }
51
+ }
52
+ }
53
+
54
+ const schemaImportLine = schemaImports.size > 0
55
+ ? `import { ${Array.from(schemaImports).join(', ')} } from '${config.imports.schemas}';\n`
56
+ : '';
57
+
58
+ // Build apiTypes import line with both value imports (enums) and type imports (DTOs)
59
+ let apiImportLine = '';
60
+ const valueImports = Array.from(enumImports);
61
+ const typeOnlyImports = Array.from(typeImports);
62
+ if (valueImports.length > 0 || typeOnlyImports.length > 0) {
63
+ const parts = [
64
+ ...valueImports,
65
+ ...typeOnlyImports.map(t => `type ${t}`),
66
+ ];
67
+ apiImportLine = `import { ${parts.join(', ')} } from '${config.imports.apiTypes}';\n`;
68
+ }
69
+
70
+ const functions = operations.map(op => generateFunction(op, tag, operations, config)).join('\n\n');
71
+
72
+ // Only import query/command/form if actually used
73
+ const hasQuery = operations.some(op => op.remoteType === 'query');
74
+ const hasCommand = operations.some(op => op.remoteType === 'command');
75
+ const hasForm = operations.some(op => op.remoteType === 'form');
76
+ const serverImports = ['getRequestEvent', ...(hasQuery ? ['query'] : []), ...(hasCommand ? ['command'] : []), ...(hasForm ? ['form'] : [])];
77
+
78
+ // Only import z from zod if any function actually uses Zod schema constructors
79
+ const usesZod = operations.some(op => {
80
+ const hasPathParams = op.parameters.some(p => p.in === 'path');
81
+ const hasQueryParams = op.parameters.some(p => p.in === 'query');
82
+ const hasInlineBody = op.inlineRequestBody !== undefined;
83
+ return hasPathParams || hasQueryParams || op.isArrayBody || hasInlineBody;
84
+ });
85
+ const zodImportLine = usesZod ? `import { z } from '${config.imports.zod}';\n` : '';
86
+ const kitImports = ['error', 'redirect', ...(hasForm ? ['invalid'] : [])];
87
+
88
+ return `// AUTO-GENERATED - DO NOT EDIT
89
+ // Generated by openapi-remote-codegen
90
+ // Source: openapi.json
91
+
92
+ import { ${serverImports.join(', ')} } from '${config.imports.server}';
93
+ import { ${kitImports.join(', ')} } from '${config.imports.kit}';
94
+ ${zodImportLine}${schemaImportLine}${apiImportLine}
95
+ ${functions}
96
+ `;
97
+ }
98
+
99
+ function generateFunction(op: OperationInfo, tag: string, allOpsInTag: OperationInfo[], config: GeneratorConfig): string {
100
+ const functionName = operationIdToFunctionName(op.operationId);
101
+ const clientProperty = op.clientPropertyName ?? getClientPropertyName(tag);
102
+ const methodName = getMethodName(op.operationId, tag);
103
+
104
+ if (op.remoteType === 'query') {
105
+ if (op.isBatch) {
106
+ return generateBatchQueryFunction(op, functionName, clientProperty, methodName, config);
107
+ }
108
+ return generateQueryFunction(op, functionName, clientProperty, methodName, config);
109
+ } else if (op.remoteType === 'form') {
110
+ return generateMutationFunction(op, functionName, clientProperty, methodName, tag, allOpsInTag, 'form', config);
111
+ } else {
112
+ return generateMutationFunction(op, functionName, clientProperty, methodName, tag, allOpsInTag, 'command', config);
113
+ }
114
+ }
115
+
116
+ function generateCatchBlock(config: GeneratorConfig, clientProperty: string, methodName: string, functionName: string): string {
117
+ const humanName = functionName.replace(/([A-Z])/g, ' $1').toLowerCase().trim();
118
+ return ` } catch (err) {
119
+ const status = (err as any)?.status;
120
+ if (status === 401) { ${config.errorHandling.on401}; }
121
+ if (status === 403) ${config.errorHandling.on403};
122
+ console.error('Error in ${clientProperty}.${methodName}:', err);
123
+ ${config.errorHandling.on500(humanName)};
124
+ }`;
125
+ }
126
+
127
+ function generateClientAccess(config: GeneratorConfig): string {
128
+ return ` const apiClient = ${config.clientAccess};`;
129
+ }
130
+
131
+ function generateQueryFunction(
132
+ op: OperationInfo,
133
+ functionName: string,
134
+ clientProperty: string,
135
+ methodName: string,
136
+ config: GeneratorConfig
137
+ ): string {
138
+ const { schemaArg, paramList, apiCallArgs } = buildParameterMapping(op);
139
+ const comment = op.summary ? `/** ${op.summary} */\n` : '';
140
+ const catchBlock = generateCatchBlock(config, clientProperty, methodName, functionName);
141
+ const clientAccess = generateClientAccess(config);
142
+
143
+ if (schemaArg) {
144
+ return `${comment}export const ${functionName} = query(${schemaArg}, async (${paramList}) => {
145
+ ${clientAccess}
146
+ try {
147
+ return await apiClient.${clientProperty}.${methodName}(${apiCallArgs});
148
+ ${catchBlock}
149
+ });`;
150
+ }
151
+
152
+ return `${comment}export const ${functionName} = query(async () => {
153
+ ${clientAccess}
154
+ try {
155
+ return await apiClient.${clientProperty}.${methodName}();
156
+ ${catchBlock}
157
+ });`;
158
+ }
159
+
160
+ function generateBatchQueryFunction(
161
+ op: OperationInfo,
162
+ functionName: string,
163
+ clientProperty: string,
164
+ methodName: string,
165
+ config: GeneratorConfig
166
+ ): string {
167
+ const { schemaArg, paramList, apiCallArgs } = buildParameterMapping(op);
168
+ const comment = op.summary ? `/** ${op.summary} */\n` : '';
169
+ const catchBlock = generateCatchBlock(config, clientProperty, methodName, functionName);
170
+ const clientAccess = generateClientAccess(config);
171
+
172
+ if (schemaArg) {
173
+ return `${comment}export const ${functionName} = query.batch(${schemaArg}, async (args) => {
174
+ ${clientAccess}
175
+ try {
176
+ const results = await Promise.all(args.map((${paramList}) => apiClient.${clientProperty}.${methodName}(${apiCallArgs})));
177
+ return (${paramList}, i) => results[i];
178
+ ${catchBlock}
179
+ });`;
180
+ }
181
+
182
+ // No-arg batch doesn't make sense - fall back to regular query
183
+ return `${comment}export const ${functionName} = query(async () => {
184
+ ${clientAccess}
185
+ try {
186
+ return await apiClient.${clientProperty}.${methodName}();
187
+ ${catchBlock}
188
+ });`;
189
+ }
190
+
191
+ function generateMutationFunction(
192
+ op: OperationInfo,
193
+ functionName: string,
194
+ clientProperty: string,
195
+ methodName: string,
196
+ tag: string,
197
+ allOpsInTag: OperationInfo[],
198
+ wrapperName: 'command' | 'form',
199
+ config: GeneratorConfig
200
+ ): string {
201
+ const { schemaArg, paramList, apiCallArgs } = buildParameterMapping(op);
202
+
203
+ // Build invalidation calls
204
+ const invalidations = op.invalidates.map(inv => {
205
+ const resolved = resolveInvalidation(inv, tag);
206
+ return resolved.functionName;
207
+ });
208
+
209
+ // Filter to only include functions that exist in the same file (same tag)
210
+ const localFunctionNames = new Set(allOpsInTag.map(o => operationIdToFunctionName(o.operationId)));
211
+ const localInvalidations = invalidations.filter(fn => localFunctionNames.has(fn));
212
+
213
+ // Build refresh calls
214
+ const commandPathParams = op.parameters.filter(p => p.in === 'path');
215
+ const commandPathParamNames = new Set(commandPathParams.map(p => p.name));
216
+
217
+ const refreshCallsArr = localInvalidations.map(fn => {
218
+ // Check if the target function actually takes a path parameter
219
+ const targetOp = allOpsInTag.find(o => operationIdToFunctionName(o.operationId) === fn);
220
+ const targetPathParams = targetOp?.parameters.filter(p => p.in === 'path') ?? [];
221
+
222
+ if (targetPathParams.length > 0) {
223
+ // Find matching path param names between the command and the target query
224
+ const matchingParams = targetPathParams.filter(p => commandPathParamNames.has(p.name));
225
+
226
+ if (matchingParams.length > 0) {
227
+ if (targetPathParams.length === 1) {
228
+ return `${fn}(${matchingParams[0].name}).refresh()`;
229
+ }
230
+ return `${fn}(${matchingParams[0].name}).refresh()`;
231
+ }
232
+
233
+ // Target requires path params the command can't provide - skip
234
+ return null;
235
+ }
236
+
237
+ return `${fn}(undefined).refresh()`;
238
+ }).filter((call): call is string => call !== null);
239
+
240
+ const refreshCalls = refreshCallsArr.length > 0
241
+ ? `\n await Promise.all([
242
+ ${refreshCallsArr.join(',\n ')}
243
+ ]);`
244
+ : '';
245
+
246
+ const comment = op.summary ? `/** ${op.summary} */\n` : '';
247
+ const catchBlock = generateCatchBlock(config, clientProperty, methodName, functionName);
248
+ const clientAccess = generateClientAccess(config);
249
+
250
+ // Handle void responses (204 No Content)
251
+ const apiCall = op.isVoidResponse
252
+ ? ` await apiClient.${clientProperty}.${methodName}(${apiCallArgs});${refreshCalls}
253
+ return { success: true };`
254
+ : ` const result = await apiClient.${clientProperty}.${methodName}(${apiCallArgs});${refreshCalls}
255
+ return result;`;
256
+
257
+ if (schemaArg) {
258
+ // form() requires StandardSchemaV1<RemoteFormInput, Record<string, any>> but z.fromJSONSchema()
259
+ // returns ZodType<unknown> - cast to any to bypass the constraint while keeping runtime validation
260
+ const schemaArgStr = wrapperName === 'form' ? `${schemaArg} as any` : schemaArg;
261
+ return `${comment}export const ${functionName} = ${wrapperName}(${schemaArgStr}, async (${paramList}) => {
262
+ ${clientAccess}
263
+ try {
264
+ ${apiCall}
265
+ ${catchBlock}
266
+ });`;
267
+ }
268
+
269
+ return `${comment}export const ${functionName} = ${wrapperName}(async () => {
270
+ ${clientAccess}
271
+ try {
272
+ ${apiCall}
273
+ ${catchBlock}
274
+ });`;
275
+ }
276
+
277
+ function buildParameterMapping(op: OperationInfo): {
278
+ schemaArg: string;
279
+ paramList: string;
280
+ apiCallArgs: string;
281
+ } {
282
+ const pathParams = op.parameters.filter(p => p.in === 'path');
283
+ const queryParams = op.parameters.filter(p => p.in === 'query');
284
+ const hasNamedBody = !!op.requestBodySchema;
285
+ const hasInlineBody = !!op.inlineRequestBody;
286
+ const hasBody = hasNamedBody || hasInlineBody;
287
+
288
+ // Single path param only (e.g., GET /trackers/{id})
289
+ if (pathParams.length === 1 && queryParams.length === 0 && !hasBody) {
290
+ const param = pathParams[0];
291
+ const zodType = param.enumName ? `z.enum(${param.enumName})` : 'z.string()';
292
+ return {
293
+ schemaArg: zodType,
294
+ paramList: param.name,
295
+ apiCallArgs: param.name,
296
+ };
297
+ }
298
+
299
+ // Multiple path params, no request body
300
+ if (pathParams.length > 1 && queryParams.length === 0 && !hasBody) {
301
+ const fields = pathParams.map(p => {
302
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : 'z.string()';
303
+ return `${p.name}: ${zodType}`;
304
+ });
305
+ const paramNames = pathParams.map(p => p.name);
306
+ return {
307
+ schemaArg: `z.object({ ${fields.join(', ')} })`,
308
+ paramList: `{ ${paramNames.join(', ')} }`,
309
+ apiCallArgs: paramNames.join(', '),
310
+ };
311
+ }
312
+
313
+ // Request body only (named schema)
314
+ if (pathParams.length === 0 && queryParams.length === 0 && hasNamedBody) {
315
+ const typeName = op.requestBodySchema!.replace(/Schema$/, '');
316
+ const bodyOptional = !op.requestBodyRequired;
317
+ if (op.isArrayBody) {
318
+ return {
319
+ schemaArg: `z.array(${op.requestBodySchema})`,
320
+ paramList: 'request',
321
+ apiCallArgs: `request as ${typeName}[]`,
322
+ };
323
+ }
324
+ if (bodyOptional) {
325
+ return {
326
+ schemaArg: `${op.requestBodySchema}.optional()`,
327
+ paramList: 'request',
328
+ apiCallArgs: `(request ?? {}) as ${typeName}`,
329
+ };
330
+ }
331
+ return {
332
+ schemaArg: op.requestBodySchema!,
333
+ paramList: 'request',
334
+ apiCallArgs: `request as ${typeName}`,
335
+ };
336
+ }
337
+
338
+ // Inline request body only
339
+ if (pathParams.length === 0 && queryParams.length === 0 && hasInlineBody) {
340
+ const inline = op.inlineRequestBody!;
341
+ const bodyOptional = !op.requestBodyRequired;
342
+ const schemaArg = bodyOptional ? `${inline.zodSchema}.optional()` : inline.zodSchema;
343
+ const requestArg = bodyOptional
344
+ ? `(request ?? {}) as ${inline.tsType}`
345
+ : `request as ${inline.tsType}`;
346
+ return {
347
+ schemaArg,
348
+ paramList: 'request',
349
+ apiCallArgs: requestArg,
350
+ };
351
+ }
352
+
353
+ // Path param(s) + named request body
354
+ if (pathParams.length >= 1 && hasNamedBody) {
355
+ const typeName = op.requestBodySchema!.replace(/Schema$/, '');
356
+ const bodyOptional = !op.requestBodyRequired;
357
+ const pathFields = pathParams.map(p => {
358
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : 'z.string()';
359
+ return `${p.name}: ${zodType}`;
360
+ });
361
+ const requestField = bodyOptional
362
+ ? `request: ${op.requestBodySchema}.optional()`
363
+ : `request: ${op.requestBodySchema}`;
364
+ const requestArg = bodyOptional
365
+ ? `(request ?? {}) as ${typeName}`
366
+ : `request as ${typeName}`;
367
+ const paramNames = pathParams.map(p => p.name);
368
+ return {
369
+ schemaArg: `z.object({ ${pathFields.join(', ')}, ${requestField} })`,
370
+ paramList: `{ ${paramNames.join(', ')}, request }`,
371
+ apiCallArgs: `${paramNames.join(', ')}, ${requestArg}`,
372
+ };
373
+ }
374
+
375
+ // Path param(s) + inline request body
376
+ if (pathParams.length >= 1 && hasInlineBody) {
377
+ const inline = op.inlineRequestBody!;
378
+ const bodyOptional = !op.requestBodyRequired;
379
+ const pathFields = pathParams.map(p => {
380
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : 'z.string()';
381
+ return `${p.name}: ${zodType}`;
382
+ });
383
+ const requestField = bodyOptional
384
+ ? `request: ${inline.zodSchema}.optional()`
385
+ : `request: ${inline.zodSchema}`;
386
+ const requestArg = bodyOptional
387
+ ? `(request ?? {}) as ${inline.tsType}`
388
+ : `request as ${inline.tsType}`;
389
+ const paramNames = pathParams.map(p => p.name);
390
+ return {
391
+ schemaArg: `z.object({ ${pathFields.join(', ')}, ${requestField} })`,
392
+ paramList: `{ ${paramNames.join(', ')}, request }`,
393
+ apiCallArgs: `${paramNames.join(', ')}, ${requestArg}`,
394
+ };
395
+ }
396
+
397
+ // Path param(s) + query param(s), no body
398
+ if (pathParams.length >= 1 && queryParams.length > 0 && !hasBody) {
399
+ const pathFields = pathParams.map(p => {
400
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : 'z.string()';
401
+ return `${p.name}: ${zodType}`;
402
+ });
403
+ const queryFields = queryParams.map(p => {
404
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : getZodTypeForParam(p);
405
+ return `${p.name}: ${zodType}${p.required ? '' : '.optional()'}`;
406
+ });
407
+ const allFields = [...pathFields, ...queryFields];
408
+ const pathParamNames = pathParams.map(p => p.name);
409
+ const queryParamAccess = queryParams.map(p => `params.${p.name}`);
410
+ return {
411
+ schemaArg: `z.object({ ${allFields.join(', ')} })`,
412
+ paramList: 'params',
413
+ apiCallArgs: [...pathParamNames.map(n => `params.${n}`), ...queryParamAccess].join(', '),
414
+ };
415
+ }
416
+
417
+ // Query params only
418
+ if (queryParams.length > 0 && !hasBody) {
419
+ const fields = queryParams.map(p => {
420
+ const zodType = p.enumName ? `z.enum(${p.enumName})` : getZodTypeForParam(p);
421
+ return `${p.name}: ${zodType}${p.required ? '' : '.optional()'}`;
422
+ });
423
+
424
+ const schemaArg = `z.object({ ${fields.join(', ')} }).optional()`;
425
+ const paramList = 'params';
426
+ const apiCallArgs = queryParams.map(p => `params?.${p.name}`).join(', ');
427
+
428
+ return { schemaArg, paramList, apiCallArgs };
429
+ }
430
+
431
+ // No params
432
+ return {
433
+ schemaArg: '',
434
+ paramList: '',
435
+ apiCallArgs: '',
436
+ };
437
+ }
438
+
439
+ function getZodType(type: string): string {
440
+ switch (type) {
441
+ case 'boolean': return 'z.boolean()';
442
+ case 'number': return 'z.number()';
443
+ case 'integer': return 'z.number().int()';
444
+ case 'Date': return 'z.coerce.date()';
445
+ default: return 'z.string()';
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Get Zod type for a parameter, handling array types by wrapping the item type.
451
+ */
452
+ function getZodTypeForParam(param: ParameterInfo): string {
453
+ if (param.type === 'array') {
454
+ const itemZod = getZodType(param.itemType ?? 'string');
455
+ return `z.array(${itemZod})`;
456
+ }
457
+ return getZodType(param.type);
458
+ }
459
+
460
+ /**
461
+ * Get the method name from operationId.
462
+ * NSwag generates method names by removing the tag prefix.
463
+ * "Trackers_GetDefinitions" -> "getDefinitions"
464
+ */
465
+ function getMethodName(operationId: string, tag: string): string {
466
+ const parts = operationId.split('_');
467
+ const name = parts.length > 1 ? parts.slice(1).join('_') : parts[0];
468
+ return name.charAt(0).toLowerCase() + name.slice(1);
469
+ }
470
+
471
+ function generateIndexFile(byTag: Map<string, OperationInfo[]>): string {
472
+ // Build map of function name -> list of tags that export it
473
+ const nameToTags = new Map<string, string[]>();
474
+ for (const [tag, operations] of byTag) {
475
+ for (const op of operations) {
476
+ const fnName = operationIdToFunctionName(op.operationId);
477
+ const existing = nameToTags.get(fnName) ?? [];
478
+ existing.push(tag);
479
+ nameToTags.set(fnName, existing);
480
+ }
481
+ }
482
+
483
+ // Find names that appear in multiple tags (collisions)
484
+ const collidingNames = new Set<string>();
485
+ for (const [name, tags] of nameToTags) {
486
+ if (tags.length > 1) collidingNames.add(name);
487
+ }
488
+
489
+ // Find tags that have at least one colliding export
490
+ const tagsWithCollisions = new Set<string>();
491
+ for (const [name, tags] of nameToTags) {
492
+ if (tags.length > 1) {
493
+ for (const tag of tags) tagsWithCollisions.add(tag);
494
+ }
495
+ }
496
+
497
+ const lines: string[] = [];
498
+ for (const [tag, operations] of byTag) {
499
+ const fileName = tagToFileName(tag);
500
+ if (!tagsWithCollisions.has(tag)) {
501
+ // No collisions - safe to export *
502
+ lines.push(`export * from './${fileName}.generated.remote';`);
503
+ } else {
504
+ // Has collisions - use explicit named exports, skipping colliding names
505
+ const safeNames = operations
506
+ .map(op => operationIdToFunctionName(op.operationId))
507
+ .filter(name => !collidingNames.has(name));
508
+ if (safeNames.length > 0) {
509
+ lines.push(`export { ${safeNames.join(', ')} } from './${fileName}.generated.remote';`);
510
+ } else {
511
+ lines.push(`// './${fileName}.generated.remote' - all exports collide, import directly`);
512
+ }
513
+ }
514
+ }
515
+
516
+ return `// AUTO-GENERATED - DO NOT EDIT
517
+ // Generated by openapi-remote-codegen
518
+
519
+ ${lines.join('\n')}
520
+ `;
521
+ }
package/src/index.ts ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'fs';
3
+ import { resolve, dirname } from 'path';
4
+ import { pathToFileURL } from 'url';
5
+ import { resolveConfig, type UserConfig } from './config.js';
6
+ import { parseOpenApiSpec } from './parser.js';
7
+ import { generateRemoteFunctions } from './generators/remote-functions.js';
8
+ import { generateApiClient } from './generators/api-client.js';
9
+
10
+ async function loadConfig(): Promise<UserConfig> {
11
+ const configNames = [
12
+ 'remote-codegen.config.ts',
13
+ 'remote-codegen.config.js',
14
+ 'remote-codegen.config.mjs',
15
+ ];
16
+
17
+ // Check for --config flag
18
+ const configFlagIndex = process.argv.indexOf('--config');
19
+ if (configFlagIndex !== -1 && process.argv[configFlagIndex + 1]) {
20
+ const configPath = resolve(process.cwd(), process.argv[configFlagIndex + 1]);
21
+ if (!existsSync(configPath)) {
22
+ console.error(`Config file not found: ${configPath}`);
23
+ process.exit(1);
24
+ }
25
+ const mod = await import(pathToFileURL(configPath).href);
26
+ return mod.default ?? mod;
27
+ }
28
+
29
+ // Auto-discover
30
+ for (const name of configNames) {
31
+ const configPath = resolve(process.cwd(), name);
32
+ if (existsSync(configPath)) {
33
+ console.log(`Using config: ${name}`);
34
+ const mod = await import(pathToFileURL(configPath).href);
35
+ return mod.default ?? mod;
36
+ }
37
+ }
38
+
39
+ return {};
40
+ }
41
+
42
+ async function main() {
43
+ const userConfig = await loadConfig();
44
+ const config = resolveConfig(userConfig);
45
+
46
+ console.log('OpenAPI Remote Function Generator');
47
+ console.log('=================================\n');
48
+
49
+ const specPath = resolve(process.cwd(), config.openApiPath);
50
+ if (!existsSync(specPath)) {
51
+ console.error(`OpenAPI spec not found: ${specPath}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ const spec = JSON.parse(readFileSync(specPath, 'utf-8'));
56
+ console.log(`Loaded OpenAPI spec: ${spec.info?.title ?? 'unknown'} v${spec.info?.version ?? 'unknown'}`);
57
+
58
+ const parsed = parseOpenApiSpec(spec);
59
+ console.log(`Found ${parsed.operations.length} annotated operations across ${parsed.tags.length} tags.\n`);
60
+
61
+ if (parsed.operations.length === 0) {
62
+ console.log('No operations with x-remote-* extensions found.');
63
+ console.log('Add x-remote-type to your OpenAPI operations, or use the companion attributes package.');
64
+ process.exit(0);
65
+ }
66
+
67
+ // Generate remote functions
68
+ console.log('Generating remote functions...');
69
+ const remoteFunctions = generateRemoteFunctions(parsed, config);
70
+
71
+ const remoteFunctionsDir = resolve(config.outputDir, config.remoteFunctionsOutput);
72
+ mkdirSync(remoteFunctionsDir, { recursive: true });
73
+
74
+ // Clean up stale generated files
75
+ const generatedFileNames = new Set(remoteFunctions.keys());
76
+ if (existsSync(remoteFunctionsDir)) {
77
+ for (const existing of readdirSync(remoteFunctionsDir)) {
78
+ if (existing.endsWith('.generated.remote.ts') && !generatedFileNames.has(existing)) {
79
+ unlinkSync(resolve(remoteFunctionsDir, existing));
80
+ console.log(` Removed stale: ${config.remoteFunctionsOutput}/${existing}`);
81
+ }
82
+ }
83
+ }
84
+
85
+ for (const [fileName, content] of remoteFunctions) {
86
+ const filePath = resolve(remoteFunctionsDir, fileName);
87
+ writeFileSync(filePath, content, 'utf-8');
88
+ console.log(` Generated: ${config.remoteFunctionsOutput}/${fileName}`);
89
+ }
90
+
91
+ // Generate ApiClient
92
+ console.log('\nGenerating ApiClient...');
93
+ const apiClientContent = generateApiClient(spec, config);
94
+ const apiClientPath = resolve(config.outputDir, config.apiClientOutput);
95
+ mkdirSync(dirname(apiClientPath), { recursive: true });
96
+ writeFileSync(apiClientPath, apiClientContent, 'utf-8');
97
+ console.log(` Generated: ${config.apiClientOutput}`);
98
+
99
+ console.log('\nDone!');
100
+ }
101
+
102
+ main().catch((err) => {
103
+ console.error(err);
104
+ process.exit(1);
105
+ });