lecodes-cli 0.20.2 → 2.0.0
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 +56 -57
- package/dist/index.js +11052 -7592
- package/package.json +14 -8
- package/runtime/materials/decal-relief.mat +12 -1
- package/runtime/materials/decal.mat +17 -3
- package/runtime/materials/lightmap.mat +117 -138
- package/runtime/materials/particles-quad.mat +6 -2
- package/runtime/materials/terrain-lightmap.mat +123 -0
- package/runtime/materials/terrain.mat +36 -55
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +5 -0
- package/runtime/web/assets/camera-5XOM8eHl.js +1 -0
- package/runtime/web/assets/creator-2d-D9WKYM6h.js +1 -0
- package/runtime/web/assets/creator-2d-ukvEmNEp.wasm +0 -0
- package/runtime/web/assets/creator-full-BBXzQlhq.js +1 -0
- package/runtime/web/assets/creator-full-D2dSDdDj.wasm +0 -0
- package/runtime/web/assets/creator-full.js-BYswGsqR.symbols +8124 -0
- package/runtime/web/assets/creator-ui-DIc92ion.wasm +0 -0
- package/runtime/web/assets/creator-ui-_1Z_AX2M.js +1 -0
- package/runtime/web/assets/geolocation-BjTKN9_P.js +1 -0
- package/runtime/web/assets/inspect-C7x78PNs.js +2 -0
- package/runtime/web/assets/map-CyBxOSV0.js +1 -0
- package/runtime/web/assets/map-XzoOFt0E.js +2 -0
- package/runtime/web/assets/mapImpl-bxvQ2h1Z.js +823 -0
- package/runtime/web/assets/maplibre-gl-worker-B7LXXUkR.js +8 -0
- package/runtime/web/assets/materials-uri4FvPE.bin +0 -0
- package/runtime/web/assets/materials-world-CQWt8TAm.bin +0 -0
- package/runtime/web/assets/qr-scanner-CK_cYoJR.js +1 -0
- package/runtime/web/assets/uberarchive-DLH-kBhM.bin +0 -0
- package/runtime/web/index.html +36 -0
- package/runtime/web/player.js +21 -0
- package/runtime/web-page/index.html +41 -0
- package/runtime/web-page/page.css +1 -0
- package/runtime/web-page/page.js +31 -0
- package/src/api.ts +1 -302
- package/src/cli/args.ts +37 -0
- package/src/cli/command.ts +113 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/help.ts +158 -0
- package/src/cli/index.ts +7 -0
- package/src/cli/output.ts +102 -0
- package/src/cli/run.ts +106 -0
- package/src/commands/{appAndroid.ts → app/android.ts} +454 -43
- package/src/commands/{appDesktop.ts → app/desktop.ts} +64 -33
- package/src/commands/{appDesktopMac.ts → app/desktopMac.ts} +34 -27
- package/src/commands/{appIcon.ts → app/icon.ts} +11 -8
- package/src/commands/{app.ts → app/index.ts} +400 -290
- package/src/commands/{appShared.ts → app/shared.ts} +43 -44
- package/src/commands/{appTemplatesAndroid.ts → app/templates/android.ts} +72 -39
- package/src/commands/{appTemplatesGradlew.ts → app/templates/gradlew.ts} +1 -1
- package/src/commands/{appTemplates.ts → app/templates/ios.ts} +2 -2
- package/src/commands/assets.ts +23 -21
- package/src/commands/clone.ts +48 -51
- package/src/commands/compile.ts +76 -89
- package/src/commands/create.ts +46 -47
- package/src/commands/design/comments.ts +361 -0
- package/src/commands/design/context.ts +101 -0
- package/src/commands/design/index.ts +391 -0
- package/src/commands/design/snapshot.ts +134 -0
- package/src/commands/{designTemplates.ts → design/templates.ts} +4 -3
- package/src/commands/desktop.ts +94 -82
- package/src/commands/dev.ts +260 -182
- package/src/commands/diff.ts +39 -51
- package/src/commands/index.ts +64 -0
- package/src/commands/{init.ts → init/index.ts} +195 -191
- package/src/commands/{projectTemplates.ts → init/templates.ts} +64 -21
- package/src/commands/install.ts +80 -67
- package/src/commands/lightmap.ts +543 -252
- package/src/commands/link.ts +87 -94
- package/src/commands/login.ts +25 -23
- package/src/commands/navmesh.ts +169 -139
- package/src/commands/plugin.ts +46 -0
- package/src/commands/pn.ts +201 -244
- package/src/commands/pull.ts +49 -56
- package/src/commands/push.ts +48 -54
- package/src/commands/render.ts +176 -111
- package/src/commands/scene.ts +63 -55
- package/src/commands/serve.ts +58 -0
- package/src/commands/{shaders.ts → shaders/index.ts} +61 -42
- package/src/commands/{shadersNew.ts → shaders/new.ts} +3 -3
- package/src/commands/shared.ts +79 -0
- package/src/commands/status.ts +22 -26
- package/src/commands/test.ts +94 -63
- package/src/commands/thumbs.ts +185 -175
- package/src/commands/update/index.ts +226 -0
- package/src/commands/{types.ts → update/types.ts} +31 -22
- package/src/compile/collect.ts +5 -5
- package/src/compile/collectLocal.ts +3 -3
- package/src/compile/designCompile.ts +4 -4
- package/src/compile/headlessBundle.ts +16 -12
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +12 -4
- package/src/compile/sceneCompile.ts +14 -26
- package/src/compile/screenEntry.ts +123 -127
- package/src/compile/shaders.ts +3 -3
- package/src/{lecodes-3d-editor.d.ts → declarations/lecodes-3d-editor.d.ts} +4 -0
- package/src/{lecodes-assets.d.ts → declarations/lecodes-assets.d.ts} +1 -1
- package/src/{lecodes-renderer.d.ts → declarations/lecodes-headless.d.ts} +3 -3
- package/src/dev/androidDev.ts +1 -1
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +78 -41
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +83 -15
- package/src/{cmgenTool.ts → hosts/cmgenTool.ts} +2 -2
- package/src/{desktopRenderer.ts → hosts/desktopRenderer.ts} +104 -32
- package/src/{desktopScript.ts → hosts/desktopScript.ts} +151 -36
- package/src/{distRoot.ts → hosts/distRoot.ts} +9 -5
- package/src/{matcTool.ts → hosts/matcTool.ts} +2 -2
- package/src/{peerInstall.ts → hosts/peerInstall.ts} +7 -7
- package/src/{peers.ts → hosts/peers.ts} +2 -2
- package/src/{releases.ts → hosts/releases.ts} +1 -1
- package/src/index.ts +35 -480
- package/src/platform/api.ts +302 -0
- package/src/{browserAuth.ts → platform/browserAuth.ts} +1 -1
- package/src/{config.ts → platform/config.ts} +1 -1
- package/src/{serverDiff.ts → platform/serverDiff.ts} +3 -3
- package/src/plugin/contract.ts +438 -0
- package/src/plugin/emitKotlin.ts +357 -0
- package/src/plugin/emitSdk.ts +259 -0
- package/src/plugin/emitSwift.ts +336 -0
- package/src/plugin/emitWeb.ts +383 -0
- package/src/plugin/generate.ts +157 -0
- package/src/plugin/lines.ts +28 -0
- package/src/plugin/lower.ts +231 -0
- package/src/plugin/manifest.ts +192 -0
- package/src/{projectEnv.ts → project/env.ts} +33 -2
- package/src/{ignore.ts → project/ignore.ts} +164 -163
- package/src/{localFiles.ts → project/localFiles.ts} +1 -1
- package/src/{manifest.ts → project/manifest.ts} +1 -1
- package/src/{project.ts → project/materialize.ts} +2 -2
- package/src/project/paths.ts +20 -0
- package/src/{textDiff.ts → project/textDiff.ts} +1 -1
- package/src/{types.ts → project/types.ts} +0 -0
- package/runtime/materials/lightmap-baked-lite.mat +0 -175
- package/runtime/materials/lightmap-baked.mat +0 -176
- package/runtime/scene-harness.json +0 -1
- package/runtime/web/assets/__vite-browser-external-BIHI7g3E.js +0 -1
- package/runtime/web/assets/basis-C64VHDVD.js +0 -1
- package/runtime/web/assets/basis_transcoder-VXdx5NbI.wasm +0 -0
- package/runtime/web/assets/createViewerLite-Ct_PZdof.js +0 -852
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-BMt7AnC5.js +0 -2
- package/runtime/web/assets/mapViewImpl-B2JcES8l.js +0 -810
- package/runtime/web/assets/maplibre-gl-worker-CJfwIrte.js +0 -8
- package/runtime/web/assets/worker-Caf-yYEI.js +0 -2
- package/runtime/web/embed.html +0 -32
- package/runtime/web/embed.js +0 -171
- package/src/commands/design.ts +0 -846
- package/src/commands/update.ts +0 -211
- package/src/util.ts +0 -146
- /package/src/{lecodes-design.d.ts → declarations/lecodes-design.d.ts} +0 -0
- /package/src/{qrcode-terminal.d.ts → declarations/qrcode-terminal.d.ts} +0 -0
- /package/src/{designMeta.ts → project/designMeta.ts} +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// The generator's emitter for `ios/`: a contract's channel → `<Name>.gen.swift`, everything of the
|
|
2
|
+
// iOS half that is not the plugin's own work (docs/plans/plugins-plan.md in core):
|
|
3
|
+
// - the contract's types as Swift (enums, structs, tagged unions) with their wire codecs;
|
|
4
|
+
// - `<Name>Code`: the codes the channel rejects with;
|
|
5
|
+
// - `<Name>Events`: one function per event;
|
|
6
|
+
// - `<Name>Plugin`: the PROTOCOL the plugin implements — one method per call, a typed `Reply`;
|
|
7
|
+
// - `<Name>Channel.register(in:_:)` + the glue that reads a call's arguments, refuses a malformed
|
|
8
|
+
// one and hands the call over.
|
|
9
|
+
// It stands on the host SDK's PluginChannel.swift (Reply, PluginFile) and on the wire's two ends
|
|
10
|
+
// (WireIn, WireOut). The wire carries VALUES, and the codecs read a struct straight out of the
|
|
11
|
+
// runtime's value and write one straight into it — no tree of Foundation's objects in between. A
|
|
12
|
+
// Uint8Array is a `Data`, a File the handle of a host buffer; a `Json` member alone is Foundation's
|
|
13
|
+
// objects, because JSON is what a half does with it.
|
|
14
|
+
|
|
15
|
+
import { EmitError } from "./emitSdk"
|
|
16
|
+
import { block, indent, text, wrap, writerName } from "./lines"
|
|
17
|
+
import { waysOf, type L, type LChannel, type LDecl, type LField, type Lowered, type Ways } from "./lower"
|
|
18
|
+
|
|
19
|
+
const KEYWORDS = new Set([
|
|
20
|
+
"associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout", "internal",
|
|
21
|
+
"let", "open", "operator", "private", "protocol", "public", "rethrows", "static", "struct", "subscript", "typealias",
|
|
22
|
+
"var", "break", "case", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if", "in",
|
|
23
|
+
"repeat", "return", "switch", "where", "while", "as", "catch", "false", "is", "nil", "super", "self", "Self", "throw",
|
|
24
|
+
"throws", "true", "try", "Any", "Type", "Protocol",
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
const ident = (name: string): string => KEYWORDS.has(name) ? `\`${name}\`` : name
|
|
28
|
+
|
|
29
|
+
/** A string of the contract as a Swift case: `"not-asked"` → `notAsked`. */
|
|
30
|
+
const caseName = (value: string): string => {
|
|
31
|
+
const words = value.split(/[^A-Za-z0-9]+/).filter(Boolean)
|
|
32
|
+
const name = words.map((w, i) => i === 0 ? w.charAt(0).toLowerCase() + w.slice(1) : w.charAt(0).toUpperCase() + w.slice(1)).join("")
|
|
33
|
+
return /^[A-Za-z_]/.test(name) ? name : `_${name}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const lowerFirst = (s: string): string => s.charAt(0).toLowerCase() + s.slice(1)
|
|
37
|
+
|
|
38
|
+
const doc = (text: string, indent = ""): string =>
|
|
39
|
+
text ? text.split("\n").map(l => `${indent}/// ${l.trim()}`.trimEnd()).join("\n") + "\n" : ""
|
|
40
|
+
|
|
41
|
+
export const swiftType = (t: L): string => {
|
|
42
|
+
switch (t.kind) {
|
|
43
|
+
case "i32": return "Int32"
|
|
44
|
+
case "f64": return "Double"
|
|
45
|
+
case "boolean": return "Bool"
|
|
46
|
+
case "string": return "String"
|
|
47
|
+
case "json": return "Any"
|
|
48
|
+
case "file": return "PluginFile"
|
|
49
|
+
case "bytes": return "Data"
|
|
50
|
+
case "void": return "Void"
|
|
51
|
+
case "enum": case "struct": case "union": return t.name
|
|
52
|
+
case "list": return `[${swiftType(t.of)}]`
|
|
53
|
+
case "map": return `[String: ${swiftType(t.of)}]`
|
|
54
|
+
case "optional": return `${swiftType(t.of)}?`
|
|
55
|
+
case "tuple": return `(${t.of.map(swiftType).join(", ")})`
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The expression of type `T?` that reads the wire's value `e` (a `WireIn`), as lines: nil = it is
|
|
60
|
+
* not a T. */
|
|
61
|
+
const decode = (t: L, e: string, depth = 0): string[] => {
|
|
62
|
+
const v = `v${depth}`
|
|
63
|
+
switch (t.kind) {
|
|
64
|
+
case "i32": return [`${e}.i32`]
|
|
65
|
+
case "f64": return [`${e}.f64`]
|
|
66
|
+
case "boolean": return [`${e}.bool`]
|
|
67
|
+
case "string": return [`${e}.string`]
|
|
68
|
+
case "json": return [`${e}.json`]
|
|
69
|
+
case "bytes": return [`${e}.bytes`]
|
|
70
|
+
case "file": return [`${e}.file`]
|
|
71
|
+
case "enum": case "struct": case "union": return [`${t.name}(wire: ${e})`]
|
|
72
|
+
// Closures are ARGUMENTS, never trailing: a trailing one does not parse inside a `guard` condition.
|
|
73
|
+
case "list": return block(`${e}.list({ ${v} in`, decode(t.of, v, depth + 1), `})`)
|
|
74
|
+
case "map": return block(`${e}.map({ ${v} in`, decode(t.of, v, depth + 1), `})`)
|
|
75
|
+
case "optional": return decode(t.of, e, depth)
|
|
76
|
+
case "tuple": {
|
|
77
|
+
const parts = t.of.map((m, i) => ({ name: `${v}_${i}`, lines: decode(m, `${v}[${i}]`, depth + 1) }))
|
|
78
|
+
// Scalars are read in one `guard`; a member of several lines has one of its own.
|
|
79
|
+
const guards = parts.every(p => p.lines.length === 1)
|
|
80
|
+
? [`guard ${parts.map(p => `let ${p.name} = ${p.lines[0]}`).join(", ")} else { return nil }`]
|
|
81
|
+
: parts.flatMap(p => wrap(`guard let ${p.name} = `, p.lines, ` else { return nil }`))
|
|
82
|
+
return [
|
|
83
|
+
`${e}.tuple(${t.of.length}).flatMap({ ${v} -> ${swiftType(t)}? in`,
|
|
84
|
+
...indent([...guards, `return (${parts.map(p => p.name).join(", ")})`]),
|
|
85
|
+
`})`,
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
case "void": throw new EmitError("void has no value to read")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The statements that write the value `e` (a `T`) to the wire `w` (a `WireOut`), as lines. */
|
|
93
|
+
const encode = (t: L, e: string, w: string, depth = 0): string[] => {
|
|
94
|
+
const v = `v${depth}`
|
|
95
|
+
switch (t.kind) {
|
|
96
|
+
case "i32": return [`${w}.i32(${e})`]
|
|
97
|
+
case "f64": return [`${w}.f64(${e})`]
|
|
98
|
+
case "boolean": return [`${w}.bool(${e})`]
|
|
99
|
+
case "string": return [`${w}.string(${e})`]
|
|
100
|
+
case "json": return [`${w}.json(${e})`]
|
|
101
|
+
case "bytes": return [`${w}.bytes(${e})`]
|
|
102
|
+
case "enum": return [`${w}.string(${e}.rawValue)`]
|
|
103
|
+
case "file": case "struct": case "union": return [`${e}.write(to: ${w})`]
|
|
104
|
+
case "list": return [`${w}.beginArray(${e}.count)`, ...block(`for ${v} in ${e} {`, encode(t.of, v, w, depth + 1), `}`), `${w}.end()`]
|
|
105
|
+
case "map": return [`${w}.beginObject(${e}.count)`, `for (k${depth}, ${v}) in ${e} {`, ...indent([`${w}.name(k${depth})`, ...encode(t.of, v, w, depth + 1)]), `}`, `${w}.end()`]
|
|
106
|
+
case "tuple": return [`${w}.beginArray(${t.of.length})`, ...t.of.flatMap((m, i) => encode(m, `${e}.${i}`, w, depth + 1)), `${w}.end()`]
|
|
107
|
+
case "optional": {
|
|
108
|
+
const some = encode(t.of, v, w, depth + 1)
|
|
109
|
+
const one = `if let ${v} = ${e} { ${some[0]} } else { ${w}.null() }`
|
|
110
|
+
return some.length === 1 && one.length <= 100 ? [one] : [`if let ${v} = ${e} {`, ...indent(some), `} else {`, ...indent([`${w}.null()`]), `}`]
|
|
111
|
+
}
|
|
112
|
+
case "void": throw new EmitError("void has no value to write")
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A value under its key: on the key's line when it is one plain statement. */
|
|
117
|
+
const keyed = (key: string, value: string[]): string[] =>
|
|
118
|
+
value.length === 1 && !/^(if|for) /.test(value[0]!) ? [`${key}; ${value[0]}`] : [key, ...value]
|
|
119
|
+
|
|
120
|
+
/** The statements that write a struct's members — an object — to the wire `w`, as lines. */
|
|
121
|
+
const encodeFields = (fields: LField[], of: (f: LField) => string, w: string): string[] => [
|
|
122
|
+
`${w}.beginObject(${fields.length})`,
|
|
123
|
+
...fields.flatMap(f => {
|
|
124
|
+
const key = `${w}.key(${JSON.stringify(f.name)})`
|
|
125
|
+
if (f.type.kind !== "optional") return keyed(key, encode(f.type, of(f), w))
|
|
126
|
+
// An optional member that is nil is left out; a nullable one is written as a null.
|
|
127
|
+
if (f.omit) return block(`if let v = ${of(f)} {`, keyed(key, encode(f.type.of, "v", w, 1)), `}`)
|
|
128
|
+
return keyed(key, encode(f.type, of(f), w))
|
|
129
|
+
}),
|
|
130
|
+
`${w}.end()`,
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
const unionCase = (m: L): string => {
|
|
134
|
+
switch (m.kind) {
|
|
135
|
+
case "f64": return "number"
|
|
136
|
+
case "i32": return "integer"
|
|
137
|
+
case "boolean": return "bool"
|
|
138
|
+
case "string": return "string"
|
|
139
|
+
case "enum": case "struct": return lowerFirst(m.name)
|
|
140
|
+
default: return m.kind
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const emitDecl = (d: LDecl, ways: Ways): string => {
|
|
145
|
+
const read = ways.read.has(d.name)
|
|
146
|
+
const write = ways.write.has(d.name)
|
|
147
|
+
if (d.kind === "enum") {
|
|
148
|
+
const cases = d.values.map(v => {
|
|
149
|
+
const name = caseName(v)
|
|
150
|
+
return ` case ${ident(name)}${name === v ? "" : ` = ${JSON.stringify(v)}`}`
|
|
151
|
+
})
|
|
152
|
+
return [
|
|
153
|
+
doc(d.doc),
|
|
154
|
+
`public enum ${d.name}: String {\n${cases.join("\n")}\n`,
|
|
155
|
+
read ? `\n init?(wire: WireIn) {\n guard let raw = wire.string, let value = ${d.name}(rawValue: raw) else { return nil }\n self = value\n }\n` : "",
|
|
156
|
+
`}\n`,
|
|
157
|
+
].join("")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (d.kind === "union") {
|
|
161
|
+
const cases = d.members.map(m => ` case ${ident(unionCase(m))}(${swiftType(m)})`)
|
|
162
|
+
const reads = !read ? [] : d.members.flatMap(m => [
|
|
163
|
+
...wrap(`if let v = `, decode(m, "wire", 1), ` {`),
|
|
164
|
+
...indent([`self = .${ident(unionCase(m))}(v)`, `return`]),
|
|
165
|
+
`}`,
|
|
166
|
+
])
|
|
167
|
+
const writes = !write ? [] : d.members.flatMap(m => {
|
|
168
|
+
const lines = encode(m, "v", "w", 1)
|
|
169
|
+
const head = `case .${ident(unionCase(m))}(let v):`
|
|
170
|
+
return lines.length === 1 ? [`${head} ${lines[0]}`] : [head, ...indent(lines)]
|
|
171
|
+
})
|
|
172
|
+
return [
|
|
173
|
+
doc(d.doc),
|
|
174
|
+
`public enum ${d.name} {\n${cases.join("\n")}\n`,
|
|
175
|
+
read ? `\n init?(wire: WireIn) {\n${text(reads, " ")}\n return nil\n }\n` : "",
|
|
176
|
+
write ? `\n func write(to w: WireOut) {\n switch self {\n${text(writes, " ")}\n }\n }\n` : "",
|
|
177
|
+
`}\n`,
|
|
178
|
+
].join("")
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const members = d.fields.map(f => `${doc(f.doc, " ")} public var ${ident(f.name)}: ${swiftType(f.type)}`)
|
|
182
|
+
const initArgs = d.fields.map(f => `${ident(f.name)}: ${swiftType(f.type)}${f.type.kind === "optional" ? " = nil" : ""}`)
|
|
183
|
+
const initBody = d.fields.map(f => ` self.${ident(f.name)} = ${ident(f.name)}`)
|
|
184
|
+
// The members are read into numbered locals and written from `self.`: a member's own name is
|
|
185
|
+
// never an identifier here, so none can shadow the wire.
|
|
186
|
+
const reads = !read ? [] : d.fields.flatMap((f, i) => {
|
|
187
|
+
const at = `wire[${JSON.stringify(f.name)}, ${i}]`
|
|
188
|
+
const name = ident(f.name)
|
|
189
|
+
if (f.type.kind !== "optional") return [...wrap(`guard let m${i} = `, decode(f.type, at), ` else { return nil }`), `self.${name} = m${i}`]
|
|
190
|
+
// An optional member that IS there must read: a wrong type is a malformed value, not an absent one.
|
|
191
|
+
return [
|
|
192
|
+
`let m${i} = ${at}`,
|
|
193
|
+
`if m${i}.isPresent {`,
|
|
194
|
+
...indent([...wrap(`guard let v = `, decode(f.type, `m${i}`), ` else { return nil }`), `self.${name} = v`]),
|
|
195
|
+
`} else {`,
|
|
196
|
+
...indent([`self.${name} = nil`]),
|
|
197
|
+
`}`,
|
|
198
|
+
]
|
|
199
|
+
})
|
|
200
|
+
return [
|
|
201
|
+
doc(d.doc),
|
|
202
|
+
`public struct ${d.name} {\n`,
|
|
203
|
+
members.length ? members.join("\n") + "\n\n" : "",
|
|
204
|
+
` public init(${initArgs.join(", ")}) {${initBody.length ? "\n" + initBody.join("\n") + "\n " : ""}}\n`,
|
|
205
|
+
!read ? "" : d.fields.length
|
|
206
|
+
? `\n init?(wire: WireIn) {\n guard wire.isObject else { return nil }\n${text(reads, " ")}\n }\n`
|
|
207
|
+
: `\n init?(wire: WireIn) {\n guard !wire.isPresent || wire.isObject else { return nil }\n }\n`,
|
|
208
|
+
!write ? "" : `\n func write(to w: WireOut) {\n${text(encodeFields(d.fields, f => `self.${ident(f.name)}`, "w"), " ")}\n }\n`,
|
|
209
|
+
`}\n`,
|
|
210
|
+
].join("")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const emitChannel = (ch: LChannel): string => {
|
|
214
|
+
const isView = ch.kind === "view"
|
|
215
|
+
const iface = ch.iface
|
|
216
|
+
const code = ch.codes.length ? `${iface}Code` : "NoCode"
|
|
217
|
+
const out: string[] = []
|
|
218
|
+
|
|
219
|
+
if (ch.codes.length) {
|
|
220
|
+
out.push(`/// The codes the calls of "${ch.name}" reject with.\npublic enum ${iface}Code: String, PluginCode {\n${ch.codes.map(c => ` case ${ident(c)}`).join("\n")}\n}\n`)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ---- events ----
|
|
224
|
+
const events = ch.events.map(e => {
|
|
225
|
+
const name = ident(e.name)
|
|
226
|
+
if (e.inline) {
|
|
227
|
+
const args = e.inline.map(f => `${ident(f.name)}: ${swiftType(f.type)}${f.type.kind === "optional" ? " = nil" : ""}`)
|
|
228
|
+
const w = writerName(e.inline.map(f => f.name))
|
|
229
|
+
return `${doc(e.doc, " ")} public func ${name}(${args.join(", ")}) {\n self.channel.emit(${JSON.stringify(e.name)}) { ${w} in\n${text(encodeFields(e.inline, f => ident(f.name), w), " ")}\n }\n }`
|
|
230
|
+
}
|
|
231
|
+
if (e.payload.kind === "void") return `${doc(e.doc, " ")} public func ${name}() { channel.emit(${JSON.stringify(e.name)}) }`
|
|
232
|
+
const emit = block(`channel.emit(${JSON.stringify(e.name)}) { w in`, encode(e.payload, "payload", "w"), `}`)
|
|
233
|
+
return `${doc(e.doc, " ")} public func ${name}(_ payload: ${swiftType(e.payload)}) {\n${text(emit, " ")}\n }`
|
|
234
|
+
})
|
|
235
|
+
out.push([
|
|
236
|
+
`/// The events of "${ch.name}": what the plugin sends the app. Any thread.\n`,
|
|
237
|
+
`public struct ${iface}Events {\n let channel: ${isView ? "NativeViewChannel" : "ServiceChannel"}\n`,
|
|
238
|
+
events.length ? "\n" + events.join("\n\n") + "\n" : "",
|
|
239
|
+
`}\n`,
|
|
240
|
+
].join(""))
|
|
241
|
+
|
|
242
|
+
// ---- the protocol ----
|
|
243
|
+
const signatures = ch.methods.map(m => {
|
|
244
|
+
const args = [...m.args.map(a => `_ ${ident(a.name)}: ${swiftType(a.type)}`), `_ reply: Reply<${swiftType(m.result)}, ${code}>`]
|
|
245
|
+
const tags = [m.doc, m.rejects.length ? `Rejects with ${m.rejects.map(r => `.${r}`).join(" / ")}.` : ""].filter(Boolean).join("\n")
|
|
246
|
+
return `${doc(tags, " ")} func ${ident(m.name)}(${args.join(", ")})`
|
|
247
|
+
})
|
|
248
|
+
out.push([
|
|
249
|
+
doc(ch.doc || `The ${ch.kind} "${ch.name}".`),
|
|
250
|
+
`/// What the plugin implements. A call arrives on the main thread; its reply is settled exactly\n/// once, now or later, from any thread.\n`,
|
|
251
|
+
`public protocol ${iface}Plugin: AnyObject {\n`,
|
|
252
|
+
isView ? ` /// The view the host mounts: fullscreen, or embedded among a screen's children.\n var view: UIView { get }\n` : "",
|
|
253
|
+
signatures.length ? (isView ? "\n" : "") + signatures.join("\n") + "\n" : "",
|
|
254
|
+
isView
|
|
255
|
+
? `\n /// The instance goes (the world swapped): release the hardware.\n func destroy()\n`
|
|
256
|
+
: `\n /// The session closed (the app's close, the world's teardown): stop what it started.\n func close()\n`,
|
|
257
|
+
`}\n\npublic extension ${iface}Plugin {\n func ${isView ? "destroy" : "close"}() {}\n}\n`,
|
|
258
|
+
].join(""))
|
|
259
|
+
|
|
260
|
+
// ---- registration ----
|
|
261
|
+
out.push([
|
|
262
|
+
`public enum ${iface}Channel {\n`,
|
|
263
|
+
` /// What the halves register under.\n public static let name = ${JSON.stringify(ch.name)}\n`,
|
|
264
|
+
` /// The contract's version: the highest \`@since\` of its members.\n public static let version = ${ch.version}\n\n`,
|
|
265
|
+
` /// Register the ${ch.kind}: \`make\` builds ${isView ? "one instance" : "one session"} from its params. ${isView ? "" : "It must be cheap — no prompt,\n /// no I/O: that belongs to the first call that needs it."}\n`.replace(/ \n/, "\n"),
|
|
266
|
+
` public static func register(in engine: LeCodesEngine, _ make: @escaping (${ch.params}, ${iface}Events) -> ${iface}Plugin) {\n`,
|
|
267
|
+
` engine.register${isView ? "View" : "Service"}(name, version: Int32(version)) { params, channel in\n`,
|
|
268
|
+
` ${iface}Glue(make(WireIn.of(params, { ${ch.params}(wire: $0) }) ?? ${ch.params}(), ${iface}Events(channel: channel)))\n`,
|
|
269
|
+
` }\n }\n}\n`,
|
|
270
|
+
].join(""))
|
|
271
|
+
|
|
272
|
+
// ---- the glue ----
|
|
273
|
+
// The arguments are read into numbered locals: an argument's own name is never an identifier here.
|
|
274
|
+
const cases = ch.methods.map(m => {
|
|
275
|
+
const lines: string[] = []
|
|
276
|
+
m.args.forEach((a, i) => {
|
|
277
|
+
const bad = `return settle.reject("bad arguments — ${iface}.${m.name}: ${a.name}")`
|
|
278
|
+
if (a.type.kind !== "optional") {
|
|
279
|
+
lines.push(...wrap(`guard let a${i} = `, decode(a.type, `args[${i}]`), ` else { ${bad} }`))
|
|
280
|
+
} else {
|
|
281
|
+
lines.push(
|
|
282
|
+
`let w${i} = args[${i}]`,
|
|
283
|
+
`var a${i}: ${swiftType(a.type)} = nil`,
|
|
284
|
+
`if w${i}.isPresent {`,
|
|
285
|
+
...indent([...wrap(`guard let v = `, decode(a.type, `w${i}`), ` else { ${bad} }`), `a${i} = v`]),
|
|
286
|
+
`}`,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
})
|
|
290
|
+
const head = `Reply<${swiftType(m.result)}, ${code}>(settle)`
|
|
291
|
+
const reply = m.result.kind === "void"
|
|
292
|
+
? [head]
|
|
293
|
+
: m.result.kind === "optional"
|
|
294
|
+
// Nothing is written for nil: the app's promise resolves with `undefined`.
|
|
295
|
+
? block(`${head} { v, w in`, block(`if let v1 = v {`, encode(m.result.of, "v1", "w", 2), `}`), `}`)
|
|
296
|
+
: block(`${head} { v, w in`, encode(m.result, "v", "w", 1), `}`)
|
|
297
|
+
lines.push(...wrap(`plugin.${ident(m.name)}(${m.args.map((_, i) => `a${i}, `).join("")}`, reply, `)`))
|
|
298
|
+
return ` case ${JSON.stringify(m.name)}:\n${text(lines, " ")}`
|
|
299
|
+
})
|
|
300
|
+
out.push([
|
|
301
|
+
`/// Reads a call off the wire and hands it to the plugin; a malformed call never reaches it.\n`,
|
|
302
|
+
`final class ${iface}Glue: ${isView ? "WireNativeViewInstance" : "WireServiceInstance"} {\n`,
|
|
303
|
+
` private let plugin: ${iface}Plugin\n\n`,
|
|
304
|
+
` init(_ plugin: ${iface}Plugin) { self.plugin = plugin }\n\n`,
|
|
305
|
+
isView ? ` var view: UIView { plugin.view }\n\n` : "",
|
|
306
|
+
` func call(_ method: String, wire args: WireIn, _ settle: ChannelSettle) {\n switch method {\n`,
|
|
307
|
+
cases.length ? cases.join("\n") + "\n" : "",
|
|
308
|
+
` default:\n settle.reject("Unknown method: \\(method)")\n }\n }\n\n`,
|
|
309
|
+
` func ${isView ? "destroy" : "close"}() { plugin.${isView ? "destroy" : "close"}() }\n}\n`,
|
|
310
|
+
].join(""))
|
|
311
|
+
|
|
312
|
+
return out.join("\n")
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export type SwiftEmitOptions = { id: string }
|
|
316
|
+
|
|
317
|
+
/** One file per plugin: the types every channel of the contract reaches, then each channel. */
|
|
318
|
+
export const emitSwift = (lowered: Lowered, opts: SwiftEmitOptions): string => {
|
|
319
|
+
const views = lowered.channels.some(c => c.kind === "view")
|
|
320
|
+
const header = lowered.channels.map(c => `the ${c.kind} "${c.name}", contract version ${c.version}`).join("; ")
|
|
321
|
+
const guarded = (what: string, body: () => string): string => {
|
|
322
|
+
try { return body() } catch (e) {
|
|
323
|
+
if (e instanceof EmitError) throw new EmitError(`${what}: ${e.message}`)
|
|
324
|
+
throw e
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
const ways = waysOf(lowered)
|
|
328
|
+
return [
|
|
329
|
+
`// GENERATED by \`lecodes plugin gen\` from contract.d.ts — do not edit.\n`,
|
|
330
|
+
`// ${opts.id}: ${header}.\n\n`,
|
|
331
|
+
`import Foundation\n${views ? "import UIKit\n" : ""}import LeCodes\n\n`,
|
|
332
|
+
`// MARK: - The contract's types\n\n`,
|
|
333
|
+
lowered.decls.map(d => guarded(d.name, () => emitDecl(d, ways))).join("\n"),
|
|
334
|
+
...lowered.channels.map(ch => `\n// MARK: - ${ch.iface}\n\n${guarded(ch.iface, () => emitChannel(ch))}`),
|
|
335
|
+
].join("")
|
|
336
|
+
}
|