lecodes-cli 2.4.2 → 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.
@@ -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
- // 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.
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
 
@@ -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 one folder under `src/main/java` that the path narrows down to. */
153
- export const kotlinPackage = (android: string): { dir: string, name: string } => {
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("."))
@@ -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 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/`).
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) {