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