fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #3175 from hey-api/feat/sdk-examples

feat: SDK examples

authored by

Lubos and committed by
GitHub
469253a3 711452e9

+905 -119
+9
.changeset/honest-cases-build.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + **plugin(@hey-api/sdk)**: add `examples` option 6 + 7 + The SDK plugin can generate ready-to-use code examples for each operation, showing how to call the SDK methods with proper parameters and setup. 8 + 9 + Learn how to generate examples on the [SDK plugin](https://heyapi.dev/openapi-ts/plugins/sdk#code-examples) page.
+5
.changeset/real-radios-sit.md
··· 1 + --- 2 + '@hey-api/codegen-core': patch 3 + --- 4 + 5 + **project**: expose `.plan()` method
+9
.changeset/rich-phones-melt.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + **output**: add `source` option 6 + 7 + Source is a copy of the input specification used to generate your output. It can be used to power documentation tools or to persist a stable snapshot alongside your generated files. 8 + 9 + Learn how to use the source on the [Output](https://heyapi.dev/openapi-ts/configuration/output#source) page.
+67 -31
dev/openapi-ts.config.ts
··· 46 46 // openapi: '3.1.0', 47 47 // paths: {}, 48 48 // }, 49 - path: path.resolve( 50 - getSpecsPath(), 51 - // '2.0.x', 52 - // '3.0.x', 53 - '3.1.x', 54 - // 'circular.yaml', 55 - // 'dutchie.json', 56 - // 'enum-names-values.yaml', 57 - // 'full.yaml', 58 - // 'integer-formats.yaml', 59 - // 'invalid', 60 - // 'object-property-names.yaml', 61 - // 'openai.yaml', 62 - 'opencode.yaml', 63 - // 'pagination-ref.yaml', 64 - // 'schema-const.yaml', 65 - // 'sdk-instance.yaml', 66 - // 'sdk-method-class-conflict.yaml', 67 - // 'sdk-nested-classes.yaml', 68 - // 'sdk-nested-conflict.yaml', 69 - // 'string-with-format.yaml', 70 - // 'transformers.json', 71 - // 'transformers-recursive.json', 72 - // 'type-format.yaml', 73 - // 'validators.yaml', 74 - // 'validators-circular-ref.json', 75 - // 'validators-circular-ref-2.yaml', 76 - // 'zoom-video-sdk.json', 77 - ), 49 + // path: path.resolve( 50 + // getSpecsPath(), 51 + // // '2.0.x', 52 + // // '3.0.x', 53 + // '3.1.x', 54 + // // 'circular.yaml', 55 + // // 'dutchie.json', 56 + // // 'enum-names-values.yaml', 57 + // // 'full.yaml', 58 + // // 'integer-formats.yaml', 59 + // // 'invalid', 60 + // // 'object-property-names.yaml', 61 + // // 'openai.yaml', 62 + // 'opencode.yaml', 63 + // // 'pagination-ref.yaml', 64 + // // 'schema-const.yaml', 65 + // // 'sdk-instance.yaml', 66 + // // 'sdk-method-class-conflict.yaml', 67 + // // 'sdk-nested-classes.yaml', 68 + // // 'sdk-nested-conflict.yaml', 69 + // // 'string-with-format.yaml', 70 + // // 'transformers.json', 71 + // // 'transformers-recursive.json', 72 + // // 'type-format.yaml', 73 + // // 'validators.yaml', 74 + // // 'validators-circular-ref.json', 75 + // // 'validators-circular-ref-2.yaml', 76 + // // 'zoom-video-sdk.json', 77 + // ), 78 78 // path: 'https://get.heyapi.dev/hey-api/backend?branch=main&version=1.0.0', 79 79 // path: 'http://localhost:4000/', 80 80 // path: 'http://localhost:8000/openapi.json', 81 81 // path: 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/2caffd88277a4e27c95dcefc7e3b6a63a3b03297-v2-2023-11-15.json', 82 - // path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 82 + path: 'https://raw.githubusercontent.com/swagger-api/swagger-petstore/master/src/main/resources/openapi.yaml', 83 83 // watch: { 84 84 // enabled: true, 85 85 // interval: 500, ··· 136 136 return 'valibot'; 137 137 } 138 138 return; 139 + }, 140 + source: { 141 + // callback(source) { 142 + // console.log('Source generated, length:', source.length); 143 + // }, 144 + // enabled: false, 145 + // extension: 'yaml', 146 + // fileName: 'spec', 147 + // path: null, 148 + // serialize(input) { 149 + // return JSON.stringify(input, null, 0); 150 + // }, 139 151 }, 140 152 // tsConfigPath: path.resolve( 141 153 // __dirname, ··· 292 304 { 293 305 auth: false, 294 306 // client: false, 307 + examples: { 308 + // enabled: false, 309 + importKind: 'default', 310 + importName: 'CatStore', 311 + importSetup: ({ $, node }) => 312 + $.new( 313 + node.name, 314 + $.object().pretty().prop('apiKey', $.literal('YOUR_API_KEY')), 315 + ), 316 + // language: 'TypeScript', 317 + moduleName: '@petstore/client', 318 + payload(operation, ctx) { 319 + const { $ } = ctx; 320 + if ( 321 + operation.path === '/pet/{petId}' || 322 + operation.path === '/pet' 323 + ) { 324 + return $.object().pretty().prop('petId', $.literal(1234)); 325 + } 326 + return; 327 + }, 328 + setupName: 'client', 329 + // transform: (example) => example.replace(/\({}\)/, '({\n ...\n})'), 330 + }, 295 331 // getSignature: ({ fields, signature, operation }) => { 296 332 // // ... 297 333 // fields.unwrap('path') ··· 304 340 // // casing: 'snake_case', 305 341 // name: 'OpencodeClient', 306 342 // }, 307 - containerName: 'OpencodeClient', 343 + containerName: 'PetStore', 308 344 // nesting(operation) { 309 345 // if (operation.path === '/pet/{petId}' || operation.path === '/pet') { 310 346 // return ['pet', operation.operationId?.replace(/Pet/, '') || operation.method.toLocaleLowerCase()];
+46
docs/openapi-ts/configuration/output.md
··· 168 168 import foo from './foo.js'; 169 169 ``` 170 170 171 + ## Source 172 + 173 + Source is a copy of the input specification used to generate your output. It can be used to power documentation tools or to persist a stable snapshot alongside your generated files. 174 + 175 + Enabling the `source` option with `true` creates a `source.json` file in your output folder. 176 + 177 + ```js 178 + export default { 179 + input: 'hey-api/backend', // sign up at app.heyapi.dev 180 + output: { 181 + path: 'src/client', 182 + source: true, // [!code ++] 183 + }, 184 + }; 185 + ``` 186 + 187 + You can customize the file name and location using `fileName` and `path`. For example, this configuration will create an `openapi.json` file inside `src/client/source` directory. 188 + 189 + ```js 190 + export default { 191 + input: 'hey-api/backend', // sign up at app.heyapi.dev 192 + output: { 193 + path: 'src/client', 194 + source: { 195 + fileName: 'openapi', // [!code ++] 196 + path: './source', // [!code ++] 197 + }, 198 + }, 199 + }; 200 + ``` 201 + 202 + To use the source without writing it to disk, you can provide a `callback` function. This is useful for logging or integrating with external systems. 203 + 204 + ```js 205 + export default { 206 + input: 'hey-api/backend', // sign up at app.heyapi.dev 207 + output: { 208 + path: 'src/client', 209 + source: { 210 + callback: (source) => console.log(source), // [!code ++] 211 + path: null, // [!code ++] 212 + }, 213 + }, 214 + }; 215 + ``` 216 + 171 217 ## Format 172 218 173 219 To format your output folder contents, set `format` to a valid formatter.
+236 -3
docs/openapi-ts/plugins/sdk.md
··· 9 9 10 10 The SDK plugin generates a high-level, ergonomic API layer on top of the low-level HTTP client. 11 11 12 - It exposes typed functions or classes for each operation, with built-in auth handling and optional request and response validation. 12 + It exposes typed functions or methods for each operation, with built-in auth handling, configurable request and response validation, and ready-to-use code examples. 13 13 14 14 ## Features 15 15 16 16 - high-level SDK layer on top of the HTTP client 17 - - typed functions or classes per operation 17 + - typed functions or methods per operation 18 18 - built-in authentication handling 19 - - optional request and response validation 19 + - request and response validation 20 + - ready-to-use code examples 20 21 21 22 ## Installation 22 23 ··· 259 260 input: 'hey-api/backend', // sign up at app.heyapi.dev 260 261 output: 'src/client', 261 262 plugins: [ 263 + // ...other plugins 262 264 { 263 265 name: '@hey-api/sdk', 264 266 validator: true, // or 'valibot' // [!code ++] ··· 282 284 input: 'hey-api/backend', // sign up at app.heyapi.dev 283 285 output: 'src/client', 284 286 plugins: [ 287 + // ...other plugins 285 288 { 286 289 name: '@hey-api/sdk', 287 290 validator: { ··· 297 300 input: 'hey-api/backend', // sign up at app.heyapi.dev 298 301 output: 'src/client', 299 302 plugins: [ 303 + // ...other plugins 300 304 { 301 305 name: '@hey-api/sdk', 302 306 validator: { ··· 310 314 ::: 311 315 312 316 Learn more about available validators on the [Validators](/openapi-ts/validators) page. 317 + 318 + ## Code Examples 319 + 320 + The SDK plugin can generate ready-to-use code examples for each operation, showing how to call the SDK methods with proper parameters and setup. 321 + 322 + Examples are not generated by default, but you can enable and customize them through the `examples` option. With the default settings, an example might look like this. 323 + 324 + ::: code-group 325 + 326 + ```ts [example] 327 + import { PetStore } from 'your-package'; 328 + 329 + await new PetStore().addPet(); 330 + ``` 331 + 332 + ```js [config] 333 + export default { 334 + input: 'hey-api/backend', // sign up at app.heyapi.dev 335 + output: 'src/client', 336 + plugins: [ 337 + // ...other plugins 338 + { 339 + examples: true, // [!code ++] 340 + name: '@hey-api/sdk', 341 + operations: { 342 + containerName: 'PetStore', 343 + strategy: 'single', 344 + }, 345 + }, 346 + ], 347 + }; 348 + ``` 349 + 350 + ::: 351 + 352 + ### Module and Setup 353 + 354 + To make examples more practical, configure `moduleName` to specify the package from which users import your SDK. 355 + 356 + Next, set `setupName` to indicate how users should instantiate the SDK, typically only once per application. 357 + 358 + ::: code-group 359 + 360 + ```ts [example] 361 + import { PetStore } from '@petstore/client'; // [!code ++] 362 + 363 + const client = new PetStore(); // [!code ++] 364 + 365 + await client.addPet(); 366 + ``` 367 + 368 + ```js [config] 369 + export default { 370 + input: 'hey-api/backend', // sign up at app.heyapi.dev 371 + output: 'src/client', 372 + plugins: [ 373 + // ...other plugins 374 + { 375 + examples: { 376 + moduleName: '@petstore/client', // [!code ++] 377 + setupName: 'client', // [!code ++] 378 + }, 379 + name: '@hey-api/sdk', 380 + operations: { 381 + containerName: 'PetStore', 382 + strategy: 'single', 383 + }, 384 + }, 385 + ], 386 + }; 387 + ``` 388 + 389 + ::: 390 + 391 + ### Initialization 392 + 393 + Often, your SDK needs to be instantiated with an API key or other configuration. In examples, `importSetup` lets you control how the SDK is initialized. 394 + 395 + ::: code-group 396 + 397 + <!-- prettier-ignore-start --> 398 + ```ts [example] 399 + import { PetStore } from '@petstore/client'; 400 + 401 + const client = new PetStore({ // [!code ++] 402 + apiKey: 'YOUR_API_KEY', // [!code ++] 403 + }); // [!code ++] 404 + 405 + await client.addPet(); 406 + ``` 407 + <!-- prettier-ignore-end --> 408 + <!-- prettier-ignore-start --> 409 + ```js [config] 410 + export default { 411 + input: 'hey-api/backend', // sign up at app.heyapi.dev 412 + output: 'src/client', 413 + plugins: [ 414 + // ...other plugins 415 + { 416 + examples: { 417 + importSetup: ({ $, node }) => // [!code ++] 418 + $.new( // [!code ++] 419 + node.name, // [!code ++] 420 + $.object() // [!code ++] 421 + .pretty() // [!code ++] 422 + .prop('apiKey', $.literal('YOUR_API_KEY')), // [!code ++] 423 + ), // [!code ++] 424 + moduleName: '@petstore/client', 425 + setupName: 'client', 426 + }, 427 + name: '@hey-api/sdk', 428 + operations: { 429 + containerName: 'PetStore', 430 + strategy: 'single', 431 + }, 432 + }, 433 + ], 434 + }; 435 + ``` 436 + <!-- prettier-ignore-end --> 437 + 438 + ::: 439 + 440 + ### Import Style 441 + 442 + If you re-export the generated SDK from your own module, you can adjust `importName` and `importKind` to match your actual import style. 443 + 444 + ::: code-group 445 + 446 + <!-- prettier-ignore-start --> 447 + ```ts [example] 448 + import CatStore from '@petstore/client'; // [!code ++] 449 + 450 + const client = new CatStore({ // [!code ++] 451 + apiKey: 'YOUR_API_KEY', 452 + }); 453 + 454 + await client.addPet(); 455 + ``` 456 + <!-- prettier-ignore-end --> 457 + 458 + ```js [config] 459 + export default { 460 + input: 'hey-api/backend', // sign up at app.heyapi.dev 461 + output: 'src/client', 462 + plugins: [ 463 + // ...other plugins 464 + { 465 + examples: { 466 + importKind: 'default', // [!code ++] 467 + importName: 'CatStore', // [!code ++] 468 + importSetup: ({ $, node }) => 469 + $(node.name).call( 470 + $.object().pretty().prop('apiKey', $.literal('YOUR_API_KEY')), 471 + ), 472 + moduleName: '@petstore/client', 473 + setupName: 'client', 474 + }, 475 + name: '@hey-api/sdk', 476 + operations: { 477 + containerName: 'PetStore', 478 + strategy: 'single', 479 + }, 480 + }, 481 + ], 482 + }; 483 + ``` 484 + 485 + ::: 486 + 487 + ### Payload 488 + 489 + You can customize the example request using the `payload` option. Requests can also be customized selectively. For example, we can provide a default payload only for the `addPet()` method. 490 + 491 + ::: code-group 492 + 493 + <!-- prettier-ignore-start --> 494 + ```ts [example] 495 + import CatStore from '@petstore/client'; 496 + 497 + const client = new CatStore({ 498 + apiKey: 'YOUR_API_KEY', 499 + }); 500 + 501 + await client.addPet({ // [!code ++] 502 + petId: 1234, // [!code ++] 503 + }); // [!code ++] 504 + ``` 505 + <!-- prettier-ignore-end --> 506 + <!-- prettier-ignore-start --> 507 + ```js [config] 508 + export default { 509 + input: 'hey-api/backend', // sign up at app.heyapi.dev 510 + output: 'src/client', 511 + plugins: [ 512 + // ...other plugins 513 + { 514 + examples: { 515 + importKind: 'default', 516 + importName: 'CatStore', 517 + importSetup: ({ $, node }) => 518 + $(node.name).call( 519 + $.object().pretty().prop('apiKey', $.literal('YOUR_API_KEY')), 520 + ), 521 + moduleName: '@petstore/client', 522 + payload(operation, ctx) { // [!code ++] 523 + const { $ } = ctx; // [!code ++] 524 + if (operation.path === '/pet/{petId}' || operation.path === '/pet') { // [!code ++] 525 + return $.object().pretty().prop('petId', $.literal(1234)); // [!code ++] 526 + } // [!code ++] 527 + }, // [!code ++] 528 + setupName: 'client', 529 + }, 530 + name: '@hey-api/sdk', 531 + operations: { 532 + containerName: 'PetStore', 533 + strategy: 'single', 534 + }, 535 + }, 536 + ], 537 + }; 538 + ``` 539 + <!-- prettier-ignore-end --> 540 + 541 + ::: 542 + 543 + ### Display 544 + 545 + Enabling examples does not produce visible output on its own. Examples are written into the source specification and can be consumed by documentation tools such as [Mintlify](https://kutt.it/6vrYy9) or [Scalar](https://kutt.it/skQUVd). To persist that specification, enable [Source](/openapi-ts/configuration/output#source) generation. 313 546 314 547 ## API 315 548
+9 -1
packages/codegen-core/src/project/project.ts
··· 15 15 import type { IProject } from './types'; 16 16 17 17 export class Project implements IProject { 18 + private _isPlanned = false; 19 + 18 20 readonly files: FileRegistry; 19 21 readonly nodes = new NodeRegistry(); 20 22 readonly symbols = new SymbolRegistry(); ··· 57 59 this.root = path.resolve(args.root).replace(/[/\\]+$/, ''); 58 60 } 59 61 60 - render(meta?: IProjectRenderMeta): ReadonlyArray<IOutput> { 62 + plan(meta?: IProjectRenderMeta): void { 63 + if (this._isPlanned) return; 61 64 new Planner(this).plan(meta); 65 + this._isPlanned = true; 66 + } 67 + 68 + render(meta?: IProjectRenderMeta): ReadonlyArray<IOutput> { 69 + if (!this._isPlanned) this.plan(meta); 62 70 const files: Array<IOutput> = []; 63 71 for (const file of this.files.registered()) { 64 72 if (!file.external && file.finalPath && file.renderer) {
+7
packages/codegen-core/src/project/types.d.ts
··· 37 37 /** Centralized node registry for the project. */ 38 38 readonly nodes: INodeRegistry; 39 39 /** 40 + * Finalizes the project structure, resolving nodes, symbols, and dependencies. 41 + * 42 + * @param meta Arbitrary metadata. 43 + * @returns void 44 + */ 45 + plan(meta?: IProjectRenderMeta): void; 46 + /** 40 47 * Produces output representations for all files in the project. 41 48 * 42 49 * @param meta Arbitrary metadata.
+5 -3
packages/openapi-ts/src/config/output.ts packages/openapi-ts/src/config/output/config.ts
··· 3 3 import { findTsConfigPath, loadTsConfig } from '~/generate/tsConfig'; 4 4 import type { Config, UserConfig } from '~/types/config'; 5 5 6 - import { valueToObject } from './utils/config'; 6 + import { valueToObject } from '../utils/config'; 7 + import { resolveSource } from './source/config'; 7 8 8 - export const getOutput = (userConfig: UserConfig): Config['output'] => { 9 + export function getOutput(userConfig: UserConfig): Config['output'] { 9 10 if (userConfig.output instanceof Array) { 10 11 throw new Error( 11 12 'Unexpected array of outputs in user configuration. This should have been expanded already.', ··· 64 65 ) { 65 66 output.importFileExtension = `.${output.importFileExtension}`; 66 67 } 68 + output.source = resolveSource(output); 67 69 return output; 68 - }; 70 + }
+3
packages/openapi-ts/src/config/output/index.ts
··· 1 + export { getOutput } from './config'; 2 + export { postprocessOutput } from './postprocess'; 3 + export type { Formatters, Linters, Output, UserOutput } from './types';
+24
packages/openapi-ts/src/config/output/source/config.ts
··· 1 + import { valueToObject } from '../../utils/config'; 2 + import type { UserOutput } from '../types'; 3 + import type { SourceConfig } from './types'; 4 + 5 + export function resolveSource(config: UserOutput): SourceConfig { 6 + const source = valueToObject({ 7 + defaultValue: { 8 + enabled: Boolean(config.source), 9 + extension: 'json', 10 + fileName: 'source', 11 + serialize: (input) => JSON.stringify(input, null, 2), 12 + }, 13 + mappers: { 14 + boolean: (enabled) => ({ enabled }), 15 + }, 16 + value: config.source, 17 + }); 18 + if (source.path === undefined || source.path === true) { 19 + source.path = ''; 20 + } else if (source.path === false) { 21 + source.path = null; 22 + } 23 + return source as SourceConfig; 24 + }
+90
packages/openapi-ts/src/config/output/source/types.d.ts
··· 1 + import type { MaybePromise } from '~/types/utils'; 2 + 3 + // TODO: json-schema-ref-parser needs to expose source extension so 4 + // we can default to it 5 + type SourceExtension = 'json'; 6 + // type SourceExtension = 'json' | 'yaml'; 7 + 8 + export interface UserSourceConfig { 9 + /** 10 + * Callback invoked with the serialized source string. 11 + * 12 + * Runs after the `serialize` function. 13 + * 14 + * @example 15 + * source => console.log(source) 16 + */ 17 + callback?: (source: string) => MaybePromise<void>; 18 + /** 19 + * Whether the source should be generated at all. 20 + * 21 + * When `false`, no source file is created or processed. 22 + * 23 + * @default true 24 + */ 25 + enabled?: boolean; 26 + // * Only `'json'` and `'yaml'` are allowed. 27 + /** 28 + * File extension for the source file. 29 + * 30 + * @default 'json' 31 + */ 32 + extension?: SourceExtension; 33 + /** 34 + * Base file name for the source file. 35 + * 36 + * The extension from `extension` will be appended automatically. 37 + * 38 + * @default 'source' 39 + */ 40 + fileName?: string; 41 + /** 42 + * Target location for the source file. 43 + * 44 + * - `true` / `undefined` → write to output root (default) 45 + * - `string` → relative to output root or absolute path 46 + * - `false` / `null` → do not write 47 + * 48 + * @default true 49 + */ 50 + path?: boolean | string | null; 51 + /** 52 + * Function to serialize the input object into a string. 53 + * 54 + * @default 55 + * JSON.stringify(input, null, 2) 56 + * 57 + * @example 58 + * input => JSON.stringify(input, null, 0) // minified 59 + */ 60 + serialize?: (input: Record<string, any>) => MaybePromise<string>; 61 + } 62 + 63 + export interface SourceConfig { 64 + /** 65 + * Callback invoked with the serialized source string. 66 + * 67 + * Runs after the `serialize` function. 68 + */ 69 + callback?: (source: string) => MaybePromise<void>; 70 + /** 71 + * Whether the source should be generated at all. 72 + */ 73 + enabled: boolean; 74 + /** 75 + * File extension for the source file. 76 + */ 77 + extension: SourceExtension; 78 + /** 79 + * Base file name for the source file. 80 + */ 81 + fileName: string; 82 + /** 83 + * Target location for the source file. 84 + */ 85 + path: string | null; 86 + /** 87 + * Function to serialize the input object into a string. 88 + */ 89 + serialize: (input: Record<string, any>) => MaybePromise<string>; 90 + }
+2 -2
packages/openapi-ts/src/createClient.ts
··· 3 3 import { $RefParser } from '@hey-api/json-schema-ref-parser'; 4 4 import colors from 'ansi-colors'; 5 5 6 + import { postprocessOutput } from '~/config/output'; 6 7 import { generateOutput } from '~/generate/output'; 7 8 import { getSpec } from '~/getSpec'; 8 9 import type { Context } from '~/ir/context'; 9 10 import { parseOpenApiSpec } from '~/openApi'; 10 11 import { buildGraph } from '~/openApi/shared/utils/graph'; 11 12 import { patchOpenApiSpec } from '~/openApi/shared/utils/patch'; 12 - import { processOutput } from '~/processOutput'; 13 13 import type { Config } from '~/types/config'; 14 14 import type { Input } from '~/types/input'; 15 15 import type { WatchValues } from '~/types/types'; ··· 327 327 328 328 const eventPostprocess = logger.timeEvent('postprocess'); 329 329 if (!config.dryRun) { 330 - processOutput({ config }); 330 + postprocessOutput(config.output); 331 331 332 332 if (config.logs.level !== 'silent') { 333 333 const outputPath = process.env.INIT_CWD
+29
packages/openapi-ts/src/generate/output.ts
··· 2 2 import path from 'node:path'; 3 3 4 4 import type { Context } from '~/ir/context'; 5 + import { IntentContext } from '~/ir/intents'; 5 6 import { getClientPlugin } from '~/plugins/@hey-api/client-core/utils'; 6 7 7 8 import { generateClientBundle } from './client'; ··· 38 39 await plugin.run(); 39 40 } 40 41 42 + context.gen.plan(); 43 + 44 + const ctx = new IntentContext(context.spec); 45 + for (const intent of context.intents) { 46 + await intent.run(ctx); 47 + } 48 + 41 49 for (const file of context.gen.render()) { 42 50 const filePath = path.resolve(outputPath, file.path); 43 51 const dir = path.dirname(filePath); 44 52 if (!context.config.dryRun) { 45 53 fs.mkdirSync(dir, { recursive: true }); 46 54 fs.writeFileSync(filePath, file.content, { encoding: 'utf8' }); 55 + } 56 + } 57 + 58 + const { source } = context.config.output; 59 + if (source.enabled) { 60 + const sourcePath = 61 + source.path === null ? undefined : path.resolve(outputPath, source.path); 62 + if (!context.config.dryRun && sourcePath && sourcePath !== outputPath) { 63 + fs.mkdirSync(sourcePath, { recursive: true }); 64 + } 65 + const serialized = await source.serialize(context.spec); 66 + // TODO: handle yaml (convert before writing) 67 + if (!context.config.dryRun && sourcePath) { 68 + fs.writeFileSync( 69 + path.resolve(sourcePath, `${source.fileName}.${source.extension}`), 70 + serialized, 71 + { encoding: 'utf8' }, 72 + ); 73 + } 74 + if (source.callback) { 75 + await source.callback(serialized); 47 76 } 48 77 } 49 78 };
+1 -1
packages/openapi-ts/src/generate/tsConfig.ts
··· 4 4 5 5 import ts from 'typescript'; 6 6 7 - import type { UserOutput } from '~/types/output'; 7 + import type { UserOutput } from '~/config/output'; 8 8 9 9 const __filename = fileURLToPath(import.meta.url); 10 10 const __dirname = path.dirname(__filename);
+5
packages/openapi-ts/src/ir/context.ts
··· 12 12 import { applyNaming } from '~/utils/naming'; 13 13 import { resolveRef } from '~/utils/ref'; 14 14 15 + import type { ExampleIntent } from './intents'; 15 16 import type { IR } from './types'; 16 17 17 18 export class Context<Spec extends Record<string, any> = any> { ··· 28 29 * The dependency graph built from the intermediate representation. 29 30 */ 30 31 graph: Graph | undefined; 32 + /** 33 + * Intents declared by plugins. 34 + */ 35 + intents: Array<ExampleIntent> = []; 31 36 /** 32 37 * Intermediate representation model obtained from `spec`. 33 38 */
+32
packages/openapi-ts/src/ir/intents.ts
··· 1 + import type { CodeSampleObject } from '~/openApi/shared/types'; 2 + import type { MaybePromise } from '~/types/utils'; 3 + 4 + import type { IR } from './types'; 5 + 6 + export interface ExampleIntent { 7 + run(ctx: IntentContext): MaybePromise<void>; 8 + } 9 + 10 + export class IntentContext<Spec extends Record<string, any> = any> { 11 + private spec: Spec; 12 + 13 + constructor(spec: Spec) { 14 + this.spec = spec; 15 + } 16 + 17 + private getOperation( 18 + path: string, 19 + method: string, 20 + ): Record<string, any> | undefined { 21 + const paths = (this.spec as any).paths; 22 + if (!paths) return; 23 + return paths[path]?.[method]; 24 + } 25 + 26 + setExample(operation: IR.OperationObject, example: CodeSampleObject): void { 27 + const source = this.getOperation(operation.path, operation.method); 28 + if (!source) return; 29 + source['x-codeSamples'] ||= []; 30 + source['x-codeSamples'].push(example); 31 + } 32 + }
+1 -1
packages/openapi-ts/src/openApi/2.0.x/types/json-schema-draft-4.d.ts
··· 1 - import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; 1 + import type { EnumExtensions } from '~/openApi/shared/types'; 2 2 3 3 export interface JsonSchemaDraft4 extends EnumExtensions { 4 4 /**
+5 -1
packages/openapi-ts/src/openApi/2.0.x/types/spec.d.ts
··· 1 - import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; 1 + import type { CodeSampleObject, EnumExtensions } from '~/openApi/shared/types'; 2 2 3 3 import type { JsonSchemaDraft4 } from './json-schema-draft-4'; 4 4 import type { OpenApiV2_0_X_Nullable_Extensions } from './openapi-spec-extensions'; ··· 584 584 * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. 585 585 */ 586 586 tags?: ReadonlyArray<string>; 587 + /** 588 + * A list of code samples associated with an operation. 589 + */ 590 + 'x-codeSamples'?: ReadonlyArray<CodeSampleObject>; 587 591 } 588 592 589 593 /**
+5 -1
packages/openapi-ts/src/openApi/3.0.x/types/spec.d.ts
··· 1 - import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; 1 + import type { CodeSampleObject, EnumExtensions } from '~/openApi/shared/types'; 2 2 3 3 /** 4 4 * OpenAPI Specification Extensions. ··· 509 509 * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. 510 510 */ 511 511 tags?: ReadonlyArray<string>; 512 + /** 513 + * A list of code samples associated with an operation. 514 + */ 515 + 'x-codeSamples'?: ReadonlyArray<CodeSampleObject>; 512 516 } 513 517 514 518 /**
+1 -1
packages/openapi-ts/src/openApi/3.1.x/types/json-schema-draft-2020-12.d.ts
··· 1 - import type { EnumExtensions } from '~/openApi/shared/types/openapi-spec-extensions'; 1 + import type { EnumExtensions } from '~/openApi/shared/types'; 2 2 3 3 import type { MaybeArray } from '../../../types/utils'; 4 4 import type { SpecificationExtensions } from './spec';
+6
packages/openapi-ts/src/openApi/3.1.x/types/spec.d.ts
··· 1 + import type { CodeSampleObject } from '~/openApi/shared/types'; 2 + 1 3 import type { JsonSchemaDraft2020_12 } from './json-schema-draft-2020-12'; 2 4 3 5 /** ··· 1103 1105 * A list of tags for API documentation control. Tags can be used for logical grouping of operations by resources or any other qualifier. 1104 1106 */ 1105 1107 tags?: ReadonlyArray<string>; 1108 + /** 1109 + * A list of code samples associated with an operation. 1110 + */ 1111 + 'x-codeSamples'?: ReadonlyArray<CodeSampleObject>; 1106 1112 } 1107 1113 1108 1114 /**
+5
packages/openapi-ts/src/openApi/shared/types/index.ts
··· 1 + export type { 2 + CodeSampleObject, 3 + EnumExtensions, 4 + LinguistLanguages, 5 + } from './openapi-spec-extensions';
+44
packages/openapi-ts/src/openApi/shared/types/openapi-spec-extensions.d.ts
··· 1 + export type LinguistLanguages = 2 + | 'C' 3 + | 'C#' 4 + | 'C++' 5 + | 'CoffeeScript' 6 + | 'CSS' 7 + | 'Dart' 8 + | 'DM' 9 + | 'Elixir' 10 + | 'Go' 11 + | 'Groovy' 12 + | 'HTML' 13 + | 'Java' 14 + | 'JavaScript' 15 + | 'Kotlin' 16 + | 'Objective-C' 17 + | 'Perl' 18 + | 'PHP' 19 + | 'PowerShell' 20 + | 'Python' 21 + | 'Ruby' 22 + | 'Rust' 23 + | 'Scala' 24 + | 'Shell' 25 + | 'Swift' 26 + | 'TypeScript'; 27 + 28 + export interface CodeSampleObject { 29 + /** 30 + * Code sample label, for example `Node` or `Python2.7`. 31 + * 32 + * @default `lang` value 33 + */ 34 + label?: string; 35 + /** 36 + * **REQUIRED**. Code sample language. Can be one of the automatically supported languages or any other language identifier of your choice (for custom code samples). 37 + */ 38 + lang: LinguistLanguages; 39 + /** 40 + * **REQUIRED**. Code sample source code, or a `$ref` to the file containing the code sample. 41 + */ 42 + source: string; 43 + } 44 + 1 45 export interface EnumExtensions { 2 46 /** 3 47 * `x-enum-descriptions` are {@link https://stackoverflow.com/a/66471626 supported} by OpenAPI Generator.
+2
packages/openapi-ts/src/plugins/@hey-api/sdk/config.ts
··· 1 1 import { definePluginConfig } from '~/plugins/shared/utils/config'; 2 2 3 + import { resolveExamples } from './examples'; 3 4 import { resolveOperations } from './operations'; 4 5 import { handler } from './plugin'; 5 6 import type { HeyApiSdkPlugin } from './types'; ··· 71 72 plugin.config.validator.response = false; 72 73 } 73 74 75 + plugin.config.examples = resolveExamples(plugin.config, context); 74 76 plugin.config.operations = resolveOperations(plugin.config, context); 75 77 }, 76 78 };
+22
packages/openapi-ts/src/plugins/@hey-api/sdk/examples/config.ts
··· 1 + import type { PluginContext } from '~/plugins/types'; 2 + 3 + import type { UserConfig } from '../types'; 4 + import type { ExamplesConfig } from './types'; 5 + 6 + type Config = Omit<UserConfig, 'name'>; 7 + 8 + export function resolveExamples( 9 + config: Config, 10 + context: PluginContext, 11 + ): ExamplesConfig { 12 + return context.valueToObject({ 13 + defaultValue: { 14 + enabled: Boolean(config.examples), 15 + language: 'JavaScript', 16 + }, 17 + mappers: { 18 + boolean: (enabled) => ({ enabled }), 19 + }, 20 + value: config.examples, 21 + }) as ExamplesConfig; 22 + }
+2
packages/openapi-ts/src/plugins/@hey-api/sdk/examples/index.ts
··· 1 + export { resolveExamples } from './config'; 2 + export type { ExamplesConfig, UserExamplesConfig } from './types';
+70
packages/openapi-ts/src/plugins/@hey-api/sdk/examples/types.d.ts
··· 1 + import type { IR } from '~/ir/types'; 2 + import type { LinguistLanguages } from '~/openApi/shared/types'; 3 + import type { CallArgs, DollarTsDsl, ExampleOptions } from '~/ts-dsl'; 4 + import type { MaybeFunc } from '~/types/utils'; 5 + 6 + export interface UserExamplesConfig extends ExampleOptions { 7 + /** 8 + * Whether to generate SDK code examples. 9 + * 10 + * @default true 11 + */ 12 + enabled?: boolean; 13 + /** 14 + * The programming language for the generated examples. 15 + * 16 + * This is used to display the language label in code blocks in 17 + * documentation UIs. 18 + * 19 + * @default 'JavaScript' 20 + */ 21 + language?: LinguistLanguages; 22 + /** 23 + * Example request payload. 24 + */ 25 + payload?: MaybeFunc< 26 + ( 27 + operation: IR.OperationObject, 28 + ctx: DollarTsDsl, 29 + ) => CallArgs | CallArgs[number] 30 + >; 31 + /** 32 + * Transform the generated example string. 33 + * 34 + * @param example The generated example string. 35 + * @param operation The operation the example was generated for. 36 + * @returns The final example string. 37 + */ 38 + transform?: (example: string, operation: IR.OperationObject) => string; 39 + } 40 + 41 + export interface ExamplesConfig extends ExampleOptions { 42 + /** 43 + * Whether to generate SDK code examples. 44 + */ 45 + enabled: boolean; 46 + /** 47 + * The programming language for the generated examples. 48 + * 49 + * This is used to display the language label in code blocks in 50 + * documentation UIs. 51 + */ 52 + language: LinguistLanguages; 53 + /** 54 + * Example request payload. 55 + */ 56 + payload?: MaybeFunc< 57 + ( 58 + operation: IR.OperationObject, 59 + ctx: DollarTsDsl, 60 + ) => CallArgs | CallArgs[number] 61 + >; 62 + /** 63 + * Transform the generated example string. 64 + * 65 + * @param example The generated example string. 66 + * @param operation The operation the example was generated for. 67 + * @returns The final example string. 68 + */ 69 + transform?: (example: string, operation: IR.OperationObject) => string; 70 + }
+15
packages/openapi-ts/src/plugins/@hey-api/sdk/types.d.ts
··· 3 3 import type { PluginClientNames, PluginValidatorNames } from '~/plugins/types'; 4 4 import type { NameTransformer } from '~/utils/naming'; 5 5 6 + import type { ExamplesConfig, UserExamplesConfig } from './examples'; 6 7 import type { OperationsConfig, UserOperationsConfig } from './operations'; 7 8 8 9 export type UserConfig = Plugin.Name<'@hey-api/sdk'> & ··· 27 28 * @default true 28 29 */ 29 30 client?: PluginClientNames | boolean; 31 + /** 32 + * Generate code examples for SDK operations and attach them to the 33 + * input source (e.g. via `x-codeSamples`). 34 + * 35 + * Set to `false` to disable example generation entirely, or provide an 36 + * object for fine-grained control over the output and post-processing. 37 + * 38 + * @default false 39 + */ 40 + examples?: boolean | UserExamplesConfig; 30 41 /** 31 42 * Should the exports from the generated files be re-exported in the index 32 43 * barrel file? ··· 209 220 * @default true 210 221 */ 211 222 client: PluginClientNames | false; 223 + /** 224 + * Configuration for generating SDK code examples. 225 + */ 226 + examples: ExamplesConfig; 212 227 /** 213 228 * Should the exports from the generated files be re-exported in the index 214 229 * barrel file?
+44 -15
packages/openapi-ts/src/plugins/@hey-api/sdk/v1/node.ts
··· 12 12 createOperationComment, 13 13 isOperationOptionsRequired, 14 14 } from '~/plugins/shared/utils/operation'; 15 - import { $ } from '~/ts-dsl'; 15 + import { $, ctx } from '~/ts-dsl'; 16 16 import { applyNaming, toCase } from '~/utils/naming'; 17 17 18 18 import { createClientClass, createRegistryClass } from '../shared/class'; ··· 238 238 ); 239 239 } 240 240 241 + function exampleIntent( 242 + node: ReturnType<typeof $.method | typeof $.var>, 243 + operation: IR.OperationObject, 244 + plugin: HeyApiSdkPlugin['Instance'], 245 + ): void { 246 + const config = plugin.config.examples; 247 + if (!config.enabled) return; 248 + plugin.intent({ 249 + async run(context) { 250 + const { payload } = config; 251 + let example = ctx.example(node, { 252 + ...config, 253 + payload: (ctx) => 254 + typeof payload === 'function' ? payload(operation, ctx) : payload, 255 + }); 256 + if (config.transform) { 257 + example = await config.transform(example, operation); 258 + } 259 + if (example) { 260 + context.setExample(operation, { 261 + lang: config.language, 262 + source: example, 263 + }); 264 + } 265 + }, 266 + }); 267 + } 268 + 241 269 function implementFn< 242 270 T extends ReturnType<typeof $.func | typeof $.method>, 243 271 >(args: { ··· 322 350 ); 323 351 node = attachComment({ node, operation }); 324 352 nodes.push(node); 353 + exampleIntent(node, operation, plugin); 325 354 } 326 355 return { nodes }; 327 356 } ··· 345 374 // TODO: object 346 375 } else { 347 376 if (index > 0 || node.hasBody) node.newline(); 348 - node.do( 349 - implementFn({ 350 - node: $.method(createFnSymbol(plugin, item), (m) => 351 - attachComment({ 352 - node: m, 353 - operation, 354 - }) 355 - .public() 356 - .static(!isAngularClient && !isInstance(plugin)), 357 - ), 358 - operation, 359 - plugin, 360 - }), 361 - ); 377 + const method = implementFn({ 378 + node: $.method(createFnSymbol(plugin, item), (m) => 379 + attachComment({ 380 + node: m, 381 + operation, 382 + }) 383 + .public() 384 + .static(!isAngularClient && !isInstance(plugin)), 385 + ), 386 + operation, 387 + plugin, 388 + }); 389 + node.do(method); 390 + exampleIntent(method, operation, plugin); 362 391 } 363 392 index += 1; 364 393 }
+11
packages/openapi-ts/src/plugins/shared/utils/instance.ts
··· 20 20 matchIrPointerToGroup, 21 21 preferGroups, 22 22 } from '~/ir/graph'; 23 + import type { ExampleIntent } from '~/ir/intents'; 23 24 import type { IR } from '~/ir/types'; 24 25 import type { OpenApi } from '~/openApi/types'; 25 26 import type { Hooks } from '~/parser/types/hooks'; ··· 306 307 this.isOperationKind(operation, 'query'), 307 308 }, 308 309 }; 310 + 311 + /** 312 + * Registers an intent in the context's intent list. 313 + * 314 + * @param intent The intent to be registered. 315 + * @returns void 316 + */ 317 + intent(intent: ExampleIntent): void { 318 + this.context.intents.push(intent); 319 + } 309 320 310 321 isSymbolRegistered(identifier: SymbolIdentifier): boolean { 311 322 return this.gen.symbols.isRegistered(identifier);
+8 -9
packages/openapi-ts/src/processOutput.ts packages/openapi-ts/src/config/output/postprocess.ts
··· 1 1 import { sync } from 'cross-spawn'; 2 2 3 - import type { Config } from '~/types/config'; 4 - import type { Formatters, Linters } from '~/types/output'; 3 + import type { Formatters, Linters, Output } from './types'; 5 4 6 5 type OutputProcessor = { 7 6 args: (path: string) => ReadonlyArray<string>; ··· 52 51 }, 53 52 }; 54 53 55 - export const processOutput = ({ config }: { config: Config }) => { 56 - if (config.output.lint) { 57 - const module = linters[config.output.lint]; 54 + export const postprocessOutput = (config: Output): void => { 55 + if (config.lint) { 56 + const module = linters[config.lint]; 58 57 console.log(`✨ Running ${module.name}`); 59 - sync(module.command, module.args(config.output.path)); 58 + sync(module.command, module.args(config.path)); 60 59 } 61 60 62 - if (config.output.format) { 63 - const module = formatters[config.output.format]; 61 + if (config.format) { 62 + const module = formatters[config.format]; 64 63 console.log(`✨ Running ${module.name}`); 65 - sync(module.command, module.args(config.output.path)); 64 + sync(module.command, module.args(config.path)); 66 65 } 67 66 };
+18 -17
packages/openapi-ts/src/ts-dsl/base.ts
··· 71 71 } 72 72 readonly '~brand' = nodeBrand; 73 73 74 - /** Access patterns for this node. */ 75 - toAccessNode?( 76 - node: this, 77 - options: AccessOptions, 78 - ctx: { 79 - /** The full chain. */ 80 - chain: ReadonlyArray<TsDsl>; 81 - /** Position in the chain (0 = root). */ 82 - index: number; 83 - /** Is this the leaf node? */ 84 - isLeaf: boolean; 85 - /** Is this the root node? */ 86 - isRoot: boolean; 87 - /** Total length of the chain. */ 88 - length: number; 89 - }, 90 - ): TsDsl | undefined; 91 74 /** Branding property to identify the DSL class at runtime. */ 92 75 abstract readonly '~dsl': string & {}; 93 76 ··· 165 148 } 166 149 return this; 167 150 } 151 + 152 + /** Access patterns for this node. */ 153 + toAccessNode?( 154 + node: this, 155 + options: AccessOptions, 156 + ctx: { 157 + /** The full chain. */ 158 + chain: ReadonlyArray<TsDsl>; 159 + /** Position in the chain (0 = root). */ 160 + index: number; 161 + /** Is this the leaf node? */ 162 + isLeaf: boolean; 163 + /** Is this the root node? */ 164 + isRoot: boolean; 165 + /** Total length of the chain. */ 166 + length: number; 167 + }, 168 + ): TsDsl | undefined; 168 169 169 170 protected $maybeId<T extends string | ts.Expression>( 170 171 expr: T,
+3 -1
packages/openapi-ts/src/ts-dsl/index.ts
··· 357 357 358 358 export type { MaybeTsDsl, TypeTsDsl } from './base'; 359 359 export { TsDsl } from './base'; 360 - export { TsDslContext } from './utils/context'; 360 + export type { CallArgs } from './expr/call'; 361 + export type { ExampleOptions } from './utils/context'; 362 + export { ctx, TsDslContext } from './utils/context'; 361 363 export { keywords } from './utils/keywords'; 362 364 export { regexp } from './utils/regexp'; 363 365 export { TypeScriptRenderer } from './utils/render';
+2 -2
packages/openapi-ts/src/ts-dsl/layout/doc.ts
··· 4 4 import type { MaybeArray } from '../base'; 5 5 import { TsDsl } from '../base'; 6 6 import { IdTsDsl } from '../expr/id'; 7 - import { TsDslContext } from '../utils/context'; 7 + import type { TsDslContext } from '../utils/context'; 8 + import { ctx } from '../utils/context'; 8 9 9 10 type DocMaybeLazy<T> = ((ctx: TsDslContext) => T) | T; 10 11 export type DocFn = (d: DocTsDsl) => void; ··· 31 32 } 32 33 33 34 apply<T extends ts.Node>(node: T): T { 34 - const ctx = new TsDslContext(); 35 35 const lines = this._lines.reduce((lines: Array<string>, line: DocLines) => { 36 36 if (typeof line === 'function') line = line(ctx); 37 37 for (const l of typeof line === 'string' ? [line] : line) {
+2 -2
packages/openapi-ts/src/ts-dsl/layout/hint.ts
··· 4 4 import type { MaybeArray } from '../base'; 5 5 import { TsDsl } from '../base'; 6 6 import { IdTsDsl } from '../expr/id'; 7 - import { TsDslContext } from '../utils/context'; 7 + import type { TsDslContext } from '../utils/context'; 8 + import { ctx } from '../utils/context'; 8 9 9 10 type HintMaybeLazy<T> = ((ctx: TsDslContext) => T) | T; 10 11 export type HintFn = (d: HintTsDsl) => void; ··· 31 32 } 32 33 33 34 apply<T extends ts.Node>(node: T): T { 34 - const ctx = new TsDslContext(); 35 35 const lines = this._lines.reduce( 36 36 (lines: Array<string>, line: HintLines) => { 37 37 if (typeof line === 'function') line = line(ctx);
+2 -2
packages/openapi-ts/src/ts-dsl/layout/note.ts
··· 4 4 import type { MaybeArray } from '../base'; 5 5 import { TsDsl } from '../base'; 6 6 import { IdTsDsl } from '../expr/id'; 7 - import { TsDslContext } from '../utils/context'; 7 + import type { TsDslContext } from '../utils/context'; 8 + import { ctx } from '../utils/context'; 8 9 9 10 type NoteMaybeLazy<T> = ((ctx: TsDslContext) => T) | T; 10 11 export type NoteFn = (d: NoteTsDsl) => void; ··· 31 32 } 32 33 33 34 apply<T extends ts.Node>(node: T): T { 34 - const ctx = new TsDslContext(); 35 35 const lines = this._lines.reduce( 36 36 (lines: Array<string>, line: NoteLines) => { 37 37 if (typeof line === 'function') line = line(ctx);
+32 -21
packages/openapi-ts/src/ts-dsl/utils/context.ts
··· 2 2 import { isSymbol } from '@hey-api/codegen-core'; 3 3 import type ts from 'typescript'; 4 4 5 + import type { DollarTsDsl } from '~/ts-dsl'; 5 6 import { $, TypeScriptRenderer } from '~/ts-dsl'; 6 7 7 - import type { TsDsl } from '../base'; 8 + import type { MaybeFunc, TsDsl } from '../base'; 8 9 import type { CallArgs } from '../expr/call'; 9 10 10 11 export type NodeChain = ReadonlyArray<TsDsl>; ··· 28 29 /** Import name for the root node. */ 29 30 importName?: string; 30 31 /** Setup to run before calling the example. */ 31 - importSetup?: 32 - | TsDsl<ts.Expression> 33 - | ((imp: TsDsl<ts.Expression>) => TsDsl<ts.Expression>); 32 + importSetup?: MaybeFunc< 33 + ( 34 + ctx: DollarTsDsl & { 35 + /** The imported expression. */ 36 + node: TsDsl<ts.Expression>; 37 + }, 38 + ) => TsDsl<ts.Expression> 39 + >; 34 40 /** Module to import from. */ 35 - moduleName: string; 41 + moduleName?: string; 36 42 /** Example request payload. */ 37 - payload?: CallArgs | CallArgs[number]; 43 + payload?: MaybeFunc<(ctx: DollarTsDsl) => CallArgs | CallArgs[number]>; 38 44 /** Variable name for setup node. */ 39 45 setupName?: string; 40 46 } ··· 191 197 */ 192 198 example( 193 199 node: TsDsl, 194 - options: ExampleOptions | undefined, 200 + options?: ExampleOptions, 195 201 astOptions?: Parameters<typeof TypeScriptRenderer.astToString>[0], 196 202 ): string { 197 203 if (astOptions) { 198 204 return TypeScriptRenderer.astToString(astOptions); 199 205 } 200 206 201 - if (!options) { 202 - throw new Error('Example options are required.'); 203 - } 207 + options ||= {}; 204 208 205 209 const accessChain = getAccessChainForNode(node); 206 210 if (options.importName) { ··· 213 217 214 218 const setupNode = options.importSetup 215 219 ? typeof options.importSetup === 'function' 216 - ? options.importSetup(importNode) 220 + ? options.importSetup({ $, node: importNode }) 217 221 : options.importSetup 218 222 : (finalChain[0]! as TsDsl<ts.Expression>); 219 223 const setupName = options.setupName; 220 - const payload = 221 - options.payload instanceof Array 222 - ? options.payload 223 - : options.payload 224 - ? [options.payload] 225 - : []; 224 + let payload = 225 + typeof options.payload === 'function' 226 + ? options.payload({ $ }) 227 + : options.payload; 228 + payload = payload instanceof Array ? payload : payload ? [payload] : []; 226 229 227 230 let nodes: Array<TsDsl> = []; 228 231 if (setupName) { 229 232 nodes = [ 230 233 $.const(setupName).assign(setupNode), 231 - accessChainToNode([$(setupName), ...finalChain.slice(1)]).call( 232 - ...payload, 234 + $.await( 235 + accessChainToNode([$(setupName), ...finalChain.slice(1)]).call( 236 + ...payload, 237 + ), 233 238 ), 234 239 ]; 235 240 } else { 236 241 nodes = [ 237 - accessChainToNode([setupNode, ...finalChain.slice(1)]).call(...payload), 242 + $.await( 243 + accessChainToNode([setupNode, ...finalChain.slice(1)]).call( 244 + ...payload, 245 + ), 246 + ), 238 247 ]; 239 248 } 240 249 ··· 256 265 isTypeOnly: false, 257 266 kind: options.importKind ?? 'named', 258 267 localName: options.importKind !== 'named' ? localName : undefined, 259 - modulePath: options.moduleName, 268 + modulePath: options.moduleName ?? 'your-package', 260 269 }, 261 270 ], 262 271 ], ··· 265 274 }); 266 275 } 267 276 } 277 + 278 + export const ctx = new TsDslContext();
+3 -2
packages/openapi-ts/src/ts-dsl/utils/lazy.ts
··· 2 2 import type ts from 'typescript'; 3 3 4 4 import { TsDsl } from '../base'; 5 - import { TsDslContext } from './context'; 5 + import type { TsDslContext } from './context'; 6 + import { ctx } from './context'; 6 7 7 8 export type LazyThunk<T extends ts.Node> = (ctx: TsDslContext) => TsDsl<T>; 8 9 ··· 22 23 } 23 24 24 25 toResult(): TsDsl<T> { 25 - return this._thunk(new TsDslContext()); 26 + return this._thunk(ctx); 26 27 } 27 28 28 29 override toAst(): T {
+1 -1
packages/openapi-ts/src/types/config.d.ts
··· 1 + import type { Output, UserOutput } from '~/config/output'; 1 2 import type { Plugin } from '~/plugins'; 2 3 import type { PluginConfigMap } from '~/plugins/config'; 3 4 import type { PluginNames } from '~/plugins/types'; 4 5 5 6 import type { Input, UserInput, Watch } from './input'; 6 7 import type { Logs } from './logs'; 7 - import type { Output, UserOutput } from './output'; 8 8 import type { Parser, UserParser } from './parser'; 9 9 import type { MaybeArray } from './utils'; 10 10
+16 -1
packages/openapi-ts/src/types/output.d.ts packages/openapi-ts/src/config/output/types.d.ts
··· 4 4 } from '@hey-api/codegen-core'; 5 5 import type ts from 'typescript'; 6 6 7 + import type { MaybeArray, MaybeFunc } from '~/types/utils'; 7 8 import type { Casing, NameTransformer } from '~/utils/naming'; 8 9 9 - import type { MaybeArray, MaybeFunc } from './utils'; 10 + import type { SourceConfig, UserSourceConfig } from './source/types'; 10 11 11 12 export type Formatters = 'biome' | 'prettier'; 12 13 ··· 126 127 */ 127 128 resolveModuleName?: (moduleName: string) => string | undefined; 128 129 /** 130 + * Configuration for generating a copy of the input source used to produce this output. 131 + * 132 + * Set to `false` to skip generating the source, or `true` to use defaults. 133 + * 134 + * You can also provide a configuration object to further customize behavior. 135 + * 136 + * @default false 137 + */ 138 + source?: boolean | UserSourceConfig; 139 + /** 129 140 * Relative or absolute path to the tsconfig file we should use to 130 141 * generate the output. If a path to tsconfig file is not provided, we 131 142 * attempt to find one starting from the location of the ··· 219 230 * Optional function to transform module specifiers. 220 231 */ 221 232 resolveModuleName: ((moduleName: string) => string | undefined) | undefined; 233 + /** 234 + * Configuration for generating a copy of the input source used to produce this output. 235 + */ 236 + source: SourceConfig; 222 237 /** 223 238 * The parsed TypeScript configuration used to generate the output. 224 239 * If no `tsconfig` file path was provided or found, this will be `null`.
+6 -1
packages/openapi-ts/src/types/utils.d.ts
··· 19 19 /** 20 20 * Accepts a value, a function returning a value, or a function returning a promise of a value. 21 21 */ 22 - export type LazyOrAsync<T> = T | (() => T) | (() => Promise<T>); 22 + export type LazyOrAsync<T> = T | (() => MaybePromise<T>); 23 23 24 24 /** 25 25 * Accepts a value or a readonly array of values of type T. ··· 32 32 export type MaybeFunc<T extends (...args: Array<any>) => any> = 33 33 | T 34 34 | ReturnType<T>; 35 + 36 + /** 37 + * Accepts a value or a promise of a value. 38 + */ 39 + export type MaybePromise<T> = T | Promise<T>; 35 40 36 41 /** 37 42 * Converts all top-level Array properties to ReadonlyArray (shallow).