UNPKG

17.1 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3const tslib_1 = require("tslib");
4const debug_1 = tslib_1.__importDefault(require("debug"));
5const ts = tslib_1.__importStar(require("typescript"));
6const jsonPointer_1 = require("../jsonPointer");
7const type_1 = require("./type");
8const ast = tslib_1.__importStar(require("./astBuilder"));
9const config_1 = tslib_1.__importDefault(require("./config"));
10const jsonSchema_1 = require("./jsonSchema");
11const referenceResolver_1 = tslib_1.__importDefault(require("./referenceResolver"));
12const utils = tslib_1.__importStar(require("./utils"));
13const debug = debug_1.default('dtsgen');
14const typeMarker = Symbol();
15class DtsGenerator {
16 constructor(contents) {
17 this.resolver = new referenceResolver_1.default();
18 this.contents = contents;
19 }
20 async generate() {
21 const plugins = await this.getPlugins();
22 const preProcess = await this.getPreProcess(plugins.pre);
23 for (const p of preProcess) {
24 this.contents = p(this.contents);
25 }
26 debug('generate type definition files.');
27 this.contents.forEach((schema) => this.resolver.registerSchema(schema));
28 await this.resolver.resolve();
29 const map = this.buildSchemaMergedMap(this.resolver.getAllRegisteredSchema());
30 const root = this.walk(map, true);
31 const file = ts.createSourceFile('_.d.ts', '', config_1.default.target, false, ts.ScriptKind.TS);
32 Object.assign(file, {
33 statements: ts.factory.createNodeArray(root),
34 });
35 const postProcess = await this.getPostProcess(plugins.post);
36 const result = ts.transform(file, postProcess);
37 result.dispose();
38 if (config_1.default.outputAST) {
39 return JSON.stringify(file, null, 2);
40 }
41 else {
42 const printer = ts.createPrinter();
43 return printer.printFile(file);
44 }
45 }
46 buildSchemaMergedMap(schemas) {
47 const map = {};
48 const paths = [];
49 for (const type of schemas) {
50 const path = type.id.toNames();
51 paths.push({ path, type });
52 }
53 for (const item of paths) {
54 const path = item.path;
55 const parent = jsonPointer_1.get(map, path, true);
56 if (parent == null) {
57 jsonPointer_1.set(map, path, { [typeMarker]: item.type });
58 }
59 else {
60 Object.assign(parent, { [typeMarker]: item.type });
61 }
62 }
63 if (Object.keys(map).length === 0) {
64 throw new Error('There is no schema in the input contents.');
65 }
66 return map;
67 }
68 async getPlugins() {
69 const pre = [];
70 const post = [];
71 for (const [name, option] of Object.entries(config_1.default.plugins)) {
72 const plugin = await type_1.loadPlugin(name, option);
73 if (plugin == null) {
74 continue;
75 }
76 if (plugin.preProcess != null) {
77 pre.push({ plugin, option });
78 }
79 if (plugin.postProcess != null) {
80 post.push({ plugin, option });
81 }
82 }
83 return { pre, post };
84 }
85 async getPreProcess(pre) {
86 debug('load pre process plugin.');
87 const result = [];
88 const inputSchemas = this.resolver.getAllRegisteredIdAndSchema();
89 for (const pc of pre) {
90 const p = pc.plugin.preProcess;
91 if (p == null) {
92 continue;
93 }
94 const handler = await p({ inputSchemas, option: pc.option });
95 if (handler != null) {
96 result.push(handler);
97 debug(' pre process plugin:', pc.plugin.meta.name, pc.plugin.meta.description);
98 }
99 }
100 return result;
101 }
102 async getPostProcess(post) {
103 debug('load post process plugin.');
104 const result = [];
105 const inputSchemas = this.resolver.getAllRegisteredIdAndSchema();
106 for (const pc of post) {
107 const p = pc.plugin.postProcess;
108 if (p == null) {
109 continue;
110 }
111 const factory = await p({ inputSchemas, option: pc.option });
112 if (factory != null) {
113 result.push(factory);
114 debug(' pre process plugin:', pc.plugin.meta.name, pc.plugin.meta.description);
115 }
116 }
117 return result;
118 }
119 walk(map, root) {
120 const result = [];
121 const keys = Object.keys(map).sort();
122 for (const key of keys) {
123 const value = map[key];
124 if (typeof value === 'object' &&
125 Object.prototype.hasOwnProperty.call(value, typeMarker)) {
126 const schema = value[typeMarker];
127 debug(` walk doProcess: key=${key} schemaId=${schema.id.getAbsoluteId()}`);
128 result.push(this.walkSchema(schema, root));
129 }
130 if (typeof value === 'object' && Object.keys(value).length > 0) {
131 result.push(ast.buildNamespaceNode(key, this.walk(value, false), root));
132 }
133 }
134 return result;
135 }
136 walkSchema(schema, root) {
137 const normalized = this.normalizeContent(schema);
138 this.currentSchema = normalized;
139 const getNode = () => {
140 const type = normalized.content.type;
141 switch (type) {
142 case 'any':
143 return this.generateAnyTypeModel(normalized, root);
144 case 'array':
145 return this.generateTypeCollection(normalized, root);
146 case 'object':
147 default:
148 return this.generateDeclareType(normalized, root);
149 }
150 };
151 return ast.addOptionalInformation(ast.addComment(getNode(), normalized, true), normalized, true);
152 }
153 normalizeContent(schema, pointer) {
154 if (pointer != null) {
155 schema = jsonSchema_1.getSubSchema(schema, pointer);
156 }
157 return Object.assign(schema, {
158 content: this.normalizeSchemaContent(schema.content),
159 });
160 }
161 normalizeSchemaContent(content) {
162 if (typeof content === 'boolean') {
163 content = content ? {} : { not: {} };
164 }
165 else {
166 if (content.allOf) {
167 const work = {};
168 const allOf = content.allOf;
169 delete content.allOf;
170 for (const sub of allOf) {
171 if (typeof sub === 'object' && sub.$ref) {
172 const ref = this.resolver.dereference(sub.$ref);
173 const normalized = this.normalizeContent(ref).content;
174 utils.mergeSchema(work, normalized);
175 }
176 else {
177 const normalized = this.normalizeSchemaContent(sub);
178 utils.mergeSchema(work, normalized);
179 }
180 }
181 utils.mergeSchema(work, content);
182 content = Object.assign(content, work);
183 }
184 if (content.type === undefined &&
185 (content.properties || content.additionalProperties)) {
186 content.type = 'object';
187 }
188 if (content.nullable) {
189 const type = content.type;
190 const anyOf = content.anyOf;
191 if (Array.isArray(anyOf)) {
192 anyOf.push({ type: 'null' });
193 }
194 else if (type == null) {
195 content.type = 'null';
196 }
197 else if (!Array.isArray(type)) {
198 content.type = [type, 'null'];
199 }
200 else {
201 type.push('null');
202 }
203 }
204 const types = content.type;
205 if (Array.isArray(types)) {
206 const reduced = utils.reduceTypes(types);
207 content.type = reduced.length === 1 ? reduced[0] : reduced;
208 }
209 }
210 return content;
211 }
212 generateDeclareType(schema, root) {
213 const content = schema.content;
214 if (content.$ref ||
215 content.oneOf ||
216 content.anyOf ||
217 content.enum ||
218 'const' in content ||
219 content.type !== 'object') {
220 const type = this.generateTypeProperty(schema);
221 return ast.buildTypeAliasNode(schema.id, type, root);
222 }
223 else {
224 const members = this.generateProperties(schema);
225 return ast.buildInterfaceNode(schema.id, members, root);
226 }
227 }
228 generateAnyTypeModel(schema, root) {
229 const member = ast.buildIndexSignatureNode('name', ast.buildStringKeyword(), ast.buildAnyKeyword());
230 return ast.buildInterfaceNode(schema.id, [member], root);
231 }
232 generateTypeCollection(schema, root) {
233 const type = this.generateArrayTypeProperty(schema);
234 return ast.buildTypeAliasNode(schema.id, type, root);
235 }
236 generateProperties(baseSchema) {
237 const result = [];
238 const content = baseSchema.content;
239 if (content.additionalProperties) {
240 const schema = this.normalizeContent(baseSchema, '/additionalProperties');
241 const valueType = content.additionalProperties === true
242 ? ast.buildAnyKeyword()
243 : this.generateTypeProperty(schema);
244 const node = ast.buildIndexSignatureNode('name', ast.buildStringKeyword(), valueType);
245 result.push(ast.addOptionalInformation(node, schema, true));
246 }
247 if (content.properties) {
248 for (const propertyName of Object.keys(content.properties)) {
249 const schema = this.normalizeContent(baseSchema, '/properties/' + jsonPointer_1.tilde(propertyName));
250 const node = ast.buildPropertySignature(schema, propertyName, this.generateTypeProperty(schema), baseSchema.content.required, false);
251 result.push(ast.addOptionalInformation(ast.addComment(node, schema, true), schema, true));
252 }
253 }
254 if (content.patternProperties) {
255 const schemasTypes = [];
256 for (const propertyName of Object.keys(content.patternProperties)) {
257 const schema = this.normalizeContent(baseSchema, '/patternProperties/' + jsonPointer_1.tilde(propertyName));
258 schemasTypes.push(this.generateTypeProperty(schema));
259 }
260 const node = ast.buildPropertySignature({ content: { readOnly: false } }, 'pattern', ts.createUnionTypeNode(schemasTypes), baseSchema.content.required, true);
261 result.push(ts.addSyntheticTrailingComment(node, ts.SyntaxKind.MultiLineCommentTrivia, ` Patterns: ${Object.keys(content.patternProperties).join(' | ')} `));
262 }
263 return result;
264 }
265 generateTypeProperty(schema, terminate = true) {
266 const content = schema.content;
267 if (content.$ref) {
268 const ref = this.resolver.dereference(content.$ref);
269 if (ref.id == null) {
270 throw new Error('target referenced id is nothing: ' + content.$ref);
271 }
272 const refSchema = this.normalizeContent(ref);
273 const node = ast.buildTypeReferenceNode(refSchema, this.currentSchema);
274 return ast.addOptionalInformation(ast.addComment(node, refSchema, false), refSchema, false);
275 }
276 if (content.anyOf) {
277 return this.generateArrayedType(schema, content.anyOf, Object.assign({}, content, { anyOf: undefined }), '/anyOf/', terminate);
278 }
279 if (content.oneOf) {
280 return this.generateArrayedType(schema, content.oneOf, Object.assign({}, content, { oneOf: undefined }), '/oneOf/', terminate);
281 }
282 if (content.enum) {
283 return ast.buildUnionTypeNode(content.enum, (value) => {
284 return this.generateLiteralTypeNode(content, value);
285 }, terminate);
286 }
287 else if ('const' in content) {
288 return this.generateLiteralTypeNode(content, content.const);
289 }
290 else {
291 return this.generateType(schema, terminate);
292 }
293 }
294 generateLiteralTypeNode(content, value) {
295 switch (content.type) {
296 case 'integer':
297 case 'number':
298 return ast.buildNumericLiteralTypeNode(String(value));
299 case 'boolean':
300 return ast.buildBooleanLiteralTypeNode(value);
301 case 'null':
302 return ast.buildNullKeyword();
303 case 'string':
304 return ast.buildStringLiteralTypeNode(value);
305 }
306 if (value === null) {
307 return ast.buildNullKeyword();
308 }
309 switch (typeof value) {
310 case 'number':
311 return ast.buildNumericLiteralTypeNode(`${value}`);
312 case 'boolean':
313 return ast.buildBooleanLiteralTypeNode(value);
314 case 'string':
315 return ast.buildStringLiteralTypeNode(value);
316 }
317 return ast.buildStringLiteralTypeNode(String(value));
318 }
319 generateArrayedType(baseSchema, contents, mergeContent, path, terminate) {
320 return ast.addOptionalInformation(ast.addComment(ast.buildUnionTypeNode(contents, (_, index) => {
321 const schema = this.normalizeContent(baseSchema, path + index.toString());
322 utils.mergeSchema(schema.content, mergeContent);
323 if (schema.id.isEmpty()) {
324 return ast.addOptionalInformation(this.generateTypeProperty(schema, false), schema, false);
325 }
326 else {
327 return ast.addOptionalInformation(ast.buildTypeReferenceNode(schema, this.currentSchema), schema, false);
328 }
329 }, terminate), baseSchema, false), baseSchema, terminate);
330 }
331 generateArrayTypeProperty(schema, terminate = true) {
332 const items = schema.content.items;
333 const minItems = schema.content.minItems;
334 const maxItems = schema.content.maxItems;
335 if (items == null) {
336 return ast.buildSimpleArrayNode(ast.buildAnyKeyword());
337 }
338 else if (!Array.isArray(items)) {
339 const subSchema = this.normalizeContent(schema, '/items');
340 const node = this.generateTypeProperty(subSchema, false);
341 return ast.buildSimpleArrayNode(ast.addOptionalInformation(node, subSchema, false));
342 }
343 else if (items.length === 0 &&
344 minItems === undefined &&
345 maxItems === undefined) {
346 return ast.buildSimpleArrayNode(ast.buildAnyKeyword());
347 }
348 else if (minItems != null &&
349 maxItems != null &&
350 maxItems < minItems) {
351 return ast.buildNeverKeyword();
352 }
353 else {
354 const types = [];
355 for (let i = 0; i < items.length; i++) {
356 const type = this.normalizeContent(schema, '/items/' + i.toString());
357 if (type.id.isEmpty()) {
358 types.push(this.generateTypeProperty(type, false));
359 }
360 else {
361 types.push(ast.addOptionalInformation(ast.buildTypeReferenceNode(type, this.currentSchema), type, false));
362 }
363 }
364 return ast.addOptionalInformation(ast.buildTupleTypeNode(types, minItems, maxItems), schema, terminate);
365 }
366 }
367 generateType(schema, terminate) {
368 const type = schema.content.type;
369 if (type == null) {
370 return ast.buildAnyKeyword();
371 }
372 else if (typeof type === 'string') {
373 return this.generateTypeName(schema, type, terminate);
374 }
375 else {
376 const types = utils.reduceTypes(type);
377 if (types.length <= 1) {
378 schema.content.type = types[0];
379 return this.generateType(schema, terminate);
380 }
381 else {
382 return ast.buildUnionTypeNode(types, (t) => {
383 return this.generateTypeName(schema, t, false);
384 }, terminate);
385 }
386 }
387 }
388 generateTypeName(schema, type, terminate) {
389 const tsType = utils.toTSType(type, schema.content);
390 if (tsType) {
391 if (tsType === ts.SyntaxKind.NullKeyword) {
392 return ast.buildNullKeyword();
393 }
394 else {
395 return ast.buildKeyword(tsType);
396 }
397 }
398 else if (type === 'object') {
399 const elements = this.generateProperties(schema);
400 if (elements.length > 0) {
401 return ast.buildTypeLiteralNode(elements);
402 }
403 else {
404 return ast.buildUnknownKeyword();
405 }
406 }
407 else if (type === 'array') {
408 return this.generateArrayTypeProperty(schema, terminate);
409 }
410 else {
411 throw new Error('unknown type: ' + type);
412 }
413 }
414}
415exports.default = DtsGenerator;