lecodes-cli 2.2.6 → 2.2.9
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 +234 -37
- package/package.json +8 -8
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/browser-BpFb50ko.js +1 -0
- package/runtime/web/assets/webrtc-CbCltXvl.js +1 -0
- package/runtime/web/player.js +6 -6
- package/runtime/web-lite/lite.js +1 -1
- package/runtime/web-lite/plugins/browser/index-BpFb50ko.js +1 -0
- package/runtime/web-lite/plugins/webrtc/index-CbCltXvl.js +1 -0
- package/runtime/web-lite/plugins.json +3 -1
- package/src/commands/app/android.ts +30 -3
- package/src/commands/app/desktop.ts +3 -2
- package/src/commands/app/index.ts +4 -4
- package/src/commands/app/shared.ts +19 -2
- package/src/commands/app/templates/android.ts +14 -2
- package/src/commands/app/templates/ios.ts +35 -3
- package/src/commands/plugin.ts +8 -2
- package/src/plugin/emitPackage.ts +98 -0
- package/src/plugin/generate.ts +51 -16
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// A plugin repo's `Package.swift`, from the manifests: the plugins with an iOS half as ONE SwiftPM
|
|
2
|
+
// package — a target per plugin (its sources `plugins/<id>/ios`, the packages its manifest names)
|
|
3
|
+
// and a product per plugin. Each plugin is its own MODULE, so two plugins may declare a type of the
|
|
4
|
+
// same name, and an Xcode project links a plugin as a product instead of compiling its folder.
|
|
5
|
+
//
|
|
6
|
+
// The targets depend on the LeCodes SDK as a package on disk: `LECODES_IOS_SDK_PATH`, else the path
|
|
7
|
+
// the repo's package.json names (`lecodes.iosSdkPath`), else the sibling `../lecodes` (a checkout
|
|
8
|
+
// of the published SDK package, or whatever stands there).
|
|
9
|
+
//
|
|
10
|
+
// A shell made by `lecodes app sync` does NOT use this package: it vendors the sources into targets
|
|
11
|
+
// of its own (templates/ios.ts), pinned by plugins.lock.json.
|
|
12
|
+
|
|
13
|
+
import type { PluginManifest } from "./manifest"
|
|
14
|
+
import { pascal } from "./manifest"
|
|
15
|
+
|
|
16
|
+
export class PackageError extends Error {}
|
|
17
|
+
|
|
18
|
+
/** A package's identity from its URL (`https://github.com/stasel/WebRTC` → `WebRTC`): what SwiftPM
|
|
19
|
+
* names it in `.product(name:package:)`. The same rule as the shell's (commands/app/shared.ts). */
|
|
20
|
+
export const swiftPackageName = (url: string): string =>
|
|
21
|
+
(url.replace(/\/+$/, "").split("/").pop() ?? url).replace(/\.git$/, "")
|
|
22
|
+
|
|
23
|
+
/** The Swift module of a plugin's iOS half: the manifest's, or `LeCodes` + the id in PascalCase. */
|
|
24
|
+
export const iosModule = (id: string, manifest: PluginManifest): string => manifest.ios?.module ?? `LeCodes${pascal(id)}`
|
|
25
|
+
|
|
26
|
+
export const PACKAGE_FILE = "Package.swift"
|
|
27
|
+
|
|
28
|
+
/** Where the SDK package stands when neither the environment nor the repo says: a sibling checkout. */
|
|
29
|
+
export const DEFAULT_SDK_PATH = "../lecodes"
|
|
30
|
+
|
|
31
|
+
/** The text of the repo's Package.swift for `plugins` (every manifest; those without an iOS half are
|
|
32
|
+
* left out); `sdkPath` is the repo's own SDK location (package.json `lecodes.iosSdkPath`). Throws
|
|
33
|
+
* PackageError when two plugins pin one package at different versions. */
|
|
34
|
+
export const emitPackageSwift = (plugins: { id: string, manifest: PluginManifest }[], sdkPath: string = DEFAULT_SDK_PATH): string => {
|
|
35
|
+
const ios = plugins.filter(p => p.manifest.ios !== null)
|
|
36
|
+
const packages = new Map<string, { url: string, exact?: string, from?: string, by: string }>()
|
|
37
|
+
for (const p of ios) {
|
|
38
|
+
for (const pkg of p.manifest.ios!.packages ?? []) {
|
|
39
|
+
const prior = packages.get(pkg.url)
|
|
40
|
+
if (prior && (prior.exact !== pkg.exact || prior.from !== pkg.from)) {
|
|
41
|
+
throw new PackageError(`"${p.id}" pins ${pkg.url} at ${pkg.exact ?? `from ${pkg.from}`} but "${prior.by}" pins ${prior.exact ?? `from ${prior.from}`} — one package resolves once`)
|
|
42
|
+
}
|
|
43
|
+
if (!prior) packages.set(pkg.url, { ...pkg, by: p.id })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const moduleOf = (p: { id: string, manifest: PluginManifest }) => iosModule(p.id, p.manifest)
|
|
47
|
+
|
|
48
|
+
const products = ios.map(p => ` .library(name: "${moduleOf(p)}", targets: ["${moduleOf(p)}"]),`)
|
|
49
|
+
const dependencies = [
|
|
50
|
+
` .package(name: "lecodes", path: sdk),`,
|
|
51
|
+
...[...packages.values()].map(pkg => ` .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`),
|
|
52
|
+
]
|
|
53
|
+
const targets = ios.map(p => {
|
|
54
|
+
const deps = [
|
|
55
|
+
`.product(name: "LeCodes", package: "lecodes")`,
|
|
56
|
+
...(p.manifest.ios!.packages ?? []).flatMap(pkg => pkg.products.map(name => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`)),
|
|
57
|
+
]
|
|
58
|
+
return [
|
|
59
|
+
` // ${p.id}`,
|
|
60
|
+
` .target(`,
|
|
61
|
+
` name: "${moduleOf(p)}",`,
|
|
62
|
+
` dependencies: [${deps.join(", ")}],`,
|
|
63
|
+
` path: "plugins/${p.id}/ios"`,
|
|
64
|
+
` ),`,
|
|
65
|
+
].join("\n")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
return `// swift-tools-version:5.9
|
|
69
|
+
// GENERATED by \`lecodes plugin gen\` from the plugins' manifests (lecodes-plugin.json) — do not edit.
|
|
70
|
+
//
|
|
71
|
+
// The plugins with an iOS half as ONE SwiftPM package: a target per plugin (its sources in
|
|
72
|
+
// plugins/<id>/ios, the packages its manifest names) and a product per plugin. Each plugin is its
|
|
73
|
+
// own module: an Xcode project links a plugin as a product instead of compiling its folder, and two
|
|
74
|
+
// plugins may name a type alike.
|
|
75
|
+
//
|
|
76
|
+
// The SDK is the LeCodes package on disk: LECODES_IOS_SDK_PATH, else the path below (the repo's
|
|
77
|
+
// package.json \`lecodes.iosSdkPath\`, or the sibling ../lecodes). A shell made by \`lecodes app sync\`
|
|
78
|
+
// does not use this package: it vendors the sources into targets of its own, pinned by
|
|
79
|
+
// plugins.lock.json.
|
|
80
|
+
import PackageDescription
|
|
81
|
+
|
|
82
|
+
let sdk = Context.environment["LECODES_IOS_SDK_PATH"] ?? ${JSON.stringify(sdkPath)}
|
|
83
|
+
|
|
84
|
+
let package = Package(
|
|
85
|
+
name: "lecodes-plugins",
|
|
86
|
+
platforms: [.iOS(.v15)],
|
|
87
|
+
products: [
|
|
88
|
+
${products.join("\n")}
|
|
89
|
+
],
|
|
90
|
+
dependencies: [
|
|
91
|
+
${dependencies.join("\n")}
|
|
92
|
+
],
|
|
93
|
+
targets: [
|
|
94
|
+
${targets.join("\n")}
|
|
95
|
+
]
|
|
96
|
+
)
|
|
97
|
+
`
|
|
98
|
+
}
|
package/src/plugin/generate.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
// with a `contract.d.ts`; the path is one plugin, or a repo that keeps them in `plugins/<id>/`.
|
|
3
3
|
// Generated files are written beside the hand-written ones of the place they travel to
|
|
4
4
|
// (`sdk/<id>.gen.ts`, `ios/<Name>.gen.swift`, `android/…/<Name>.gen.kt`, `web/<id>.gen.ts`) and are
|
|
5
|
-
// committed — `check` is what CI runs.
|
|
5
|
+
// committed — `check` is what CI runs. A REPO of plugins also gets its `Package.swift` (the iOS
|
|
6
|
+
// halves as one SwiftPM package, emitPackage.ts) from the manifests.
|
|
6
7
|
|
|
7
8
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs"
|
|
8
9
|
import { createRequire } from "node:module"
|
|
@@ -15,7 +16,8 @@ import { EmitError, emitSdk } from "./emitSdk"
|
|
|
15
16
|
import { emitSwift } from "./emitSwift"
|
|
16
17
|
import { emitWeb } from "./emitWeb"
|
|
17
18
|
import { lower } from "./lower"
|
|
18
|
-
import { ManifestError, readManifestIfAny } from "./manifest"
|
|
19
|
+
import { ManifestError, readManifestIfAny, type PluginManifest } from "./manifest"
|
|
20
|
+
import { DEFAULT_SDK_PATH, PACKAGE_FILE, PackageError, emitPackageSwift } from "./emitPackage"
|
|
19
21
|
|
|
20
22
|
// A path as the report shows it: relative to `from`, with `/` on every platform (a report is
|
|
21
23
|
// compared and pasted; `\` is Windows' own).
|
|
@@ -52,12 +54,12 @@ export const loadTypescript = (dir: string): typeof tsModule => {
|
|
|
52
54
|
export type Generated = { file: string, text: string }
|
|
53
55
|
|
|
54
56
|
/** Everything one plugin's contract generates, as `path → text`. Throws ContractError / EmitError. */
|
|
55
|
-
export const generatePlugin = (ts: typeof tsModule, plugin: PluginDir): { contract: Contract, files: Generated[] } => {
|
|
57
|
+
export const generatePlugin = (ts: typeof tsModule, plugin: PluginDir): { contract: Contract, manifest: PluginManifest | null, files: Generated[] } => {
|
|
56
58
|
const file = join(plugin.dir, CONTRACT_FILE)
|
|
57
59
|
const contract = readContract(ts, file, readFileSync(file, "utf8"))
|
|
58
60
|
// The manifest is held to the folders the plugin has: a half the manifest does not name is built
|
|
59
61
|
// into no host.
|
|
60
|
-
readManifestIfAny(plugin.dir, plugin.id)
|
|
62
|
+
const manifest = readManifestIfAny(plugin.dir, plugin.id)
|
|
61
63
|
const handWritten = existsSync(join(plugin.dir, "sdk", `${plugin.id}.ts`))
|
|
62
64
|
const single = contract.channels.length === 1
|
|
63
65
|
const files: Generated[] = contract.channels.map(channel => ({
|
|
@@ -81,7 +83,7 @@ export const generatePlugin = (ts: typeof tsModule, plugin: PluginDir): { contra
|
|
|
81
83
|
const foreign = hostImports(join(plugin.dir, "web"))
|
|
82
84
|
if (foreign.length) throw new EmitError(`web/ imports the host — a web half depends on its generated file, the document it is handed and the browser:\n${foreign.map(f => ` ${f}`).join("\n")}`)
|
|
83
85
|
}
|
|
84
|
-
return { contract, files }
|
|
86
|
+
return { contract, manifest, files }
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
/** The imports of a web half that name a LeCodes package (`lecodes-sdk`, `lecodes-web-shared`, …), as
|
|
@@ -121,8 +123,26 @@ export const kotlinPackage = (android: string): { dir: string, name: string } =>
|
|
|
121
123
|
return { dir, name: relative(root, dir).split(/[\\/]/).join(".") }
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
/** The SDK package's path the repo names for its Package.swift: `lecodes.iosSdkPath` of the repo's
|
|
127
|
+
* package.json (the first-party repo, a submodule of core, says `../apple/lecodes`); else the default. */
|
|
128
|
+
export const repoSdkPath = (root: string): string => {
|
|
129
|
+
const file = join(root, "package.json")
|
|
130
|
+
if (!existsSync(file)) return DEFAULT_SDK_PATH
|
|
131
|
+
try {
|
|
132
|
+
const pkg = JSON.parse(readFileSync(file, "utf8")) as { lecodes?: { iosSdkPath?: unknown } }
|
|
133
|
+
const path = pkg.lecodes?.iosSdkPath
|
|
134
|
+
if (path !== undefined && typeof path !== "string") throw new PackageError(`package.json lecodes.iosSdkPath: a path, as a string`)
|
|
135
|
+
return path ?? DEFAULT_SDK_PATH
|
|
136
|
+
} catch (e) {
|
|
137
|
+
if (e instanceof PackageError) throw e
|
|
138
|
+
throw new PackageError(`package.json does not parse: ${e instanceof Error ? e.message : String(e)}`)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
124
142
|
export type GenReport = {
|
|
125
143
|
plugins: { id: string, contract: Contract, written: string[], stale: string[], fresh: string[] }[]
|
|
144
|
+
/** The repo's own files (its Package.swift), when the path is a repo of plugins. */
|
|
145
|
+
repo: { written: string[], stale: string[], fresh: string[] }
|
|
126
146
|
/** Contracts that did not read or emit, as printable text. */
|
|
127
147
|
failures: string[]
|
|
128
148
|
}
|
|
@@ -134,21 +154,24 @@ export const generate = (path: string, opts: { check?: boolean } = {}): GenRepor
|
|
|
134
154
|
if (plugins.length === 0) throw new CliError(`No plugin under ${resolve(path)}: a plugin is a directory with a ${CONTRACT_FILE} (one, or plugins/<id>/ of a repo).`)
|
|
135
155
|
const ts = loadTypescript(plugins[0]!.dir)
|
|
136
156
|
const root = resolve(path)
|
|
137
|
-
const report: GenReport = { plugins: [], failures: [] }
|
|
157
|
+
const report: GenReport = { plugins: [], repo: { written: [], stale: [], fresh: [] }, failures: [] }
|
|
158
|
+
const place = (f: Generated, entry: { written: string[], stale: string[], fresh: string[] }): void => {
|
|
159
|
+
const name = shown(root, f.file) || basename(f.file)
|
|
160
|
+
const current = existsSync(f.file) ? readFileSync(f.file, "utf8") : null
|
|
161
|
+
if (current === f.text) { entry.fresh.push(name); return }
|
|
162
|
+
if (opts.check) { entry.stale.push(name); return }
|
|
163
|
+
mkdirSync(join(f.file, ".."), { recursive: true })
|
|
164
|
+
writeFileSync(f.file, f.text)
|
|
165
|
+
entry.written.push(name)
|
|
166
|
+
}
|
|
167
|
+
const manifests: { id: string, manifest: PluginManifest }[] = []
|
|
138
168
|
|
|
139
169
|
for (const plugin of plugins) {
|
|
140
170
|
try {
|
|
141
|
-
const { contract, files } = generatePlugin(ts, plugin)
|
|
171
|
+
const { contract, manifest, files } = generatePlugin(ts, plugin)
|
|
172
|
+
if (manifest) manifests.push({ id: plugin.id, manifest })
|
|
142
173
|
const entry = { id: plugin.id, contract, written: [] as string[], stale: [] as string[], fresh: [] as string[] }
|
|
143
|
-
for (const f of files)
|
|
144
|
-
const name = shown(root, f.file) || basename(f.file)
|
|
145
|
-
const current = existsSync(f.file) ? readFileSync(f.file, "utf8") : null
|
|
146
|
-
if (current === f.text) { entry.fresh.push(name); continue }
|
|
147
|
-
if (opts.check) { entry.stale.push(name); continue }
|
|
148
|
-
mkdirSync(join(f.file, ".."), { recursive: true })
|
|
149
|
-
writeFileSync(f.file, f.text)
|
|
150
|
-
entry.written.push(name)
|
|
151
|
-
}
|
|
174
|
+
for (const f of files) place(f, entry)
|
|
152
175
|
report.plugins.push(entry)
|
|
153
176
|
} catch (e) {
|
|
154
177
|
if (e instanceof ContractError) report.failures.push(e.problems.map(p => `${shown(root, e.file) || e.file}:${p.line}: ${p.message}`).join("\n"))
|
|
@@ -157,5 +180,17 @@ export const generate = (path: string, opts: { check?: boolean } = {}): GenRepor
|
|
|
157
180
|
else throw e
|
|
158
181
|
}
|
|
159
182
|
}
|
|
183
|
+
|
|
184
|
+
// A repo of plugins (not one plugin): its Package.swift, from every manifest that read. Written
|
|
185
|
+
// only when every contract made it — a repo with a broken plugin keeps the package it has.
|
|
186
|
+
const isRepo = !existsSync(join(root, CONTRACT_FILE))
|
|
187
|
+
if (isRepo && report.failures.length === 0 && manifests.some(m => m.manifest.ios)) {
|
|
188
|
+
try {
|
|
189
|
+
place({ file: join(root, PACKAGE_FILE), text: emitPackageSwift(manifests, repoSdkPath(root)) }, report.repo)
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (e instanceof PackageError) report.failures.push(`${PACKAGE_FILE}: ${e.message}`)
|
|
192
|
+
else throw e
|
|
193
|
+
}
|
|
194
|
+
}
|
|
160
195
|
return report
|
|
161
196
|
}
|