1#!/usr/bin/env node 2'use strict'; 3 4import fs from 'fs'; 5import path from 'path'; 6import * as prettier from 'prettier'; 7import ts from 'typescript'; 8 9import { Closure, ClosureTypes, OutputModuleDefinition } from './types'; 10 11const directoryPath = process.cwd(); 12 13function maybeUnwrapSwiftArray(type: string) { 14 const isArray = type.startsWith('[') && type.endsWith(']'); 15 if (!isArray) { 16 return type; 17 } 18 const innerType = type.substring(1, type.length - 1); 19 return innerType; 20} 21 22function isSwiftArray(type: string) { 23 return type.startsWith('[') && type.endsWith(']'); 24} 25 26function mapSwiftTypeToTsType( 27 type: string 28): ts.KeywordTypeNode | ts.TypeReferenceNode | ts.ArrayTypeNode { 29 if (!type) { 30 return ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword); 31 } 32 if (isSwiftArray(type)) { 33 return ts.factory.createArrayTypeNode(mapSwiftTypeToTsType(maybeUnwrapSwiftArray(type))); 34 } 35 switch (type) { 36 case 'unknown': 37 return ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword); 38 case 'String': 39 return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword); 40 case 'Bool': 41 return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword); 42 case 'Int': 43 case 'Float': 44 case 'Double': 45 return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword); 46 default: 47 return ts.factory.createTypeReferenceNode(type); 48 } 49} 50 51function getMockReturnStatements( 52 tsReturnType: ts.KeywordTypeNode | ts.TypeReferenceNode | ts.ArrayTypeNode 53) { 54 if (!tsReturnType) { 55 return []; 56 } 57 switch (tsReturnType.kind) { 58 case ts.SyntaxKind.AnyKeyword: 59 return [ts.factory.createReturnStatement(ts.factory.createNull())]; 60 case ts.SyntaxKind.StringKeyword: 61 return [ts.factory.createReturnStatement(ts.factory.createStringLiteral(''))]; 62 case ts.SyntaxKind.BooleanKeyword: 63 return [ts.factory.createReturnStatement(ts.factory.createFalse())]; 64 case ts.SyntaxKind.NumberKeyword: 65 return [ts.factory.createReturnStatement(ts.factory.createNumericLiteral('0'))]; 66 case ts.SyntaxKind.VoidKeyword: 67 return []; 68 case ts.SyntaxKind.ArrayType: 69 return [ts.factory.createReturnStatement(ts.factory.createArrayLiteralExpression())]; 70 } 71 return []; 72} 73 74function wrapWithAsync(tsType: ts.TypeNode) { 75 return ts.factory.createTypeReferenceNode('Promise', [tsType]); 76} 77 78function getMockedFunctions(functions: Closure[], async = false) { 79 return functions.map((fnStructure) => { 80 const name = ts.factory.createIdentifier(fnStructure.name); 81 const returnType = mapSwiftTypeToTsType(fnStructure.types?.returnType); 82 const func = ts.factory.createFunctionDeclaration( 83 [ 84 ts.factory.createToken(ts.SyntaxKind.ExportKeyword), 85 async ? ts.factory.createToken(ts.SyntaxKind.AsyncKeyword) : undefined, 86 ].filter((f) => !!f) as ts.ModifierToken<any>[], 87 undefined, 88 name, 89 undefined, 90 fnStructure?.types?.parameters.map((p) => 91 ts.factory.createParameterDeclaration( 92 undefined, 93 undefined, 94 p.name, 95 undefined, 96 mapSwiftTypeToTsType(p.typename), 97 undefined 98 ) 99 ) ?? [], 100 async ? wrapWithAsync(returnType) : returnType, 101 ts.factory.createBlock(getMockReturnStatements(returnType), true) 102 ); 103 return func; 104 }); 105} 106 107function getTypesToMock(module: OutputModuleDefinition) { 108 const foundTypes: string[] = []; 109 110 Object.values(module) 111 .flatMap((t) => (Array.isArray(t) ? t?.map((t2) => (t2 as Closure)?.types) : [] ?? [])) 112 .forEach((types: ClosureTypes | null) => { 113 types?.parameters.forEach(({ typename }) => { 114 foundTypes.push(maybeUnwrapSwiftArray(typename)); 115 }); 116 types?.returnType && foundTypes.push(maybeUnwrapSwiftArray(types.returnType)); 117 }); 118 return new Set( 119 foundTypes.filter((ft) => mapSwiftTypeToTsType(ft).kind === ts.SyntaxKind.TypeReference) 120 ); 121} 122 123function getMockedTypes(types: Set<string>) { 124 return Array.from(types).map((type) => { 125 const name = ts.factory.createIdentifier(type); 126 const typeAlias = ts.factory.createTypeAliasDeclaration( 127 [ts.factory.createToken(ts.SyntaxKind.ExportKeyword)], 128 name, 129 undefined, 130 ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword) 131 ); 132 return typeAlias; 133 }); 134} 135 136function getMockForModule(module: OutputModuleDefinition) { 137 return ([] as (ts.TypeAliasDeclaration | ts.FunctionDeclaration)[]).concat( 138 getMockedTypes(getTypesToMock(module)), 139 getMockedFunctions(module.functions), 140 getMockedFunctions(module.asyncFunctions, true) 141 ); 142} 143 144export async function generateMocks(modules: OutputModuleDefinition[]) { 145 const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); 146 147 for (const m of modules) { 148 const resultFile = ts.createSourceFile( 149 m.name + '.ts', 150 '', 151 ts.ScriptTarget.Latest, 152 false, 153 ts.ScriptKind.TSX 154 ); 155 fs.mkdirSync(path.join(directoryPath, 'mocks'), { recursive: true }); 156 const filePath = path.join(directoryPath, 'mocks', m.name + '.ts'); 157 // get ts nodearray from getMockForModule(m) array 158 const mock = ts.factory.createNodeArray(getMockForModule(m)); 159 const printedTs = printer.printList(ts.ListFormat.MultiLine, mock, resultFile); 160 const compiledJs = ts.transpileModule(printedTs, { 161 compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext }, 162 }).outputText; 163 const prettyJs = await prettier.format(compiledJs, { 164 parser: 'babel', 165 tabWidth: 2, 166 singleQuote: true, 167 }); 168 fs.writeFileSync(filePath, prettyJs); 169 } 170} 171