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,3 +1,5 @@
|
|
|
1
|
+
import type { AppLink } from "../shared"
|
|
2
|
+
|
|
1
3
|
/* Templates for `lecodes app init android` / `lecodes app sync` — the Android mirror of
|
|
2
4
|
* ios.ts, distilled from the LetaryCreatorViewer host (lecodes-android :app). The
|
|
3
5
|
* same three ownership tiers: app/ is USER-owned after generation (init writes once, sync
|
|
@@ -155,6 +157,75 @@ export const runtimeManifestTemplate = (updateUrl: string | undefined): string =
|
|
|
155
157
|
</manifest>
|
|
156
158
|
`
|
|
157
159
|
|
|
160
|
+
/** The window setup every shell's theme inherits (what Theme.LeCodesApp carried before the splash):
|
|
161
|
+
* edge-to-edge bars, no title, and the window background — black, or app.json "splash"'s
|
|
162
|
+
* layer-list. */
|
|
163
|
+
const runtimeThemeItems = (windowBackground: string): string => `
|
|
164
|
+
<item name="android:windowTranslucentNavigation">true</item>
|
|
165
|
+
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
166
|
+
<item name="android:windowNoTitle">true</item>
|
|
167
|
+
<item name="android:windowTranslucentStatus">true</item>
|
|
168
|
+
<item name="android:windowBackground">${windowBackground}</item>`
|
|
169
|
+
|
|
170
|
+
/** What app.json "splash" resolved to, as the runtime module's resources need it (`size` = the side
|
|
171
|
+
* of the square the image is fitted into, dp). */
|
|
172
|
+
export interface AndroidSplashResources { backgroundHex: string, image: boolean, size: number }
|
|
173
|
+
|
|
174
|
+
/** The Android 12+ splash-icon canvas (an icon without an icon background), dp — the system shows
|
|
175
|
+
* its inner 192 dp circle. The image sits on it inset by (canvas − size) / 2. */
|
|
176
|
+
export const SPLASH_ICON_CANVAS_DP = 288
|
|
177
|
+
|
|
178
|
+
/** lecodes-runtime/src/main/res/values/lecodes_splash.xml — CLI-owned, written by init and every
|
|
179
|
+
* sync: Theme.LeCodesRuntime, the parent of the app's user-owned Theme.LeCodesApp (themes.xml).
|
|
180
|
+
* Without "splash" its window is black, as a shell's always was; with it, the colour and the
|
|
181
|
+
* layer-list lecodes_splash_window (API 23–30 show the splash as the window background). The
|
|
182
|
+
* parent needs appcompat on the module's classpath: SDK_RUNTIME_DEPS declares it. */
|
|
183
|
+
export const runtimeThemeTemplate = (splash?: AndroidSplashResources): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
184
|
+
<!-- Generated by \`lecodes app sync\` — regenerated on every sync, do not edit. Theme.LeCodesRuntime is
|
|
185
|
+
the parent of the app's Theme.LeCodesApp (app/src/main/res/values/themes.xml); app.json "splash"
|
|
186
|
+
draws its window. -->
|
|
187
|
+
<resources>${splash ? `
|
|
188
|
+
<color name="lecodes_splash_background">${splash.backgroundHex}</color>` : ""}
|
|
189
|
+
<style name="Theme.LeCodesRuntime" parent="Theme.AppCompat.Light.NoActionBar">${runtimeThemeItems(splash ? "@drawable/lecodes_splash_window" : "@android:color/black")}
|
|
190
|
+
</style>
|
|
191
|
+
</resources>
|
|
192
|
+
`
|
|
193
|
+
|
|
194
|
+
/** lecodes-runtime/src/main/res/values-v31/lecodes_splash.xml — only with app.json "splash": the
|
|
195
|
+
* theme's Android 12+ twin (a v31 style replaces the base one whole, hence every item again) adding
|
|
196
|
+
* the system splash — the colour, and as the icon the image inset (288 − size) / 2 dp on each side
|
|
197
|
+
* (the 288 dp icon canvas with the image at its size, 160 dp by default) or nothing (transparent: the system would draw the launcher
|
|
198
|
+
* icon otherwise). */
|
|
199
|
+
export const runtimeThemeV31Template = (splash: AndroidSplashResources): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
200
|
+
<!-- Generated by \`lecodes app sync\` from app.json "splash" — regenerated on every sync, do not edit. -->
|
|
201
|
+
<resources>
|
|
202
|
+
<style name="Theme.LeCodesRuntime" parent="Theme.AppCompat.Light.NoActionBar">${runtimeThemeItems("@drawable/lecodes_splash_window")}
|
|
203
|
+
<item name="android:windowSplashScreenBackground">@color/lecodes_splash_background</item>
|
|
204
|
+
<item name="android:windowSplashScreenAnimatedIcon">${splash.image ? "@drawable/lecodes_splash_icon" : "@android:color/transparent"}</item>
|
|
205
|
+
</style>
|
|
206
|
+
</resources>
|
|
207
|
+
`
|
|
208
|
+
|
|
209
|
+
/** lecodes-runtime/src/main/res/drawable/lecodes_splash_window.xml — the splash as a window
|
|
210
|
+
* background: the colour, and the image centred at size × size dp (drawable-*dpi/lecodes_splash.png). */
|
|
211
|
+
export const splashWindowTemplate = (splash: AndroidSplashResources): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
212
|
+
<!-- Generated by \`lecodes app sync\` from app.json "splash" — regenerated on every sync, do not edit. -->
|
|
213
|
+
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
|
|
214
|
+
<item android:drawable="@color/lecodes_splash_background" />${splash.image ? `
|
|
215
|
+
<item android:width="${splash.size}dp" android:height="${splash.size}dp" android:gravity="center" android:drawable="@drawable/lecodes_splash" />` : ""}
|
|
216
|
+
</layer-list>
|
|
217
|
+
`
|
|
218
|
+
|
|
219
|
+
/** lecodes-runtime/src/main/res/drawable/lecodes_splash_icon.xml — Android 12+'s splash icon: the
|
|
220
|
+
* size dp image inset to the 288 dp canvas the system draws (and masks to a 192 dp circle). The
|
|
221
|
+
* inset is rounded down — an odd size leaves the image half a dp larger, which nobody sees. */
|
|
222
|
+
export const splashIconTemplate = (size: number): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
223
|
+
<!-- Generated by \`lecodes app sync\` from app.json "splash" — regenerated on every sync, do not edit. -->
|
|
224
|
+
<inset xmlns:android="http://schemas.android.com/apk/res/android"
|
|
225
|
+
android:drawable="@drawable/lecodes_splash"
|
|
226
|
+
android:inset="${Math.floor((SPLASH_ICON_CANVAS_DP - size) / 2)}dp" />
|
|
227
|
+
`
|
|
228
|
+
|
|
158
229
|
/** lecodes-runtime/src/main/kotlin/io/letary/lecodes/runtime/LeCodesRuntime.kt — the generated
|
|
159
230
|
* registration + host-hook surface. Plugins are called by FQN (no import collisions); the
|
|
160
231
|
* activity hooks forward to the SDK's LeCodesAppHooks, which NAMES NO PLUGIN — a plugin that
|
|
@@ -259,7 +330,8 @@ lecodes app build android # signed release .apk + .aab → ../dist/
|
|
|
259
330
|
| \`app/\` (sources, manifest, res, build.gradle.kts, proguard-rules.pro) | **Yours** after init — sync never rewrites them (it only restamps the \`versionName\`/\`versionCode\` and \`isMinifyEnabled\`/\`isShrinkResources\` values, manages \`// lecodes:plugin\`-tagged lines in the plugins block, and — when \`app.json\` sets \`"icon"\` — the icon files below) |
|
|
260
331
|
| \`app/src/main/assets/\` | CLI-owned — wiped and regenerated by every sync (the compiled \`app.js\` + its \`// preload:\` assets) |
|
|
261
332
|
| \`res/mipmap-*/ic_launcher*\`, \`res/values/lecodes_icon.xml\` | CLI-owned **only when** \`app.json\` sets \`"icon"\` — regenerated by every sync (plus a value-only \`android:icon\` restamp in the manifest); without the key sync never touches icons |
|
|
262
|
-
| \`
|
|
333
|
+
| \`app/src/main/res/values/themes.xml\` | **Yours** — its \`Theme.LeCodesApp\` must keep \`parent="Theme.LeCodesRuntime"\` (the window setup and the splash, see "Splash screen") |
|
|
334
|
+
| \`settings.gradle.kts\`, \`build.gradle.kts\` (root), \`gradle.properties\`, \`lecodes-runtime/\`, \`plugins/\` | CLI-owned — regenerated by every sync (\`lecodes-runtime/src/main/res/\`: \`Theme.LeCodesRuntime\` and, when \`app.json\` sets \`"splash"\`, the splash resources) |
|
|
263
335
|
| \`gradle/\`, \`gradlew\`, \`gradlew.bat\` | Written once at init |
|
|
264
336
|
|
|
265
337
|
## The SDK
|
|
@@ -346,23 +418,50 @@ Prefer hand-made icons? Leave \`"icon"\` unset and use Android Studio (right-cli
|
|
|
346
418
|
New → Image Asset) — sync never touches icon files then. \`drawable/ic_launcher.xml\` is only
|
|
347
419
|
the init-time placeholder.
|
|
348
420
|
|
|
421
|
+
## Splash screen
|
|
422
|
+
|
|
423
|
+
\`"splash": { "background": "#RRGGBB", "image": "./splash.png", "size": 160 }\` in \`../app.json\`
|
|
424
|
+
(every key optional) makes every sync render the splash into \`lecodes-runtime/src/main/res/\`: the
|
|
425
|
+
colour (\`background\`, default \`"iconBackground"\`, default white) with the image fitted centred into
|
|
426
|
+
a \`size\`×\`size\` dp square (160 by default) — never at its pixel size — from
|
|
427
|
+
\`drawable-mdpi…xxxhdpi/lecodes_splash.png\` (size × 1/1.5/2/3/4 px: 160…640 at the default) of the one
|
|
428
|
+
master. Android 12+ shows it as the system splash (\`windowSplashScreenBackground\` +
|
|
429
|
+
\`windowSplashScreenAnimatedIcon\`: the image on the 288 dp icon canvas, inset (288 − size) / 2 dp,
|
|
430
|
+
of which the system shows only the inner 192 dp circle — keep the mark inside one; a \`size\` above
|
|
431
|
+
192 is cut there, sync warns, and 288 is the most it takes), API 23–30 as the window background. Use
|
|
432
|
+
a square mark of at least 3 × size px (a non-square one is fitted into the square, a smaller one is
|
|
433
|
+
upscaled). No image = the colour
|
|
434
|
+
alone (and no icon on Android 12+, not the launcher one). The app holds the splash until its first
|
|
435
|
+
frame — through the first-launch wait of an OTA shell too.
|
|
436
|
+
|
|
437
|
+
It reaches the app through the theme: \`Theme.LeCodesApp\` in \`app/src/main/res/values/themes.xml\`
|
|
438
|
+
(yours) must say \`parent="Theme.LeCodesRuntime"\` and set no \`android:windowBackground\` of its
|
|
439
|
+
own. A shell generated before the key existed does not — \`lecodes app sync\` and \`lecodes app
|
|
440
|
+
doctor\` print the edit. Without the key the runtime theme keeps the black window shells always had.
|
|
441
|
+
|
|
349
442
|
## Notes
|
|
350
443
|
|
|
351
444
|
|
|
352
445
|
## Over-the-air updates
|
|
353
446
|
|
|
354
|
-
Native, before the JS engine: \`LeCodesShell.boot(engine,
|
|
355
|
-
source for newer code (a conditional GET; offline is a no-op)
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
447
|
+
Native, before the JS engine: \`LeCodesShell.boot(engine, activity)\` in \`AppViewModel\` takes the
|
|
448
|
+
newest LOCAL bundle, then asks the source for newer code (a conditional GET; offline is a no-op)
|
|
449
|
+
and stores it. The FIRST launch of an installed binary waits for that answer (5 s at most, the
|
|
450
|
+
splash held on screen) and runs the newest bundle at once, so a fresh install from the store never
|
|
451
|
+
starts on the older code it shipped with; every later launch runs the local bundle at once and a
|
|
452
|
+
download applies on the NEXT launch. Configured in \`../app.json\`: \`"update": true\` = the
|
|
453
|
+
project's live bundle on the platform (a pushed project), \`"update": { "url": "https://…/app.js" }\`
|
|
454
|
+
= your own hosting, \`false\` = off. Sync writes the URL into
|
|
359
455
|
\`lecodes-runtime/src/main/AndroidManifest.xml\` (meta-data \`io.letary.lecodes.UPDATE_URL\`). A
|
|
360
|
-
download runs only when it is of this shell's SDK major and not older than the bundle running
|
|
361
|
-
store update
|
|
362
|
-
|
|
456
|
+
download runs only when it is of this shell's SDK major and not older than the bundle running. A
|
|
457
|
+
store update keeps a downloaded bundle only when it is newer code than the one the update ships
|
|
458
|
+
(by compile date); otherwise the store build wins. A shell generated before OTA existed reads
|
|
459
|
+
\`assets/app.js\` itself, and one generated before the first-launch wait calls
|
|
460
|
+
\`LeCodesShell.boot(engine, getApplication<Application>())\` (which never waits) — replace either
|
|
461
|
+
with:
|
|
363
462
|
|
|
364
463
|
\`\`\`kotlin
|
|
365
|
-
if (!LeCodesShell.boot(engine,
|
|
464
|
+
if (!LeCodesShell.boot(engine, activity)) Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
|
|
366
465
|
\`\`\`
|
|
367
466
|
- Push: the notification service needs \`app/google-services.json\` from the le.codes Firebase
|
|
368
467
|
project; \`lecodes app sync\` applies the google-services Gradle plugin only when that file
|
|
@@ -467,7 +566,31 @@ export const urlSchemeFilter = (urlScheme?: string): string => !urlScheme ? "" :
|
|
|
467
566
|
</intent-filter>
|
|
468
567
|
<!-- /lecodes:urlScheme -->`
|
|
469
568
|
|
|
470
|
-
|
|
569
|
+
/** The verified-link intent-filters of app.json "appLinks" (`restampManifestAppLinks`): one
|
|
570
|
+
* `autoVerify` filter per host — the `<data>` elements of one filter cross-multiply, so a host's
|
|
571
|
+
* paths stay in its own filter; a root link (no path) takes the whole host. Empty without links. */
|
|
572
|
+
export const appLinksFilter = (links: AppLink[]): string => {
|
|
573
|
+
if (links.length === 0) return ""
|
|
574
|
+
const hosts = [...new Set(links.map(l => l.host))]
|
|
575
|
+
const filters = hosts.map(host => {
|
|
576
|
+
const paths = links.filter(l => l.host === host).map(l => l.path)
|
|
577
|
+
const prefixes = paths.includes("") ? [] : paths
|
|
578
|
+
return `
|
|
579
|
+
<intent-filter android:autoVerify="true">
|
|
580
|
+
<action android:name="android.intent.action.VIEW" />
|
|
581
|
+
<category android:name="android.intent.category.DEFAULT" />
|
|
582
|
+
<category android:name="android.intent.category.BROWSABLE" />
|
|
583
|
+
<data android:scheme="https" />
|
|
584
|
+
<data android:host="${host}" />${prefixes.map(p => `
|
|
585
|
+
<data android:pathPrefix="${p}" />`).join("")}
|
|
586
|
+
</intent-filter>`
|
|
587
|
+
}).join("")
|
|
588
|
+
return `
|
|
589
|
+
<!-- lecodes:appLinks — app.json "appLinks": the verified https links the app opens on (the "url" app event); each host serves /.well-known/assetlinks.json -->${filters}
|
|
590
|
+
<!-- /lecodes:appLinks -->`
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export const appManifestTemplate = (urlScheme?: string, appLinks: AppLink[] = []): string => `<?xml version="1.0" encoding="utf-8"?>
|
|
471
594
|
<!-- Generated by \`lecodes app init\` — yours to edit. Sync leaves it alone, except that with
|
|
472
595
|
app.json "icon" set it restamps the android:icon VALUE to the generated @mipmap set.
|
|
473
596
|
Plugin permissions/services arrive through the plugin modules' own manifests via the AGP
|
|
@@ -492,7 +615,7 @@ export const appManifestTemplate = (urlScheme?: string): string => `<?xml versio
|
|
|
492
615
|
<intent-filter>
|
|
493
616
|
<action android:name="android.intent.action.MAIN" />
|
|
494
617
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
495
|
-
</intent-filter>${urlSchemeFilter(urlScheme)}
|
|
618
|
+
</intent-filter>${urlSchemeFilter(urlScheme)}${appLinksFilter(appLinks)}
|
|
496
619
|
</activity>
|
|
497
620
|
</application>
|
|
498
621
|
|
|
@@ -616,12 +739,14 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
|
|
|
616
739
|
fun onActivityCreate(activity: Activity) {
|
|
617
740
|
engine.attach(activity)
|
|
618
741
|
if (!isCreated) {
|
|
619
|
-
// OTA (native, no JS involved): LeCodesShell.boot
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
|
|
742
|
+
// OTA (native, no JS involved): LeCodesShell.boot takes the newest local bundle (a
|
|
743
|
+
// downloaded update, or the embedded assets/app.js) and starts the check BEFORE it runs
|
|
744
|
+
// — even a bundle that crashes on boot can never block its own fix. The first launch of
|
|
745
|
+
// this binary waits for the check (5 s at most; the splash stays, the first frame is
|
|
746
|
+
// held) and runs the newest bundle; later launches run at once and new code applies on
|
|
747
|
+
// the next launch. Configured via app.json "update" (→ the runtime module's manifest
|
|
748
|
+
// meta-data); offline: no wait.
|
|
749
|
+
if (!LeCodesShell.boot(engine, activity)) {
|
|
625
750
|
android.util.Log.e("LeCodes", "bundled app.js is missing — run \`lecodes app sync\`")
|
|
626
751
|
}
|
|
627
752
|
isCreated = true
|
|
@@ -631,13 +756,11 @@ class AppViewModel(application: Application) : AndroidViewModel(application) {
|
|
|
631
756
|
`
|
|
632
757
|
|
|
633
758
|
export const themesXmlTemplate = (): string => `<resources>
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
<item name="android:windowBackground">@color/black</item>
|
|
640
|
-
</style>
|
|
759
|
+
<!-- The app's theme. Its parent, Theme.LeCodesRuntime (the CLI-owned lecodes-runtime module),
|
|
760
|
+
carries the window setup and app.json "splash" — the launch window and Android 12+'s
|
|
761
|
+
system splash; keep that parent and set no android:windowBackground here, or the splash
|
|
762
|
+
is lost. Your own items go inside. -->
|
|
763
|
+
<style name="Theme.LeCodesApp" parent="Theme.LeCodesRuntime" />
|
|
641
764
|
</resources>
|
|
642
765
|
`
|
|
643
766
|
|
|
@@ -636,6 +636,69 @@ export const LAUNCH_STORYBOARD = `<?xml version="1.0" encoding="UTF-8" standalon
|
|
|
636
636
|
</document>
|
|
637
637
|
`
|
|
638
638
|
|
|
639
|
+
/** App/Base.lproj/LaunchScreen.storyboard as sync writes it when app.json sets "splash" (the file
|
|
640
|
+
* is CLI-owned then; without the key it stays the person's, LAUNCH_STORYBOARD of init): the view's
|
|
641
|
+
* colour is the colorset LeCodesSplashBackground, the image — when there is one — a size×size pt
|
|
642
|
+
* UIImageView (aspect fit; "splash.size", 160 by default) of the imageset LeCodesSplash centred on
|
|
643
|
+
* the view. Both live in Assets.xcassets, which
|
|
644
|
+
* the project already compiles, so no pbxproj edit. `rgb` (0xRRGGBB) only feeds the storyboard's
|
|
645
|
+
* resource cache (Interface Builder's preview); the app reads the colorset. LeCodesShellViewController
|
|
646
|
+
* mounts this same storyboard over its container until the first frame. */
|
|
647
|
+
export const splashStoryboardTemplate = (image: boolean, rgb: number, size = 160): string => {
|
|
648
|
+
const channel = (shift: number) => String(((rgb >> shift) & 0xff) / 255)
|
|
649
|
+
// The frame is Interface Builder's preview on its 375×667 canvas; the constraints place the view.
|
|
650
|
+
const x = (375 - size) / 2, y = (667 - size) / 2
|
|
651
|
+
const imageView = image ? `
|
|
652
|
+
<subviews>
|
|
653
|
+
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LeCodesSplash" translatesAutoresizingMaskIntoConstraints="NO" id="LcS-im-001">
|
|
654
|
+
<rect key="frame" x="${x}" y="${y}" width="${size}" height="${size}"/>
|
|
655
|
+
<constraints>
|
|
656
|
+
<constraint firstAttribute="width" constant="${size}" id="LcS-cw-001"/>
|
|
657
|
+
<constraint firstAttribute="height" constant="${size}" id="LcS-ch-001"/>
|
|
658
|
+
</constraints>
|
|
659
|
+
</imageView>
|
|
660
|
+
</subviews>` : ""
|
|
661
|
+
const centre = image ? `
|
|
662
|
+
<constraints>
|
|
663
|
+
<constraint firstItem="LcS-im-001" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="LcS-cx-001"/>
|
|
664
|
+
<constraint firstItem="LcS-im-001" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="LcS-cy-001"/>
|
|
665
|
+
</constraints>` : ""
|
|
666
|
+
return `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
667
|
+
<!-- Generated by \`lecodes app sync\` from app.json "splash" — regenerated on every sync, do not edit. -->
|
|
668
|
+
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
|
669
|
+
<dependencies>
|
|
670
|
+
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
|
671
|
+
<capability name="Named colors" minToolsVersion="9.0"/>
|
|
672
|
+
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
673
|
+
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
674
|
+
</dependencies>
|
|
675
|
+
<scenes>
|
|
676
|
+
<!--View Controller-->
|
|
677
|
+
<scene sceneID="EHf-IW-A2E">
|
|
678
|
+
<objects>
|
|
679
|
+
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
|
680
|
+
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
|
681
|
+
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
682
|
+
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>${imageView}
|
|
683
|
+
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
|
684
|
+
<color key="backgroundColor" name="LeCodesSplashBackground"/>${centre}
|
|
685
|
+
</view>
|
|
686
|
+
</viewController>
|
|
687
|
+
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
|
688
|
+
</objects>
|
|
689
|
+
<point key="canvasLocation" x="53" y="375"/>
|
|
690
|
+
</scene>
|
|
691
|
+
</scenes>
|
|
692
|
+
<resources>${image ? `
|
|
693
|
+
<image name="LeCodesSplash" width="${size}" height="${size}"/>` : ""}
|
|
694
|
+
<namedColor name="LeCodesSplashBackground">
|
|
695
|
+
<color red="${channel(16)}" green="${channel(8)}" blue="${channel(0)}" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
|
696
|
+
</namedColor>
|
|
697
|
+
</resources>
|
|
698
|
+
</document>
|
|
699
|
+
`
|
|
700
|
+
}
|
|
701
|
+
|
|
639
702
|
export const ASSETS_ROOT_JSON = `{
|
|
640
703
|
"info" : {
|
|
641
704
|
"author" : "xcode",
|
|
@@ -703,8 +766,16 @@ export interface RuntimePlugin {
|
|
|
703
766
|
id: string
|
|
704
767
|
/** Swift packages the target links (manifest ios.packages). */
|
|
705
768
|
packages?: IosPackage[]
|
|
769
|
+
/** The binary targets the target links (manifest ios.binaries), by name: the xcframeworks sync
|
|
770
|
+
* copies to `runtimeBinaryPath(id, name)`. */
|
|
771
|
+
binaries?: string[]
|
|
706
772
|
}
|
|
707
773
|
|
|
774
|
+
/** Where sync copies a plugin's prebuilt xcframework (manifest ios.binaries), relative to the
|
|
775
|
+
* LeCodesRuntime package: inside it, as SwiftPM takes a binary target's path, beside the vendored
|
|
776
|
+
* sources and with the same lifecycle (replaced on every sync, removed with the plugin). */
|
|
777
|
+
export const runtimeBinaryPath = (id: string, name: string): string => `Binaries/${id}/${name}.xcframework`
|
|
778
|
+
|
|
708
779
|
/** The SDK's engine variants — one tag of lecodes-ios-sdk per variant (`<version>` = full,
|
|
709
780
|
* `<version>-3d`, `-2d`, `-core`): core = UI only, 2d = + the 2D engine, 3d = + Filament with Jolt,
|
|
710
781
|
* full = everything. The Swift sources are the same; the engine binary differs. */
|
|
@@ -722,8 +793,9 @@ export const sdkProducts = (variant: SdkVariant): string[] =>
|
|
|
722
793
|
/** LeCodesRuntime/Package.swift — the CLI-owned dependency graph: the SDK (one tag per
|
|
723
794
|
* variant, pinned with `.exact(…)`: the variant is the engine binary the tag's manifest names,
|
|
724
795
|
* so SwiftPM must never drift on its own — moving the version is the CLI's job, `lecodes app
|
|
725
|
-
* update` or app.json ios.sdk) + one vendored target per installed plugin
|
|
726
|
-
* (
|
|
796
|
+
* update` or app.json ios.sdk) + one vendored target per installed plugin, + a binary target per
|
|
797
|
+
* xcframework a plugin carries (its copy under Binaries/<id>/). The SDK's assets (the 3D variants'
|
|
798
|
+
* shader archives) ride inside the package. */
|
|
727
799
|
export const runtimePackageTemplate = (sdkVersion: string, variant: SdkVariant, plugins: RuntimePlugin[], sdkPath?: string): string => {
|
|
728
800
|
const sdkDep = (name: string) => `.product(name: "${name}", package: "lecodes-ios-sdk")`
|
|
729
801
|
// LECODES_IOS_SDK_PATH: the SDK-development loop — a package tree on disk (release-ios-sdk.sh
|
|
@@ -746,18 +818,31 @@ export const runtimePackageTemplate = (sdkVersion: string, variant: SdkVariant,
|
|
|
746
818
|
}
|
|
747
819
|
const packageDecls = [...packages.values()].map(pkg =>
|
|
748
820
|
`\n .package(url: "${pkg.url}", ${pkg.exact ? `exact: "${pkg.exact}"` : `from: "${pkg.from}"`}),`).join("")
|
|
821
|
+
// The plugins' prebuilt xcframeworks (manifest ios.binaries): a binary target each, by its copy's
|
|
822
|
+
// path. A name is one target of the package — two plugins carrying one alike cannot both link.
|
|
823
|
+
const targetNames = new Map<string, string>([["LeCodesRuntime", "the runtime's own target"], ...plugins.map(p => [p.module, `the module of "${p.id}"`] as [string, string])])
|
|
824
|
+
for (const p of plugins) {
|
|
825
|
+
for (const name of p.binaries ?? []) {
|
|
826
|
+
const prior = targetNames.get(name)
|
|
827
|
+
if (prior) throw new Error(`Plugin "${p.id}" carries the binary ${name} and ${name} is ${prior} — one name is one target of the shell's package.`)
|
|
828
|
+
targetNames.set(name, `a binary of "${p.id}"`)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
749
831
|
const pluginTargets = plugins.map(p => {
|
|
750
832
|
const deps = [
|
|
751
833
|
sdkDep("LeCodes"),
|
|
752
834
|
...(p.packages ?? []).flatMap(pkg => pkg.products.map(name => `.product(name: "${name}", package: "${swiftPackageName(pkg.url)}")`)),
|
|
835
|
+
...(p.binaries ?? []).map(name => `"${name}"`),
|
|
753
836
|
]
|
|
837
|
+
const binaries = (p.binaries ?? []).map(name => `
|
|
838
|
+
.binaryTarget(name: "${name}", path: "${runtimeBinaryPath(p.id, name)}"),`).join("")
|
|
754
839
|
return `
|
|
755
840
|
// plugin: ${p.id} (vendored — see plugins.lock.json)
|
|
756
841
|
.target(
|
|
757
842
|
name: "${p.module}",
|
|
758
843
|
dependencies: [${deps.join(", ")}],
|
|
759
844
|
path: "Sources/${p.module}"
|
|
760
|
-
)
|
|
845
|
+
),${binaries}`
|
|
761
846
|
}).join("")
|
|
762
847
|
const runtimeDeps = [...sdkProducts(variant).map(sdkDep), ...plugins.map(p => `"${p.module}"`)]
|
|
763
848
|
return `// swift-tools-version:5.9
|
|
@@ -877,7 +962,7 @@ Yours — \`lecodes app sync\` never touches these:
|
|
|
877
962
|
≥1024px image and every sync renders AppIcon.png from it (flattened over \`"iconBackground"\`,
|
|
878
963
|
default white — App Store icons must be opaque). Or leave the key unset and drop a
|
|
879
964
|
1024×1024 PNG into AppIcon.appiconset/ yourself — sync never touches it then.
|
|
880
|
-
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
965
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\` — unless \`app.json\` sets \`"splash"\` (below)
|
|
881
966
|
|
|
882
967
|
CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
883
968
|
|
|
@@ -888,10 +973,18 @@ CLI-owned — regenerated on every sync, hand edits are overwritten:
|
|
|
888
973
|
- \`App/App.entitlements\` — the capabilities the installed plugins declare (e.g. push)
|
|
889
974
|
- \`LeCodesRuntime/\` — local Swift package: the lecodes-ios-sdk dependency (one tag per engine
|
|
890
975
|
variant), vendored plugin sources (Sources/<Module>/), and LeCodesAppViewController — the SDK's
|
|
891
|
-
shell controller with this project's plugins registered (App/ViewController.swift subclasses it)
|
|
976
|
+
shell controller with this project's plugins registered (App/ViewController.swift subclasses it).
|
|
977
|
+
A plugin's prebuilt xcframeworks (its manifest's \`ios.binaries\`) are copied to
|
|
978
|
+
Binaries/<plugin id>/: binary targets its vendored target links
|
|
892
979
|
- the version values inside \`App.xcodeproj/project.pbxproj\` (from app.json)
|
|
893
980
|
- this README
|
|
894
981
|
|
|
982
|
+
CLI-owned **only when** \`../app.json\` sets \`"splash"\` — regenerated on every sync then;
|
|
983
|
+
without the key sync never touches them (see "Splash screen"):
|
|
984
|
+
|
|
985
|
+
- \`App/Base.lproj/LaunchScreen.storyboard\`
|
|
986
|
+
- \`App/Assets.xcassets/LeCodesSplashBackground.colorset/\`, \`App/Assets.xcassets/LeCodesSplash.imageset/\`
|
|
987
|
+
|
|
895
988
|
## SDK variant
|
|
896
989
|
|
|
897
990
|
Sync pins the SDK tag of the smallest engine variant that covers what the compiled bundle actually
|
|
@@ -900,30 +993,48 @@ physics, ~10 MB heavier), or \`full\` (everything) — \`<version>-core\`, \`-2d
|
|
|
900
993
|
bare \`<version>\` for full. Adding your first 3D scene or 2D game to the project switches it
|
|
901
994
|
automatically on the next sync; set \`ios.variant\` in \`../app.json\` to pin one instead.
|
|
902
995
|
|
|
996
|
+
## Splash screen
|
|
997
|
+
|
|
998
|
+
\`"splash": { "background": "#RRGGBB", "image": "./splash.png", "size": 160 }\` in \`../app.json\`
|
|
999
|
+
(every key optional) makes every sync write the launch screen: a full-screen view of the colour
|
|
1000
|
+
(\`background\`, default \`"iconBackground"\`, default white) with the image fitted centred into a
|
|
1001
|
+
\`size\`×\`size\` pt square (160 by default) — never at its pixel size — from @1x/@2x/@3x renderings
|
|
1002
|
+
(size × 1/2/3 px: 160/320/480 at the default) of the one master. Use a square mark of at least
|
|
1003
|
+
3 × size px (a non-square one is fitted into the square, a smaller one is upscaled); the same key
|
|
1004
|
+
draws the Android splash, which crops the mark to a circle on Android 12+ — above a \`size\` of 192
|
|
1005
|
+
its corners are cut there (sync warns; 288 is the most it takes). No image = the colour alone. The app holds this screen until its first frame — through the
|
|
1006
|
+
first-launch wait of an OTA shell too — so nothing flashes between the two. iOS caches launch
|
|
1007
|
+
screens: after a change, delete the app from the device (or restart the device) to see the new one.
|
|
1008
|
+
|
|
903
1009
|
## Over-the-air updates
|
|
904
1010
|
|
|
905
1011
|
With \`"update": true\` in \`../app.json\` (server-backed projects; or
|
|
906
1012
|
\`"update": {"url": "https://…/app.js"}\` for any static hosting), the app checks that URL
|
|
907
1013
|
at launch — natively, before any JS runs, so a broken bundle can never block its own fix.
|
|
908
|
-
Unchanged code costs a ~0-byte 304
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1014
|
+
Unchanged code costs a ~0-byte 304. The FIRST launch of an installed build waits for the answer
|
|
1015
|
+
(5 s at most, the launch screen held on screen) and runs the newest bundle at once, so a fresh
|
|
1016
|
+
install from the App Store never starts on the older code it shipped with; every later launch
|
|
1017
|
+
runs the local bundle at once, new code downloads in the background and runs on the NEXT launch.
|
|
1018
|
+
Offline it's a silent no-op with no wait — the app always launches from a local bundle. An App
|
|
1019
|
+
Store update keeps a downloaded bundle only when it is newer code than the one the update ships
|
|
1020
|
+
(by compile date); otherwise the store build wins. JS only: adding/removing native plugins still
|
|
1021
|
+
needs a store release.
|
|
912
1022
|
|
|
913
1023
|
Shipping an update is just \`lecodes push --publish\` — no rebuild, no store review:
|
|
914
1024
|
running apps download the new code on their next launch and run it on the launch after.
|
|
915
1025
|
|
|
916
1026
|
Shells generated before OTA support have a \`ViewController.swift\` that reads app.js from
|
|
917
1027
|
\`Bundle.main\` directly, so downloaded updates would never run — and since \`App/*.swift\`
|
|
918
|
-
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this).
|
|
919
|
-
\`
|
|
1028
|
+
is yours, sync won't rewrite it (\`lecodes app doctor\` flags this). Make it subclass
|
|
1029
|
+
\`LeCodesAppViewController\` (the boot above, the first-launch wait included), or replace the
|
|
1030
|
+
\`Bundle.main\` load with (no wait):
|
|
920
1031
|
|
|
921
1032
|
\`\`\`swift
|
|
922
|
-
LeCodesUpdater.checkForUpdate()
|
|
923
1033
|
guard let code = LeCodesUpdater.activeBundle() else {
|
|
924
1034
|
print("LeCodes: bundled app.js is missing — run \`lecodes app sync\`")
|
|
925
1035
|
return
|
|
926
1036
|
}
|
|
1037
|
+
LeCodesUpdater.checkForUpdate()
|
|
927
1038
|
engine.run(code)
|
|
928
1039
|
\`\`\`
|
|
929
1040
|
|