UNPKG

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