UNPKG

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