UNPKG

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