lecodes-cli 2.4.1 → 2.4.3
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 +36134 -11203
- package/package.json +10 -9
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/assets/ar-D6oDL4pw.js +1 -0
- package/runtime/web/assets/ar-mVXX3kRQ.js +2 -0
- package/runtime/web/assets/qrAnchorWorker-Cb5E8cF-.js +1 -0
- package/runtime/web/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web/index.html +1 -0
- package/runtime/web/player.js +12 -12
- package/runtime/web-lite/assets/{index-B2zEJlWS.js → index-oPsDD47E.js} +1 -1
- package/runtime/web-lite/assets/scene3d-BAl_QQyi.js +1105 -0
- package/runtime/web-lite/features.json +2 -2
- package/runtime/web-lite/index.html +2 -1
- package/runtime/web-lite/lite.js +9 -9
- package/runtime/web-lite/plugins/ar/_virtual_lecodes-half-resources-CNYmVR88.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qrAnchorWorker-DcxVKAqo.js +1 -0
- package/runtime/web-lite/plugins/ar/assets/qr_anchor_wasm_bg-D_ePkCCu.wasm +0 -0
- package/runtime/web-lite/plugins/ar/index-C2uuuLhz.js +2 -0
- package/runtime/web-lite/plugins.json +1 -1
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +84 -17
- package/src/commands/app/shared.ts +157 -24
- package/src/commands/app/splash.ts +251 -0
- package/src/commands/app/templates/android.ts +148 -25
- package/src/commands/app/templates/ios.ts +123 -12
- package/src/commands/init/index.ts +195 -195
- package/src/commands/plugin.ts +3 -3
- package/src/commands/push.ts +2 -2
- package/src/commands/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/commands/web.ts +12 -5
- package/src/compile/collect.ts +2 -3
- package/src/compile/collectLocal.ts +13 -2
- package/src/compile/designCompile.ts +2 -3
- package/src/compile/projectCompile.ts +3 -0
- package/src/compile/screenEntry.ts +123 -123
- package/src/dev/devServer.ts +63 -7
- package/src/dev/https.ts +50 -0
- package/src/dev/serve.ts +15 -9
- package/src/dev/webRunner.ts +11 -3
- package/src/platform/serverDiff.ts +2 -3
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/emitWeb.ts +17 -8
- package/src/plugin/firstParty.ts +30 -0
- package/src/plugin/generate.ts +52 -24
- package/src/plugin/manifest.ts +66 -7
- package/src/project/ignore.ts +164 -164
- package/src/project/localFiles.ts +44 -7
- package/src/web/build.ts +33 -9
- package/src/web/halves.ts +76 -0
- package/runtime/web/assets/ar-CHvaraG4.js +0 -1
- package/runtime/web-lite/assets/scene3d-Bew8g7Ws.js +0 -1081
- package/runtime/web-lite/plugins/ar/index-CHvaraG4.js +0 -1
package/src/plugin/emitWeb.ts
CHANGED
|
@@ -358,18 +358,27 @@ export interface PluginGL {
|
|
|
358
358
|
/** The pose of the scene's camera (its world matrix). */
|
|
359
359
|
setCameraMatrix(scene: number, m16: ArrayLike<number>): void
|
|
360
360
|
/** A projection the half computes (column-major, OpenGL clip conventions), with the near / far it
|
|
361
|
-
* was built with, in place of the scene's fov projection
|
|
361
|
+
* was built with, in place of the scene's fov projection: drawn as it is — an off-centre one (a
|
|
362
|
+
* camera's intrinsics) too. Made for one view: a change of \`viewport\` wants a new one. */
|
|
362
363
|
setProjection(scene: number, m16: ArrayLike<number>, near: number, far: number): void
|
|
363
364
|
/** Back to the scene's own (fov) projection. */
|
|
364
365
|
clearProjection(scene: number): void
|
|
365
|
-
/** The scene's background: a playing video
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
|
|
369
|
-
|
|
366
|
+
/** The scene's background, behind the scene: a playing video (a camera's stream), or a canvas the
|
|
367
|
+
* half (or an engine it runs) draws the camera's picture on; null = none. \`uv\` = the affine map
|
|
368
|
+
* from the viewport's normalized coordinates (0,0 top-left) to the picture's, row-major
|
|
369
|
+
* [a, b, tx, c, d, ty] (identity by default). Web Lite shows the element itself: moved under the
|
|
370
|
+
* scene's canvas (its inline style replaced: absolute, the scene's box filled, \`object-fit: cover\`
|
|
371
|
+
* — \`uv\` is not read; a canvas sized to \`viewport\` × devicePixelRatio is shown 1:1), taken out of
|
|
372
|
+
* the page when it is replaced or cleared. The web player (a preview) shows a video only. */
|
|
373
|
+
setBackground(scene: number, source: HTMLVideoElement | HTMLCanvasElement | null, uv?: ArrayLike<number>): void
|
|
374
|
+
/** The web has no external textures: a video or a canvas is handed to setBackground as it is.
|
|
375
|
+
* Always null. */
|
|
370
376
|
createExternalTexture(): null
|
|
371
|
-
/** \`body\` before each frame
|
|
372
|
-
*
|
|
377
|
+
/** \`body\` before each frame of the 3D engine, until the returned function is called — or the
|
|
378
|
+
* world swap: in the host's own animation frame, before the tick and the render, so what a body
|
|
379
|
+
* sets (a pose, a projection) is that frame's picture. A requestAnimationFrame callback of the
|
|
380
|
+
* half's own is a frame late unless the browser runs it before the host's (callbacks run in the
|
|
381
|
+
* order they were asked for). Web Lite runs the bodies while a scene is on show. */
|
|
373
382
|
onFrame(body: () => void): () => void
|
|
374
383
|
/** The composite of the scene's next frame — the 3D picture, not the UI over it: RGBA8, top row
|
|
375
384
|
* first, width, height; null (0 × 0) when the scene is not the one presented. */
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The first-party plugins as the SDK carries them: `plugins/gen/<id>/` of its source tree — the
|
|
2
|
+
// contract.d.ts and the app's wrapper (sdk/), copies of the plugin repo's (core's
|
|
3
|
+
// scripts/vendor-plugins.ts). A project's HALF FOLDER (`implements` in its manifest) implements one
|
|
4
|
+
// of these contracts: the one of the SDK this CLI compiles with, so the glue `lecodes plugin gen`
|
|
5
|
+
// writes for the half and the wrapper in the bundle are of one contract.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import { sdkSourceDir } from "../compile/projectCompile"
|
|
10
|
+
|
|
11
|
+
const ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
12
|
+
|
|
13
|
+
/** The SDK's copy of a first-party plugin's contract, or null when the SDK carries no such plugin. */
|
|
14
|
+
export const firstPartyContractFile = (id: string): string | null => {
|
|
15
|
+
if (!sdkSourceDir || !ID.test(id)) return null
|
|
16
|
+
const file = join(sdkSourceDir, "plugins", "gen", id, "contract.d.ts")
|
|
17
|
+
return existsSync(file) ? file : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The first-party plugins the SDK carries, by id. */
|
|
21
|
+
export const firstPartyIds = (): string[] => {
|
|
22
|
+
if (!sdkSourceDir) return []
|
|
23
|
+
const gen = join(sdkSourceDir, "plugins", "gen")
|
|
24
|
+
return existsSync(gen) ? readdirSync(gen).filter((id) => existsSync(join(gen, id, "contract.d.ts"))).sort() : []
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A copy's text as the plugin wrote it: the vendoring points the import of the SDK at its own file
|
|
28
|
+
* (`../../../plugin`), a contract reads `lecodes-sdk/plugin` and nothing else. */
|
|
29
|
+
export const firstPartyContractText = (file: string): string =>
|
|
30
|
+
readFileSync(file, "utf8").replace(/from "(?:\.\.\/)+plugin"/g, `from "lecodes-sdk/plugin"`)
|
package/src/plugin/generate.ts
CHANGED
|
@@ -16,7 +16,8 @@ import { EmitError, emitSdk } from "./emitSdk"
|
|
|
16
16
|
import { emitSwift } from "./emitSwift"
|
|
17
17
|
import { emitWeb } from "./emitWeb"
|
|
18
18
|
import { lower } from "./lower"
|
|
19
|
-
import { MANIFEST_FILE, ManifestError, readManifestIfAny, type PluginManifest } from "./manifest"
|
|
19
|
+
import { CONTRACT_FILE, MANIFEST_FILE, ManifestError, readManifestIfAny, type PluginManifest } from "./manifest"
|
|
20
|
+
import { firstPartyContractFile, firstPartyContractText } from "./firstParty"
|
|
20
21
|
import { missingDeclarations, usesFeatures, usesPermissions, type PermissionKind } from "./permissions"
|
|
21
22
|
import { DEFAULT_SDK_PATH, PACKAGE_FILE, PackageError, emitPackageSwift } from "./emitPackage"
|
|
22
23
|
|
|
@@ -24,19 +25,31 @@ import { DEFAULT_SDK_PATH, PACKAGE_FILE, PackageError, emitPackageSwift } from "
|
|
|
24
25
|
// compared and pasted; `\` is Windows' own).
|
|
25
26
|
const shown = (from: string, to: string): string => relative(from, to).split(sep).join("/")
|
|
26
27
|
|
|
27
|
-
export
|
|
28
|
+
export { CONTRACT_FILE }
|
|
28
29
|
|
|
29
30
|
export type PluginDir = { id: string, dir: string }
|
|
30
31
|
|
|
31
|
-
/** The
|
|
32
|
+
/** The first-party plugin a folder's manifest says it implements (a project's half folder), or null.
|
|
33
|
+
* Read loosely: what the manifest holds is readManifest's to say. */
|
|
34
|
+
const implementsOf = (dir: string): string | null => {
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(readFileSync(join(dir, MANIFEST_FILE), "utf8").replace(/^/, "")) as { implements?: unknown }
|
|
37
|
+
return typeof raw.implements === "string" ? raw.implements : null
|
|
38
|
+
} catch { return null }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A plugin's folder: one with its contract, or a half folder that implements a first-party one's. */
|
|
42
|
+
const isPluginDir = (dir: string): boolean => existsSync(join(dir, CONTRACT_FILE)) || implementsOf(dir) !== null
|
|
43
|
+
|
|
44
|
+
/** The plugins under `path`: itself when it is one, else every `plugins/<id>/` that is. */
|
|
32
45
|
export const findPlugins = (path: string): PluginDir[] => {
|
|
33
46
|
const root = resolve(path)
|
|
34
|
-
if (
|
|
47
|
+
if (isPluginDir(root)) return [{ id: basename(root), dir: root }]
|
|
35
48
|
const holder = join(root, "plugins")
|
|
36
49
|
if (!existsSync(holder)) return []
|
|
37
50
|
return readdirSync(holder).sort()
|
|
38
51
|
.map(id => ({ id, dir: join(holder, id) }))
|
|
39
|
-
.filter(p => statSync(p.dir).isDirectory() &&
|
|
52
|
+
.filter(p => statSync(p.dir).isDirectory() && isPluginDir(p.dir))
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/** The CLI carries no TypeScript: a contract is read with the one its author has — the plugin's own
|
|
@@ -54,34 +67,39 @@ export const loadTypescript = (dir: string): typeof tsModule => {
|
|
|
54
67
|
|
|
55
68
|
export type Generated = { file: string, text: string }
|
|
56
69
|
|
|
57
|
-
/** Everything one plugin's contract generates, as `path → text`. Throws ContractError / EmitError
|
|
70
|
+
/** Everything one plugin's contract generates, as `path → text`. Throws ContractError / EmitError /
|
|
71
|
+
* ManifestError. A half folder (`implements`) is generated from the contract of the plugin it
|
|
72
|
+
* implements, the SDK's copy (firstParty.ts), and under that plugin's id: `web/ar.gen.ts` registers
|
|
73
|
+
* the service "ar"; it has no sdk/ — the app's side is the SDK's wrapper. */
|
|
58
74
|
export const generatePlugin = (ts: typeof tsModule, plugin: PluginDir): { contract: Contract, manifest: PluginManifest | null, files: Generated[] } => {
|
|
59
|
-
const file = join(plugin.dir, CONTRACT_FILE)
|
|
60
|
-
const contract = readContract(ts, file, readFileSync(file, "utf8"))
|
|
61
75
|
// The manifest is held to the folders the plugin has: a half the manifest does not name is built
|
|
62
76
|
// into no host.
|
|
63
77
|
const manifest = readManifestIfAny(plugin.dir, plugin.id)
|
|
78
|
+
const implemented = manifest?.implements
|
|
79
|
+
const file = implemented ? firstPartyContractFile(implemented)! : join(plugin.dir, CONTRACT_FILE)
|
|
80
|
+
const contract = readContract(ts, file, implemented ? firstPartyContractText(file) : readFileSync(file, "utf8"))
|
|
64
81
|
checkPermissions(plugin, contract, manifest)
|
|
65
|
-
const
|
|
82
|
+
const id = implemented ?? plugin.id
|
|
83
|
+
const handWritten = existsSync(join(plugin.dir, "sdk", `${id}.ts`))
|
|
66
84
|
const single = contract.channels.length === 1
|
|
67
|
-
const files: Generated[] = contract.channels.map(channel => ({
|
|
85
|
+
const files: Generated[] = implemented ? [] : contract.channels.map(channel => ({
|
|
68
86
|
// One channel is the rule; a contract with several names each file after its channel.
|
|
69
|
-
file: join(plugin.dir, "sdk", single ? `${
|
|
70
|
-
text: emitSdk(contract, channel, { id
|
|
87
|
+
file: join(plugin.dir, "sdk", single ? `${id}.gen.ts` : `${id}.${channel.iface}.gen.ts`),
|
|
88
|
+
text: emitSdk(contract, channel, { id, handWritten }),
|
|
71
89
|
}))
|
|
72
90
|
// A platform's file is generated where its FOLDER exists: the plugin says which platforms it has
|
|
73
91
|
// by having them.
|
|
74
92
|
if (existsSync(join(plugin.dir, "ios"))) {
|
|
75
93
|
const name = single ? contract.channels[0]!.iface : contract.channels[0]!.iface + "Plugin"
|
|
76
|
-
files.push({ file: join(plugin.dir, "ios", `${name}.gen.swift`), text: emitSwift(lower(contract), { id
|
|
94
|
+
files.push({ file: join(plugin.dir, "ios", `${name}.gen.swift`), text: emitSwift(lower(contract), { id }) })
|
|
77
95
|
}
|
|
78
96
|
if (existsSync(join(plugin.dir, "android"))) {
|
|
79
97
|
const name = single ? contract.channels[0]!.iface : contract.channels[0]!.iface + "Plugin"
|
|
80
|
-
const pkg = kotlinPackage(join(plugin.dir, "android"))
|
|
81
|
-
files.push({ file: join(pkg.dir, `${name}.gen.kt`), text: emitKotlin(lower(contract), { id
|
|
98
|
+
const pkg = kotlinPackage(join(plugin.dir, "android"), manifest?.android?.register)
|
|
99
|
+
files.push({ file: join(pkg.dir, `${name}.gen.kt`), text: emitKotlin(lower(contract), { id, package: pkg.name }) })
|
|
82
100
|
}
|
|
83
101
|
if (existsSync(join(plugin.dir, "web"))) {
|
|
84
|
-
files.push({ file: join(plugin.dir, "web", `${
|
|
102
|
+
files.push({ file: join(plugin.dir, "web", `${id}.gen.ts`), text: emitWeb(lower(contract), { id }) })
|
|
85
103
|
const foreign = hostImports(join(plugin.dir, "web"))
|
|
86
104
|
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")}`)
|
|
87
105
|
}
|
|
@@ -131,9 +149,17 @@ export const hostImports = (web: string): string[] => {
|
|
|
131
149
|
|
|
132
150
|
/** Where a plugin's Kotlin lives: `android/` is laid out as a Gradle module (its manifest and
|
|
133
151
|
* resources travel with the sources), and the generated file is a file of the plugin's PACKAGE —
|
|
134
|
-
* the
|
|
135
|
-
|
|
152
|
+
* the package of the type that registers the half (the manifest's `android.register`, which is
|
|
153
|
+
* also the module's namespace), when its folder is there; else the one folder under
|
|
154
|
+
* `src/main/java` that the path narrows down to (a plugin with no manifest yet). A half may hold
|
|
155
|
+
* other packages beside its own (a vendored library's JNI class): they do not move the file. */
|
|
156
|
+
export const kotlinPackage = (android: string, register?: string): { dir: string, name: string } => {
|
|
136
157
|
const root = join(android, "src", "main", "java")
|
|
158
|
+
if (register) {
|
|
159
|
+
const name = register.split(".").slice(0, -1).join(".")
|
|
160
|
+
const dir = join(root, ...name.split("."))
|
|
161
|
+
if (name && existsSync(dir)) return { dir, name }
|
|
162
|
+
}
|
|
137
163
|
let dir = root
|
|
138
164
|
while (existsSync(dir)) {
|
|
139
165
|
const entries = readdirSync(dir).filter(e => !e.startsWith("."))
|
|
@@ -164,7 +190,8 @@ export const repoSdkPath = (root: string): string => {
|
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
export type GenReport = {
|
|
167
|
-
|
|
193
|
+
/** `implements`: a half folder's first-party plugin, whose contract it was generated from. */
|
|
194
|
+
plugins: { id: string, implements?: string, contract: Contract, written: string[], stale: string[], fresh: string[] }[]
|
|
168
195
|
/** The repo's own files (its Package.swift), when the path is a repo of plugins. */
|
|
169
196
|
repo: { written: string[], stale: string[], fresh: string[] }
|
|
170
197
|
/** Contracts that did not read or emit, as printable text. */
|
|
@@ -175,7 +202,7 @@ export type GenReport = {
|
|
|
175
202
|
* to `path`. A failed contract does not stop the others. */
|
|
176
203
|
export const generate = (path: string, opts: { check?: boolean } = {}): GenReport => {
|
|
177
204
|
const plugins = findPlugins(path)
|
|
178
|
-
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).`)
|
|
205
|
+
if (plugins.length === 0) throw new CliError(`No plugin under ${resolve(path)}: a plugin is a directory with a ${CONTRACT_FILE}, or a half folder whose ${MANIFEST_FILE} says what it \`implements\` (one, or plugins/<id>/ of a repo or a project).`)
|
|
179
206
|
const ts = loadTypescript(plugins[0]!.dir)
|
|
180
207
|
const root = resolve(path)
|
|
181
208
|
const report: GenReport = { plugins: [], repo: { written: [], stale: [], fresh: [] }, failures: [] }
|
|
@@ -194,7 +221,7 @@ export const generate = (path: string, opts: { check?: boolean } = {}): GenRepor
|
|
|
194
221
|
try {
|
|
195
222
|
const { contract, manifest, files } = generatePlugin(ts, plugin)
|
|
196
223
|
if (manifest) manifests.push({ id: plugin.id, manifest })
|
|
197
|
-
const entry = { id: plugin.id, contract, written: [] as string[], stale: [] as string[], fresh: [] as string[] }
|
|
224
|
+
const entry = { id: plugin.id, implements: manifest?.implements, contract, written: [] as string[], stale: [] as string[], fresh: [] as string[] }
|
|
198
225
|
for (const f of files) place(f, entry)
|
|
199
226
|
report.plugins.push(entry)
|
|
200
227
|
} catch (e) {
|
|
@@ -206,9 +233,10 @@ export const generate = (path: string, opts: { check?: boolean } = {}): GenRepor
|
|
|
206
233
|
}
|
|
207
234
|
|
|
208
235
|
// A repo of plugins (not one plugin): its Package.swift, from every manifest that read. Written
|
|
209
|
-
// only when every contract made it — a repo with a broken plugin keeps the package it has
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
// only when every contract made it — a repo with a broken plugin keeps the package it has — and
|
|
237
|
+
// only when one has an iOS half: a project whose plugins are web halves has no package to make.
|
|
238
|
+
const isRepo = !(plugins.length === 1 && plugins[0]!.dir === root)
|
|
239
|
+
if (isRepo && report.failures.length === 0 && manifests.some(m => m.manifest.ios !== null)) {
|
|
212
240
|
try {
|
|
213
241
|
place({ file: join(root, PACKAGE_FILE), text: emitPackageSwift(manifests, repoSdkPath(root)) }, report.repo)
|
|
214
242
|
} catch (e) {
|
package/src/plugin/manifest.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
// A plugin's manifest (`lecodes-plugin.json`, 2.x): what a host needs to BUILD the plugin in — the
|
|
2
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
|
|
4
|
-
// their names and their types are the contract's (contract.d.ts), the folders
|
|
5
|
-
// (`sdk/`, `ios/`, `android/`, `web/`).
|
|
3
|
+
// packages, prebuilt binaries, Gradle dependencies, bundler resources). What the plugin DOES is not
|
|
4
|
+
// here: the channels, their names and their types are the contract's (contract.d.ts), the folders
|
|
5
|
+
// are the convention (`sdk/`, `ios/`, `android/`, `web/`).
|
|
6
6
|
//
|
|
7
7
|
// A platform is a key of the manifest, ALWAYS: an object where the plugin has a half, `null` where
|
|
8
8
|
// it has none. A missing key is refused — "not supported" is a statement, not an omission.
|
|
9
9
|
//
|
|
10
10
|
// Desktop plugins are another system (a prebuilt library behind a C ABI) with a manifest of their
|
|
11
11
|
// own shape; nothing of it is read here.
|
|
12
|
+
//
|
|
13
|
+
// A HALF FOLDER of a project (`implements: "<first-party id>"`) has no contract and no sdk/ of its
|
|
14
|
+
// own: it implements the contract of a first-party plugin the SDK carries, for the platforms it has
|
|
15
|
+
// a half of — app.json names it for those (`"ar": { "web": "./plugins/ar-8thwall" }`), the
|
|
16
|
+
// first-party halves stay everywhere else.
|
|
12
17
|
|
|
13
18
|
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
14
|
-
import { join } from "node:path"
|
|
19
|
+
import { isAbsolute, join } from "node:path"
|
|
20
|
+
import { firstPartyContractFile, firstPartyIds } from "./firstParty"
|
|
15
21
|
|
|
16
22
|
export const MANIFEST_FILE = "lecodes-plugin.json"
|
|
23
|
+
export const CONTRACT_FILE = "contract.d.ts"
|
|
17
24
|
|
|
18
25
|
/** A SwiftPM dependency the iOS half links. */
|
|
19
26
|
export type IosPackage = { url: string, exact?: string, from?: string, products: string[] }
|
|
@@ -30,6 +37,9 @@ export type PluginManifest = {
|
|
|
30
37
|
* contract between a bundle, the runtime and a half. */
|
|
31
38
|
sdk: string
|
|
32
39
|
description?: string
|
|
40
|
+
/** A HALF FOLDER: the first-party plugin whose contract the folder implements (it has no
|
|
41
|
+
* contract.d.ts and no sdk/ of its own — the app's side is the SDK's wrapper). */
|
|
42
|
+
implements?: string
|
|
33
43
|
ios: {
|
|
34
44
|
/** The Swift module the half is built as in a shell. Default: `LeCodes` + the id in PascalCase. */
|
|
35
45
|
module?: string
|
|
@@ -38,6 +48,12 @@ export type PluginManifest = {
|
|
|
38
48
|
infoPlist?: Record<string, string>
|
|
39
49
|
entitlements?: Record<string, unknown>
|
|
40
50
|
packages?: IosPackage[]
|
|
51
|
+
/** Prebuilt xcframeworks the half links, by their paths in the plugin
|
|
52
|
+
* (`binaries/ios/QrAnchorFFI.xcframework`): folders of the plugin, never under ios/ — SwiftPM
|
|
53
|
+
* takes a binary as a target of its own, beside the sources. Each is a binary target named after
|
|
54
|
+
* its folder (`binaryName`), which the half's target depends on; a name is one in a package
|
|
55
|
+
* (two plugins may not carry one alike). */
|
|
56
|
+
binaries?: string[]
|
|
41
57
|
} | null
|
|
42
58
|
android: {
|
|
43
59
|
/** The Gradle module the half is built as in a shell. Default: `lecodes-` + the id. */
|
|
@@ -56,6 +72,11 @@ export type PluginManifest = {
|
|
|
56
72
|
dependencies?: Record<string, string>
|
|
57
73
|
/** The half's bundler resources, by the name `register` receives them under. */
|
|
58
74
|
resources?: Record<string, WebResource>
|
|
75
|
+
/** A folder of the plugin (relative to it) put beside the built module AS IS, names unhashed: a
|
|
76
|
+
* runtime the half loads by its own url (`new URL("<static>/x.js", import.meta.url)`), which
|
|
77
|
+
* fetches its files relative to itself. A project's half; the first-party halves are built in
|
|
78
|
+
* Web Lite's page with `resources`. */
|
|
79
|
+
static?: string
|
|
59
80
|
} | null
|
|
60
81
|
}
|
|
61
82
|
|
|
@@ -69,16 +90,16 @@ const ID = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/
|
|
|
69
90
|
const VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/
|
|
70
91
|
const RANGE = /^[\^~]?\d+\.\d+\.\d+$/
|
|
71
92
|
const PLATFORMS = ["ios", "android", "web"] as const
|
|
72
|
-
const KNOWN = new Set(["id", "name", "version", "sdk", "description", ...PLATFORMS])
|
|
93
|
+
const KNOWN = new Set(["id", "name", "version", "sdk", "description", "implements", ...PLATFORMS])
|
|
73
94
|
const GONE: Record<string, string> = {
|
|
74
95
|
provides: "the channels are the contract's (contract.d.ts)",
|
|
75
96
|
js: "the wrapper is found by convention (sdk/)",
|
|
76
97
|
desktop: "desktop plugins are another system, with a manifest of their own",
|
|
77
98
|
}
|
|
78
99
|
const BRANCH: Record<(typeof PLATFORMS)[number], Set<string>> = {
|
|
79
|
-
ios: new Set(["module", "register", "infoPlist", "entitlements", "packages"]),
|
|
100
|
+
ios: new Set(["module", "register", "infoPlist", "entitlements", "packages", "binaries"]),
|
|
80
101
|
android: new Set(["module", "register", "dependencies", "gradlePlugins"]),
|
|
81
|
-
web: new Set(["register", "dependencies", "resources"]),
|
|
102
|
+
web: new Set(["register", "dependencies", "resources", "static"]),
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
const isObject = (v: unknown): v is Record<string, unknown> => typeof v === "object" && v !== null && !Array.isArray(v)
|
|
@@ -86,6 +107,12 @@ const isFolder = (path: string): boolean => existsSync(path) && statSync(path).i
|
|
|
86
107
|
|
|
87
108
|
export const pascal = (id: string): string => id.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("")
|
|
88
109
|
|
|
110
|
+
const XCFRAMEWORK = ".xcframework"
|
|
111
|
+
|
|
112
|
+
/** The binary target of an `ios.binaries` path: its folder's name without `.xcframework`
|
|
113
|
+
* (`binaries/ios/QrAnchorFFI.xcframework` → `QrAnchorFFI`). */
|
|
114
|
+
export const binaryName = (path: string): string => path.split(/[\\/]/).pop()!.slice(0, -XCFRAMEWORK.length)
|
|
115
|
+
|
|
89
116
|
/** The manifest of the plugin in `dir` (its folder's name is its id). Every problem of the file is
|
|
90
117
|
* reported at once. */
|
|
91
118
|
export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
@@ -116,6 +143,13 @@ export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
|
116
143
|
const sdk = text("sdk")
|
|
117
144
|
if (sdk !== undefined && !RANGE.test(sdk)) problems.push(`sdk: "${sdk}" — a range of the SDK's version: ^2.0.0`)
|
|
118
145
|
if (raw.description !== undefined && typeof raw.description !== "string") problems.push("description: a text")
|
|
146
|
+
if (raw.implements !== undefined) {
|
|
147
|
+
const impl = raw.implements
|
|
148
|
+
if (typeof impl !== "string" || !ID.test(impl)) problems.push(`implements: the id of the first-party plugin whose contract the folder implements ("ar")`)
|
|
149
|
+
else if (!firstPartyContractFile(impl)) problems.push(`implements: "${impl}" is no first-party plugin of this SDK (${firstPartyIds().join(", ") || "it carries none"})`)
|
|
150
|
+
if (existsSync(join(dir, CONTRACT_FILE))) problems.push(`implements: the folder implements another plugin's contract — it has no ${CONTRACT_FILE} of its own`)
|
|
151
|
+
if (isFolder(join(dir, "sdk"))) problems.push("implements: the folder implements another plugin — its app side is the SDK's, it has no sdk/ of its own")
|
|
152
|
+
}
|
|
119
153
|
|
|
120
154
|
for (const platform of PLATFORMS) {
|
|
121
155
|
const branch = raw[platform]
|
|
@@ -159,6 +193,23 @@ export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
|
159
193
|
}
|
|
160
194
|
})
|
|
161
195
|
}
|
|
196
|
+
if (ios.binaries !== undefined) {
|
|
197
|
+
if (!Array.isArray(ios.binaries)) problems.push(`ios.binaries: a list of the plugin's xcframeworks, by their paths in it ("binaries/ios/X.xcframework")`)
|
|
198
|
+
else {
|
|
199
|
+
const named = new Map<string, string>()
|
|
200
|
+
ios.binaries.forEach((path, i) => {
|
|
201
|
+
const segments = typeof path === "string" ? path.split(/[\\/]/) : []
|
|
202
|
+
if (typeof path !== "string" || path.trim() === "" || isAbsolute(path) || segments.some(s => s === ".." || s === "." || s === "")) {
|
|
203
|
+
problems.push(`ios.binaries[${i}]: a folder of the plugin, by its path in it ("binaries/ios/X.xcframework")`)
|
|
204
|
+
} else if (!path.endsWith(XCFRAMEWORK)) problems.push(`ios.binaries[${i}]: "${path}" — an ${XCFRAMEWORK} folder`)
|
|
205
|
+
else if (segments[0] === "ios") problems.push(`ios.binaries[${i}]: ${path} is in ios/, the half's sources — a binary stands beside them ("binaries/ios/X.xcframework")`)
|
|
206
|
+
else if (!/^[A-Za-z_]\w*$/.test(binaryName(path))) problems.push(`ios.binaries[${i}]: "${path}" — the folder's name is its binary target's: a Swift name before ${XCFRAMEWORK}`)
|
|
207
|
+
else if (!isFolder(join(dir, path))) problems.push(`ios.binaries[${i}]: ${path} is not a folder of the plugin`)
|
|
208
|
+
else if (named.has(binaryName(path))) problems.push(`ios.binaries[${i}]: ${path} is named "${binaryName(path)}" like ${named.get(binaryName(path))} — a name is one binary target`)
|
|
209
|
+
else named.set(binaryName(path), path)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
162
213
|
}
|
|
163
214
|
const android = isObject(raw.android) ? raw.android : null
|
|
164
215
|
if (android) {
|
|
@@ -181,6 +232,14 @@ export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
|
181
232
|
}
|
|
182
233
|
}
|
|
183
234
|
}
|
|
235
|
+
const folder = web.static
|
|
236
|
+
if (folder !== undefined) {
|
|
237
|
+
const segments = typeof folder === "string" ? folder.split(/[\\/]/) : []
|
|
238
|
+
if (typeof folder !== "string" || folder.trim() === "" || isAbsolute(folder) || segments.some((s) => s === ".." || s === "." || s === "")) {
|
|
239
|
+
problems.push(`web.static: a folder of the plugin, by its path in it ("xr-runtime")`)
|
|
240
|
+
} else if (!isFolder(join(dir, folder))) problems.push(`web.static: ${folder} is not a folder of the plugin`)
|
|
241
|
+
else if (folder === "index.js") problems.push(`web.static: "index.js" is the built module's name`)
|
|
242
|
+
}
|
|
184
243
|
}
|
|
185
244
|
|
|
186
245
|
if (problems.length) throw new ManifestError(file, problems)
|