This commit is contained in:
Haowen Chen 2020-12-09 13:58:26 +08:00 коммит произвёл GitHub
Родитель 7a89fe576f
Коммит ad0b71cd04
22 изменённых файлов: 9629 добавлений и 0 удалений

78
.eslintrc.js Normal file
Просмотреть файл

@ -0,0 +1,78 @@
// eslint-disable-next-line no-undef
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
"project": "tsconfig.json",
"sourceType": "module"
},
env: {
"browser": true,
},
plugins: [
"@typescript-eslint",
],
extends: [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"eslint-config-airbnb-base",
"eslint-config-prettier"
],
settings: {
"import/resolver": {
"node": {
"extensions": [".ts"]
}
}
},
rules: {
"import/no-unresolved": [2],
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/explicit-module-boundary-types": "error",
"@typescript-eslint/prefer-regexp-exec": "warn",
"@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unnecessary-type-assertion": "warn",
"@typescript-eslint/no-unsafe-return": "warn",
"@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/semi": [
"error",
"always"
],
"@typescript-eslint/no-unused-vars": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": [
"error",
{ "functions": false }
],
"no-unused-vars": "off",
"no-useless-escape": "warn",
"no-prototype-builtins": "warn",
"no-console": "off",
"arrow-parens": [
"off",
"always"
],
"sort-keys": "off",
"max-len": "off",
"no-bitwise": "off",
"no-duplicate-case": "error",
"quotes": ["off", "single"],
"curly": "error",
"import/extensions": "off",
"class-methods-use-this": "off",
"no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],
"camelcase": ["error", {"allow": ["UNSAFE_componentWillReceiveProps"]}],
"import/prefer-default-export": "off",
"import/no-default-export": "error",
"no-param-reassign": ["error", { "props": false }],
"no-underscore-dangle": ["error", { "enforceInMethodNames": true, "allowAfterThis": true }],
"no-useless-constructor": "off",
"no-empty-function": ["error", {"allow": ["constructors"]}],
}
};

116
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,116 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

43
azure-pipelines.yml Normal file
Просмотреть файл

@ -0,0 +1,43 @@
name: $(Build.BuildId)
trigger:
batch: true
branches:
include:
- '*'
tags:
include:
- '*'
jobs:
- job: BuildJavaScript
displayName: Build JavaScript
pool:
vmName: ubuntu-18.04
demands:
- npm
steps:
- task: Npm@1
displayName: 'npm cache clean --force'
inputs:
command: custom
verbose: false
customCommand: 'cache clean --force'
- task: Npm@1
displayName: 'npm install'
- task: Npm@1
displayName: 'npm run lint:ci'
inputs:
command: custom
verbose: false
customCommand: 'run lint:ci'
- task: Npm@1
displayName: 'npm run build'
inputs:
command: custom
verbose: false
customCommand: 'run build'

36
gulpfile.ts Normal file
Просмотреть файл

@ -0,0 +1,36 @@
import {series, watch, dest} from 'gulp'
import ts from 'gulp-typescript'
import del from 'del'
import {spawn} from 'child_process'
const tsProject = ts.createProject("tsconfig.json", {declaration: true})
function tsBuild() {
return tsProject
.src()
.pipe(tsProject())
.pipe(dest("dist"))
}
async function run() {
spawn('node', ['dist/index.js'], {stdio: 'inherit'})
}
async function tsRun() {
spawn('ts-node', ['src/index.ts'], {stdio: 'inherit'})
}
function watchRun() {
watch('src/*.ts', tsRun)
}
export const build = tsBuild
export function clean() {
return del('dist')
}
export const start = series(clean, build, run)
export const dev = series(tsRun, watchRun)

4414
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

49
package.json Normal file
Просмотреть файл

@ -0,0 +1,49 @@
{
"name": "ts-codegen-swift",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "gulp build",
"clean": "gulp clean",
"start": "gulp start",
"dev": "gulp dev",
"start:example": "ts-node ./src/example/exampleDemo",
"test": "echo \"Error: no test specified\" && exit 1",
"lint": "npx eslint ./src --ext .js,.jsx,.ts,.tsx",
"prettier:write": "npx prettier --write \"src/**/*.ts\"",
"prettier:check": "npx prettier --check \"src/**/*.ts\"",
"lint:fix": "npm run lint -- --fix && npm run prettier:write",
"lint:ci": "npm run lint && npm run prettier:check"
},
"bin": {
"ts-codegen-swift": "dist/index.js"
},
"files": [
"dist/**/*.js"
],
"types": "typings/*/**.d.ts",
"keywords": [],
"author": "",
"license": "private",
"devDependencies": {
"@types/gulp": "^4.0.6",
"@types/yargs": "^15.0.9",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"del": "^5.1.0",
"eslint": "^7.5.0",
"eslint-config-airbnb-base": "^14.2.1",
"eslint-config-prettier": "^7.0.0",
"eslint-plugin-import": "^2.22.1",
"gulp": "^4.0.2",
"gulp-typescript": "^6.0.0-alpha.1",
"prettier": "^2.2.1",
"ts-node": "^8.10.2"
},
"dependencies": {
"glob": "^7.1.6",
"typescript": "^3.9.7",
"yargs": "^16.1.0"
}
}

12
prettier.config.js Normal file
Просмотреть файл

@ -0,0 +1,12 @@
module.exports = {
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"semi": true,
"trailingComma": "es5",
"bracketSpacing": true,
"parser": "babel-ts",
"quoteProps": "as-needed",
"jsxBracketSameLine": false,
}

394
src/Parser.ts Normal file
Просмотреть файл

@ -0,0 +1,394 @@
import ts from 'typescript';
import { glob } from 'glob';
import {
Module,
Method,
Field,
ValueType,
BasicTypeValue,
CustomTypeKind,
ArrayTypeKind,
BasicTypeKind,
ValueTypeKindFlag,
} from './types';
export class Parser {
program: ts.Program;
checker: ts.TypeChecker;
constructor(globPatterns: string[]) {
const filePaths = globPatterns.flatMap((pattern) => glob.sync(pattern));
this.program = ts.createProgram({
rootNames: filePaths,
options: {},
});
this.checker = this.program.getTypeChecker();
}
parse = (): Module[] => {
const modules: Module[] = [];
this.program.getSourceFiles().forEach((sourceFile) => {
ts.forEachChild(sourceFile, (node) => {
const module = this.moduleFromNode(node);
if (module !== null) {
modules.push(module);
}
});
});
return modules;
};
private moduleFromNode = (node: ts.Node): Module | null => {
if (!this.isNodeExported(node) || !ts.isInterfaceDeclaration(node)) {
return null;
}
if (node.name === undefined) {
return null;
}
const interfaceName = node.name?.text;
if (node.members === undefined || node.members === null) {
return null;
}
if (!this.isNodeExtended(node, 'IExportedApi')) {
return null;
}
const methods: Method[] = [];
node.members.forEach((methodNode) => {
if (!ts.isPropertySignature(methodNode)) {
return;
}
const methodType = methodNode.type;
if (!methodType) {
return;
}
if (!ts.isFunctionTypeNode(methodType)) {
return;
}
const method = this.methodFromMethodNode(methodNode.name.getText(), methodType);
if (method) {
methods.push(method);
}
});
if (methods.length === 0) {
return null;
}
return {
name: interfaceName,
methods,
};
};
private methodFromMethodNode = (methodName: string, node: ts.FunctionTypeNode): Method | null => {
let parameters: Field[] = [];
const fields = this.fieldsFromFunctionTypeNodeForParameters(node, methodName);
if (fields !== null) {
parameters = fields;
}
let returnValueType: ValueType | null = null;
if (node.type !== undefined) {
const valueType = this.valueTypeFromNode(node, `${methodName}Return`);
if (valueType !== null) {
returnValueType = valueType;
}
}
return {
name: methodName,
parameters,
returnType: returnValueType,
};
};
private valueTypeFromNode = (
node: ts.Node & { type?: ts.TypeNode; questionToken?: ts.QuestionToken },
literalTypeDescription: string
): ValueType | null => {
if (node.type === undefined) {
return null;
}
const nullable = this.isValueTypeNullableFromNode(node);
return this.valueTypeFromTypeNode(node.type, nullable, literalTypeDescription);
};
private isValueTypeNullableFromNode = (node: ts.Node & { type?: ts.TypeNode; questionToken?: ts.QuestionToken }): boolean => {
const nullable = node.questionToken !== undefined;
return nullable;
};
private valueTypeFromTypeNode = (
typeNode: ts.TypeNode,
nullable: boolean,
literalTypeDescription: string
): ValueType | null => {
if (ts.isUnionTypeNode(typeNode)) {
return this.extractUnionTypeNode(typeNode, literalTypeDescription, nullable);
}
const typeKind = this.basicTypeKindFromTypeNode(typeNode);
if (typeKind !== null) {
return {
kind: typeKind,
nullable,
};
}
let customTypeKind = this.referenceTypeKindFromTypeNode(typeNode);
if (customTypeKind !== null) {
return {
kind: customTypeKind,
nullable,
};
}
customTypeKind = this.literalTypeKindFromTypeNode(typeNode, literalTypeDescription);
if (customTypeKind !== null) {
return {
kind: customTypeKind,
nullable,
};
}
const arrayTypeKind = this.arrayTypeKindFromTypeNode(typeNode, `${literalTypeDescription}Array`);
if (arrayTypeKind !== null) {
return {
kind: arrayTypeKind,
nullable,
};
}
return null;
};
private fieldsFromFunctionTypeNodeForParameters = (
node: ts.FunctionTypeNode,
literalTypeDescription: string
): Field[] | null => {
if (!node.parameters || !node.parameters.length) {
return null;
}
return node.parameters
.map((item) =>
this.fieldFromParameter(
item,
`${literalTypeDescription}Parameters${this.getNameWithCapitalFirstLetter(item.name.getText())}`
)
)
.filter((field): field is Field => field !== null);
};
private fieldFromParameter = (node: ts.ParameterDeclaration, literalTypeDescription: string): Field | null => {
const name = node.name.getText();
const valueType = this.valueTypeFromNode(node, literalTypeDescription);
if (valueType !== null) {
return {
name,
type: valueType,
};
}
return null;
};
private fieldsFromLiteralTypeNode = (type: ts.TypeNode, literalTypeDescription: string): Field[] | null => {
if (!ts.isTypeLiteralNode(type)) {
return null;
}
return type.members
.map((item, index) =>
this.fieldFromTypeElement(
item,
`${literalTypeDescription}Members${this.getNameWithCapitalFirstLetter(item.name?.getText()) || index}`
)
)
.filter((field): field is Field => field !== null);
};
private fieldFromTypeElement = (node: ts.TypeElement, literalTypeDescription: string): Field | null => {
if (!ts.isPropertySignature(node) || node.type === undefined) {
return null;
}
let name = node.name.getText();
if (!name || name === '__type') {
name = `${literalTypeDescription}Type`;
}
const valueType = this.valueTypeFromNode(node, literalTypeDescription);
if (valueType !== null) {
return {
name,
type: valueType,
};
}
return null;
};
private basicTypeKindFromTypeNode = (node: ts.TypeNode): BasicTypeKind | null => {
if (node.kind === ts.SyntaxKind.StringKeyword) {
return {
flag: ValueTypeKindFlag.basicType,
value: BasicTypeValue.string,
};
}
if (node.kind === ts.SyntaxKind.NumberKeyword) {
return {
flag: ValueTypeKindFlag.basicType,
value: BasicTypeValue.number,
};
}
if (node.kind === ts.SyntaxKind.BooleanKeyword) {
return {
flag: ValueTypeKindFlag.basicType,
value: BasicTypeValue.boolean,
};
}
return null;
};
private referenceTypeKindFromTypeNode = (node: ts.TypeNode): CustomTypeKind | null => {
if (!ts.isTypeReferenceNode(node)) {
return null;
}
const declarations = this.checker.getTypeFromTypeNode(node).symbol.getDeclarations();
if (declarations === undefined || declarations.length !== 1) {
return null;
}
const declNode = declarations[0];
if (!ts.isInterfaceDeclaration(declNode)) {
return null;
}
const name = declNode.name.getText();
const members = declNode.members
.map((item, index) =>
this.fieldFromTypeElement(item, `${name}Members${this.getNameWithCapitalFirstLetter(item.name?.getText()) || index}`)
)
.filter((field): field is Field => field !== null);
return {
flag: ValueTypeKindFlag.customType,
name,
members,
};
};
private literalTypeKindFromTypeNode = (node: ts.TypeNode, literalTypeDescription: string): CustomTypeKind | null => {
if (!ts.isTypeLiteralNode(node)) {
return null;
}
const fields = this.fieldsFromLiteralTypeNode(node, literalTypeDescription);
if (fields) {
return {
flag: ValueTypeKindFlag.customType,
name: `${literalTypeDescription}Type`,
isTypeLiteral: true,
members: fields,
};
}
return null;
};
private arrayTypeKindFromTypeNode = (node: ts.TypeNode, literalTypeDescription: string): ArrayTypeKind | null => {
if (!ts.isArrayTypeNode(node)) {
return null;
}
const elementType = this.valueTypeFromTypeNode(node.elementType, false, `${literalTypeDescription}Element`);
if (elementType) {
return {
flag: ValueTypeKindFlag.arrayType,
elementType,
};
}
return null;
};
private extractUnionTypeNode = (node: ts.Node, literalTypeDescription: string, nullable: boolean): ValueType | null => {
if (!ts.isUnionTypeNode(node)) {
return null;
}
let isNullable = nullable;
let kind: ValueType['kind'] | undefined;
node.types.forEach((typeNode) => {
if (!isNullable && this.isUndefinedOrNull(typeNode)) {
isNullable = true;
}
if (!kind && !this.isUndefinedOrNull(typeNode)) {
kind = this.valueTypeFromTypeNode(typeNode, false, literalTypeDescription)?.kind;
}
});
if (!kind) {
return null;
}
return {
kind,
nullable: isNullable,
};
};
private isUndefinedOrNull = (node: ts.TypeNode): boolean => {
if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
return true;
}
if (node.kind === ts.SyntaxKind.NullKeyword) {
return true;
}
return false;
};
private isNodeExported = (node: ts.Node): boolean =>
(ts.getCombinedModifierFlags(node as ts.Declaration) & ts.ModifierFlags.Export) !== 0 ||
(!!node.parent && node.parent.kind === ts.SyntaxKind.SourceFile);
private isNodeExtended(node: ts.InterfaceDeclaration, extendedInterfaceName: string): boolean {
if (!node.heritageClauses?.length) {
return false;
}
const extendHeritageClause = node.heritageClauses.find((item) => item.token === ts.SyntaxKind.ExtendsKeyword);
if (!extendHeritageClause) {
return false;
}
const extendedInterface = extendHeritageClause.types.find(
(item) => this.checker.getTypeAtLocation(item).symbol.escapedName === extendedInterfaceName
);
return !!extendedInterface;
}
private getNameWithCapitalFirstLetter(name?: string): string | undefined {
let targetName = name;
if (name && name.length > 0) {
targetName = name[0].toUpperCase() + name.slice(1);
}
return targetName;
}
}

16
src/demo/data/demoApi.ts Normal file
Просмотреть файл

@ -0,0 +1,16 @@
import { IExportedApi } from '../../index';
export interface IHtmlApi extends IExportedApi {
getHeight: () => number;
getHeightWithBottomAnchor: (sta: string[]) => number;
getHTML: () => string;
requestRenderingResult: () => void;
}
export interface IImageOptionApi extends IExportedApi {
hideElementWithID: (ID: string) => void;
restoreElementVisibilityWithID: (ID: string) => void;
getSourceOfImageWithID: (ID: string) => string | null;
getImageDataList: () => string;
getContentBoundsOfElementWithID: (ID: string) => string | null;
}

Просмотреть файл

@ -0,0 +1,253 @@
[
{
"name": "IHtmlApi",
"methods": [
{
"name": "getHeight",
"parameters": [
{
"name": "test",
"type": {
"kind": {
"flag": "customType",
"name": "Test",
"members": [
{
"name": "st",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
]
},
"nullable": true
}
},
{
"name": "test2",
"type": {
"kind": {
"flag": "customType",
"name": "getHeightParametersTest2Type",
"isTypeLiteral": true,
"members": [
{
"name": "st",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
},
{
"name": "embed",
"type": {
"kind": {
"flag": "customType",
"name": "getHeightParametersTest2MembersEmbedType",
"isTypeLiteral": true,
"members": [
{
"name": "a",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": true
}
}
]
},
"nullable": false
}
}
]
},
"nullable": true
}
}
],
"returnType": {
"kind": {
"flag": "arrayType",
"elementType": {
"kind": {
"flag": "customType",
"name": "Test",
"members": [
{
"name": "st",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
]
},
"nullable": false
}
},
"nullable": false
}
},
{
"name": "getHeightWithBottomAnchor",
"parameters": [
{
"name": "sta",
"type": {
"kind": {
"flag": "arrayType",
"elementType": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
},
"nullable": false
}
}
],
"returnType": {
"kind": {
"flag": "basicType",
"value": "number"
},
"nullable": false
}
},
{
"name": "getHTML",
"parameters": [],
"returnType": {
"kind": {
"flag": "customType",
"name": "Test",
"members": [
{
"name": "st",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
]
},
"nullable": false
}
},
{
"name": "requestRenderingResult",
"parameters": [],
"returnType": null
}
]
},
{
"name": "IImageOptionApi",
"methods": [
{
"name": "hideElementWithID",
"parameters": [
{
"name": "ID",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
],
"returnType": null
},
{
"name": "restoreElementVisibilityWithID",
"parameters": [
{
"name": "ID",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
],
"returnType": null
},
{
"name": "getSourceOfImageWithID",
"parameters": [
{
"name": "ID",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
],
"returnType": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": true
}
},
{
"name": "getImageDataList",
"parameters": [],
"returnType": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
},
{
"name": "getContentBoundsOfElementWithID",
"parameters": [
{
"name": "ID",
"type": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": false
}
}
],
"returnType": {
"kind": {
"flag": "basicType",
"value": "string"
},
"nullable": true
}
}
]
}
]

23
src/demo/demo.ts Normal file
Просмотреть файл

@ -0,0 +1,23 @@
import { Parser } from '../Parser';
import { RendererConfig, DefaultSwiftRendererConfig } from '../renderer/RenderConfig';
import { CustomTypeCollector } from '../renderer/CustomTypeCollector';
import { ExampleCodeRenderer } from './demoCodeRenderer';
function run(): void {
const config = new DefaultSwiftRendererConfig();
console.log('Api will be generated with config: \n', config);
const parser = new Parser(['src/example/data/exampleApi.ts']);
const apiModules = parser.parse();
console.log(JSON.stringify(apiModules, null, 4));
const rendererConfig = config as RendererConfig;
const typeTransformer = new CustomTypeCollector(rendererConfig);
const renderer = new ExampleCodeRenderer(rendererConfig, typeTransformer);
renderer.print();
console.log(typeTransformer.toSourceLike().join('\n'));
}
run();

Просмотреть файл

@ -0,0 +1,68 @@
import dummyData from './data/parsedModules.json';
import { Module } from '../types';
import { RendererConfig } from '../renderer/RenderConfig';
import { InternalDataStructure } from '../renderer/InternalDataStructure';
import { GenericCodeRenderer } from '../renderer/GenericCodeRenderer';
import { TypeTransformer } from '../renderer/CustomTypeCollector';
export class ExampleCodeRenderer extends GenericCodeRenderer {
constructor(rendererConfig: RendererConfig, private typeTransformer: TypeTransformer) {
super(rendererConfig);
}
render(): void {
const dummyDataTyped = dummyData as Module[];
dummyDataTyped.forEach((module) => {
this.namespaces.push(module.name);
this.emitLine(0, `// ${module.name}`);
module.methods.forEach((method) => {
const source = `${method.name}`;
const parameterSources = method.parameters.map(
(parameter) => `${parameter.name}: ${this.typeTransformer.transformType(parameter.type)}`
);
let returnTypeStatement: string;
if (method.returnType !== null) {
const returnTypeString = this.typeTransformer.transformType(method.returnType, true);
returnTypeStatement = `@escaping (BridgeCompletion<${returnTypeString}?>)? = nil`;
} else {
returnTypeStatement = `BridgeJSExecutor.Completion? = nil`;
}
parameterSources.push(`completion: ${returnTypeStatement})`);
const parameterSourceStatement = parameterSources.join(', ');
this.emitLine(0, `func ${source}(${parameterSourceStatement}`);
this.emitCurlyBracketBegin();
const hasParam = method.parameters.length > 0;
if (hasParam) {
const argSource = new InternalDataStructure(
this.rendererConfig,
'Args',
this.typeTransformer,
method.parameters
).toSourceCode();
this.emitLines(2, argSource);
this.emitLine(2, `let args = Args(`);
method.parameters.forEach((param) => this.emitLine(4, `${param.name}: ${param.name}`));
this.emitLine(2, `)`);
}
const executeMethod = method.returnType !== null ? 'executeFetch' : 'execute';
const endpoint = this.resolveEndpoint(module.name);
this.emitLine(
2,
`jsExecutor.${executeMethod}(with: "${endpoint}", feature: "${method.name}", args: ${
hasParam ? 'args' : 'nil'
}, completion: completion)`
);
this.emitCurlyBracketEnd();
});
});
}
print(): void {
console.log(this.toSourceCode().join('\n'));
}
}

56
src/index.ts Normal file
Просмотреть файл

@ -0,0 +1,56 @@
#!/usr/bin/env node
import yargs from 'yargs';
import fs from 'fs';
import { Parser } from './Parser';
import { RendererConfig } from './renderer/RenderConfig';
import { CustomTypeCollector } from './renderer/CustomTypeCollector';
import { SwiftCodeRenderer } from './renderer/SwiftCodeRenderer';
const program = yargs(process.argv.slice(2));
const args = program
.options({
config: {
alias: 'c',
type: 'string',
demandOption: true,
describe: 'Code-generate config JSON which should implement interface RendererConfig',
},
path: {
alias: 'p',
type: 'string',
describe: 'The path of api interface which should extend IExportedApi',
demandOption: true,
},
output: {
alias: 'o',
type: 'string',
demandOption: true,
describe: 'The path of output file',
},
})
.help().argv;
function run(): void {
const configJson = fs.readFileSync(args.config, { encoding: 'utf8' });
const config = JSON.parse(configJson) as RendererConfig;
console.log('Native Api will be generated with config: \n', config);
const parser = new Parser([args.path]);
const apiModules = parser.parse();
// console.log(JSON.stringify(result, null, 4))
const rendererConfig = config;
const typeTransformer = new CustomTypeCollector(rendererConfig);
const renderer = new SwiftCodeRenderer(rendererConfig, typeTransformer, apiModules, args.output);
renderer.print();
// console.log(typeTransformer.toSourceLike().join('\n'));
}
run();
export interface IExportedApi {}
export type { RendererConfig } from './renderer/RenderConfig';

Просмотреть файл

@ -0,0 +1,83 @@
import { CustomTypeKind, ValueType, Field, ArrayTypeKind, BasicTypeKind, ValueTypeKindFlag } from '../types';
import { SourceLike } from './SourceLike';
import { RendererConfig } from './RenderConfig';
import { InternalDataStructure } from './InternalDataStructure';
export interface TypeTransformer {
transformType(fieldType: ValueType | Field[], ignoreNullable?: boolean): string;
toSourceLike(): SourceLike[];
}
export class CustomTypeCollector implements TypeTransformer {
private customTypes: Record<string, CustomTypeKind> = {};
constructor(protected rendererConfig: RendererConfig) {}
public emit(customType: CustomTypeKind): void {
this.customTypes[customType.name] = customType;
}
public toSourceLike(): SourceLike[] {
let result: SourceLike[] = [];
Object.keys(this.customTypes).forEach((typeName) => {
const refinedTypeName = typeName.replace(this.rendererConfig.tsCustomTypePrefix, '');
const customDataStructure = new InternalDataStructure(
this.rendererConfig,
refinedTypeName,
this,
this.customTypes[typeName].members
);
result = result.concat(customDataStructure.toSourceCode());
});
return result;
}
public transformType(fieldType: ValueType, ignoreNullable = false): string {
let targetType: string;
const UNKNOWN_TYPE = 'unknown';
if (this.isBasicTypeKind(fieldType.kind)) {
switch (fieldType.kind.value) {
case 'string':
targetType = 'String';
break;
case 'number':
targetType = 'CGFloat';
break;
case 'boolean':
targetType = 'Bool';
break;
default:
targetType = UNKNOWN_TYPE;
}
} else if (this.isCustomTypeKind(fieldType.kind)) {
targetType = fieldType.kind.name;
if (targetType.startsWith(this.rendererConfig.tsCustomTypePrefix)) {
targetType = targetType.replace(this.rendererConfig.tsCustomTypePrefix, '');
}
this.emit(fieldType.kind);
} else if (this.isArrayTypeKind(fieldType.kind)) {
targetType = `[${this.transformType(fieldType.kind.elementType)}]`;
} else {
targetType = UNKNOWN_TYPE;
}
if (!ignoreNullable && fieldType.nullable && targetType !== UNKNOWN_TYPE) {
targetType += '?';
}
return targetType;
}
private isBasicTypeKind(kind: ValueType['kind']): kind is BasicTypeKind {
return kind.flag === ValueTypeKindFlag.basicType;
}
private isCustomTypeKind(kind: ValueType['kind']): kind is CustomTypeKind {
return kind.flag === ValueTypeKindFlag.customType;
}
private isArrayTypeKind(kind: ValueType['kind']): kind is ArrayTypeKind {
return kind.flag === ValueTypeKindFlag.arrayType;
}
}

Просмотреть файл

@ -0,0 +1,44 @@
import { SourceLike } from './SourceLike';
import { RendererConfig } from './RenderConfig';
export abstract class GenericCodeRenderer {
private sourceLines: SourceLike[] = [];
private typeLines: SourceLike[] = [];
namespaces: string[] = [];
constructor(protected rendererConfig: RendererConfig) {}
public toSourceCode(): SourceLike[] {
this.render();
return this.sourceLines;
}
protected abstract render(): void;
protected resolveEndpoint(moduleName: string): string {
return this.rendererConfig.pathMap[moduleName];
}
protected emitLine(indent: number, content: SourceLike): void {
const indentString = ' '.repeat(indent);
this.sourceLines.push(`${indentString}${content}`);
}
protected emitLines(indent: number, contents: SourceLike[]): void {
contents.forEach((content) => this.emitLine(indent, content));
}
protected emitNewLine(): void {
this.sourceLines.push('');
}
protected emitCurlyBracketBegin(indent = 0): void {
this.emitLine(indent, '{');
}
protected emitCurlyBracketEnd(indent = 0): void {
this.emitLine(indent, '}');
}
}

Просмотреть файл

@ -0,0 +1,23 @@
import { Field } from '../types';
import { GenericCodeRenderer } from './GenericCodeRenderer';
import { RendererConfig } from './RenderConfig';
import { TypeTransformer } from './CustomTypeCollector';
export class InternalDataStructure extends GenericCodeRenderer {
constructor(
protected rendererConfig: RendererConfig,
private predefinedName: string,
private typeTransformer: TypeTransformer,
private fields: Field[]
) {
super(rendererConfig);
}
render(): void {
this.emitLine(0, `struct ${this.predefinedName}: Codable {`);
this.fields.forEach((field) => {
this.emitLine(2, `let ${field.name}: ${this.typeTransformer.transformType(field.type)}`);
});
this.emitLine(0, `}`);
}
}

Просмотреть файл

@ -0,0 +1,31 @@
export interface RendererConfig {
globalEntry: string;
pathMap: Record<string, string>;
tsCustomTypePrefix: string;
mergeCustomInterface: boolean;
headerTemplate?: string;
footerTemplate?: string;
makeFunctionPublic?: boolean;
baseIndent: number;
}
export class DefaultSwiftRendererConfig implements RendererConfig {
'globalEntry' = 'Test';
'pathMap' = {
IHtmlApi: 'htmlApi',
IImageOptionApi: 'imageOptionApi',
};
'tsCustomTypePrefix' = 'I';
'mergeCustomInterface' = true;
'headerTemplate' = '{';
'footerTemplate' = '}';
'makeFunctionPublic' = true;
'baseIndent' = 2;
}

Просмотреть файл

@ -0,0 +1,3 @@
// The idea of this one is originally from:
// https://github.com/quicktype/quicktype/blob/master/src/quicktype-core/Source.ts#L65
export type SourceLike = string;

Просмотреть файл

@ -0,0 +1,88 @@
import fs from 'fs';
import { Module } from '../types';
import { RendererConfig } from './RenderConfig';
import { InternalDataStructure } from './InternalDataStructure';
import { GenericCodeRenderer } from './GenericCodeRenderer';
import { TypeTransformer } from './CustomTypeCollector';
export class SwiftCodeRenderer extends GenericCodeRenderer {
constructor(
rendererConfig: RendererConfig,
private typeTransformer: TypeTransformer,
private parsedModules: Module[],
private outputPath: string
) {
super(rendererConfig);
}
render(): void {
const apiModules = this.parsedModules;
const { baseIndent } = this.rendererConfig;
apiModules.forEach((module) => {
this.namespaces.push(module.name);
this.emitLine(0 + baseIndent, `// ${module.name}`);
module.methods.forEach((method) => {
const source = `${method.name}`;
const parameterSources = method.parameters.map(
(parameter) => `${parameter.name}: ${this.typeTransformer.transformType(parameter.type)}`
);
let returnTypeStatement: string;
if (method.returnType !== null) {
const returnTypeString = this.typeTransformer.transformType(method.returnType, true);
returnTypeStatement = `@escaping (BridgeCompletion<${returnTypeString}?>)`;
} else {
returnTypeStatement = `BridgeJSExecutor.Completion? = nil`;
}
parameterSources.push(`completion: ${returnTypeStatement})`);
const parameterSourceStatement = parameterSources.join(', ');
const modifier = this.rendererConfig.makeFunctionPublic ? 'public ' : '';
this.emitLine(0 + baseIndent, `${modifier}func ${source}(${parameterSourceStatement}`);
this.emitCurlyBracketBegin(baseIndent);
const hasParam = method.parameters.length > 0;
if (hasParam) {
const argSource = new InternalDataStructure(
this.rendererConfig,
'Args',
this.typeTransformer,
method.parameters
).toSourceCode();
this.emitLines(2 + baseIndent, argSource);
this.emitLine(2 + baseIndent, `let args = Args(`);
method.parameters.forEach((param) => this.emitLine(4 + baseIndent, `${param.name}: ${param.name}`));
this.emitLine(2 + baseIndent, `)`);
}
const executeMethod = method.returnType !== null ? 'executeFetch' : 'execute';
const endpoint = this.resolveEndpoint(module.name);
this.emitLine(
2 + baseIndent,
`jsExecutor.${executeMethod}(with: "${endpoint}", feature: "${method.name}", args: ${
hasParam ? 'args' : 'nil'
}, completion: completion)`
);
this.emitCurlyBracketEnd(baseIndent);
});
});
if (this.rendererConfig.mergeCustomInterface) {
this.emitLines(0 + baseIndent, this.typeTransformer.toSourceLike());
}
}
print(): void {
let content = this.toSourceCode().join('\n');
if (this.rendererConfig.headerTemplate) {
content = `${this.rendererConfig.headerTemplate}\n${content}`;
}
if (this.rendererConfig.footerTemplate) {
content = `${content}\n${this.rendererConfig.footerTemplate}`;
}
fs.writeFileSync(this.outputPath, content);
console.log('Generated api has been printed successfully');
}
}

53
src/types.ts Normal file
Просмотреть файл

@ -0,0 +1,53 @@
export interface Module {
name: string;
methods: Method[];
}
export interface Method {
name: string;
parameters: Field[];
returnType: ValueType | null;
}
export interface Field {
name: string;
type: ValueType;
}
export interface ValueType {
kind: ArrayTypeKind | CustomTypeKind | BasicTypeKind;
nullable: boolean;
}
export enum ValueTypeKindFlag {
basicType = 'basicType',
customType = 'customType',
arrayType = 'arrayType',
}
interface ValueTypeKind {
flag: ValueTypeKindFlag;
}
export interface ArrayTypeKind extends ValueTypeKind {
flag: ValueTypeKindFlag.arrayType;
elementType: ValueType;
}
export interface CustomTypeKind extends ValueTypeKind {
flag: ValueTypeKindFlag.customType;
isTypeLiteral?: boolean;
name: string;
members: Field[];
}
export interface BasicTypeKind extends ValueTypeKind {
flag: ValueTypeKindFlag.basicType;
value: BasicTypeValue;
}
export enum BasicTypeValue {
string = 'string',
number = 'number',
boolean = 'boolean',
}

19
tsconfig.json Normal file
Просмотреть файл

@ -0,0 +1,19 @@
{
"include": ["src/*.ts", "src/**/*.ts"],
"compilerOptions": {
"resolveJsonModule": true,
"target": "es6",
"lib": [
"es2019"
],
"module": "commonjs",
"outDir": "dist",
"declaration": true,
"declarationDir": "typings",
"strict": true,
"noImplicitAny": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}

3727
yarn.lock Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу