lecodes-cli 1.0.0 → 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/dist/index.js +6211 -3649
- package/package.json +12 -8
- package/runtime/materials/lightmap.mat +115 -139
- 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/cli/help.ts +25 -0
- package/src/cli/index.ts +7 -7
- package/src/commands/app/android.ts +3 -1
- package/src/commands/app/desktop.ts +1 -1
- package/src/commands/app/desktopMac.ts +10 -9
- package/src/commands/app/icon.ts +9 -6
- package/src/commands/app/index.ts +3 -1
- package/src/commands/app/shared.ts +13 -5
- package/src/commands/app/templates/android.ts +10 -11
- package/src/commands/app/templates/ios.ts +1 -1
- package/src/commands/assets.ts +1 -1
- package/src/commands/compile.ts +15 -2
- package/src/commands/design/index.ts +4 -4
- package/src/commands/design/snapshot.ts +3 -3
- package/src/commands/desktop.ts +5 -1
- package/src/commands/dev.ts +82 -29
- package/src/commands/index.ts +64 -60
- package/src/commands/init/index.ts +11 -15
- package/src/commands/init/templates.ts +64 -21
- package/src/commands/lightmap.ts +459 -200
- package/src/commands/navmesh.ts +2 -2
- package/src/commands/plugin.ts +46 -0
- package/src/commands/render.ts +22 -16
- package/src/commands/scene.ts +6 -1
- package/src/commands/serve.ts +58 -0
- package/src/commands/shaders/new.ts +1 -1
- package/src/commands/test.ts +402 -398
- package/src/commands/update/index.ts +3 -3
- package/src/compile/designCompile.ts +1 -1
- package/src/compile/headlessBundle.ts +133 -133
- package/src/compile/nativeStack.ts +1 -1
- package/src/compile/projectCompile.ts +10 -2
- package/src/compile/sceneCompile.ts +8 -22
- package/src/declarations/lecodes-3d-editor.d.ts +4 -0
- package/src/declarations/lecodes-assets.d.ts +1 -1
- package/src/declarations/{lecodes-renderer.d.ts → lecodes-headless.d.ts} +3 -3
- package/src/dev/clientTemplates.ts +18 -14
- package/src/dev/devServer.ts +75 -38
- package/src/dev/iosDev.ts +180 -0
- package/src/dev/webRunner.ts +82 -14
- package/src/hosts/cmgenTool.ts +1 -1
- package/src/hosts/desktopRenderer.ts +98 -27
- package/src/hosts/desktopScript.ts +150 -35
- package/src/hosts/distRoot.ts +1 -1
- package/src/hosts/matcTool.ts +1 -1
- package/src/hosts/peerInstall.ts +5 -5
- package/src/hosts/peers.ts +2 -2
- package/src/index.ts +4 -1
- 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/project/ignore.ts +164 -163
- package/runtime/materials/lightmap-baked-lite.mat +0 -180
- package/runtime/materials/lightmap-baked.mat +0 -181
- 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-DHqTFhWB.js +0 -866
- package/runtime/web/assets/draco-BiISTFcR.js +0 -118
- package/runtime/web/assets/draco_decoder-DsQ12WqX.wasm +0 -0
- package/runtime/web/assets/index-Q06c6oHx.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
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// The contract's model → the model of the NATIVE emitters (Swift, Kotlin): every type a native
|
|
2
|
+
// language must declare has a NAME. TypeScript writes `{ message: string }`, `"front" | "back"` and
|
|
3
|
+
// `f64 | string` in place; Swift and Kotlin need a struct, an enum and a tagged union for them, so
|
|
4
|
+
// the anonymous ones are named after where they stand (`PushStatus.permission` →
|
|
5
|
+
// `PushStatusPermission`) and the aliases are resolved.
|
|
6
|
+
//
|
|
7
|
+
// Also decided here, once for both languages: `T | null` and an optional member are ONE thing on
|
|
8
|
+
// the native side (an optional), and a union is told apart by what its members are on the wire —
|
|
9
|
+
// a union the wire cannot tell apart is refused.
|
|
10
|
+
|
|
11
|
+
import type { Channel, Contract, Decl, Field, Type } from "./contract"
|
|
12
|
+
import { EmitError } from "./emitSdk"
|
|
13
|
+
|
|
14
|
+
export type Scalar = "i32" | "f64" | "boolean" | "string" | "json" | "file" | "bytes" | "void"
|
|
15
|
+
|
|
16
|
+
export type L =
|
|
17
|
+
| { kind: Scalar }
|
|
18
|
+
| { kind: "enum" | "struct" | "union", name: string }
|
|
19
|
+
| { kind: "list" | "map" | "optional", of: L }
|
|
20
|
+
| { kind: "tuple", of: L[] }
|
|
21
|
+
|
|
22
|
+
export type LField = {
|
|
23
|
+
name: string
|
|
24
|
+
/** `optional` when the member may be absent or null. */
|
|
25
|
+
type: L
|
|
26
|
+
/** Absent when nil (an optional member); a null on the wire otherwise (`T | null`). */
|
|
27
|
+
omit: boolean
|
|
28
|
+
doc: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LDecl =
|
|
32
|
+
| { kind: "enum", name: string, values: string[], doc: string }
|
|
33
|
+
| { kind: "struct", name: string, fields: LField[], doc: string }
|
|
34
|
+
| { kind: "union", name: string, members: L[], doc: string }
|
|
35
|
+
|
|
36
|
+
/** `omit`: the argument may be left out (`name?:`); a nullable one (`T | null`) is always named. */
|
|
37
|
+
export type LArg = { name: string, type: L, omit: boolean }
|
|
38
|
+
|
|
39
|
+
export type LMethod = { name: string, args: LArg[], result: L, since: number, rejects: string[], doc: string }
|
|
40
|
+
|
|
41
|
+
export type LEvent = {
|
|
42
|
+
name: string
|
|
43
|
+
payload: L
|
|
44
|
+
/** The payload was written in place (`error: { message: string }`): its members are the
|
|
45
|
+
* arguments of the event's function, no struct is declared for it. */
|
|
46
|
+
inline?: LField[]
|
|
47
|
+
since: number
|
|
48
|
+
doc: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type LChannel = {
|
|
52
|
+
kind: "view" | "service"
|
|
53
|
+
name: string
|
|
54
|
+
iface: string
|
|
55
|
+
version: number
|
|
56
|
+
/** The struct of the params; the channel declares an empty one when the marker names none. */
|
|
57
|
+
params: string
|
|
58
|
+
methods: LMethod[]
|
|
59
|
+
events: LEvent[]
|
|
60
|
+
/** Every code a method of the channel rejects with, in the order they appear. */
|
|
61
|
+
codes: string[]
|
|
62
|
+
doc: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type Lowered = { decls: LDecl[], channels: LChannel[] }
|
|
66
|
+
|
|
67
|
+
const upper = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1)
|
|
68
|
+
|
|
69
|
+
/** What a value is on the wire — two members of a union must differ in it, which is how a reader
|
|
70
|
+
* tells them apart: by what the value at hand IS, before it reads it. */
|
|
71
|
+
export const wireClass = (t: L): string => {
|
|
72
|
+
switch (t.kind) {
|
|
73
|
+
case "i32": case "f64": return "number"
|
|
74
|
+
case "string": case "enum": return "string"
|
|
75
|
+
case "boolean": return "boolean"
|
|
76
|
+
case "struct": case "map": return "object"
|
|
77
|
+
case "list": case "tuple": return "array"
|
|
78
|
+
default: return t.kind
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const lower = (contract: Contract): Lowered => {
|
|
83
|
+
const source = new Map<string, Decl>(contract.decls.map(d => [d.name, d]))
|
|
84
|
+
const decls = new Map<string, LDecl>()
|
|
85
|
+
const resolving = new Set<string>()
|
|
86
|
+
const aliases = new Map<string, L>()
|
|
87
|
+
|
|
88
|
+
const addDecl = (d: LDecl, where: string): void => {
|
|
89
|
+
const taken = decls.get(d.name)
|
|
90
|
+
if (taken && JSON.stringify(taken) !== JSON.stringify(d)) {
|
|
91
|
+
throw new EmitError(`${where}: the name ${d.name} the generator gives this type is taken — name the type yourself (export type ${d.name}… = …)`)
|
|
92
|
+
}
|
|
93
|
+
decls.set(d.name, d)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fields = (list: Field[], owner: string): LField[] => list.map(f => {
|
|
97
|
+
const t = type(f.type, `${owner}${upper(f.name)}`, `${owner}.${f.name}`)
|
|
98
|
+
const optional = f.optional || t.kind === "optional"
|
|
99
|
+
return { name: f.name, type: optional && t.kind !== "optional" ? { kind: "optional", of: t } : t, omit: f.optional, doc: f.doc }
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/** `name`: what an anonymous type is called if this one needs a declaration. */
|
|
103
|
+
const type = (t: Type, name: string, where: string): L => {
|
|
104
|
+
switch (t.kind) {
|
|
105
|
+
case "ref": return named(t.name, where)
|
|
106
|
+
case "literals": addDecl({ kind: "enum", name, values: t.values, doc: "" }, where); return { kind: "enum", name }
|
|
107
|
+
case "object": addDecl({ kind: "struct", name, fields: fields(t.fields, name), doc: "" }, where); return { kind: "struct", name }
|
|
108
|
+
case "map": return { kind: "map", of: type(t.of, `${name}Value`, where) }
|
|
109
|
+
case "list": return { kind: "list", of: type(t.of, `${name}Item`, where) }
|
|
110
|
+
case "tuple": return { kind: "tuple", of: t.of.map((m, i) => type(m, `${name}${i}`, where)) }
|
|
111
|
+
case "nullable": {
|
|
112
|
+
const of = type(t.of, name, where)
|
|
113
|
+
return of.kind === "optional" ? of : { kind: "optional", of }
|
|
114
|
+
}
|
|
115
|
+
case "union": {
|
|
116
|
+
const members = t.of.map((m, i) => type(m, `${name}${upper(memberLabel(m, i))}`, where))
|
|
117
|
+
const seen = new Map<string, L>()
|
|
118
|
+
for (const m of members) {
|
|
119
|
+
if (m.kind === "json" || m.kind === "file" || m.kind === "bytes" || m.kind === "optional" || m.kind === "union") {
|
|
120
|
+
throw new EmitError(`${where}: a union holds scalars, enums, structs, lists — not ${m.kind === "optional" ? "a nullable member" : m.kind}`)
|
|
121
|
+
}
|
|
122
|
+
const c = wireClass(m)
|
|
123
|
+
if (seen.has(c)) throw new EmitError(`${where}: two members of the union are both a ${c} on the wire — a half cannot tell them apart`)
|
|
124
|
+
seen.set(c, m)
|
|
125
|
+
}
|
|
126
|
+
addDecl({ kind: "union", name, members, doc: "" }, where)
|
|
127
|
+
return { kind: "union", name }
|
|
128
|
+
}
|
|
129
|
+
default: return { kind: t.kind }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const memberLabel = (t: Type, i: number): string => t.kind === "ref" ? t.name : t.kind === "literals" ? "name" : `${t.kind}${i}`
|
|
134
|
+
|
|
135
|
+
/** A declaration of the contract by its name: a struct, or an alias resolved to what it names. */
|
|
136
|
+
const named = (name: string, where: string): L => {
|
|
137
|
+
const d = source.get(name)!
|
|
138
|
+
if (d.kind === "struct") {
|
|
139
|
+
if (!decls.has(name) && !resolving.has(name)) {
|
|
140
|
+
resolving.add(name)
|
|
141
|
+
const f = fields(d.fields, name)
|
|
142
|
+
resolving.delete(name)
|
|
143
|
+
addDecl({ kind: "struct", name, fields: f, doc: d.doc }, where)
|
|
144
|
+
}
|
|
145
|
+
return { kind: "struct", name }
|
|
146
|
+
}
|
|
147
|
+
const known = aliases.get(name)
|
|
148
|
+
if (known) return known
|
|
149
|
+
if (resolving.has(name)) throw new EmitError(`${where}: the alias ${name} names itself`)
|
|
150
|
+
resolving.add(name)
|
|
151
|
+
const l = type(d.type, name, name)
|
|
152
|
+
resolving.delete(name)
|
|
153
|
+
aliases.set(name, l)
|
|
154
|
+
const own = decls.get(name)
|
|
155
|
+
if (own && own.doc === "" && d.doc) decls.set(name, { ...own, doc: d.doc })
|
|
156
|
+
return l
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const channels = contract.channels.map((ch: Channel): LChannel => {
|
|
160
|
+
const iface = ch.iface
|
|
161
|
+
const params = ch.paramsType ?? `${iface}Params`
|
|
162
|
+
if (ch.paramsType) named(ch.paramsType, iface)
|
|
163
|
+
else addDecl({ kind: "struct", name: params, fields: [], doc: "" }, iface)
|
|
164
|
+
|
|
165
|
+
const methods = ch.methods.map((m): LMethod => {
|
|
166
|
+
const where = `${iface}.${m.name}`
|
|
167
|
+
return {
|
|
168
|
+
name: m.name,
|
|
169
|
+
args: m.args.map(a => {
|
|
170
|
+
const t = type(a.type, `${iface}${upper(m.name)}${upper(a.name)}`, `${where}(${a.name})`)
|
|
171
|
+
return { name: a.name, type: a.optional && t.kind !== "optional" ? { kind: "optional", of: t } : t, omit: a.optional }
|
|
172
|
+
}),
|
|
173
|
+
result: m.result.kind === "void" ? { kind: "void" } : type(m.result, `${iface}${upper(m.name)}Result`, `${where}()`),
|
|
174
|
+
since: m.since, rejects: m.rejects, doc: m.doc,
|
|
175
|
+
}
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const events = ch.events.map((e): LEvent => {
|
|
179
|
+
const where = `${iface} event ${e.name}`
|
|
180
|
+
if (e.payload.kind === "object") {
|
|
181
|
+
return { name: e.name, payload: { kind: "void" }, inline: fields(e.payload.fields, `${iface}${upper(e.name)}`), since: e.since, doc: e.doc }
|
|
182
|
+
}
|
|
183
|
+
return { name: e.name, payload: e.payload.kind === "void" ? { kind: "void" } : type(e.payload, `${iface}${upper(e.name)}Payload`, where), since: e.since, doc: e.doc }
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
const codes: string[] = []
|
|
187
|
+
for (const m of methods) for (const c of m.rejects) if (!codes.includes(c)) codes.push(c)
|
|
188
|
+
return { kind: ch.kind, name: ch.name, iface, version: ch.version, params, methods, events, codes, doc: ch.doc }
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// Only what a channel reaches is declared: a struct of the contract nothing names is not a type
|
|
192
|
+
// a half ever sees.
|
|
193
|
+
return { decls: [...decls.values()], channels }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Which way a declaration crosses: the app SENDS it (an argument, a param) → it is read here; the
|
|
197
|
+
* plugin sends it (a result, an event) → it is written here. Only the codec a way needs is
|
|
198
|
+
* emitted by a native emitter — a struct that carries a File is written, never read. */
|
|
199
|
+
export type Ways = { read: Set<string>, write: Set<string> }
|
|
200
|
+
|
|
201
|
+
export const waysOf = (lowered: Lowered): Ways => {
|
|
202
|
+
const byName = new Map(lowered.decls.map(d => [d.name, d]))
|
|
203
|
+
const ways: Ways = { read: new Set(), write: new Set() }
|
|
204
|
+
const walk = (t: L, set: Set<string>): void => {
|
|
205
|
+
switch (t.kind) {
|
|
206
|
+
case "list": case "map": case "optional": walk(t.of, set); return
|
|
207
|
+
case "tuple": t.of.forEach(m => walk(m, set)); return
|
|
208
|
+
case "enum": case "struct": case "union": {
|
|
209
|
+
if (set.has(t.name)) return
|
|
210
|
+
set.add(t.name)
|
|
211
|
+
const d = byName.get(t.name)!
|
|
212
|
+
if (d.kind === "struct") d.fields.forEach(f => walk(f.type, set))
|
|
213
|
+
if (d.kind === "union") d.members.forEach(m => walk(m, set))
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
default: return
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
for (const ch of lowered.channels) {
|
|
220
|
+
walk({ kind: "struct", name: ch.params }, ways.read)
|
|
221
|
+
for (const m of ch.methods) {
|
|
222
|
+
m.args.forEach(a => walk(a.type, ways.read))
|
|
223
|
+
walk(m.result, ways.write)
|
|
224
|
+
}
|
|
225
|
+
for (const e of ch.events) {
|
|
226
|
+
walk(e.payload, ways.write)
|
|
227
|
+
e.inline?.forEach(f => walk(f.type, ways.write))
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return ways
|
|
231
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// A plugin's manifest (`lecodes-plugin.json`, 2.x): what a host needs to BUILD the plugin in — the
|
|
2
|
+
// type that registers each half, what a half asks of its platform (usage strings, entitlements,
|
|
3
|
+
// packages, Gradle dependencies, bundler resources). What the plugin DOES is not here: the channels,
|
|
4
|
+
// their names and their types are the contract's (contract.d.ts), the folders are the convention
|
|
5
|
+
// (`sdk/`, `ios/`, `android/`, `web/`).
|
|
6
|
+
//
|
|
7
|
+
// A platform is a key of the manifest, ALWAYS: an object where the plugin has a half, `null` where
|
|
8
|
+
// it has none. A missing key is refused — "not supported" is a statement, not an omission.
|
|
9
|
+
//
|
|
10
|
+
// Desktop plugins are another system (a prebuilt library behind a C ABI) with a manifest of their
|
|
11
|
+
// own shape; nothing of it is read here.
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
14
|
+
import { join } from "node:path"
|
|
15
|
+
|
|
16
|
+
export const MANIFEST_FILE = "lecodes-plugin.json"
|
|
17
|
+
|
|
18
|
+
/** A SwiftPM dependency the iOS half links. */
|
|
19
|
+
export type IosPackage = { url: string, exact?: string, from?: string, products: string[] }
|
|
20
|
+
|
|
21
|
+
/** What a web half asks of its bundler: a file of a package, handed over as a URL of a script that
|
|
22
|
+
* stands alone (`worker-url`), as a URL (`url`) or as its text (`text`). */
|
|
23
|
+
export type WebResource = { module: string, as: "worker-url" | "url" | "text" }
|
|
24
|
+
|
|
25
|
+
export type PluginManifest = {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
version: string
|
|
29
|
+
/** The SDK the plugin is written for, as a range of its ONE version (`^2.0.0`): the major is the
|
|
30
|
+
* contract between a bundle, the runtime and a half. */
|
|
31
|
+
sdk: string
|
|
32
|
+
description?: string
|
|
33
|
+
ios: {
|
|
34
|
+
/** The Swift module the half is built as in a shell. Default: `LeCodes` + the id in PascalCase. */
|
|
35
|
+
module?: string
|
|
36
|
+
/** The type with `static func register(in: LeCodesEngine)`. */
|
|
37
|
+
register: string
|
|
38
|
+
infoPlist?: Record<string, string>
|
|
39
|
+
entitlements?: Record<string, unknown>
|
|
40
|
+
packages?: IosPackage[]
|
|
41
|
+
} | null
|
|
42
|
+
android: {
|
|
43
|
+
/** The Gradle module the half is built as in a shell. Default: `lecodes-` + the id. */
|
|
44
|
+
module?: string
|
|
45
|
+
/** The object with `fun register(engine: LecodesEngine, context: Context)`, by its full name. */
|
|
46
|
+
register: string
|
|
47
|
+
/** Maven coordinates; a `platform:` prefix is a BoM. */
|
|
48
|
+
dependencies?: string[]
|
|
49
|
+
/** App-level Gradle plugins the shell applies (`com.google.gms.google-services`). */
|
|
50
|
+
gradlePlugins?: string[]
|
|
51
|
+
} | null
|
|
52
|
+
web: {
|
|
53
|
+
/** The module that exports `register(host, resources?)`, relative to the plugin. */
|
|
54
|
+
register: string
|
|
55
|
+
/** npm packages the half imports, as a package.json's dependencies. */
|
|
56
|
+
dependencies?: Record<string, string>
|
|
57
|
+
/** The half's bundler resources, by the name `register` receives them under. */
|
|
58
|
+
resources?: Record<string, WebResource>
|
|
59
|
+
} | null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class ManifestError extends Error {
|
|
63
|
+
constructor(readonly file: string, readonly problems: string[]) {
|
|
64
|
+
super(`${file}:\n${problems.map(p => ` ${p}`).join("\n")}`)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
69
|
+
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/
|
|
70
|
+
const RANGE = /^[\^~]?\d+\.\d+\.\d+$/
|
|
71
|
+
const PLATFORMS = ["ios", "android", "web"] as const
|
|
72
|
+
const KNOWN = new Set(["id", "name", "version", "sdk", "description", ...PLATFORMS])
|
|
73
|
+
const GONE: Record<string, string> = {
|
|
74
|
+
provides: "the channels are the contract's (contract.d.ts)",
|
|
75
|
+
js: "the wrapper is found by convention (sdk/)",
|
|
76
|
+
desktop: "desktop plugins are another system, with a manifest of their own",
|
|
77
|
+
}
|
|
78
|
+
const BRANCH: Record<(typeof PLATFORMS)[number], Set<string>> = {
|
|
79
|
+
ios: new Set(["module", "register", "infoPlist", "entitlements", "packages"]),
|
|
80
|
+
android: new Set(["module", "register", "dependencies", "gradlePlugins"]),
|
|
81
|
+
web: new Set(["register", "dependencies", "resources"]),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
85
|
+
const isFolder = (path: string): boolean => existsSync(path) && statSync(path).isDirectory()
|
|
86
|
+
|
|
87
|
+
export const pascal = (id: string): string => id.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("")
|
|
88
|
+
|
|
89
|
+
/** The manifest of the plugin in `dir` (its folder's name is its id). Every problem of the file is
|
|
90
|
+
* reported at once. */
|
|
91
|
+
export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
92
|
+
const file = join(dir, MANIFEST_FILE)
|
|
93
|
+
let raw: unknown
|
|
94
|
+
try { raw = JSON.parse(readFileSync(file, "utf8").replace(/^/, "")) } catch (e) {
|
|
95
|
+
throw new ManifestError(file, [`not JSON: ${e instanceof Error ? e.message : String(e)}`])
|
|
96
|
+
}
|
|
97
|
+
if (!isObject(raw)) throw new ManifestError(file, ["a manifest is an object"])
|
|
98
|
+
|
|
99
|
+
const problems: string[] = []
|
|
100
|
+
const text = (key: string, of: Record<string, unknown> = raw, where = ""): string | undefined => {
|
|
101
|
+
const v = of[key]
|
|
102
|
+
if (typeof v === "string" && v.trim() !== "") return v
|
|
103
|
+
problems.push(`${where}${key}: ${v === undefined ? "missing" : "a text"}`)
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const key of Object.keys(raw)) {
|
|
108
|
+
if (GONE[key]) problems.push(`${key}: gone in 2.x — ${GONE[key]}`)
|
|
109
|
+
else if (!KNOWN.has(key)) problems.push(`${key}: not a key of a manifest`)
|
|
110
|
+
}
|
|
111
|
+
if (text("id") !== undefined && raw.id !== id) problems.push(`id: "${String(raw.id)}" — the plugin's folder is "${id}", and the folder is the id`)
|
|
112
|
+
if (!ID.test(id)) problems.push(`id: "${id}" — lower-case words joined by "-"`)
|
|
113
|
+
text("name")
|
|
114
|
+
const version = text("version")
|
|
115
|
+
if (version !== undefined && !VERSION.test(version)) problems.push(`version: "${version}" — a version is 2.0.0`)
|
|
116
|
+
const sdk = text("sdk")
|
|
117
|
+
if (sdk !== undefined && !RANGE.test(sdk)) problems.push(`sdk: "${sdk}" — a range of the SDK's version: ^2.0.0`)
|
|
118
|
+
if (raw.description !== undefined && typeof raw.description !== "string") problems.push("description: a text")
|
|
119
|
+
|
|
120
|
+
for (const platform of PLATFORMS) {
|
|
121
|
+
const branch = raw[platform]
|
|
122
|
+
const folder = isFolder(join(dir, platform))
|
|
123
|
+
if (branch === undefined) { problems.push(`${platform}: missing — an object, or null where the plugin has no ${platform} half`); continue }
|
|
124
|
+
if (branch === null) {
|
|
125
|
+
if (folder) problems.push(`${platform}: null, and the plugin has a ${platform}/ folder`)
|
|
126
|
+
continue
|
|
127
|
+
}
|
|
128
|
+
if (!isObject(branch)) { problems.push(`${platform}: an object, or null`); continue }
|
|
129
|
+
if (!folder) problems.push(`${platform}: the plugin has no ${platform}/ folder — say null`)
|
|
130
|
+
for (const key of Object.keys(branch)) {
|
|
131
|
+
if (key === "sources" || key === "minSdk") problems.push(`${platform}.${key}: gone in 2.x — ${key === "sources" ? `the half is the ${platform}/ folder` : "the SDK a plugin needs is `sdk`, one for every platform"}`)
|
|
132
|
+
else if (!BRANCH[platform].has(key)) problems.push(`${platform}.${key}: not a key of a manifest`)
|
|
133
|
+
}
|
|
134
|
+
const register = text("register", branch, `${platform}.`)
|
|
135
|
+
if (platform === "android" && register !== undefined && !/^[A-Za-z_][\w]*(\.[A-Za-z_][\w]*)+$/.test(register)) {
|
|
136
|
+
problems.push(`android.register: "${register}" — the object's full name (com.example.plugin.Plugin)`)
|
|
137
|
+
}
|
|
138
|
+
if (platform === "ios" && register !== undefined && !/^[A-Za-z_]\w*$/.test(register)) problems.push(`ios.register: "${register}" — a type's name`)
|
|
139
|
+
if (platform === "web" && register !== undefined) {
|
|
140
|
+
if (!register.startsWith("web/")) problems.push(`web.register: "${register}" — a module of the web/ folder`)
|
|
141
|
+
else if (!existsSync(join(dir, register))) problems.push(`web.register: ${register} is not there`)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const ios = isObject(raw.ios) ? raw.ios : null
|
|
146
|
+
if (ios) {
|
|
147
|
+
for (const key of ["infoPlist"] as const) {
|
|
148
|
+
const v = ios[key]
|
|
149
|
+
if (v !== undefined && (!isObject(v) || Object.values(v).some(x => typeof x !== "string"))) problems.push(`ios.${key}: a key → a text`)
|
|
150
|
+
}
|
|
151
|
+
if (ios.entitlements !== undefined && !isObject(ios.entitlements)) problems.push("ios.entitlements: an object")
|
|
152
|
+
if (ios.packages !== undefined) {
|
|
153
|
+
if (!Array.isArray(ios.packages)) problems.push("ios.packages: a list")
|
|
154
|
+
else ios.packages.forEach((p, i) => {
|
|
155
|
+
if (!isObject(p) || typeof p.url !== "string" || !Array.isArray(p.products) || p.products.length === 0 || p.products.some(x => typeof x !== "string")) {
|
|
156
|
+
problems.push(`ios.packages[${i}]: { url, exact | from, products }`)
|
|
157
|
+
} else if ((typeof p.exact === "string") === (typeof p.from === "string")) {
|
|
158
|
+
problems.push(`ios.packages[${i}]: one of exact and from`)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const android = isObject(raw.android) ? raw.android : null
|
|
164
|
+
if (android) {
|
|
165
|
+
for (const key of ["dependencies", "gradlePlugins"] as const) {
|
|
166
|
+
const v = android[key]
|
|
167
|
+
if (v !== undefined && (!Array.isArray(v) || v.some(x => typeof x !== "string"))) problems.push(`android.${key}: a list of texts`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const web = isObject(raw.web) ? raw.web : null
|
|
171
|
+
if (web) {
|
|
172
|
+
const deps = web.dependencies
|
|
173
|
+
if (deps !== undefined && (!isObject(deps) || Object.values(deps).some(x => typeof x !== "string"))) problems.push("web.dependencies: a package → its range")
|
|
174
|
+
const resources = web.resources
|
|
175
|
+
if (resources !== undefined) {
|
|
176
|
+
if (!isObject(resources)) problems.push("web.resources: a name → { module, as }")
|
|
177
|
+
else for (const [name, r] of Object.entries(resources)) {
|
|
178
|
+
if (!/^[A-Za-z_]\w*$/.test(name)) problems.push(`web.resources.${name}: a name the half reads it under`)
|
|
179
|
+
if (!isObject(r) || typeof r.module !== "string" || (r.as !== "worker-url" && r.as !== "url" && r.as !== "text")) {
|
|
180
|
+
problems.push(`web.resources.${name}: { module, as: "worker-url" | "url" | "text" }`)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (problems.length) throw new ManifestError(file, problems)
|
|
187
|
+
return raw as unknown as PluginManifest
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** The manifest, or null where the plugin has none yet (a contract is where a plugin starts). */
|
|
191
|
+
export const readManifestIfAny = (dir: string, id: string): PluginManifest | null =>
|
|
192
|
+
existsSync(join(dir, MANIFEST_FILE)) ? readManifest(dir, id) : null
|