UNPKG

11 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 path_1 = __importDefault(require("path"));
17const tempy_1 = __importDefault(require("tempy"));
18const TS = __importStar(require("ts-simple-ast"));
19const TJS = __importStar(require("typescript-json-schema"));
20const package_1 = require("./package");
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 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 // initialize and compile TS program
38 const compilerOptions = Object.assign({ allowJs: true, ignoreCompilerErrors: true,
39 // TODO: why do we need to specify the full filename for these lib definition files?
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 // TODO: throw error?
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 // extract main function type and documentation info
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 // TODO: figure out a better workaround than mutating the source file directly
103 // TODO: fix support for JS files since you can't save TS in JS
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 // postProcessDefinition(builder)
116 }
117 finally {
118 await fs_extra_1.default.remove(tempSourceFilePath);
119 }
120 return builder.definition;
121}
122exports.generateDefinition = generateDefinition;
123/** Find main exported function declaration */
124function 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 // TODO: arrow function exports are a lil hacky
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 // TODO: handle named exports `export const foo = () => 'bar'`
167 if (exportSymbol) {
168 const defaultExportPos = exportSymbol
169 .getValueDeclarationOrThrow()
170 .getPos();
171 const exportAssignmentPos = exportAssignment.getPos();
172 // TODO: better way of comparing nodes
173 const isDefaultExport = defaultExportPos === exportAssignmentPos;
174 if (isDefaultExport) {
175 definition.config.defaultExport = true;
176 return func;
177 }
178 }
179 }
180 return undefined;
181}
182function 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 // TODO: this handles alias type resolution i think...
199 // need to test multiple levels of aliasing
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 // TODO: ensure context has valid type `FTS.Context`
207 // ignore context in parameter aggregation
208 continue;
209 }
210 else {
211 // TODO: ensure that type is valid:
212 // not `FTS.Context`
213 // not Promise<T>
214 // not Function or ArrowFunction
215 // not RegExp
216 // TODO: does json schema handle Date type for us?
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}
227function 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}
246function addPropertyToDeclaration(declaration, structure, jsdoc) {
247 const isDate = structure.type === 'Date';
248 const isBuffer = structure.type === 'Buffer';
249 // Type coercion for non-JSON primitives like Date and Buffer
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}
266function 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/*
273if (!module.parent) {
274 // useful for quick testing purposes
275 generateDefinition('./fixtures/date.ts')
276 .then((definition) => {
277 console.log(JSON.stringify(definition, null, 2))
278 })
279 .catch((err) => {
280 console.error(err)
281 process.exit(1)
282 })
283}
284*/
285//# sourceMappingURL=parser.js.map
\No newline at end of file