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