lecodes-cli 2.4.2 → 2.4.4
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 +20169 -12323
- package/package.json +8 -8
- package/runtime/sdk-types.json +1 -1
- package/runtime/web/VERSION.json +3 -3
- package/runtime/web/player.js +17 -17
- package/runtime/web-lite/lite.js +3 -3
- package/runtime/web-page/page.js +6 -6
- package/src/commands/app/android.ts +68 -19
- package/src/commands/app/index.ts +58 -8
- package/src/commands/app/shared.ts +87 -2
- 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/shared.ts +79 -79
- package/src/commands/thumbs.ts +185 -185
- package/src/compile/screenEntry.ts +123 -123
- package/src/plugin/emitPackage.ts +18 -5
- package/src/plugin/generate.ts +11 -3
- package/src/plugin/manifest.ts +33 -4
- package/src/project/ignore.ts +164 -164
|
@@ -1,123 +1,123 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
-
import { isAbsolute, join, relative, resolve } from "node:path"
|
|
3
|
-
import { screenFrame } from "../project/designMeta"
|
|
4
|
-
import { CliError } from "../cli"
|
|
5
|
-
import { toKey } from "../project/paths"
|
|
6
|
-
import type { CompileEntry } from "./projectCompile"
|
|
7
|
-
|
|
8
|
-
/*
|
|
9
|
-
* Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
|
|
10
|
-
* `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
|
|
11
|
-
* a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
|
|
12
|
-
* supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
|
|
13
|
-
* file written that way — inside design/ or not — renders with the full `lecodes render` flag set
|
|
14
|
-
* (--png/--clip/--script/--device/…) against the project's real imports and assets.
|
|
15
|
-
*
|
|
16
|
-
* Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
/** Extensions a screen module may carry (so `render design/screens/login` works too). */
|
|
20
|
-
const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
21
|
-
|
|
22
|
-
/** Where the generated entry lives — the project root, so its import specifier is the screen's
|
|
23
|
-
* own project key ("./design/screens/login"). Leading dot: never a real project file. */
|
|
24
|
-
const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
|
|
25
|
-
|
|
26
|
-
/** The design folder a bare screen id (`lecodes render login`) is looked up in. */
|
|
27
|
-
const DEFAULT_DESIGN_DIR = "design"
|
|
28
|
-
|
|
29
|
-
export type ScreenTarget = {
|
|
30
|
-
/** The virtual entry module — hand it to the compiler alongside the project's own entries. */
|
|
31
|
-
entry: CompileEntry
|
|
32
|
-
/** Path of that entry, for `entryOverride`. */
|
|
33
|
-
entryPath: string
|
|
34
|
-
/** The board frame from the design folder's meta.json, when this IS a design screen. */
|
|
35
|
-
frame?: [number, number]
|
|
36
|
-
/** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
|
|
37
|
-
label: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
|
|
41
|
-
|
|
42
|
-
/** Whether the argument names a path at all (vs a bare screen id). */
|
|
43
|
-
const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
|
|
44
|
-
|
|
45
|
-
/** Find the screen file: a path (with or without extension), or a bare id resolved against the
|
|
46
|
-
* design folder's screens/ (`lecodes render login`, honoring --dir). */
|
|
47
|
-
const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
|
|
48
|
-
const candidates = [resolve(process.cwd(), file)]
|
|
49
|
-
if (!hasSeparator(file)) {
|
|
50
|
-
candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
|
|
51
|
-
}
|
|
52
|
-
for (const base of candidates) {
|
|
53
|
-
if (existsSync(base) && statSync(base).isFile()) return base
|
|
54
|
-
for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
|
|
55
|
-
}
|
|
56
|
-
throw new CliError(
|
|
57
|
-
`No screen file at "${file}".` +
|
|
58
|
-
(hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
|
|
62
|
-
* or null when it isn't laid out as a design screen. */
|
|
63
|
-
const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
|
|
64
|
-
const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
|
|
65
|
-
if (!m) return null
|
|
66
|
-
const dir = join(root, m[1])
|
|
67
|
-
return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Resolve `<file>` into the virtual entry that opens its default export.
|
|
72
|
-
* `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
|
|
73
|
-
*/
|
|
74
|
-
const resolveScreenTarget = (
|
|
75
|
-
root: string, file: string, opts: { state?: string, dir?: string } = {},
|
|
76
|
-
): ScreenTarget => {
|
|
77
|
-
const abs = findScreenFile(root, file, opts.dir)
|
|
78
|
-
const rel = relative(root, abs)
|
|
79
|
-
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
80
|
-
throw new CliError(
|
|
81
|
-
`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
|
|
82
|
-
`imports and asset() files resolve from the same tree.`)
|
|
83
|
-
}
|
|
84
|
-
const key = toKey(rel)
|
|
85
|
-
|
|
86
|
-
// A file with no default export compiles to an empty render; say so here instead.
|
|
87
|
-
if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
|
|
88
|
-
throw new CliError(
|
|
89
|
-
`${key.slice(1)} has no default export. A screen file ends with ` +
|
|
90
|
-
`\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
|
|
94
|
-
// chosen state (a bare call uses the parameter's default = the canonical state), a value is used
|
|
95
|
-
// as-is, and either way the screen is opened so the runtime has something to render.
|
|
96
|
-
const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
|
|
97
|
-
const spec = "." + key.replace(/\.[^./]+$/, "")
|
|
98
|
-
const design = designDirOf(root, key)
|
|
99
|
-
return {
|
|
100
|
-
entryPath: SCREEN_ENTRY_PATH,
|
|
101
|
-
entry: {
|
|
102
|
-
path: SCREEN_ENTRY_PATH,
|
|
103
|
-
type: "text",
|
|
104
|
-
text: `import screen from ${JSON.stringify(spec)}\n`
|
|
105
|
-
+ `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
|
|
106
|
-
+ `__screen.open()\n`,
|
|
107
|
-
},
|
|
108
|
-
frame: design ? screenFrame(design.dir, design.id) : undefined,
|
|
109
|
-
label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** The screen target named on the command line (`render <file>`), or undefined when no screen was
|
|
114
|
-
* named. Rejects the flag combinations that would fight over the entrypoint. */
|
|
115
|
-
export type ScreenArgs = { file?: string, state?: string, entry?: string, dir?: string }
|
|
116
|
-
export const screenTargetFrom = (root: string, a: ScreenArgs): ScreenTarget | undefined => {
|
|
117
|
-
if (a.file === undefined) {
|
|
118
|
-
if (a.state !== undefined) throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
|
|
119
|
-
return undefined
|
|
120
|
-
}
|
|
121
|
-
if (a.entry !== undefined) throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
|
|
122
|
-
return resolveScreenTarget(root, a.file, { state: a.state, dir: a.dir })
|
|
123
|
-
}
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs"
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path"
|
|
3
|
+
import { screenFrame } from "../project/designMeta"
|
|
4
|
+
import { CliError } from "../cli"
|
|
5
|
+
import { toKey } from "../project/paths"
|
|
6
|
+
import type { CompileEntry } from "./projectCompile"
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Rendering ONE screen module instead of the whole app — `lecodes render <file>` (and
|
|
10
|
+
* `--desktop`). A design screen (`design/screens/login.ts`) never boots itself: it default-exports
|
|
11
|
+
* a `UIScreen`, or a `(state) => UIScreen` function for its states, and the design board / snapshot
|
|
12
|
+
* supply the `.open()`. This builds the same virtual entry for the normal project pipeline, so any
|
|
13
|
+
* file written that way — inside design/ or not — renders with the full `lecodes render` flag set
|
|
14
|
+
* (--png/--clip/--script/--device/…) against the project's real imports and assets.
|
|
15
|
+
*
|
|
16
|
+
* Kept in step with compile/designCompile.ts, which does the same wrapping per screen for the board.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Extensions a screen module may carry (so `render design/screens/login` works too). */
|
|
20
|
+
const SCREEN_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs"]
|
|
21
|
+
|
|
22
|
+
/** Where the generated entry lives — the project root, so its import specifier is the screen's
|
|
23
|
+
* own project key ("./design/screens/login"). Leading dot: never a real project file. */
|
|
24
|
+
const SCREEN_ENTRY_PATH = "/.screen-entry.ts"
|
|
25
|
+
|
|
26
|
+
/** The design folder a bare screen id (`lecodes render login`) is looked up in. */
|
|
27
|
+
const DEFAULT_DESIGN_DIR = "design"
|
|
28
|
+
|
|
29
|
+
export type ScreenTarget = {
|
|
30
|
+
/** The virtual entry module — hand it to the compiler alongside the project's own entries. */
|
|
31
|
+
entry: CompileEntry
|
|
32
|
+
/** Path of that entry, for `entryOverride`. */
|
|
33
|
+
entryPath: string
|
|
34
|
+
/** The board frame from the design folder's meta.json, when this IS a design screen. */
|
|
35
|
+
frame?: [number, number]
|
|
36
|
+
/** How the screen is named in progress notes ("design/screens/login.ts", "login@empty"). */
|
|
37
|
+
label: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** `lecodes render <file|screen-id>` — the positional a screen render is asked for by. */
|
|
41
|
+
|
|
42
|
+
/** Whether the argument names a path at all (vs a bare screen id). */
|
|
43
|
+
const hasSeparator = (file: string) => file.includes("/") || file.includes("\\")
|
|
44
|
+
|
|
45
|
+
/** Find the screen file: a path (with or without extension), or a bare id resolved against the
|
|
46
|
+
* design folder's screens/ (`lecodes render login`, honoring --dir). */
|
|
47
|
+
const findScreenFile = (root: string, file: string, dirFlag: string | undefined): string => {
|
|
48
|
+
const candidates = [resolve(process.cwd(), file)]
|
|
49
|
+
if (!hasSeparator(file)) {
|
|
50
|
+
candidates.push(join(root, (dirFlag ?? DEFAULT_DESIGN_DIR).replace(/[/\\]+$/, ""), "screens", file))
|
|
51
|
+
}
|
|
52
|
+
for (const base of candidates) {
|
|
53
|
+
if (existsSync(base) && statSync(base).isFile()) return base
|
|
54
|
+
for (const ext of SCREEN_EXTS) if (existsSync(base + ext)) return base + ext
|
|
55
|
+
}
|
|
56
|
+
throw new CliError(
|
|
57
|
+
`No screen file at "${file}".` +
|
|
58
|
+
(hasSeparator(file) ? "" : ` Pass a path, or a screen id that exists in ${dirFlag ?? DEFAULT_DESIGN_DIR}/screens/.`))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The design folder a screen file belongs to ("<root>/design" for "/design/screens/login.ts"),
|
|
62
|
+
* or null when it isn't laid out as a design screen. */
|
|
63
|
+
const designDirOf = (root: string, key: string): { dir: string, id: string } | null => {
|
|
64
|
+
const m = /^\/(.*)screens\/([^/]+)\.[^./]+$/.exec(key)
|
|
65
|
+
if (!m) return null
|
|
66
|
+
const dir = join(root, m[1])
|
|
67
|
+
return existsSync(join(dir, "meta.json")) ? { dir, id: m[2] } : null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve `<file>` into the virtual entry that opens its default export.
|
|
72
|
+
* `state` picks the branch of a `(state) => UIScreen` screen (ignored by a value export).
|
|
73
|
+
*/
|
|
74
|
+
const resolveScreenTarget = (
|
|
75
|
+
root: string, file: string, opts: { state?: string, dir?: string } = {},
|
|
76
|
+
): ScreenTarget => {
|
|
77
|
+
const abs = findScreenFile(root, file, opts.dir)
|
|
78
|
+
const rel = relative(root, abs)
|
|
79
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
80
|
+
throw new CliError(
|
|
81
|
+
`"${file}" is outside the project (${root}) — a screen renders as part of its project, so its ` +
|
|
82
|
+
`imports and asset() files resolve from the same tree.`)
|
|
83
|
+
}
|
|
84
|
+
const key = toKey(rel)
|
|
85
|
+
|
|
86
|
+
// A file with no default export compiles to an empty render; say so here instead.
|
|
87
|
+
if (!/^\s*export\s+default\b/m.test(readFileSync(abs, "utf8"))) {
|
|
88
|
+
throw new CliError(
|
|
89
|
+
`${key.slice(1)} has no default export. A screen file ends with ` +
|
|
90
|
+
`\`export default UIScreen([...])\` — or \`export default (state) => UIScreen([...])\` for screen states.`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Same wrapper the design board compiles (designCompile.ts): a function export is invoked with the
|
|
94
|
+
// chosen state (a bare call uses the parameter's default = the canonical state), a value is used
|
|
95
|
+
// as-is, and either way the screen is opened so the runtime has something to render.
|
|
96
|
+
const invoke = opts.state !== undefined ? `screen(${JSON.stringify(opts.state)})` : "screen()"
|
|
97
|
+
const spec = "." + key.replace(/\.[^./]+$/, "")
|
|
98
|
+
const design = designDirOf(root, key)
|
|
99
|
+
return {
|
|
100
|
+
entryPath: SCREEN_ENTRY_PATH,
|
|
101
|
+
entry: {
|
|
102
|
+
path: SCREEN_ENTRY_PATH,
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `import screen from ${JSON.stringify(spec)}\n`
|
|
105
|
+
+ `const __screen = typeof screen === "function" ? ${invoke} : screen\n`
|
|
106
|
+
+ `__screen.open()\n`,
|
|
107
|
+
},
|
|
108
|
+
frame: design ? screenFrame(design.dir, design.id) : undefined,
|
|
109
|
+
label: (design ? design.id : key.slice(1)) + (opts.state !== undefined ? `@${opts.state}` : ""),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The screen target named on the command line (`render <file>`), or undefined when no screen was
|
|
114
|
+
* named. Rejects the flag combinations that would fight over the entrypoint. */
|
|
115
|
+
export type ScreenArgs = { file?: string, state?: string, entry?: string, dir?: string }
|
|
116
|
+
export const screenTargetFrom = (root: string, a: ScreenArgs): ScreenTarget | undefined => {
|
|
117
|
+
if (a.file === undefined) {
|
|
118
|
+
if (a.state !== undefined) throw new CliError("--state picks a screen's state — name the screen file too: lecodes render <file> --state <name>")
|
|
119
|
+
return undefined
|
|
120
|
+
}
|
|
121
|
+
if (a.entry !== undefined) throw new CliError("Use either a screen file or --entry, not both — each names the module to compile from.")
|
|
122
|
+
return resolveScreenTarget(root, a.file, { state: a.state, dir: a.dir })
|
|
123
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
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
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// package — a target per plugin (its sources `plugins/<id>/ios`, the packages its manifest names,
|
|
3
|
+
// a binary target per prebuilt xcframework of `ios.binaries`, by its path in the repo) and a product
|
|
4
|
+
// per plugin. Each plugin is its own MODULE, so two plugins may declare a type of the same name, and
|
|
5
|
+
// an Xcode project links a plugin as a product instead of compiling its folder.
|
|
5
6
|
//
|
|
6
7
|
// The targets depend on the LeCodes SDK as a package on disk: `LECODES_IOS_SDK_PATH`, else the path
|
|
7
8
|
// the repo's package.json names (`lecodes.iosSdkPath`), else the sibling `../lecodes` (a checkout
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
// of its own (templates/ios.ts), pinned by plugins.lock.json.
|
|
12
13
|
|
|
13
14
|
import type { PluginManifest } from "./manifest"
|
|
14
|
-
import { pascal } from "./manifest"
|
|
15
|
+
import { binaryName, pascal } from "./manifest"
|
|
15
16
|
|
|
16
17
|
export class PackageError extends Error {}
|
|
17
18
|
|
|
@@ -30,7 +31,8 @@ export const DEFAULT_SDK_PATH = "../lecodes"
|
|
|
30
31
|
|
|
31
32
|
/** The text of the repo's Package.swift for `plugins` (every manifest; those without an iOS half are
|
|
32
33
|
* 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
|
+
* PackageError when two plugins pin one package at different versions, or when a binary's name is
|
|
35
|
+
* another target's (one name, one target of the package). */
|
|
34
36
|
export const emitPackageSwift = (plugins: { id: string, manifest: PluginManifest }[], sdkPath: string = DEFAULT_SDK_PATH): string => {
|
|
35
37
|
const ios = plugins.filter(p => p.manifest.ios !== null)
|
|
36
38
|
const packages = new Map<string, { url: string, exact?: string, from?: string, by: string }>()
|
|
@@ -44,6 +46,14 @@ export const emitPackageSwift = (plugins: { id: string, manifest: PluginManifest
|
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
48
|
const moduleOf = (p: { id: string, manifest: PluginManifest }) => iosModule(p.id, p.manifest)
|
|
49
|
+
const targetNames = new Map<string, string>(ios.map(p => [moduleOf(p), `the module of "${p.id}"`]))
|
|
50
|
+
for (const p of ios) {
|
|
51
|
+
for (const path of p.manifest.ios!.binaries ?? []) {
|
|
52
|
+
const prior = targetNames.get(binaryName(path))
|
|
53
|
+
if (prior) throw new PackageError(`"${p.id}" carries the binary ${binaryName(path)} and ${binaryName(path)} is ${prior} — one name is one target of the package`)
|
|
54
|
+
targetNames.set(binaryName(path), `a binary of "${p.id}"`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
47
57
|
|
|
48
58
|
const products = ios.map(p => ` .library(name: "${moduleOf(p)}", targets: ["${moduleOf(p)}"]),`)
|
|
49
59
|
const dependencies = [
|
|
@@ -51,9 +61,11 @@ export const emitPackageSwift = (plugins: { id: string, manifest: PluginManifest
|
|
|
51
61
|
...[...packages.values()].map(pkg => ` .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`),
|
|
52
62
|
]
|
|
53
63
|
const targets = ios.map(p => {
|
|
64
|
+
const binaries = p.manifest.ios!.binaries ?? []
|
|
54
65
|
const deps = [
|
|
55
66
|
`.product(name: "LeCodes", package: "lecodes")`,
|
|
56
67
|
...(p.manifest.ios!.packages ?? []).flatMap(pkg => pkg.products.map(name => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`)),
|
|
68
|
+
...binaries.map(path => `"${binaryName(path)}"`),
|
|
57
69
|
]
|
|
58
70
|
return [
|
|
59
71
|
` // ${p.id}`,
|
|
@@ -62,6 +74,7 @@ export const emitPackageSwift = (plugins: { id: string, manifest: PluginManifest
|
|
|
62
74
|
` dependencies: [${deps.join(", ")}],`,
|
|
63
75
|
` path: "plugins/${p.id}/ios"`,
|
|
64
76
|
` ),`,
|
|
77
|
+
...binaries.map(path => ` .binaryTarget(name: "${binaryName(path)}", path: "plugins/${p.id}/${path.split(/[\\/]/).join("/")}"),`),
|
|
65
78
|
].join("\n")
|
|
66
79
|
})
|
|
67
80
|
|
package/src/plugin/generate.ts
CHANGED
|
@@ -95,7 +95,7 @@ export const generatePlugin = (ts: typeof tsModule, plugin: PluginDir): { contra
|
|
|
95
95
|
}
|
|
96
96
|
if (existsSync(join(plugin.dir, "android"))) {
|
|
97
97
|
const name = single ? contract.channels[0]!.iface : contract.channels[0]!.iface + "Plugin"
|
|
98
|
-
const pkg = kotlinPackage(join(plugin.dir, "android"))
|
|
98
|
+
const pkg = kotlinPackage(join(plugin.dir, "android"), manifest?.android?.register)
|
|
99
99
|
files.push({ file: join(pkg.dir, `${name}.gen.kt`), text: emitKotlin(lower(contract), { id, package: pkg.name }) })
|
|
100
100
|
}
|
|
101
101
|
if (existsSync(join(plugin.dir, "web"))) {
|
|
@@ -149,9 +149,17 @@ export const hostImports = (web: string): string[] => {
|
|
|
149
149
|
|
|
150
150
|
/** Where a plugin's Kotlin lives: `android/` is laid out as a Gradle module (its manifest and
|
|
151
151
|
* resources travel with the sources), and the generated file is a file of the plugin's PACKAGE —
|
|
152
|
-
* the
|
|
153
|
-
|
|
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 } => {
|
|
154
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
|
+
}
|
|
155
163
|
let dir = root
|
|
156
164
|
while (existsSync(dir)) {
|
|
157
165
|
const entries = readdirSync(dir).filter(e => !e.startsWith("."))
|
package/src/plugin/manifest.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
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.
|
|
@@ -48,6 +48,12 @@ export type PluginManifest = {
|
|
|
48
48
|
infoPlist?: Record<string, string>
|
|
49
49
|
entitlements?: Record<string, unknown>
|
|
50
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[]
|
|
51
57
|
} | null
|
|
52
58
|
android: {
|
|
53
59
|
/** The Gradle module the half is built as in a shell. Default: `lecodes-` + the id. */
|
|
@@ -91,7 +97,7 @@ const GONE: Record<string, string> = {
|
|
|
91
97
|
desktop: "desktop plugins are another system, with a manifest of their own",
|
|
92
98
|
}
|
|
93
99
|
const BRANCH: Record<(typeof PLATFORMS)[number], Set<string>> = {
|
|
94
|
-
ios: new Set(["module", "register", "infoPlist", "entitlements", "packages"]),
|
|
100
|
+
ios: new Set(["module", "register", "infoPlist", "entitlements", "packages", "binaries"]),
|
|
95
101
|
android: new Set(["module", "register", "dependencies", "gradlePlugins"]),
|
|
96
102
|
web: new Set(["register", "dependencies", "resources", "static"]),
|
|
97
103
|
}
|
|
@@ -101,6 +107,12 @@ const isFolder = (path: string): boolean => existsSync(path) && statSync(path).i
|
|
|
101
107
|
|
|
102
108
|
export const pascal = (id: string): string => id.split("-").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join("")
|
|
103
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
|
+
|
|
104
116
|
/** The manifest of the plugin in `dir` (its folder's name is its id). Every problem of the file is
|
|
105
117
|
* reported at once. */
|
|
106
118
|
export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
@@ -181,6 +193,23 @@ export const readManifest = (dir: string, id: string): PluginManifest => {
|
|
|
181
193
|
}
|
|
182
194
|
})
|
|
183
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
|
+
}
|
|
184
213
|
}
|
|
185
214
|
const android = isObject(raw.android) ? raw.android : null
|
|
186
215
|
if (android) {
|