UNPKG

73.1 kBPlain TextView Raw
1import * as glob from "glob";
2import { stringify } from "safe-stable-stringify";
3import * as path from "path";
4import { createHash } from "crypto";
5import * as ts from "typescript";
6import { JSONSchema7, JSONSchema7TypeName } from "json-schema";
7import { pathEqual } from "path-equal";
8export { Program, CompilerOptions, Symbol } from "typescript";
9
10const vm = require("vm");
11
12const REGEX_FILE_NAME_OR_SPACE = /(\bimport\(".*?"\)|".*?")\.| /g;
13const REGEX_TSCONFIG_NAME = /^.*\.json$/;
14const REGEX_TJS_JSDOC = /^-([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g;
15const REGEX_GROUP_JSDOC = /^[.]?([\w]+)\s+(\S|\S[\s\S]*\S)\s*$/g;
16/**
17 * Resolve required file, his path and a property name,
18 * pattern: require([file_path]).[property_name]
19 *
20 * the part ".[property_name]" is optional in the regex
21 *
22 * will match:
23 *
24 * require('./path.ts')
25 * require('./path.ts').objectName
26 * require("./path.ts")
27 * require("./path.ts").objectName
28 * require('@module-name')
29 *
30 * match[2] = file_path (a path to the file with quotes)
31 * match[3] = (optional) property_name (a property name, exported in the file)
32 *
33 * for more details, see tests/require.test.ts
34 */
35const REGEX_REQUIRE = /^(\s+)?require\((\'@?[a-zA-Z0-9.\/_-]+\'|\"@?[a-zA-Z0-9.\/_-]+\")\)(\.([a-zA-Z0-9_$]+))?(\s+|$)/;
36const NUMERIC_INDEX_PATTERN = "^[0-9]+$";
37
38export function getDefaultArgs(): Args {
39 return {
40 ref: true,
41 aliasRef: false,
42 topRef: false,
43 titles: false,
44 defaultProps: false,
45 noExtraProps: false,
46 propOrder: false,
47 typeOfKeyword: false,
48 required: false,
49 strictNullChecks: false,
50 esModuleInterop: false,
51 skipLibCheck: false,
52 experimentalDecorators: true,
53 ignoreErrors: false,
54 out: "",
55 validationKeywords: [],
56 include: [],
57 excludePrivate: false,
58 uniqueNames: false,
59 rejectDateType: false,
60 id: "",
61 defaultNumberType: "number",
62 tsNodeRegister: false,
63 constAsEnum: false,
64 };
65}
66
67export type ValidationKeywords = {
68 [prop: string]: boolean;
69};
70
71export type Args = {
72 ref: boolean;
73 aliasRef: boolean;
74 topRef: boolean;
75 titles: boolean;
76 defaultProps: boolean;
77 noExtraProps: boolean;
78 propOrder: boolean;
79 typeOfKeyword: boolean;
80 required: boolean;
81 strictNullChecks: boolean;
82 esModuleInterop: boolean;
83 skipLibCheck: boolean;
84 ignoreErrors: boolean;
85 experimentalDecorators: boolean;
86 out: string;
87 validationKeywords: string[];
88 include: string[];
89 excludePrivate: boolean;
90 uniqueNames: boolean;
91 rejectDateType: boolean;
92 id: string;
93 defaultNumberType: "number" | "integer";
94 tsNodeRegister: boolean;
95 constAsEnum: boolean;
96};
97
98export type PartialArgs = Partial<Args>;
99
100export type PrimitiveType = number | boolean | string | null;
101
102type MetaDefinitionFields = "ignore";
103type RedefinedFields =
104 | "items"
105 | "additionalItems"
106 | "contains"
107 | "properties"
108 | "patternProperties"
109 | "additionalProperties"
110 | "dependencies"
111 | "propertyNames"
112 | "if"
113 | "then"
114 | "else"
115 | "allOf"
116 | "anyOf"
117 | "oneOf"
118 | "not"
119 | "definitions";
120export type DefinitionOrBoolean = Definition | boolean;
121export interface Definition extends Omit<JSONSchema7, RedefinedFields> {
122 // Non-standard fields
123 propertyOrder?: string[];
124 defaultProperties?: string[];
125 typeof?: "function";
126
127 // Fields that must be redefined because they make use of this definition itself
128 items?: DefinitionOrBoolean | DefinitionOrBoolean[];
129 additionalItems?: DefinitionOrBoolean;
130 contains?: JSONSchema7;
131 properties?: {
132 [key: string]: DefinitionOrBoolean;
133 };
134 patternProperties?: {
135 [key: string]: DefinitionOrBoolean;
136 };
137 additionalProperties?: DefinitionOrBoolean;
138 dependencies?: {
139 [key: string]: DefinitionOrBoolean | string[];
140 };
141 propertyNames?: DefinitionOrBoolean;
142 if?: DefinitionOrBoolean;
143 then?: DefinitionOrBoolean;
144 else?: DefinitionOrBoolean;
145 allOf?: DefinitionOrBoolean[];
146 anyOf?: DefinitionOrBoolean[];
147 oneOf?: DefinitionOrBoolean[];
148 not?: DefinitionOrBoolean;
149 definitions?: {
150 [key: string]: DefinitionOrBoolean;
151 };
152}
153
154/** A looser Definition type that allows for indexing with arbitrary strings. */
155type DefinitionIndex = { [key: string]: Definition[keyof Definition] };
156
157export type SymbolRef = {
158 name: string;
159 typeName: string;
160 fullyQualifiedName: string;
161 symbol: ts.Symbol;
162};
163
164function extend(target: any, ..._: any[]): any {
165 if (target == null) {
166 // TypeError if undefined or null
167 throw new TypeError("Cannot convert undefined or null to object");
168 }
169
170 const to = Object(target);
171
172 for (var index = 1; index < arguments.length; index++) {
173 const nextSource = arguments[index];
174
175 if (nextSource != null) {
176 // Skip over if undefined or null
177 for (const nextKey in nextSource) {
178 // Avoid bugs when hasOwnProperty is shadowed
179 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
180 to[nextKey] = nextSource[nextKey];
181 }
182 }
183 }
184 }
185 return to;
186}
187
188function unique(arr: string[]): string[] {
189 const temp: Record<string, true> = {};
190 for (const e of arr) {
191 temp[e] = true;
192 }
193 const r: string[] = [];
194 for (const k in temp) {
195 // Avoid bugs when hasOwnProperty is shadowed
196 if (Object.prototype.hasOwnProperty.call(temp, k)) {
197 r.push(k);
198 }
199 }
200 return r;
201}
202
203/**
204 * Resolve required file
205 */
206function resolveRequiredFile(symbol: ts.Symbol, key: string, fileName: string, objectName: string): any {
207 const sourceFile = getSourceFile(symbol);
208 const requiredFilePath = /^[.\/]+/.test(fileName)
209 ? fileName === "."
210 ? path.resolve(sourceFile.fileName)
211 : path.resolve(path.dirname(sourceFile.fileName), fileName)
212 : fileName;
213 const requiredFile = require(requiredFilePath);
214 if (!requiredFile) {
215 throw Error("Required: File couldn't be loaded");
216 }
217 const requiredObject = objectName ? requiredFile[objectName] : requiredFile.default;
218 if (requiredObject === undefined) {
219 throw Error("Required: Variable is undefined");
220 }
221 if (typeof requiredObject === "function") {
222 throw Error("Required: Can't use function as a variable");
223 }
224 if (key === "examples" && !Array.isArray(requiredObject)) {
225 throw Error("Required: Variable isn't an array");
226 }
227 return requiredObject;
228}
229
230export function regexRequire(value: string) {
231 return REGEX_REQUIRE.exec(value);
232}
233
234/**
235 * Try to parse a value and returns the string if it fails.
236 */
237function parseValue(symbol: ts.Symbol, key: string, value: string): any {
238 const match = regexRequire(value);
239 if (match) {
240 const fileName = match[2].substr(1, match[2].length - 2).trim();
241 const objectName = match[4];
242 return resolveRequiredFile(symbol, key, fileName, objectName);
243 }
244 try {
245 return JSON.parse(value);
246 } catch (error) {
247 return value;
248 }
249}
250
251function extractLiteralValue(typ: ts.Type): PrimitiveType | undefined {
252 let str = (<ts.LiteralType>typ).value;
253 if (str === undefined) {
254 str = (typ as any).text;
255 }
256 if (typ.flags & ts.TypeFlags.StringLiteral) {
257 return str as string;
258 } else if (typ.flags & ts.TypeFlags.BooleanLiteral) {
259 return (typ as any).intrinsicName === "true";
260 } else if (typ.flags & ts.TypeFlags.EnumLiteral) {
261 // or .text for old TS
262 const num = parseFloat(str as string);
263 return isNaN(num) ? (str as string) : num;
264 } else if (typ.flags & ts.TypeFlags.NumberLiteral) {
265 return parseFloat(str as string);
266 }
267 return undefined;
268}
269
270/**
271 * Checks whether a type is a tuple type.
272 */
273function resolveTupleType(propertyType: ts.Type): ts.TupleTypeNode | null {
274 if (
275 !propertyType.getSymbol() &&
276 propertyType.getFlags() & ts.TypeFlags.Object &&
277 (<ts.ObjectType>propertyType).objectFlags & ts.ObjectFlags.Reference
278 ) {
279 return (propertyType as ts.TypeReference).target as any;
280 }
281 if (
282 !(
283 propertyType.getFlags() & ts.TypeFlags.Object &&
284 (<ts.ObjectType>propertyType).objectFlags & ts.ObjectFlags.Tuple
285 )
286 ) {
287 return null;
288 }
289 return propertyType as any;
290}
291
292const simpleTypesAllowedProperties: Record<string, true> = {
293 type: true,
294 description: true,
295};
296
297function addSimpleType(def: Definition, type: JSONSchema7TypeName): boolean {
298 for (const k in def) {
299 if (!simpleTypesAllowedProperties[k]) {
300 return false;
301 }
302 }
303
304 if (!def.type) {
305 def.type = type;
306 } else if (typeof def.type !== "string") {
307 if (
308 !(<Object[]>def.type).every((val) => {
309 return typeof val === "string";
310 })
311 ) {
312 return false;
313 }
314
315 if (def.type.indexOf("null") === -1) {
316 def.type.push("null");
317 }
318 } else {
319 if (typeof def.type !== "string") {
320 return false;
321 }
322
323 if (def.type !== "null") {
324 def.type = [def.type, "null"];
325 }
326 }
327 return true;
328}
329
330function makeNullable(def: Definition): Definition {
331 if (!addSimpleType(def, "null")) {
332 const union = def.oneOf || def.anyOf;
333 if (union) {
334 union.push({ type: "null" });
335 } else {
336 const subdef: DefinitionIndex = {};
337 for (var k in def as any) {
338 if (def.hasOwnProperty(k)) {
339 subdef[k] = def[k as keyof Definition];
340 delete def[k as keyof typeof def];
341 }
342 }
343 def.anyOf = [subdef, { type: "null" }];
344 }
345 }
346 return def;
347}
348
349/**
350 * Given a Symbol, returns a canonical Definition. That can be either:
351 * 1) The Symbol's valueDeclaration parameter if defined, or
352 * 2) The sole entry in the Symbol's declarations array, provided that array has a length of 1.
353 *
354 * valueDeclaration is listed as a required parameter in the definition of a Symbol, but I've
355 * experienced crashes when it's undefined at runtime, which is the reason for this function's
356 * existence. Not sure if that's a compiler API bug or what.
357 */
358function getCanonicalDeclaration(sym: ts.Symbol): ts.Declaration {
359 if (sym.valueDeclaration !== undefined) {
360 return sym.valueDeclaration;
361 } else if (sym.declarations?.length === 1) {
362 return sym.declarations[0];
363 }
364
365 const declarationCount = sym.declarations?.length ?? 0;
366 throw new Error(`Symbol "${sym.name}" has no valueDeclaration and ${declarationCount} declarations.`);
367}
368
369/**
370 * Given a Symbol, finds the place it was declared and chases parent pointers until we find a
371 * node where SyntaxKind === SourceFile.
372 */
373function getSourceFile(sym: ts.Symbol): ts.SourceFile {
374 let currentDecl: ts.Node = getCanonicalDeclaration(sym);
375
376 while (currentDecl.kind !== ts.SyntaxKind.SourceFile) {
377 if (currentDecl.parent === undefined) {
378 throw new Error(`Unable to locate source file for declaration "${sym.name}".`);
379 }
380 currentDecl = currentDecl.parent;
381 }
382
383 return currentDecl as ts.SourceFile;
384}
385
386/**
387 * JSDoc keywords that should be used to annotate the JSON schema.
388 *
389 * Many of these validation keywords are defined here: http://json-schema.org/latest/json-schema-validation.html
390 */
391// prettier-ignore
392const validationKeywords = {
393 multipleOf: true, // 6.1.
394 maximum: true, // 6.2.
395 exclusiveMaximum: true, // 6.3.
396 minimum: true, // 6.4.
397 exclusiveMinimum: true, // 6.5.
398 maxLength: true, // 6.6.
399 minLength: true, // 6.7.
400 pattern: true, // 6.8.
401 items: true, // 6.9.
402 // additionalItems: true, // 6.10.
403 maxItems: true, // 6.11.
404 minItems: true, // 6.12.
405 uniqueItems: true, // 6.13.
406 contains: true, // 6.14.
407 maxProperties: true, // 6.15.
408 minProperties: true, // 6.16.
409 // required: true, // 6.17. This is not required. It is auto-generated.
410 // properties: true, // 6.18. This is not required. It is auto-generated.
411 // patternProperties: true, // 6.19.
412 additionalProperties: true, // 6.20.
413 // dependencies: true, // 6.21.
414 // propertyNames: true, // 6.22.
415 enum: true, // 6.23.
416 // const: true, // 6.24.
417 type: true, // 6.25.
418 // allOf: true, // 6.26.
419 // anyOf: true, // 6.27.
420 // oneOf: true, // 6.28.
421 // not: true, // 6.29.
422 examples: true, // Draft 6 (draft-handrews-json-schema-validation-01)
423
424 ignore: true,
425 description: true,
426 format: true,
427 default: true,
428 $ref: true,
429 id: true,
430 $id: true,
431 $comment: true,
432 title: true
433};
434
435/**
436 * Subset of descriptive, non-type keywords that are permitted alongside a $ref.
437 * Prior to JSON Schema draft 2019-09, $ref is a special keyword that doesn't
438 * permit keywords alongside it, and so AJV may raise warnings if it encounters
439 * any type-related keywords; see https://github.com/ajv-validator/ajv/issues/1121
440 */
441const annotationKeywords: { [k in keyof typeof validationKeywords]?: true } = {
442 description: true,
443 default: true,
444 examples: true,
445 title: true,
446 // A JSDoc $ref annotation can appear as a $ref.
447 $ref: true,
448};
449
450const subDefinitions: Record<string, true> = {
451 items: true,
452 additionalProperties: true,
453 contains: true,
454};
455
456export class JsonSchemaGenerator {
457 private tc: ts.TypeChecker;
458
459 /**
460 * Holds all symbols within a custom SymbolRef object, containing useful
461 * information.
462 */
463 private symbols: SymbolRef[];
464 /**
465 * All types for declarations of classes, interfaces, enums, and type aliases
466 * defined in all TS files.
467 */
468 private allSymbols: { [name: string]: ts.Type };
469 /**
470 * All symbols for declarations of classes, interfaces, enums, and type aliases
471 * defined in non-default-lib TS files.
472 */
473 private userSymbols: { [name: string]: ts.Symbol };
474 /**
475 * Maps from the names of base types to the names of the types that inherit from
476 * them.
477 */
478 private inheritingTypes: { [baseName: string]: string[] };
479
480 /**
481 * This map holds references to all reffed definitions, including schema
482 * overrides and generated definitions.
483 */
484 private reffedDefinitions: { [key: string]: Definition } = {};
485
486 /**
487 * This map only holds explicit schema overrides. This helps differentiate between
488 * user defined schema overrides and generated definitions.
489 */
490 private schemaOverrides = new Map<string, Definition>();
491
492 /**
493 * This is a set of all the user-defined validation keywords.
494 */
495 private userValidationKeywords: ValidationKeywords;
496
497 /**
498 * If true, this makes constants be defined as enums with a single value. This is useful
499 * for cases where constant values are not supported, such as OpenAPI.
500 */
501 private constAsEnum: boolean;
502
503 /**
504 * Types are assigned names which are looked up by their IDs. This is the
505 * map from type IDs to type names.
506 */
507 private typeNamesById: { [id: number]: string } = {};
508 /**
509 * Whenever a type is assigned its name, its entry in this dictionary is set,
510 * so that we don't give the same name to two separate types.
511 */
512 private typeIdsByName: { [name: string]: number } = {};
513
514 constructor(
515 symbols: SymbolRef[],
516 allSymbols: { [name: string]: ts.Type },
517 userSymbols: { [name: string]: ts.Symbol },
518 inheritingTypes: { [baseName: string]: string[] },
519 tc: ts.TypeChecker,
520 private args = getDefaultArgs()
521 ) {
522 this.symbols = symbols;
523 this.allSymbols = allSymbols;
524 this.userSymbols = userSymbols;
525 this.inheritingTypes = inheritingTypes;
526 this.tc = tc;
527 this.userValidationKeywords = args.validationKeywords.reduce((acc, word) => ({ ...acc, [word]: true }), {});
528 this.constAsEnum = args.constAsEnum;
529 }
530
531 public get ReffedDefinitions(): { [key: string]: Definition } {
532 return this.reffedDefinitions;
533 }
534
535 private isFromDefaultLib(symbol: ts.Symbol) {
536 const declarations = symbol.getDeclarations();
537 if (declarations && declarations.length > 0 && declarations[0].parent) {
538 return declarations[0].parent.getSourceFile().hasNoDefaultLib;
539 }
540 return false;
541 }
542
543 private resetSchemaSpecificProperties(includeAllOverrides: boolean = false) {
544 this.reffedDefinitions = {};
545 this.typeIdsByName = {};
546 this.typeNamesById = {};
547
548 // restore schema overrides
549 if (includeAllOverrides) {
550 this.schemaOverrides.forEach((value, key) => {
551 this.reffedDefinitions[key] = value;
552 });
553 }
554 }
555
556 /**
557 * Parse the comments of a symbol into the definition and other annotations.
558 */
559 private parseCommentsIntoDefinition(symbol: ts.Symbol, definition: Definition, otherAnnotations: Record<string, true>): void {
560 if (!symbol) {
561 return;
562 }
563
564 if (!this.isFromDefaultLib(symbol)) {
565 // the comments for a symbol
566 const comments = symbol.getDocumentationComment(this.tc);
567
568 if (comments.length) {
569 definition.description = comments
570 .map((comment) => {
571 const newlineNormalizedComment = comment.text.replace(/\r\n/g, "\n");
572
573 // If a comment contains a "{@link XYZ}" inline tag that could not be
574 // resolved by the TS checker, then this comment will contain a trailing
575 // whitespace that we need to remove.
576 if (comment.kind === "linkText") {
577 return newlineNormalizedComment.trim();
578 }
579
580 return newlineNormalizedComment;
581 })
582 .join("").trim();
583 }
584 }
585
586 // jsdocs are separate from comments
587 const jsdocs = symbol.getJsDocTags();
588 jsdocs.forEach((doc) => {
589 // if we have @TJS-... annotations, we have to parse them
590 let name = doc.name;
591 const originalText = doc.text ? doc.text.map((t) => t.text).join("") : "";
592 let text = originalText;
593 // In TypeScript versions prior to 3.7, it stops parsing the annotation
594 // at the first non-alphanumeric character and puts the rest of the line as the
595 // "text" of the annotation, so we have a little hack to check for the name
596 // "TJS" and then we sort of re-parse the annotation to support prior versions
597 // of TypeScript.
598 if (name.startsWith("TJS-")) {
599 name = name.slice(4);
600 if (!text) {
601 text = "true";
602 }
603 } else if (name === "TJS" && text.startsWith("-")) {
604 let match: string[] | RegExpExecArray | null = new RegExp(REGEX_TJS_JSDOC).exec(originalText);
605 if (match) {
606 name = match[1];
607 text = match[2];
608 } else {
609 // Treat empty text as boolean true
610 name = (text as string).replace(/^[\s\-]+/, "");
611 text = "true";
612 }
613 }
614
615 // In TypeScript ~3.5, the annotation name splits at the dot character so we have
616 // to process the "." and beyond from the value
617 if (subDefinitions[name]) {
618 const match: string[] | RegExpExecArray | null = new RegExp(REGEX_GROUP_JSDOC).exec(text);
619 if (match) {
620 const k = match[1];
621 const v = match[2];
622 (definition as DefinitionIndex)[name] = { ...(definition as Record<string, Record<string, unknown>>)[name], [k]: v ? parseValue(symbol, k, v) : true };
623 return;
624 }
625 }
626
627 // In TypeScript 3.7+, the "." is kept as part of the annotation name
628 if (name.includes(".")) {
629 const parts = name.split(".");
630 const key = parts[0] as keyof Definition;
631 if (parts.length === 2 && subDefinitions[key]) {
632 (definition as DefinitionIndex)[key] = {
633 ...definition[key] as Record<string, unknown>,
634 [parts[1]]: text ? parseValue(symbol, name, text) : true,
635 };
636 }
637 }
638
639 if (validationKeywords[name as keyof typeof validationKeywords] || this.userValidationKeywords[name]) {
640 (definition as DefinitionIndex)[name] = text === undefined ? "" : parseValue(symbol, name, text);
641 } else {
642 // special annotations
643 otherAnnotations[doc.name] = true;
644 }
645 });
646 }
647
648 private getDefinitionForRootType(
649 propertyType: ts.Type,
650 reffedType: ts.Symbol,
651 definition: Definition,
652 defaultNumberType = this.args.defaultNumberType,
653 ignoreUndefined = false,
654 ): Definition {
655 const tupleType = resolveTupleType(propertyType);
656
657 if (tupleType) {
658 // tuple
659 const elemTypes: ts.NodeArray<ts.TypeNode> = (propertyType as any).typeArguments;
660 const fixedTypes = elemTypes.map((elType) => this.getTypeDefinition(elType as any));
661 definition.type = "array";
662 if (fixedTypes.length > 0) {
663 definition.items = fixedTypes;
664 }
665 const targetTupleType = (propertyType as ts.TupleTypeReference).target;
666 definition.minItems = targetTupleType.minLength;
667 if (targetTupleType.hasRestElement) {
668 definition.additionalItems = fixedTypes[fixedTypes.length - 1];
669 fixedTypes.splice(fixedTypes.length - 1, 1);
670 } else {
671 definition.maxItems = targetTupleType.fixedLength;
672 }
673 } else {
674 const propertyTypeString = this.tc.typeToString(
675 propertyType,
676 undefined,
677 ts.TypeFormatFlags.UseFullyQualifiedType
678 );
679 const flags = propertyType.flags;
680 const arrayType = this.tc.getIndexTypeOfType(propertyType, ts.IndexKind.Number);
681
682 if (flags & ts.TypeFlags.String) {
683 definition.type = "string";
684 } else if (flags & ts.TypeFlags.Number) {
685 const isInteger =
686 definition.type === "integer" ||
687 reffedType?.getName() === "integer" ||
688 defaultNumberType === "integer";
689 definition.type = isInteger ? "integer" : "number";
690 } else if (flags & ts.TypeFlags.Boolean) {
691 definition.type = "boolean";
692 } else if (flags & ts.TypeFlags.ESSymbol) {
693 definition.type = "object";
694 } else if (flags & ts.TypeFlags.Null) {
695 definition.type = "null";
696 } else if (flags & ts.TypeFlags.Undefined || propertyTypeString === "void") {
697 if (!ignoreUndefined) {
698 throw new Error("Not supported: root type undefined");
699 }
700 // will be deleted
701 definition.type = "undefined" as any;
702 } else if (flags & ts.TypeFlags.Any || flags & ts.TypeFlags.Unknown) {
703 // no type restriction, so that anything will match
704 } else if (propertyTypeString === "Date" && !this.args.rejectDateType) {
705 definition.type = "string";
706 definition.format = definition.format || "date-time";
707 } else if (propertyTypeString === "object") {
708 definition.type = "object";
709 definition.properties = {};
710 definition.additionalProperties = true;
711 } else if (propertyTypeString === "bigint") {
712 definition.type = "number";
713 definition.properties = {};
714 definition.additionalProperties = false;
715 } else {
716 const value = extractLiteralValue(propertyType);
717 if (value !== undefined) {
718 // typeof value can be: "string", "boolean", "number", or "object" if value is null
719 const typeofValue = typeof value;
720 switch (typeofValue) {
721 case "string":
722 case "boolean":
723 definition.type = typeofValue;
724 break;
725 case "number":
726 definition.type = this.args.defaultNumberType;
727 break;
728 case "object":
729 definition.type = "null";
730 break;
731 default:
732 throw new Error(`Not supported: ${value} as a enum value`);
733 }
734 if (this.constAsEnum) {
735 definition.enum = [value];
736 } else {
737 definition.const = value;
738 }
739 } else if (arrayType !== undefined) {
740 if (
741 propertyType.flags & ts.TypeFlags.Object &&
742 (propertyType as ts.ObjectType).objectFlags &
743 (ts.ObjectFlags.Anonymous | ts.ObjectFlags.Interface | ts.ObjectFlags.Mapped)
744 ) {
745 definition.type = "object";
746 definition.additionalProperties = false;
747 definition.patternProperties = {
748 [NUMERIC_INDEX_PATTERN]: this.getTypeDefinition(arrayType),
749 };
750 if (!!Array.from((<any>propertyType).members)?.find((member: [string]) => member[0] !== "__index")) {
751 this.getClassDefinition(propertyType, definition);
752 }
753 } else if (propertyType.flags & ts.TypeFlags.TemplateLiteral) {
754 definition.type = "string";
755 // @ts-ignore
756 const {texts, types} = propertyType;
757 const pattern = [];
758 for (let i = 0; i < texts.length; i++) {
759 const text = texts[i].replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
760 const type = types[i];
761
762 if (i === 0) {
763 pattern.push(`^`);
764 }
765
766 if (type) {
767 if (type.flags & ts.TypeFlags.String) {
768 pattern.push(`${text}.*`);
769 }
770
771 if (type.flags & ts.TypeFlags.Number
772 || type.flags & ts.TypeFlags.BigInt) {
773 pattern.push(`${text}[0-9]*`);
774 }
775
776 if (type.flags & ts.TypeFlags.Undefined) {
777 pattern.push(`${text}undefined`);
778 }
779
780 if (type.flags & ts.TypeFlags.Null) {
781 pattern.push(`${text}null`);
782 }
783 }
784
785
786 if (i === texts.length - 1) {
787 pattern.push(`${text}$`);
788 }
789 }
790 definition.pattern = pattern.join("");
791 } else {
792 definition.type = "array";
793 if (!definition.items) {
794 definition.items = this.getTypeDefinition(arrayType);
795 }
796 }
797 } else {
798 // Report that type could not be processed
799 const error = new TypeError("Unsupported type: " + propertyTypeString);
800 (error as any).type = propertyType;
801 throw error;
802 // definition = this.getTypeDefinition(propertyType, tc);
803 }
804 }
805 }
806
807 return definition;
808 }
809
810 private getReferencedTypeSymbol(prop: ts.Symbol): ts.Symbol | undefined {
811 const decl = prop.getDeclarations();
812 if (decl?.length) {
813 const type = <ts.TypeReferenceNode>(<any>decl[0]).type;
814 if (type && type.kind & ts.SyntaxKind.TypeReference && type.typeName) {
815 const symbol = this.tc.getSymbolAtLocation(type.typeName);
816 if (symbol && symbol.flags & ts.SymbolFlags.Alias) {
817 return this.tc.getAliasedSymbol(symbol);
818 }
819 return symbol;
820 }
821 }
822 return undefined;
823 }
824
825 private getDefinitionForProperty(prop: ts.Symbol, node: ts.Node): Definition | null {
826 if (prop.flags & ts.SymbolFlags.Method) {
827 return null;
828 }
829 const propertyName = prop.getName();
830 const propertyType = this.tc.getTypeOfSymbolAtLocation(prop, node);
831
832 const reffedType = this.getReferencedTypeSymbol(prop);
833
834 const definition = this.getTypeDefinition(propertyType, undefined, undefined, prop, reffedType);
835
836 if (this.args.titles) {
837 definition.title = propertyName;
838 }
839
840 if (definition.hasOwnProperty("ignore")) {
841 return null;
842 }
843
844 // try to get default value
845 const valDecl = prop.valueDeclaration as ts.VariableDeclaration;
846 if (valDecl?.initializer) {
847 let initial = valDecl.initializer;
848
849 while (ts.isTypeAssertionExpression(initial)) {
850 initial = initial.expression;
851 }
852
853 if ((<any>initial).expression) {
854 // node
855 console.warn("initializer is expression for property " + propertyName);
856 } else if ((<any>initial).kind && (<any>initial).kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
857 definition.default = initial.getText();
858 } else {
859 try {
860 const sandbox = { sandboxvar: null as any };
861 vm.runInNewContext("sandboxvar=" + initial.getText(), sandbox);
862
863 const val = sandbox.sandboxvar;
864 if (
865 val === null ||
866 typeof val === "string" ||
867 typeof val === "number" ||
868 typeof val === "boolean" ||
869 Object.prototype.toString.call(val) === "[object Array]"
870 ) {
871 definition.default = val;
872 } else if (val) {
873 console.warn("unknown initializer for property " + propertyName + ": " + val);
874 }
875 } catch (e) {
876 console.warn("exception evaluating initializer for property " + propertyName);
877 }
878 }
879 }
880
881 return definition;
882 }
883
884 private getEnumDefinition(clazzType: ts.Type, definition: Definition): Definition {
885 const node = clazzType.getSymbol()!.getDeclarations()![0];
886 const fullName = this.tc.typeToString(clazzType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
887 const members: ts.NodeArray<ts.EnumMember> =
888 node.kind === ts.SyntaxKind.EnumDeclaration
889 ? (node as ts.EnumDeclaration).members
890 : ts.factory.createNodeArray([node as ts.EnumMember]);
891 var enumValues: (number | boolean | string | null)[] = [];
892 const enumTypes: JSONSchema7TypeName[] = [];
893
894 const addType = (type: JSONSchema7TypeName) => {
895 if (enumTypes.indexOf(type) === -1) {
896 enumTypes.push(type);
897 }
898 };
899
900 members.forEach((member) => {
901 const caseLabel = (<ts.Identifier>member.name).text;
902 const constantValue = this.tc.getConstantValue(member);
903 if (constantValue !== undefined) {
904 enumValues.push(constantValue);
905 addType(typeof constantValue as JSONSchema7TypeName); // can be only string or number;
906 } else {
907 // try to extract the enums value; it will probably by a cast expression
908 const initial: ts.Expression | undefined = member.initializer;
909 if (initial) {
910 if ((<any>initial).expression) {
911 // node
912 const exp = (<any>initial).expression;
913 const text = (<any>exp).text;
914 // if it is an expression with a text literal, chances are it is the enum convention:
915 // CASELABEL = 'literal' as any
916 if (text) {
917 enumValues.push(text);
918 addType("string");
919 } else if (exp.kind === ts.SyntaxKind.TrueKeyword || exp.kind === ts.SyntaxKind.FalseKeyword) {
920 enumValues.push(exp.kind === ts.SyntaxKind.TrueKeyword);
921 addType("boolean");
922 } else {
923 console.warn("initializer is expression for enum: " + fullName + "." + caseLabel);
924 }
925 } else if (initial.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
926 enumValues.push(initial.getText());
927 addType("string");
928 } else if (initial.kind === ts.SyntaxKind.NullKeyword) {
929 enumValues.push(null);
930 addType("null");
931 }
932 }
933 }
934 });
935
936 if (enumTypes.length) {
937 definition.type = enumTypes.length === 1 ? enumTypes[0] : enumTypes;
938 }
939
940 if (enumValues.length > 0) {
941 if (enumValues.length > 1) {
942 definition.enum = enumValues;
943 } else {
944 definition.const = enumValues[0];
945 }
946 }
947
948 return definition;
949 }
950
951 private getUnionDefinition(
952 unionType: ts.UnionType,
953 unionModifier: keyof Definition,
954 definition: Definition
955 ): Definition {
956 const enumValues: PrimitiveType[] = [];
957 const simpleTypes: JSONSchema7TypeName[] = [];
958 const schemas: Definition[] = [];
959
960 const pushSimpleType = (type: JSONSchema7TypeName) => {
961 if (simpleTypes.indexOf(type) === -1) {
962 simpleTypes.push(type);
963 }
964 };
965
966 const pushEnumValue = (val: PrimitiveType) => {
967 if (enumValues.indexOf(val) === -1) {
968 enumValues.push(val);
969 }
970 };
971
972 for (const valueType of unionType.types) {
973 const value = extractLiteralValue(valueType);
974 if (value !== undefined) {
975 pushEnumValue(value);
976 } else {
977 const symbol = valueType.aliasSymbol;
978 const def = this.getTypeDefinition(valueType, undefined, undefined, symbol, symbol, undefined, undefined, true);
979 if (def.type === "undefined" as any) {
980 continue;
981 }
982 const keys = Object.keys(def);
983 if (keys.length === 1 && keys[0] === "type") {
984 if (typeof def.type !== "string") {
985 console.error("Expected only a simple type.");
986 } else {
987 pushSimpleType(def.type);
988 }
989 } else {
990 schemas.push(def);
991 }
992 }
993 }
994
995 if (enumValues.length > 0) {
996 // If the values are true and false, just add "boolean" as simple type
997 const isOnlyBooleans =
998 enumValues.length === 2 &&
999 typeof enumValues[0] === "boolean" &&
1000 typeof enumValues[1] === "boolean" &&
1001 enumValues[0] !== enumValues[1];
1002
1003 if (isOnlyBooleans) {
1004 pushSimpleType("boolean");
1005 } else {
1006 const enumSchema: Definition = enumValues.length > 1 ? { enum: enumValues.sort() } : { const: enumValues[0] };
1007
1008 // If all values are of the same primitive type, add a "type" field to the schema
1009 if (
1010 enumValues.every((x) => {
1011 return typeof x === "string";
1012 })
1013 ) {
1014 enumSchema.type = "string";
1015 } else if (
1016 enumValues.every((x) => {
1017 return typeof x === "number";
1018 })
1019 ) {
1020 enumSchema.type = "number";
1021 } else if (
1022 enumValues.every((x) => {
1023 return typeof x === "boolean";
1024 })
1025 ) {
1026 enumSchema.type = "boolean";
1027 }
1028
1029 schemas.push(enumSchema);
1030 }
1031 }
1032
1033 if (simpleTypes.length > 0) {
1034 schemas.push({ type: simpleTypes.length === 1 ? simpleTypes[0] : simpleTypes });
1035 }
1036
1037 if (schemas.length === 1) {
1038 for (const k in schemas[0]) {
1039 if (schemas[0].hasOwnProperty(k)) {
1040 if (k === "description" && definition.hasOwnProperty(k)) {
1041 // If we already have a more specific description, don't overwrite it.
1042 continue;
1043 }
1044 (definition as DefinitionIndex)[k] = schemas[0][k as keyof Definition];
1045 }
1046 }
1047 } else {
1048 (definition as DefinitionIndex)[unionModifier] = schemas;
1049 }
1050 return definition;
1051 }
1052
1053 private getIntersectionDefinition(intersectionType: ts.IntersectionType, definition: Definition): Definition {
1054 const simpleTypes: JSONSchema7TypeName[] = [];
1055 const schemas: Definition[] = [];
1056
1057 const pushSimpleType = (type: JSONSchema7TypeName) => {
1058 if (simpleTypes.indexOf(type) === -1) {
1059 simpleTypes.push(type);
1060 }
1061 };
1062
1063 for (const intersectionMember of intersectionType.types) {
1064 const def = this.getTypeDefinition(intersectionMember);
1065 const keys = Object.keys(def);
1066 if (keys.length === 1 && keys[0] === "type") {
1067 if (typeof def.type !== "string") {
1068 console.error("Expected only a simple type.");
1069 } else {
1070 pushSimpleType(def.type);
1071 }
1072 } else {
1073 schemas.push(def);
1074 }
1075 }
1076
1077 if (simpleTypes.length > 0) {
1078 schemas.push({ type: simpleTypes.length === 1 ? simpleTypes[0] : simpleTypes });
1079 }
1080
1081 if (schemas.length === 1) {
1082 for (const k in schemas[0]) {
1083 if (schemas[0].hasOwnProperty(k)) {
1084 (definition as DefinitionIndex)[k] = schemas[0][k as keyof Definition];
1085 }
1086 }
1087 } else {
1088 definition.allOf = schemas;
1089 }
1090 return definition;
1091 }
1092
1093 private getClassDefinition(clazzType: ts.Type, definition: Definition): Definition {
1094 const node = clazzType.getSymbol()!.getDeclarations()![0];
1095
1096 // Example: typeof globalThis may not have any declaration
1097 if (!node) {
1098 definition.type = "object";
1099 return definition;
1100 }
1101
1102 if (this.args.typeOfKeyword && node.kind === ts.SyntaxKind.FunctionType) {
1103 definition.typeof = "function";
1104 return definition;
1105 }
1106
1107 const clazz = <ts.ClassDeclaration>node;
1108 const props = this.tc.getPropertiesOfType(clazzType).filter((prop) => {
1109 // filter never and undefined
1110 const propertyFlagType = this.tc.getTypeOfSymbolAtLocation(prop, node).getFlags();
1111 if (ts.TypeFlags.Never === propertyFlagType || ts.TypeFlags.Undefined === propertyFlagType) {
1112 return false;
1113 }
1114 if (!this.args.excludePrivate) {
1115 return true;
1116 }
1117
1118 const decls = prop.declarations;
1119 return !(
1120 decls &&
1121 decls.filter((decl) => {
1122 const mods = (decl as any).modifiers;
1123 return mods && mods.filter((mod: any) => mod.kind === ts.SyntaxKind.PrivateKeyword).length > 0;
1124 }).length > 0
1125 );
1126 });
1127 const fullName = this.tc.typeToString(clazzType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
1128
1129 const modifierFlags = ts.getCombinedModifierFlags(node);
1130
1131 if (modifierFlags & ts.ModifierFlags.Abstract && this.inheritingTypes[fullName]) {
1132 const oneOf = this.inheritingTypes[fullName].map((typename) => {
1133 return this.getTypeDefinition(this.allSymbols[typename]);
1134 });
1135
1136 definition.oneOf = oneOf;
1137 } else {
1138 if (clazz.members) {
1139 const indexSignatures =
1140 clazz.members == null ? [] : clazz.members.filter((x) => x.kind === ts.SyntaxKind.IndexSignature);
1141 if (indexSignatures.length === 1) {
1142 // for case "array-types"
1143 const indexSignature = indexSignatures[0] as ts.IndexSignatureDeclaration;
1144 if (indexSignature.parameters.length !== 1) {
1145 throw new Error("Not supported: IndexSignatureDeclaration parameters.length != 1");
1146 }
1147 const indexSymbol: ts.Symbol = (<any>indexSignature.parameters[0]).symbol;
1148 const indexType = this.tc.getTypeOfSymbolAtLocation(indexSymbol, node);
1149 const isIndexedObject = indexType.flags === ts.TypeFlags.String || indexType.flags === ts.TypeFlags.Number;
1150 if (indexType.flags !== ts.TypeFlags.Number && !isIndexedObject) {
1151 throw new Error(
1152 "Not supported: IndexSignatureDeclaration with index symbol other than a number or a string"
1153 );
1154 }
1155
1156 const typ = this.tc.getTypeAtLocation(indexSignature.type!);
1157 let def: Definition | undefined;
1158 if (typ.flags & ts.TypeFlags.IndexedAccess) {
1159 const targetName = ts.escapeLeadingUnderscores((<any>clazzType).mapper?.target?.value);
1160 const indexedAccessType = <ts.IndexedAccessType>typ;
1161 const symbols: Map<ts.__String, ts.Symbol> = (<any>indexedAccessType.objectType).members;
1162 const targetSymbol = symbols?.get(targetName);
1163
1164 if (targetSymbol) {
1165 const targetNode = targetSymbol.getDeclarations()![0];
1166 const targetDef = this.getDefinitionForProperty(targetSymbol, targetNode);
1167 if (targetDef) {
1168 def = targetDef;
1169 }
1170 }
1171 }
1172 if (!def) {
1173 def = this.getTypeDefinition(typ, undefined, "anyOf");
1174 }
1175 if (isIndexedObject) {
1176 definition.type = "object";
1177 if (!Object.keys(definition.patternProperties || {}).length) {
1178 definition.additionalProperties = def;
1179 }
1180 } else {
1181 definition.type = "array";
1182 if (!definition.items) {
1183 definition.items = def;
1184 }
1185 }
1186 }
1187 }
1188
1189 const propertyDefinitions = props.reduce<Record<string, Definition>>((all, prop) => {
1190 const propertyName = prop.getName();
1191 const propDef = this.getDefinitionForProperty(prop, node);
1192 if (propDef != null) {
1193 all[propertyName] = propDef;
1194 }
1195 return all;
1196 }, {});
1197
1198 if (definition.type === undefined) {
1199 definition.type = "object";
1200 }
1201
1202 if (definition.type === "object" && Object.keys(propertyDefinitions).length > 0) {
1203 definition.properties = propertyDefinitions;
1204 }
1205
1206 if (this.args.defaultProps) {
1207 definition.defaultProperties = [];
1208 }
1209 if (this.args.noExtraProps && definition.additionalProperties === undefined) {
1210 definition.additionalProperties = false;
1211 }
1212 if (this.args.propOrder) {
1213 // propertyOrder is non-standard, but useful:
1214 // https://github.com/json-schema/json-schema/issues/87
1215 const propertyOrder = props.reduce((order: string[], prop: ts.Symbol) => {
1216 order.push(prop.getName());
1217 return order;
1218 }, []);
1219
1220 definition.propertyOrder = propertyOrder;
1221 }
1222 if (this.args.required) {
1223 const requiredProps = props.reduce((required: string[], prop: ts.Symbol) => {
1224 const def = {};
1225 this.parseCommentsIntoDefinition(prop, def, {});
1226 const allUnionTypesFlags: number[] = (<any>prop).links?.type?.types?.map?.((t: any) => t.flags) || [];
1227 if (
1228 !(prop.flags & ts.SymbolFlags.Optional) &&
1229 !(prop.flags & ts.SymbolFlags.Method) &&
1230 !allUnionTypesFlags.includes(ts.TypeFlags.Undefined) &&
1231 !allUnionTypesFlags.includes(ts.TypeFlags.Void) &&
1232 !def.hasOwnProperty("ignore")
1233 ) {
1234 required.push(prop.getName());
1235 }
1236 return required;
1237 }, []);
1238
1239 if (requiredProps.length > 0) {
1240 definition.required = unique(requiredProps).sort();
1241 }
1242 }
1243 }
1244 return definition;
1245 }
1246
1247 /**
1248 * Gets/generates a globally unique type name for the given type
1249 */
1250 private getTypeName(typ: ts.Type): string {
1251 const id = (typ as any).id as number;
1252 if (this.typeNamesById[id]) {
1253 // Name already assigned?
1254 return this.typeNamesById[id];
1255 }
1256 return this.makeTypeNameUnique(
1257 typ,
1258 this.tc
1259 .typeToString(
1260 typ,
1261 undefined,
1262 ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.UseFullyQualifiedType
1263 )
1264 .replace(REGEX_FILE_NAME_OR_SPACE, "")
1265 );
1266 }
1267
1268 private makeTypeNameUnique(typ: ts.Type, baseName: string): string {
1269 const id = (typ as any).id as number;
1270
1271 let name = baseName;
1272 // If a type with same name exists
1273 // Try appending "_1", "_2", etc.
1274 for (let i = 1; this.typeIdsByName[name] !== undefined && this.typeIdsByName[name] !== id; ++i) {
1275 name = baseName + "_" + i;
1276 }
1277
1278 this.typeNamesById[id] = name;
1279 this.typeIdsByName[name] = id;
1280 return name;
1281 }
1282
1283 private recursiveTypeRef = new Map();
1284
1285 private getTypeDefinition(
1286 typ: ts.Type,
1287 asRef = this.args.ref,
1288 unionModifier: keyof Definition = "anyOf",
1289 prop?: ts.Symbol,
1290 reffedType?: ts.Symbol,
1291 pairedSymbol?: ts.Symbol,
1292 forceNotRef: boolean = false,
1293 ignoreUndefined = false,
1294 ): Definition {
1295 const definition: Definition = {}; // real definition
1296
1297 // Ignore any number of Readonly and Mutable type wrappings, since they only add and remove readonly modifiers on fields and JSON Schema is not concerned with mutability
1298 while (
1299 typ.aliasSymbol &&
1300 (typ.aliasSymbol.escapedName === "Readonly" || typ.aliasSymbol.escapedName === "Mutable") &&
1301 typ.aliasTypeArguments &&
1302 typ.aliasTypeArguments[0]
1303 ) {
1304 typ = typ.aliasTypeArguments[0];
1305 reffedType = undefined;
1306 }
1307
1308 if (
1309 this.args.typeOfKeyword &&
1310 typ.flags & ts.TypeFlags.Object &&
1311 (<ts.ObjectType>typ).objectFlags & ts.ObjectFlags.Anonymous
1312 ) {
1313 definition.typeof = "function";
1314 return definition;
1315 }
1316
1317 let returnedDefinition = definition; // returned definition, may be a $ref
1318
1319 // Parse property comments now to skip recursive if ignore.
1320 if (prop) {
1321 const defs: Definition & { [k in MetaDefinitionFields]?: "" } = {};
1322 const others = {};
1323 this.parseCommentsIntoDefinition(prop, defs, others);
1324 if (defs.hasOwnProperty("ignore") || defs.hasOwnProperty("type")) {
1325 return defs;
1326 }
1327 }
1328
1329 const symbol = typ.getSymbol();
1330 // FIXME: We can't just compare the name of the symbol - it ignores the namespace
1331 let isRawType =
1332 !symbol ||
1333 // Window is incorrectly marked as rawType here for some reason
1334 (this.tc.getFullyQualifiedName(symbol) !== "Window" &&
1335 (this.tc.getFullyQualifiedName(symbol) === "Date" ||
1336 symbol.name === "integer" ||
1337 this.tc.getIndexInfoOfType(typ, ts.IndexKind.Number) !== undefined));
1338
1339 if (isRawType && (typ as any).aliasSymbol?.escapedName && (typ as any).types) {
1340 isRawType = false;
1341 }
1342
1343 // special case: an union where all child are string literals -> make an enum instead
1344 let isStringEnum = false;
1345 if (typ.flags & ts.TypeFlags.Union) {
1346 const unionType = <ts.UnionType>typ;
1347 isStringEnum = unionType.types.every((propType) => {
1348 return (propType.getFlags() & ts.TypeFlags.StringLiteral) !== 0;
1349 });
1350 }
1351
1352 // aliased types must be handled slightly different
1353 const asTypeAliasRef = asRef && reffedType && (this.args.aliasRef || isStringEnum);
1354 if (!asTypeAliasRef) {
1355 if (
1356 isRawType ||
1357 (typ.getFlags() & ts.TypeFlags.Object && (<ts.ObjectType>typ).objectFlags & ts.ObjectFlags.Anonymous)
1358 ) {
1359 asRef = false; // raw types and inline types cannot be reffed,
1360 // unless we are handling a type alias
1361 // or it is recursive type - see below
1362 }
1363 }
1364
1365 let fullTypeName = "";
1366 if (asTypeAliasRef) {
1367 const typeName = this.tc
1368 .getFullyQualifiedName(
1369 reffedType!.getFlags() & ts.SymbolFlags.Alias ? this.tc.getAliasedSymbol(reffedType!) : reffedType!
1370 )
1371 .replace(REGEX_FILE_NAME_OR_SPACE, "");
1372 if (this.args.uniqueNames && reffedType) {
1373 const sourceFile = getSourceFile(reffedType);
1374 const relativePath = path.relative(process.cwd(), sourceFile.fileName);
1375 fullTypeName = `${typeName}.${generateHashOfNode(getCanonicalDeclaration(reffedType!), relativePath)}`;
1376 } else {
1377 fullTypeName = this.makeTypeNameUnique(typ, typeName);
1378 }
1379 } else {
1380 // typ.symbol can be undefined
1381 if (this.args.uniqueNames && typ.symbol) {
1382 const sym = typ.symbol;
1383 const sourceFile = getSourceFile(sym);
1384 const relativePath = path.relative(process.cwd(), sourceFile.fileName);
1385 fullTypeName = `${this.getTypeName(typ)}.${generateHashOfNode(
1386 getCanonicalDeclaration(sym),
1387 relativePath
1388 )}`;
1389 } else if (reffedType && this.schemaOverrides.has(reffedType.escapedName as string)) {
1390 fullTypeName = reffedType.escapedName as string;
1391 } else {
1392 fullTypeName = this.getTypeName(typ);
1393 }
1394 }
1395
1396 // Handle recursive types
1397 if (!isRawType || !!typ.aliasSymbol) {
1398 if (this.recursiveTypeRef.has(fullTypeName) && !forceNotRef) {
1399 asRef = true;
1400 } else {
1401 this.recursiveTypeRef.set(fullTypeName, definition);
1402 }
1403 }
1404
1405 if (asRef) {
1406 // We don't return the full definition, but we put it into
1407 // reffedDefinitions below.
1408 returnedDefinition = {
1409 $ref: `${this.args.id}#/definitions/` + fullTypeName,
1410 };
1411 }
1412
1413 // Parse comments
1414 const otherAnnotations: Record<string, true> = {};
1415 this.parseCommentsIntoDefinition(reffedType!, definition, otherAnnotations); // handle comments in the type alias declaration
1416 this.parseCommentsIntoDefinition(symbol!, definition, otherAnnotations);
1417 this.parseCommentsIntoDefinition(typ.aliasSymbol!, definition, otherAnnotations);
1418 if (prop) {
1419 this.parseCommentsIntoDefinition(prop, returnedDefinition, otherAnnotations);
1420 }
1421 if (pairedSymbol && symbol && this.isFromDefaultLib(symbol)) {
1422 this.parseCommentsIntoDefinition(pairedSymbol, definition, otherAnnotations);
1423 }
1424
1425 // Create the actual definition only if is an inline definition, or
1426 // if it will be a $ref and it is not yet created.
1427 // Prioritise overrides.
1428 const overrideDefinition = this.schemaOverrides.get(fullTypeName);
1429 if (overrideDefinition) {
1430 this.reffedDefinitions[fullTypeName] = overrideDefinition;
1431 } else if (!asRef || !this.reffedDefinitions[fullTypeName]) {
1432 if (asRef) {
1433 // must be here to prevent recursivity problems
1434 let reffedDefinition: Definition;
1435 if (asTypeAliasRef && reffedType && typ.symbol !== reffedType && symbol) {
1436 reffedDefinition = this.getTypeDefinition(typ, true, undefined, symbol, symbol);
1437 } else {
1438 reffedDefinition = definition;
1439 }
1440 this.reffedDefinitions[fullTypeName] = reffedDefinition;
1441 if (this.args.titles && fullTypeName) {
1442 definition.title = fullTypeName;
1443 }
1444 }
1445 const node = symbol?.getDeclarations() !== undefined ? symbol.getDeclarations()![0] : null;
1446
1447 if (definition.type === undefined) {
1448 // if users override the type, do not try to infer it
1449 if (typ.flags & ts.TypeFlags.Union && (node === null || node.kind !== ts.SyntaxKind.EnumDeclaration)) {
1450 this.getUnionDefinition(typ as ts.UnionType, unionModifier, definition);
1451 } else if (typ.flags & ts.TypeFlags.Intersection) {
1452 if (this.args.noExtraProps) {
1453 // extend object instead of using allOf because allOf does not work well with additional properties. See #107
1454 if (this.args.noExtraProps) {
1455 definition.additionalProperties = false;
1456 }
1457
1458 const types = (<ts.IntersectionType>typ).types;
1459 for (const member of types) {
1460 const other = this.getTypeDefinition(member, false, undefined, undefined, undefined, undefined, true);
1461 definition.type = other.type; // should always be object
1462 definition.properties = {
1463 ...definition.properties,
1464 ...other.properties,
1465 };
1466
1467 if (Object.keys(other.default || {}).length > 0) {
1468 definition.default = extend(definition.default || {}, other.default);
1469 }
1470 if (other.required) {
1471 definition.required = unique((definition.required || []).concat(other.required)).sort();
1472 }
1473 }
1474 } else {
1475 this.getIntersectionDefinition(typ as ts.IntersectionType, definition);
1476 }
1477 } else if (isRawType) {
1478 if (pairedSymbol) {
1479 this.parseCommentsIntoDefinition(pairedSymbol, definition, {});
1480 }
1481 this.getDefinitionForRootType(typ, reffedType!, definition, undefined, ignoreUndefined);
1482 } else if (
1483 node &&
1484 (node.kind === ts.SyntaxKind.EnumDeclaration || node.kind === ts.SyntaxKind.EnumMember)
1485 ) {
1486 this.getEnumDefinition(typ, definition);
1487 } else if (
1488 symbol &&
1489 symbol.flags & ts.SymbolFlags.TypeLiteral &&
1490 symbol.members!.size === 0 &&
1491 !(node && node.kind === ts.SyntaxKind.MappedType)
1492 ) {
1493 // {} is TypeLiteral with no members. Need special case because it doesn't have declarations.
1494 definition.type = "object";
1495 definition.properties = {};
1496 } else {
1497 this.getClassDefinition(typ, definition);
1498 }
1499 }
1500 }
1501
1502 if (this.recursiveTypeRef.get(fullTypeName) === definition) {
1503 this.recursiveTypeRef.delete(fullTypeName);
1504 // If the type was recursive (there is reffedDefinitions) - lets replace it to reference
1505 if (this.reffedDefinitions[fullTypeName]) {
1506 const annotations = Object.entries(returnedDefinition).reduce<Record<string, unknown>>((acc, [key, value]) => {
1507 if (annotationKeywords[key as keyof typeof annotationKeywords] && typeof value !== undefined) {
1508 acc[key] = value;
1509 }
1510 return acc;
1511 }, {});
1512
1513 returnedDefinition = {
1514 $ref: `${this.args.id}#/definitions/` + fullTypeName,
1515 ...annotations,
1516 };
1517 }
1518 }
1519
1520 if (otherAnnotations["nullable"]) {
1521 makeNullable(returnedDefinition);
1522 }
1523
1524 return returnedDefinition;
1525 }
1526
1527 public setSchemaOverride(symbolName: string, schema: Definition): void {
1528 this.schemaOverrides.set(symbolName, schema);
1529 }
1530
1531 public getSchemaForSymbol(symbolName: string, includeReffedDefinitions: boolean = true, includeAllOverrides: boolean = false): Definition {
1532 const overrideDefinition = this.schemaOverrides.get(symbolName);
1533 if (!this.allSymbols[symbolName] && !overrideDefinition) {
1534 throw new Error(`type ${symbolName} not found`);
1535 }
1536
1537 this.resetSchemaSpecificProperties(includeAllOverrides);
1538
1539 let def;
1540 if (overrideDefinition) {
1541 def = { ...overrideDefinition };
1542 } else {
1543 def = overrideDefinition ? overrideDefinition : this.getTypeDefinition(
1544 this.allSymbols[symbolName],
1545 this.args.topRef,
1546 undefined,
1547 undefined,
1548 undefined,
1549 this.userSymbols[symbolName] || undefined
1550 );
1551 }
1552
1553 if (this.args.ref && includeReffedDefinitions && Object.keys(this.reffedDefinitions).length > 0) {
1554 def.definitions = this.reffedDefinitions;
1555 }
1556 def["$schema"] = "http://json-schema.org/draft-07/schema#";
1557 const id = this.args.id;
1558 if (id) {
1559 def["$id"] = this.args.id;
1560 }
1561 return def;
1562 }
1563
1564 public getSchemaForSymbols(symbolNames: string[], includeReffedDefinitions: boolean = true, includeAllOverrides: boolean = false): Definition {
1565 const root: {
1566 $id?: string,
1567 $schema: string,
1568 definitions: Record<string, Definition>
1569 } = {
1570 $schema: "http://json-schema.org/draft-07/schema#",
1571 definitions: {},
1572 };
1573
1574 this.resetSchemaSpecificProperties(includeAllOverrides);
1575
1576 const id = this.args.id;
1577
1578 if (id) {
1579 root["$id"] = id;
1580 }
1581
1582 for (const symbolName of symbolNames) {
1583 root.definitions[symbolName] = this.getTypeDefinition(
1584 this.allSymbols[symbolName],
1585 this.args.topRef,
1586 undefined,
1587 undefined,
1588 undefined,
1589 this.userSymbols[symbolName]
1590 );
1591 }
1592 if (this.args.ref && includeReffedDefinitions && Object.keys(this.reffedDefinitions).length > 0) {
1593 root.definitions = { ...root.definitions, ...this.reffedDefinitions };
1594 }
1595 return root;
1596 }
1597
1598 public getSymbols(name?: string): SymbolRef[] {
1599 if (name === void 0) {
1600 return this.symbols;
1601 }
1602
1603 return this.symbols.filter((symbol) => symbol.typeName === name);
1604 }
1605
1606 public getUserSymbols(): string[] {
1607 return Object.keys(this.userSymbols);
1608 }
1609
1610 public getMainFileSymbols(program: ts.Program, onlyIncludeFiles?: string[]): string[] {
1611 function includeFile(file: ts.SourceFile): boolean {
1612 if (onlyIncludeFiles === undefined) {
1613 return !file.isDeclarationFile;
1614 }
1615 return onlyIncludeFiles.filter((f) => pathEqual(f, file.fileName)).length > 0;
1616 }
1617 const files = program.getSourceFiles().filter(includeFile);
1618 if (files.length) {
1619 return Object.keys(this.userSymbols).filter((key) => {
1620 const symbol = this.userSymbols[key];
1621 if (!symbol || !symbol.declarations || !symbol.declarations.length) {
1622 return false;
1623 }
1624 let node: ts.Node = symbol.declarations[0];
1625 while (node?.parent) {
1626 node = node.parent;
1627 }
1628 return files.indexOf(node.getSourceFile()) > -1;
1629 });
1630 }
1631 return [];
1632 }
1633}
1634
1635export function getProgramFromFiles(
1636 files: string[],
1637 jsonCompilerOptions: any = {},
1638 basePath: string = "./"
1639): ts.Program {
1640 // use built-in default options
1641 const compilerOptions = ts.convertCompilerOptionsFromJson(jsonCompilerOptions, basePath).options;
1642 const options: ts.CompilerOptions = {
1643 noEmit: true,
1644 emitDecoratorMetadata: true,
1645 experimentalDecorators: true,
1646 target: ts.ScriptTarget.ES5,
1647 module: ts.ModuleKind.CommonJS,
1648 allowUnusedLabels: true,
1649 };
1650 for (const k in compilerOptions) {
1651 if (compilerOptions.hasOwnProperty(k)) {
1652 options[k] = compilerOptions[k];
1653 }
1654 }
1655 return ts.createProgram(files, options);
1656}
1657
1658function generateHashOfNode(node: ts.Node, relativePath: string): string {
1659 return createHash("md5").update(relativePath).update(node.pos.toString()).digest("hex").substring(0, 8);
1660}
1661
1662export function buildGenerator(
1663 program: ts.Program,
1664 args: PartialArgs = {},
1665 onlyIncludeFiles?: string[]
1666): JsonSchemaGenerator | null {
1667 function isUserFile(file: ts.SourceFile): boolean {
1668 if (onlyIncludeFiles === undefined) {
1669 return !file.hasNoDefaultLib;
1670 }
1671 return onlyIncludeFiles.indexOf(file.fileName) >= 0;
1672 }
1673 // Use defaults unless otherwise specified
1674 const settings = getDefaultArgs();
1675
1676 for (const pref in args) {
1677 if (args.hasOwnProperty(pref)) {
1678 (settings as Record<string, Partial<Args>[keyof Args]>)[pref as keyof Args] = args[pref as keyof Args];
1679 }
1680 }
1681
1682 if (args.tsNodeRegister) {
1683 require("ts-node/register");
1684 }
1685
1686 let diagnostics: ReadonlyArray<ts.Diagnostic> = [];
1687
1688 if (!args.ignoreErrors) {
1689 diagnostics = ts.getPreEmitDiagnostics(program);
1690 }
1691
1692 if (diagnostics.length === 0) {
1693 const typeChecker = program.getTypeChecker();
1694
1695 const symbols: SymbolRef[] = [];
1696 const allSymbols: { [name: string]: ts.Type } = {};
1697 const userSymbols: { [name: string]: ts.Symbol } = {};
1698 const inheritingTypes: { [baseName: string]: string[] } = {};
1699 const workingDir = program.getCurrentDirectory();
1700
1701 program.getSourceFiles().forEach((sourceFile, _sourceFileIdx) => {
1702 const relativePath = path.relative(workingDir, sourceFile.fileName);
1703
1704 function inspect(node: ts.Node, tc: ts.TypeChecker) {
1705 if (
1706 node.kind === ts.SyntaxKind.ClassDeclaration ||
1707 node.kind === ts.SyntaxKind.InterfaceDeclaration ||
1708 node.kind === ts.SyntaxKind.EnumDeclaration ||
1709 node.kind === ts.SyntaxKind.TypeAliasDeclaration
1710 ) {
1711 const symbol: ts.Symbol = (<any>node).symbol;
1712 const nodeType = tc.getTypeAtLocation(node);
1713 const fullyQualifiedName = tc.getFullyQualifiedName(symbol);
1714 const typeName = fullyQualifiedName.replace(/".*"\./, "");
1715 const name = !args.uniqueNames ? typeName : `${typeName}.${generateHashOfNode(node, relativePath)}`;
1716
1717 symbols.push({ name, typeName, fullyQualifiedName, symbol });
1718 if (!userSymbols[name]) {
1719 allSymbols[name] = nodeType;
1720 }
1721
1722 if (isUserFile(sourceFile)) {
1723 userSymbols[name] = symbol;
1724 }
1725
1726 const baseTypes = nodeType.getBaseTypes() || [];
1727
1728 baseTypes.forEach((baseType) => {
1729 var baseName = tc.typeToString(baseType, undefined, ts.TypeFormatFlags.UseFullyQualifiedType);
1730 if (!inheritingTypes[baseName]) {
1731 inheritingTypes[baseName] = [];
1732 }
1733 inheritingTypes[baseName].push(name);
1734 });
1735 } else {
1736 ts.forEachChild(node, (n) => inspect(n, tc));
1737 }
1738 }
1739 inspect(sourceFile, typeChecker);
1740 });
1741
1742 return new JsonSchemaGenerator(symbols, allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
1743 } else {
1744 diagnostics.forEach((diagnostic) => {
1745 const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
1746 if (diagnostic.file) {
1747 const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start!);
1748 console.error(`${diagnostic.file.fileName} (${line + 1},${character + 1}): ${message}`);
1749 } else {
1750 console.error(message);
1751 }
1752 });
1753 return null;
1754 }
1755}
1756
1757export function generateSchema(
1758 program: ts.Program,
1759 fullTypeName: string,
1760 args: PartialArgs = {},
1761 onlyIncludeFiles?: string[],
1762 externalGenerator?: JsonSchemaGenerator
1763): Definition | null {
1764 const generator = externalGenerator ?? buildGenerator(program, args, onlyIncludeFiles);
1765
1766 if (generator === null) {
1767 return null;
1768 }
1769
1770 if (fullTypeName === "*") {
1771 // All types in file(s)
1772 return generator.getSchemaForSymbols(generator.getMainFileSymbols(program, onlyIncludeFiles), true, true);
1773 } else if (args.uniqueNames) {
1774 // Find the hashed type name to use as the root object
1775 const matchingSymbols = generator.getSymbols(fullTypeName);
1776 if (matchingSymbols.length === 1) {
1777 return generator.getSchemaForSymbol(matchingSymbols[0].name);
1778 } else {
1779 throw new Error(`${matchingSymbols.length} definitions found for requested type "${fullTypeName}".`);
1780 }
1781 } else {
1782 // Use specific type as root object
1783 return generator.getSchemaForSymbol(fullTypeName);
1784 }
1785}
1786
1787export function programFromConfig(configFileName: string, onlyIncludeFiles?: string[]): ts.Program {
1788 // basically a copy of https://github.com/Microsoft/TypeScript/blob/3663d400270ccae8b69cbeeded8ffdc8fa12d7ad/src/compiler/tsc.ts -> parseConfigFile
1789 const result = ts.parseConfigFileTextToJson(configFileName, ts.sys.readFile(configFileName)!);
1790 const configObject = result.config;
1791
1792 const configParseResult = ts.parseJsonConfigFileContent(
1793 configObject,
1794 ts.sys,
1795 path.dirname(configFileName),
1796 {},
1797 path.basename(configFileName)
1798 );
1799 const options = configParseResult.options;
1800 options.noEmit = true;
1801 delete options.out;
1802 delete options.outDir;
1803 delete options.outFile;
1804 delete options.declaration;
1805 delete options.declarationDir;
1806 delete options.declarationMap;
1807
1808 const program = ts.createProgram({
1809 rootNames: onlyIncludeFiles || configParseResult.fileNames,
1810 options,
1811 projectReferences: configParseResult.projectReferences,
1812 });
1813 return program;
1814}
1815
1816function normalizeFileName(fn: string): string {
1817 while (fn.substr(0, 2) === "./") {
1818 fn = fn.substr(2);
1819 }
1820 return fn;
1821}
1822
1823export async function exec(filePattern: string, fullTypeName: string, args = getDefaultArgs()): Promise<void> {
1824 let program: ts.Program;
1825 let onlyIncludeFiles: string[] | undefined = undefined;
1826 if (REGEX_TSCONFIG_NAME.test(path.basename(filePattern))) {
1827 if (args.include && args.include.length > 0) {
1828 const globs: string[][] = args.include.map((f) => glob.sync(f));
1829 onlyIncludeFiles = ([] as string[]).concat(...globs).map(normalizeFileName);
1830 }
1831 program = programFromConfig(filePattern, onlyIncludeFiles);
1832 } else {
1833 onlyIncludeFiles = glob.sync(filePattern);
1834 program = getProgramFromFiles(onlyIncludeFiles, {
1835 strictNullChecks: args.strictNullChecks,
1836 esModuleInterop: args.esModuleInterop,
1837 skipLibCheck: args.skipLibCheck,
1838 emitDecoratorMetadata: args.experimentalDecorators,
1839 experimentalDecorators: args.experimentalDecorators,
1840 });
1841 onlyIncludeFiles = onlyIncludeFiles.map(normalizeFileName);
1842 }
1843
1844 const definition = generateSchema(program, fullTypeName, args, onlyIncludeFiles);
1845 if (definition === null) {
1846 throw new Error("No output definition. Probably caused by errors prior to this?");
1847 }
1848
1849 const json = stringify(definition, null, 4) + "\n\n";
1850 if (args.out) {
1851 return new Promise((resolve, reject) => {
1852 const fs = require("fs");
1853 fs.mkdir(path.dirname(args.out), { recursive: true }, function (mkErr: Error) {
1854 if (mkErr) {
1855 return reject(new Error("Unable to create parent directory for output file: " + mkErr.message));
1856 }
1857 fs.writeFile(args.out, json, function (wrErr: Error) {
1858 if (wrErr) {
1859 return reject(new Error("Unable to write output file: " + wrErr.message));
1860 }
1861 resolve();
1862 });
1863 });
1864 });
1865 } else {
1866 const hasBeenBuffered = process.stdout.write(json);
1867 if (hasBeenBuffered) {
1868 return new Promise((resolve) => process.stdout.on("drain", () => resolve()));
1869 }
1870 }
1871}
1872
\No newline at end of file