├── .github └── workflows │ ├── branches.yml │ └── master.yml ├── .gitignore ├── .yarn ├── plugins │ └── @yarnpkg │ │ └── plugin-interactive-tools.cjs └── releases │ └── yarn-3.2.4.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── assets └── logo.svg ├── babel.config.cjs ├── fixtures ├── sub │ └── types2.ts └── types1.ts ├── jest.config.js ├── lib ├── __snapshots__ │ ├── batch-convert.test.ts.snap │ ├── converter.test.ts.snap │ ├── error.test.ts.snap │ ├── file.test.ts.snap │ └── format-graph.test.ts.snap ├── batch-convert.test.ts ├── batch-convert.ts ├── bin │ ├── __snapshots__ │ │ └── typeconv.test.ts.snap │ ├── typeconv.test.ts │ └── typeconv.ts ├── convert-core-types.test.ts ├── convert-core-types.ts ├── convert-graphql.test.ts ├── convert-graphql.ts ├── convert-json-schema.test.ts ├── convert-json-schema.ts ├── convert-suretype.ts ├── convert-typescript.ts ├── converter.test.ts ├── converter.ts ├── error.test.ts ├── error.ts ├── file.test.ts ├── file.ts ├── fixtures │ └── validator.st.js ├── format-graph.test.ts ├── format-graph.ts ├── index.test.ts ├── index.ts ├── package.ts ├── reader.ts ├── tests │ ├── __snapshots__ │ │ ├── ts-to-json-schema.test.ts.snap │ │ └── ts-to-openapi.test.ts.snap │ ├── ts-to-json-schema.test.ts │ └── ts-to-openapi.test.ts ├── types.ts ├── utils.test.ts ├── utils.ts └── writer.ts ├── package.json ├── test └── utils.ts ├── tsconfig.json ├── tsconfig.prod.json └── yarn.lock /.github/workflows/branches.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Branches 5 | 6 | on: 7 | push: 8 | branches-ignore: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: 19 | - 14.x 20 | - 16.x 21 | - 18.x 22 | - 20.x 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Use Node.js ${{ matrix.node-version }} 26 | uses: actions/setup-node@v1 27 | with: 28 | node-version: ${{ matrix.node-version }} 29 | - run: yarn 30 | - run: yarn build 31 | - run: yarn test 32 | env: 33 | CI: true 34 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Master 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: 18 | - 14.x 19 | - 16.x 20 | - 18.x 21 | - 20.x 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: yarn 29 | - run: yarn build 30 | - run: yarn test 31 | env: 32 | CI: true 33 | 34 | release: 35 | name: Release 36 | runs-on: ubuntu-latest 37 | needs: build 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v1 41 | - name: Setup Node.js 42 | uses: actions/setup-node@v1 43 | with: 44 | node-version: 18 45 | - run: yarn 46 | - run: yarn build 47 | - run: yarn test 48 | env: 49 | CI: true 50 | - name: Coveralls 51 | uses: coverallsapp/github-action@master 52 | with: 53 | github-token: ${{ secrets.GITHUB_TOKEN }} 54 | - name: Release 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 58 | run: npx semantic-release 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules 3 | yarn-* 4 | .yarn/ 5 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.2.4.cjs 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Gustaf Räntilä 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version][npm-image]][npm-url] 2 | [![downloads][downloads-image]][npm-url] 3 | [![build status][build-image]][build-url] 4 | [![coverage status][coverage-image]][coverage-url] 5 | [![Node.JS version][node-version]][node-url] 6 | 7 | 8 | 9 | 10 | **typeconv** is an extremely fast *silver bullet* type conversion utility. 11 | 12 | It converts between any of its supported types, bidirectionally. 13 | 14 | typeconv lets you convert between type systems which have [`core-types`][core-types-github-url] converters, such as JSON Schema, TypeScript, GraphQL, Open API and [SureType][suretype-github-url]. This package can be used as an API programatically or as an application (installed in `node_modules/.bin` or by using e.g. [`npx`](https://www.npmjs.com/package/npx)). 15 | 16 | By taking advantage of the [`core-types`][core-types-github-url] ([npm][core-types-npm-url]) toolbox for generic type handling, typeconv can convert and maintain source code location information, comments, descriptions etc. when converting between the different type systems. It is using the following converter packages: 17 | * [`core-types-json-schema`][core-types-json-schema-github-url] ([npm][core-types-json-schema-npm-url]) 18 | * [`core-types-ts`][core-types-ts-github-url] ([npm][core-types-ts-npm-url]) 19 | * [`core-types-graphql`][core-types-graphql-github-url] ([npm][core-types-graphql-npm-url]) 20 | * [`core-types-suretype`][core-types-suretype-github-url] ([npm][core-types-suretype-npm-url]) 21 | 22 | These type systems don't share the same set of types and constraints. For example, JSON Schema has *value* constraints (like *"a string must be longer than 5 characters*") and GraphQL doesn't have `null` or key-value objects as a first-class type. Convertions will therefore produce the smallest common denominator of type information, but still be very useful. See [`core-types`][core-types-github-url] for more information on its supported types, and why not implement a new conversion package yourself! 23 | 24 | 25 | # TL;DR CLI 26 | 27 | Convert files from TypeScript (*"ts"*) to GraphQL (*"gql"*), put the generated files in the `gql-schemas` directory in the same directory structure as the source files: 28 | 29 | ```zsh 30 | $ typeconv -f ts -t gql -o gql-schemas 'types/**/*.ts' 31 | ``` 32 | 33 | This generates `gql-schemas/*.graphql` for each `.ts` file in `types/` (and sub-directories). 34 | 35 | *Note that when using glob patterns, put them in quotes to not have the shell try to expand the pattern - typeconv will do it a lot better!* 36 | 37 | 38 | ## SureType 39 | 40 | When converting *from* SureType, typeconv will extract all *exported* validators. 41 | 42 | 43 | ## Versions 44 | 45 | Since v2: 46 | * The package is a pure ESM module, no more CommonJS support. This will likely not affect CLI usages. 47 | * Node 12 support is dropped. 48 | 49 | 50 | # Contents 51 | 52 | * [Conversion example](#conversion-example) 53 | * [Usage](#usage) 54 | * [Command line](#command-line) 55 | * [As API](#as-api) 56 | * [JSON Schema](#json-schema) 57 | * [Open API](#open-api) 58 | * [TypeScript](#typescript) 59 | * [GraphQL](#graphql) 60 | * [SureType](#suretype) 61 | 62 | 63 | # Conversion example 64 | 65 |
66 | Converting the following JSON Schema: 67 |

68 | 69 | ```json 70 | { 71 | "definitions": { 72 | "User": { 73 | "type": "object", 74 | "title": "User type", 75 | "description": "This type holds the user information, such as name", 76 | "properties": { "name": { "type": "string", "title": "The real name" } }, 77 | "required": [ "name" ] 78 | }, 79 | "ChatLine": { 80 | "type": "object", 81 | "title": "A chat line", 82 | "properties": { 83 | "user": { "$ref": "#/definitions/User" }, 84 | "line": { "type": "string" } 85 | }, 86 | "required": [ "user", "line" ] 87 | } 88 | } 89 | } 90 | ``` 91 | 92 |

93 |
94 | 95 |
96 | ... to TypeScript will produce: 97 |

98 | 99 | ```ts 100 | /*User type 101 | 102 | This type holds the user information, such as name*/ 103 | export interface User { 104 | /*The real name*/ 105 | name: string; 106 | } 107 | 108 | /*A chat line*/ 109 | export interface ChatLine { 110 | user: User; 111 | line: string; 112 | } 113 | ``` 114 | 115 | or if converted into TypeScript _declarations_ (for `.d.ts` files), `export interface` will be `export declare interface`. 116 | 117 |

118 |
119 | 120 |
121 | ... and to GraphQL will produce: 122 |

123 | 124 | ```graphql 125 | """ 126 | # User type 127 | 128 | This type holds the user information, such as name 129 | """ 130 | type User { 131 | "The real name" 132 | name: String! 133 | } 134 | 135 | "A chat line" 136 | type ChatLine { 137 | user: User! 138 | line: String! 139 | } 140 | ``` 141 | 142 |

143 |
144 | 145 | Conversions are bi-directional, so any of these type systems can convert to any other. 146 | 147 | 148 | # Usage 149 | 150 | 151 | ## Command line 152 | 153 | You can depend on `typeconv`, and if so, you'll have `node_modules/.bin/typeconv` installed. Or you run it with on-the-fly installation using `npx`, as `npx typeconv args...`. 154 | 155 | Use `-f` (or `--from-type`) to specify *from* which type system to convert, and `-t` (or `--to-type`) to specify which type system to convert *to*. Other options can be used to configure each configuration (both the *from* and the *to*) and these options are usually only available for a specific type system. 156 | 157 | The types supported are `gql` (GraphQL), `ts` (TypeScript), `jsc` (JSON Schema), `oapi` (Open API) and `st` (SureType). 158 | 159 |
160 | $ typeconv --help 161 |

162 | 163 | ``` 164 | Usage: typeconv [options] file ... 165 | 166 | Options: 167 | 168 | -h, --help Print (this) help screen 169 | --version Print the program version 170 | -v, --verbose Verbose informational output (default: false) 171 | --dry-run Prepare and perform conversion, but write no output (default: false) 172 | --(no-)hidden Include hidden files, i.e. files in .gitignore, 173 | files beginning with '.' and the '.git' directory 174 | (default: true) 175 | -f, --from-type Type system to convert from 176 | 177 | Values: 178 | ts TypeScript 179 | jsc JSON Schema 180 | gql GraphQL 181 | oapi Open API 182 | st SureType 183 | ct core-types 184 | 185 | -t, --to-type Type system to convert to 186 | 187 | Values: 188 | ts TypeScript 189 | jsc JSON Schema 190 | gql GraphQL 191 | oapi Open API 192 | st SureType 193 | ct core-types 194 | 195 | --(no-)shortcut Shortcut conversion if possible (bypassing core-types). 196 | This is possible between SureType, JSON Schema and Open API 197 | to preserve all features which would otherwise be erased. 198 | (default: true) 199 | -o, --output-directory

Output directory. Defaults to the same as the input files. 200 | -O, --output-extension Output filename extension to use. 201 | Defaults to 'ts'/'d.ts', 'json', 'yaml' and 'graphql'. 202 | Use '-' to not save a file, but output to stdout instead. 203 | --(no-)strip-annotations Removes all annotations (descriptions, comments, ...) (default: false) 204 | TypeScript 205 | --(no-)ts-declaration Output TypeScript declarations (default: false) 206 | --(no-)ts-disable-lint-header Output comments for disabling linting (default: true) 207 | --(no-)ts-descriptive-header Output the header comment (default: true) 208 | --(no-)ts-use-unknown Use 'unknown' type instead of 'any' (default: true) 209 | --ts-non-exported Strategy for non-exported types (default: include-if-referenced) 210 | 211 | Values: 212 | fail Fail conversion 213 | ignore Don't include non-exported types, 214 | even if referenced 215 | include Include non-exported types 216 | inline Don't include non-exported types, 217 | inline them if necessary. 218 | Will fail on cyclic types 219 | include-if-referenced Include non-exported types only if they 220 | are referenced from exported types 221 | 222 | --ts-namespaces Namespace strategy. (default: ignore) 223 | 224 | Values: 225 | ignore Ignore namespaces entirely (default). 226 | - When converting from TypeScript, types in namespaces 227 | aren't exported. 228 | - When converting to TypeScript, no attempt to 229 | reconstruct namespaces is performed. 230 | hoist When converting from TypeScript, hoist types inside 231 | namespaces to top-level, so that the types are 232 | included, but without their namespace. 233 | This can cause conflicts, in which case deeper 234 | declarations will be dropped in favor of more top- 235 | level declarations. 236 | In case of same-level (namespace depth) declarations 237 | with the same name, only one will be exported in a 238 | non-deterministic manner. 239 | dot When converting from TypeScript, join the namespaces 240 | and the exported type with a dot (.). 241 | When converting to TypeScript, try to reconstruct 242 | namespaces by splitting the name on dot (.). 243 | underscore When converting from TypeScript, join the namespaces 244 | and the exported type with an underscore (_). 245 | When converting to TypeScript, try to reconstruct 246 | namespaces by splitting the name on underscore (_). 247 | reconstruct-all When converting to TypeScript, try to reconstruct 248 | namespaces by splitting the name on both dot and 249 | underscore. 250 | 251 | GraphQL 252 | --gql-unsupported Method to use for unsupported types 253 | 254 | Values: 255 | ignore Ignore (skip) type 256 | warn Ignore type, but warn 257 | error Throw an error 258 | 259 | --gql-null-typename Custom type name to use for null 260 | Open API 261 | --oapi-format Output format for Open API (default: yaml) 262 | 263 | Values: 264 | json JSON 265 | yaml YAML ('yml' is also allowed) 266 | 267 | --oapi-title Open API title to use in output document. 268 | Defaults to the input filename. 269 | --oapi-version <version> Open API document version to use in output document. (default: 1) 270 | SureType 271 | --st-ref-method <method> SureType reference export method (default: provided) 272 | 273 | Values: 274 | no-refs Don't ref anything, inline all types 275 | provided Reference types that are explicitly exported 276 | ref-all Ref all provided types and those with names 277 | 278 | --st-missing-ref <method> What to do when detecting an unresolvable reference (default: warn) 279 | 280 | Values: 281 | ignore Ignore; skip type or cast to any 282 | warn Same as 'ignore', but warn 283 | error Fail conversion 284 | 285 | --(no-)st-inline-types Inline pretty typescript types aside validator code (default: true) 286 | --(no-)st-export-type Export the deduced types (or the pretty types, 287 | depending on --st-inline-types) 288 | (default: true) 289 | --(no-)st-export-schema Export validator schemas (default: false) 290 | --(no-)st-export-validator Export regular validators (default: true) 291 | --(no-)st-export-ensurer Export 'ensurer' validators (default: true) 292 | --(no-)st-export-type-guard Export type guards (is* validators) (default: true) 293 | --(no-)st-use-unknown Use 'unknown' type instead of 'any' (default: true) 294 | --(no-)st-forward-schema Forward the JSON Schema, and create an untyped validator schema 295 | with the raw JSON Schema under the hood 296 | (default: false) 297 | ``` 298 | 299 | </p> 300 | </details> 301 | 302 | 303 | ## As API 304 | 305 | To convert from one type system to another, you create a *reader* for the type system to convert **from** and a *writer* for the type system to convert **to**. The readers and writers for the different type systems have their own set of options. Some have no options (like the JSON Schema reader), some require options (like the Open API writer). 306 | 307 | 308 | ### makeConverter 309 | 310 | Making a converter is done using `makeConverter(reader, writer, options?)`, which takes a reader and a writer, and optionally options: 311 | 312 | #### **cwd** (string) 313 | 314 | The current working directory, only useful when working with files. 315 | 316 | #### **simplify** (boolean) (default true) 317 | 318 | When simplify is true, the converter will let core-types 319 | [*compress*](https://github.com/grantila/core-types#simplify) the 320 | types after having converted from {reader} format to core-types. 321 | This is usually recommended, but may cause some annotations (comments) 322 | to be dropped. 323 | 324 | #### **map** (ConvertMapFunction) 325 | 326 | Custom map function for transforming each type after it has been 327 | converted *from* the source type (and after it has been simplified), 328 | but before it's written to the target type system. 329 | 330 | Type: `(node: NamedType, index: number, array: ReadonlyArray<NamedType>) => NamedType` 331 | ([NamedType ref](https://github.com/grantila/core-types#specification)) 332 | 333 | If `filter` is used as well, this runs before `filter`. 334 | 335 | If `transform` is used as well, this runs before `transform`. 336 | 337 | #### **filter** (ConvertFilterFunction) 338 | 339 | Custom filter function for filtering types after they have been 340 | converted *from* the source type. 341 | 342 | Type: `(node: NamedType, index: number, array: ReadonlyArray<NamedType>) => boolean` 343 | ([NamedType ref](https://github.com/grantila/core-types#specification)) 344 | 345 | If `map` is used as well, this runs after `map`. 346 | 347 | If `transform` is used as well, this runs before `transform`. 348 | 349 | #### **transform** (ConvertTransformFunction) 350 | 351 | Custom filter function for filtering types after they have been 352 | converted *from* the source type. 353 | 354 | Type: `( doc: NodeDocument ) => NodeDocument` 355 | ([NodeDocument ref](https://github.com/grantila/core-types#specification)) 356 | 357 | If `map` is used as well, this runs after `map`. 358 | 359 | If `filter` is used as well, this runs after `filter`. 360 | 361 | #### **shortcut** (boolean) (default true) 362 | 363 | Shortcut reader and writer if possible (bypassing core-types). 364 | 365 | 366 | 367 | ### Basic example conversion 368 | 369 | ```ts 370 | import { 371 | getTypeScriptReader, 372 | getOpenApiWriter, 373 | makeConverter, 374 | } from 'typeconv' 375 | 376 | const reader = getTypeScriptReader( ); 377 | const writer = getOpenApiWriter( { format: 'yaml', title: 'My API', version: 'v1' } ); 378 | const { convert } = makeConverter( reader, writer ); 379 | const { data } = await convert( { data: "export type Foo = string | number;" } ); 380 | data; // This is the Open API yaml as a string 381 | ``` 382 | 383 | 384 | ### JSON Schema 385 | 386 | There are two exported functions for JSON Schema: 387 | 388 | ```ts 389 | import { getJsonSchemaReader, getJsonSchemaWriter } from 'typeconv' 390 | 391 | const reader = getJsonSchemaReader( ); 392 | const writer = getJsonSchemaWriter( ); 393 | ``` 394 | 395 | They don't have any options. 396 | 397 | typeconv expects the JSON Schema to contain **definitions**, i.e. to be in the form: 398 | 399 | <details style="padding-left: 32px;border-left: 4px solid gray;"> 400 | <summary>JSON Schema</summary> 401 | <p> 402 | 403 | ```json 404 | { 405 | "definitions": { 406 | "User": { 407 | "type": "object", 408 | "properties": { "name": { "type": "string" } }, 409 | "required": [ "name" ] 410 | }, 411 | "ChatLine": { 412 | "type": "object", 413 | "properties": { 414 | "user": { "$ref": "#/definitions/User" }, 415 | "line": { "type": "string" } 416 | }, 417 | "required": [ "user", "line" ] 418 | } 419 | } 420 | } 421 | ``` 422 | 423 | </p> 424 | </details> 425 | 426 | typeconv doesn't support external references (to other files). If you have that, you need to use a reference parser and merge it into one inline-referenced file before using typeconv. 427 | 428 | 429 | ### Open API 430 | 431 | Converting to or from Open API can be done with both JSON and YAML. The default is JSON. 432 | 433 | When reading, if the filename ends with `.yml` or `.yaml`, typeconv will interpret the input as YAML. 434 | 435 | Writing however, is decided in the writer factory and provided to `getOpenApiWriter`. 436 | 437 | ```ts 438 | import { getOpenApiReader, getOpenApiWriter } from 'typeconv' 439 | 440 | const reader = getOpenApiReader( ); 441 | const writer = getOpenApiWriter( { 442 | format: 'yaml', 443 | title: 'My API', 444 | version: 'v1', 445 | schemaVersion: '3.0.0', 446 | } ); 447 | ``` 448 | 449 | The options to `getOpenApiWriter` is: 450 | 451 | ```ts 452 | interface { 453 | format?: string; 454 | title: string; 455 | version: string; 456 | schemaVersion?: string; 457 | } 458 | ``` 459 | 460 | 461 | ### TypeScript 462 | 463 | TypeScript conversion is done using: 464 | 465 | ```ts 466 | import { getTypeScriptReader, getTypeScriptWriter } from 'typeconv' 467 | 468 | const reader = getTypeScriptReader( ); 469 | const writer = getTypeScriptWriter( ); 470 | ``` 471 | 472 | Both these take an optional argument. 473 | 474 | The `getTypeScriptReader` takes an optional 475 | [`FromTsOptions`](https://github.com/grantila/core-types-ts#typescript-to-core-types) 476 | object from [`core-types-ts`][core-types-ts-github-url], although `warn` isn't necessary since it's set by typeconv internally. 477 | 478 | The `getTypeScriptWriter` takes an optional 479 | [`ToTsOptions`](https://github.com/grantila/core-types-ts#core-types-to-typescript) 480 | object from [`core-types-ts`][core-types-ts-github-url], although `warn`, `filename`, `sourceFilename`, `userPackage` and `userPackageUrl` aren't necessary since they're set by typeconv internally. 481 | 482 | 483 | ### GraphQL 484 | 485 | GraphQL conversion is done using; 486 | 487 | ```ts 488 | import { getGraphQLReader, getGraphQLWriter } from 'typeconv' 489 | 490 | const reader = getGraphQLReader( ); 491 | const writer = getGraphQLWriter( ); 492 | ``` 493 | 494 | Both these take an optional argument. 495 | 496 | The `getGraphQLReader` takes an optional 497 | [`GraphqlToCoreTypesOptions`](https://github.com/grantila/core-types-graphql#graphql-to-core-types) 498 | object from [`core-types-graphql`][core-types-graphql-github-url], although `warn` isn't necessary since it's set by typeconv internally. 499 | 500 | The `getGraphQLWriter` takes an optional 501 | [`CoreTypesToGraphqlOptions`](https://github.com/grantila/core-types-graphql#core-types-to-graphql) 502 | object from [`core-types-graphql`][core-types-graphql-github-url], although `warn`, `filename`, `sourceFilename`, `userPackage` and `userPackageUrl` aren't necessary since they're set by typeconv internally. 503 | 504 | 505 | ### SureType 506 | 507 | SureType conversion is done using; 508 | 509 | ```ts 510 | import { getSureTypeReader, getSureTypeWriter } from 'typeconv' 511 | 512 | const reader = getSureTypeReader( ); 513 | const writer = getSureTypeWriter( ); 514 | ``` 515 | 516 | Both these take an optional argument from the [`core-types-suretype`][core-types-suretype-github-url] package. 517 | 518 | The `getSureTypeReader` takes an optional 519 | [`SuretypeToJsonSchemaOptions`](https://github.com/grantila/core-types-suretype#suretype-to-core-types). 520 | 521 | The `getSureTypeWriter` takes an optional 522 | [`JsonSchemaToSuretypeOptions`](https://github.com/grantila/core-types-suretype#core-types-to-suretype). 523 | 524 | 525 | [npm-image]: https://img.shields.io/npm/v/typeconv.svg 526 | [npm-url]: https://npmjs.org/package/typeconv 527 | [downloads-image]: https://img.shields.io/npm/dm/typeconv.svg 528 | [build-image]: https://img.shields.io/github/actions/workflow/status/grantila/typeconv/master.yml?branch=master 529 | [build-url]: https://github.com/grantila/typeconv/actions?query=workflow%3AMaster 530 | [coverage-image]: https://coveralls.io/repos/github/grantila/typeconv/badge.svg?branch=master 531 | [coverage-url]: https://coveralls.io/github/grantila/typeconv?branch=master 532 | [node-version]: https://img.shields.io/node/v/typeconv 533 | [node-url]: https://nodejs.org/en/ 534 | 535 | [core-types-npm-url]: https://npmjs.org/package/core-types 536 | [core-types-github-url]: https://github.com/grantila/core-types 537 | [core-types-json-schema-npm-url]: https://npmjs.org/package/core-types-json-schema 538 | [core-types-json-schema-github-url]: https://github.com/grantila/core-types-json-schema 539 | [core-types-ts-npm-url]: https://npmjs.org/package/core-types-ts 540 | [core-types-ts-github-url]: https://github.com/grantila/core-types-ts 541 | [core-types-graphql-npm-url]: https://npmjs.org/package/core-types-graphql 542 | [core-types-graphql-github-url]: https://github.com/grantila/core-types-graphql 543 | [core-types-suretype-npm-url]: https://npmjs.org/package/core-types-suretype 544 | [core-types-suretype-github-url]: https://github.com/grantila/core-types-suretype 545 | [suretype-github-url]: https://github.com/grantila/suretype 546 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | modules: false, 7 | targets: { 8 | node: 'current', 9 | }, 10 | }, 11 | ], 12 | '@babel/preset-typescript', 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /fixtures/sub/types2.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SubDirType 3 | { 4 | // Name of the foo 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /fixtures/types1.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Foo 3 | { 4 | // Name of the bar 5 | bar: string; 6 | /** 7 | * A baz is a number of another Foo 8 | * 9 | * @see baz documentation at http://documentation.yada 10 | */ 11 | baz: number | Foo; 12 | } 13 | 14 | export interface Foo2 15 | { 16 | // The bak is a bit stream 17 | bak: Array< boolean >; 18 | } 19 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | resolver: "ts-jest-resolver", 3 | testEnvironment: 'node', 4 | testMatch: [ '<rootDir>/lib/**/*.test.ts' ], 5 | collectCoverageFrom: [ '<rootDir>/lib/**' ], 6 | coveragePathIgnorePatterns: [ '/node_modules/', '/__snapshots__/', '/bin/' ], 7 | coverageReporters: [ 'lcov', 'text', 'html' ], 8 | extensionsToTreatAsEsm: [ ".ts" ], 9 | }; 10 | -------------------------------------------------------------------------------- /lib/__snapshots__/batch-convert.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`batch-convert batchConvert 1`] = ` 4 | { 5 | "$comment": "Generated from types1.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 6 | "definitions": { 7 | "Foo": { 8 | "additionalProperties": false, 9 | "properties": { 10 | "bar": { 11 | "title": "Foo.bar", 12 | "type": "string", 13 | }, 14 | "baz": { 15 | "anyOf": [ 16 | { 17 | "title": "Foo.baz", 18 | "type": "number", 19 | }, 20 | { 21 | "$ref": "#/definitions/Foo", 22 | "title": "Foo.baz", 23 | }, 24 | ], 25 | "description": "A baz is a number of another Foo 26 | 27 | @see baz documentation at http://documentation.yada", 28 | "title": "Foo.baz", 29 | }, 30 | }, 31 | "required": [ 32 | "bar", 33 | "baz", 34 | ], 35 | "title": "Foo", 36 | "type": "object", 37 | }, 38 | "Foo2": { 39 | "additionalProperties": false, 40 | "properties": { 41 | "bak": { 42 | "items": { 43 | "type": "boolean", 44 | }, 45 | "title": "Foo2.bak", 46 | "type": "array", 47 | }, 48 | }, 49 | "required": [ 50 | "bak", 51 | ], 52 | "title": "Foo2", 53 | "type": "object", 54 | }, 55 | }, 56 | } 57 | `; 58 | 59 | exports[`batch-convert batchConvert 2`] = ` 60 | { 61 | "$comment": "Generated from sub/types2.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 62 | "definitions": { 63 | "SubDirType": { 64 | "additionalProperties": false, 65 | "properties": { 66 | "name": { 67 | "title": "SubDirType.name", 68 | "type": "string", 69 | }, 70 | }, 71 | "required": [ 72 | "name", 73 | ], 74 | "title": "SubDirType", 75 | "type": "object", 76 | }, 77 | }, 78 | } 79 | `; 80 | 81 | exports[`batch-convert batchConvert verbose 1`] = ` 82 | { 83 | "$comment": "Generated from types1.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 84 | "definitions": { 85 | "Foo": { 86 | "additionalProperties": false, 87 | "properties": { 88 | "bar": { 89 | "title": "Foo.bar", 90 | "type": "string", 91 | }, 92 | "baz": { 93 | "anyOf": [ 94 | { 95 | "title": "Foo.baz", 96 | "type": "number", 97 | }, 98 | { 99 | "$ref": "#/definitions/Foo", 100 | "title": "Foo.baz", 101 | }, 102 | ], 103 | "description": "A baz is a number of another Foo 104 | 105 | @see baz documentation at http://documentation.yada", 106 | "title": "Foo.baz", 107 | }, 108 | }, 109 | "required": [ 110 | "bar", 111 | "baz", 112 | ], 113 | "title": "Foo", 114 | "type": "object", 115 | }, 116 | "Foo2": { 117 | "additionalProperties": false, 118 | "properties": { 119 | "bak": { 120 | "items": { 121 | "type": "boolean", 122 | }, 123 | "title": "Foo2.bak", 124 | "type": "array", 125 | }, 126 | }, 127 | "required": [ 128 | "bak", 129 | ], 130 | "title": "Foo2", 131 | "type": "object", 132 | }, 133 | }, 134 | } 135 | `; 136 | 137 | exports[`batch-convert batchConvert verbose 2`] = ` 138 | { 139 | "$comment": "Generated from sub/types2.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 140 | "definitions": { 141 | "SubDirType": { 142 | "additionalProperties": false, 143 | "properties": { 144 | "name": { 145 | "title": "SubDirType.name", 146 | "type": "string", 147 | }, 148 | }, 149 | "required": [ 150 | "name", 151 | ], 152 | "title": "SubDirType", 153 | "type": "object", 154 | }, 155 | }, 156 | } 157 | `; 158 | 159 | exports[`batch-convert batchConvert verbose 3`] = ` 160 | [ 161 | "fixtures", 162 | [ 163 | "[typeconv] types1.ts -> stdout, 100% types converted (2/2)", 164 | ], 165 | [ 166 | "[typeconv] sub/types2.ts -> stdout, 100% types converted (1/1)", 167 | ], 168 | ] 169 | `; 170 | 171 | exports[`batch-convert batchConvertGlob 1`] = ` 172 | { 173 | "$comment": "Generated from sub/types2.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 174 | "definitions": { 175 | "SubDirType": { 176 | "additionalProperties": false, 177 | "properties": { 178 | "name": { 179 | "title": "SubDirType.name", 180 | "type": "string", 181 | }, 182 | }, 183 | "required": [ 184 | "name", 185 | ], 186 | "title": "SubDirType", 187 | "type": "object", 188 | }, 189 | }, 190 | } 191 | `; 192 | 193 | exports[`batch-convert batchConvertGlob 2`] = ` 194 | { 195 | "$comment": "Generated from types1.ts by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)", 196 | "definitions": { 197 | "Foo": { 198 | "additionalProperties": false, 199 | "properties": { 200 | "bar": { 201 | "title": "Foo.bar", 202 | "type": "string", 203 | }, 204 | "baz": { 205 | "anyOf": [ 206 | { 207 | "title": "Foo.baz", 208 | "type": "number", 209 | }, 210 | { 211 | "$ref": "#/definitions/Foo", 212 | "title": "Foo.baz", 213 | }, 214 | ], 215 | "description": "A baz is a number of another Foo 216 | 217 | @see baz documentation at http://documentation.yada", 218 | "title": "Foo.baz", 219 | }, 220 | }, 221 | "required": [ 222 | "bar", 223 | "baz", 224 | ], 225 | "title": "Foo", 226 | "type": "object", 227 | }, 228 | "Foo2": { 229 | "additionalProperties": false, 230 | "properties": { 231 | "bak": { 232 | "items": { 233 | "type": "boolean", 234 | }, 235 | "title": "Foo2.bak", 236 | "type": "array", 237 | }, 238 | }, 239 | "required": [ 240 | "bak", 241 | ], 242 | "title": "Foo2", 243 | "type": "object", 244 | }, 245 | }, 246 | } 247 | `; 248 | -------------------------------------------------------------------------------- /lib/__snapshots__/converter.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`converter json-schema-to-ts self-reading and writing JSON Schema 1`] = ` 4 | "export type foo = string; 5 | " 6 | `; 7 | 8 | exports[`converter shortcut should run graph if necessary (gql->st) 1`] = ` 9 | "/* tslint:disable */ 10 | /* eslint-disable */ 11 | /** 12 | * This file is generated by core-types-suretype on behalf of typeconv, DO NOT EDIT. 13 | * For more information, see: 14 | * - {@link https://github.com/grantila/core-types-suretype} 15 | * - {@link https://github.com/grantila/typeconv} 16 | */ 17 | 18 | import { suretype, v, compile, annotate } from 'suretype'; 19 | 20 | /** The validation schema for a Foo */ 21 | export const schemaFoo = suretype({ 22 | name: "Foo", 23 | title: "Foo", 24 | description: "The Foo type" 25 | }, v.object({ 26 | int: annotate({ 27 | title: "Foo.int", 28 | description: "This is the integer thing\\n\\n@default 55" 29 | }, v.number().integer()), 30 | str: annotate({ 31 | title: "Foo.str" 32 | }, v.string().required()), 33 | stra: annotate({ 34 | title: "Foo.stra", 35 | description: "Excellent array of strings" 36 | }, v.array(v.string()).required()) 37 | })); 38 | 39 | /** The Foo type */ 40 | export interface Foo { 41 | /** 42 | * This is the integer thing 43 | * 44 | * @default 55 45 | */ 46 | int?: number; 47 | str: string; 48 | /** Excellent array of strings */ 49 | stra: string[]; 50 | } 51 | 52 | /** 53 | * ## Validate that a variable is a Foo 54 | * 55 | * @returns ValidationResult 56 | */ 57 | export const validateFoo = compile(schemaFoo); 58 | 59 | /** 60 | * ## Ensure a variable is a Foo 61 | * 62 | * This call will throw a ValidationError if the variable isn't a Foo. 63 | * 64 | * If the variable **is** a Foo, the returned variable will be of that type. 65 | */ 66 | export const ensureFoo = compile<typeof schemaFoo, Foo>(schemaFoo, { ensure: true }); 67 | 68 | /** 69 | * ## Validates that a variable is a Foo 70 | * 71 | * @returns boolean 72 | */ 73 | export const isFoo = compile(schemaFoo, { simple: true }); 74 | " 75 | `; 76 | 77 | exports[`converter shortcut should run graph if necessary (st->oapi) 1`] = ` 78 | "openapi: 3.0.0 79 | info: 80 | title: Title 81 | version: '1' 82 | x-comment: >- 83 | Generated from validator.st.js by core-types-suretype 84 | (https://github.com/grantila/core-types-suretype) on behalf of typeconv 85 | (https://github.com/grantila/typeconv) 86 | paths: {} 87 | components: 88 | schemas: 89 | Foo: 90 | description: This is Foo 91 | properties: 92 | gt5: 93 | exclusiveMinimum: 5 94 | type: number 95 | gte5: 96 | minimum: 5 97 | type: number 98 | type: object 99 | " 100 | `; 101 | 102 | exports[`converter shortcut should run shortcut if possible 1`] = ` 103 | "openapi: 3.0.0 104 | info: 105 | title: Title 106 | version: '1' 107 | x-comment: >- 108 | Generated by core-types-json-schema 109 | (https://github.com/grantila/core-types-json-schema) on behalf of typeconv 110 | (https://github.com/grantila/typeconv) 111 | paths: {} 112 | components: 113 | schemas: 114 | foo: 115 | type: string 116 | " 117 | `; 118 | -------------------------------------------------------------------------------- /lib/__snapshots__/error.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`error formatCoreTypesError should display nice error message 1`] = ` 4 | "> 1 | type X = bad type; 5 | | ^^^^ [Error] foo error" 6 | `; 7 | 8 | exports[`error formatError should properly display errors 1`] = ` 9 | "> 1 | type X = bad type; 10 | | ^^^^ foo message" 11 | `; 12 | -------------------------------------------------------------------------------- /lib/__snapshots__/file.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`file 1`] = `"]8;;file:///the/root/foo/file.namefoo/file.name]8;;"`; 4 | -------------------------------------------------------------------------------- /lib/__snapshots__/format-graph.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`format-graph findAllPaths (incl core-types) 1`] = ` 4 | [ 5 | "ts->{ct}->gql", 6 | "ts->{ct}->jsc jsc->{ct}->gql", 7 | "ts->{ct}->oapi oapi->{ct}->gql", 8 | "ts->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql", 9 | "ts->{ct}->jsc jsc->{ct}->oapi oapi->{ct}->gql", 10 | "ts->{ct}->jsc jsc->{jsc}->oapi oapi->{ct}->gql", 11 | "ts->{ct}->oapi oapi->{ct}->ts ts->{ct}->gql", 12 | "ts->{ct}->oapi oapi->{ct}->jsc jsc->{ct}->gql", 13 | "ts->{ct}->oapi oapi->{jsc}->jsc jsc->{ct}->gql", 14 | "ts->{ct}->st st->{ct}->ts ts->{ct}->gql", 15 | "ts->{ct}->st st->{ct}->jsc jsc->{ct}->gql", 16 | "ts->{ct}->st st->{ct}->oapi oapi->{ct}->gql", 17 | "ts->{ct}->st st->{jsc}->jsc jsc->{ct}->gql", 18 | "ts->{ct}->st st->{jsc}->oapi oapi->{ct}->gql", 19 | "ts->{ct}->jsc jsc->{ct}->ts ts->{ct}->oapi oapi->{ct}->gql", 20 | "ts->{ct}->jsc jsc->{ct}->oapi oapi->{ct}->ts ts->{ct}->gql", 21 | "ts->{ct}->jsc jsc->{jsc}->oapi oapi->{ct}->ts ts->{ct}->gql", 22 | "ts->{ct}->oapi oapi->{ct}->ts ts->{ct}->jsc jsc->{ct}->gql", 23 | "ts->{ct}->oapi oapi->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql", 24 | "ts->{ct}->oapi oapi->{jsc}->jsc jsc->{ct}->ts ts->{ct}->gql", 25 | "ts->{ct}->st st->{ct}->ts ts->{ct}->jsc jsc->{ct}->gql", 26 | "ts->{ct}->st st->{ct}->ts ts->{ct}->oapi oapi->{ct}->gql", 27 | "ts->{ct}->st st->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql", 28 | "ts->{ct}->st st->{ct}->jsc jsc->{ct}->oapi oapi->{ct}->gql", 29 | "ts->{ct}->st st->{ct}->jsc jsc->{jsc}->oapi oapi->{ct}->gql", 30 | "ts->{ct}->st st->{ct}->oapi oapi->{ct}->ts ts->{ct}->gql", 31 | "ts->{ct}->st st->{ct}->oapi oapi->{ct}->jsc jsc->{ct}->gql", 32 | "ts->{ct}->st st->{ct}->oapi oapi->{jsc}->jsc jsc->{ct}->gql", 33 | "ts->{ct}->st st->{jsc}->jsc jsc->{ct}->ts ts->{ct}->gql", 34 | "ts->{ct}->st st->{jsc}->jsc jsc->{ct}->oapi oapi->{ct}->gql", 35 | "ts->{ct}->st st->{jsc}->jsc jsc->{jsc}->oapi oapi->{ct}->gql", 36 | "ts->{ct}->st st->{jsc}->oapi oapi->{ct}->ts ts->{ct}->gql", 37 | "ts->{ct}->st st->{jsc}->oapi oapi->{ct}->jsc jsc->{ct}->gql", 38 | "ts->{ct}->st st->{jsc}->oapi oapi->{jsc}->jsc jsc->{ct}->gql", 39 | "ts->{ct}->st st->{ct}->ts ts->{ct}->jsc jsc->{ct}->oapi oapi->{ct}->gql", 40 | "ts->{ct}->st st->{ct}->ts ts->{ct}->jsc jsc->{jsc}->oapi oapi->{ct}->gql", 41 | "ts->{ct}->st st->{ct}->ts ts->{ct}->oapi oapi->{ct}->jsc jsc->{ct}->gql", 42 | "ts->{ct}->st st->{ct}->ts ts->{ct}->oapi oapi->{jsc}->jsc jsc->{ct}->gql", 43 | "ts->{ct}->st st->{ct}->jsc jsc->{ct}->ts ts->{ct}->oapi oapi->{ct}->gql", 44 | "ts->{ct}->st st->{ct}->jsc jsc->{ct}->oapi oapi->{ct}->ts ts->{ct}->gql", 45 | "ts->{ct}->st st->{ct}->jsc jsc->{jsc}->oapi oapi->{ct}->ts ts->{ct}->gql", 46 | "ts->{ct}->st st->{ct}->oapi oapi->{ct}->ts ts->{ct}->jsc jsc->{ct}->gql", 47 | "ts->{ct}->st st->{ct}->oapi oapi->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql", 48 | "ts->{ct}->st st->{ct}->oapi oapi->{jsc}->jsc jsc->{ct}->ts ts->{ct}->gql", 49 | "ts->{ct}->st st->{jsc}->jsc jsc->{ct}->ts ts->{ct}->oapi oapi->{ct}->gql", 50 | "ts->{ct}->st st->{jsc}->jsc jsc->{ct}->oapi oapi->{ct}->ts ts->{ct}->gql", 51 | "ts->{ct}->st st->{jsc}->jsc jsc->{jsc}->oapi oapi->{ct}->ts ts->{ct}->gql", 52 | "ts->{ct}->st st->{jsc}->oapi oapi->{ct}->ts ts->{ct}->jsc jsc->{ct}->gql", 53 | "ts->{ct}->st st->{jsc}->oapi oapi->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql", 54 | "ts->{ct}->st st->{jsc}->oapi oapi->{jsc}->jsc jsc->{ct}->ts ts->{ct}->gql", 55 | ] 56 | `; 57 | 58 | exports[`format-graph findAllPaths (no shortcuts) 1`] = ` 59 | [ 60 | "st->{ct}->oapi", 61 | "st->{ct}->gql gql->{ct}->oapi", 62 | "st->{ct}->ts ts->{ct}->oapi", 63 | "st->{ct}->jsc jsc->{ct}->oapi", 64 | "st->{ct}->gql gql->{ct}->ts ts->{ct}->oapi", 65 | "st->{ct}->gql gql->{ct}->jsc jsc->{ct}->oapi", 66 | "st->{ct}->ts ts->{ct}->gql gql->{ct}->oapi", 67 | "st->{ct}->ts ts->{ct}->jsc jsc->{ct}->oapi", 68 | "st->{ct}->jsc jsc->{ct}->gql gql->{ct}->oapi", 69 | "st->{ct}->jsc jsc->{ct}->ts ts->{ct}->oapi", 70 | "st->{ct}->gql gql->{ct}->ts ts->{ct}->jsc jsc->{ct}->oapi", 71 | "st->{ct}->gql gql->{ct}->jsc jsc->{ct}->ts ts->{ct}->oapi", 72 | "st->{ct}->ts ts->{ct}->gql gql->{ct}->jsc jsc->{ct}->oapi", 73 | "st->{ct}->ts ts->{ct}->jsc jsc->{ct}->gql gql->{ct}->oapi", 74 | "st->{ct}->jsc jsc->{ct}->gql gql->{ct}->ts ts->{ct}->oapi", 75 | "st->{ct}->jsc jsc->{ct}->ts ts->{ct}->gql gql->{ct}->oapi", 76 | ] 77 | `; 78 | 79 | exports[`format-graph findAllPaths (only shortcuts when exist) 1`] = ` 80 | [ 81 | "st->{jsc}->oapi", 82 | "st->{jsc}->jsc jsc->{jsc}->oapi", 83 | ] 84 | `; 85 | 86 | exports[`format-graph findAllPaths (only shortcuts when none exist) 1`] = `[]`; 87 | -------------------------------------------------------------------------------- /lib/batch-convert.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { fileURLToPath } from "url" 3 | 4 | import { batchConvert, batchConvertGlob } from './batch-convert.js' 5 | import { Converter, makeConverter } from './converter.js' 6 | import { withConsoleLog, withConsoleMock } from '../test/utils.js' 7 | import { getTypeScriptReader } from './convert-typescript.js' 8 | import { getJsonSchemaWriter } from './convert-json-schema.js' 9 | 10 | 11 | const __filename = fileURLToPath( import.meta.url ); 12 | const __dirname = path.dirname( __filename ); 13 | 14 | const fixtureDir = path.join( __dirname, '..', 'fixtures' ); 15 | 16 | function makeMockConverter( ): Converter 17 | { 18 | const reader = getTypeScriptReader( ); 19 | const writer = getJsonSchemaWriter( ); 20 | return makeConverter( reader, writer, { cwd: fixtureDir } ); 21 | } 22 | 23 | describe( "batch-convert", ( ) => 24 | { 25 | it( "batchConvert", withConsoleLog( async ( { log } ) => 26 | { 27 | const converter = makeMockConverter( ); 28 | await batchConvert( 29 | converter, 30 | [ 31 | path.join( fixtureDir, 'types1.ts' ), 32 | path.join( fixtureDir, 'sub', 'types2.ts' ), 33 | ], 34 | { 35 | outputExtension: '-', 36 | concurrency: 1, 37 | } 38 | ); 39 | 40 | expect( log.mock.calls.length ).toBe( 2 ); 41 | const types1 = JSON.parse( log.mock.calls[ 0 ] as any ); 42 | const types2 = JSON.parse( log.mock.calls[ 1 ] as any ); 43 | 44 | expect( types1 ).toMatchSnapshot( ); 45 | expect( types2 ).toMatchSnapshot( ); 46 | } ) ); 47 | 48 | it( "batchConvert verbose", withConsoleMock( async ( { log, error } ) => 49 | { 50 | const converter = makeMockConverter( ); 51 | await batchConvert( 52 | converter, 53 | [ 54 | path.join( fixtureDir, 'types1.ts' ), 55 | path.join( fixtureDir, 'sub', 'types2.ts' ), 56 | ], 57 | { 58 | outputExtension: '-', 59 | concurrency: 1, 60 | verbose: true, 61 | } 62 | ); 63 | 64 | expect( log.mock.calls.length ).toBe( 2 ); 65 | const types1 = JSON.parse( log.mock.calls[ 0 ] as any ); 66 | const types2 = JSON.parse( log.mock.calls[ 1 ] as any ); 67 | 68 | expect( types1 ).toMatchSnapshot( ); 69 | expect( types2 ).toMatchSnapshot( ); 70 | 71 | expect( error.mock.calls.length ).toBe( 3 ); 72 | expect( 73 | error.mock.calls 74 | .map( ( line, i ) => 75 | i === 0 76 | // Absolute local files system, must be aligned with CI/CD 77 | ? path.basename( line[ 0 ] as any ) 78 | : line 79 | ) 80 | ).toMatchSnapshot( ); 81 | }, [ 'log', 'error' ] ) ); 82 | 83 | it( "batchConvertGlob", withConsoleLog( async ( { log } ) => 84 | { 85 | const converter = makeMockConverter( ); 86 | await batchConvertGlob( 87 | converter, 88 | [ path.join( fixtureDir, '**/*.ts' ) ], 89 | { 90 | outputExtension: '-', 91 | concurrency: 1, 92 | filesTransform: files => files.sort( ), // be deterministic 93 | } 94 | ); 95 | 96 | expect( log.mock.calls.length ).toBe( 2 ); 97 | const types1 = JSON.parse( log.mock.calls[ 0 ] as any ); 98 | const types2 = JSON.parse( log.mock.calls[ 1 ] as any ); 99 | 100 | expect( types1 ).toMatchSnapshot( ); 101 | expect( types2 ).toMatchSnapshot( ); 102 | } ) ); 103 | } ); 104 | -------------------------------------------------------------------------------- /lib/batch-convert.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { map } from "already" 4 | import chalk from "chalk" 5 | 6 | import { Converter, Target } from "./converter.js" 7 | import { glob, reRootFiles, prettyFile } from "./file.js" 8 | 9 | export interface BatchConvertOptions 10 | { 11 | outputDirectory?: string; 12 | outputExtension: string; 13 | verbose?: boolean; 14 | dryRun?: boolean; 15 | concurrency?: number; 16 | } 17 | 18 | export interface BatchConvertGlobOptions extends BatchConvertOptions 19 | { 20 | hidden?: boolean; 21 | filesTransform?: ( files: Array< string > ) => Array< string >; 22 | } 23 | 24 | export interface BatchConvertResult 25 | { 26 | files: number; 27 | types: number; 28 | } 29 | 30 | export async function batchConvert( 31 | converter: Converter, 32 | filenames: Array< string >, 33 | options: BatchConvertOptions 34 | ) 35 | : Promise< BatchConvertResult > 36 | { 37 | const { 38 | outputExtension, 39 | verbose, 40 | dryRun, 41 | // Not really CPU concurrency in this case, but enough I/O concurrency 42 | // to always have files prepared/read/written for CPU-bound conversion. 43 | // Set to 1 for deterministic unit tests. 44 | concurrency = 16 45 | } = options; 46 | 47 | const { cwd, convert, fromFormat } = converter; 48 | 49 | const { root, newRoot, files } = 50 | reRootFiles( filenames, cwd, options.outputDirectory ); 51 | 52 | if ( verbose ) 53 | { 54 | console.error( `Converting files relative to ${root}` ); 55 | if ( newRoot !== root ) 56 | console.error( `Storing files in ${newRoot}` ); 57 | } 58 | 59 | const innerExtension: string | undefined = 60 | fromFormat === 'ts' 61 | ? '.d' 62 | : undefined; 63 | 64 | const changeExt = ( outFile: string, outExt: string ) => 65 | changeExtension( outFile, outExt, innerExtension ); 66 | 67 | const firstOverwritten = files.find( file => 68 | file.in === changeExt( file.out, outputExtension ) 69 | ); 70 | if ( outputExtension !== '-' && firstOverwritten ) 71 | throw new Error( 72 | "Won't convert - would overwrite source file with target file: " + 73 | firstOverwritten.out 74 | ); 75 | 76 | let convertedTypes = 0; 77 | 78 | await map( 79 | files, 80 | { concurrency }, 81 | async ( { in: filename, out: outFilename, rel } ) => 82 | { 83 | const to: Target = 84 | ( dryRun || outputExtension === '-' ) 85 | ? undefined as unknown as Target 86 | : { 87 | filename: changeExt( outFilename, outputExtension ), 88 | relFilename: changeExt( rel, outputExtension ), 89 | }; 90 | 91 | const { data, ...info } = await convert( { filename, cwd }, to ); 92 | 93 | convertedTypes += info.out.convertedTypes.length; 94 | 95 | if ( verbose ) 96 | { 97 | const outputRel = changeExt( rel, outputExtension ); 98 | const outName = outputExtension === '-' ? 'stdout' : outputRel; 99 | 100 | const allInputTypes = 101 | info.in.convertedTypes.length + 102 | info.in.notConvertedTypes.length; 103 | const notConverted = 104 | info.in.notConvertedTypes.length + 105 | info.out.notConvertedTypes.length; 106 | const converted = info.out.convertedTypes.length; 107 | const percent = 108 | allInputTypes === 0 109 | ? 'no' 110 | : `${Math.round( converted * 100 / allInputTypes )}%`; 111 | 112 | const prefixText = '[typeconv]'; 113 | const prefix = 114 | allInputTypes === 0 115 | ? chalk.gray( prefixText ) 116 | : notConverted 117 | ? chalk.hex( '#D2D200' )( prefixText ) 118 | : chalk.hex( '#00D21F' )( prefixText ); 119 | 120 | console.error( 121 | `${prefix} ${prettyFile( rel, root )} -> ` + 122 | `${prettyFile( outName, newRoot )}, ${percent} types ` + 123 | `converted (${converted}/${allInputTypes})` + 124 | ( !notConverted ? '' : `, ${notConverted} rejected` ) 125 | ); 126 | } 127 | 128 | if ( outputExtension === '-' && !dryRun ) 129 | console.log( data ); 130 | } 131 | ); 132 | 133 | return { 134 | types: convertedTypes, 135 | files: files.length, 136 | }; 137 | } 138 | 139 | export async function batchConvertGlob( 140 | converter: Converter, 141 | globs: Array< string >, 142 | options: BatchConvertGlobOptions 143 | ) 144 | : Promise< BatchConvertResult > 145 | { 146 | const { hidden, filesTransform = v => v } = options; 147 | const { cwd } = converter; 148 | const files = await glob( globs, cwd, hidden ); 149 | return batchConvert( converter, filesTransform( files ), options ); 150 | } 151 | 152 | function changeExtension( 153 | filename: string, 154 | extension: string, 155 | innerExtension?: string 156 | ) 157 | { 158 | extension = extension.startsWith( '.' ) ? extension : `.${extension}`; 159 | 160 | const baseName = path.basename( filename, path.extname( filename ) ); 161 | const innerName = 162 | innerExtension 163 | ? path.basename( baseName, innerExtension ) 164 | : baseName; 165 | 166 | return path.join( path.dirname( filename ), innerName + extension ); 167 | } 168 | -------------------------------------------------------------------------------- /lib/bin/__snapshots__/typeconv.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`cli should convert typescript to graphql 1`] = ` 4 | "# The file types1.graphql was auto-generated from fixtures/types1.ts using 5 | # core-types-graphql (https://github.com/grantila/core-types-graphql) 6 | # on behalf of 7 | # typeconv (https://github.com/grantila/typeconv) 8 | 9 | type Foo { 10 | bar: String! 11 | } 12 | 13 | type Foo2 { 14 | bak: [Boolean!]! 15 | } 16 | " 17 | `; 18 | 19 | exports[`cli should convert typescript to graphql 2`] = ` 20 | "# The file sub/types2.graphql was auto-generated from fixtures/sub/types2.ts using 21 | # core-types-graphql (https://github.com/grantila/core-types-graphql) 22 | # on behalf of 23 | # typeconv (https://github.com/grantila/typeconv) 24 | 25 | type SubDirType { 26 | name: String! 27 | } 28 | " 29 | `; 30 | -------------------------------------------------------------------------------- /lib/bin/typeconv.test.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from "fs" 2 | import path from "path" 3 | 4 | import { execa } from "execa" 5 | import { temporaryDirectory } from "tempy" 6 | 7 | describe( "cli", ( ) => 8 | { 9 | it( "should convert typescript to graphql", async ( ) => 10 | { 11 | const dir = temporaryDirectory( ); 12 | await execa( 13 | "node", 14 | [ 15 | "dist/bin/typeconv.js", 16 | "--verbose", 17 | "-f", 18 | "ts", 19 | "-t", 20 | "gql", 21 | "-o", 22 | dir, 23 | "fixtures/**" 24 | ] 25 | ); 26 | 27 | const result1 = 28 | await fsPromises.readFile( 29 | path.join( dir, "types1.graphql" ), 30 | "utf-8" 31 | ); 32 | 33 | const result2 = 34 | await fsPromises.readFile( 35 | path.join( dir, "sub/types2.graphql" ), 36 | "utf-8" 37 | ); 38 | 39 | expect( result1 ).toMatchSnapshot( ); 40 | expect( result2 ).toMatchSnapshot( ); 41 | } ); 42 | } ); 43 | -------------------------------------------------------------------------------- /lib/bin/typeconv.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { promises as fsPromises } from "fs" 4 | import path from "path" 5 | import { fileURLToPath } from "url" 6 | 7 | import chalk from "chalk" 8 | import { oppa } from "oppa" 9 | import { stripAnnotations } from "core-types" 10 | import type { JsonSchemaToSuretypeOptions } from "core-types-suretype" 11 | import type { FromTsOptions, ToTsOptions } from "core-types-ts" 12 | import type { ExportRefMethod } from "suretype" 13 | 14 | import { makeConverter } from "../converter.js" 15 | import { batchConvertGlob } from "../batch-convert.js" 16 | import { Reader } from "../reader.js" 17 | import { Writer } from "../writer.js" 18 | import { 19 | getJsonSchemaReader, 20 | getJsonSchemaWriter, 21 | getOpenApiReader, 22 | getOpenApiWriter, 23 | } from "../convert-json-schema.js" 24 | import { 25 | getGraphQLReader, 26 | getGraphQLWriter, 27 | } from "../convert-graphql.js" 28 | import { 29 | getTypeScriptReader, 30 | getTypeScriptWriter, 31 | } from "../convert-typescript.js" 32 | import { 33 | getCoreTypesReader, 34 | getCoreTypesWriter, 35 | } from "../convert-core-types.js" 36 | import { 37 | getSureTypeReader, 38 | getSureTypeWriter, 39 | } from "../convert-suretype.js" 40 | import { userPackage, userPackageUrl } from "../package.js" 41 | import { TypeImplementation } from "../types.js" 42 | import { ensureType } from "../utils.js" 43 | 44 | 45 | const __filename = fileURLToPath( import.meta.url ); 46 | const __dirname = path.dirname( __filename ); 47 | 48 | type SureTypeMissingRef = JsonSchemaToSuretypeOptions[ 'missingReference' ]; 49 | 50 | const implementations: Array< Record< TypeImplementation, string > > 51 | = [ 52 | { ts: "TypeScript" } as Record< TypeImplementation, string >, 53 | { jsc: "JSON Schema" } as Record< TypeImplementation, string >, 54 | { gql: "GraphQL" } as Record< TypeImplementation, string >, 55 | { oapi: "Open API" } as Record< TypeImplementation, string >, 56 | { st: "SureType" } as Record< TypeImplementation, string >, 57 | { ct: "core-types" } as Record< TypeImplementation, string >, 58 | ]; 59 | 60 | const { version } = 61 | JSON.parse( 62 | await fsPromises.readFile( 63 | path.resolve( __dirname, "..", "..", "package.json" ), 64 | 'utf-8' 65 | ) 66 | ); 67 | 68 | const oppaInstance = 69 | oppa( { 70 | version: version, 71 | usage: "typeconv [options] file ...", 72 | noVersionAlias: true, 73 | } ) 74 | .add( { 75 | name: 'verbose', 76 | alias: 'v', 77 | type: 'boolean', 78 | negatable: false, 79 | description: "Verbose informational output", 80 | default: false, 81 | } ) 82 | .add( { 83 | name: 'dry-run', 84 | type: 'boolean', 85 | negatable: false, 86 | description: "Prepare and perform conversion, but write no output", 87 | default: false, 88 | } ) 89 | .add( { 90 | name: 'hidden', 91 | type: 'boolean', 92 | negatable: true, 93 | description: [ 94 | "Include hidden files, i.e. files in .gitignore,", 95 | "files beginning with '.' and the '.git' directory", 96 | ], 97 | default: true, 98 | } ) 99 | .add( { 100 | name: 'from-type', 101 | argumentName: 'type', 102 | alias: 'f', 103 | type: 'string', 104 | description: "Type system to convert from", 105 | values: implementations, 106 | } ) 107 | .add( { 108 | name: 'to-type', 109 | argumentName: 'type', 110 | alias: 't', 111 | type: 'string', 112 | description: "Type system to convert to", 113 | values: implementations, 114 | } ) 115 | .add( { 116 | name: 'shortcut', 117 | type: 'boolean', 118 | description: [ 119 | "Shortcut conversion if possible (bypassing core-types).", 120 | "This is possible between SureType, JSON Schema and Open API", 121 | "to preserve all features which would otherwise be erased." 122 | ], 123 | default: true, 124 | negatable: true, 125 | } ) 126 | .add( { 127 | name: 'output-directory', 128 | argumentName: 'dir', 129 | alias: 'o', 130 | type: 'string', 131 | description: 132 | "Output directory. Defaults to the same as the input files.", 133 | } ) 134 | .add( { 135 | name: 'output-extension', 136 | argumentName: 'ext', 137 | alias: 'O', 138 | type: 'string', 139 | description: [ 140 | "Output filename extension to use.", 141 | "Defaults to 'ts'/'d.ts', 'json', 'yaml' and 'graphql'.", 142 | "Use '-' to not save a file, but output to stdout instead." 143 | ], 144 | } ) 145 | .add( { 146 | name: 'strip-annotations', 147 | type: 'boolean', 148 | description: "Removes all annotations (descriptions, comments, ...)", 149 | default: false, 150 | } ) 151 | .add( { 152 | name: 'merge-objects', 153 | type: 'boolean', 154 | description: [ 155 | "When simplifying the types, merge intersections of objects", 156 | "into single self-contained objects.", 157 | "This defaults to true when converting to JSON Schema/OpenAPI." 158 | ], 159 | } ) 160 | 161 | .group( { 162 | name: "TypeScript", 163 | backgroundColor: '#0077c8', 164 | color: '#fff', 165 | } ) 166 | .add( { 167 | name: 'ts-declaration', 168 | type: 'boolean', 169 | description: "Output TypeScript declarations", 170 | default: false, 171 | } ) 172 | .add( { 173 | name: 'ts-disable-lint-header', 174 | type: 'boolean', 175 | description: "Output comments for disabling linting", 176 | default: true, 177 | } ) 178 | .add( { 179 | name: 'ts-descriptive-header', 180 | type: 'boolean', 181 | description: "Output the header comment", 182 | default: true, 183 | } ) 184 | .add( { 185 | name: 'ts-use-unknown', 186 | type: 'boolean', 187 | description: "Use 'unknown' type instead of 'any'", 188 | default: true, 189 | } ) 190 | .add( { 191 | name: 'ts-non-exported', 192 | type: 'string', 193 | argumentName: 'method', 194 | description: "Strategy for non-exported types", 195 | default: 'include-if-referenced', 196 | values: [ 197 | { 'fail': "Fail conversion" }, 198 | { 'ignore': [ 199 | "Don't include non-exported types,", 200 | "even if referenced", 201 | ] }, 202 | { 'include': "Include non-exported types" }, 203 | { 'inline': [ 204 | "Don't include non-exported types, ", 205 | "inline them if necessary.", 206 | "Will fail on cyclic types" 207 | ] }, 208 | { 'include-if-referenced': [ 209 | "Include non-exported types only if they", 210 | "are referenced from exported types", 211 | ] }, 212 | ], 213 | } ) 214 | .add( { 215 | name: "ts-namespaces", 216 | type: "string", 217 | argumentName: "method", 218 | description: [ 219 | "Namespace strategy." 220 | ], 221 | default: "ignore", 222 | values: [ 223 | { "ignore": [ 224 | "Ignore namespaces entirely (default).", 225 | "- When converting from TypeScript, types in namespaces", 226 | "aren't exported.", 227 | "- When converting to TypeScript, no attempt to", 228 | "reconstruct namespaces is performed.", 229 | ] }, 230 | { "hoist": [ 231 | "When converting from TypeScript, hoist types inside", 232 | "namespaces to top-level, so that the types are", 233 | "included, but without their namespace.", 234 | "This can cause conflicts, in which case deeper", 235 | "declarations will be dropped in favor of more top-", 236 | "level declarations.", 237 | "In case of same-level (namespace depth) declarations", 238 | "with the same name, only one will be exported in a", 239 | "non-deterministic manner.", 240 | ] }, 241 | { "dot": [ 242 | "When converting from TypeScript, join the namespaces", 243 | "and the exported type with a dot (.).", 244 | "When converting to TypeScript, try to reconstruct", 245 | "namespaces by splitting the name on dot (.).", 246 | ] }, 247 | { "underscore": [ 248 | "When converting from TypeScript, join the namespaces", 249 | "and the exported type with an underscore (_).", 250 | "When converting to TypeScript, try to reconstruct", 251 | "namespaces by splitting the name on underscore (_).", 252 | ] }, 253 | { "reconstruct-all": [ 254 | "When converting to TypeScript, try to reconstruct", 255 | "namespaces by splitting the name on both dot and", 256 | "underscore.", 257 | ] }, 258 | ], 259 | } ) 260 | 261 | .group( { 262 | name: "GraphQL", 263 | backgroundColor: '#e10098', 264 | color: '#fff', 265 | } ) 266 | .add( { 267 | name: 'gql-unsupported', 268 | argumentName: 'method', 269 | type: 'string', 270 | description: "Method to use for unsupported types", 271 | values: [ 272 | { ignore: 'Ignore (skip) type' }, 273 | { warn: 'Ignore type, but warn' }, 274 | { error: 'Throw an error' }, 275 | ], 276 | } ) 277 | .add( { 278 | name: 'gql-null-typename', 279 | argumentName: 'name', 280 | type: 'string', 281 | description: "Custom type name to use for null", 282 | } ) 283 | .group( { 284 | name: "Open API", 285 | backgroundColor: '#85ea2d', 286 | color: '#222', 287 | } ) 288 | .add( { 289 | name: 'oapi-format', 290 | argumentName: 'fmt', 291 | type: 'string', 292 | description: "Output format for Open API", 293 | values: [ 294 | { json: "JSON" }, 295 | { yaml: "YAML ('yml' is also allowed)" }, 296 | ], 297 | default: 'yaml', 298 | } ) 299 | .add( { 300 | name: 'oapi-title', 301 | argumentName: 'title', 302 | type: 'string', 303 | description: [ 304 | "Open API title to use in output document.", 305 | "Defaults to the input filename." 306 | ], 307 | } ) 308 | .add( { 309 | name: 'oapi-version', 310 | argumentName: 'version', 311 | type: 'string', 312 | description: "Open API document version to use in output document.", 313 | default: '1', 314 | } ) 315 | .group( { 316 | name: "SureType", 317 | backgroundColor: '#4f81a0', 318 | color: '#fff', 319 | } ) 320 | .add( { 321 | name: 'st-ref-method', 322 | argumentName: 'method', 323 | type: 'string', 324 | description: "SureType reference export method", 325 | values: [ 326 | { "no-refs": "Don't ref anything, inline all types" }, 327 | { "provided": "Reference types that are explicitly exported" }, 328 | { "ref-all": "Ref all provided types and those with names" }, 329 | ], 330 | default: 'provided', 331 | } ) 332 | .add( { 333 | name: 'st-missing-ref', 334 | argumentName: 'method', 335 | type: 'string', 336 | description: "What to do when detecting an unresolvable reference", 337 | values: [ 338 | { "ignore": "Ignore; skip type or cast to any" }, 339 | { "warn": "Same as 'ignore', but warn" }, 340 | { "error": "Fail conversion" }, 341 | ], 342 | default: 'warn', 343 | } ) 344 | .add( { 345 | name: 'st-inline-types', 346 | type: 'boolean', 347 | default: true, 348 | description: "Inline pretty typescript types aside validator code", 349 | } ) 350 | .add( { 351 | name: 'st-export-type', 352 | type: 'boolean', 353 | description: [ 354 | "Export the deduced types (or the pretty types,", 355 | "depending on --st-inline-types)", 356 | ], 357 | default: true, 358 | } ) 359 | .add( { 360 | name: 'st-export-schema', 361 | type: 'boolean', 362 | description: "Export validator schemas", 363 | default: false, 364 | } ) 365 | .add( { 366 | name: 'st-export-validator', 367 | type: 'boolean', 368 | description: "Export regular validators", 369 | default: true, 370 | } ) 371 | .add( { 372 | name: 'st-export-ensurer', 373 | type: 'boolean', 374 | description: "Export 'ensurer' validators", 375 | default: true, 376 | } ) 377 | .add( { 378 | name: 'st-export-type-guard', 379 | type: 'boolean', 380 | description: "Export type guards (is* validators)", 381 | default: true, 382 | } ) 383 | .add( { 384 | name: 'st-use-unknown', 385 | type: 'boolean', 386 | description: "Use 'unknown' type instead of 'any'", 387 | default: true, 388 | } ) 389 | .add( { 390 | name: 'st-forward-schema', 391 | type: 'boolean', 392 | description: [ 393 | "Forward the JSON Schema, and create an untyped validator schema", 394 | "with the raw JSON Schema under the hood", 395 | ], 396 | default: false, 397 | } ); 398 | 399 | const result = oppaInstance.parse( ); 400 | 401 | const printHelp = ( ) => oppaInstance.showHelp( true ); 402 | 403 | const { args, rest: globFiles } = result; 404 | 405 | const { 406 | verbose, 407 | "dry-run": dryRun, 408 | hidden, 409 | "from-type": fromType, 410 | "to-type": toType, 411 | shortcut, 412 | "output-directory": outputDirectory, 413 | "output-extension": outputExtension, 414 | "strip-annotations": doStripAnnotations, 415 | "merge-objects": mergeObjects, 416 | 417 | // TypeScript 418 | "ts-declaration": tsDeclaration, 419 | "ts-disable-lint-header": tsDisableLintHeader, 420 | "ts-descriptive-header": tsDescriptiveHeader, 421 | "ts-use-unknown": tsUseUnknown, 422 | "ts-non-exported": tsNonExported, 423 | "ts-namespaces": tsNamespaces, 424 | 425 | // JSON Schema 426 | 427 | // GraphQL 428 | "gql-unsupported": gqlUnsupported, 429 | "gql-null-typename": gqlNullTypename, 430 | 431 | // Open API 432 | "oapi-format": oapiFormat, 433 | "oapi-title": oapiTitle, 434 | "oapi-version": oapiVersion, 435 | 436 | // suretype 437 | "st-ref-method": stRefMethod, 438 | "st-missing-ref": stMissingReference, 439 | "st-inline-types": stInlineTypes, 440 | "st-export-type": stExportType, 441 | "st-export-schema": stExportSchema, 442 | "st-export-validator": stExportValidator, 443 | "st-export-ensurer": stExportEnsurer, 444 | "st-export-type-guard": stExportTypeGuard, 445 | "st-use-unknown": stUseUnknown, 446 | "st-forward-schema": stForwardSchema, 447 | } = args; 448 | 449 | const typeImplementations = implementations.map( obj => 450 | Object.keys( obj )[ 0 ] as TypeImplementation 451 | ); 452 | 453 | if ( !ensureType< TypeImplementation >( 454 | fromType, 455 | 'type system identifyer', 456 | typeImplementations, 457 | printHelp 458 | ) ) 459 | throw new Error( ); 460 | 461 | if ( !ensureType< TypeImplementation >( 462 | toType, 463 | 'type system identifyer', 464 | typeImplementations, 465 | printHelp 466 | ) ) 467 | throw new Error( ); 468 | 469 | if ( !ensureType< FromTsOptions[ 'nonExported' ] >( 470 | tsNonExported, 471 | 'ts-non-exported', 472 | [ 'fail', 'ignore', 'include', 'inline', 'include-if-referenced' ], 473 | printHelp 474 | ) ) 475 | throw new Error( ); 476 | 477 | const mergeObjectsDefault = toType === 'jsc' || toType === 'oapi'; 478 | 479 | if ( !ensureType< 480 | 'ignore' | 'hoist' | 'dot' | 'underscore' | 'reconstruct-all' 481 | >( 482 | tsNamespaces, 483 | 'ts-namespaces', 484 | [ 'ignore', 'hoist', 'dot', 'underscore', 'reconstruct-all' ], 485 | printHelp 486 | ) ) 487 | throw new Error( ); 488 | 489 | const toTsNamespaces: ToTsOptions[ 'namespaces' ] = 490 | tsNamespaces === 'reconstruct-all' ? 'all' : 491 | tsNamespaces === 'dot' ? 'dot' : 492 | tsNamespaces === 'underscore' ? 'underscore' : 493 | 'ignore'; 494 | 495 | const fromTsNamespaces: FromTsOptions[ 'namespaces' ] = 496 | tsNamespaces === 'hoist' ? 'hoist' : 497 | tsNamespaces === 'dot' ? 'join-dot' : 498 | tsNamespaces === 'underscore' ? 'join-underscore' : 499 | 'ignore'; 500 | 501 | if ( !ensureType< ExportRefMethod | undefined >( 502 | stRefMethod, 503 | 'ref-method', 504 | [ 'no-refs', 'provided', 'ref-all', undefined ], 505 | printHelp 506 | ) ) 507 | throw new Error( ); 508 | 509 | if ( !ensureType< SureTypeMissingRef | undefined >( 510 | stMissingReference, 511 | 'missing-ref', 512 | [ 'ignore', 'warn', 'error', undefined ], 513 | printHelp 514 | ) ) 515 | throw new Error( ); 516 | 517 | const getReader = ( ): Reader => 518 | { 519 | return fromType === 'ts' 520 | ? getTypeScriptReader( { 521 | nonExported: tsNonExported, 522 | namespaces: fromTsNamespaces, 523 | } ) 524 | : fromType === 'jsc' 525 | ? getJsonSchemaReader( ) 526 | : fromType === 'oapi' 527 | ? getOpenApiReader( ) 528 | : fromType === 'gql' 529 | ? getGraphQLReader( { 530 | unsupported: gqlUnsupported as 'ignore' | 'warn' | 'error', 531 | } ) 532 | : fromType === 'st' 533 | ? getSureTypeReader( { 534 | refMethod: stRefMethod, 535 | nameConflict: 'error', 536 | } ) 537 | : getCoreTypesReader( ); 538 | } 539 | 540 | const getWriter = ( ): Writer => 541 | { 542 | return toType === 'ts' 543 | ? getTypeScriptWriter( { 544 | userPackage, 545 | userPackageUrl, 546 | declaration: tsDeclaration, 547 | noDisableLintHeader: tsDisableLintHeader, 548 | noDescriptiveHeader: tsDescriptiveHeader, 549 | namespaces: toTsNamespaces, 550 | useUnknown: tsUseUnknown, 551 | } ) 552 | : toType === 'jsc' 553 | ? getJsonSchemaWriter( ) 554 | : toType === 'oapi' 555 | ? getOpenApiWriter( { 556 | format: oapiFormat, 557 | title: oapiTitle, 558 | version: oapiVersion, 559 | } ) 560 | : toType === 'gql' 561 | ? getGraphQLWriter( { 562 | unsupported: gqlUnsupported as 'ignore' | 'warn' | 'error', 563 | nullTypeName: gqlNullTypename, 564 | } ) 565 | : toType === 'st' 566 | ? getSureTypeWriter( { 567 | inlineTypes: stInlineTypes, 568 | exportType: stExportType, 569 | exportSchema: stExportSchema, 570 | exportValidator: stExportValidator, 571 | exportEnsurer: stExportEnsurer, 572 | exportTypeGuard: stExportTypeGuard, 573 | useUnknown: stUseUnknown, 574 | forwardSchema: stForwardSchema, 575 | missingReference: stMissingReference, 576 | } ) 577 | : getCoreTypesWriter( ); 578 | } 579 | 580 | ( async ( ) => 581 | { 582 | const cwd = process.cwd( ); 583 | 584 | const converter = makeConverter( 585 | getReader( ), 586 | getWriter( ), 587 | { 588 | ...( 589 | !doStripAnnotations ? { } : 590 | { map: ( node => stripAnnotations( node ) ) } 591 | ), 592 | mergeObjects: mergeObjects ?? mergeObjectsDefault, 593 | cwd, 594 | shortcut, 595 | } 596 | ); 597 | 598 | const before = Date.now( ); 599 | 600 | const result = await batchConvertGlob( 601 | converter, 602 | globFiles, 603 | { 604 | outputDirectory, 605 | outputExtension: 606 | outputExtension ?? ( 607 | toType === 'ts' ? '.ts' 608 | : toType === 'jsc' ? '.json' 609 | : toType === 'oapi' ? `.${oapiFormat}` 610 | : toType === 'gql' ? '.graphql' 611 | : toType === 'st' ? '.ts' 612 | : '.json' 613 | ), 614 | verbose, 615 | dryRun, 616 | hidden, 617 | } 618 | ); 619 | 620 | const after = Date.now( ); 621 | 622 | const sec = ( ( after - before ) / 1000 ).toFixed( 1 ); 623 | 624 | console.error( chalk.bold( 625 | `💡 Converted ${result.types} types in ${result.files} files, ` + 626 | `in ${sec}s` 627 | ) ); 628 | } )( ) 629 | .catch( ( err: any ) => 630 | { 631 | console.error( process.env.DEBUG ? err?.stack : err?.message ); 632 | process.exit( 1 ); 633 | } ); 634 | -------------------------------------------------------------------------------- /lib/convert-core-types.test.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument } from "core-types" 2 | import { jest } from "@jest/globals" 3 | 4 | import { 5 | getCoreTypesReader, 6 | getCoreTypesWriter, 7 | } from "./convert-core-types.js" 8 | 9 | 10 | const fixture: NodeDocument = { 11 | version: 1, 12 | types: [ 13 | { 14 | name: 'Foo', 15 | title: 'The foo', 16 | type: 'string', 17 | }, 18 | ], 19 | }; 20 | 21 | const warn = jest.fn( ); 22 | 23 | describe( "convert-core-types", ( ) => 24 | { 25 | describe( "getCoreTypesReader", ( ) => 26 | { 27 | it( "should have correct kind", ( ) => 28 | { 29 | expect( getCoreTypesReader( ).kind ).toBe( "ct" ); 30 | } ); 31 | 32 | it( "should read properly", ( ) => 33 | { 34 | expect( 35 | getCoreTypesReader( ).read( 36 | JSON.stringify( fixture ), 37 | { warn } 38 | ) 39 | ).toStrictEqual( { 40 | data: fixture, 41 | convertedTypes: [ 'Foo' ], 42 | notConvertedTypes: [ ], 43 | } ); 44 | } ); 45 | } ); 46 | 47 | describe( "getCoreTypesWriter", ( ) => 48 | { 49 | it( "should have correct kind", ( ) => 50 | { 51 | expect( getCoreTypesWriter( ).kind ).toBe( "ct" ); 52 | } ); 53 | 54 | it( "should write properly", async ( ) => 55 | { 56 | const { 57 | data, 58 | convertedTypes, 59 | notConvertedTypes, 60 | } = await getCoreTypesWriter( ).write( 61 | fixture, 62 | { warn, rawInput: '' } 63 | ); 64 | 65 | const resData = JSON.parse( data ); 66 | 67 | expect( resData ).toStrictEqual( fixture ); 68 | expect( convertedTypes ).toStrictEqual( [ 'Foo' ] ); 69 | expect( notConvertedTypes ).toStrictEqual( [ ] ); 70 | } ); 71 | } ); 72 | } ); 73 | -------------------------------------------------------------------------------- /lib/convert-core-types.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument } from "core-types" 2 | 3 | import type { Reader } from "./reader.js" 4 | import type { Writer } from "./writer.js" 5 | import { stringify } from "./utils.js" 6 | import { registerReader, registerWriter } from "./format-graph.js" 7 | 8 | 9 | export function getCoreTypesReader( ): Reader 10 | { 11 | return { 12 | kind: 'ct', 13 | read( source ) 14 | { 15 | const doc = JSON.parse( source ) as NodeDocument; 16 | return { 17 | data: doc, 18 | convertedTypes: doc.types.map( ( { name } ) => name ), 19 | notConvertedTypes: [ ], 20 | }; 21 | }, 22 | }; 23 | } 24 | 25 | export function getCoreTypesWriter( ): Writer 26 | { 27 | return { 28 | kind: 'ct', 29 | write( doc ) 30 | { 31 | return { 32 | data: stringify( doc ), 33 | convertedTypes: doc.types.map( ( { name } ) => name ), 34 | notConvertedTypes: [ ], 35 | }; 36 | }, 37 | }; 38 | } 39 | 40 | registerReader( getCoreTypesReader( ) ); 41 | registerWriter( getCoreTypesWriter( ) ); 42 | -------------------------------------------------------------------------------- /lib/convert-graphql.test.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument } from "core-types" 2 | import { jest } from "@jest/globals" 3 | 4 | import { getGraphQLReader, getGraphQLWriter } from "./convert-graphql.js" 5 | 6 | 7 | const schema = `"Foo type" 8 | type Foo { 9 | bar: String! 10 | num: Float 11 | int: Int 12 | } 13 | `; 14 | 15 | const warn = jest.fn( ); 16 | 17 | describe( "convert-graphql", ( ) => 18 | { 19 | describe( "getGraphQLReader", ( ) => 20 | { 21 | it( "should have correct kind", ( ) => 22 | { 23 | expect( getGraphQLReader( ).kind ).toBe( "gql" ); 24 | } ); 25 | 26 | it( "should read properly", ( ) => 27 | { 28 | expect( 29 | getGraphQLReader( ).read( schema, { warn } ) 30 | ).toStrictEqual( { 31 | data: { 32 | version: 1, 33 | types: [ 34 | { 35 | name: 'Foo', 36 | title: 'Foo', 37 | description: 'Foo type', 38 | type: 'object', 39 | loc: expect.anything( ), 40 | properties: { 41 | bar: { 42 | node: { 43 | type: 'string', 44 | title: 'Foo.bar', 45 | loc: expect.anything( ), 46 | }, 47 | required: true, 48 | }, 49 | num: { 50 | node: { 51 | type: 'number', 52 | title: 'Foo.num', 53 | loc: expect.anything( ), 54 | }, 55 | required: false, 56 | }, 57 | int: { 58 | node: { 59 | type: 'integer', 60 | title: 'Foo.int', 61 | loc: expect.anything( ), 62 | }, 63 | required: false, 64 | }, 65 | }, 66 | additionalProperties: false, 67 | } 68 | ], 69 | }, 70 | convertedTypes: [ 'Foo' ], 71 | notConvertedTypes: [ ], 72 | } ); 73 | } ); 74 | } ); 75 | 76 | describe( "getGraphQLWriter", ( ) => 77 | { 78 | it( "should have correct kind", ( ) => 79 | { 80 | expect( getGraphQLWriter( ).kind ).toBe( "gql" ); 81 | } ); 82 | 83 | it( "should write properly", async ( ) => 84 | { 85 | const doc: NodeDocument = { 86 | version: 1, 87 | types: [ 88 | { 89 | name: 'Foo', 90 | title: 'Foo', 91 | description: 'Foo type', 92 | type: 'object', 93 | properties: { 94 | bar: { 95 | node: { 96 | type: 'string', 97 | }, 98 | required: true, 99 | }, 100 | num: { 101 | node: { 102 | type: 'number', 103 | }, 104 | required: false, 105 | }, 106 | int: { 107 | node: { 108 | type: 'integer', 109 | }, 110 | required: false, 111 | }, 112 | }, 113 | additionalProperties: false, 114 | }, 115 | ], 116 | }; 117 | 118 | const writer = getGraphQLWriter( { includeComment: false } ); 119 | 120 | const { 121 | data, 122 | convertedTypes, 123 | notConvertedTypes, 124 | } = await writer.write( doc, { warn, rawInput: '' } ); 125 | 126 | expect( data ).toStrictEqual( schema ); 127 | expect( convertedTypes ).toStrictEqual( [ 'Foo' ] ); 128 | expect( notConvertedTypes ).toStrictEqual( [ ] ); 129 | } ); 130 | } ); 131 | } ); 132 | -------------------------------------------------------------------------------- /lib/convert-graphql.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertCoreTypesToGraphql, 3 | convertGraphqlToCoreTypes, 4 | CoreTypesToGraphqlOptions, 5 | GraphqlToCoreTypesOptions, 6 | } from 'core-types-graphql' 7 | 8 | import { Reader } from "./reader.js" 9 | import { Writer } from "./writer.js" 10 | import { userPackage, userPackageUrl } from "./package.js" 11 | import { registerReader, registerWriter } from "./format-graph.js" 12 | 13 | 14 | export function getGraphQLReader( graphqlOptions?: GraphqlToCoreTypesOptions ) 15 | : Reader 16 | { 17 | return { 18 | kind: 'gql', 19 | read( source, options ) 20 | { 21 | return convertGraphqlToCoreTypes( 22 | source, 23 | { 24 | ...options, 25 | ...graphqlOptions, 26 | } 27 | ); 28 | } 29 | }; 30 | } 31 | 32 | export function getGraphQLWriter( graphqlOptions?: CoreTypesToGraphqlOptions ) 33 | : Writer 34 | { 35 | return { 36 | kind: 'gql', 37 | write( doc, options ) 38 | { 39 | return convertCoreTypesToGraphql( 40 | doc, 41 | { 42 | warn: options.warn, 43 | filename: options.filename, 44 | sourceFilename: options.sourceFilename, 45 | userPackage, 46 | userPackageUrl, 47 | ...graphqlOptions, 48 | } 49 | ); 50 | } 51 | }; 52 | } 53 | 54 | registerReader( getGraphQLReader( ) ); 55 | registerWriter( getGraphQLWriter( ) ); 56 | -------------------------------------------------------------------------------- /lib/convert-json-schema.test.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument } from "core-types" 2 | import type { JSONSchema7 } from "json-schema" 3 | import type { PartialOpenApiSchema } from "openapi-json-schema" 4 | import { load as readYaml, dump as writeYaml } from "js-yaml" 5 | import { jest } from "@jest/globals" 6 | 7 | import { 8 | getJsonSchemaReader, 9 | getJsonSchemaWriter, 10 | getOpenApiReader, 11 | getOpenApiWriter, 12 | } from "./convert-json-schema.js" 13 | 14 | 15 | const jsonSchemaFixture: JSONSchema7 = { 16 | definitions: { 17 | Foo: { 18 | title: "Foo type", 19 | type: "object", 20 | properties: { 21 | bar: { type: "string" }, 22 | num: { type: "number" }, 23 | int: { type: "integer" }, 24 | }, 25 | required: [ "bar" ], 26 | additionalProperties: false, 27 | }, 28 | }, 29 | }; 30 | 31 | const openApiSchemaFixture: PartialOpenApiSchema = { 32 | info: { 33 | title: "Title", 34 | version: "1", 35 | }, 36 | openapi: "3.0.0", 37 | paths: { }, 38 | components: { 39 | schemas: { 40 | Foo: { 41 | title: "Foo type", 42 | type: "object", 43 | properties: { 44 | bar: { type: "string" }, 45 | num: { type: "number" }, 46 | int: { type: "integer" }, 47 | }, 48 | required: [ "bar" ], 49 | additionalProperties: false, 50 | }, 51 | }, 52 | }, 53 | }; 54 | 55 | const warn = jest.fn( ); 56 | 57 | describe( "convert-json-schema", ( ) => 58 | { 59 | describe( "getJsonSchemaReader", ( ) => 60 | { 61 | it( "should have correct kind", ( ) => 62 | { 63 | expect( getJsonSchemaReader( ).kind ).toBe( "jsc" ); 64 | } ); 65 | 66 | it( "should read properly", ( ) => 67 | { 68 | expect( 69 | getJsonSchemaReader( ).read( 70 | JSON.stringify( jsonSchemaFixture ), 71 | { warn } 72 | ) 73 | ).toStrictEqual( { 74 | data: { 75 | version: 1, 76 | types: [ 77 | { 78 | name: 'Foo', 79 | title: 'Foo type', 80 | type: 'object', 81 | properties: { 82 | bar: { 83 | node: { 84 | type: 'string', 85 | }, 86 | required: true, 87 | }, 88 | num: { 89 | node: { 90 | type: 'number', 91 | }, 92 | required: false, 93 | }, 94 | int: { 95 | node: { 96 | type: 'integer', 97 | }, 98 | required: false, 99 | }, 100 | }, 101 | additionalProperties: false, 102 | } 103 | ], 104 | }, 105 | convertedTypes: [ 'Foo' ], 106 | notConvertedTypes: [ ], 107 | } ); 108 | } ); 109 | } ); 110 | 111 | describe( "getJsonSchemaWriter", ( ) => 112 | { 113 | it( "should have correct kind", ( ) => 114 | { 115 | expect( getJsonSchemaWriter( ).kind ).toBe( "jsc" ); 116 | } ); 117 | 118 | it( "should write properly", async ( ) => 119 | { 120 | const doc: NodeDocument = { 121 | version: 1, 122 | types: [ 123 | { 124 | name: 'Foo', 125 | title: 'Foo type', 126 | type: 'object', 127 | properties: { 128 | bar: { 129 | node: { 130 | type: 'string', 131 | }, 132 | required: true, 133 | }, 134 | num: { 135 | node: { 136 | type: 'number', 137 | }, 138 | required: false, 139 | }, 140 | int: { 141 | node: { 142 | type: 'integer', 143 | }, 144 | required: false, 145 | }, 146 | }, 147 | additionalProperties: false, 148 | }, 149 | ], 150 | }; 151 | 152 | const writer = getJsonSchemaWriter( ); 153 | 154 | const { 155 | data, 156 | convertedTypes, 157 | notConvertedTypes, 158 | } = await writer.write( doc, { warn, rawInput: '' } ); 159 | 160 | const out = JSON.parse( data ); 161 | delete out.$comment; 162 | 163 | expect( out ).toStrictEqual( jsonSchemaFixture ); 164 | expect( convertedTypes ).toStrictEqual( [ 'Foo' ] ); 165 | expect( notConvertedTypes ).toStrictEqual( [ ] ); 166 | } ); 167 | } ); 168 | 169 | describe( "getOpenApiReader", ( ) => 170 | { 171 | it( "should have correct kind", ( ) => 172 | { 173 | expect( getOpenApiReader( ).kind ).toBe( "oapi" ); 174 | } ); 175 | 176 | it( "should read properly (json)", ( ) => 177 | { 178 | expect( 179 | getOpenApiReader( ).read( 180 | JSON.stringify( openApiSchemaFixture ), 181 | { warn } 182 | ) 183 | ).toStrictEqual( { 184 | data: { 185 | version: 1, 186 | types: [ 187 | { 188 | name: 'Foo', 189 | title: 'Foo type', 190 | type: 'object', 191 | properties: { 192 | bar: { 193 | node: { 194 | type: 'string', 195 | }, 196 | required: true, 197 | }, 198 | num: { 199 | node: { 200 | type: 'number', 201 | }, 202 | required: false, 203 | }, 204 | int: { 205 | node: { 206 | type: 'integer', 207 | }, 208 | required: false, 209 | }, 210 | }, 211 | additionalProperties: false, 212 | } 213 | ], 214 | }, 215 | convertedTypes: [ 'Foo' ], 216 | notConvertedTypes: [ ], 217 | } ); 218 | } ); 219 | 220 | it( "should read properly (yaml)", ( ) => 221 | { 222 | expect( 223 | getOpenApiReader( ).read( 224 | writeYaml( openApiSchemaFixture ), 225 | { warn, filename: 'foo.yaml' } 226 | ) 227 | ).toStrictEqual( { 228 | data: { 229 | version: 1, 230 | types: [ 231 | { 232 | name: 'Foo', 233 | title: 'Foo type', 234 | type: 'object', 235 | properties: { 236 | bar: { 237 | node: { 238 | type: 'string', 239 | }, 240 | required: true, 241 | }, 242 | num: { 243 | node: { 244 | type: 'number', 245 | }, 246 | required: false, 247 | }, 248 | int: { 249 | node: { 250 | type: 'integer', 251 | }, 252 | required: false, 253 | }, 254 | }, 255 | additionalProperties: false, 256 | } 257 | ], 258 | }, 259 | convertedTypes: [ 'Foo' ], 260 | notConvertedTypes: [ ], 261 | } ); 262 | } ); 263 | } ); 264 | 265 | describe( "getOpenApiWriter", ( ) => 266 | { 267 | const getWriter = ( format: string ) => 268 | getOpenApiWriter( { 269 | format, 270 | title: openApiSchemaFixture.info.title, 271 | version: openApiSchemaFixture.info.version, 272 | } ); 273 | 274 | it( "should have correct kind", ( ) => 275 | { 276 | expect( getWriter( 'json' ).kind ).toBe( "oapi" ); 277 | } ); 278 | 279 | it( "should write properly (json)", async ( ) => 280 | { 281 | const doc: NodeDocument = { 282 | version: 1, 283 | types: [ 284 | { 285 | name: 'Foo', 286 | title: 'Foo type', 287 | type: 'object', 288 | properties: { 289 | bar: { 290 | node: { 291 | type: 'string', 292 | }, 293 | required: true, 294 | }, 295 | num: { 296 | node: { 297 | type: 'number', 298 | }, 299 | required: false, 300 | }, 301 | int: { 302 | node: { 303 | type: 'integer', 304 | }, 305 | required: false, 306 | }, 307 | }, 308 | additionalProperties: false, 309 | }, 310 | ], 311 | }; 312 | 313 | const writer = getWriter( 'json' ); 314 | 315 | const { 316 | data, 317 | convertedTypes, 318 | notConvertedTypes, 319 | } = await writer.write( doc, { warn, rawInput: '' } ); 320 | 321 | const out = JSON.parse( data ); 322 | delete out.info['x-comment']; 323 | 324 | expect( out ).toStrictEqual( openApiSchemaFixture ); 325 | expect( convertedTypes ).toStrictEqual( [ 'Foo' ] ); 326 | expect( notConvertedTypes ).toStrictEqual( [ ] ); 327 | } ); 328 | 329 | it( "should write properly (yaml)", async ( ) => 330 | { 331 | const doc: NodeDocument = { 332 | version: 1, 333 | types: [ 334 | { 335 | name: 'Foo', 336 | title: 'Foo type', 337 | type: 'object', 338 | properties: { 339 | bar: { 340 | node: { 341 | type: 'string', 342 | }, 343 | required: true, 344 | }, 345 | num: { 346 | node: { 347 | type: 'number', 348 | }, 349 | required: false, 350 | }, 351 | int: { 352 | node: { 353 | type: 'integer', 354 | }, 355 | required: false, 356 | }, 357 | }, 358 | additionalProperties: false, 359 | }, 360 | ], 361 | }; 362 | 363 | const writer = getWriter( 'yaml' ); 364 | 365 | const { 366 | data, 367 | convertedTypes, 368 | notConvertedTypes, 369 | } = await writer.write( doc, { warn, rawInput: '' } ); 370 | 371 | const out = readYaml( data ) as any; 372 | delete out.info['x-comment']; 373 | 374 | expect( out ).toStrictEqual( openApiSchemaFixture ); 375 | expect( convertedTypes ).toStrictEqual( [ 'Foo' ] ); 376 | expect( notConvertedTypes ).toStrictEqual( [ ] ); 377 | } ); 378 | } ); 379 | } ); 380 | -------------------------------------------------------------------------------- /lib/convert-json-schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertJsonSchemaToCoreTypes, 3 | convertCoreTypesToJsonSchema, 4 | convertOpenApiToCoreTypes, 5 | convertCoreTypesToOpenApi, 6 | jsonSchemaDocumentToOpenApi, 7 | openApiToJsonSchema, 8 | } from "core-types-json-schema" 9 | import { 10 | JsonSchemaDocumentToOpenApiOptions, 11 | PartialOpenApiSchema, 12 | } from "openapi-json-schema" 13 | import { load as readYaml, dump as writeYaml } from "js-yaml" 14 | import * as path from "path" 15 | 16 | import { stringify } from "./utils.js" 17 | import { Reader, ReaderOptions } from "./reader.js" 18 | import { Writer } from "./writer.js" 19 | import { userPackage, userPackageUrl } from "./package.js" 20 | import { registerReader, registerWriter } from "./format-graph.js" 21 | 22 | 23 | function maybeYamlReader( data: string, { warn, filename }: ReaderOptions ) 24 | : string | PartialOpenApiSchema 25 | { 26 | const file = filename?.toLowerCase( ) ?? ''; 27 | const isYaml = file.endsWith( 'yml' ) || file.endsWith( 'yaml' ); 28 | 29 | const input = isYaml 30 | ? readYaml( 31 | data, 32 | { 33 | filename, 34 | onWarning( { message, stack } ) 35 | { 36 | warn( message, { blob: { stack } } ); 37 | }, 38 | } 39 | ) as PartialOpenApiSchema 40 | // TODO: Maybe test if JSON parsing fails, and fallback to yaml, 41 | // despite no filename match 42 | : data; 43 | 44 | return input; 45 | } 46 | 47 | export function getJsonSchemaReader( ): Reader 48 | { 49 | return { 50 | kind: 'jsc', 51 | read( schema ) 52 | { 53 | return convertJsonSchemaToCoreTypes( schema ); 54 | }, 55 | }; 56 | } 57 | 58 | export function getJsonSchemaWriter( ): Writer 59 | { 60 | return { 61 | kind: 'jsc', 62 | write( doc, { filename, sourceFilename } ) 63 | { 64 | const { data: schemaObject, ...info } = 65 | convertCoreTypesToJsonSchema( 66 | doc, 67 | { filename, sourceFilename, userPackage, userPackageUrl } 68 | ); 69 | return { 70 | data: stringify( schemaObject ), 71 | ...info, 72 | }; 73 | }, 74 | shortcut: { 75 | oapi( schema, readerOptions ) 76 | { 77 | const openApiSchema = maybeYamlReader( schema, readerOptions ); 78 | const jsonSchema = openApiToJsonSchema( openApiSchema ); 79 | return { 80 | data: stringify( jsonSchema ), 81 | convertedTypes: 82 | Object.keys( jsonSchema.definitions ?? { } ), 83 | notConvertedTypes: [ ], 84 | }; 85 | } 86 | }, 87 | }; 88 | } 89 | 90 | export function getOpenApiReader( ): Reader 91 | { 92 | return { 93 | kind: 'oapi', 94 | read( schema, readerOptions ) 95 | { 96 | const openApiSchema = maybeYamlReader( schema, readerOptions ); 97 | return convertOpenApiToCoreTypes( openApiSchema ); 98 | }, 99 | }; 100 | } 101 | 102 | export interface OpenAPIWriterOptions 103 | extends JsonSchemaDocumentToOpenApiOptions 104 | { 105 | format: string; // 'yaml' | 'json' 106 | } 107 | 108 | export function getOpenApiWriter( 109 | { format, ...openApiOptions }: OpenAPIWriterOptions 110 | ) 111 | : Writer 112 | { 113 | const formatOutput = ( data: unknown ) => 114 | ( format === 'yaml' || format === 'yml' ) 115 | ? writeYaml( data ) 116 | : stringify( data ); 117 | 118 | const getOpenApiOptions = ( filename?: string ) => ( { 119 | ...openApiOptions, 120 | title: 121 | openApiOptions.title ?? 122 | ( 123 | filename 124 | ? `Converted from ${path.basename( filename )} with typeconv` 125 | : 'Converted with typeconv' 126 | ), 127 | version: openApiOptions.version ?? '1', 128 | } ); 129 | 130 | return { 131 | kind: 'oapi', 132 | write( doc, { filename, sourceFilename } ) 133 | { 134 | const { data: schemaObject, ...info } = 135 | convertCoreTypesToOpenApi( 136 | doc, 137 | { 138 | filename, 139 | sourceFilename, 140 | userPackage, 141 | userPackageUrl, 142 | ...getOpenApiOptions( filename ), 143 | } 144 | ); 145 | return { 146 | data: formatOutput( schemaObject ), 147 | ...info, 148 | }; 149 | }, 150 | shortcut: { 151 | jsc( data, { filename } ) 152 | { 153 | const jsonSchema = JSON.parse( data ); 154 | const openApiSchemaObject = 155 | jsonSchemaDocumentToOpenApi( 156 | jsonSchema, 157 | getOpenApiOptions( filename ) 158 | ); 159 | return { 160 | data: formatOutput( openApiSchemaObject ), 161 | convertedTypes: 162 | Object.keys( jsonSchema.definitions ?? { } ), 163 | notConvertedTypes: [ ], 164 | }; 165 | } 166 | }, 167 | }; 168 | } 169 | 170 | const defaultOpenApiOptions: OpenAPIWriterOptions = 171 | { format: 'json', title: '', version: '' }; 172 | 173 | registerReader( getJsonSchemaReader( ) ); 174 | registerWriter( getJsonSchemaWriter( ) ); 175 | registerReader( getOpenApiReader( ) ); 176 | registerWriter( getOpenApiWriter( defaultOpenApiOptions ) ); 177 | -------------------------------------------------------------------------------- /lib/convert-suretype.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertCoreTypesToSureType, 3 | convertJsonSchemaToSureType, 4 | convertSureTypeToCoreTypes, 5 | convertSuretypeToJsonSchema, 6 | JsonSchemaToSuretypeOptions, 7 | SuretypeToJsonSchemaOptions, 8 | } from "core-types-suretype" 9 | 10 | import { Reader } from "./reader.js" 11 | import { Writer } from "./writer.js" 12 | import { userPackage, userPackageUrl } from "./package.js" 13 | import { registerReader, registerWriter } from "./format-graph.js" 14 | import { stringify } from "./utils.js" 15 | 16 | 17 | export function getSureTypeReader( options?: SuretypeToJsonSchemaOptions ) 18 | : Reader 19 | { 20 | return { 21 | kind: 'st', 22 | managedRead: true, 23 | read( filename, { warn } ) 24 | { 25 | return convertSureTypeToCoreTypes( 26 | filename, 27 | { 28 | warn, 29 | userPackage, 30 | userPackageUrl, 31 | ...options, 32 | } 33 | ); 34 | }, 35 | shortcut: { 36 | async jsc( filename, { warn } ) 37 | { 38 | const result = await convertSuretypeToJsonSchema( 39 | filename, 40 | { 41 | warn, 42 | userPackage, 43 | userPackageUrl, 44 | ...options, 45 | } 46 | ); 47 | const { convertedTypes, notConvertedTypes, data } = result; 48 | return { 49 | convertedTypes, notConvertedTypes, data: stringify( data ), 50 | }; 51 | }, 52 | }, 53 | }; 54 | } 55 | 56 | export function getSureTypeWriter( options?: JsonSchemaToSuretypeOptions ) 57 | : Writer 58 | { 59 | return { 60 | kind: 'st', 61 | write( doc, { warn, filename, sourceFilename } ) 62 | { 63 | return convertCoreTypesToSureType( 64 | doc, 65 | { 66 | warn, 67 | filename, 68 | sourceFilename, 69 | userPackage, 70 | userPackageUrl, 71 | ...options, 72 | } 73 | ); 74 | }, 75 | shortcut: { 76 | jsc( dataString, { warn, filename, sourceFilename } ) 77 | { 78 | return convertJsonSchemaToSureType( 79 | dataString, 80 | { 81 | warn, 82 | filename, 83 | sourceFilename, 84 | userPackage, 85 | userPackageUrl, 86 | ...options 87 | } 88 | ); 89 | }, 90 | }, 91 | }; 92 | } 93 | 94 | registerReader( getSureTypeReader( ) ); 95 | registerWriter( getSureTypeWriter( ) ); 96 | -------------------------------------------------------------------------------- /lib/convert-typescript.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convertCoreTypesToTypeScript, 3 | convertTypeScriptToCoreTypes, 4 | FromTsOptions, 5 | ToTsOptions, 6 | } from "core-types-ts" 7 | 8 | import { Reader } from "./reader.js" 9 | import { Writer } from "./writer.js" 10 | import { userPackage, userPackageUrl } from "./package.js" 11 | import { registerReader, registerWriter } from "./format-graph.js" 12 | 13 | 14 | export function getTypeScriptReader( tsOptions?: FromTsOptions ): Reader 15 | { 16 | return { 17 | kind: 'ts', 18 | read( source, { warn, filename } ) 19 | { 20 | return convertTypeScriptToCoreTypes( 21 | source, 22 | { 23 | warn, 24 | ...tsOptions, 25 | } 26 | ); 27 | } 28 | }; 29 | } 30 | 31 | export function getTypeScriptWriter( tsOptions?: ToTsOptions ): Writer 32 | { 33 | return { 34 | kind: 'ts', 35 | write( doc, { warn, filename, sourceFilename } ) 36 | { 37 | return convertCoreTypesToTypeScript( 38 | doc, 39 | { 40 | warn, 41 | filename, 42 | sourceFilename, 43 | userPackage, 44 | userPackageUrl, 45 | ...tsOptions, 46 | } 47 | ); 48 | } 49 | }; 50 | } 51 | 52 | registerReader( getTypeScriptReader( ) ); 53 | registerWriter( getTypeScriptWriter( ) ); 54 | -------------------------------------------------------------------------------- /lib/converter.test.ts: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { fileURLToPath } from "url" 3 | 4 | import { jest } from "@jest/globals" 5 | 6 | import { makeConverter } from "./converter.js" 7 | import { 8 | getJsonSchemaReader, 9 | getJsonSchemaWriter, 10 | getOpenApiWriter, 11 | } from "./convert-json-schema.js" 12 | import { getTypeScriptWriter } from "./convert-typescript.js" 13 | import { getGraphQLReader, getGraphQLWriter } from "./convert-graphql.js" 14 | import { getSureTypeReader, getSureTypeWriter } from "./convert-suretype.js" 15 | import { withConsoleWarn } from "../test/utils.js" 16 | 17 | 18 | const __filename = fileURLToPath( import.meta.url ); 19 | const __dirname = path.dirname( __filename ); 20 | 21 | const fixturesDir = path.resolve( __dirname, 'fixtures' ); 22 | 23 | describe( "converter", ( ) => 24 | { 25 | it( "self-reading and writing JSON Schema", async ( ) => 26 | { 27 | const { convert } = makeConverter( 28 | getJsonSchemaReader( ), 29 | getJsonSchemaWriter( ), 30 | ); 31 | const example = { 32 | definitions: { 33 | foo: { type: 'string' } 34 | } 35 | }; 36 | const { data: out } = 37 | await convert( { data: JSON.stringify( example, null, 4 ) } ); 38 | 39 | expect( JSON.parse( out ) ).toMatchObject( example ); 40 | } ); 41 | 42 | describe( "json-schema-to-ts", ( ) => 43 | { 44 | it( "self-reading and writing JSON Schema", async ( ) => 45 | { 46 | const { convert } = makeConverter( 47 | getJsonSchemaReader( ), 48 | getTypeScriptWriter( { 49 | noDescriptiveHeader: true, 50 | noDisableLintHeader: true, 51 | } ), 52 | ); 53 | const example = { 54 | definitions: { 55 | foo: { type: 'string' } 56 | } 57 | }; 58 | const { data: out } = 59 | await convert( { data: JSON.stringify( example, null, 4 ) } ); 60 | 61 | expect( out ).toMatchSnapshot( ); 62 | } ); 63 | } ); 64 | 65 | describe( "warn", ( ) => 66 | { 67 | it( "should invoke warn function when present", withConsoleWarn( 68 | async ( { warn } ) => 69 | { 70 | const warnFn = jest.fn( ); 71 | const { convert } = makeConverter( 72 | getJsonSchemaReader( ), 73 | getGraphQLWriter( { warn: warnFn } ), 74 | ); 75 | const example = { 76 | definitions: { 77 | foo: { type: 'null' } 78 | } 79 | }; 80 | await convert( { data: JSON.stringify( example, null, 4 ) } ); 81 | 82 | expect( warnFn ).toHaveBeenCalledWith( 83 | "Type 'null' not supported", 84 | expect.any( Error ) 85 | ); 86 | expect( warn ).toHaveBeenCalledTimes( 0 ); 87 | } ) ); 88 | 89 | it( "should invoke console.warn by default", withConsoleWarn( 90 | async ( { warn } ) => 91 | { 92 | const { convert } = makeConverter( 93 | getJsonSchemaReader( ), 94 | getGraphQLWriter( ), 95 | ); 96 | const example = { 97 | definitions: { 98 | foo: { type: 'null' } 99 | } 100 | }; 101 | await convert( { data: JSON.stringify( example, null, 4 ) } ); 102 | 103 | expect( warn ).toHaveBeenCalledWith( 104 | "Type 'null' not supported" 105 | ); 106 | } ) ); 107 | } ); 108 | 109 | describe( "shortcut", ( ) => 110 | { 111 | it( "should run shortcut if possible", async ( ) => 112 | { 113 | const { convert } = makeConverter( 114 | getJsonSchemaReader( ), 115 | getOpenApiWriter( { 116 | format: 'yaml', 117 | title: 'Title', 118 | version: '1', 119 | schemaVersion: '3.0.0', 120 | } ), 121 | { shortcut: true } 122 | ); 123 | const example = { 124 | definitions: { 125 | foo: { type: 'string' } 126 | } 127 | }; 128 | const { data: out } = 129 | await convert( { data: JSON.stringify( example, null, 4 ) } ); 130 | 131 | expect( out ).toMatchSnapshot( ); 132 | } ); 133 | 134 | it( "should run graph if necessary (st->oapi)", async ( ) => 135 | { 136 | const { convert } = makeConverter( 137 | getSureTypeReader( ), 138 | getOpenApiWriter( { 139 | format: 'yaml', 140 | title: 'Title', 141 | version: '1', 142 | schemaVersion: '3.0.0', 143 | } ), 144 | { shortcut: true } 145 | ); 146 | const stFile = path.resolve( fixturesDir, 'validator.st.js' ); 147 | 148 | const { data, in: _in, out } = 149 | await convert( { filename: stFile, cwd: '/' } ); 150 | 151 | const expectedConversionResult = { 152 | convertedTypes: [ 'Foo' ], 153 | notConvertedTypes: [ ], 154 | }; 155 | expect( _in ).toStrictEqual( expectedConversionResult ); 156 | expect( out ).toStrictEqual( expectedConversionResult ); 157 | expect( data ).toMatchSnapshot( ); 158 | } ); 159 | 160 | 161 | it( "should run graph if necessary (gql->st)", async ( ) => 162 | { 163 | const { convert } = makeConverter( 164 | getGraphQLReader( ), 165 | getSureTypeWriter( { } ), 166 | { shortcut: true } 167 | ); 168 | 169 | const { data, in: _in, out } = 170 | await convert( { 171 | data: ` 172 | """ 173 | The Foo type 174 | """ 175 | type Foo { 176 | """ 177 | This is the integer thing 178 | 179 | @default 55 180 | """ 181 | int: Int 182 | str: String! 183 | "Excellent array of strings" 184 | stra: [String!]! 185 | } 186 | `, 187 | } ); 188 | 189 | const expectedConversionResult = { 190 | convertedTypes: [ 'Foo' ], 191 | notConvertedTypes: [ ], 192 | }; 193 | expect( _in ).toStrictEqual( expectedConversionResult ); 194 | expect( out ).toStrictEqual( expectedConversionResult ); 195 | expect( data ).toMatchSnapshot( ); 196 | } ); 197 | } ); 198 | } ); 199 | -------------------------------------------------------------------------------- /lib/converter.ts: -------------------------------------------------------------------------------- 1 | import { 2 | decorateError, 3 | decorateErrorMeta, 4 | isCoreTypesError, 5 | NamedType, 6 | NodeDocument, 7 | simplify, 8 | WarnFunction, 9 | ConversionResult as CoreTypesConversionResult, 10 | } from "core-types" 11 | 12 | import type { Reader, ReaderOptions } from "./reader.js" 13 | import type { Writer, WriterOptions } from "./writer.js" 14 | import type { Source, SourceFile } from "./file.js" 15 | import { getSource, writeFile, relFile } from "./file.js" 16 | import { formatError } from "./error.js" 17 | import { ConversionContext } from "./format-graph.js" 18 | import { TypeImplementation } from "./types.js" 19 | 20 | 21 | export interface Target 22 | { 23 | filename: string; 24 | relFilename?: string; 25 | } 26 | 27 | export interface ConversionResultInformation 28 | { 29 | in: Omit< CoreTypesConversionResult, 'data' >; 30 | out: Omit< CoreTypesConversionResult, 'data' >; 31 | } 32 | 33 | export type ConversionResult< T > = ConversionResultInformation & { data: T }; 34 | 35 | export interface Converter 36 | { 37 | /** 38 | * Convert and save to file 39 | */ 40 | convert( from: Source, to: Target ): Promise< ConversionResult< void > >; 41 | 42 | /** 43 | * Convert and return result as string 44 | */ 45 | convert( from: Source ): Promise< ConversionResult< string > >; 46 | 47 | /** 48 | * The current working directory for this converter 49 | */ 50 | cwd: string; 51 | 52 | /** 53 | * From format 54 | */ 55 | fromFormat: TypeImplementation; 56 | } 57 | 58 | export type ConvertMapFunction = 59 | ( node: NamedType, index: number, array: ReadonlyArray< NamedType > ) => 60 | NamedType; 61 | export type ConvertFilterFunction = 62 | ( node: NamedType, index: number, array: ReadonlyArray< NamedType > ) => 63 | boolean; 64 | export type ConvertTransformFunction = ( doc: NodeDocument ) => NodeDocument; 65 | 66 | export interface ConvertOptions 67 | { 68 | /** 69 | * The current working directory to use when converting to or from files. 70 | * Is not necessary for string-to-string conversion. 71 | */ 72 | cwd?: string; 73 | 74 | /** 75 | * When simplify is true, the converter will let core-types _compress_ the 76 | * types after having converted from {reader} format to core-types. 77 | * This is usually recommended, but may cause some annotations (comments) 78 | * to be dropped. 79 | * 80 | * @default 81 | * true 82 | */ 83 | simplify?: boolean; 84 | 85 | /** 86 | * When simplifying (which is implied by this option), and-types of objects 87 | * will be merged into one self-contained object. 88 | * This is useful for type systems that don't treat object intersections the 89 | * same way e.g. TypeScript does. 90 | */ 91 | mergeObjects?: boolean; 92 | 93 | /** 94 | * Custom map function for transforming each type after it has been 95 | * converted *from* the source type (and after it has been simplified), 96 | * but before it's written to the target type system. 97 | * 98 | * If `filter` is used as well, this runs before `filter`. 99 | * If `transform` is used as well, this runs before `transform`. 100 | */ 101 | map?: ConvertMapFunction; 102 | 103 | /** 104 | * Custom filter function for filtering types after they have been 105 | * converted *from* the source type. 106 | * 107 | * If `map` is used as well, this runs after `map`. 108 | * If `transform` is used as well, this runs before `transform`. 109 | */ 110 | filter?: ConvertFilterFunction; 111 | 112 | /** 113 | * Custom filter function for filtering types after they have been 114 | * converted *from* the source type. 115 | * 116 | * If `map` is used as well, this runs after `map`. 117 | * If `filter` is used as well, this runs after `filter`. 118 | */ 119 | transform?: ConvertTransformFunction; 120 | 121 | /** 122 | * Shortcut reader and writer if possible (bypassing core-types). 123 | * 124 | * @default 125 | * true 126 | */ 127 | shortcut?: boolean; 128 | } 129 | 130 | interface ReadSourceManaged 131 | { 132 | data?: undefined; 133 | filename: string; 134 | } 135 | 136 | interface ReadSourceNormal 137 | { 138 | data: string; 139 | filename?: string; 140 | } 141 | 142 | type ReadSource = ReadSourceManaged | ReadSourceNormal; 143 | 144 | async function readSource( from: Source, reader: Reader ) 145 | : Promise< ReadSource > 146 | { 147 | if ( reader.managedRead ) 148 | { 149 | const { filename } = from as SourceFile; 150 | if ( !filename ) 151 | throw new Error( "Internal error, expected filename not data" ); 152 | return { filename }; 153 | } 154 | 155 | return await getSource( from ); 156 | } 157 | 158 | interface SingleConversionResult 159 | { 160 | output: string; 161 | convertedTypes: Array< string >; 162 | notConvertedTypes: Array< string >; 163 | outConvertedTypes: Array< string >; 164 | outNotConvertedTypes: Array< string >; 165 | } 166 | 167 | async function convertAny( 168 | data: string, 169 | reader: Reader, 170 | writer: Writer, 171 | format: TypeImplementation, 172 | readOpts: ReaderOptions, 173 | writeOpts: WriterOptions 174 | ) 175 | : Promise< SingleConversionResult > 176 | { 177 | if ( format === 'ct' ) 178 | { 179 | const read = await reader.read( data, readOpts ); 180 | 181 | const written = await writer.write( read.data, writeOpts ); 182 | 183 | return { 184 | output: written.data, 185 | convertedTypes: read.convertedTypes, 186 | notConvertedTypes: read.notConvertedTypes, 187 | outConvertedTypes: written.convertedTypes, 188 | outNotConvertedTypes: written.notConvertedTypes, 189 | }; 190 | } 191 | else 192 | { 193 | const read = await reader.shortcut![format]!( data, readOpts ); 194 | 195 | const written = 196 | await writer.shortcut![format]!( read.data, writeOpts, reader ); 197 | 198 | return { 199 | output: written.data, 200 | convertedTypes: read.convertedTypes, 201 | notConvertedTypes: read.notConvertedTypes, 202 | outConvertedTypes: written.convertedTypes, 203 | outNotConvertedTypes: written.notConvertedTypes, 204 | }; 205 | } 206 | } 207 | 208 | export function makeConverter( 209 | reader: Reader, 210 | writer: Writer, 211 | options?: ConvertOptions 212 | ) 213 | : Converter 214 | { 215 | const { shortcut = true, cwd = process.cwd( ) } = options ?? { }; 216 | 217 | const relFilename = ( filename: string ) => 218 | relFile( options?.cwd, filename ); 219 | 220 | const context = new ConversionContext( reader, writer, { shortcut } ); 221 | const conversionPath = context.getPath( ); 222 | const simpleSingleConversion = 223 | conversionPath.length === 1 && conversionPath[ 0 ].format === 'ct'; 224 | 225 | async function convert( from: Source, to?: Target ) 226 | : Promise< ConversionResult< void | string > > 227 | { 228 | const { data, filename } = await readSource( from, reader ); 229 | const dataOrFilename = ( data ?? filename )!; 230 | 231 | const warn: WarnFunction = ( msg, meta ) => 232 | { 233 | const fullMeta = decorateErrorMeta( 234 | { ...meta }, 235 | { filename, source: data } 236 | ); 237 | console.warn( formatError( msg, fullMeta ) ); 238 | } 239 | 240 | const toFilename = 241 | to?.relFilename ?? relFilename( to?.filename ?? '' ); 242 | 243 | const readOpts: ReaderOptions = { 244 | warn, 245 | filename: filename ? relFilename( filename ) : undefined, 246 | }; 247 | 248 | const writeOpts: WriterOptions = { 249 | warn, 250 | ...( 251 | to 252 | ? { filename: toFilename } 253 | : { } 254 | ), 255 | ...( 256 | filename 257 | ? { sourceFilename: relFilename( filename ) } 258 | : { } 259 | ), 260 | rawInput: data, 261 | }; 262 | 263 | const convertByGraphPath = async ( data: string, pathIndex: number ) 264 | : Promise< SingleConversionResult > => 265 | { 266 | const { reader, writer, format } = conversionPath[ pathIndex ]; 267 | 268 | const result = await convertAny( 269 | data, 270 | reader, 271 | writer, 272 | format, 273 | readOpts, 274 | writeOpts 275 | ); 276 | 277 | const uniqAppend = ( a: Array< string >, b: Array< string > ) => 278 | [ ...new Set( a ), ...new Set( b ) ]; 279 | 280 | if ( conversionPath.length > pathIndex + 1 ) 281 | { 282 | // Recurse - follow path and convert again 283 | const recursionResult = 284 | await convertByGraphPath( result.output, pathIndex + 1 ); 285 | 286 | return { 287 | output: recursionResult.output, 288 | convertedTypes: recursionResult.convertedTypes, 289 | notConvertedTypes: uniqAppend( 290 | result.notConvertedTypes, 291 | recursionResult.notConvertedTypes 292 | ), 293 | outConvertedTypes: recursionResult.convertedTypes, 294 | outNotConvertedTypes: uniqAppend( 295 | result.outNotConvertedTypes, 296 | recursionResult.outNotConvertedTypes 297 | ), 298 | }; 299 | } 300 | else 301 | { 302 | return result; 303 | } 304 | }; 305 | 306 | const convertDefault = async ( ) => 307 | { 308 | const { data: doc, convertedTypes, notConvertedTypes } = 309 | await reader.read( dataOrFilename, readOpts ); 310 | 311 | const simplifiedDoc = 312 | options?.simplify === false 313 | ? doc 314 | : simplify( doc, { mergeObjects: options?.mergeObjects } ); 315 | 316 | const { map, filter, transform } = options ?? { }; 317 | 318 | const mappedDoc = 319 | typeof map === 'function' 320 | ? { 321 | ...simplifiedDoc, 322 | types: doc.types.map( ( type, index ) => 323 | map( type, index, doc.types ) 324 | ), 325 | } 326 | : simplifiedDoc; 327 | 328 | const filteredDoc = 329 | typeof filter === 'function' 330 | ? { 331 | ...mappedDoc, 332 | types: doc.types.filter( ( type, index ) => 333 | filter( type, index, doc.types ) 334 | ), 335 | } 336 | : mappedDoc; 337 | 338 | const finalDoc = 339 | typeof transform === 'function' 340 | ? transform( filteredDoc ) 341 | : filteredDoc; 342 | 343 | const { 344 | data: output, 345 | convertedTypes: outConvertedTypes, 346 | notConvertedTypes: outNotConvertedTypes 347 | } = await writer.write( finalDoc, writeOpts ); 348 | 349 | return { 350 | output, 351 | convertedTypes, 352 | notConvertedTypes, 353 | outConvertedTypes, 354 | outNotConvertedTypes, 355 | }; 356 | }; 357 | 358 | try 359 | { 360 | const { 361 | output, 362 | convertedTypes, 363 | notConvertedTypes, 364 | outConvertedTypes, 365 | outNotConvertedTypes, 366 | } = 367 | simpleSingleConversion 368 | ? await convertDefault( ) 369 | : await convertByGraphPath( dataOrFilename, 0 ); 370 | 371 | const info: ConversionResultInformation = { 372 | in: { convertedTypes, notConvertedTypes }, 373 | out: { 374 | convertedTypes: outConvertedTypes, 375 | notConvertedTypes: outNotConvertedTypes, 376 | }, 377 | } 378 | 379 | if ( typeof to?.filename === 'undefined' ) 380 | return { data: output, ...info } as ConversionResult< string >; 381 | 382 | // Only write non-empty files 383 | if ( outConvertedTypes.length > 0 ) 384 | await writeFile( to?.filename, output ); 385 | 386 | return info as ConversionResult< void >; 387 | } 388 | catch ( err ) 389 | { 390 | if ( isCoreTypesError( err ) ) 391 | decorateError( err, { 392 | source: data, 393 | ...( filename ? { filename } : { } ), 394 | } ); 395 | throw err; 396 | } 397 | } 398 | 399 | const fromFormat = reader.kind; 400 | return { convert: convert as Converter[ 'convert' ], cwd, fromFormat }; 401 | } 402 | -------------------------------------------------------------------------------- /lib/error.test.ts: -------------------------------------------------------------------------------- 1 | import { UnsupportedError } from "core-types" 2 | 3 | import { formatError, formatCoreTypesError } from "./error.js" 4 | 5 | describe( "error", ( ) => 6 | { 7 | it( "formatError should fallback to error message", ( ) => 8 | { 9 | const output = formatError( "foo message" ); 10 | expect( output ).toEqual( "foo message" ); 11 | } ); 12 | 13 | it( "formatError should properly display errors", ( ) => 14 | { 15 | const output = formatError( 16 | "foo message", 17 | { 18 | source: "type X = bad type;", 19 | loc: { start: 13, end: 17 }, 20 | } 21 | ); 22 | expect( output ).toMatchSnapshot( ); 23 | } ); 24 | 25 | it( "formatCoreTypesError should fallback to error message", ( ) => 26 | { 27 | const output = formatCoreTypesError( new Error( "foo error" ) ); 28 | expect( output ).toEqual( "foo error" ); 29 | } ); 30 | 31 | it( "formatCoreTypesError should display nice error message", ( ) => 32 | { 33 | const err = new UnsupportedError( 34 | "foo error", 35 | { 36 | source: "type X = bad type;", 37 | loc: { start: 13, end: 17 }, 38 | } 39 | ); 40 | const output = formatCoreTypesError( err ); 41 | expect( output ).toMatchSnapshot( ); 42 | } ); 43 | } ); 44 | -------------------------------------------------------------------------------- /lib/error.ts: -------------------------------------------------------------------------------- 1 | import { codeFrameColumns } from "awesome-code-frame" 2 | import { 3 | CoreTypesErrorMeta, 4 | locationToLineColumn, 5 | isCoreTypesError, 6 | } from "core-types" 7 | 8 | 9 | export function formatCoreTypesError( err: Error ): string 10 | { 11 | if ( isCoreTypesError( err ) ) 12 | { 13 | const message = `[${err.name}] ${err.message}`; 14 | return formatError( message, err ); 15 | } 16 | 17 | return err.message; 18 | } 19 | 20 | export function formatError( 21 | message: string, 22 | meta?: CoreTypesErrorMeta 23 | ) 24 | : string 25 | { 26 | if ( meta?.loc?.start && meta?.source ) 27 | { 28 | const loc = locationToLineColumn( meta.source, meta.loc ); 29 | const { start, end } = loc; 30 | 31 | if ( start ) 32 | return codeFrameColumns( 33 | meta.source, 34 | { start, end }, 35 | { message } 36 | ); 37 | } 38 | 39 | return message; 40 | } 41 | -------------------------------------------------------------------------------- /lib/file.test.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals" 2 | 3 | process.env.FORCE_HYPERLINK = "1"; 4 | 5 | jest.unstable_mockModule( "fs", ( ) => ( { 6 | promises: { 7 | readFile: ( filename: string, encoding: string ): Promise< string > => 8 | Promise.resolve( `Data in ${filename}` ), 9 | writeFile: 10 | jest.fn( 11 | ( filename: string, data: string ) => Promise.resolve( ) 12 | ), 13 | }, 14 | } ) ); 15 | 16 | jest.unstable_mockModule( "globby", ( ) => ( { 17 | globby: jest.fn( ( ...args ) => args ), 18 | } ) ); 19 | 20 | import type { promises as FsPromisesType } from "fs" 21 | import type GlobbyType from "globby" 22 | 23 | const getFsPromises = async ( ): Promise< typeof FsPromisesType > => 24 | import( "fs" ).then( mod => mod.promises ); 25 | 26 | const getGlobby = async ( ): Promise< typeof GlobbyType > => 27 | import( "globby" ); 28 | 29 | const getFileApi = async ( ) => import( "./file.js" ); 30 | 31 | describe( "file", ( ) => 32 | { 33 | it( "getSource should handle string data", async ( ) => 34 | { 35 | const { getSource } = await getFileApi( ); 36 | 37 | const data = "foo"; 38 | expect( await getSource( { data } ) ).toStrictEqual( { data } ); 39 | } ); 40 | 41 | it( "getSource should handle file reading", async ( ) => 42 | { 43 | const { getSource } = await getFileApi( ); 44 | 45 | expect( await getSource( { cwd: "", filename: "/a/b" } ) ) 46 | .toStrictEqual( { 47 | data: "Data in /a/b", 48 | filename: "/a/b", 49 | } ); 50 | } ); 51 | 52 | it( "getSource should fail on invalid input", async ( ) => 53 | { 54 | const { getSource } = await getFileApi( ); 55 | 56 | expect( getSource( { } as any ) ).rejects 57 | .toThrowError( /Invalid source/ ); 58 | } ); 59 | 60 | it( "relFile should handle non-from", async ( ) => 61 | { 62 | const { relFile } = await getFileApi( ); 63 | 64 | expect( relFile( undefined, "/a/b" ) ).toBe( "/a/b" ); 65 | } ); 66 | 67 | it( "relFile should handle from with absolute to", async ( ) => 68 | { 69 | const { relFile } = await getFileApi( ); 70 | 71 | expect( relFile( "/a/b/c", "/a/b/d" ) ).toBe( "../d" ); 72 | } ); 73 | 74 | it( "writeFile should handle from", async ( ) => 75 | { 76 | const { writeFile } = await getFileApi( ); 77 | const fsPromises = await getFsPromises( ); 78 | 79 | await writeFile( "/a/c", "the data" ); 80 | expect( fsPromises.writeFile ).toHaveBeenCalledTimes( 1 ); 81 | expect( fsPromises.writeFile ) 82 | .toHaveBeenCalledWith( "/a/c", "the data" ); 83 | } ); 84 | 85 | it( "globby mock", async ( ) => 86 | { 87 | const { glob } = await getFileApi( ); 88 | 89 | const { globby } = await getGlobby( ); 90 | 91 | const globs = [ "a/b", "a/c" ]; 92 | const cwd = "/x"; 93 | 94 | glob( globs, cwd, false ); 95 | expect( ( globby as any as jest.Mock ).mock.calls[ 0 ] ) 96 | .toEqual( [ globs, { cwd, dot: true, gitignore: false } ] ); 97 | 98 | glob( globs, cwd, true ); 99 | expect( ( globby as any as jest.Mock ).mock.calls[ 1 ] ) 100 | .toEqual( [ 101 | [ ...globs, '!.git' ], 102 | { cwd, dot: false, gitignore: true } 103 | ] ); 104 | } ); 105 | 106 | it( "ensureAbsolute should handle relative", async ( ) => 107 | { 108 | const { ensureAbsolute } = await getFileApi( ); 109 | 110 | expect( ensureAbsolute( "a/b", "/x/y" ) ).toEqual( "/x/y/a/b" ); 111 | } ); 112 | 113 | it( "ensureAbsolute should handle absolute", async ( ) => 114 | { 115 | const { ensureAbsolute } = await getFileApi( ); 116 | 117 | expect( ensureAbsolute( "/a/b", "/x/y" ) ).toEqual( "/a/b" ); 118 | } ); 119 | 120 | it( "getRootFolderOfFiles should handle different depth", async ( ) => 121 | { 122 | const { getRootFolderOfFiles } = await getFileApi( ); 123 | 124 | const root = getRootFolderOfFiles( 125 | [ "/a/b/x", "/a/b/d/e" ], 126 | "/a/b" 127 | ); 128 | 129 | expect( root ).toEqual( "/a/b" ); 130 | } ); 131 | 132 | it( "getRootFolderOfFiles should handle absolute & relative paths", 133 | async ( ) => 134 | { 135 | const { getRootFolderOfFiles } = await getFileApi( ); 136 | 137 | const root = getRootFolderOfFiles( 138 | [ "/a/b/c/e", "d/e" ], 139 | "/a/b" 140 | ); 141 | 142 | expect( root ).toEqual( "/a/b" ); 143 | } ); 144 | 145 | it( "getRootFolderOfFiles empty files should return empty string", 146 | async ( ) => 147 | { 148 | const { getRootFolderOfFiles } = await getFileApi( ); 149 | 150 | const root = getRootFolderOfFiles( [ ], "/a/b" ); 151 | 152 | expect( root ).toEqual( "" ); 153 | } ); 154 | 155 | it( "reRootFiles", async ( ) => 156 | { 157 | const { reRootFiles } = await getFileApi( ); 158 | 159 | const root = reRootFiles( 160 | [ "a/b", "a/c", "a/d/e" ], 161 | "/x1/y1", 162 | "/x2/y2/new" 163 | ); 164 | 165 | expect( root ).toEqual( { 166 | files: [ 167 | { 168 | "in": "/x1/y1/a/b", 169 | "out": "/x2/y2/new/b", 170 | "rel": "b", 171 | }, 172 | { 173 | "in": "/x1/y1/a/c", 174 | "out": "/x2/y2/new/c", 175 | "rel": "c", 176 | }, 177 | { 178 | "in": "/x1/y1/a/d/e", 179 | "out": "/x2/y2/new/d/e", 180 | "rel": "d/e", 181 | }, 182 | ], 183 | root: "/x1/y1/a", 184 | newRoot: "/x2/y2/new", 185 | } ); 186 | } ); 187 | 188 | it( "", async ( ) => 189 | { 190 | const { prettyFile } = await getFileApi( ); 191 | 192 | const text = prettyFile( "foo/file.name", "/the/root" ); 193 | expect( text ).toMatchSnapshot( ); 194 | } ); 195 | } ); 196 | -------------------------------------------------------------------------------- /lib/file.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from "fs" 2 | import path from "path" 3 | 4 | import { globby } from "globby" 5 | import chalk from "chalk" 6 | import terminalLink from "terminal-link" 7 | 8 | 9 | export interface SourceFile 10 | { 11 | cwd: string; 12 | filename: string; 13 | } 14 | 15 | export interface SourceString 16 | { 17 | data: string; 18 | } 19 | 20 | export type Source = SourceFile | SourceString; 21 | 22 | export interface SourceData 23 | { 24 | data: string; 25 | filename?: string; 26 | } 27 | 28 | export async function getSource( source: Source ): Promise< SourceData > 29 | { 30 | if ( ( source as SourceString ).data ) 31 | return { 32 | data: ( source as SourceString ).data, 33 | }; 34 | 35 | else if ( ( source as SourceFile ).filename && fsPromises.readFile ) 36 | return { 37 | data: await fsPromises.readFile( 38 | ( source as SourceFile ).filename, 39 | 'utf-8' 40 | ), 41 | filename: ( source as SourceFile ).filename, 42 | }; 43 | 44 | throw new Error( "Invalid source: " + JSON.stringify( source ) ); 45 | } 46 | 47 | export async function writeFile( filename: string, data: string ) 48 | : Promise< void > 49 | { 50 | const tryWrite = ( ) => fsPromises.writeFile( filename, data ); 51 | try 52 | { 53 | await tryWrite( ); 54 | } 55 | catch ( err: any ) 56 | { 57 | if ( err?.code === 'ENOENT' ) 58 | { 59 | await fsPromises.mkdir( 60 | path.dirname( filename ), 61 | { recursive: true } 62 | ); 63 | await tryWrite( ); 64 | } 65 | } 66 | } 67 | 68 | export function relFile( from: string | undefined, to: string ): string 69 | { 70 | if ( typeof from === 'undefined' ) 71 | return to; 72 | return path.relative( from, to ); 73 | } 74 | 75 | export async function glob( 76 | globs: Array< string >, 77 | cwd: string, 78 | hidden = true 79 | ) 80 | { 81 | const patterns = hidden ? [ ...globs, '!.git' ] : globs; 82 | const dot = !hidden; 83 | const gitignore = hidden; 84 | return globby( patterns, { cwd, dot, gitignore } ); 85 | } 86 | 87 | export function ensureAbsolute( filename: string, cwd: string ) 88 | { 89 | return path.isAbsolute( filename ) 90 | ? filename 91 | : path.normalize( path.join( cwd, filename ) ); 92 | } 93 | 94 | /** 95 | * Find the deepest common directory of a set of files. 96 | */ 97 | export function getRootFolderOfFiles( files: Array< string >, cwd: string ) 98 | : string 99 | { 100 | const map: any = { }; 101 | const last = new WeakMap< object, string >( ); 102 | 103 | files 104 | .map( filename => ensureAbsolute( filename, cwd ) ) 105 | .map( filename => path.dirname( filename ) ) 106 | .forEach( dirname => 107 | { 108 | const dirSegments = dirname.split( path.sep ); 109 | let cur = map; 110 | dirSegments.forEach( segment => 111 | { 112 | cur[ segment ] ??= { }; 113 | cur = cur[ segment ]; 114 | } ); 115 | last.set( cur, dirname ); 116 | } ); 117 | 118 | let curPath = [ ]; 119 | let cur = map; 120 | while ( true ) 121 | { 122 | const keys = Object.getOwnPropertyNames( cur ); 123 | if ( keys.length !== 1 ) 124 | return curPath.join( path.sep ); 125 | else if ( last.has( cur ) ) 126 | return last.get( cur ) as string; 127 | cur = cur[ keys[ 0 ] ]; 128 | curPath.push( keys[ 0 ] ); 129 | } 130 | } 131 | 132 | /** 133 | * Get the common "root" directory of files, their relative path to this 134 | * directory, and their relative path to new root directory. 135 | */ 136 | export function reRootFiles( 137 | files: Array< string >, 138 | cwd: string, 139 | newRoot?: string 140 | ) 141 | : { 142 | root: string; 143 | newRoot: string; 144 | files: Array< { in: string; out: string; rel: string; } >; 145 | } 146 | { 147 | const root = getRootFolderOfFiles( files, cwd ); 148 | 149 | const newAbsRoot = typeof newRoot === 'undefined' 150 | ? root 151 | : ensureAbsolute( newRoot, cwd ); 152 | 153 | return { 154 | root, 155 | newRoot: newAbsRoot, 156 | files: files 157 | .map( filename => ensureAbsolute( filename, cwd ) ) 158 | .map( filename => 159 | { 160 | const rel = path.relative( root, filename ); 161 | const out = ensureAbsolute( rel, newAbsRoot ); 162 | return { in: filename, out, rel }; 163 | } ), 164 | }; 165 | } 166 | 167 | export function prettyFile( filename: string, cwd: string ) 168 | { 169 | const absFile = 'file://' + ensureAbsolute( filename, cwd ); 170 | const baseName = path.basename( filename ); 171 | const dirName = path.dirname( filename ); 172 | 173 | const name = 174 | ( ( dirName && dirName !== '.' ) ? ( dirName + path.sep ) : '' ) + 175 | chalk.bold( baseName ); 176 | 177 | return terminalLink( name, absFile, { fallback: false } ); 178 | } 179 | -------------------------------------------------------------------------------- /lib/fixtures/validator.st.js: -------------------------------------------------------------------------------- 1 | import { suretype, v } from 'suretype' 2 | 3 | export const myval = suretype( 4 | { name: 'Foo', description: 'This is Foo' }, 5 | v.object( { 6 | gt5: v.number( ).gt( 5 ), 7 | gte5: v.number( ).gte( 5 ), 8 | } ), 9 | ); 10 | -------------------------------------------------------------------------------- /lib/format-graph.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from "@jest/globals" 2 | 3 | import { FormatGraph, makePathKey } from "./format-graph.js" 4 | import { Reader } from "./reader.js" 5 | import { Writer } from "./writer.js" 6 | import { TypeImplementation } from "./types.js" 7 | 8 | 9 | function fakeReader( 10 | kind: TypeImplementation, 11 | formats: Array< TypeImplementation > = [ ], 12 | managedRead = false 13 | ) 14 | : Reader 15 | { 16 | return { 17 | kind, 18 | read: null as any, 19 | managedRead, 20 | shortcut: Object.fromEntries( 21 | formats.map( format => [ format, null ] ) 22 | ), 23 | }; 24 | } 25 | 26 | function fakeWriter( 27 | kind: TypeImplementation, 28 | formats: Array< TypeImplementation > = [ ] 29 | ) 30 | : Writer 31 | { 32 | return { 33 | kind, 34 | write: null as any, 35 | shortcut: Object.fromEntries( 36 | formats.map( format => [ format, null ] ) 37 | ), 38 | }; 39 | } 40 | 41 | describe( "format-graph", ( ) => 42 | { 43 | const gqlRead = fakeReader( 'gql', [ 'jsc' ] ); 44 | const jscRead = fakeReader( 'jsc', [ 'jsc', 'oapi' ] ); 45 | const oapiRead = fakeReader( 'oapi', [ 'jsc', 'oapi' ] ); 46 | const tsRead = fakeReader( 'ts' ); 47 | const stRead = fakeReader( 'st', [ 'jsc' ], true ); 48 | 49 | const gqlWrite = fakeWriter( 'gql' ); 50 | const tsWrite = fakeWriter( 'ts' ); 51 | const jscWrite = fakeWriter( 'jsc', [ 'jsc' ] ); 52 | const oapiWrite = fakeWriter( 'oapi', [ 'jsc' ] ); 53 | const stWrite = fakeWriter( 'st', [ 'jsc' ] ); 54 | 55 | const makeGraph = ( ) => 56 | { 57 | const graph = new FormatGraph( ); 58 | 59 | graph.registerReader( gqlRead ); 60 | graph.registerReader( jscRead ); 61 | graph.registerReader( oapiRead ); 62 | graph.registerReader( tsRead ); 63 | graph.registerReader( stRead ); 64 | 65 | graph.registerWriter( gqlWrite ); 66 | graph.registerWriter( tsWrite ); 67 | graph.registerWriter( jscWrite ); 68 | graph.registerWriter( oapiWrite ); 69 | graph.registerWriter( stWrite ); 70 | 71 | return graph; 72 | }; 73 | 74 | it( "findAllPaths (incl core-types)", ( ) => 75 | { 76 | const graph = makeGraph( ); 77 | 78 | const paths = graph.findAllPaths( 79 | tsRead, 80 | gqlWrite, 81 | undefined 82 | ); 83 | 84 | expect( paths.map( path => makePathKey( path ) ) ).toMatchSnapshot( ); 85 | } ); 86 | 87 | it( "findAllPaths (only shortcuts when none exist)", ( ) => 88 | { 89 | const graph = makeGraph( ); 90 | 91 | const paths = graph.findAllPaths( 92 | tsRead, 93 | gqlWrite, 94 | true 95 | ); 96 | 97 | expect( paths.map( path => makePathKey( path ) ) ).toMatchSnapshot( ); 98 | } ); 99 | 100 | it( "findAllPaths (only shortcuts when exist)", ( ) => 101 | { 102 | const graph = makeGraph( ); 103 | 104 | const paths = graph.findAllPaths( 105 | stRead, 106 | oapiWrite, 107 | true 108 | ); 109 | 110 | expect( paths.map( path => makePathKey( path ) ) ).toMatchSnapshot( ); 111 | } ); 112 | 113 | it( "findAllPaths (no shortcuts)", ( ) => 114 | { 115 | const graph = makeGraph( ); 116 | 117 | const paths = graph.findAllPaths( 118 | stRead, 119 | oapiWrite, 120 | false 121 | ); 122 | 123 | expect( paths.map( path => makePathKey( path ) ) ).toMatchSnapshot( ); 124 | } ); 125 | } ); 126 | -------------------------------------------------------------------------------- /lib/format-graph.ts: -------------------------------------------------------------------------------- 1 | import { TypeImplementation } from "./types.js" 2 | import { Reader } from "./reader.js" 3 | import { Writer } from "./writer.js" 4 | 5 | 6 | type TI = TypeImplementation; 7 | 8 | interface GraphPathSegment 9 | { 10 | format: TI; 11 | reader: Reader; 12 | writer: Writer; 13 | } 14 | 15 | type PathKey = string; 16 | type GraphPath = Array< GraphPathSegment >; 17 | 18 | export function makePathKey( path: GraphPath ): PathKey 19 | { 20 | return path 21 | .map( ( { format, reader, writer } ) => 22 | `${reader.kind}->{${format}}->${writer.kind}` 23 | ) 24 | .join( ' ' ); 25 | } 26 | 27 | export class FormatGraph 28 | { 29 | // Maps from-type -> (type-type -> reader) 30 | private readerGraph = new Map< TI, Map< TI, Reader > >( ); 31 | // Maps from-type -> (type-type -> writer) 32 | private writerGraph = new Map< TI, Map< TI, Writer > >( ); 33 | 34 | constructor( ) { } 35 | 36 | registerReader( reader: Reader ) 37 | { 38 | const toMap: Array< [ TI, Reader ] > = [ 39 | [ 'ct', reader ], 40 | ...Object 41 | .keys( reader.shortcut ?? { } ) 42 | .map( ( key ): [ TI, Reader ] => [ key as TI, reader ] ), 43 | ]; 44 | this.readerGraph.set( reader.kind, new Map( toMap ) ); 45 | } 46 | 47 | registerWriter( writer: Writer ) 48 | { 49 | const makeTo = ( ): [ TI, Writer ] => [ writer.kind, writer ]; 50 | 51 | const insertWriter = ( from: TI ) => 52 | { 53 | const old = [ ...this.writerGraph.get( from ) ?? [ ] ]; 54 | this.writerGraph.set( from, new Map( [ ...old, makeTo( ) ] ) ); 55 | }; 56 | 57 | insertWriter( 'ct' ); 58 | Object 59 | .keys( writer.shortcut ?? { } ) 60 | .forEach( key => insertWriter( key as TI ) ); 61 | } 62 | 63 | findAllPaths( 64 | reader: Reader, 65 | writer: Writer, 66 | shortcuts: boolean | undefined 67 | ) 68 | : Array< GraphPath > 69 | { 70 | const paths = new Map< PathKey, GraphPath >( ); 71 | 72 | interface Opts 73 | { 74 | allowManaged?: boolean; 75 | cache: Set< Reader >; 76 | } 77 | 78 | const appendSet = < T >( set: Set< T >, val: T ) => 79 | new Set( [ ...set, val ] ); 80 | 81 | const recurse = ( reader: Reader, path: GraphPath, opts: Opts ) => 82 | { 83 | const { allowManaged = false, cache } = opts; 84 | 85 | const handleFound = ( writer: Writer, format: TI ) => 86 | { 87 | if ( reader.managedRead && !allowManaged ) 88 | return; 89 | const newPath = [ ...path, { reader, writer, format } ]; 90 | const pathKey = makePathKey( newPath ); 91 | paths.set( pathKey, newPath ); 92 | }; 93 | 94 | const formats = [ 95 | ...shortcuts ? [ ] : [ 'ct' ], 96 | ...shortcuts !== false 97 | ? Object.keys( reader.shortcut ?? { } ) 98 | : [ ] 99 | ]; 100 | for ( const format of formats as Array< TI > ) 101 | { 102 | const writers = this.writerGraph.get( format ); 103 | for ( const [ to, _writer ] of writers?.entries( ) ?? [ ] ) 104 | { 105 | if ( writer.kind === _writer.kind ) 106 | handleFound( writer, format ); 107 | else if ( reader.kind === _writer.kind ) 108 | continue; 109 | else 110 | { 111 | // Find readers and recurse 112 | const readers = this.readerGraph.get( to ); 113 | for ( const _reader of readers?.values( ) ?? [ ] ) 114 | { 115 | if ( _reader.managedRead && !allowManaged ) 116 | continue; 117 | else if ( cache.has( _reader ) ) 118 | continue; // Cyclic 119 | recurse( 120 | _reader, 121 | [ ...path, { reader, writer: _writer, format } ], 122 | { 123 | cache: appendSet( cache, _reader ), 124 | } 125 | ); 126 | } 127 | } 128 | } 129 | } 130 | }; 131 | 132 | recurse( reader, [ ], { allowManaged: true, cache: new Set( ) } ); 133 | 134 | return [ ...paths.values( ) ].sort( ( a, b ) => a.length - b.length ); 135 | } 136 | 137 | findBestPath( 138 | reader: Reader, 139 | writer: Writer, 140 | shortcut: boolean | undefined 141 | ) 142 | { 143 | const paths = this.findAllPaths( reader, writer, shortcut ); 144 | if ( paths.length > 0 ) 145 | return paths[ 0 ]; 146 | 147 | // Allow all, find shortest path 148 | return this.findAllPaths( reader, writer, undefined )[ 0 ]; 149 | } 150 | 151 | clone( ) 152 | { 153 | const clone = new FormatGraph( ); 154 | 155 | const readers = [ ...this.readerGraph.values( ) ] 156 | .flatMap( map => [ ...map.values( ) ] ); 157 | const writers = [ ...this.writerGraph.values( ) ] 158 | .flatMap( map => [ ...map.values( ) ] ); 159 | 160 | readers.forEach( reader => clone.registerReader( reader ) ); 161 | writers.forEach( writer => clone.registerWriter( writer ) ); 162 | 163 | return clone; 164 | } 165 | } 166 | 167 | const defaultGraph = new FormatGraph( ); 168 | 169 | export function registerReader( reader: Reader ) 170 | { 171 | defaultGraph.registerReader( reader ); 172 | } 173 | export function registerWriter( writer: Writer ) 174 | { 175 | defaultGraph.registerWriter( writer ); 176 | } 177 | 178 | export interface ConversionOptions 179 | { 180 | shortcut?: boolean; 181 | } 182 | 183 | export class ConversionContext 184 | { 185 | private shortcut: boolean | undefined; 186 | private graph: FormatGraph; 187 | 188 | constructor( 189 | private reader: Reader, 190 | private writer: Writer, 191 | opts: ConversionOptions = { } 192 | ) 193 | { 194 | this.shortcut = opts.shortcut; 195 | this.graph = defaultGraph.clone( ); 196 | this.graph.registerReader( reader ); 197 | this.graph.registerWriter( writer ); 198 | } 199 | 200 | getPath( ) 201 | { 202 | return this.graph.findBestPath( 203 | this.reader, 204 | this.writer, 205 | this.shortcut 206 | ); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /lib/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | makeConverter, 3 | getTypeScriptReader, 4 | getJsonSchemaWriter, 5 | } from "./index.js" 6 | 7 | 8 | describe( "index", ( ) => 9 | { 10 | it( "", async ( ) => 11 | { 12 | const converter = 13 | makeConverter( getTypeScriptReader( ), getJsonSchemaWriter( ) ); 14 | 15 | const { data } = 16 | await converter.convert( { 17 | data: 'export type Foo = number | string;' 18 | } ); 19 | 20 | expect( JSON.parse( data ) ).toStrictEqual( { 21 | $comment: expect.anything( ), 22 | definitions: { 23 | Foo: { type: [ 'number', 'string' ], title: 'Foo' }, 24 | }, 25 | } ); 26 | } ); 27 | } ); 28 | -------------------------------------------------------------------------------- /lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types.js" 2 | export * from "./reader.js" 3 | export * from "./writer.js" 4 | 5 | export * from "./error.js" 6 | 7 | export * from "./convert-graphql.js" 8 | export * from "./convert-json-schema.js" 9 | export * from "./convert-suretype.js" 10 | export * from "./convert-typescript.js" 11 | 12 | export * from "./converter.js" 13 | export * from "./batch-convert.js" 14 | -------------------------------------------------------------------------------- /lib/package.ts: -------------------------------------------------------------------------------- 1 | export const userPackage = 'typeconv'; 2 | export const userPackageUrl = 'https://github.com/grantila/typeconv'; 3 | -------------------------------------------------------------------------------- /lib/reader.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument, WarnFunction, ConversionResult } from "core-types" 2 | 3 | import type { SyncOrAsync, TypeImplementation } from "./types.js" 4 | 5 | 6 | export interface ReaderOptions 7 | { 8 | warn: WarnFunction; 9 | filename?: string; 10 | } 11 | 12 | export type ReaderFunction = ( data: string, opts: ReaderOptions ) => 13 | SyncOrAsync< ConversionResult< NodeDocument > >; 14 | 15 | export type ShortcutReaderFunction = ( data: string, opts: ReaderOptions ) => 16 | SyncOrAsync< ConversionResult< string > >; 17 | 18 | export interface Reader 19 | { 20 | kind: TypeImplementation; 21 | read: ReaderFunction; 22 | managedRead?: boolean; 23 | shortcut?: Partial< Record< TypeImplementation, ShortcutReaderFunction > >; 24 | } 25 | -------------------------------------------------------------------------------- /lib/tests/__snapshots__/ts-to-json-schema.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ts-to-json-schema interface heritage 1`] = ` 4 | "{ 5 | "definitions": { 6 | "Foo": { 7 | "type": "object", 8 | "properties": { 9 | "b": { 10 | "type": "string", 11 | "title": "Bar.b" 12 | }, 13 | "f": { 14 | "type": "number", 15 | "title": "Foo.f" 16 | } 17 | }, 18 | "required": [ 19 | "b", 20 | "f" 21 | ], 22 | "additionalProperties": false, 23 | "title": "Foo, Bar" 24 | }, 25 | "Bar": { 26 | "type": "object", 27 | "properties": { 28 | "b": { 29 | "type": "string", 30 | "title": "Bar.b" 31 | } 32 | }, 33 | "required": [ 34 | "b" 35 | ], 36 | "additionalProperties": false, 37 | "title": "Bar" 38 | } 39 | }, 40 | "$comment": "Generated by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)" 41 | }" 42 | `; 43 | 44 | exports[`ts-to-json-schema type intersection 1`] = ` 45 | "{ 46 | "definitions": { 47 | "Foo": { 48 | "type": "object", 49 | "properties": { 50 | "b": { 51 | "type": "string", 52 | "title": "Bar.b" 53 | }, 54 | "f": { 55 | "type": "number", 56 | "title": "f" 57 | } 58 | }, 59 | "required": [ 60 | "b", 61 | "f" 62 | ], 63 | "additionalProperties": false, 64 | "title": "Foo, Bar" 65 | }, 66 | "Bar": { 67 | "type": "object", 68 | "properties": { 69 | "b": { 70 | "type": "string", 71 | "title": "Bar.b" 72 | } 73 | }, 74 | "required": [ 75 | "b" 76 | ], 77 | "additionalProperties": false, 78 | "title": "Bar" 79 | } 80 | }, 81 | "$comment": "Generated by core-types-json-schema (https://github.com/grantila/core-types-json-schema) on behalf of typeconv (https://github.com/grantila/typeconv)" 82 | }" 83 | `; 84 | -------------------------------------------------------------------------------- /lib/tests/__snapshots__/ts-to-openapi.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`ts-to-openapi ensure descriptions are forwarded 1`] = ` 4 | "openapi: 3.0.0 5 | info: 6 | title: My API 7 | version: v1 8 | x-comment: >- 9 | Generated by core-types-json-schema 10 | (https://github.com/grantila/core-types-json-schema) on behalf of typeconv 11 | (https://github.com/grantila/typeconv) 12 | paths: {} 13 | components: 14 | schemas: 15 | Point: 16 | properties: 17 | x: 18 | title: Point.x 19 | description: The distance from the left in mm 20 | type: number 21 | 'y': 22 | title: Point.y 23 | description: The distance from the top in mm 24 | type: number 25 | required: 26 | - x 27 | - 'y' 28 | additionalProperties: false 29 | title: Point 30 | type: object 31 | " 32 | `; 33 | 34 | exports[`ts-to-openapi typescript to openapi 1`] = ` 35 | "openapi: 3.0.0 36 | info: 37 | title: My API 38 | version: v1 39 | x-comment: >- 40 | Generated by core-types-json-schema 41 | (https://github.com/grantila/core-types-json-schema) on behalf of typeconv 42 | (https://github.com/grantila/typeconv) 43 | paths: {} 44 | components: 45 | schemas: 46 | Foo: 47 | properties: 48 | a: 49 | title: Foo.a 50 | type: string 51 | b: 52 | title: Foo.b 53 | nullable: true 54 | c: 55 | title: Foo.c 56 | type: number 57 | d: 58 | title: Foo.d 59 | type: boolean 60 | e: 61 | $ref: '#/components/schemas/Thing' 62 | title: Foo.e 63 | required: 64 | - a 65 | - b 66 | - c 67 | - d 68 | - e 69 | additionalProperties: false 70 | title: Foo 71 | type: object 72 | Thing: 73 | properties: 74 | x: 75 | title: Thing.x 76 | enum: 77 | - 6 78 | type: number 79 | 'y': 80 | title: Thing.y 81 | type: string 82 | required: 83 | - x 84 | - 'y' 85 | additionalProperties: false 86 | title: Thing 87 | type: object 88 | " 89 | `; 90 | -------------------------------------------------------------------------------- /lib/tests/ts-to-json-schema.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from "@jest/globals" 2 | 3 | import { 4 | makeConverter, 5 | getTypeScriptReader, 6 | getJsonSchemaWriter, 7 | } from '../index.js' 8 | 9 | 10 | describe( 'ts-to-json-schema', ( ) => 11 | { 12 | // https://github.com/grantila/typeconv/issues/15 13 | // https://github.com/grantila/typeconv/issues/35 14 | it( 'interface heritage', async ( ) => 15 | { 16 | const input = ` 17 | interface Bar { 18 | b: string; 19 | } 20 | 21 | export interface Foo extends Bar { 22 | f: number; 23 | } 24 | `; 25 | 26 | const { convert } = makeConverter( 27 | getTypeScriptReader( ), 28 | getJsonSchemaWriter( ), 29 | { 30 | mergeObjects: true, 31 | } 32 | ); 33 | 34 | const { data } = await convert( { data: input } ); 35 | 36 | expect( data ).toMatchSnapshot( ); 37 | } ); 38 | 39 | // https://github.com/grantila/typeconv/issues/35 40 | it( 'type intersection', async ( ) => 41 | { 42 | const input = ` 43 | type Bar = { 44 | b: string; 45 | } 46 | 47 | export type Foo = Bar & { 48 | f: number; 49 | } 50 | `; 51 | 52 | const { convert } = makeConverter( 53 | getTypeScriptReader( ), 54 | getJsonSchemaWriter( ), 55 | { 56 | mergeObjects: true, 57 | } 58 | ); 59 | 60 | const { data } = await convert( { data: input } ); 61 | 62 | expect( data ).toMatchSnapshot( ); 63 | } ); 64 | } ); 65 | -------------------------------------------------------------------------------- /lib/tests/ts-to-openapi.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, describe, it } from "@jest/globals" 2 | 3 | import { 4 | makeConverter, 5 | getTypeScriptReader, 6 | getOpenApiWriter, 7 | } from '../index.js' 8 | 9 | 10 | describe( 'ts-to-openapi', ( ) => 11 | { 12 | it( 'typescript to openapi', async ( ) => 13 | { 14 | const input = ` 15 | type Thing = { 16 | x: 6 17 | y: string 18 | } 19 | 20 | export type Foo = { 21 | a: string 22 | b: null 23 | c: number 24 | d: boolean 25 | e: Thing 26 | } 27 | `; 28 | 29 | const { convert } = makeConverter( 30 | getTypeScriptReader( ), 31 | getOpenApiWriter( { 32 | format: 'yaml', 33 | title: 'My API', 34 | version: 'v1' 35 | } ) 36 | ); 37 | 38 | const { data } = await convert( { data: input } ); 39 | 40 | expect( data ).toMatchSnapshot( ); 41 | } ); 42 | 43 | it( 'ensure descriptions are forwarded', async ( ) => 44 | { 45 | const input = ` 46 | export type Point = { 47 | /** The distance from the left in mm */ 48 | x: number; 49 | /** The distance from the top in mm */ 50 | y: number; 51 | }; 52 | `; 53 | 54 | const { convert } = makeConverter( 55 | getTypeScriptReader( ), 56 | getOpenApiWriter( { 57 | format: 'yaml', 58 | title: 'My API', 59 | version: 'v1' 60 | } ) 61 | ); 62 | 63 | const { data } = await convert( { data: input } ); 64 | 65 | expect( data ).toMatchSnapshot( ); 66 | } ); 67 | } ); 68 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Options 3 | { 4 | declaration?: boolean; 5 | userPackage?: string; 6 | userPackageUrl?: string; 7 | noDisableLintHeader?: boolean; 8 | noDescriptiveHeader?: boolean; 9 | } 10 | 11 | export type SyncOrAsync< T > = T | PromiseLike< T >; 12 | 13 | export type TypeImplementation = 14 | // TypeScript 15 | | 'ts' 16 | // JSON Schema 17 | | 'jsc' 18 | // GraphQL 19 | | 'gql' 20 | // Open API 21 | | 'oapi' 22 | // SureType 23 | | 'st' 24 | // core-types 25 | | 'ct'; 26 | -------------------------------------------------------------------------------- /lib/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, expect, describe, it } from "@jest/globals" 2 | 3 | import { ensureType, stringify } from "./utils.js" 4 | 5 | describe( "utils", ( ) => 6 | { 7 | describe( "stringify", ( ) => 8 | { 9 | it( "should stringify string", ( ) => 10 | { 11 | expect( stringify( "foo" ) ).toBe( '"foo"' ); 12 | } ); 13 | 14 | it( "should stringify object", ( ) => 15 | { 16 | expect( stringify( { foo: "bar" } ) ).toBe( '{\n "foo": "bar"\n}' ); 17 | } ); 18 | } ); 19 | 20 | describe( "ensureType", ( ) => 21 | { 22 | it( "should fail on invalida data", ( ) => 23 | { 24 | const consoleErrorOrig = console.error; 25 | 26 | const orElse = jest.fn( ); 27 | const consoleError = jest.fn( ); 28 | console.error = consoleError; 29 | 30 | const ret = 31 | ensureType( "foo", "bars", [ "bar", "baz" ], orElse as any ); 32 | 33 | console.error = consoleErrorOrig; 34 | 35 | expect( ret ).toBe( undefined ); 36 | expect( orElse ).toHaveBeenCalledTimes( 1 ); 37 | expect( consoleError ).toHaveBeenCalledWith( 'Invalid bars: foo' ); 38 | } ); 39 | 40 | it( "should succeed if valid data", ( ) => 41 | { 42 | const consoleErrorOrig = console.error; 43 | 44 | const orElse = jest.fn( ); 45 | const consoleError = jest.fn( ); 46 | console.error = consoleError; 47 | 48 | const ret = 49 | ensureType( "bar", "bars", [ "bar", "baz" ], orElse as any ); 50 | 51 | console.error = consoleErrorOrig; 52 | 53 | expect( ret ).toBe( true ); 54 | expect( orElse ).not.toHaveBeenCalled( ); 55 | expect( consoleError ).not.toHaveBeenCalled( ); 56 | } ); 57 | } ); 58 | } ); 59 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | export function stringify( value: any ) 3 | { 4 | return JSON.stringify( value, null, 2 ); 5 | } 6 | 7 | export function ensureType< T >( 8 | data: T | string, 9 | typeName: string, 10 | valids: Array< T >, 11 | orElse: ( ) => never 12 | ) 13 | // @ts-ignore 14 | : data is T 15 | { 16 | if ( valids.includes( data as T ) ) 17 | return true; 18 | 19 | if ( data ) 20 | console.error( `Invalid ${typeName}: ${data}` ); 21 | 22 | orElse( ); 23 | } 24 | -------------------------------------------------------------------------------- /lib/writer.ts: -------------------------------------------------------------------------------- 1 | import type { NodeDocument, WarnFunction, ConversionResult } from "core-types" 2 | 3 | import type { SyncOrAsync, TypeImplementation } from "./types.js" 4 | import type { Reader } from "./reader.js" 5 | 6 | 7 | export interface WriterOptions 8 | { 9 | warn: WarnFunction; 10 | sourceFilename?: string; 11 | filename?: string; 12 | rawInput?: string; 13 | } 14 | 15 | export type WriterFunction = 16 | ( doc: NodeDocument, opts: WriterOptions ) => 17 | SyncOrAsync< ConversionResult< string > >; 18 | 19 | export type Shortcut = 20 | ( 21 | data: string, 22 | writeOpts: WriterOptions, 23 | reader: Reader 24 | ) => 25 | SyncOrAsync< ConversionResult< string > >; 26 | 27 | export interface Writer 28 | { 29 | kind: TypeImplementation; 30 | write: WriterFunction; 31 | shortcut?: Partial< Record< TypeImplementation, Shortcut > >; 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeconv", 3 | "version": "0.0.0-development", 4 | "description": "Convert between JSON Schema, TypeScript, GraphQL and Open API", 5 | "author": "Gustaf Räntilä", 6 | "license": "MIT", 7 | "bugs": { 8 | "url": "https://github.com/grantila/typeconv/issues" 9 | }, 10 | "homepage": "https://github.com/grantila/typeconv#readme", 11 | "types": "./dist/index.d.ts", 12 | "main": "./dist/index.js", 13 | "browser": "./dist/index.js", 14 | "exports": { 15 | ".": "./dist/index.js", 16 | "./package.json": "./package.json" 17 | }, 18 | "directories": {}, 19 | "type": "module", 20 | "sideEffects": "false", 21 | "engines": { 22 | "node": ">=14.13.1" 23 | }, 24 | "files": [ 25 | "dist" 26 | ], 27 | "bin": "dist/bin/typeconv.js", 28 | "scripts": { 29 | "build": "rimraf dist && tsc -p tsconfig.prod.json", 30 | "dev": "ts-node-esm lib/bin/typeconv.ts", 31 | "test": "CI=1 NODE_OPTIONS=--experimental-vm-modules jest --coverage", 32 | "cz": "git-cz" 33 | }, 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/grantila/typeconv" 37 | }, 38 | "keywords": [ 39 | "convert", 40 | "types", 41 | "json", 42 | "schema", 43 | "typescript", 44 | "graphql" 45 | ], 46 | "devDependencies": { 47 | "@babel/preset-env": "^7.22.4", 48 | "@babel/preset-typescript": "^7.21.5", 49 | "@types/babel__code-frame": "^7.0.3", 50 | "@types/jest": "^29.5.2", 51 | "@types/js-yaml": "^4.0.5", 52 | "@types/node": "^18.16.16", 53 | "cz-conventional-changelog": "^3.3.0", 54 | "execa": "^7.1.1", 55 | "jest": "^29.5.0", 56 | "rimraf": "^5.0.1", 57 | "tempy": "^3.0.0", 58 | "ts-jest-resolver": "^2.0.1", 59 | "ts-node": "^10.9.1", 60 | "typescript": "^5.1.3" 61 | }, 62 | "dependencies": { 63 | "already": "^3.4.1", 64 | "awesome-code-frame": "^1.1.0", 65 | "chalk": "^5.2.0", 66 | "core-types": "^3.1.0", 67 | "core-types-graphql": "^3.0.0", 68 | "core-types-json-schema": "^2.2.0", 69 | "core-types-suretype": "^3.2.0", 70 | "core-types-ts": "^4.1.0", 71 | "globby": "^13.1.4", 72 | "js-yaml": "^4.1.0", 73 | "oppa": "^0.4.0", 74 | "terminal-link": "^3.0.0" 75 | }, 76 | "config": { 77 | "commitizen": { 78 | "path": "./node_modules/cz-conventional-changelog" 79 | } 80 | }, 81 | "packageManager": "yarn@3.2.4" 82 | } 83 | -------------------------------------------------------------------------------- /test/utils.ts: -------------------------------------------------------------------------------- 1 | import { jest } from "@jest/globals" 2 | 3 | export type MockedLoggers< F extends 'log' | 'warn' | 'error' > = 4 | { 5 | [ K in F ]: jest.Mock; 6 | } 7 | 8 | export function withConsoleMock< R, F extends 'log' | 'warn' | 'error' >( 9 | fn: ( mocks: MockedLoggers< F > ) => R, 10 | logFn: Array< F > 11 | ) 12 | { 13 | return async ( ) => 14 | { 15 | const mockFns = logFn 16 | .map( fn => 17 | { 18 | const orig = console[ fn ]; 19 | const mock = jest.fn( ); 20 | console[ fn ] = mock; 21 | return { fn, orig, mock }; 22 | } ); 23 | 24 | const mocks = Object.fromEntries( 25 | mockFns.map( ( { fn, mock } ) => [ fn, mock ] ) 26 | ) as MockedLoggers< F >; 27 | 28 | await fn( mocks ); 29 | 30 | mockFns.forEach( ( { fn, orig } ) => 31 | { 32 | console[ fn ] = orig; 33 | } ); 34 | } 35 | } 36 | 37 | export function withConsoleError< R >( 38 | fn: ( mocks: MockedLoggers< 'error' > ) => R 39 | ) 40 | { 41 | return withConsoleMock( fn, [ 'error' ] ); 42 | } 43 | 44 | export function withConsoleWarn< R >( 45 | fn: ( mocks: MockedLoggers< 'warn' > ) => R 46 | ) 47 | { 48 | return withConsoleMock( fn, [ 'warn' ] ); 49 | } 50 | 51 | export function withConsoleLog< R >( 52 | fn: ( mocks: MockedLoggers< 'log' > ) => R 53 | ) 54 | { 55 | return withConsoleMock( fn, [ 'log' ] ); 56 | } 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "declaration": true, 5 | "sourceMap": true, 6 | "lib": [ "ES2020" ], 7 | "types": [ 8 | "node", 9 | "jest" 10 | ], 11 | "noEmit": true, 12 | "target": "ES2020", 13 | "module": "ESNext", 14 | "esModuleInterop": true, 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "noImplicitAny": true, 18 | "noUnusedLocals": true, 19 | "pretty": true, 20 | "strict": true, 21 | "alwaysStrict": true, 22 | }, 23 | "include": [ 24 | "lib", 25 | "test" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "dist", 6 | "rootDir": "lib", 7 | "noEmit": false 8 | }, 9 | "include": [ 10 | "lib" 11 | ], 12 | "exclude": [ 13 | "**/fixtures", 14 | "**/*.test.ts" 15 | ] 16 | } 17 | --------------------------------------------------------------------------------