json-schema-to-typescript
Version:
compile json schema to typescript typings
195 lines (174 loc) • 6.23 kB
text/typescript
import {readFileSync} from 'fs'
import {JSONSchema4} from 'json-schema'
import {ParserOptions as $RefOptions} from '@apidevtools/json-schema-ref-parser'
import {cloneDeep, endsWith, merge} from 'lodash'
import {dirname} from 'path'
import {Options as PrettierOptions} from 'prettier'
import {format} from './formatter'
import {generate} from './generator'
import {normalize} from './normalizer'
import {optimize} from './optimizer'
import {parse} from './parser'
import {dereference} from './resolver'
import {error, stripExtension, Try, log, parseFileAsJSONSchema} from './utils'
import {validate} from './validator'
import {isDeepStrictEqual} from 'util'
import {link} from './linker'
import {validateOptions} from './optionValidator'
import {JSONSchema as LinkedJSONSchema} from './types/JSONSchema'
export {EnumJSONSchema, JSONSchema, NamedEnumJSONSchema, CustomTypeJSONSchema} from './types/JSONSchema'
export interface Options {
/**
* [$RefParser](https://github.com/APIDevTools/json-schema-ref-parser) Options, used when resolving `$ref`s
*/
$refOptions: $RefOptions
/**
* Default value for additionalProperties, when it is not explicitly set.
*/
additionalProperties: boolean
/**
* Disclaimer comment prepended to the top of each generated file.
*/
bannerComment: string
/**
* Custom function to provide a type name for a given schema
*/
customName?: (schema: LinkedJSONSchema, keyNameFromDefinition: string | undefined) => string | undefined
/**
* Root directory for resolving [`$ref`](https://tools.ietf.org/id/draft-pbryan-zyp-json-ref-03.html)s.
*/
cwd: string
/**
* Declare external schemas referenced via `$ref`?
*/
declareExternallyReferenced: boolean
/**
* Prepend enums with [`const`](https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)?
*/
enableConstEnums: boolean
/**
* Create enums from JSON enums with eponymous keys
*/
inferStringEnumKeysFromValues: boolean
/**
* Format code? Set this to `false` to improve performance.
*/
format: boolean
/**
* Ignore maxItems and minItems for `array` types, preventing tuples being generated.
*/
ignoreMinAndMaxItems: boolean
/**
* Maximum number of unioned tuples to emit when representing bounded-size array types,
* before falling back to emitting unbounded arrays. Increase this to improve precision
* of emitted types, decrease it to improve performance, or set it to `-1` to ignore
* `minItems` and `maxItems`.
*/
maxItems: number
/**
* Append all index signatures with `| undefined` so that they are strictly typed.
*
* This is required to be compatible with `strictNullChecks`.
*/
strictIndexSignatures: boolean
/**
* A [Prettier](https://prettier.io/docs/en/options.html) configuration.
*/
style: PrettierOptions
/**
* Generate code for `definitions` that aren't referenced by the schema?
*/
unreachableDefinitions: boolean
/**
* Generate unknown type instead of any
*/
unknownAny: boolean
}
export const DEFAULT_OPTIONS: Options = {
$refOptions: {},
additionalProperties: true, // TODO: default to empty schema (as per spec) instead
bannerComment: `/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/`,
cwd: process.cwd(),
declareExternallyReferenced: true,
enableConstEnums: true,
inferStringEnumKeysFromValues: false,
format: true,
ignoreMinAndMaxItems: false,
maxItems: 20,
strictIndexSignatures: false,
style: {
bracketSpacing: false,
printWidth: 120,
semi: true,
singleQuote: false,
tabWidth: 2,
trailingComma: 'none',
useTabs: false,
},
unreachableDefinitions: false,
unknownAny: true,
}
export function compileFromFile(filename: string, options: Partial<Options> = DEFAULT_OPTIONS): Promise<string> {
const schema = parseAsJSONSchema(filename)
return compile(schema, stripExtension(filename), {cwd: dirname(filename), ...options})
}
function parseAsJSONSchema(filename: string): JSONSchema4 {
const contents = Try(
() => readFileSync(filename),
() => {
throw new ReferenceError(`Unable to read file "${filename}"`)
},
)
return parseFileAsJSONSchema(filename, contents.toString())
}
export async function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): Promise<string> {
validateOptions(options)
const _options = merge({}, DEFAULT_OPTIONS, options)
const start = Date.now()
function time() {
return `(${Date.now() - start}ms)`
}
// normalize options
if (!endsWith(_options.cwd, '/')) {
_options.cwd += '/'
}
// Initial clone to avoid mutating the input
const _schema = cloneDeep(schema)
const {dereferencedPaths, dereferencedSchema} = await dereference(_schema, _options)
if (process.env.VERBOSE) {
if (isDeepStrictEqual(_schema, dereferencedSchema)) {
log('green', 'dereferencer', time(), '✅ No change')
} else {
log('green', 'dereferencer', time(), '✅ Result:', dereferencedSchema)
}
}
const linked = link(dereferencedSchema)
if (process.env.VERBOSE) {
log('green', 'linker', time(), '✅ No change')
}
const errors = validate(linked, name)
if (errors.length) {
errors.forEach(_ => error(_))
throw new ValidationError()
}
if (process.env.VERBOSE) {
log('green', 'validator', time(), '✅ No change')
}
const normalized = normalize(linked, dereferencedPaths, name, _options)
log('yellow', 'normalizer', time(), '✅ Result:', normalized)
const parsed = parse(normalized, _options)
log('blue', 'parser', time(), '✅ Result:', parsed)
const optimized = optimize(parsed, _options)
log('cyan', 'optimizer', time(), '✅ Result:', optimized)
const generated = generate(optimized, _options)
log('magenta', 'generator', time(), '✅ Result:', generated)
const formatted = await format(generated, _options)
log('white', 'formatter', time(), '✅ Result:', formatted)
return formatted
}
export class ValidationError extends Error {}