UNPKG

@anttiviljami/dtsgenerator

Version:

TypeScript d.ts file generator for JSON Schema file

414 lines (390 loc) 12.5 kB
import * as ts from 'typescript'; import config from './config'; import { NormalizedSchema } from './jsonSchema'; import SchemaId from './schemaId'; import { Schema } from './type'; import { toValidIdentifier, checkInvalidCharacter } from './validateIdentifier'; function buildTypeNameIdentifier(name: string): ts.Identifier { return ts.factory.createIdentifier(toValidIdentifier(name, config.target)); } function buildPropertyNameIdentifier(name: string): ts.PropertyName { if (checkInvalidCharacter(name, config.target)) { return ts.factory.createIdentifier(name); } else { return ts.factory.createStringLiteral(name); } } export function buildKeyword( kind: ts.KeywordTypeSyntaxKind, ): ts.KeywordTypeNode { return ts.factory.createKeywordTypeNode(kind); } export function buildAnyKeyword(): ts.KeywordTypeNode { return buildKeyword(ts.SyntaxKind.AnyKeyword); } export function buildNeverKeyword(): ts.KeywordTypeNode { return buildKeyword(ts.SyntaxKind.NeverKeyword); } export function buildUnknownKeyword(): ts.KeywordTypeNode { return buildKeyword(ts.SyntaxKind.UnknownKeyword); } export function buildVoidKeyword(): ts.KeywordTypeNode { return buildKeyword(ts.SyntaxKind.VoidKeyword); } export function buildStringKeyword(): ts.KeywordTypeNode { return buildKeyword(ts.SyntaxKind.StringKeyword); } export function buildSimpleArrayNode(element: ts.TypeNode): ts.ArrayTypeNode { return ts.factory.createArrayTypeNode(element); } export function buildNullKeyword(): ts.LiteralTypeNode { return ts.factory.createLiteralTypeNode( ts.factory.createToken(ts.SyntaxKind.NullKeyword), ); } export function buildStringLiteralTypeNode(s: string): ts.LiteralTypeNode { return ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(s)); } export function buildNumericLiteralTypeNode(n: string): ts.LiteralTypeNode { if (!isNaN(parseFloat(n)) && parseFloat(n) < 0) { return ts.factory.createLiteralTypeNode( ts.factory.createPrefixUnaryExpression( ts.SyntaxKind.MinusToken, ts.factory.createNumericLiteral(Math.abs(parseFloat(n))), ), ); } return ts.factory.createLiteralTypeNode(ts.factory.createNumericLiteral(n)); } export function buildBooleanLiteralTypeNode(b: boolean): ts.LiteralTypeNode { return ts.factory.createLiteralTypeNode( b ? ts.factory.createTrue() : ts.factory.createFalse(), ); } export function buildNamespaceNode( name: string, statements: ts.Statement[], root: boolean, ): ts.ModuleDeclaration { const modifiers = root ? [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : undefined; return ts.factory.createModuleDeclaration( modifiers, buildTypeNameIdentifier(name), ts.factory.createModuleBlock(statements), ts.NodeFlags.Namespace | ts.NodeFlags.ExportContext | ts.NodeFlags.ContextFlags, ); } export function buildInterfaceNode( id: SchemaId, members: ts.TypeElement[], root: boolean, ): ts.InterfaceDeclaration { const name = getLastTypeName(id); const modifiers = root ? [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)]; return ts.factory.createInterfaceDeclaration( modifiers, buildTypeNameIdentifier(name), undefined, undefined, members, ); } export function buildTypeAliasNode( id: SchemaId, type: ts.TypeNode, root: boolean, ): ts.TypeAliasDeclaration { const name = getLastTypeName(id); const modifiers = root ? [ts.factory.createModifier(ts.SyntaxKind.DeclareKeyword)] : [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)]; return ts.factory.createTypeAliasDeclaration( modifiers, buildTypeNameIdentifier(name), undefined, type, ); } export function buildPropertySignature( _schema: NormalizedSchema, propertyName: string, valueType: ts.TypeNode, required: string[] | undefined, isPattern: boolean | undefined, ): ts.PropertySignature | ts.IndexSignatureDeclaration { const modifiers = undefined; const questionToken = required == null || !required.includes(propertyName) ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined; if (isPattern) { return ts.factory.createIndexSignature( modifiers, [ ts.factory.createParameterDeclaration( [], undefined, ts.factory.createIdentifier('pattern'), undefined, ts.factory.createTypeReferenceNode('string', []), undefined, ), ], valueType, ); } return ts.factory.createPropertySignature( modifiers, buildPropertyNameIdentifier(propertyName), questionToken, valueType, ); } export function buildIndexSignatureNode( name: string, indexType: ts.TypeNode, valueType: ts.TypeNode, ): ts.IndexSignatureDeclaration { return ts.factory.createIndexSignature( undefined, [ ts.factory.createParameterDeclaration( undefined, undefined, buildTypeNameIdentifier(name), undefined, indexType, undefined, ), ], valueType, ); } export function buildTypeLiteralNode(elements: ts.TypeElement[]): ts.TypeNode { return ts.factory.createTypeLiteralNode(elements); } export function buildFreeFormObjectTypeLiteralNode(): ts.TypeNode { return ts.factory.createTypeLiteralNode([ buildIndexSignatureNode('key', buildStringKeyword(), buildAnyKeyword()), ]); } export function buildEmptyObjectTypeLiteralNode(): ts.TypeNode { return ts.factory.createTypeLiteralNode([ buildIndexSignatureNode( 'key', buildStringKeyword(), buildNeverKeyword(), ), ]); } export function buildUnionTypeNode<T>( types: T[], builder: (t: T, index: number) => ts.TypeNode, terminate: boolean, ): ts.TypeNode { const node = ts.factory.createUnionTypeNode(types.map(builder)); if (terminate) { return node; } return ts.factory.createParenthesizedType(node); } export function buildIntersectionTypeNode( types: readonly ts.TypeNode[], terminate: boolean, ): ts.TypeNode { const node = ts.factory.createIntersectionTypeNode(types); if (terminate) { return node; } return ts.factory.createParenthesizedType(node); } export function buildTupleTypeNode( types: ts.TypeNode | ts.TypeNode[], minItems?: number, maxItems?: number, additionalItems?: ts.TypeNode | false, ): ts.TypeNode { function typesIsArray<T>(types: T | T[]): types is T[] { return Array.isArray(types); } const typesLength = typesIsArray(types) ? types.length : 1; const nodes: ts.TypeNode[] = []; if (additionalItems === false && (minItems ?? 0) > typesLength) { return buildNeverKeyword(); } const itemCount = maxItems != null ? additionalItems === false ? typesLength : maxItems : Math.max(typesLength, minItems ?? 0); for (let i = 0; i < itemCount; i++) { let node = typesIsArray(types) ? i < types.length ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion types[i]! : additionalItems !== undefined ? additionalItems === false ? buildNeverKeyword() : additionalItems : buildAnyKeyword() : types; if (minItems == null || i >= minItems) { node = ts.factory.createOptionalTypeNode(node); } nodes.push(node); } if ( maxItems == null && (!typesIsArray(types) || additionalItems !== false) ) { nodes.push( ts.factory.createRestTypeNode( ts.factory.createArrayTypeNode( typesIsArray(types) ? additionalItems !== undefined ? (additionalItems as ts.TypeNode) : buildAnyKeyword() : types, ), ), ); } return ts.factory.createTupleTypeNode(nodes); } export function buildTypeReferenceNode( schema: NormalizedSchema, currentSchema: Schema, ): ts.TypeReferenceNode { const typeName = getTypename(schema.id, currentSchema); let node: ts.EntityName | undefined; for (const name of typeName) { if (node == null) { node = buildTypeNameIdentifier(name); } else { node = ts.factory.createQualifiedName( node, buildTypeNameIdentifier(name), ); } } if (node == null) { throw new Error('TypeName array must not be empty.'); } return ts.factory.createTypeReferenceNode(node, undefined); } function getTypename(id: SchemaId, baseSchema: Schema): string[] { const result = id.toNames(); const baseId = baseSchema.id; const baseTypes = baseId.toNames().slice(0, -1); for (const type of baseTypes) { if (result.length === 1) { break; } if (result[0] === type) { result.shift(); } else { break; } } return result; } export function addComment<T extends ts.Node>( node: T, schema: NormalizedSchema, terminate: boolean, ): T { const comments = getComment(schema); if (comments.length === 0) { return node; } else if (!terminate && comments.length === 1) { return ts.addSyntheticLeadingComment( node, ts.SyntaxKind.MultiLineCommentTrivia, ` ${comments[0] ?? ''} `, false, ); } else { let result = '*\n'; for (const comment of comments) { result += ' * ' + comment + '\n'; } result += ' '; return ts.addSyntheticLeadingComment( node, ts.SyntaxKind.MultiLineCommentTrivia, result, true, ); } } function getComment(schema: NormalizedSchema): string[] { const content = schema.content; let comments: string[] = []; function protectComment(str: string): string { return str.replace(/\*\//g, '*\u200B/'); // Unicode [ZERO WIDTH SPACE] } function appendComment(value?: any): void { if (value == null) { return; } const s = typeof value === 'string' ? value : JSON.stringify(value, null, 2); const lines = s.split('\n').map((line: string) => protectComment(line)); comments = comments.concat(...lines); } if ('$comment' in content) { appendComment(content.$comment); } appendComment(content.title); appendComment(content.description); if ('example' in content || 'examples' in content) { appendComment('example:'); if ('example' in content) { appendComment(content.example); } if ('examples' in content) { if (content.examples) { for (const e of content.examples) { appendComment(e); } } } } return comments; } export function addOptionalInformation<T extends ts.Node>( node: T, schema: NormalizedSchema, terminate: boolean, ): T { const format = schema.content.format; const pattern = schema.content.pattern; if (!format && !pattern) { return node; } let comment = ''; if (format) { comment += ' ' + format; } if (pattern) { comment += ' ' + pattern; } if (!terminate) { comment += ' '; } const kind = terminate ? ts.SyntaxKind.SingleLineCommentTrivia : ts.SyntaxKind.MultiLineCommentTrivia; return ts.addSyntheticTrailingComment(node, kind, comment, false); } function getLastTypeName(id: SchemaId): string { const names = id.toNames(); return names[names.length - 1] ?? ''; }