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
|
@@ -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
|
+
}
|
package/src/dev/devServer.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { createReadStream, readFileSync, statSync } from "node:fs"
|
|
2
3
|
import { join } from "node:path"
|
|
3
4
|
import { Readable } from "node:stream"
|
|
4
5
|
import type { Manifest } from "../project/manifest"
|
|
@@ -14,6 +15,7 @@ import { toKey } from "../project/paths"
|
|
|
14
15
|
import { makeStackDemangler } from "../compile/nativeStack"
|
|
15
16
|
import type { WebRunner } from "./webRunner"
|
|
16
17
|
import { createProjectBackend } from "./projectBackend"
|
|
18
|
+
import { buildHalf, halfModule, projectHalves, type ProjectHalf } from "../web/halves"
|
|
17
19
|
|
|
18
20
|
/*
|
|
19
21
|
* The `lecodes dev` LAN server: serves the project to devices and hot-reloads it on change. It
|
|
@@ -35,7 +37,12 @@ import { createProjectBackend } from "./projectBackend"
|
|
|
35
37
|
* the native prelude; the player owns reload (ws / version
|
|
36
38
|
* polling) and the log backchannel. A phone is sent here at once
|
|
37
39
|
* GET /t/<tok>/lite/[file] the page of Web Lite (lecodes-web-lite's dist), opened as
|
|
38
|
-
* lite/?dev=.. — the same bundle.js?t=web, the same reload and logs
|
|
40
|
+
* lite/?dev=.. — the same bundle.js?t=web, the same reload and logs;
|
|
41
|
+
* the project's own web halves (web/halves.ts) under it as a build
|
|
42
|
+
* has them — lite/plugins/<folder>/index.js (built with each
|
|
43
|
+
* compile), its `web.static` folder from the project, and
|
|
44
|
+
* lite/plugins.json naming them for their ids (before the
|
|
45
|
+
* first-party half of the id: its fallback)
|
|
39
46
|
* POST /t/<tok>/server/api/<id> the project's backend (its `*.server.ts` files, run on this
|
|
40
47
|
* WS /t/<tok>/server/ws machine — projectBackend.ts): the wire of sdk/src/runtime/rpc.ts,
|
|
41
48
|
* the one the runner serves on le.codes
|
|
@@ -63,8 +70,12 @@ export type DevServerOptions = {
|
|
|
63
70
|
/** What is served under /web/ (src/dev/webRunner.ts): the player, and the page around it; absent =
|
|
64
71
|
* no browser route. */
|
|
65
72
|
web?: WebRunner | null
|
|
66
|
-
/** The page of Web Lite (lecodes-web-lite's dist), served under /lite/; absent = no such route.
|
|
73
|
+
/** The page of Web Lite (lecodes-web-lite's dist), served under /lite/; absent = no such route.
|
|
74
|
+
* With it, every compile builds the project's own web halves too (web/halves.ts). */
|
|
67
75
|
lite?: string | null
|
|
76
|
+
/** Serve over https with this certificate (`lecodes web dev --https`, dev/https.ts); `origin`
|
|
77
|
+
* says https then. */
|
|
78
|
+
tls?: { cert: string, key: string }
|
|
68
79
|
/** Diagnostics (compile errors, device logs); defaults to stderr-style silence-safe noop. */
|
|
69
80
|
log?: (msg: string) => void
|
|
70
81
|
/** The folder of the backend's database (default `.lecodes/db`; the browser tests keep their own). */
|
|
@@ -189,9 +200,12 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
189
200
|
const onWarning = (w: string) => { warnings.push(w) }
|
|
190
201
|
const serverBundle = await backend.compile(entries)
|
|
191
202
|
const js = await compileProject({ entries, name, publicUrl: origin, entryOverride, header: true, fontsHeader, iconsHeader, onWarning, serverUrl: serverBundle ? serverUrl : undefined })
|
|
203
|
+
// the page's own halves, before anything is told to reload: one that does not build fails the
|
|
204
|
+
// compile, the page keeps the last good ones
|
|
205
|
+
const halvesChanged = lite ? await buildHalves() : false
|
|
192
206
|
// the backend takes the new bundle once the app compiled, and before devices are told to reload
|
|
193
207
|
warnings.push(...await backend.run(serverBundle))
|
|
194
|
-
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs)
|
|
208
|
+
const changed = bundleJs === null || stripVolatile(js) !== stripVolatile(bundleJs) || halvesChanged
|
|
195
209
|
if (changed) {
|
|
196
210
|
bundleJs = js
|
|
197
211
|
v++
|
|
@@ -203,6 +217,36 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
220
|
+
// ---- the project's own web halves (web/halves.ts), for the page of Web Lite -----------------------
|
|
221
|
+
// Built with every compile; plugins.json names each by its hash (`index.js?h=…`), so a page that
|
|
222
|
+
// imported a half sees it changed and loads again (a half is imported once per document).
|
|
223
|
+
let halves: { half: ProjectHalf, js: string, hash: string }[] = []
|
|
224
|
+
const buildHalves = async (): Promise<boolean> => {
|
|
225
|
+
const next: typeof halves = []
|
|
226
|
+
for (const half of projectHalves(root)) {
|
|
227
|
+
const js = await buildHalf(half, { dev: true })
|
|
228
|
+
next.push({ half, js, hash: createHash("sha256").update(js).digest("hex").slice(0, 10) })
|
|
229
|
+
}
|
|
230
|
+
const key = (list: typeof halves) => list.map((h) => `${h.half.id}=${h.half.folder}@${h.hash}`).join(";")
|
|
231
|
+
const changed = key(next) !== key(halves)
|
|
232
|
+
halves = next
|
|
233
|
+
return changed
|
|
234
|
+
}
|
|
235
|
+
/** The page's plugins.json: Web Lite's first-party halves, the project's BEFORE theirs — a
|
|
236
|
+
* project's half of an id is tried first, the first-party one takes the page where it declines
|
|
237
|
+
* (`"ar": ["plugins/ar-8thwall/index.js?h=…", "plugins/ar/index-….js"]`, as `web build` writes). */
|
|
238
|
+
const liteTable = (): Record<string, string | string[]> => {
|
|
239
|
+
let plugins: Record<string, string> = {}
|
|
240
|
+
try { plugins = (JSON.parse(readFileSync(join(lite!, "plugins.json"), "utf8")) as { plugins?: Record<string, string> }).plugins ?? {} } catch { /* a page without plugins */ }
|
|
241
|
+
const table: Record<string, string | string[]> = { ...plugins }
|
|
242
|
+
for (const { half, hash } of halves) {
|
|
243
|
+
const own = `${halfModule(half)}?h=${hash}`
|
|
244
|
+
const firstParty = plugins[half.id]
|
|
245
|
+
table[half.id] = firstParty !== undefined ? [own, firstParty] : own
|
|
246
|
+
}
|
|
247
|
+
return table
|
|
248
|
+
}
|
|
249
|
+
|
|
206
250
|
// ---- the project's backend (projectBackend.ts), started with the first compile that has server files
|
|
207
251
|
const backend = createProjectBackend({ root, log, dbDir: options.dbDir, command: "lecodes dev" })
|
|
208
252
|
/** What the app's stubs call. Baked with the advertised origin; a bundle is served with the origin
|
|
@@ -259,9 +303,11 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
259
303
|
const transport = transportOf(url)
|
|
260
304
|
return transport === "web" ? js : js + "\n" + renderPrelude({ origin: reqOrigin, base, transport, v })
|
|
261
305
|
}
|
|
262
|
-
//
|
|
306
|
+
// Every url of this server the bundle names likewise — its assets, its shaders, the backend: the
|
|
307
|
+
// app reaches them through the origin the bundle was fetched from. (`web dev` advertises
|
|
308
|
+
// localhost; a phone that opened the page by the LAN address would otherwise ask ITSELF.)
|
|
263
309
|
const rebased = (js: string, reqOrigin: string) =>
|
|
264
|
-
reqOrigin === origin ? js : js.split(
|
|
310
|
+
reqOrigin === origin ? js : js.split(`${origin}${base}/`).join(`${reqOrigin}${base}/`)
|
|
265
311
|
const web = options.web ?? null
|
|
266
312
|
const lite = options.lite ?? null
|
|
267
313
|
/** A file of a built folder; the entry files revalidate — a CLI update swaps them under the same
|
|
@@ -280,7 +326,7 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
280
326
|
|
|
281
327
|
const handle = async (req: Request, ip: string | undefined): Promise<Response> => {
|
|
282
328
|
const host = req.headers.get("host")
|
|
283
|
-
const reqOrigin = host ?
|
|
329
|
+
const reqOrigin = host ? `${options.tls ? "https" : "http"}://${host}` : origin
|
|
284
330
|
const url = new URL(req.url)
|
|
285
331
|
const path = url.pathname
|
|
286
332
|
// Browsers ask for it on every page load (the /web/ runner) — keep the console clean.
|
|
@@ -317,6 +363,15 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
317
363
|
url.searchParams.set("dev", "..")
|
|
318
364
|
return redirect(`${base}/lite/?${url.searchParams.toString()}${url.hash}`)
|
|
319
365
|
}
|
|
366
|
+
if (rel === "/plugins.json") return json(200, { plugins: liteTable() })
|
|
367
|
+
const own = halves.find(({ half }) => rel.startsWith(`/plugins/${half.folder}/`))
|
|
368
|
+
if (own) {
|
|
369
|
+
const sub = rel.slice(`/plugins/${own.half.folder}/`.length)
|
|
370
|
+
if (sub === "index.js") return text(200, "text/javascript", own.js)
|
|
371
|
+
const folder = own.half.static
|
|
372
|
+
if (folder && sub.startsWith(`${folder}/`)) return built(own.half.dir, sub)
|
|
373
|
+
return text(404, "text/plain", "Not found")
|
|
374
|
+
}
|
|
320
375
|
return built(lite, rel === "/" ? "index.html" : rel.slice(1))
|
|
321
376
|
}
|
|
322
377
|
if (route === "/version") {
|
|
@@ -372,6 +427,7 @@ export const startDevServer = async (options: DevServerOptions): Promise<DevServ
|
|
|
372
427
|
const server = await serve({
|
|
373
428
|
port: options.port,
|
|
374
429
|
hostname: options.bind,
|
|
430
|
+
tls: options.tls,
|
|
375
431
|
fetch: (req, peer) => handle(req, peer.ip),
|
|
376
432
|
websocket: (req, peer) => {
|
|
377
433
|
const path = new URL(req.url).pathname
|
package/src/dev/https.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { join } from "node:path"
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* The certificate of `lecodes web dev --https`. A phone's camera and motion sensors are given to a
|
|
7
|
+
* SECURE page only, and on the LAN that is an https one: a self-signed certificate, made once and
|
|
8
|
+
* kept in ~/.lecodes/dev-https/ — for every address it was asked for (localhost, the machine's LAN
|
|
9
|
+
* addresses), made again when one is new or when it is old. No authority signed it: the browser
|
|
10
|
+
* warns, the person accepts it once per device.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const DIR = () => join(homedir(), ".lecodes", "dev-https")
|
|
14
|
+
/** Days a certificate is valid: under the 398 a phone's browser accepts for a server's. */
|
|
15
|
+
const DAYS = 397
|
|
16
|
+
/** Made again after this many: before it runs out. */
|
|
17
|
+
const RENEW_DAYS = 300
|
|
18
|
+
|
|
19
|
+
type Kept = { names: string[], made: number }
|
|
20
|
+
|
|
21
|
+
/** The certificate for `names` (+ localhost); `dir` is where it is kept (a test's own). */
|
|
22
|
+
export const devCertificate = async (names: string[], dir = DIR()): Promise<{ cert: string, key: string }> => {
|
|
23
|
+
const files = { cert: join(dir, "cert.pem"), key: join(dir, "key.pem"), meta: join(dir, "names.json") }
|
|
24
|
+
let kept: Kept | null = null
|
|
25
|
+
try { kept = JSON.parse(readFileSync(files.meta, "utf8")) as Kept } catch { /* none yet */ }
|
|
26
|
+
const wanted = [...new Set(["localhost", "127.0.0.1", ...names])]
|
|
27
|
+
const fresh = kept && Date.now() - kept.made < RENEW_DAYS * 86_400_000 && wanted.every((n) => kept!.names.includes(n))
|
|
28
|
+
if (fresh && existsSync(files.cert) && existsSync(files.key)) {
|
|
29
|
+
return { cert: readFileSync(files.cert, "utf8"), key: readFileSync(files.key, "utf8") }
|
|
30
|
+
}
|
|
31
|
+
// (only here: the one command that asks for it loads it)
|
|
32
|
+
const { generate } = await import("selfsigned")
|
|
33
|
+
const all = [...new Set([...(kept?.names ?? []), ...wanted])]
|
|
34
|
+
const ip = /^\d{1,3}(\.\d{1,3}){3}$/
|
|
35
|
+
const made = generate([{ name: "commonName", value: "LeCodes dev" }, { name: "organizationName", value: "LeCodes dev server" }], {
|
|
36
|
+
days: DAYS, keySize: 2048, algorithm: "sha256",
|
|
37
|
+
extensions: [
|
|
38
|
+
{ name: "basicConstraints", cA: false },
|
|
39
|
+
{ name: "keyUsage", digitalSignature: true, keyEncipherment: true },
|
|
40
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
41
|
+
// altName types of X.509: 2 = a DNS name, 7 = an IP address
|
|
42
|
+
{ name: "subjectAltName", altNames: all.map((n) => (ip.test(n) ? { type: 7, ip: n } : { type: 2, value: n })) },
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
mkdirSync(dir, { recursive: true })
|
|
46
|
+
writeFileSync(files.cert, made.cert)
|
|
47
|
+
writeFileSync(files.key, made.private)
|
|
48
|
+
writeFileSync(files.meta, JSON.stringify({ names: all, made: Date.now() } satisfies Kept))
|
|
49
|
+
return { cert: made.cert, key: made.private }
|
|
50
|
+
}
|
package/src/dev/serve.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { createServer, type IncomingMessage } from "node:http"
|
|
2
|
-
import {
|
|
1
|
+
import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
|
|
2
|
+
import { createServer as createHttpsServer } from "node:https"
|
|
3
|
+
import { Readable, type Duplex } from "node:stream"
|
|
3
4
|
import { upgradeWebSocket } from "./wsServer"
|
|
4
5
|
|
|
5
6
|
/*
|
|
@@ -35,6 +36,8 @@ export type ServeOptions = {
|
|
|
35
36
|
port: number
|
|
36
37
|
/** Bind address (default 0.0.0.0). */
|
|
37
38
|
hostname?: string
|
|
39
|
+
/** https (and wss) with this certificate and key, PEM. */
|
|
40
|
+
tls?: { cert: string, key: string }
|
|
38
41
|
fetch: (req: Request, peer: Peer) => Response | Promise<Response>
|
|
39
42
|
/** A websocket asks to connect: return what takes the open socket, or null to refuse. Decided
|
|
40
43
|
* from the request alone (its path), before the handshake. */
|
|
@@ -64,6 +67,7 @@ const serveBun = (opts: ServeOptions): Served => {
|
|
|
64
67
|
server = Bun.serve<BunSocketData>({
|
|
65
68
|
port: opts.port,
|
|
66
69
|
hostname: opts.hostname ?? "0.0.0.0",
|
|
70
|
+
...(opts.tls ? { tls: { cert: opts.tls.cert, key: opts.tls.key } } : {}),
|
|
67
71
|
// seconds, the most Bun takes: an endpoint of a project's backend may think for 30
|
|
68
72
|
idleTimeout: 255,
|
|
69
73
|
async fetch(req, srv) {
|
|
@@ -110,33 +114,35 @@ const serveBun = (opts: ServeOptions): Served => {
|
|
|
110
114
|
|
|
111
115
|
// ───────────────────────────── Node ─────────────────────────────
|
|
112
116
|
|
|
113
|
-
const requestOf = (req: IncomingMessage, upgrade = false): Request => {
|
|
117
|
+
const requestOf = (req: IncomingMessage, upgrade = false, scheme = "http"): Request => {
|
|
114
118
|
const headers = new Headers()
|
|
115
119
|
for (const [k, v] of Object.entries(req.headers)) {
|
|
116
120
|
if (v !== undefined) headers.set(k, Array.isArray(v) ? v.join(", ") : v)
|
|
117
121
|
}
|
|
118
122
|
const method = req.method ?? "GET"
|
|
119
123
|
const bodyless = upgrade || method === "GET" || method === "HEAD"
|
|
120
|
-
return new Request(new URL(req.url ?? "/",
|
|
124
|
+
return new Request(new URL(req.url ?? "/", `${scheme}://${req.headers.host ?? "localhost"}`).href, {
|
|
121
125
|
method, headers,
|
|
122
126
|
...(bodyless ? {} : { body: Readable.toWeb(req), duplex: "half" }),
|
|
123
127
|
} as unknown as RequestInit)
|
|
124
128
|
}
|
|
125
129
|
|
|
126
130
|
const serveNode = async (opts: ServeOptions): Promise<Served> => {
|
|
127
|
-
const
|
|
131
|
+
const scheme = opts.tls ? "https" : "http"
|
|
132
|
+
const onRequest = (req: IncomingMessage, res: ServerResponse) => {
|
|
128
133
|
void (async () => {
|
|
129
134
|
let response: Response
|
|
130
|
-
try { response = await opts.fetch(requestOf(req), { ip: req.socket.remoteAddress }) }
|
|
135
|
+
try { response = await opts.fetch(requestOf(req, false, scheme), { ip: req.socket.remoteAddress }) }
|
|
131
136
|
catch (e) { response = failure(opts, e) }
|
|
132
137
|
res.writeHead(response.status, Object.fromEntries(response.headers))
|
|
133
138
|
if (!response.body || req.method === "HEAD") { res.end(); return }
|
|
134
139
|
Readable.fromWeb(response.body as unknown as import("node:stream/web").ReadableStream).on("error", () => res.destroy()).pipe(res)
|
|
135
140
|
})()
|
|
136
|
-
}
|
|
137
|
-
server.
|
|
141
|
+
}
|
|
142
|
+
const server = opts.tls ? createHttpsServer({ cert: opts.tls.cert, key: opts.tls.key }, onRequest) : createServer(onRequest)
|
|
143
|
+
server.on("upgrade", (req: IncomingMessage, socket: Duplex) => {
|
|
138
144
|
let accept: ((socket: Socket) => SocketHandlers) | null = null
|
|
139
|
-
try { accept = opts.websocket?.(requestOf(req, true), { ip: req.socket.remoteAddress }) ?? null }
|
|
145
|
+
try { accept = opts.websocket?.(requestOf(req, true, scheme), { ip: req.socket.remoteAddress }) ?? null }
|
|
140
146
|
catch (e) { opts.onError?.(e) }
|
|
141
147
|
if (!accept) { socket.destroy(); return }
|
|
142
148
|
let handlers: SocketHandlers | null = null
|
package/src/dev/webRunner.ts
CHANGED
|
@@ -34,6 +34,8 @@ type Built = {
|
|
|
34
34
|
sources: string[]
|
|
35
35
|
/** The arguments of the `vite build` that makes it. */
|
|
36
36
|
build: string[]
|
|
37
|
+
/** A command run after it in the package (a build step outside vite), when there is one. */
|
|
38
|
+
after?: string[]
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const PLAYER: Built = {
|
|
@@ -47,10 +49,11 @@ const PAGE: Built = {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// Web Lite (lecodes-web-lite's dist): the app as a web page, DOM and CSS — what `lecodes web`
|
|
50
|
-
// serves and builds from.
|
|
52
|
+
// serves and builds from. Its plugins' halves are a step of their own after the vite build (which
|
|
53
|
+
// empties dist/): build/plugins.ts writes dist/plugins/ and plugins.json from the plugin repo.
|
|
51
54
|
const LITE: Built = {
|
|
52
|
-
pkg: "lecodes-web-lite", dist: "dist", entry: "lite.js", vendored: "web-lite", build: [],
|
|
53
|
-
sources: ["hosts/web-lite/src", "hosts/web-lite/index.html", "renderers/web-lite/src", "renderers/web-shared/src", "hosts/web-player/src/dev.ts", "engines/gl-lite/src"],
|
|
55
|
+
pkg: "lecodes-web-lite", dist: "dist", entry: "lite.js", vendored: "web-lite", build: [], after: ["bun", "build/plugins.ts"],
|
|
56
|
+
sources: ["hosts/web-lite/src", "hosts/web-lite/index.html", "hosts/web-lite/build", "renderers/web-lite/src", "renderers/web-shared/src", "hosts/web-player/src/dev.ts", "engines/gl-lite/src", "hosts/plugins/plugins"],
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
/** The built page of Web Lite, or null when this CLI has none. */
|
|
@@ -71,6 +74,11 @@ const resolve = (built: Built, log: (message: string) => void): string | null =>
|
|
|
71
74
|
log(`Building ${built.pkg} (its sources are newer than its build)…`)
|
|
72
75
|
const done = spawnSync("bunx", ["vite", "build", ...built.build], { cwd: checkout, stdio: "pipe", encoding: "utf8", shell: process.platform === "win32" })
|
|
73
76
|
if (done.status !== 0) log(`${built.pkg} did not build — the browser gets the build that is there:\n${done.stderr || done.stdout}`)
|
|
77
|
+
else if (built.after) {
|
|
78
|
+
const [command, ...args] = built.after
|
|
79
|
+
const step = spawnSync(command!, args, { cwd: checkout, stdio: "pipe", encoding: "utf8", shell: process.platform === "win32" })
|
|
80
|
+
if (step.status !== 0) log(`${built.pkg}: \`${built.after.join(" ")}\` failed — its build is without that step:\n${step.stderr || step.stdout}`)
|
|
81
|
+
}
|
|
74
82
|
}
|
|
75
83
|
if (has(dist)) return dist
|
|
76
84
|
}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs"
|
|
2
2
|
import type { Project, ProjectAsset } from "./api"
|
|
3
3
|
import type { LocalScan } from "../project/localFiles"
|
|
4
|
-
import {
|
|
4
|
+
import { isTextKind } from "../project/localFiles"
|
|
5
5
|
import { buildPaths } from "../project/materialize"
|
|
6
|
-
import { basename } from "node:path"
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
* Compares the local working tree against the project's current server state — the basis for
|
|
@@ -49,7 +48,7 @@ export const computeServerDiff = (project: Project, scan: LocalScan): ServerDiff
|
|
|
49
48
|
const local = localByPath.get(path)
|
|
50
49
|
|
|
51
50
|
if (local && !server) {
|
|
52
|
-
const text = isTextKind(
|
|
51
|
+
const text = isTextKind(local.kind)
|
|
53
52
|
entries.push({ path, status: "added", binary: !text, serverText: "", localText: text ? readFileSync(local.absPath, "utf8") : undefined })
|
|
54
53
|
continue
|
|
55
54
|
}
|
|
@@ -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
|
|