1 | "use strict";
|
2 |
|
3 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
4 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
5 | };
|
6 | var __importStar = (this && this.__importStar) || function (mod) {
|
7 | if (mod && mod.__esModule) return mod;
|
8 | var result = {};
|
9 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
|
10 | result["default"] = mod;
|
11 | return result;
|
12 | };
|
13 | Object.defineProperty(exports, "__esModule", { value: true });
|
14 | const doctrine_1 = __importDefault(require("doctrine"));
|
15 | const fs_extra_1 = __importDefault(require("fs-extra"));
|
16 | const fts_core_1 = require("fts-core");
|
17 | const path_1 = __importDefault(require("path"));
|
18 | const tempy_1 = __importDefault(require("tempy"));
|
19 | const TS = __importStar(require("ts-morph"));
|
20 | const TJS = __importStar(require("typescript-json-schema"));
|
21 | const FTSReturns = 'FTSReturns';
|
22 | const FTSParams = 'FTSParams';
|
23 | const promiseTypeRe = /^Promise<(.*)>$/;
|
24 | const supportedExtensions = {
|
25 | js: 'javascript',
|
26 | jsx: 'javascript',
|
27 | ts: 'typescript',
|
28 | tsx: 'typescript'
|
29 | };
|
30 | async function generateDefinition(file, options = {}) {
|
31 | file = path_1.default.resolve(file);
|
32 | const fileInfo = path_1.default.parse(file);
|
33 | const language = supportedExtensions[fileInfo.ext.substr(1)];
|
34 | if (!language) {
|
35 | throw new Error(`File type "${fileInfo.ext}" not supported. "${file}"`);
|
36 | }
|
37 | const outDir = tempy_1.default.directory();
|
38 |
|
39 | const compilerOptions = Object.assign({ allowJs: true, ignoreCompilerErrors: true, esModuleInterop: true,
|
40 |
|
41 | lib: ['lib.es2018.d.ts', 'lib.dom.d.ts'], target: TS.ScriptTarget.ES5, outDir }, (options.compilerOptions || {}));
|
42 | const jsonSchemaOptions = Object.assign({ noExtraProps: true, required: true, validationKeywords: ['coerceTo', 'coerceFrom'] }, (options.jsonSchemaOptions || {}));
|
43 | const definition = {
|
44 | config: {
|
45 | defaultExport: true,
|
46 | language
|
47 | },
|
48 | params: {
|
49 | http: false,
|
50 | context: false,
|
51 | order: [],
|
52 | schema: null
|
53 | },
|
54 | returns: {
|
55 | async: false,
|
56 | http: false,
|
57 | schema: null
|
58 | },
|
59 | version: fts_core_1.version
|
60 | };
|
61 | const project = new TS.Project({ compilerOptions });
|
62 | project.addExistingSourceFile(file);
|
63 | project.resolveSourceFileDependencies();
|
64 | const diagnostics = project.getPreEmitDiagnostics();
|
65 | if (diagnostics.length > 0) {
|
66 | console.log(project.formatDiagnosticsWithColorAndContext(diagnostics));
|
67 |
|
68 | }
|
69 | const sourceFile = project.getSourceFileOrThrow(file);
|
70 | const main = extractMainFunction(sourceFile, definition);
|
71 | if (!main) {
|
72 | throw new Error(`Unable to infer a main function export "${file}"`);
|
73 | }
|
74 |
|
75 | const title = main.getName ? main.getName() : path_1.default.parse(file).name;
|
76 | const mainTypeParams = main.getTypeParameters();
|
77 | definition.title = title;
|
78 | if (mainTypeParams.length > 0) {
|
79 | throw new Error(`Generic Type Parameters are not supported for function "${title}"`);
|
80 | }
|
81 | const doc = main.getJsDocs()[0];
|
82 | let docs;
|
83 | if (doc) {
|
84 | const { description } = doc.getStructure();
|
85 | docs = doctrine_1.default.parse(description);
|
86 | if (docs.description) {
|
87 | definition.description = docs.description;
|
88 | }
|
89 | }
|
90 | const builder = {
|
91 | definition,
|
92 | docs,
|
93 | main,
|
94 | sourceFile,
|
95 | title
|
96 | };
|
97 | if (options.emit) {
|
98 | const result = project.emit(options.emitOptions);
|
99 | if (result.getEmitSkipped()) {
|
100 | throw new Error('emit skipped');
|
101 | }
|
102 | }
|
103 | addParamsDeclaration(builder);
|
104 | addReturnTypeDeclaration(builder);
|
105 |
|
106 |
|
107 | const tempSourceFilePath = path_1.default.format({
|
108 | dir: fileInfo.dir,
|
109 | ext: '.ts',
|
110 | name: `.${fileInfo.name}-fts`
|
111 | });
|
112 | const tempSourceFile = sourceFile.copy(tempSourceFilePath, {
|
113 | overwrite: true
|
114 | });
|
115 | await tempSourceFile.save();
|
116 | try {
|
117 | extractJSONSchemas(builder, tempSourceFilePath, jsonSchemaOptions);
|
118 | }
|
119 | finally {
|
120 | await fs_extra_1.default.remove(tempSourceFilePath);
|
121 | }
|
122 | return builder.definition;
|
123 | }
|
124 | exports.generateDefinition = generateDefinition;
|
125 |
|
126 | function extractMainFunction(sourceFile, definition) {
|
127 | const functionDefaultExports = sourceFile
|
128 | .getFunctions()
|
129 | .filter((f) => f.isDefaultExport());
|
130 | if (functionDefaultExports.length === 1) {
|
131 | definition.config.defaultExport = true;
|
132 | return functionDefaultExports[0];
|
133 | }
|
134 | else {
|
135 | definition.config.defaultExport = false;
|
136 | }
|
137 | const functionExports = sourceFile
|
138 | .getFunctions()
|
139 | .filter((f) => f.isExported());
|
140 | if (functionExports.length === 1) {
|
141 | const func = functionExports[0];
|
142 | definition.config.namedExport = func.getName();
|
143 | return func;
|
144 | }
|
145 | if (functionExports.length > 1) {
|
146 | const externalFunctions = functionExports.filter((f) => {
|
147 | const docs = f.getJsDocs()[0];
|
148 | return (docs &&
|
149 | docs.getTags().find((tag) => {
|
150 | const tagName = tag.getTagName();
|
151 | return tagName === 'external' || tagName === 'public';
|
152 | }));
|
153 | });
|
154 | if (externalFunctions.length === 1) {
|
155 | const func = externalFunctions[0];
|
156 | definition.config.namedExport = func.getName();
|
157 | return func;
|
158 | }
|
159 | }
|
160 |
|
161 | const arrowFunctionExports = sourceFile
|
162 | .getDescendantsOfKind(TS.SyntaxKind.ArrowFunction)
|
163 | .filter((f) => TS.TypeGuards.isExportAssignment(f.getParent()));
|
164 | if (arrowFunctionExports.length === 1) {
|
165 | const func = arrowFunctionExports[0];
|
166 | const exportAssignment = func.getParent();
|
167 | const exportSymbol = sourceFile.getDefaultExportSymbol();
|
168 |
|
169 | if (exportSymbol) {
|
170 | const defaultExportPos = exportSymbol
|
171 | .getValueDeclarationOrThrow()
|
172 | .getPos();
|
173 | const exportAssignmentPos = exportAssignment.getPos();
|
174 |
|
175 | const isDefaultExport = defaultExportPos === exportAssignmentPos;
|
176 | if (isDefaultExport) {
|
177 | definition.config.defaultExport = true;
|
178 | return func;
|
179 | }
|
180 | }
|
181 | }
|
182 | return undefined;
|
183 | }
|
184 | function addParamsDeclaration(builder) {
|
185 | const mainParams = builder.main.getParameters();
|
186 | const paramsDeclaration = builder.sourceFile.addClass({
|
187 | name: FTSParams
|
188 | });
|
189 | const paramComments = {};
|
190 | if (builder.docs) {
|
191 | const paramTags = builder.docs.tags.filter((tag) => tag.title === 'param');
|
192 | for (const tag of paramTags) {
|
193 | paramComments[tag.name] = tag.description;
|
194 | }
|
195 | }
|
196 | let httpParameterName;
|
197 | for (let i = 0; i < mainParams.length; ++i) {
|
198 | const param = mainParams[i];
|
199 | const name = param.getName();
|
200 | const structure = param.getStructure();
|
201 | if (structure.isRestParameter) {
|
202 | builder.definition.params.schema = { additionalProperties: true };
|
203 | continue;
|
204 | }
|
205 |
|
206 |
|
207 | structure.type = param.getType().getText();
|
208 | if (name === 'context') {
|
209 | if (i !== mainParams.length - 1) {
|
210 | throw new Error(`Function parameter "context" must be last parameter to main function "${builder.title}"`);
|
211 | }
|
212 |
|
213 | builder.definition.params.context = true;
|
214 | if (mainParams.length === 1) {
|
215 | builder.definition.params.http = true;
|
216 | httpParameterName = name;
|
217 | }
|
218 |
|
219 | continue;
|
220 | }
|
221 | else if (structure.type === 'Buffer') {
|
222 | if (mainParams.length > 2 || i > 0) {
|
223 | throw new Error(`Function parameter "${name}" of type "Buffer" must not include additional parameters to main function "${builder.title}"`);
|
224 | }
|
225 | builder.definition.params.http = true;
|
226 | httpParameterName = name;
|
227 | builder.definition.params.order.push(name);
|
228 |
|
229 | continue;
|
230 | }
|
231 | else {
|
232 |
|
233 |
|
234 |
|
235 |
|
236 |
|
237 | }
|
238 | const promiseReMatch = structure.type.match(promiseTypeRe);
|
239 | if (promiseReMatch) {
|
240 | throw new Error(`Parameter "${name}" has unsupported type "${structure.type}"`);
|
241 | }
|
242 | addPropertyToDeclaration(paramsDeclaration, structure, paramComments[name]);
|
243 | builder.definition.params.order.push(name);
|
244 | }
|
245 | if (builder.definition.params.http &&
|
246 | builder.definition.params.order.length > 1) {
|
247 | throw new Error(`Function parameter "${httpParameterName}" of type "Buffer" must not include additional parameters to main function "${builder.title}"`);
|
248 | }
|
249 | return paramsDeclaration;
|
250 | }
|
251 | function addReturnTypeDeclaration(builder) {
|
252 | const mainReturnType = builder.main.getReturnType();
|
253 | let type = mainReturnType.getText();
|
254 | const promiseReMatch = type.match(promiseTypeRe);
|
255 | const isAsync = !!promiseReMatch;
|
256 | builder.definition.returns.async = builder.main.isAsync();
|
257 | if (isAsync) {
|
258 | type = promiseReMatch[1];
|
259 | builder.definition.returns.async = true;
|
260 | }
|
261 | if (type === 'void') {
|
262 | type = 'any';
|
263 | }
|
264 | if (type.endsWith('HttpResponse') &&
|
265 | (isAsync || mainReturnType.isInterface())) {
|
266 | builder.definition.returns.http = true;
|
267 | }
|
268 | const declaration = builder.sourceFile.addInterface({
|
269 | name: FTSReturns
|
270 | });
|
271 | const jsdoc = builder.docs &&
|
272 | builder.docs.tags.find((tag) => tag.title === 'returns' || tag.title === 'return');
|
273 | addPropertyToDeclaration(declaration, { name: 'result', type }, jsdoc && jsdoc.description);
|
274 | }
|
275 | function addPropertyToDeclaration(declaration, structure, jsdoc) {
|
276 | const isDate = structure.type === 'Date';
|
277 | const isBuffer = structure.type === 'Buffer';
|
278 |
|
279 | if (isDate || isBuffer) {
|
280 | const coercionType = structure.type;
|
281 | if (isDate) {
|
282 | structure.type = 'Date';
|
283 | }
|
284 | else {
|
285 | structure.type = 'string';
|
286 | }
|
287 | jsdoc = `${jsdoc ? jsdoc + '\n' : ''}@coerceTo ${coercionType}`;
|
288 | }
|
289 | const property = declaration.addProperty(structure);
|
290 | if (jsdoc) {
|
291 | property.addJsDoc(jsdoc);
|
292 | }
|
293 | return property;
|
294 | }
|
295 | function extractJSONSchemas(builder, file, jsonSchemaOptions = {}, jsonCompilerOptions = {}) {
|
296 | const compilerOptions = Object.assign({ allowJs: true, lib: ['es2018', 'dom'], target: 'es5', esModuleInterop: true }, jsonCompilerOptions);
|
297 | const program = TJS.getProgramFromFiles([file], compilerOptions, process.cwd());
|
298 | builder.definition.params.schema = Object.assign({}, TJS.generateSchema(program, FTSParams, jsonSchemaOptions), (builder.definition.params.schema || {})
|
299 | );
|
300 | if (!builder.definition.params.schema) {
|
301 | throw new Error(`Error generating params JSON schema for TS file "${file}"`);
|
302 | }
|
303 |
|
304 | const { schema } = builder.definition.params;
|
305 | schema.required = (schema.required || []).filter((k) => schema.properties[k].default === undefined);
|
306 | if (!schema.required.length) {
|
307 | delete schema.required;
|
308 | }
|
309 | builder.definition.returns.schema = TJS.generateSchema(program, FTSReturns, Object.assign({}, jsonSchemaOptions, { required: false }));
|
310 | if (!builder.definition.returns.schema) {
|
311 | throw new Error(`Error generating returns JSON schema for TS file "${file}"`);
|
312 | }
|
313 | }
|
314 |
|
315 |
|
316 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
\ | No newline at end of file |