UNPKG

13.1 kBJavaScriptView Raw
1"use strict";
2// TODO: parser would be ~2x faster if we reused the underlying ts program from TS in TJS
3var __importDefault = (this && this.__importDefault) || function (mod) {
4 return (mod && mod.__esModule) ? mod : { "default": mod };
5};
6var __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};
13Object.defineProperty(exports, "__esModule", { value: true });
14const doctrine_1 = __importDefault(require("doctrine"));
15const fs_extra_1 = __importDefault(require("fs-extra"));
16const fts_core_1 = require("fts-core");
17const path_1 = __importDefault(require("path"));
18const tempy_1 = __importDefault(require("tempy"));
19const TS = __importStar(require("ts-morph"));
20const TJS = __importStar(require("typescript-json-schema"));
21const FTSReturns = 'FTSReturns';
22const FTSParams = 'FTSParams';
23const promiseTypeRe = /^Promise<(.*)>$/;
24const supportedExtensions = {
25 js: 'javascript',
26 jsx: 'javascript',
27 ts: 'typescript',
28 tsx: 'typescript'
29};
30async 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 // initialize and compile TS program
39 const compilerOptions = Object.assign({ allowJs: true, ignoreCompilerErrors: true, esModuleInterop: true,
40 // TODO: why do we need to specify the full filename for these lib definition files?
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 // TODO: throw error?
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 // extract main function type and documentation info
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 // TODO: figure out a better workaround than mutating the source file directly
106 // TODO: fix support for JS files since you can't save TS in JS
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}
124exports.generateDefinition = generateDefinition;
125/** Find main exported function declaration */
126function 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 // TODO: arrow function exports are a lil hacky
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 // TODO: handle named exports `export const foo = () => 'bar'`
169 if (exportSymbol) {
170 const defaultExportPos = exportSymbol
171 .getValueDeclarationOrThrow()
172 .getPos();
173 const exportAssignmentPos = exportAssignment.getPos();
174 // TODO: better way of comparing nodes
175 const isDefaultExport = defaultExportPos === exportAssignmentPos;
176 if (isDefaultExport) {
177 definition.config.defaultExport = true;
178 return func;
179 }
180 }
181 }
182 return undefined;
183}
184function 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 // TODO: this handles alias type resolution i think...
206 // need to test multiple levels of aliasing
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 // TODO: ensure context has valid type `FTS.Context`
213 builder.definition.params.context = true;
214 if (mainParams.length === 1) {
215 builder.definition.params.http = true;
216 httpParameterName = name;
217 }
218 // ignore context in parameter aggregation
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 // ignore Buffer in parameter aggregation
229 continue;
230 }
231 else {
232 // TODO: ensure that type is valid:
233 // not `FTS.Context`
234 // not Promise<T>
235 // not Function or ArrowFunction
236 // not RegExp
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}
251function 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}
275function addPropertyToDeclaration(declaration, structure, jsdoc) {
276 const isDate = structure.type === 'Date';
277 const isBuffer = structure.type === 'Buffer';
278 // Type coercion for non-JSON primitives like Date and Buffer
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}
295function 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 || {}) // Spread any existing schema params
299 );
300 if (!builder.definition.params.schema) {
301 throw new Error(`Error generating params JSON schema for TS file "${file}"`);
302 }
303 // fix required parameters to only be those which do not have default values
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// useful for quick testing purposes
316if (!module.parent) {
317 generateDefinition('./fixtures/http-request.ts')
318 .then((definition) => {
319 console.log(JSON.stringify(definition, null, 2))
320 })
321 .catch((err) => {
322 console.error(err)
323 process.exit(1)
324 })
325}
326*/
327//# sourceMappingURL=parser.js.map
\No newline at end of file