UNPKG

@rxap/json-schema-to-typescript

Version:

Generate TypeScript interfaces from JSON Schema definitions. It allows you to programmatically create and manipulate TypeScript interface definitions based on JSON schema inputs. The package provides utilities to convert JSON schema to TypeScript interfac

439 lines 19.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.TypescriptInterfaceGenerator = void 0; const tslib_1 = require("tslib"); const $RefParser = require("@apidevtools/json-schema-ref-parser"); const ts_morph_1 = require("ts-morph"); const coerce_imports_1 = require("./coerce-imports"); const get_from_object_1 = require("./get-from-object"); const join_1 = require("./join"); const strings_1 = require("./strings"); class TypescriptInterfaceGenerator { static isRequired(schema, key) { return (!!schema.required && Array.isArray(schema.required) && schema.required.includes(key)); } static coercePropertyKey(key) { if (key.match(/(^[0-9]+|-|#|\.|@|\/|:|\*)/) && !key.match(/\[\w+:\s?\w+\]/)) { return `'${key}'`; } return key; } static unionType(array) { if (array.length < 2) { return array[0]; } const first = array.shift(); const second = array.shift(); return ts_morph_1.Writers.unionType(first, second, ...array); } static intersectionType(array) { if (array.length < 2) { return array[0]; } const first = array.shift(); const second = array.shift(); return ts_morph_1.Writers.intersectionType(first, second, ...array); } constructor(schema, options = {}, project = null) { this.schema = schema; this.options = options; this.bundledSchema = null; this.project = project !== null && project !== void 0 ? project : new ts_morph_1.Project({ useInMemoryFileSystem: true, manipulationSettings: { indentationText: ts_morph_1.IndentationText.TwoSpaces, quoteKind: ts_morph_1.QuoteKind.Single, useTrailingCommas: true, }, }); } static bundleSchema(schema, options = {}) { return $RefParser.bundle(schema, options); } build(name) { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!name) { throw new Error('The name must not be empty!'); } yield this.bundleSchema(); return this.buildSync(name, this.bundledSchema); }); } buildSync(name, bundledSchema) { var _a; if (bundledSchema === void 0) { bundledSchema = (_a = this.bundledSchema) !== null && _a !== void 0 ? _a : this.schema; } if (!name) { throw new Error('The name must not be empty!'); } this.bundledSchema = bundledSchema; if (!this.bundledSchema) { throw new Error('If buildSync is called, bundledSchema must be provided!'); } return this.addType(this.bundledSchema, name); } addType(schema, name) { let resolvedSchema; if (schema.$ref) { resolvedSchema = this.resolveRef(schema.$ref); } else { resolvedSchema = schema; } let sourceFile; const importList = []; if (resolvedSchema.type === 'object' && resolvedSchema.properties) { sourceFile = this.addInterface(resolvedSchema, name, importList); } else { sourceFile = this.addTypeAlias(resolvedSchema, name, importList); } for (const importItem of importList) { (0, coerce_imports_1.CoerceImports)(sourceFile, { isTypeOnly: true, moduleSpecifier: importItem.moduleSpecifier, namedImports: [importItem.name], }); } sourceFile.organizeImports(); return sourceFile; } addTypeAlias(schema, name, importList) { const filePath = (0, join_1.joinPath)(this.options.basePath, `${this.getFileName(name)}.ts`); let sourceFile = this.project.getSourceFile(filePath); if (sourceFile) { return sourceFile; } sourceFile = this.project.createSourceFile(filePath); const type = this.propertyTypeWriteFunction(sourceFile, schema, name, importList, name); if (!type) { throw new Error('Could not create a write function for the type!'); } const typeAliasDeclarationStructure = { name: this.buildName(name), type: type === 'unknown' ? 'T' : type, isExported: true, typeParameters: type === 'unknown' ? [ { name: 'T', default: 'unknown', }, ] : undefined, }; sourceFile.addTypeAlias(typeAliasDeclarationStructure); sourceFile.organizeImports(); return sourceFile; } addInterface(schema, name, importList) { const filePath = (0, join_1.joinPath)(this.options.basePath, `${this.getFileName(name)}.ts`); let sourceFile = this.project.getSourceFile(filePath); if (sourceFile) { return sourceFile; } sourceFile = this.project.createSourceFile(filePath); if (!schema.properties) { console.debug(schema.$ref, schema.type, Object.keys(schema)); throw new Error('The provided schema has not a properties declaration!'); } const properties = []; for (const [key, property] of Object.entries(schema.properties)) { properties.push(this.buildPropertySignatureStructure(sourceFile, key, property, TypescriptInterfaceGenerator.isRequired(schema, key), importList, name)); } const typeName = this.buildName(name); if (schema.additionalProperties === true) { sourceFile.addTypeAlias({ name: typeName, isExported: true, type: w => { ts_morph_1.Writers.objectType({ properties, })(w); w.write(' & '); w.write('T'); }, typeParameters: [ { name: 'T', default: 'unknown', }, ], }); } else { const ref = sourceFile.addInterface({ name: typeName, properties, isExported: true, }); if (!properties.length) { sourceFile.insertStatements(ref.getChildIndex(), '// eslint-disable-next-line @typescript-eslint/no-empty-interface'); } } sourceFile.organizeImports(); return sourceFile; } buildPropertySignatureStructure(currentFile, key, property, required, importList, parentName) { const propertyStructure = { name: TypescriptInterfaceGenerator.coercePropertyKey(key), type: this.propertyTypeWriteFunction(currentFile, property, key, importList, parentName), hasQuestionToken: !required, }; if (property.description) { propertyStructure.docs = [property.description]; } return propertyStructure; } recordType(propertyType, keyType = 'string') { return (writer) => { writer.write('Record<'); writer.write(keyType); writer.write(','); if (typeof propertyType === 'string') { writer.write(propertyType); } else { propertyType(writer); } writer.write('>'); }; } propertyTypeWriteFunction(currentFile, schema, propertyName, importList, parentName) { var _a; // convert to any to support non-standard types like int, unknown, file, etc. switch (schema.type) { case 'string': if (schema.enum) { const isStringArray = (array) => { return array.every((item) => typeof item === 'string'); }; const isNumberArray = (array) => { return array.every((item) => typeof item === 'number'); }; if (isStringArray(schema.enum)) { if (schema.enum.every(item => item.match(/^\d+$/))) { return TypescriptInterfaceGenerator.unionType(schema.enum); } else { if (this.options.useStringTuple) { return TypescriptInterfaceGenerator.unionType(schema.enum.map(item => w => w.quote(item))); } else { const enumName = `${parentName ? (0, strings_1.classify)(parentName) : ''}${(0, strings_1.classify)(propertyName)}${this.options.suffix ? (0, strings_1.classify)(this.options.suffix) : ''}Enum`; currentFile.addEnum({ name: enumName, isExported: true, members: schema.enum.map(item => ({ name: (0, strings_1.underscore)(item).toUpperCase(), value: item, })), }); return enumName; } } } if (isNumberArray(schema.enum)) { return TypescriptInterfaceGenerator.unionType(schema.enum.map(item => item.toFixed(0))); } return TypescriptInterfaceGenerator.unionType(schema.enum.map((item) => (writer) => { if (!isNaN(Number(item))) { writer.write(item); } else if (['true', 'false'].includes(item)) { writer.write(item); } else { writer.quote(item); } })); } if (schema.format === 'binary') { return '(Blob | File | Buffer | ArrayBuffer | Uint32Array | Uint8Array | Uint16Array) & { filename?: string }'; } return 'string'; case 'integer': case 'int': case 'number': return 'number'; case 'boolean': return 'boolean'; case 'null': return 'null'; case 'any': return 'any'; case 'unknown': return 'unknown'; case 'array': if (schema.items) { const items = schema.items; if (!Array.isArray(items) && items !== true) { return (writer) => { writer.write('Array<'); const type = this.propertyTypeWriteFunction(currentFile, items, propertyName, importList, parentName); if (typeof type === 'string') { writer.write(type); } else if (type === undefined) { writer.write('unknown'); } else { type(writer); } writer.write('>'); }; } } return 'Array<unknown>'; case 'object': case undefined: if (schema.properties || schema.additionalProperties || schema.patternProperties) { const objectTypeStructure = { properties: [], }; if (schema.properties) { for (const [key, property] of Object.entries(schema.properties)) { (_a = objectTypeStructure.properties) === null || _a === void 0 ? void 0 : _a.push(this.buildPropertySignatureStructure(currentFile, key, property, TypescriptInterfaceGenerator.isRequired(schema, key), importList, propertyName)); } } if (schema.patternProperties && Object.keys(schema.patternProperties).length) { const typeList = []; for (const [key, property] of Object.entries(schema.patternProperties)) { typeList.push(this.propertyTypeWriteFunction(currentFile, property, propertyName, importList, parentName)); } if (schema.properties && Object.keys(schema.properties).length) { return ts_morph_1.Writers.intersectionType(ts_morph_1.Writers.objectType(objectTypeStructure), this.recordType(typeList.shift()), ...typeList.map((type) => this.recordType(type))); } else { return TypescriptInterfaceGenerator.intersectionType(typeList.map((type) => this.recordType(type))); } } if (schema.additionalProperties) { let type = 'Record<string, unknown>'; if (schema.additionalProperties !== true) { type = this.propertyTypeWriteFunction(currentFile, schema.additionalProperties, propertyName, importList, parentName); } if (schema.properties && Object.keys(schema.properties).length) { return ts_morph_1.Writers.intersectionType(ts_morph_1.Writers.objectType(objectTypeStructure), this.recordType(type)); } else { return this.recordType(type); } } return ts_morph_1.Writers.objectType(objectTypeStructure); } else if (schema.$ref) { const name = this.resolveRefName(schema.$ref); this.addType(this.resolveRef(schema.$ref), name); if (this.options.addImports && currentFile.getBaseNameWithoutExtension() !== this.getFileName(name)) { importList.push({ name: this.buildName(name), moduleSpecifier: `./${this.getFileName(name)}`, }); } return this.buildName(name); } else if (schema.oneOf) { const typeList = []; for (const oneOf of schema.oneOf) { if (typeof oneOf !== 'boolean') { typeList.push(this.propertyTypeWriteFunction(currentFile, oneOf, propertyName, importList, parentName)); } } return TypescriptInterfaceGenerator.unionType(typeList); } else if (schema.anyOf) { const typeList = []; for (const oneOf of schema.anyOf) { if (typeof oneOf !== 'boolean') { typeList.push(this.propertyTypeWriteFunction(currentFile, oneOf, propertyName, importList, parentName)); } } return TypescriptInterfaceGenerator.unionType(typeList); } else if (schema.allOf) { const typeList = []; for (const oneOf of schema.allOf) { if (typeof oneOf !== 'boolean') { typeList.push(this.propertyTypeWriteFunction(currentFile, oneOf, propertyName, importList, parentName)); } } return TypescriptInterfaceGenerator.intersectionType(typeList); } else if (schema.const) { return (writer) => writer.quote(schema.const); } else if (schema.type === 'object') { return w => w.write('Record<string, unknown>'); } console.warn(`The property type is undefined and a ref is not defined! found: ${Object.keys(schema)}`); return 'unknown'; case 'file': return '(Blob | File | Buffer | ArrayBuffer | Uint32Array | Uint8Array | Uint16Array) & { filename?: string }'; default: if (Array.isArray(schema.type)) { const primitiveTypeList = schema.type.filter((type) => ['string', 'integer', 'number', 'boolean', 'null', 'any', 'unknown'].includes(type)); const complexTypeList = schema.type.filter((type) => ![ 'string', 'integer', 'number', 'boolean', 'null', 'any', 'unknown', ].includes(type)); const typeList = primitiveTypeList; for (const type of complexTypeList) { typeList.push(this.propertyTypeWriteFunction(currentFile, Object.assign(Object.assign({}, schema), { type: type }), propertyName, importList, parentName)); } return TypescriptInterfaceGenerator.unionType(typeList); } throw new Error(`The property type '${schema.type}' is not supported!`); } } getFileName(name) { if (this.options.suffix) { return [(0, strings_1.dasherize)(name), (0, strings_1.dasherize)(this.options.suffix)].join('.'); } return (0, strings_1.dasherize)(name); } buildName(name) { if (this.options.suffix) { return (0, strings_1.classify)([name, this.options.suffix].join('-')); } return (0, strings_1.classify)(name); } resolveRefName(ref) { const nameMatch = ref.match(/\/([^/]+)$/); if (!nameMatch) { throw new Error(`Could not resolve ref name '${ref}'`); } return nameMatch[1]; } resolveRef(ref) { const path = ref.split('/'); // remove the prefix '#' path.shift(); const schema = (0, get_from_object_1.getFromObject)(this.bundledSchema, path.join('.')); if (!schema) { throw new Error(`Could not resolve $ref '${ref}'.`); } return schema; } bundleSchema() { return tslib_1.__awaiter(this, void 0, void 0, function* () { if (!this.bundledSchema) { this.bundledSchema = yield TypescriptInterfaceGenerator.bundleSchema(this.schema, this.options); } }); } } exports.TypescriptInterfaceGenerator = TypescriptInterfaceGenerator; //# sourceMappingURL=typescript-interface-generator.js.map