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