зеркало из https://github.com/microsoft/ts-gyb.git
Parse module associated named types and generate in modules (#16)
* Parse module associated named types * Generate associated types in module * Lint * Serialize associated types * Lint * Support multiple module paths * Fix demo.ts * Modify template * Improve print * Lower cased enum * Uncapitalize enum * Update template * Remove gulp * Cleanup
This commit is contained in:
Родитель
a7c6cc4a9c
Коммит
8437bf3d95
|
@ -2,6 +2,7 @@
|
||||||
// Copyright 2013-2018 Microsoft Inc.
|
// Copyright 2013-2018 Microsoft Inc.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// swiftformat:disable redundantRawValues
|
||||||
// Don't modify this file manually, it's auto generated.
|
// Don't modify this file manually, it's auto generated.
|
||||||
|
|
||||||
public class {{moduleName}} {
|
public class {{moduleName}} {
|
||||||
|
@ -34,3 +35,7 @@ public class {{moduleName}} {
|
||||||
}
|
}
|
||||||
{{/methods}}
|
{{/methods}}
|
||||||
}
|
}
|
||||||
|
{{#associatedTypes}}
|
||||||
|
|
||||||
|
{{> swift-named-type}}
|
||||||
|
{{/associatedTypes}}
|
|
@ -1,13 +1,4 @@
|
||||||
//
|
{{#custom}}
|
||||||
// Copyright 2013-2018 Microsoft Inc.
|
|
||||||
//
|
|
||||||
|
|
||||||
// swiftformat:disable redundantRawValues
|
|
||||||
// Don't modify this file manually, it's auto generated.
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
{{#customTypes}}
|
|
||||||
|
|
||||||
public struct {{typeName}}: Codable {
|
public struct {{typeName}}: Codable {
|
||||||
{{#members}}
|
{{#members}}
|
||||||
public var {{name}}: {{type}}
|
public var {{name}}: {{type}}
|
||||||
|
@ -19,12 +10,11 @@ public struct {{typeName}}: Codable {
|
||||||
{{/members}}
|
{{/members}}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{{/customTypes}}
|
{{/custom}}
|
||||||
{{#enumTypes}}
|
{{#enum}}
|
||||||
|
|
||||||
public enum {{typeName}}: {{valueType}}, Codable {
|
public enum {{typeName}}: {{valueType}}, Codable {
|
||||||
{{#members}}
|
{{#members}}
|
||||||
case {{key}} = {{{value}}}
|
case {{key}} = {{{value}}}
|
||||||
{{/members}}
|
{{/members}}
|
||||||
}
|
}
|
||||||
{{/enumTypes}}
|
{{/enum}}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// Copyright 2013-2018 Microsoft Inc.
|
||||||
|
//
|
||||||
|
|
||||||
|
// swiftformat:disable redundantRawValues
|
||||||
|
// Don't modify this file manually, it's auto generated.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
{{#.}}
|
||||||
|
|
||||||
|
{{> swift-named-type}}
|
||||||
|
{{/.}}
|
|
@ -2,6 +2,8 @@
|
||||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// swiftformat:disable redundantRawValues
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol {{moduleName}}: EditorNativeModule {
|
public protocol {{moduleName}}: EditorNativeModule {
|
||||||
|
@ -46,6 +48,7 @@ class {{customTags.bridgeName}}: NativeModuleBridge {
|
||||||
parameters = try decoder.decode(Parameters.self, from: parametersData)
|
parameters = try decoder.decode(Parameters.self, from: parametersData)
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
logAssertFail("Parameters of {{methodName}} are invalid: \(error)")
|
||||||
completion(.failure(NativeMethodError.invalidParameters(parametersData)))
|
completion(.failure(NativeMethodError.invalidParameters(parametersData)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -57,3 +60,7 @@ class {{customTags.bridgeName}}: NativeModuleBridge {
|
||||||
}
|
}
|
||||||
{{/methods}}
|
{{/methods}}
|
||||||
}
|
}
|
||||||
|
{{#associatedTypes}}
|
||||||
|
|
||||||
|
{{> swift-named-type}}
|
||||||
|
{{/associatedTypes}}
|
36
gulpfile.ts
36
gulpfile.ts
|
@ -1,36 +0,0 @@
|
||||||
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)
|
|
||||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
11
package.json
11
package.json
|
@ -3,14 +3,12 @@
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"description": "Generate Native API based on TS interface",
|
"description": "Generate Native API based on TS interface",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp build",
|
"build": "tsc",
|
||||||
"clean": "gulp clean",
|
"clean": "rm -rf dist",
|
||||||
"start": "gulp start",
|
|
||||||
"dev": "gulp dev",
|
|
||||||
"debug": "ts-node ./src/index",
|
"debug": "ts-node ./src/index",
|
||||||
"start:example": "ts-node ./src/demo/demo",
|
"start:example": "ts-node ./src/demo/demo",
|
||||||
"test": "mocha -r ts-node/register test/**/*.ts",
|
"test": "mocha -r ts-node/register test/**/*.ts",
|
||||||
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
|
"lint": "eslint ./src --ext .js,.ts",
|
||||||
"prettier:write": "prettier --write \"src/**/*.ts\"",
|
"prettier:write": "prettier --write \"src/**/*.ts\"",
|
||||||
"prettier:check": "prettier --check \"src/**/*.ts\"",
|
"prettier:check": "prettier --check \"src/**/*.ts\"",
|
||||||
"lint:fix": "npm run lint -- --fix && npm run prettier:write",
|
"lint:fix": "npm run lint -- --fix && npm run prettier:write",
|
||||||
|
@ -30,7 +28,6 @@
|
||||||
"license": "private",
|
"license": "private",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.2.18",
|
"@types/chai": "^4.2.18",
|
||||||
"@types/gulp": "^4.0.6",
|
|
||||||
"@types/mocha": "^8.2.2",
|
"@types/mocha": "^8.2.2",
|
||||||
"@types/mustache": "^4.1.1",
|
"@types/mustache": "^4.1.1",
|
||||||
"@types/sinon": "^10.0.1",
|
"@types/sinon": "^10.0.1",
|
||||||
|
@ -45,8 +42,6 @@
|
||||||
"eslint-config-airbnb-base": "^14.2.1",
|
"eslint-config-airbnb-base": "^14.2.1",
|
||||||
"eslint-config-prettier": "^7.0.0",
|
"eslint-config-prettier": "^7.0.0",
|
||||||
"eslint-plugin-import": "^2.22.1",
|
"eslint-plugin-import": "^2.22.1",
|
||||||
"gulp": "^4.0.2",
|
|
||||||
"gulp-typescript": "^6.0.0-alpha.1",
|
|
||||||
"mocha": "^8.4.0",
|
"mocha": "^8.4.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"sinon": "^11.1.1",
|
"sinon": "^11.1.1",
|
||||||
|
|
|
@ -2,13 +2,16 @@ import { CodeGenerator, RenderingLanguage } from '../generator/CodeGenerator';
|
||||||
|
|
||||||
function run(): void {
|
function run(): void {
|
||||||
const generator = new CodeGenerator();
|
const generator = new CodeGenerator();
|
||||||
generator.parse({ tag: 'APIs', interfacePaths: ['src/demo/data/demoApi.ts'], defaultCustomTags: {}, dropInterfaceIPrefix: true });
|
generator.parse({
|
||||||
generator.render({
|
interfacePaths: ['src/demo/data/demoApi.ts'],
|
||||||
tag: 'APIs',
|
defaultCustomTags: {},
|
||||||
|
dropInterfaceIPrefix: true,
|
||||||
|
});
|
||||||
|
generator.renderModules({
|
||||||
|
index: 0,
|
||||||
language: RenderingLanguage.Swift,
|
language: RenderingLanguage.Swift,
|
||||||
outputDirectory: 'generated',
|
outputDirectory: 'generated',
|
||||||
moduleTemplatePath: 'templates/swift-bridge.mustache',
|
moduleTemplatePath: 'templates/swift-bridge.mustache',
|
||||||
namedTypesTemplatePath: 'templates/swift-named-types.mustache',
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { dropIPrefixInCustomTypes, fetchNamedTypes, NamedType } from './named-types';
|
import { dropIPrefixInCustomTypes, fetchNamedTypes, NamedType, NamedTypesResult } from './named-types';
|
||||||
import { Parser } from '../parser/Parser';
|
import { Parser } from '../parser/Parser';
|
||||||
import { renderCode } from '../renderer/renderer';
|
import { renderCode } from '../renderer/renderer';
|
||||||
import { SwiftCustomTypeView } from '../renderer/swift/SwiftCustomTypeView';
|
import { SwiftCustomTypeView } from '../renderer/swift/SwiftCustomTypeView';
|
||||||
import { SwiftEnumTypeView } from '../renderer/swift/SwiftEnumTypeView';
|
import { SwiftEnumTypeView } from '../renderer/swift/SwiftEnumTypeView';
|
||||||
import { SwiftModuleView } from '../renderer/swift/SwiftModuleView';
|
import { SwiftModuleView } from '../renderer/swift/SwiftModuleView';
|
||||||
import { CustomTypeView, EnumTypeView, ModuleView, NamedTypesView } from '../renderer/views';
|
import { CustomTypeView, EnumTypeView, ModuleView, NamedTypeView } from '../renderer/views';
|
||||||
import { serializeModule, serializeNamedType } from '../serializers';
|
import { serializeModule, serializeNamedType } from '../serializers';
|
||||||
import { CustomType, EnumType, isCustomType, Module } from '../types';
|
import { CustomType, EnumType, isCustomType, Module } from '../types';
|
||||||
import { applyDefaultCustomTags } from './utils';
|
import { applyDefaultCustomTags } from './utils';
|
||||||
|
@ -16,20 +16,18 @@ export enum RenderingLanguage {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CodeGenerator {
|
export class CodeGenerator {
|
||||||
private modulesMap: Record<string, Module[]> = {};
|
private modulesMap: Module[][] = [];
|
||||||
|
|
||||||
private namedTypes: Record<string, NamedType> = {};
|
private namedTypes?: NamedTypesResult;
|
||||||
|
|
||||||
parse({
|
parse({
|
||||||
tag,
|
|
||||||
interfacePaths,
|
interfacePaths,
|
||||||
defaultCustomTags,
|
defaultCustomTags,
|
||||||
dropInterfaceIPrefix,
|
dropInterfaceIPrefix,
|
||||||
}: {
|
}: {
|
||||||
tag: string;
|
|
||||||
interfacePaths: string[];
|
interfacePaths: string[];
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
defaultCustomTags: Record<string, any>,
|
defaultCustomTags: Record<string, any>;
|
||||||
dropInterfaceIPrefix: boolean;
|
dropInterfaceIPrefix: boolean;
|
||||||
}): void {
|
}): void {
|
||||||
const parser = new Parser(interfacePaths);
|
const parser = new Parser(interfacePaths);
|
||||||
|
@ -41,59 +39,80 @@ export class CodeGenerator {
|
||||||
dropIPrefixInCustomTypes(modules);
|
dropIPrefixInCustomTypes(modules);
|
||||||
}
|
}
|
||||||
|
|
||||||
const namedTypes = fetchNamedTypes(modules);
|
this.modulesMap.push(modules);
|
||||||
|
|
||||||
this.modulesMap[tag] = modules;
|
|
||||||
this.pushNamedTypes(namedTypes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
printModules({ tag }: { tag: string }): void {
|
parseNamedTypes(): void {
|
||||||
const modules = this.modulesMap[tag];
|
this.namedTypes = fetchNamedTypes(Object.values(this.modulesMap).flatMap((modules) => modules));
|
||||||
|
}
|
||||||
|
|
||||||
|
printModules(index: number): void {
|
||||||
|
const modules = this.modulesMap[index];
|
||||||
if (modules === undefined) {
|
if (modules === undefined) {
|
||||||
throw Error('Modules not parsed. Run parse first.');
|
throw Error('Modules not parsed. Run parse first.');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Modules:\n');
|
console.log('Modules:\n');
|
||||||
console.log(modules.map((module) => serializeModule(module)).join('\n\n'));
|
|
||||||
}
|
|
||||||
|
|
||||||
printNamedTypes(): void {
|
|
||||||
console.log('\nNamed types:\n');
|
|
||||||
console.log(
|
console.log(
|
||||||
Object.entries(this.namedTypes)
|
modules.map((module) => serializeModule(module, this.namedTypes?.associatedTypes[module.name] ?? [])).join('\n\n')
|
||||||
.map(([typeName, namedType]) => serializeNamedType(typeName, namedType))
|
|
||||||
.join('\n\n')
|
|
||||||
);
|
);
|
||||||
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
render({
|
printSharedNamedTypes(): void {
|
||||||
tag,
|
if (this.namedTypes === undefined) {
|
||||||
|
throw Error('Named types not parsed. Run parseNamedTypes first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Shared named types:\n');
|
||||||
|
console.log(this.namedTypes.sharedTypes.map((namedType) => serializeNamedType(namedType)).join('\n\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
renderModules({
|
||||||
|
index,
|
||||||
language,
|
language,
|
||||||
outputDirectory,
|
outputDirectory,
|
||||||
moduleTemplatePath,
|
moduleTemplatePath,
|
||||||
namedTypesTemplatePath,
|
|
||||||
}: {
|
}: {
|
||||||
tag: string;
|
index: number;
|
||||||
language: RenderingLanguage;
|
language: RenderingLanguage;
|
||||||
outputDirectory: string;
|
outputDirectory: string;
|
||||||
moduleTemplatePath: string;
|
moduleTemplatePath: string;
|
||||||
namedTypesTemplatePath: string;
|
|
||||||
}): void {
|
}): void {
|
||||||
const modules = this.modulesMap[tag];
|
const modules = this.modulesMap[index];
|
||||||
if (modules === undefined) {
|
if (modules === undefined) {
|
||||||
throw Error('Modules not parsed. Run parse first.');
|
throw Error('Modules not parsed. Run parse first.');
|
||||||
}
|
}
|
||||||
|
if (this.namedTypes === undefined) {
|
||||||
|
throw Error('Named types not parsed. Run parseNamedTypes first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { associatedTypes } = this.namedTypes;
|
||||||
|
|
||||||
modules.forEach((module) => {
|
modules.forEach((module) => {
|
||||||
const moduleView = this.getModuleView(language, module);
|
const moduleView = this.getModuleView(language, module, associatedTypes[module.name] ?? []);
|
||||||
const renderedCode = renderCode(moduleTemplatePath, moduleView);
|
const renderedCode = renderCode(moduleTemplatePath, moduleView);
|
||||||
|
|
||||||
this.writeFile(renderedCode, outputDirectory, `${moduleView.moduleName}${this.getFileExtension(language)}`);
|
this.writeFile(renderedCode, outputDirectory, `${moduleView.moduleName}${this.getFileExtension(language)}`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const namedTypesView = this.getNamedTypesView(language, this.namedTypes);
|
renderNamedTypes({
|
||||||
|
language,
|
||||||
|
namedTypesTemplatePath,
|
||||||
|
namedTypesOutputPath,
|
||||||
|
}: {
|
||||||
|
language: RenderingLanguage;
|
||||||
|
namedTypesTemplatePath: string;
|
||||||
|
namedTypesOutputPath: string;
|
||||||
|
}): void {
|
||||||
|
if (this.namedTypes === undefined) {
|
||||||
|
throw Error('Named types not parsed. Run parseNamedTypes first.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const namedTypesView = this.namedTypes.sharedTypes.map((namedType) => this.getNamedTypeView(language, namedType));
|
||||||
const renderedCode = renderCode(namedTypesTemplatePath, namedTypesView);
|
const renderedCode = renderCode(namedTypesTemplatePath, namedTypesView);
|
||||||
this.writeFile(renderedCode, outputDirectory, `Generated_CustomInterface${this.getFileExtension(language)}`);
|
fs.writeFileSync(namedTypesOutputPath, renderedCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getFileExtension(language: RenderingLanguage): string {
|
private getFileExtension(language: RenderingLanguage): string {
|
||||||
|
@ -105,24 +124,26 @@ export class CodeGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNamedTypesView(language: RenderingLanguage, namedTypes: Record<string, NamedType>): NamedTypesView {
|
private getNamedTypeView(language: RenderingLanguage, namedType: NamedType): NamedTypeView {
|
||||||
const namedTypesView: NamedTypesView = { customTypes: [], enumTypes: [] };
|
let namedTypeView: NamedTypeView;
|
||||||
|
if (isCustomType(namedType)) {
|
||||||
|
namedTypeView = this.getCustomTypeView(language, namedType.name, namedType);
|
||||||
|
namedTypeView.custom = true;
|
||||||
|
} else {
|
||||||
|
namedTypeView = this.getEnumTypeView(language, namedType);
|
||||||
|
namedTypeView.enum = true;
|
||||||
|
}
|
||||||
|
|
||||||
Object.entries(namedTypes).forEach(([typeName, namedType]) => {
|
return namedTypeView;
|
||||||
if (isCustomType(namedType)) {
|
|
||||||
namedTypesView.customTypes.push(this.getCustomTypeView(language, typeName, namedType));
|
|
||||||
} else {
|
|
||||||
namedTypesView.enumTypes.push(this.getEnumTypeView(language, typeName, namedType));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return namedTypesView;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getModuleView(language: RenderingLanguage, module: Module): ModuleView {
|
private getModuleView(language: RenderingLanguage, module: Module, associatedTypes: NamedType[]): ModuleView {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case RenderingLanguage.Swift:
|
case RenderingLanguage.Swift:
|
||||||
return new SwiftModuleView(module);
|
return new SwiftModuleView(
|
||||||
|
module,
|
||||||
|
associatedTypes.map((associatedType) => this.getNamedTypeView(language, associatedType))
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
throw Error('Unhandled language');
|
throw Error('Unhandled language');
|
||||||
}
|
}
|
||||||
|
@ -137,10 +158,10 @@ export class CodeGenerator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEnumTypeView(language: RenderingLanguage, typeName: string, enumType: EnumType): EnumTypeView {
|
private getEnumTypeView(language: RenderingLanguage, enumType: EnumType): EnumTypeView {
|
||||||
switch (language) {
|
switch (language) {
|
||||||
case RenderingLanguage.Swift:
|
case RenderingLanguage.Swift:
|
||||||
return new SwiftEnumTypeView(typeName, enumType);
|
return new SwiftEnumTypeView(enumType);
|
||||||
default:
|
default:
|
||||||
throw Error('Unhandled language');
|
throw Error('Unhandled language');
|
||||||
}
|
}
|
||||||
|
@ -150,14 +171,4 @@ export class CodeGenerator {
|
||||||
const filePath = path.join(outputDirectory, fileName);
|
const filePath = path.join(outputDirectory, fileName);
|
||||||
fs.writeFileSync(filePath, content);
|
fs.writeFileSync(filePath, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private pushNamedTypes(namedTypes: Record<string, NamedType>): void {
|
|
||||||
Object.entries(namedTypes).forEach(([typeName, namedType]) => {
|
|
||||||
if (this.namedTypes[typeName] !== undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.namedTypes[typeName] = namedType;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { capitalize } from '../utils';
|
import { capitalize } from '../utils';
|
||||||
import {
|
import {
|
||||||
CustomType,
|
|
||||||
EnumType,
|
|
||||||
isArraryType,
|
isArraryType,
|
||||||
isCustomType,
|
isCustomType,
|
||||||
isDictionaryType,
|
isDictionaryType,
|
||||||
|
@ -9,53 +7,74 @@ import {
|
||||||
isOptionalType,
|
isOptionalType,
|
||||||
Module,
|
Module,
|
||||||
ValueType,
|
ValueType,
|
||||||
|
CustomType,
|
||||||
|
EnumType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export type NamedType = CustomType | EnumType;
|
export type NamedType = (CustomType & { name: string }) | EnumType;
|
||||||
|
export type NamedTypesResult = { associatedTypes: Record<string, NamedType[]>; sharedTypes: NamedType[] };
|
||||||
|
|
||||||
export function dropIPrefixInCustomTypes(modules: Module[]): void {
|
export function dropIPrefixInCustomTypes(modules: Module[]): void {
|
||||||
fetchRootTypes(modules).forEach((valueType) => {
|
modules
|
||||||
recursiveVisitNamedType(valueType, (namedType) => {
|
.flatMap((module) => fetchRootTypes(module))
|
||||||
if (!isCustomType(namedType)) {
|
.forEach((valueType) => {
|
||||||
return;
|
recursiveVisitNamedType(valueType, (namedType) => {
|
||||||
}
|
if (!isCustomType(namedType)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
namedType.name = namedType.name?.replace(/^I/, '');
|
namedType.name = namedType.name?.replace(/^I/, '');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchNamedTypes(modules: Module[]): Record<string, NamedType> {
|
export function fetchNamedTypes(modules: Module[]): NamedTypesResult {
|
||||||
const typeMap: Record<string, NamedType> = {};
|
const typeMap: Record<string, { namedType: NamedType; associatedModules: Set<string> }> = {};
|
||||||
|
|
||||||
fetchRootTypes(modules).forEach((valueType) => {
|
modules.forEach((module) => {
|
||||||
recursiveVisitNamedType(valueType, (namedType, path) => {
|
fetchRootTypes(module).forEach((valueType) => {
|
||||||
if (namedType.name === undefined) {
|
recursiveVisitNamedType(valueType, (namedType, path) => {
|
||||||
namedType.name = path;
|
if (namedType.name === undefined) {
|
||||||
}
|
namedType.name = path;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeMap[namedType.name] !== undefined) {
|
if (typeMap[namedType.name] === undefined) {
|
||||||
return;
|
typeMap[namedType.name] = { namedType: namedType as NamedType, associatedModules: new Set() };
|
||||||
}
|
}
|
||||||
|
|
||||||
typeMap[namedType.name] = namedType;
|
typeMap[namedType.name].associatedModules.add(module.name);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return typeMap;
|
const associatedTypes: Record<string, NamedType[]> = {};
|
||||||
|
const sharedTypes: NamedType[] = [];
|
||||||
|
|
||||||
|
Object.values(typeMap).forEach(({ namedType, associatedModules }) => {
|
||||||
|
if (associatedModules.size === 1) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
const moduleName: string = associatedModules.values().next().value;
|
||||||
|
if (associatedTypes[moduleName] === undefined) {
|
||||||
|
associatedTypes[moduleName] = [];
|
||||||
|
}
|
||||||
|
associatedTypes[moduleName].push(namedType);
|
||||||
|
} else {
|
||||||
|
sharedTypes.push(namedType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { associatedTypes, sharedTypes };
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchRootTypes(modules: Module[]): ValueType[] {
|
function fetchRootTypes(module: Module): ValueType[] {
|
||||||
return modules
|
return module.methods.flatMap((method) =>
|
||||||
.flatMap((module) => module.methods)
|
method.parameters.map((parameter) => parameter.type).concat(method.returnType ? [method.returnType] : [])
|
||||||
.flatMap((method) =>
|
);
|
||||||
method.parameters.map((parameter) => parameter.type).concat(method.returnType ? [method.returnType] : [])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recursiveVisitNamedType(
|
function recursiveVisitNamedType(
|
||||||
valueType: ValueType,
|
valueType: ValueType,
|
||||||
visit: (namedType: NamedType, path: string) => void,
|
visit: (namedType: CustomType | EnumType, path: string) => void,
|
||||||
path = ''
|
path = ''
|
||||||
): void {
|
): void {
|
||||||
if (isCustomType(valueType)) {
|
if (isCustomType(valueType)) {
|
||||||
|
|
57
src/index.ts
57
src/index.ts
|
@ -2,27 +2,33 @@ import yargs from 'yargs';
|
||||||
import { CodeGenerator, RenderingLanguage } from './generator/CodeGenerator';
|
import { CodeGenerator, RenderingLanguage } from './generator/CodeGenerator';
|
||||||
import { parseKeyValueText } from './utils';
|
import { parseKeyValueText } from './utils';
|
||||||
|
|
||||||
|
interface Config {
|
||||||
|
moduleGenerationMaps: { interfacePaths: string[]; moduleTemplatePath: string; outputDirectory: string }[];
|
||||||
|
namedTypesTemplatePath: string;
|
||||||
|
namedTypesOutputPath: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
defaultCustomTags?: any;
|
||||||
|
dropInterfaceIPrefix?: boolean;
|
||||||
|
}
|
||||||
const program = yargs(process.argv.slice(2));
|
const program = yargs(process.argv.slice(2));
|
||||||
|
|
||||||
const args = program
|
const args = program
|
||||||
|
.config()
|
||||||
.options({
|
.options({
|
||||||
interfacePaths: {
|
interfacePaths: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
array: true,
|
array: true,
|
||||||
demandOption: true,
|
|
||||||
describe: 'The path of api interface which should extend IExportedApi',
|
describe: 'The path of api interface which should extend IExportedApi',
|
||||||
},
|
},
|
||||||
outputDirectory: {
|
outputDirectory: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
demandOption: true,
|
|
||||||
describe: 'The path of output directory',
|
describe: 'The path of output directory',
|
||||||
},
|
},
|
||||||
moduleTemplatePath: {
|
moduleTemplatePath: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
demandOption: true,
|
|
||||||
describe: 'The path of module template',
|
describe: 'The path of module template',
|
||||||
},
|
},
|
||||||
namedTypesTemplate: {
|
namedTypesTemplatePath: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
demandOption: true,
|
demandOption: true,
|
||||||
describe: 'The path of named types template',
|
describe: 'The path of named types template',
|
||||||
|
@ -30,7 +36,7 @@ const args = program
|
||||||
defaultCustomTag: {
|
defaultCustomTag: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
array: true,
|
array: true,
|
||||||
coerce: (values: string[]) => values.map(tagString => parseKeyValueText(tagString)),
|
coerce: (values: string[]) => values.map((tagString) => parseKeyValueText(tagString)),
|
||||||
default: [],
|
default: [],
|
||||||
describe: 'Default values for custom tags',
|
describe: 'Default values for custom tags',
|
||||||
},
|
},
|
||||||
|
@ -43,20 +49,39 @@ const args = program
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|
||||||
function run(): void {
|
function run(): void {
|
||||||
|
const config = args as unknown as Config;
|
||||||
|
|
||||||
const generator = new CodeGenerator();
|
const generator = new CodeGenerator();
|
||||||
generator.parse({
|
config.moduleGenerationMaps.forEach((moduleGenerationMap, index) => {
|
||||||
tag: 'APIs',
|
generator.parse({
|
||||||
interfacePaths: args.interfacePaths,
|
interfacePaths: moduleGenerationMap.interfacePaths,
|
||||||
defaultCustomTags: Object.fromEntries(args.defaultCustomTag.map(tag => [tag.key, tag.value])),
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
dropInterfaceIPrefix: args.dropInterfaceIPrefix,
|
defaultCustomTags: config.defaultCustomTags ?? {},
|
||||||
|
dropInterfaceIPrefix: config.dropInterfaceIPrefix ?? false,
|
||||||
|
});
|
||||||
|
generator.printModules(index);
|
||||||
});
|
});
|
||||||
generator.printModules({ tag: 'APIs' });
|
|
||||||
generator.render({
|
generator.parseNamedTypes();
|
||||||
tag: 'APIs',
|
generator.printSharedNamedTypes();
|
||||||
|
|
||||||
|
config.moduleGenerationMaps.forEach((_, index) => {
|
||||||
|
generator.printModules(index);
|
||||||
|
});
|
||||||
|
|
||||||
|
config.moduleGenerationMaps.forEach((moduleGenerationMap, index) => {
|
||||||
|
generator.renderModules({
|
||||||
|
index,
|
||||||
|
language: RenderingLanguage.Swift,
|
||||||
|
outputDirectory: moduleGenerationMap.outputDirectory,
|
||||||
|
moduleTemplatePath: moduleGenerationMap.moduleTemplatePath,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
generator.renderNamedTypes({
|
||||||
language: RenderingLanguage.Swift,
|
language: RenderingLanguage.Swift,
|
||||||
outputDirectory: args.outputDirectory,
|
namedTypesTemplatePath: config.namedTypesTemplatePath,
|
||||||
moduleTemplatePath: args.moduleTemplatePath,
|
namedTypesOutputPath: config.namedTypesOutputPath,
|
||||||
namedTypesTemplatePath: args.namedTypesTemplate,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,11 @@ export class Parser {
|
||||||
parse(): Module[] {
|
parse(): Module[] {
|
||||||
const modules: Module[] = [];
|
const modules: Module[] = [];
|
||||||
|
|
||||||
this.program.getSourceFiles().forEach((sourceFile) => {
|
this.program.getRootFileNames().forEach((fileName) => {
|
||||||
|
const sourceFile = this.program.getSourceFile(fileName);
|
||||||
|
if (sourceFile === undefined) {
|
||||||
|
throw Error('Source file not found');
|
||||||
|
}
|
||||||
ts.forEachChild(sourceFile, (node) => {
|
ts.forEachChild(sourceFile, (node) => {
|
||||||
const module = this.moduleFromNode(node);
|
const module = this.moduleFromNode(node);
|
||||||
if (module !== null) {
|
if (module !== null) {
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
|
|
||||||
export function renderCode<View>(templatePath: string, view: View): string {
|
export function renderCode<View>(templatePath: string, view: View): string {
|
||||||
const template = fs.readFileSync(templatePath).toString();
|
const template = fs.readFileSync(templatePath).toString();
|
||||||
return Mustache.render(template, view);
|
const directory = path.dirname(templatePath);
|
||||||
|
return Mustache.render(template, view, (partialName) => {
|
||||||
|
const partialPath = path.join(directory, `${partialName}.mustache`);
|
||||||
|
return fs.readFileSync(partialPath).toString();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,13 @@
|
||||||
|
import { uncapitalize } from '../../utils';
|
||||||
import { EnumSubType, EnumType } from '../../types';
|
import { EnumSubType, EnumType } from '../../types';
|
||||||
import { EnumTypeView } from '../views';
|
import { EnumTypeView } from '../views';
|
||||||
|
|
||||||
export class SwiftEnumTypeView implements EnumTypeView {
|
export class SwiftEnumTypeView implements EnumTypeView {
|
||||||
constructor(readonly typeName: string, private enumType: EnumType) {}
|
constructor(private enumType: EnumType) {}
|
||||||
|
|
||||||
|
get typeName(): string {
|
||||||
|
return this.enumType.name;
|
||||||
|
}
|
||||||
|
|
||||||
get valueType(): string {
|
get valueType(): string {
|
||||||
switch (this.enumType.subType) {
|
switch (this.enumType.subType) {
|
||||||
|
@ -17,7 +22,8 @@ export class SwiftEnumTypeView implements EnumTypeView {
|
||||||
|
|
||||||
get members(): { key: string; value: string }[] {
|
get members(): { key: string; value: string }[] {
|
||||||
return Object.entries(this.enumType.members).map(([key, value]) => ({
|
return Object.entries(this.enumType.members).map(([key, value]) => ({
|
||||||
key,
|
// TODO: Convert to camel case instead of uncapitalize
|
||||||
|
key: uncapitalize(key),
|
||||||
value: typeof value === 'string' ? `"${value}"` : `${value}`,
|
value: typeof value === 'string' ? `"${value}"` : `${value}`,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Module } from '../../types';
|
import { Module } from '../../types';
|
||||||
import { ModuleView, MethodView } from '../views';
|
import { ModuleView, MethodView, NamedTypeView } from '../views';
|
||||||
import { SwiftMethodView } from './SwiftMethodView';
|
import { SwiftMethodView } from './SwiftMethodView';
|
||||||
|
|
||||||
export class SwiftModuleView implements ModuleView {
|
export class SwiftModuleView implements ModuleView {
|
||||||
constructor(private module: Module) {}
|
constructor(private readonly module: Module, readonly associatedTypes: NamedTypeView[]) {}
|
||||||
|
|
||||||
get moduleName(): string {
|
get moduleName(): string {
|
||||||
return this.module.name;
|
return this.module.name;
|
||||||
|
|
|
@ -9,13 +9,11 @@ export interface MethodView {
|
||||||
export interface ModuleView {
|
export interface ModuleView {
|
||||||
readonly moduleName: string;
|
readonly moduleName: string;
|
||||||
readonly methods: MethodView[];
|
readonly methods: MethodView[];
|
||||||
|
readonly associatedTypes: NamedTypeView[];
|
||||||
readonly customTags: Record<string, string>;
|
readonly customTags: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NamedTypesView {
|
export type NamedTypeView = (CustomTypeView | EnumTypeView) & { custom?: boolean; enum?: boolean };
|
||||||
readonly customTypes: CustomTypeView[];
|
|
||||||
readonly enumTypes: EnumTypeView[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CustomTypeView {
|
export interface CustomTypeView {
|
||||||
readonly typeName: string;
|
readonly typeName: string;
|
||||||
|
|
|
@ -18,7 +18,8 @@ const typeColor = chalk.yellow;
|
||||||
const valueColor = chalk.cyan;
|
const valueColor = chalk.cyan;
|
||||||
const documentationColor = chalk.gray;
|
const documentationColor = chalk.gray;
|
||||||
|
|
||||||
export function serializeModule(module: Module): string {
|
export function serializeModule(module: Module, associatedTypes: NamedType[]): string {
|
||||||
|
const serializedAssociatedTypes = associatedTypes.map((associatedType) => serializeNamedType(associatedType));
|
||||||
const customTags =
|
const customTags =
|
||||||
Object.keys(module.customTags).length > 0 ? `Custom tags: ${JSON.stringify(module.customTags)}\n` : '';
|
Object.keys(module.customTags).length > 0 ? `Custom tags: ${JSON.stringify(module.customTags)}\n` : '';
|
||||||
|
|
||||||
|
@ -32,13 +33,21 @@ ${module.methods
|
||||||
.map((line) => ` ${line}`)
|
.map((line) => ` ${line}`)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
)
|
)
|
||||||
.join('\n')}
|
.join('\n')}${
|
||||||
|
serializedAssociatedTypes.length > 0
|
||||||
|
? `\n\n${serializedAssociatedTypes
|
||||||
|
.join('\n')
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => ` ${line}`)
|
||||||
|
.join('\n')}`
|
||||||
|
: ''
|
||||||
|
}
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeNamedType(typeName: string, namedType: NamedType): string {
|
export function serializeNamedType(namedType: NamedType): string {
|
||||||
if (isCustomType(namedType)) {
|
if (isCustomType(namedType)) {
|
||||||
return `${keywordColor('Type')} ${typeName} {
|
return `${keywordColor('Type')} ${namedType.name} {
|
||||||
${namedType.members
|
${namedType.members
|
||||||
.map(
|
.map(
|
||||||
(member) =>
|
(member) =>
|
||||||
|
@ -48,7 +57,7 @@ ${namedType.members
|
||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${keywordColor('Enum')} ${typeName} {
|
return `${keywordColor('Enum')} ${namedType.name} {
|
||||||
${Object.entries(namedType.members)
|
${Object.entries(namedType.members)
|
||||||
.map(([key, value]) => ` ${identifierColor(key)} = ${valueColor(value)}`)
|
.map(([key, value]) => ` ${identifierColor(key)} = ${valueColor(value)}`)
|
||||||
.join('\n')}
|
.join('\n')}
|
||||||
|
|
12
src/utils.ts
12
src/utils.ts
|
@ -6,8 +6,16 @@ export function capitalize(text: string): string {
|
||||||
return text[0].toUpperCase() + text.slice(1);
|
return text[0].toUpperCase() + text.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uncapitalize(text: string): string {
|
||||||
|
if (text.length === 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text[0].toLowerCase() + text.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function parseKeyValueText(text: string): { key: string, value: any } {
|
export function parseKeyValueText(text: string): [string, any] {
|
||||||
const index = text.indexOf('=');
|
const index = text.indexOf('=');
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
throw Error('Invalid custom tag');
|
throw Error('Invalid custom tag');
|
||||||
|
@ -24,5 +32,5 @@ export function parseKeyValueText(text: string): { key: string, value: any } {
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
return { key, value };
|
return [key, value];
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ describe('Parser', () => {
|
||||||
`;
|
`;
|
||||||
withTempParser(exportedTrueSourceCode, parser => {
|
withTempParser(exportedTrueSourceCode, parser => {
|
||||||
const modules = parser.parse();
|
const modules = parser.parse();
|
||||||
expect(modules).to.deep.equal([{name: 'ExportTrueInterface', methods: [], documentation: ''}]);
|
expect(modules).to.deep.equal([{name: 'ExportTrueInterface', methods: [], documentation: '', customTags: {}}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,7 +52,8 @@ describe('Parser', () => {
|
||||||
returnType: null,
|
returnType: null,
|
||||||
documentation: 'This is an example documentation for the method',
|
documentation: 'This is an example documentation for the method',
|
||||||
}],
|
}],
|
||||||
documentation: 'This is an example documentation for the module'
|
documentation: 'This is an example documentation for the module',
|
||||||
|
customTags: {},
|
||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -71,7 +72,7 @@ describe('Parser', () => {
|
||||||
const stubWarn = sinon.stub(console, 'warn');
|
const stubWarn = sinon.stub(console, 'warn');
|
||||||
|
|
||||||
const modules = parser.parse();
|
const modules = parser.parse();
|
||||||
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: ''}]);
|
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: '', customTags: {}}]);
|
||||||
|
|
||||||
const expectedWarning = warnMessage(`Skipped "invalidProperty: string;" at ${filePath}:5 because it is not valid method signature. Please define only methods.`);
|
const expectedWarning = warnMessage(`Skipped "invalidProperty: string;" at ${filePath}:5 because it is not valid method signature. Please define only methods.`);
|
||||||
expect(stubWarn).to.have.been.calledWith(expectedWarning);
|
expect(stubWarn).to.have.been.calledWith(expectedWarning);
|
||||||
|
@ -94,7 +95,7 @@ describe('Parser', () => {
|
||||||
const stubWarn = sinon.stub(console, 'warn');
|
const stubWarn = sinon.stub(console, 'warn');
|
||||||
|
|
||||||
const modules = parser.parse();
|
const modules = parser.parse();
|
||||||
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: ''}]);
|
expect(modules).to.deep.equal([{name: 'MockedInterface', methods: [], documentation: '', customTags: {}}]);
|
||||||
|
|
||||||
const expectedWarning = warnMessage(`Skipped "multipleParamsMethod(foo: string, bar: number);" at ${filePath}:5 because it has multiple parameters. Methods should only have one property. Please use destructuring object for multiple parameters.`);
|
const expectedWarning = warnMessage(`Skipped "multipleParamsMethod(foo: string, bar: number);" at ${filePath}:5 because it has multiple parameters. Methods should only have one property. Please use destructuring object for multiple parameters.`);
|
||||||
expect(stubWarn).to.have.been.calledWith(expectedWarning);
|
expect(stubWarn).to.have.been.calledWith(expectedWarning);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"include": ["src/*.ts", "src/**/*.ts", "test/*.ts", "test/**/*.ts"],
|
"include": ["src/*.ts", "src/**/*.ts"],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"target": "es6",
|
"target": "es6",
|
||||||
|
|
3727
yarn.lock
3727
yarn.lock
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
Загрузка…
Ссылка в новой задаче