@tutao/licc 3.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -0
- package/cli.js +0 -0
- package/dist/Accumulator.js +23 -0
- package/dist/KotlinGenerator.js +224 -0
- package/dist/Parser.js +49 -0
- package/dist/SwiftGenerator.js +232 -0
- package/dist/TypescriptGenerator.js +199 -0
- package/dist/cli.js +83 -0
- package/dist/common.js +18 -0
- package/dist/index.js +118 -0
- package/dist/tsbuildinfo +1 -0
- package/lib/Accumulator.ts +30 -0
- package/lib/KotlinGenerator.ts +241 -0
- package/lib/Parser.ts +68 -0
- package/lib/SwiftGenerator.ts +248 -0
- package/lib/TypescriptGenerator.ts +222 -0
- package/lib/cli.ts +94 -0
- package/lib/common.ts +91 -0
- package/lib/index.ts +126 -0
- package/package.json +25 -0
- package/test/ParserTest.ts +124 -0
- package/test/Suite.ts +6 -0
- package/test/tsconfig.json +19 -0
- package/tsconfig.json +111 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import {Accumulator} from "./Accumulator.js"
|
|
2
|
+
import {FacadeDefinition, getArgs, LangGenerator, minusculize, RenderedType, StructDefinition, TypeRefDefinition} from "./common.js"
|
|
3
|
+
import {ParsedType, parseType} from "./Parser.js"
|
|
4
|
+
import path from "path"
|
|
5
|
+
|
|
6
|
+
export class TypescriptGenerator implements LangGenerator {
|
|
7
|
+
|
|
8
|
+
generateGlobalDispatcher(name: string, facadeNames: Array<string>): string {
|
|
9
|
+
const acc = new Accumulator()
|
|
10
|
+
for (let facadeName of facadeNames) {
|
|
11
|
+
acc.line(`import {${facadeName}} from "./${facadeName}.js"`)
|
|
12
|
+
acc.line(`import {${facadeName}ReceiveDispatcher} from "./${facadeName}ReceiveDispatcher.js"`)
|
|
13
|
+
}
|
|
14
|
+
acc.line()
|
|
15
|
+
acc.line(`export class ${name} {`)
|
|
16
|
+
const methodAcc = acc.indent()
|
|
17
|
+
for (let facadeName of facadeNames) {
|
|
18
|
+
methodAcc.line(`private readonly ${minusculize(facadeName)} : ${facadeName}ReceiveDispatcher`)
|
|
19
|
+
}
|
|
20
|
+
methodAcc.line(`constructor(`)
|
|
21
|
+
for (let facadeName of facadeNames) {
|
|
22
|
+
methodAcc.indent().line(`${minusculize(facadeName)} : ${facadeName},`)
|
|
23
|
+
}
|
|
24
|
+
methodAcc.line(`) {`)
|
|
25
|
+
for (let facadeName of facadeNames) {
|
|
26
|
+
methodAcc.indent().line(`this.${minusculize(facadeName)} = new ${facadeName}ReceiveDispatcher(${minusculize(facadeName)})`)
|
|
27
|
+
}
|
|
28
|
+
methodAcc.line("}")
|
|
29
|
+
methodAcc.line()
|
|
30
|
+
|
|
31
|
+
methodAcc.line(`async dispatch(facadeName: string, methodName: string, args: Array<any>) {`)
|
|
32
|
+
const switchAcc = methodAcc.indent()
|
|
33
|
+
switchAcc.line(`switch (facadeName) {`)
|
|
34
|
+
const caseAcc = switchAcc.indent()
|
|
35
|
+
for (let facadeName of facadeNames) {
|
|
36
|
+
caseAcc.line(`case "${facadeName}":`)
|
|
37
|
+
caseAcc.indent().line(`return this.${minusculize(facadeName)}.dispatch(methodName, args)`)
|
|
38
|
+
}
|
|
39
|
+
caseAcc.line(`default:`)
|
|
40
|
+
caseAcc.indent().line(`throw new Error("licc messed up! " + facadeName)`)
|
|
41
|
+
switchAcc.line(`}`)
|
|
42
|
+
methodAcc.line(`}`)
|
|
43
|
+
acc.line(`}`)
|
|
44
|
+
return acc.finish()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handleStructDefinition(definition: StructDefinition): string {
|
|
48
|
+
let acc = new Accumulator()
|
|
49
|
+
this.generateDocComment(acc, definition.doc)
|
|
50
|
+
acc.line(`export interface ${definition.name} {`)
|
|
51
|
+
let bodyGenerator = acc.indent()
|
|
52
|
+
for (const [fieldName, fieldType] of Object.entries(definition.fields)) {
|
|
53
|
+
const {name, externals} = typeNameTypescript(fieldType)
|
|
54
|
+
for (const external of externals) {
|
|
55
|
+
acc.addImport(`import {${external}} from "./${external}.js"`)
|
|
56
|
+
}
|
|
57
|
+
bodyGenerator.line(`readonly ${fieldName}: ${name}`)
|
|
58
|
+
}
|
|
59
|
+
acc.line("}")
|
|
60
|
+
|
|
61
|
+
return acc.finish()
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private generateDocComment(acc: Accumulator, comment: string | null | undefined) {
|
|
66
|
+
if (!comment) return
|
|
67
|
+
|
|
68
|
+
acc.line("/**")
|
|
69
|
+
acc.line(` * ${comment}`)
|
|
70
|
+
acc.line(" */")
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private static generateNativeInterface(accumulator: Accumulator) {
|
|
74
|
+
// Duplicate interface to not import it
|
|
75
|
+
accumulator.line("interface NativeInterface {")
|
|
76
|
+
accumulator.indent().line("invokeNative(requestType: string, args: unknown[]): Promise<any>")
|
|
77
|
+
accumulator.line("}")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
generateFacade(definition: FacadeDefinition): string {
|
|
81
|
+
const acc = new Accumulator()
|
|
82
|
+
this.generateDocComment(acc, definition.doc)
|
|
83
|
+
acc.line(`export interface ${definition.name} {\n`)
|
|
84
|
+
let methodAcc = acc.indent()
|
|
85
|
+
for (const [name, method] of Object.entries(definition.methods)) {
|
|
86
|
+
this.generateDocComment(methodAcc, method.doc)
|
|
87
|
+
methodAcc.line(`${name}(`)
|
|
88
|
+
let argAccumulator = methodAcc.indent()
|
|
89
|
+
for (const arg of getArgs(name, method)) {
|
|
90
|
+
const name = renderTypeAndAddImports(arg.type, acc)
|
|
91
|
+
argAccumulator.line(`${arg.name}: ${name},`)
|
|
92
|
+
}
|
|
93
|
+
const resolvedReturnType = renderTypeAndAddImports(method.ret, acc)
|
|
94
|
+
methodAcc.line(`): Promise<${resolvedReturnType}>`)
|
|
95
|
+
methodAcc.line()
|
|
96
|
+
}
|
|
97
|
+
acc.line("}")
|
|
98
|
+
return acc.finish()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
generateReceiveDispatcher(definition: FacadeDefinition): string {
|
|
102
|
+
const acc = new Accumulator()
|
|
103
|
+
acc.line(`import {${definition.name}} from "./${definition.name}.js"`)
|
|
104
|
+
acc.line()
|
|
105
|
+
acc.line(`export class ${definition.name}ReceiveDispatcher {`)
|
|
106
|
+
acc.indent().line(`constructor(private readonly facade: ${definition.name}) {}`)
|
|
107
|
+
acc.indent().line(`async dispatch(method: string, arg: Array<any>) : Promise<any> {`)
|
|
108
|
+
acc.indent().indent().line(`switch(method) {`)
|
|
109
|
+
const switchAccumulator = acc.indent().indent().indent()
|
|
110
|
+
for (const [methodName, methodDef] of Object.entries(definition.methods)) {
|
|
111
|
+
switchAccumulator.line(`case "${methodName}": {`)
|
|
112
|
+
const arg = getArgs(methodName, methodDef)
|
|
113
|
+
const decodedArgs = []
|
|
114
|
+
for (let i = 0; i < arg.length; i++) {
|
|
115
|
+
const {name: argName, type} = arg[i]
|
|
116
|
+
const renderedArgType = renderTypeAndAddImports(type, acc)
|
|
117
|
+
decodedArgs.push([argName, renderedArgType] as const)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < arg.length; i++) {
|
|
121
|
+
const [argName, renderedType] = decodedArgs[i]
|
|
122
|
+
switchAccumulator.indent().line(`const ${argName}: ${renderedType} = arg[${i}]`)
|
|
123
|
+
}
|
|
124
|
+
switchAccumulator.indent().line(`return this.facade.${methodName}(`)
|
|
125
|
+
for (let i = 0; i < arg.length; i++) {
|
|
126
|
+
const [argName] = decodedArgs[i]
|
|
127
|
+
switchAccumulator.indent().indent().line(`${argName},`)
|
|
128
|
+
}
|
|
129
|
+
switchAccumulator.indent().line(`)`)
|
|
130
|
+
switchAccumulator.line(`}`)
|
|
131
|
+
}
|
|
132
|
+
acc.indent().indent().line(`}`)
|
|
133
|
+
acc.indent().line(`}`)
|
|
134
|
+
acc.line(`}`)
|
|
135
|
+
return acc.finish()
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
generateSendDispatcher(definition: FacadeDefinition): string {
|
|
139
|
+
const acc = new Accumulator()
|
|
140
|
+
acc.line(`import {${definition.name}} from "./${definition.name}.js"`)
|
|
141
|
+
acc.line()
|
|
142
|
+
TypescriptGenerator.generateNativeInterface(acc)
|
|
143
|
+
acc.line(`export class ${definition.name}SendDispatcher implements ${definition.name} {`)
|
|
144
|
+
acc.indent().line(`constructor(private readonly transport: NativeInterface) {}`)
|
|
145
|
+
for (const [methodName, _] of Object.entries(definition.methods)) {
|
|
146
|
+
const methodAccumulator = acc.indent()
|
|
147
|
+
methodAccumulator.line(`async ${methodName}(...args: Parameters<${definition.name}["${methodName}"]>) {`)
|
|
148
|
+
methodAccumulator.indent().line(`return this.transport.invokeNative("ipc", ["${definition.name}", "${methodName}", ...args])`)
|
|
149
|
+
methodAccumulator.line(`}`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
acc.line(`}`)
|
|
153
|
+
return acc.finish()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
generateExtraFiles(): Record<string, string> {
|
|
157
|
+
return {};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
generateTypeRef(outDir: string, definitionPath: string, definition: TypeRefDefinition): string {
|
|
161
|
+
const acc = new Accumulator()
|
|
162
|
+
let tsPath = definition.location.typescript
|
|
163
|
+
const isRelative = tsPath.startsWith(".")
|
|
164
|
+
const actualPath = (isRelative)
|
|
165
|
+
? path.relative(
|
|
166
|
+
path.resolve(outDir),
|
|
167
|
+
path.resolve(definitionPath, tsPath)
|
|
168
|
+
)
|
|
169
|
+
: tsPath
|
|
170
|
+
acc.line(`export {${definition.name}} from "${actualPath}"`)
|
|
171
|
+
|
|
172
|
+
return acc.finish()
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderTypescriptType(parsed: ParsedType): RenderedType {
|
|
177
|
+
const {baseName, nullable, external} = parsed
|
|
178
|
+
switch (baseName) {
|
|
179
|
+
case "List":
|
|
180
|
+
const renderedListInner = renderTypescriptType(parsed.generics[0])
|
|
181
|
+
return {
|
|
182
|
+
externals: renderedListInner.externals,
|
|
183
|
+
name: maybeNullable(`ReadonlyArray<${renderedListInner.name}>`, nullable)
|
|
184
|
+
}
|
|
185
|
+
case "Map":
|
|
186
|
+
const renderedKey = renderTypescriptType(parsed.generics[0])
|
|
187
|
+
const renderedValue = renderTypescriptType(parsed.generics[1])
|
|
188
|
+
return {
|
|
189
|
+
externals: [...renderedKey.externals, ...renderedValue.externals],
|
|
190
|
+
name: maybeNullable(`Record<${renderedKey.name}, ${renderedValue.name}>`, nullable)
|
|
191
|
+
}
|
|
192
|
+
case "string":
|
|
193
|
+
return {externals: [], name: maybeNullable("string", nullable)}
|
|
194
|
+
case "boolean":
|
|
195
|
+
return {externals: [], name: maybeNullable("boolean", nullable)}
|
|
196
|
+
case "number":
|
|
197
|
+
return {externals: [], name: maybeNullable("number", nullable)}
|
|
198
|
+
case "bytes":
|
|
199
|
+
return {externals: [], name: maybeNullable("Uint8Array", nullable)}
|
|
200
|
+
case "void":
|
|
201
|
+
return {externals: [], name: maybeNullable("void", nullable)}
|
|
202
|
+
default:
|
|
203
|
+
return {externals: [baseName], name: maybeNullable(baseName, nullable)}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function maybeNullable(name: string, nullable: boolean): string {
|
|
208
|
+
return nullable ? name + " | null" : name
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function typeNameTypescript(name: string): RenderedType {
|
|
212
|
+
const parsed = parseType(name)
|
|
213
|
+
return renderTypescriptType(parsed)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderTypeAndAddImports(name: string, acc: Accumulator) {
|
|
217
|
+
const rendered = typeNameTypescript(name)
|
|
218
|
+
for (const external of rendered.externals) {
|
|
219
|
+
acc.addImport(`import {${external}} from "./${external}.js"`)
|
|
220
|
+
}
|
|
221
|
+
return rendered.name
|
|
222
|
+
}
|
package/lib/cli.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {generate} from "./index.js"
|
|
3
|
+
import * as fs from "fs"
|
|
4
|
+
import * as path from "path"
|
|
5
|
+
import {globby} from "zx"
|
|
6
|
+
import {Platform} from "./common"
|
|
7
|
+
import {Argument, Option, program} from "commander"
|
|
8
|
+
|
|
9
|
+
const PLATFORMS: Array<Platform> = ["ios", "web", "android", "desktop"]
|
|
10
|
+
|
|
11
|
+
const USAGE = `licc [options] from_dir to_dir
|
|
12
|
+
|
|
13
|
+
will recursively take all JSON files from \`from-dir\` and compile them.
|
|
14
|
+
output files are written into \`to-dir\`, without preserving subdirectory structure.`
|
|
15
|
+
|
|
16
|
+
await program
|
|
17
|
+
.usage(USAGE)
|
|
18
|
+
.addArgument(new Argument("from_dir").argRequired())
|
|
19
|
+
.addArgument(new Argument("to_dir").argOptional())
|
|
20
|
+
.addOption(new Option(
|
|
21
|
+
'-p, --platform <platform>',
|
|
22
|
+
'platform to generate code for. if not specified, from_dir must be omitted as well. In this case, licc will read <from_dir>/.liccc as a json map from platform to output dir. if -p is set, from_dir must be set as well.'
|
|
23
|
+
)
|
|
24
|
+
.makeOptionMandatory(false)
|
|
25
|
+
.choices(PLATFORMS))
|
|
26
|
+
.action(async (from_dir, to_dir, {platform}) => {
|
|
27
|
+
assert(!(platform == null && to_dir != null),
|
|
28
|
+
"can't omit platform and use an explicit output dir. specify both -p <platform> and to_dir or none of them.")
|
|
29
|
+
assert(!(platform != null && to_dir == null),
|
|
30
|
+
"can't use an explicit platform but no output dir. specify both -p <platform> and to_dir or none of them.")
|
|
31
|
+
|
|
32
|
+
let conf: Record<string, string> = {}
|
|
33
|
+
if (platform != null) {
|
|
34
|
+
conf[platform] = path.resolve(process.cwd(), to_dir)
|
|
35
|
+
} else {
|
|
36
|
+
// check if there's a .liccc file that states the desired platforms and output dirs
|
|
37
|
+
const confPath = path.join(from_dir, ".liccc")
|
|
38
|
+
try {
|
|
39
|
+
const relConf: Record<Platform, string> = JSON.parse(await fs.promises.readFile(confPath, {encoding: "utf-8"}))
|
|
40
|
+
for (let [relPlatform, relPath] of Object.entries(relConf) as [Platform | "__comment", string][]) {
|
|
41
|
+
if (relPlatform === "__comment") continue
|
|
42
|
+
assert(PLATFORMS.includes(relPlatform), `invalid platform in .liccc: ${relPlatform}`)
|
|
43
|
+
conf[relPlatform] = path.resolve(process.cwd(), from_dir, relPath)
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.log(`unable to read ${confPath} as JSON: ${e}`)
|
|
47
|
+
process.exit(1)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
await run(from_dir, conf)
|
|
51
|
+
})
|
|
52
|
+
.parseAsync(process.argv)
|
|
53
|
+
|
|
54
|
+
async function run(from_dir: string, conf: Record<Platform, string>): Promise<void> {
|
|
55
|
+
const inputFiles = await globby(path.join(process.cwd(), from_dir, "*/**/*.json"))
|
|
56
|
+
const inputMap = new Map(inputFiles.map((n: string) => (
|
|
57
|
+
[path.basename(n, ".json"), fs.readFileSync(n, "utf8")]
|
|
58
|
+
)))
|
|
59
|
+
|
|
60
|
+
// doing it here because some platforms generate into the same dir.
|
|
61
|
+
for (let outDir of Object.values(conf)) {
|
|
62
|
+
clearDir(outDir)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (let [confPlatform, confOutDir] of Object.entries(conf)) {
|
|
66
|
+
console.log("generating for", confPlatform, "into", confOutDir)
|
|
67
|
+
try {
|
|
68
|
+
await generate(confPlatform as Platform, inputMap, confOutDir,)
|
|
69
|
+
} catch (e) {
|
|
70
|
+
assert(false, `compilation failed with ${e}`)
|
|
71
|
+
}
|
|
72
|
+
console.log("done; no errors\n")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function assert(proposition: boolean, text: string): void {
|
|
77
|
+
if (proposition) return
|
|
78
|
+
console.log("\nFatal Error:\n", text)
|
|
79
|
+
console.log("")
|
|
80
|
+
console.log(program.helpInformation())
|
|
81
|
+
process.exit(1)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function clearDir(dir: string) {
|
|
85
|
+
console.log("clearing dir:", dir)
|
|
86
|
+
try {
|
|
87
|
+
const files = fs.readdirSync(dir)
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
fs.unlinkSync(path.join(dir, file))
|
|
90
|
+
}
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log("could not clear dir:", e)
|
|
93
|
+
}
|
|
94
|
+
}
|
package/lib/common.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export interface LangGenerator {
|
|
2
|
+
/**
|
|
3
|
+
* generate a structured type definition
|
|
4
|
+
* @param definition
|
|
5
|
+
*/
|
|
6
|
+
handleStructDefinition(definition: StructDefinition): string
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* generate a collection of methods with arguments and return types
|
|
10
|
+
* and its associated dispatchers
|
|
11
|
+
* @param definition
|
|
12
|
+
*/
|
|
13
|
+
generateFacade(definition: FacadeDefinition): string
|
|
14
|
+
|
|
15
|
+
generateReceiveDispatcher(definition: FacadeDefinition): string
|
|
16
|
+
|
|
17
|
+
generateSendDispatcher(definition: FacadeDefinition): string
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* generate the receiving dispatcher for the facades we are on the receiving side of
|
|
21
|
+
*/
|
|
22
|
+
generateGlobalDispatcher(name: string, facadeNames: Array<string>): string
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* generate extra type definitions needed to make the interface work
|
|
26
|
+
*/
|
|
27
|
+
generateExtraFiles(): Record<string, string>
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* external types that don't get generated but are located somewhere else
|
|
31
|
+
*/
|
|
32
|
+
generateTypeRef(outDir: string, definitionPath: string, definition: TypeRefDefinition): string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Platform = "ios" | "web" | "android" | "desktop"
|
|
36
|
+
export type Language = "kotlin" | "swift" | "typescript"
|
|
37
|
+
|
|
38
|
+
export interface StructDefinition {
|
|
39
|
+
type: "struct"
|
|
40
|
+
name: string
|
|
41
|
+
fields: Record<string, string>
|
|
42
|
+
doc?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FacadeDefinition {
|
|
46
|
+
type: "facade"
|
|
47
|
+
name: string
|
|
48
|
+
senders: Array<Platform>
|
|
49
|
+
receivers: Array<Platform>
|
|
50
|
+
methods: Record<string, MethodDefinition>
|
|
51
|
+
doc?: string,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface TypeRefDefinition {
|
|
55
|
+
"type": "typeref"
|
|
56
|
+
"name": string
|
|
57
|
+
"location": Record<Language, string>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface MethodDefinition {
|
|
61
|
+
arg: Array<ArgumentDefinition>
|
|
62
|
+
ret: string
|
|
63
|
+
doc?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type ArgumentDefinition = Record<string, string>
|
|
67
|
+
|
|
68
|
+
export interface RenderedType {
|
|
69
|
+
externals: string[],
|
|
70
|
+
name: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function getArgs(methName: string, methodDef: MethodDefinition): {name: string, type: string}[] {
|
|
74
|
+
return methodDef.arg.map((a, i) => {
|
|
75
|
+
const entries = Object.entries(a)
|
|
76
|
+
if (entries.length === 0) {
|
|
77
|
+
throw new Error(`Syntax Error: method ${methName} argument ${i} is empty`)
|
|
78
|
+
} else if (entries.length > 1) {
|
|
79
|
+
throw new Error(`Syntax Error: method ${methName} argument ${i} has too many entries`)
|
|
80
|
+
}
|
|
81
|
+
return {"name": entries[0][0], "type": entries[0][1]}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function capitalize(input: string): string {
|
|
86
|
+
return input.replace(/^\w/, c => c.toUpperCase())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function minusculize(input: string): string {
|
|
90
|
+
return input.replace(/^\w/, c => c.toLowerCase())
|
|
91
|
+
}
|
package/lib/index.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import {TypescriptGenerator} from "./TypescriptGenerator.js"
|
|
2
|
+
import {capitalize, FacadeDefinition, LangGenerator, Language, Platform, StructDefinition, TypeRefDefinition} from "./common.js"
|
|
3
|
+
import {SwiftGenerator} from "./SwiftGenerator.js"
|
|
4
|
+
import {KotlinGenerator} from "./KotlinGenerator.js"
|
|
5
|
+
import * as path from "path"
|
|
6
|
+
import * as fs from "fs"
|
|
7
|
+
|
|
8
|
+
function generatorForLang(lang: Language): LangGenerator {
|
|
9
|
+
switch (lang) {
|
|
10
|
+
case "typescript":
|
|
11
|
+
return new TypescriptGenerator()
|
|
12
|
+
case "swift":
|
|
13
|
+
return new SwiftGenerator()
|
|
14
|
+
case "kotlin":
|
|
15
|
+
return new KotlinGenerator()
|
|
16
|
+
default:
|
|
17
|
+
throw new Error("Unknown output language:" + lang)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mapPlatformToLang(platform: string): Language {
|
|
22
|
+
switch (platform) {
|
|
23
|
+
case "ios":
|
|
24
|
+
return "swift"
|
|
25
|
+
case "android":
|
|
26
|
+
return "kotlin"
|
|
27
|
+
case "web":
|
|
28
|
+
case "desktop":
|
|
29
|
+
return "typescript"
|
|
30
|
+
default:
|
|
31
|
+
throw new Error("unknown platform " + platform)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* generate and write all target language source files.
|
|
37
|
+
*
|
|
38
|
+
* @param platform one of the supported platform names
|
|
39
|
+
* @param sources a map from the definition file name to the definition json string
|
|
40
|
+
* @param outDir the directory the output files should be written to
|
|
41
|
+
*/
|
|
42
|
+
export function generate(platform: Platform, sources: Map<string, string>, outDir: string) {
|
|
43
|
+
const lang = mapPlatformToLang(platform)
|
|
44
|
+
const ext = getFileExtensionForLang(lang)
|
|
45
|
+
const generator = generatorForLang(lang)
|
|
46
|
+
const facadesToImplement: Array<string> = []
|
|
47
|
+
for (const [inputPath, source] of Array.from(sources.entries())) {
|
|
48
|
+
console.log("handling ipc schema file", inputPath)
|
|
49
|
+
const definition = JSON.parse(source) as FacadeDefinition | StructDefinition | TypeRefDefinition
|
|
50
|
+
if (!("name" in definition)) {
|
|
51
|
+
throw new Error(`malformed definition: ${inputPath} doesn't have name field`)
|
|
52
|
+
}
|
|
53
|
+
if (!("type" in definition)) {
|
|
54
|
+
throw new Error(`missing type declaration: ${inputPath}`)
|
|
55
|
+
}
|
|
56
|
+
switch (definition.type) {
|
|
57
|
+
case "facade":
|
|
58
|
+
assertReturnTypesPresent(definition)
|
|
59
|
+
const isReceiving = definition.receivers.includes(platform)
|
|
60
|
+
const isSending = definition.senders.includes(platform)
|
|
61
|
+
if (!isReceiving && !isSending) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
const facadeOutput = generator.generateFacade(definition)
|
|
65
|
+
write(facadeOutput, outDir, definition.name + ext)
|
|
66
|
+
if (isReceiving) {
|
|
67
|
+
const receiveOutput = generator.generateReceiveDispatcher(definition)
|
|
68
|
+
write(receiveOutput, outDir, definition.name + "ReceiveDispatcher" + ext)
|
|
69
|
+
facadesToImplement.push(definition.name)
|
|
70
|
+
}
|
|
71
|
+
if (isSending) {
|
|
72
|
+
const sendOutput = generator.generateSendDispatcher(definition)
|
|
73
|
+
write(sendOutput, outDir, definition.name + "SendDispatcher" + ext)
|
|
74
|
+
}
|
|
75
|
+
break
|
|
76
|
+
case "struct":
|
|
77
|
+
const structOutput = generator.handleStructDefinition(definition)
|
|
78
|
+
write(structOutput, outDir, definition.name + ext)
|
|
79
|
+
break
|
|
80
|
+
case "typeref":
|
|
81
|
+
const refOutput = generator.generateTypeRef(outDir, inputPath, definition)
|
|
82
|
+
if (refOutput != null) {
|
|
83
|
+
write(refOutput, outDir, definition.name + ext)
|
|
84
|
+
}
|
|
85
|
+
break
|
|
86
|
+
default:
|
|
87
|
+
throw new Error(`unknown definition type in ${inputPath}: ` + (definition as any).type)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const extraFiles = generator.generateExtraFiles()
|
|
92
|
+
for (let extraFilesKey in extraFiles) {
|
|
93
|
+
write(extraFiles[extraFilesKey], outDir, extraFilesKey + ext)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const dispatcherName = `${capitalize(platform)}GlobalDispatcher`
|
|
97
|
+
const dispatcherCode = generator.generateGlobalDispatcher(dispatcherName, facadesToImplement)
|
|
98
|
+
write(dispatcherCode, outDir, dispatcherName + ext)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getFileExtensionForLang(lang: string): string {
|
|
102
|
+
switch (lang) {
|
|
103
|
+
case "typescript":
|
|
104
|
+
return ".ts"
|
|
105
|
+
case "swift":
|
|
106
|
+
return ".swift"
|
|
107
|
+
case "kotlin":
|
|
108
|
+
return ".kt"
|
|
109
|
+
default:
|
|
110
|
+
throw new Error("unknown output lang: " + lang)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function write(code: string, outDir: string, target: string) {
|
|
115
|
+
fs.mkdirSync(outDir, {recursive: true})
|
|
116
|
+
const filePath = path.join(outDir, target)
|
|
117
|
+
fs.writeFileSync(filePath, code)
|
|
118
|
+
console.log("written:", filePath)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function assertReturnTypesPresent(definition: FacadeDefinition): void {
|
|
122
|
+
const methNoRet = Object.entries(definition.methods).find(([_, {ret}]) => ret == null)
|
|
123
|
+
if (methNoRet) {
|
|
124
|
+
throw new Error(`missing return type on method ${methNoRet[0]} in ${definition.name}`)
|
|
125
|
+
}
|
|
126
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tutao/licc",
|
|
3
|
+
"version": "3.98.2",
|
|
4
|
+
"bin": {
|
|
5
|
+
"licc": "dist/cli.js"
|
|
6
|
+
},
|
|
7
|
+
"description": "little interface compiler",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc -b",
|
|
12
|
+
"test": "rm -rf ./test/build && tsc -b test && cd test/build && node test/Suite.js",
|
|
13
|
+
"preinstall": "mkdir -p dist && touch dist/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"author": "",
|
|
16
|
+
"license": "GPL-3.0",
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"zx": "6.1.0",
|
|
19
|
+
"commander": "9.3.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@tutao/tutanota-test-utils": "3.98.2",
|
|
23
|
+
"ospec": "https://github.com/tutao/ospec.git#0472107629ede33be4c4d19e89f237a6d7b0cb11"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import o from "ospec"
|
|
2
|
+
import {ParsedType, parseType} from "../lib/Parser.js"
|
|
3
|
+
import {assertThrows} from "@tutao/tutanota-test-utils"
|
|
4
|
+
|
|
5
|
+
o.spec('Parser', function () {
|
|
6
|
+
o.spec("illegal identifiers are caught", function () {
|
|
7
|
+
o("empty string", async function () {
|
|
8
|
+
await assertThrows(Error, () => Promise.resolve(parseType("")))
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
o("number at start", async function () {
|
|
12
|
+
await assertThrows(Error, () => Promise.resolve(parseType("0hello")))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
o("special character at start", async function () {
|
|
16
|
+
await assertThrows(Error, () => Promise.resolve(parseType("?some")))
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
o.spec("simple types are parsed correctly", function () {
|
|
21
|
+
o("string", async function () {
|
|
22
|
+
o(parseType("string")).deepEquals({baseName: "string", generics: [], nullable: false, external: false})
|
|
23
|
+
})
|
|
24
|
+
o("string?", async function () {
|
|
25
|
+
o(parseType("string?")).deepEquals({baseName: "string", generics: [], nullable: true, external: false})
|
|
26
|
+
})
|
|
27
|
+
o("external type", async function () {
|
|
28
|
+
o(parseType("SomeExternalType")).deepEquals({baseName: "SomeExternalType", external: true, generics: [], nullable: false})
|
|
29
|
+
})
|
|
30
|
+
o("nullable external", async function () {
|
|
31
|
+
o(parseType("SomeExternalType?")).deepEquals({baseName: "SomeExternalType", external: true, generics: [], nullable: true})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
o("void", async function () {
|
|
35
|
+
o(parseType("void ")).deepEquals({baseName: "void", nullable: false, external: false, generics: []})
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
function stringToTest(typeString: string, expected: ParsedType) {
|
|
40
|
+
o(`"${typeString}"`, function () {
|
|
41
|
+
o(parseType(typeString)).deepEquals(expected)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
o.spec("list type is parsed correctly", function () {
|
|
46
|
+
stringToTest("List<boolean>", {
|
|
47
|
+
baseName: "List",
|
|
48
|
+
generics: [{baseName: "boolean", nullable: false, generics: [], external: false}],
|
|
49
|
+
nullable: false,
|
|
50
|
+
external: false
|
|
51
|
+
})
|
|
52
|
+
stringToTest("List<string?>", {
|
|
53
|
+
baseName: "List",
|
|
54
|
+
generics: [{baseName: "string", nullable: true, generics: [], external: false}],
|
|
55
|
+
nullable: false,
|
|
56
|
+
external: false
|
|
57
|
+
})
|
|
58
|
+
stringToTest("List<number>?", {
|
|
59
|
+
baseName: "List",
|
|
60
|
+
generics: [{baseName: "number", nullable: false, generics: [], external: false}],
|
|
61
|
+
nullable: true,
|
|
62
|
+
external: false
|
|
63
|
+
})
|
|
64
|
+
stringToTest("List<List<External?>?>", {
|
|
65
|
+
baseName: "List",
|
|
66
|
+
generics: [{
|
|
67
|
+
baseName: "List", nullable: true, generics: [
|
|
68
|
+
{baseName: "External", nullable: true, external: true, generics: []}
|
|
69
|
+
], external: false
|
|
70
|
+
}],
|
|
71
|
+
nullable: false,
|
|
72
|
+
external: false
|
|
73
|
+
})
|
|
74
|
+
stringToTest(" List< number > ", {
|
|
75
|
+
baseName: "List",
|
|
76
|
+
generics: [{baseName: "number", nullable: false, generics: [], external: false}],
|
|
77
|
+
nullable: false,
|
|
78
|
+
external: false
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
o.spec("map type is parsed correctly", function () {
|
|
83
|
+
stringToTest("Map<boolean?, Foo>", {
|
|
84
|
+
baseName: "Map", external: false, generics: [{
|
|
85
|
+
baseName: 'boolean',
|
|
86
|
+
generics: [],
|
|
87
|
+
external: false,
|
|
88
|
+
nullable: true
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
baseName: 'Foo',
|
|
92
|
+
generics: [],
|
|
93
|
+
external: true,
|
|
94
|
+
nullable: false
|
|
95
|
+
}], nullable: false
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
stringToTest("Map<number, number>?", {
|
|
99
|
+
baseName: "Map", external: false, generics: [
|
|
100
|
+
{baseName: "number", nullable: false, external: false, generics: []},
|
|
101
|
+
{baseName: "number", nullable: false, external: false, generics: []}
|
|
102
|
+
], nullable: true
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
stringToTest("Map<string, string>", {
|
|
106
|
+
baseName: "Map", external: false, generics: [
|
|
107
|
+
{baseName: "string", nullable: false, external: false, generics: []},
|
|
108
|
+
{baseName: "string", nullable: false, external: false, generics: []}
|
|
109
|
+
], nullable: false
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
stringToTest("Map<List<number>?, number?>", {
|
|
113
|
+
baseName: "Map", external: false, generics: [
|
|
114
|
+
{
|
|
115
|
+
baseName: "List",
|
|
116
|
+
generics: [{baseName: "number", nullable: false, generics: [], external: false}],
|
|
117
|
+
nullable: true,
|
|
118
|
+
external: false
|
|
119
|
+
},
|
|
120
|
+
{baseName: "number", nullable: true, external: false, generics: []}
|
|
121
|
+
], nullable: false
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
})
|