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